DmitrMakeev commited on
Commit
fb374f9
·
verified ·
1 Parent(s): 8048f77

Update app.py

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