100 lines
4.0 KiB
Python
100 lines
4.0 KiB
Python
# 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", []))
|