Initial commit: планирование эксперимента и оптимизация смесей
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
# 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", []))
|
||||
Reference in New Issue
Block a user