DmitrMakeev commited on
Commit
683a6e7
·
verified ·
1 Parent(s): ddb54ce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +197 -275
app.py CHANGED
@@ -692,315 +692,237 @@ def nutri_call():
692
 
693
 
694
 
695
- # Список всех питательных веществ
696
- nutrients_stencil = [
697
- 'N (NO3-)', 'N (NH4+)', 'P', 'K', 'Mg', 'Ca', 'S',
698
- 'Fe', 'Zn', 'B', 'Mn', 'Cu', 'Mo'
699
- ]
700
-
701
- # Класс для работы с составами удобрений
702
- class Composition:
703
- def __init__(self, name='', vector=None):
704
- self.name = name
705
- if vector is None:
706
- self.vector = np.zeros(len(nutrients_stencil))
707
- else:
708
- if len(vector) != len(nutrients_stencil):
709
- raise ValueError(f"Vector length ({len(vector)}) does not match "
710
- f"nutrients stencil length ({len(nutrients_stencil)}).")
711
- self.vector = np.array(vector)
712
-
713
- @classmethod
714
- def from_dict(cls, composition_dict):
715
- if not composition_dict:
716
- raise ValueError("Empty composition dictionary provided.")
717
- name, nutrients_dict = tuple(composition_dict.items())[0]
718
- vector = np.zeros(len(nutrients_stencil))
719
- for i, nutrient in enumerate(nutrients_stencil):
720
- if nutrient in nutrients_dict:
721
- vector[i] = nutrients_dict[nutrient]
722
- return cls(name, vector)
723
-
724
- def __add__(self, composition):
725
- if not isinstance(composition, Composition):
726
- raise TypeError("Can only add Composition objects.")
727
- name = f'{self.name} + {composition.name}'
728
- vector = self.vector + composition.vector
729
- return Composition(name, vector)
730
-
731
- def __neg__(self):
732
- return Composition(f'- ({self.name})', -self.vector)
733
-
734
- def __sub__(self, composition):
735
- if not isinstance(composition, Composition):
736
- raise TypeError("Can only subtract Composition objects.")
737
- name = f'{self.name} - {composition.name}'
738
- vector = self.vector - composition.vector
739
- return Composition(name, vector)
740
-
741
- def __eq__(self, other):
742
- if not isinstance(other, Composition):
743
- return False
744
- return np.all(self.vector == other.vector)
745
-
746
- def __repr__(self):
747
- return self.table()
748
-
749
- def __str__(self):
750
- return f'{self.name} :: ' + ', '.join(
751
- f'{nutrient}: {amount_ppm:.2f}'
752
- for nutrient, amount_ppm
753
- in zip(nutrients_stencil, 10**6 * self.vector)
754
- )
755
-
756
- def __rmul__(self, number):
757
- if not isinstance(number, (int, float)):
758
- raise TypeError("Can only multiply by a scalar.")
759
- name = f'{number} * ({self.name})'
760
- vector = self.vector * number
761
- return Composition(name, vector)
762
-
763
- def __len__(self):
764
- return len(self.vector)
765
-
766
- def as_dict(self):
767
- nutrients_dict = {
768
- nutrient: float(value)
769
- for nutrient, value in zip(nutrients_stencil, self.vector)
770
- if value != 0
771
- }
772
- return {self.name: nutrients_dict}
773
-
774
- def table(self, sparse=True, ref=None, tablefmt='simple'):
775
- description = f'Composition: {self.name}'
776
- nutrients = np.array(nutrients_stencil)
777
- vector = self.vector
778
- if ref is not None:
779
- if not isinstance(ref, Composition):
780
- raise TypeError("Reference must be a Composition object.")
781
- vector_ref = ref.vector
782
- else:
783
- vector_ref = np.zeros(len(nutrients_stencil))
784
- if sparse:
785
- mask_nonzero = (vector != 0) | (vector_ref != 0)
786
- nutrients = nutrients[mask_nonzero]
787
- vector = vector[mask_nonzero]
788
- vector_ref = vector_ref[mask_nonzero]
789
- table_dict = {
790
- 'Nutrient': nutrients,
791
- 'Ratio': vector,
792
- 'Amount mg/kg': 10**6 * vector,
793
- }
794
- if ref is not None:
795
- description += f'\nReference: {ref.name}'
796
- table_dict['Diff mg/kg'] = 10**6 * (vector - vector_ref)
797
- table = tabulate(table_dict, headers='keys', tablefmt=tablefmt)
798
- return '\n\n'.join((description, table))
799
 
800
 
801
- # Основной класс калькулятора удобрений
802
- class NutrientCalculator:
803
- def __init__(self, volume_liters=1.0):
804
- self.volume = volume_liters
805
 
806
- # Целевой профиль (дополнен нулями для отсутствующих элементов)
807
- self.target_profile = {nutrient: BASE_PROFILE.get(nutrient, 0.0) for nutrient in nutrients_stencil}
 
 
 
808
 
809
- # Удобрения (дополнены нулями для отсутствующих элементов)
810
- self.fertilizers = {
811
- name: Composition.from_dict({name: {nutrient: content.get(nutrient, 0.0) for nutrient in nutrients_stencil}})
812
- for name, content in NUTRIENT_CONTENT_IN_FERTILIZERS.items()
813
- }
 
 
 
 
814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
  self.total_ec = 0.0
816
- self.best_solution = None
817
- self.min_difference = float('inf')
818
- self.max_recursion_depth = 1000
819
- self.current_depth = 0
820
 
821
  # Расчёт азота
822
  total_parts = NO3_RATIO + NH4_RATIO
823
  self.target_profile['N (NO3-)'] = TOTAL_NITROGEN * (NO3_RATIO / total_parts)
824
  self.target_profile['N (NH4+)'] = TOTAL_NITROGEN * (NH4_RATIO / total_parts)
825
 
826
- # Целевой профиль как объект Composition
827
- self.target_composition = Composition('Target Profile', list(self.target_profile.values()))
 
 
 
828
 
829
- def _copy_results(self):
830
- """Создаёт глубокую копию результатов"""
831
- return {
832
- fert_name: {
833
- 'граммы': data['граммы'],
834
- 'миллиграммы': data['миллиграммы'],
835
- 'вклад в EC': data['вклад в EC']
836
- }
837
- for fert_name, data in self.results.items()
 
 
 
 
 
 
 
838
  }
 
839
 
840
  def calculate(self):
841
  try:
842
- self.actual_profile = {k: 0.0 for k in self.target_profile}
843
- self.results = {}
844
- self.current_depth = 0
845
-
846
- if self._backtrack_search():
847
- print("Оптимальная комбинация найдена!")
848
- return self.best_solution
849
- else:
850
- print("Идеальное решение не найдено. Возвращаю лучшее найденное решение.")
851
- return self.best_solution or {"error": "Не удалось найти подходящую комбинацию"}
852
 
 
 
 
 
 
 
 
853
  except Exception as e:
854
  print(f"Ошибка при расчёте: {str(e)}")
855
  raise
856
 
857
- def _backtrack_search(self, fertilizer_index=0, step=1.0):
858
- self.current_depth += 1
859
- if self.current_depth > self.max_recursion_depth:
860
- return False
861
-
862
- # Текущий профиль как объект Composition
863
- current_composition = Composition('Current Profile', list(self.actual_profile.values()))
864
- current_diff = self._calculate_difference(current_composition)
865
-
866
- if current_diff < self.min_difference:
867
- self.min_difference = current_diff
868
- self.best_solution = {
869
- "results": self._copy_results(),
870
- "actual_profile": self.actual_profile.copy(),
871
- "total_ec": self.total_ec,
872
- "difference": current_diff
873
- }
874
-
875
- if current_diff < 1.0: # Допустимая погрешность
876
- return True
 
 
 
 
 
 
 
 
 
 
 
 
877
 
878
- # Пробуем добавлять удобрения с текущего индекса
879
- for i in range(fertilizer_index, len(self.fertilizers)):
880
- fert_name = list(self.fertilizers.keys())[i]
881
- fert_composition = self.fertilizers[fert_name]
882
-
883
- # Проверяем, можно ли применить удобрение
884
- if not self._can_apply_fertilizer(fert_composition):
885
- continue
886
-
887
- # Пробуем добавить удобрение с текущим шагом
888
- self._apply_fertilizer(fert_name, step)
889
 
890
- # Рекурсивно продолжаем поиск
891
- if self._backtrack_search(i, step):
892
- return True
 
 
 
 
 
 
893
 
894
- # Если не получилось - откатываемся
895
- self._remove_fertilizer(fert_name, step)
896
 
897
- # Пробуем уменьшить шаг для более точного поиска
898
- if step > 0.1 and current_diff > 5.0:
899
- if self._backtrack_search(i, step / 2):
900
- return True
901
-
902
- return False
903
-
904
- def _can_apply_fertilizer(self, fert_composition):
905
- """Проверяет, ��ожно ли применить удобрение без перебора"""
906
- for element, content in zip(nutrients_stencil, fert_composition.vector):
907
- added_ppm = (1 * content * 1000) / self.volume
908
- if self.actual_profile[element] + added_ppm > self.target_profile[element] + 1.0:
909
- return False
910
- return True
911
-
912
- def _apply_fertilizer(self, fert_name, amount):
913
- """Добавляет указанное количество удобрения"""
914
- fert_composition = self.fertilizers[fert_name]
915
- scaled_composition = amount * fert_composition
916
-
917
- if fert_name not in self.results:
918
- self.results[fert_name] = {
919
- 'граммы': 0.0,
920
- 'миллиграммы': 0,
921
- 'вклад в EC': 0.0
922
- }
923
-
924
- self.results[fert_name]['граммы'] += amount
925
- self.results[fert_name]['миллиграммы'] += int(amount * 1000)
926
-
927
- for i, nutrient in enumerate(nutrients_stencil):
928
- added_ppm = scaled_composition.vector[i] * 1000 / self.volume
929
- self.actual_profile[nutrient] += added_ppm
930
-
931
- def _remove_fertilizer(self, fert_name, amount):
932
- """Удаляет указанное количество удобрения"""
933
- fert_composition = self.fertilizers[fert_name]
934
- scaled_composition = amount * fert_composition
935
-
936
- if fert_name in self.results:
937
- self.results[fert_name]['граммы'] -= amount
938
- self.results[fert_name]['миллиграммы'] -= int(amount * 1000)
939
-
940
- for i, nutrient in enumerate(nutrients_stencil):
941
- removed_ppm = scaled_composition.vector[i] * 1000 / self.volume
942
- self.actual_profile[nutrient] -= removed_ppm
943
 
944
- if self.results[fert_name]['граммы'] <= 0.001:
945
- del self.results[fert_name]
 
 
 
 
 
946
 
947
- def _calculate_difference(self, current_composition):
948
- """Вычисляет общее отклонение от целевого профиля"""
949
- diff_vector = self.target_composition.vector - current_composition.vector
950
- return np.sum(np.abs(diff_vector))
951
 
952
- def generate_report(self):
953
- """Генерация отчета о питательном растворе"""
954
  try:
955
- actual_composition = Composition('Actual Profile', list(self.actual_profile.values()))
956
- report = actual_composition.table(sparse=True, ref=self.target_composition)
957
- return report
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
958
  except Exception as e:
959
  print(f"Ошибка при выводе отчёта: {str(e)}")
960
  raise
961
 
962
-
963
- # Пример данных
964
- BASE_PROFILE = {
965
- 'N (NO3-)': 106.7,
966
- 'N (NH4+)': 13.3,
967
- 'P': 50,
968
- 'K': 210,
969
- 'Mg': 120,
970
- 'Ca': 150,
971
- 'S': 50
972
- }
973
-
974
- NUTRIENT_CONTENT_IN_FERTILIZERS = {
975
- 'Кальциевая селитра': {
976
- 'N (NO3-)': 13.0,
977
- 'Ca': 17.0
978
- },
979
- 'Калий сернокислый': {
980
- 'K': 44.0,
981
- 'S': 18.0
982
- },
983
- 'Магний сернокислый': {
984
- 'Mg': 9.8,
985
- 'S': 13.0
986
- }
987
- }
988
-
989
- TOTAL_NITROGEN = 120.0
990
- NO3_RATIO = 90.0
991
- NH4_RATIO = 10.0
992
-
993
- # Создание калькулятора
994
- calculator = NutrientCalculator(volume_liters=100)
995
-
996
- # Расчет
997
- solution = calculator.calculate()
998
-
999
- # Генерация отчета
1000
- if solution:
1001
- print(calculator.generate_report())
1002
- else:
1003
- print("Решение не найдено.")
1004
 
1005
 
1006
 
 
692
 
693
 
694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
 
696
 
697
+ from tabulate import tabulate
 
 
 
698
 
699
+ # Глобальные параметры
700
+ TOTAL_NITROGEN = 120.0 # Общее количество азота
701
+ NO3_RATIO = 8.0 # Соотношение NO3:NH4
702
+ NH4_RATIO = 1.00 # Соотношение NH4:NO3
703
+ VOLUME_LITERS = 100 # Объем раствора
704
 
705
+ BASE_PROFILE = {
706
+ "P": 50, # Фосфор
707
+ "K": 300, # Калий
708
+ "Mg": 120, # Магний (высокий уровень)
709
+ "Ca": 150, # Кальций
710
+ "S": 100, # Сера
711
+ "N (NO3-)": 0, # Рассчитывается автоматически
712
+ "N (NH4+)": 0 # Рассчитывается автоматически
713
+ }
714
 
715
+ NUTRIENT_CONTENT_IN_FERTILIZERS = {
716
+ "Кальциевая селитра": {"N (NO3-)": 0.11863, "Ca": 0.16972},
717
+ "Калий азотнокислый": {"N (NO3-)": 0.136, "K": 0.382},
718
+ "Калий сернокислый": {"K": 0.44874, "S": 0.18401},
719
+ "Аммоний азотнокислый": {"N (NO3-)": 0.17499, "N (NH4+)": 0.17499},
720
+ "Сульфат магния": {"Mg": 0.09861, "S": 0.13010},
721
+ "Монофосфат калия": {"P": 0.218, "K": 0.275},
722
+ "Сульфат кальция": {"Ca": 0.23, "S": 0.186}
723
+ }
724
+
725
+ EC_COEFFICIENTS = {
726
+ 'P': 0.0012, 'K': 0.0018, 'Mg': 0.0015,
727
+ 'Ca': 0.0016, 'S': 0.0014,
728
+ 'N (NO3-)': 0.0017, 'N (NH4+)': 0.0019
729
+ }
730
+
731
+ class NutrientCalculator:
732
+ def __init__(self, volume_liters=1.0, profile=BASE_PROFILE):
733
+ self.volume = volume_liters
734
+ self.results = {}
735
+ self.target_profile = profile.copy()
736
+ self.actual_profile = {k: 0.0 for k in self.target_profile}
737
+ self.fertilizers = NUTRIENT_CONTENT_IN_FERTILIZERS
738
  self.total_ec = 0.0
 
 
 
 
739
 
740
  # Расчёт азота
741
  total_parts = NO3_RATIO + NH4_RATIO
742
  self.target_profile['N (NO3-)'] = TOTAL_NITROGEN * (NO3_RATIO / total_parts)
743
  self.target_profile['N (NH4+)'] = TOTAL_NITROGEN * (NH4_RATIO / total_parts)
744
 
745
+ # Сохраняем исходный профиль азота
746
+ self.initial_n_profile = {
747
+ "NO3-": self.target_profile['N (NO3-)'],
748
+ "NH4+": self.target_profile['N (NH4+)']
749
+ }
750
 
751
+ # Веса компенсации
752
+ self.compensation_weights = {
753
+ "Ca": {"weight": 0.3, "fert": "Сульфат кальция", "main_element": "Ca"},
754
+ "K": {"weight": 0.2, "fert": "Калий азотнокислый", "main_element": "K"},
755
+ "Mg": {"weight": 0.2, "fert": "Сульфат магния", "main_element": "Mg"},
756
+ "P": {"weight": 0.1, "fert": "Монофосфат калия", "main_element": "P"},
757
+ "S": {"weight": 0.1, "fert": "Калий сернокислый", "main_element": "S"},
758
+ "N (NO3-)": {"weight": 0.05, "fert": "Калий азотнокислый", "main_element": "N (NO3-)"},
759
+ "N (NH4+)": {"weight": 0.05, "fert": "Аммоний азотнокислый", "main_element": "N (NH4+)"}
760
+ }
761
+
762
+ def _label(self, element):
763
+ """Форматирование названий элементов для вывода"""
764
+ labels = {
765
+ 'N (NO3-)': 'NO3',
766
+ 'N (NH4+)': 'NH4'
767
  }
768
+ return labels.get(element, element)
769
 
770
  def calculate(self):
771
  try:
772
+ # Первый проход: компенсация основных элементов
773
+ self._compensate_main_elements()
774
+
775
+ # Второй проход: компенсация азота
776
+ self._compensate_nitrogen()
 
 
 
 
 
777
 
778
+ # Третий проход: компенсация второстепенных элементов
779
+ self._compensate_secondary_elements()
780
+
781
+ # Четвертый проход: корректировка перебора
782
+ self._adjust_overages()
783
+
784
+ return self.results
785
  except Exception as e:
786
  print(f"Ошибка при расчёте: {str(e)}")
787
  raise
788
 
789
+ def _compensate_main_elements(self):
790
+ """Компенсация основных элементов (Ca, Mg, P)"""
791
+ for element, weight_data in self.compensation_weights.items():
792
+ if element in ["Ca", "Mg", "P"]:
793
+ fert_name = weight_data["fert"]
794
+ main_element = weight_data["main_element"]
795
+ required_ppm = self.target_profile[main_element] - self.actual_profile[main_element]
796
+ if required_ppm > 0.1:
797
+ self._apply_with_limit(fert_name, main_element, required_ppm)
798
+
799
+ def _compensate_nitrogen(self):
800
+ """Компенсация азота (NO3-, NH4+)"""
801
+ for nitrogen_type in ["N (NO3-)", "N (NH4+)"]:
802
+ required_ppm = self.target_profile[nitrogen_type] - self.actual_profile[nitrogen_type]
803
+ if required_ppm > 0.1:
804
+ fert_name = self.compensation_weights[nitrogen_type]["fert"]
805
+ self._apply_with_limit(fert_name, nitrogen_type, required_ppm)
806
+
807
+ def _compensate_secondary_elements(self):
808
+ """Компенсация второстепенных элементов (K, S)"""
809
+ for element, weight_data in self.compensation_weights.items():
810
+ if element in ["K", "S"]:
811
+ fert_name = weight_data["fert"]
812
+ main_element = weight_data["main_element"]
813
+ required_ppm = self.target_profile[main_element] - self.actual_profile[main_element]
814
+ if required_ppm > 0.1:
815
+ self._apply_with_limit(fert_name, main_element, required_ppm)
816
+
817
+ def _apply_with_limit(self, fert_name, main_element, required_ppm):
818
+ """Применение удобрения с ограничением по перебору"""
819
+ if required_ppm <= 0:
820
+ return
821
 
822
+ try:
823
+ content = self.fertilizers[fert_name][main_element]
824
+ max_allowed_ppm = self.target_profile[main_element] - self.actual_profile[main_element]
825
+ grams = min((required_ppm * self.volume) / (content * 1000), (max_allowed_ppm * self.volume) / (content * 1000))
 
 
 
 
 
 
 
826
 
827
+ if fert_name not in self.results:
828
+ result = {
829
+ 'граммы': 0.0,
830
+ 'миллиграммы': 0,
831
+ 'вклад в EC': 0.0
832
+ }
833
+ for element in self.fertilizers[fert_name]:
834
+ result[f'внесет {self._label(element)}'] = 0.0
835
+ self.results[fert_name] = result
836
 
837
+ self.results[fert_name]['граммы'] += grams
838
+ self.results[fert_name]['миллиграммы'] += int(grams * 1000)
839
 
840
+ fert_ec = 0.0
841
+ for element, percent in self.fertilizers[fert_name].items():
842
+ added_ppm = (grams * percent * 1000) / self.volume
843
+ self.results[fert_name][f'внесет {self._label(element)}'] += added_ppm
844
+ self.actual_profile[element] += added_ppm
845
+ fert_ec += added_ppm * EC_COEFFICIENTS.get(element, 0.0015)
846
+
847
+ self.results[fert_name]['вклад в EC'] += fert_ec
848
+ self.total_ec += fert_ec
849
+ except KeyError as e:
850
+ print(f"Ошибка: отсутствует элемент {str(e)} в удобрении {fert_name}")
851
+ raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
 
853
+ def _adjust_overages(self):
854
+ """Корректировка перебора элементов"""
855
+ for element in self.actual_profile:
856
+ if self.actual_profile[element] > self.target_profile[element]:
857
+ overage = self.actual_profile[element] - self.target_profile[element]
858
+ self.actual_profile[element] -= overage
859
+ print(f"Корректировка перебора: {element} уменьшен на {overage:.2f} ppm")
860
 
861
+ def calculate_ec(self):
862
+ return round(self.total_ec, 2)
 
 
863
 
864
+ def print_initial_nitrogen_report(self):
 
865
  try:
866
+ print("Исходный расчёт азота:")
867
+ print(f" NO3-: {self.initial_n_profile['NO3-']} ppm")
868
+ print(f" NH4+: {self.initial_n_profile['NH4+']} ppm")
869
+ except Exception as e:
870
+ print(f"Ошибка при выводе отчёта: {str(e)}")
871
+ raise
872
+ def print_report(self):
873
+ try:
874
+ print("\n" + "="*60)
875
+ print("ПРОФИЛЬ ПИТАТЕЛЬНОГО РАСТВОРА (ИТОГО):")
876
+ print("="*60)
877
+ table = [[el, round(self.actual_profile[el], 1)] for el in self.actual_profile]
878
+ print(tabulate(table, headers=["Элемент", "ppm"]))
879
+
880
+ print("\nИсходный расчёт азота:")
881
+ for form, val in self.initial_n_profile.items():
882
+ print(f" {form}: {round(val, 1)} ppm")
883
+
884
+ print("\n" + "="*60)
885
+ print(f"РАСЧЕТ ДЛЯ {self.volume} ЛИТРОВ РАСТВОРА")
886
+ print("="*60)
887
+ print(f"Общая концентрация: {round(sum(self.actual_profile.values()), 1)} ppm")
888
+ print(f"EC: {self.calculate_ec()} mS/cm")
889
+
890
+ print("\nРЕКОМЕНДУЕМЫЕ УДОБРЕНИЯ:")
891
+ fert_table = []
892
+ for fert, data in self.results.items():
893
+ adds = [f"+{k}: {v:.1f} ppm" for k, v in data.items() if k.startswith('внесет')]
894
+ fert_table.append([
895
+ fert,
896
+ round(data['граммы'], 3),
897
+ data['миллиграммы'],
898
+ round(data['вклад в EC'], 3),
899
+ "\n".join(adds)
900
+ ])
901
+ print(tabulate(fert_table,
902
+ headers=["Удобрение", "Граммы", "Миллиграммы", "EC (мСм/см)", "Добавит"]))
903
+
904
+ print("\nОСТАТОЧНЫЙ ДЕФИЦИТ:")
905
+ deficit = {
906
+ k: round(self.target_profile[k] - self.actual_profile[k], 1)
907
+ for k in self.target_profile
908
+ if abs(self.target_profile[k] - self.actual_profile[k]) > 0.1
909
+ }
910
+ if deficit:
911
+ for el, val in deficit.items():
912
+ print(f" {el}: {val} ppm")
913
+ else:
914
+ print(" Все элементы покрыты полностью")
915
  except Exception as e:
916
  print(f"Ошибка при выводе отчёта: {str(e)}")
917
  raise
918
 
919
+ if __name__ == "__main__":
920
+ try:
921
+ calculator = NutrientCalculator(volume_liters=VOLUME_LITERS)
922
+ calculator.calculate()
923
+ calculator.print_report() # Правильный вызов метода класса
924
+ except Exception as e:
925
+ print(f"Критическая ошибка: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
 
927
 
928