289 lines
12 KiB
Python
289 lines
12 KiB
Python
"""
|
|
Планирование эксперимента (DoE)
|
|
|
|
Модуль для генерации полнофакторных планов экспериментов
|
|
и анализа результатов.
|
|
|
|
Основные функции:
|
|
- generate_factorial_design() - генерация плана
|
|
- analyze_experiment() - статистический анализ
|
|
- calculate_factor_levels() - расчёт уровней факторов
|
|
"""
|
|
|
|
from typing import List, Dict, Tuple, Optional
|
|
import random
|
|
import numpy as np
|
|
from calculations.medium import (
|
|
calculate_medium_composition,
|
|
convert_units,
|
|
VOLUME_UNITS,
|
|
MASS_UNITS
|
|
)
|
|
from calculations.models.project_data import FactorData
|
|
# Типы расчёта шага
|
|
FACTOR_TYPES = {
|
|
'absolute': 'ед.', # абсолютный шаг
|
|
'relative': '%', # относительный шаг (процент от нулевого уровня)
|
|
}
|
|
|
|
|
|
def calculate_factor_levels(
|
|
factor: 'FactorData',
|
|
total_volume: float = 1000,
|
|
volume_unit: str = "мл"
|
|
) -> 'FactorData':
|
|
"""Рассчитывает high/low уровни фактора через калькулятор сред"""
|
|
|
|
# Функция для расчёта количества реагента при заданном проценте
|
|
def calc_amount(percentage: float) -> float:
|
|
reagents = [{
|
|
'name': factor.name,
|
|
'percentage': max(0, percentage),
|
|
'unit': factor.unit,
|
|
'dilution_factor': factor.dilution_factor or 1.0
|
|
}]
|
|
result = calculate_medium_composition(total_volume, volume_unit, reagents)
|
|
return result['reagents'][0]['calculated_amount']
|
|
|
|
|
|
# Определяем проценты для верхнего и нижнего уровней
|
|
if factor.step_type == "%":
|
|
low = calc_amount(factor.percentage - factor.step) # low - нижний уровень
|
|
high = calc_amount(factor.percentage + factor.step) # high - верхний уровень
|
|
else: # "ед."
|
|
# Конвертируем абсолютный шаг в проценты
|
|
low = calc_amount(factor.percentage) - factor.step
|
|
high = calc_amount(factor.center) + factor.step
|
|
|
|
# ВОЗВРАЩАЕМ НОВЫЙ ОБЪЕКТ FactorData
|
|
return FactorData(
|
|
name=factor.name,
|
|
center=factor.center,
|
|
low=low, # low - нижний уровень
|
|
high=high, # high - верхний уровень
|
|
step=factor.step,
|
|
step_type=factor.step_type,
|
|
unit=factor.unit,
|
|
percentage=factor.percentage or factor.center,
|
|
dilution_factor=factor.dilution_factor
|
|
)
|
|
|
|
def calculate_all_factors_levels(factors: List['FactorData'], **kwargs) -> List['FactorData']:
|
|
"""Рассчитывает уровни для всех факторов"""
|
|
return [calculate_factor_levels(f, **kwargs) for f in factors]
|
|
|
|
def get_active_factors(factors):
|
|
return [f for f in factors if f['step'] != 0]
|
|
|
|
def get_inactive_factors(factors):
|
|
return [f for f in factors if f['step'] == 0]
|
|
|
|
def generate_factorial_design(
|
|
factors: List[Dict],
|
|
center_points: int = 3,
|
|
randomize: bool = True
|
|
) -> List[Dict]:
|
|
"""
|
|
Генерирует полнофакторный план 2^k с правильным порядком изменения факторов:
|
|
- фактор 1 меняется через 1 эксперимент (2^0)
|
|
- фактор 2 меняется через 2 эксперимента (2^1)
|
|
- фактор 3 меняется через 4 эксперимента (2^2)
|
|
- и т.д.
|
|
"""
|
|
|
|
k = len(factors)
|
|
if k == 0:
|
|
return []
|
|
|
|
n_factorial = 2 ** k
|
|
design = []
|
|
|
|
# Генерация факторных точек в правильном порядке (двоичный счётчик)
|
|
for i in range(n_factorial):
|
|
experiment = {}
|
|
for j in range(k):
|
|
# Правильный порядок битов: младший бит - первый фактор
|
|
# (j) - для прямого порядка: фактор 1 меняется чаще всего
|
|
coded_level = -1 if (i >> j) & 1 == 0 else 1
|
|
|
|
natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high']
|
|
|
|
experiment[f"Фактор_{j+1}"] = {
|
|
'coded': coded_level,
|
|
'natural': natural_value,
|
|
'name': factors[j]['name'],
|
|
'unit': factors[j].get('unit', '')
|
|
}
|
|
design.append(experiment)
|
|
|
|
# Добавление центральных точек
|
|
for i in range(center_points):
|
|
center_experiment = {}
|
|
for j in range(k):
|
|
center_experiment[f"Фактор_{j+1}"] = {
|
|
'coded': 0,
|
|
'natural': factors[j]['center'],
|
|
'name': factors[j]['name'],
|
|
'unit': factors[j].get('unit', '')
|
|
}
|
|
center_experiment['is_center'] = True
|
|
center_experiment['center_num'] = i + 1
|
|
design.append(center_experiment)
|
|
|
|
# Перемешивание порядка (опционально)
|
|
if randomize:
|
|
random.shuffle(design)
|
|
|
|
return design
|
|
|
|
|
|
def analyze_experiment(
|
|
results: List[List[float]],
|
|
design: List[Dict],
|
|
responses: List[Dict]
|
|
) -> Dict:
|
|
"""
|
|
ПРОВОДИТ СТАТИСТИЧЕСКИЙ АНАЛИЗ РЕЗУЛЬТАТОВ ЭКСПЕРИМЕНТА
|
|
|
|
ПАРАМЕТРЫ:
|
|
----------
|
|
results : List[List[float]]
|
|
Матрица результатов. Каждая строка - эксперимент,
|
|
каждый столбец - отклик.
|
|
Размер: [n_experiments, n_responses]
|
|
|
|
design : List[Dict]
|
|
План эксперимента, возвращённый generate_factorial_design()
|
|
|
|
responses : List[Dict]
|
|
Список откликов. Каждый отклик - словарь с ключами:
|
|
- name (str): название отклика
|
|
- unit (str): единица измерения
|
|
|
|
ВОЗВРАЩАЕТ:
|
|
-----------
|
|
Dict
|
|
Словарь с анализом для каждого отклика:
|
|
{
|
|
'Название отклика': {
|
|
'mean': float, # среднее значение всех опытов
|
|
'variance': float, # общая дисперсия
|
|
'std_dev': float, # стандартное отклонение
|
|
'cv': float, # коэффициент вариации (%)
|
|
'factorial_values': list, # значения в факторных точках
|
|
'center_values': list, # значения в центральных точках
|
|
'center_variance': float, # дисперсия воспроизводимости
|
|
'n_factorial': int, # количество факторных точек
|
|
'n_center': int, # количество центральных точек
|
|
'fisher_ratio': float, # критерий Фишера (если применимо)
|
|
'model_adequate': bool|None # адекватность модели
|
|
}
|
|
}
|
|
|
|
ПРИМЕР ИСПОЛЬЗОВАНИЯ:
|
|
---------------------
|
|
>>> factors = [{'name': 'X', 'low': 0, 'high': 10, 'center': 5, 'unit': ''}]
|
|
>>> design = generate_factorial_design(factors, center_points=2)
|
|
>>> results = [[2.0], [8.0], [5.1], [4.9]] # 2 факторные + 2 центральные
|
|
>>> responses = [{'name': 'Выход', 'unit': '%'}]
|
|
>>> analysis = analyze_experiment(results, design, responses)
|
|
>>> print(analysis['Выход']['mean'])
|
|
5.0
|
|
"""
|
|
|
|
analysis = {}
|
|
|
|
for resp_idx, response in enumerate(responses):
|
|
resp_name = response.get('name', f'Отклик_{resp_idx+1}')
|
|
|
|
# Собираем все значения для этого отклика
|
|
y_values = [results[i][resp_idx] for i in range(len(results))]
|
|
|
|
# Базовые статистики
|
|
mean_y = np.mean(y_values)
|
|
variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0
|
|
std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0
|
|
cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0
|
|
|
|
# Разделяем факторные и центральные точки
|
|
factorial_y = []
|
|
center_y = []
|
|
|
|
for i, exp in enumerate(design):
|
|
if exp.get('is_center', False):
|
|
center_y.append(y_values[i])
|
|
else:
|
|
factorial_y.append(y_values[i])
|
|
|
|
# Анализ воспроизводимости
|
|
center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0
|
|
|
|
# Критерий Фишера для проверки адекватности модели
|
|
fisher_ratio = None
|
|
model_adequate = None
|
|
|
|
if len(center_y) > 1 and len(factorial_y) > 1:
|
|
factorial_variance = np.var(factorial_y, ddof=1)
|
|
if factorial_variance > 0 and center_variance > 0:
|
|
fisher_ratio = max(factorial_variance, center_variance) / min(factorial_variance, center_variance)
|
|
# Критическое значение F (приблизительное, для p=0.05)
|
|
# Более точное требует знания степеней свободы
|
|
model_adequate = fisher_ratio < 4.0
|
|
|
|
analysis[resp_name] = {
|
|
'mean': mean_y,
|
|
'variance': variance,
|
|
'std_dev': std_dev,
|
|
'cv': cv,
|
|
'factorial_values': factorial_y,
|
|
'center_values': center_y,
|
|
'center_variance': center_variance,
|
|
'n_factorial': len(factorial_y),
|
|
'n_center': len(center_y),
|
|
'fisher_ratio': fisher_ratio,
|
|
'model_adequate': model_adequate
|
|
}
|
|
|
|
return analysis
|
|
|
|
|
|
|
|
def create_factor_from_reagent(
|
|
reagent: Dict,
|
|
total_volume: float,
|
|
volume_unit: str,
|
|
step_percent: float = 10.0
|
|
) -> Dict:
|
|
"""
|
|
СОЗДАЁТ ФАКТОР ИЗ РЕАГЕНТА (для интеграции калькулятора и DoE)
|
|
|
|
Преобразует рассчитанный реагент в фактор для планирования эксперимента.
|
|
|
|
Параметры:
|
|
reagent: рассчитанный реагент (из calculate_medium_composition)
|
|
total_volume: общий объём среды
|
|
volume_unit: единица объёма
|
|
step_percent: шаг варьирования в процентах от нулевого уровня
|
|
|
|
Возвращает:
|
|
Dict: фактор для использования в generate_factorial_design()
|
|
"""
|
|
center_value = reagent.get('undiluted_amount', reagent.get('calculated_amount', 0))
|
|
step_value = center_value * step_percent / 100
|
|
|
|
high_level, low_level = calculate_factor_levels(
|
|
center_value, step_value, "ед."
|
|
)
|
|
|
|
return {
|
|
'name': reagent['name'],
|
|
'center': center_value,
|
|
'low': low_level,
|
|
'high': high_level,
|
|
'step': step_value,
|
|
'step_type': 'абс',
|
|
'unit': reagent.get('unit', volume_unit),
|
|
'percentage': reagent.get('percentage', 0),
|
|
'dilution_factor': reagent.get('dilution_factor', 1.0)
|
|
}
|