Initial commit: планирование эксперимента и оптимизация смесей

This commit is contained in:
2026-07-23 15:12:32 +05:00
commit 025421193d
17 changed files with 1634 additions and 0 deletions
+99
View File
@@ -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", []))