Initial commit: планирование эксперимента и оптимизация смесей
This commit is contained in:
+183
@@ -0,0 +1,183 @@
|
||||
# core/classes.py
|
||||
"""
|
||||
Модуль содержит классы для представления ингредиентов и смеси.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
|
||||
class Ingredient:
|
||||
"""Представляет ингредиент с именем, единицей измерения и значением."""
|
||||
def __init__(self, name: str = "Water", unit: str = "ml", value: float = 1.0):
|
||||
self.name = name
|
||||
self.unit = unit
|
||||
self.value = value
|
||||
|
||||
def to_dict(self):
|
||||
return {"name": self.name, "unit": self.unit, "value": self.value}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data):
|
||||
return Ingredient(data["name"], data["unit"], data["value"])
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.name}: {self.value} {self.unit}"
|
||||
|
||||
@dataclass
|
||||
class IngredientInfo:
|
||||
"""
|
||||
Обёртка для ингредиента в составе смеси, добавляет разбавление и плотность.
|
||||
"""
|
||||
ingredient: Ingredient
|
||||
dilution: float = 1.0
|
||||
density: float = 1.0
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"ingredient": self.ingredient.to_dict(),
|
||||
"dilution": self.dilution,
|
||||
"density": self.density
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data):
|
||||
ing = Ingredient.from_dict(data["ingredient"])
|
||||
return IngredientInfo(
|
||||
ingredient=ing,
|
||||
dilution=data.get("dilution", 1.0),
|
||||
density=data.get("density", 1.0)
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.ingredient.name
|
||||
|
||||
@property
|
||||
def unit(self):
|
||||
return self.ingredient.unit
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.ingredient.value
|
||||
|
||||
@value.setter
|
||||
def value(self, new_value):
|
||||
self.ingredient.value = new_value
|
||||
|
||||
class Mixture:
|
||||
"""
|
||||
Основной класс для работы со смесью.
|
||||
Первый ингредиент считается растворителем (его процент пересчитывается автоматически).
|
||||
"""
|
||||
def __init__(self, solvent=None, ings=None, total_amount=1000.0, total_unit="ml"):
|
||||
all_ings = []
|
||||
if solvent is None:
|
||||
water = Ingredient(name="Water", unit=total_unit, value=100.0)
|
||||
solvent_info = IngredientInfo(ingredient=water)
|
||||
elif isinstance(solvent, Ingredient):
|
||||
solvent_info = IngredientInfo(ingredient=solvent)
|
||||
elif isinstance(solvent, IngredientInfo):
|
||||
solvent_info = solvent
|
||||
else:
|
||||
raise TypeError("solvent must be Ingredient or IngredientInfo")
|
||||
all_ings.append(solvent_info)
|
||||
|
||||
if ings is not None:
|
||||
if not isinstance(ings, list):
|
||||
raise TypeError("ings must be a list")
|
||||
for item in ings:
|
||||
if isinstance(item, Ingredient):
|
||||
all_ings.append(IngredientInfo(ingredient=item))
|
||||
elif isinstance(item, IngredientInfo):
|
||||
all_ings.append(item)
|
||||
else:
|
||||
raise TypeError("Each item in ings must be Ingredient or IngredientInfo")
|
||||
|
||||
self.ings = all_ings
|
||||
self.total_amount = total_amount
|
||||
self.total_unit = total_unit
|
||||
self.comp_solvent_percent()
|
||||
|
||||
def comp_solvent_percent(self) -> float:
|
||||
"""Пересчёт процента растворителя = 100% - сумма остальных."""
|
||||
if not self.ings:
|
||||
return 0.0
|
||||
total_others = sum(info.value for info in self.ings[1:])
|
||||
if total_others > 100.0:
|
||||
raise ValueError("Sum of all ingredient percentages exceeds 100%")
|
||||
self.ings[0].value = 100.0 - total_others
|
||||
return self.ings[0].value
|
||||
|
||||
def add_ingredient(self, ingredient: Ingredient, dilution: float = 1.0, density: float = 1.0):
|
||||
info = IngredientInfo(ingredient, dilution, density)
|
||||
self.ings.append(info)
|
||||
self.comp_solvent_percent()
|
||||
|
||||
def remove_ingredient(self, index: int):
|
||||
if index == 0:
|
||||
raise ValueError("Нельзя удалить растворитель (первый ингредиент).")
|
||||
removed = self.ings.pop(index)
|
||||
self.comp_solvent_percent()
|
||||
return removed
|
||||
|
||||
def get_absolute_ingredients(self):
|
||||
"""Возвращает абсолютные значения ингредиентов (с учётом плотности и разбавления)."""
|
||||
abs_list = []
|
||||
sum_rest = 0.0
|
||||
for info in self.ings[1:]:
|
||||
abs_value = (info.value / 100.0) * self.total_amount * info.dilution * info.density
|
||||
abs_list.append(Ingredient(name=info.name, unit=info.unit, value=abs_value))
|
||||
sum_rest += abs_value
|
||||
if sum_rest > self.total_amount:
|
||||
raise ValueError("Сумма ингредиентов (кроме растворителя) превышает общее количество")
|
||||
solvent_abs = self.total_amount - sum_rest
|
||||
solvent = Ingredient(name=self.ings[0].name, unit=self.ings[0].unit, value=solvent_abs)
|
||||
abs_list.insert(0, solvent)
|
||||
return abs_list
|
||||
|
||||
def get_ingredient_info(self, index: int):
|
||||
return self.ings[index]
|
||||
|
||||
def set_total_amount(self, new_amount: float, new_unit: str):
|
||||
if new_amount <= 0:
|
||||
raise ValueError("Общее количество должно быть положительным.")
|
||||
if not new_unit.strip():
|
||||
raise ValueError("Единица измерения не может быть пустой.")
|
||||
self.total_amount = new_amount
|
||||
self.total_unit = new_unit.strip()
|
||||
|
||||
def set_composition(self, *percentages):
|
||||
expected = len(self.ings) - 1
|
||||
if len(percentages) != expected:
|
||||
raise ValueError(f"Expected {expected} percentages, got {len(percentages)}")
|
||||
total = sum(percentages)
|
||||
if total > 100.0:
|
||||
raise ValueError(f"Sum of percentages ({total:.2f}) exceeds 100")
|
||||
for i, pct in enumerate(percentages, start=1):
|
||||
if pct < 0:
|
||||
raise ValueError(f"Percentage for {self.ings[i].name} cannot be negative")
|
||||
self.ings[i].value = pct
|
||||
self.comp_solvent_percent()
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"ings": [info.to_dict() for info in self.ings],
|
||||
"total_amount": self.total_amount,
|
||||
"total_unit": self.total_unit
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data):
|
||||
ings = [IngredientInfo.from_dict(item) for item in data["ings"]]
|
||||
solvent = ings[0] if ings else None
|
||||
rest = ings[1:] if len(ings) > 1 else None
|
||||
return Mixture(
|
||||
solvent=solvent,
|
||||
ings=rest,
|
||||
total_amount=data["total_amount"],
|
||||
total_unit=data["total_unit"]
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
parts = []
|
||||
for info in self.ings:
|
||||
parts.append(f"{info.name} {info.value:.1f}% (разб.{info.dilution:.2f}, плотн.{info.density:.2f})")
|
||||
return f"Mixture({', '.join(parts)}, total={self.total_amount} {self.total_unit})"
|
||||
Reference in New Issue
Block a user