DmitrMakeev commited on
Commit
24fe437
·
verified ·
1 Parent(s): 99d4fdf

Update app.py

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