# core/experiment.py """ Класс ExperimentDesign для хранения шагов и матрицы плана, а также генерации экспериментальных смесей. """ from core.classes import Mixture, Ingredient, IngredientInfo class ExperimentDesign: def __init__(self, steps=None, plan=None): self.steps = steps if steps is not None else [] self.plan = plan if plan is not None else [] def set_steps(self, steps): """ Устанавливает шаги для всех факторов (ингредиентов кроме растворителя). Каждый шаг должен быть >= 0. Нулевые шаги означают, что данный ингредиент не является фактором. """ for s in steps: if s < 0: raise ValueError("Step cannot be negative") self.steps = list(steps) def _active_indices(self): """Возвращает список индексов (в self.steps) с шагом > 0.""" return [i for i, s in enumerate(self.steps) if s > 0] def _active_steps(self): """Возвращает список шагов > 0.""" return [s for s in self.steps if s > 0] def generate_plan(self, k=None): """ Генерирует полный факторный план 2^k, где k - число активных факторов (шагов > 0). Если k не указано, вычисляется автоматически. Если активных факторов < 2, выбрасывает ValueError. """ active = self._active_steps() if k is None: k = len(active) if k < 2: raise ValueError(f"Need at least 2 active factors (steps > 0), but got {k}") from core.doe import ffe self.plan = ffe(k) def get_experiment_mixture(self, mixture, exp_index): """ Возвращает Mixture для опыта с индексом exp_index (0-based). Изменяются только те ингредиенты, у которых шаг > 0. """ if not self.plan or not self.steps: raise ValueError("Plan or steps not set") if exp_index >= len(self.plan): raise IndexError("Experiment index out of range") active_indices = self._active_indices() active_steps = self._active_steps() if len(active_indices) != len(self.plan[0]): raise ValueError("Number of active steps does not match plan columns") base_percents = [info.value for info in mixture.ings] new_percents = base_percents.copy() for col, idx_in_steps in enumerate(active_indices): factor = self.plan[exp_index][col] ingredient_idx = idx_in_steps + 1 delta = active_steps[col] * factor new_percents[ingredient_idx] = base_percents[ingredient_idx] + delta new_ings = [] for i, info in enumerate(mixture.ings): new_ings.append( IngredientInfo( ingredient=Ingredient(name=info.name, unit=info.unit, value=new_percents[i]), dilution=info.dilution, density=info.density ) ) temp_mixture = Mixture( solvent=new_ings[0], ings=new_ings[1:], total_amount=mixture.total_amount, total_unit=mixture.total_unit ) return temp_mixture def get_all_mixtures(self, mixture): """Возвращает список Mixture для всех опытов.""" return [self.get_experiment_mixture(mixture, i) for i in range(len(self.plan))] def to_dict(self): return { "steps": self.steps, "plan": self.plan } @classmethod def from_dict(cls, data): return cls(steps=data.get("steps", []), plan=data.get("plan", []))