Переработан интерфейс программы, расчёт занчений в процессе исправления.

This commit is contained in:
2026-05-18 15:53:24 +05:00
parent e6e86e50a3
commit 3ddd05acff
38 changed files with 2820 additions and 3555 deletions
+28
View File
@@ -0,0 +1,28 @@
"""Библиотека расчётов для биохимика"""
from .medium import (
calculate_medium_composition,
convert_units,
VOLUME_UNITS,
MASS_UNITS
)
from .doe import (
generate_factorial_design,
analyze_experiment,
calculate_factor_levels,
create_factor_from_reagent, # Добавлено
FACTOR_TYPES
)
__all__ = [
'calculate_medium_composition',
'convert_units',
'VOLUME_UNITS',
'MASS_UNITS',
'generate_factorial_design',
'analyze_experiment',
'calculate_factor_levels',
'create_factor_from_reagent',
'FACTOR_TYPES',
]
+352
View File
@@ -0,0 +1,352 @@
"""
Планирование эксперимента (DoE)
Модуль для генерации полнофакторных планов экспериментов
и анализа результатов.
Основные функции:
- generate_factorial_design() - генерация плана
- analyze_experiment() - статистический анализ
- calculate_factor_levels() - расчёт уровней факторов
"""
from typing import List, Dict, Tuple, Optional
import random
import numpy as np
# Типы расчёта шага
FACTOR_TYPES = {
'absolute': 'абс', # абсолютный шаг
'relative': '%', # относительный шаг (процент от нулевого уровня)
}
def calculate_factor_levels(
center_value: float,
step_value: float,
step_type: str,
base_value: float = None
) -> Tuple[float, float]:
"""
РАССЧИТЫВАЕТ ВЕРХНИЙ И НИЖНИЙ УРОВНИ ФАКТОРА
Параметры:
center_value: нулевой уровень фактора (центральная точка)
step_value: значение шага
step_type: тип шага ("абс" - абсолютный, "%" - относительный)
base_value: базовое значение для относительного шага (если None, используется center_value)
Возвращает:
(high_level, low_level): верхний и нижний уровни
Пример:
>>> calculate_factor_levels(100, 10, "%")
(110.0, 90.0)
>>> calculate_factor_levels(100, 20, "абс")
(120.0, 80.0)
"""
# Определяем абсолютное значение шага
if step_type == "%":
base = base_value if base_value is not None else center_value
step_abs = center_value * step_value / 100
else: # "абс" или "absolute"
step_abs = step_value
high_level = center_value + step_abs
low_level = center_value - step_abs
return high_level, low_level
def generate_factorial_design(
factors: List[Dict],
center_points: int = 3,
randomize: bool = True
) -> List[Dict]:
"""
ГЕНЕРИРУЕТ ПОЛНОФАКТОРНЫЙ ПЛАН ЭКСПЕРИМЕНТА
Создаёт матрицу планирования для 2^k полнофакторного эксперимента
с добавлением центральных точек.
ПАРАМЕТРЫ:
----------
factors : List[Dict]
Список факторов. Каждый фактор - словарь с ключами:
- name (str): название фактора
- low (float): нижний уровень (-1)
- high (float): верхний уровень (+1)
- center (float): нулевой уровень (0)
- unit (str): единица измерения
- step (float, опционально): шаг варьирования
- step_type (str, опционально): тип шага ("абс" или "%")
Минимально необходимые ключи: name, low, high, center, unit
center_points : int
Количество центральных точек (повторений в центре плана)
По умолчанию 3
randomize : bool
Перемешивать ли порядок опытов случайным образом
По умолчанию True
ВОЗВРАЩАЕТ:
-----------
List[Dict]
Список экспериментов. Каждый эксперимент - словарь:
- для каждого фактора: "Фактор_N" с полями:
- coded: кодированное значение (-1, 0, +1)
- natural: натуральное значение
- name: название фактора
- unit: единица измерения
- is_center (bool): является ли точка центральной
- center_num (int): номер центральной точки (если is_center)
ПРИМЕР ИСПОЛЬЗОВАНИЯ:
---------------------
>>> factors = [
... {'name': 'Температура', 'low': 25, 'high': 37, 'center': 31, 'unit': '°C'},
... {'name': 'pH', 'low': 6.5, 'high': 7.5, 'center': 7.0, 'unit': ''}
... ]
>>> design = generate_factorial_design(factors, center_points=2)
>>> print(len(design)) # 2^2 + 2 = 6
6
>>> design[0]['Фактор_1']['coded'] # первый фактор в первом опыте
-1
"""
k = len(factors)
if k == 0:
return []
n_factorial = 2 ** k
design = []
# Генерация факторных точек (все комбинации уровней)
for i in range(n_factorial):
experiment = {}
for j in range(k):
# Кодированный уровень: -1 для 0, +1 для 1 в бите
# (k-1-j) для правильного порядка факторов
coded_level = -1 if (i >> (k - 1 - 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)
}
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)
}
+211
View File
@@ -0,0 +1,211 @@
"""
Расчёт питательных сред
Этот модуль содержит функции для расчёта состава питательных сред
на основе процентов, коэффициентов конверсии и разбавления.
Основная функция: calculate_medium_composition()
"""
from typing import List, Dict, Tuple
# Константы для конверсии единиц измерения
# Базовые единицы: мкл для объёма, мг для массы
VOLUME_UNITS = {
'нл': 0.001, # нанолитры -> микролитры
'мкл': 1.0, # микролитры (база)
'мл': 1000.0, # миллилитры -> микролитры
'л': 1000000.0, # литры -> микролитры
}
MASS_UNITS = {
'нг': 0.000001, # нанограммы -> миллиграммы
'мкг': 0.001, # микрограммы -> миллиграммы
'мг': 1.0, # миллиграммы (база)
'г': 1000.0, # граммы -> миллиграммы
'кг': 1000000.0, # килограммы -> миллиграммы
}
def convert_units(value: float, from_unit: str, to_unit: str = None) -> float:
"""
Конвертирует значение между единицами объёма или массы
Параметры:
value: числовое значение
from_unit: исходная единица (например "мл" или "мг")
to_unit: целевая единица (если None, конвертирует в базовую)
Возвращает:
float: сконвертированное значение
Пример:
>>> convert_units(100, 'мл') # конвертирует в базовую (мкл)
100000.0
>>> convert_units(500, 'мкл', 'мл')
0.5
"""
# Определяем тип единицы (объём или масса)
if from_unit in VOLUME_UNITS:
units_map = VOLUME_UNITS
elif from_unit in MASS_UNITS:
units_map = MASS_UNITS
else:
raise ValueError(f"Неизвестная единица измерения: {from_unit}")
# Конвертируем в базовую единицу (мкл для объёма, мг для массы)
value_in_base = value * units_map[from_unit]
# Если нужна конвертация в другую единицу
if to_unit and to_unit in units_map:
return value_in_base / units_map[to_unit]
return value_in_base
def calculate_medium_composition(
total_volume: float,
volume_unit: str,
reagents: List[Dict],
solvent_name: str = "Вода"
) -> Dict:
"""
РАССЧИТЫВАЕТ СОСТАВ ПИТАТЕЛЬНОЙ СРЕДЫ
Эта функция является основной для расчёта питательных сред.
ВХОДНЫЕ ПАРАМЕТРЫ:
---------------
total_volume : float
Общий объём/количество среды (например 1000)
volume_unit : str
Единица измерения общего объёма: "нл", "мкл", "мл", "л"
reagents : List[Dict]
Список реагентов. Каждый реагент - словарь с ключами:
- name (str): название реагента
- percentage (float): процентное содержание в среде (0-100)
- unit (str): единица измерения реагента (нг, мкг, мг, г, кг, нл, мкл, мл, л)
- dilution_factor (float): фактор разбавления (необяз., по умолч. 1.0)
Пример реагента:
{
'name': 'Глюкоза',
'percentage': 2.5,
'unit': 'г',
'dilution_factor': 1.0
}
solvent_name : str, optional
Название растворителя (по умолчанию "Вода")
ВОЗВРАЩАЕМЫЙ СЛОВАРЬ:
--------------------
{
'total_volume': float, # Исходный объём
'total_unit': str, # Единица измерения
'solvent_name': str, # Название растворителя
'solvent_volume': float, # Объём растворителя
'solvent_percentage': float, # Процент растворителя
'reagents': List[Dict] # Список реагентов с рассчитанными количествами
}
Каждый реагент в возвращаемом списке содержит:
- все исходные поля
- calculated_amount (float): рассчитанное количество реагента
- undiluted_amount (float): количество до разбавления
- amount_unit (str): единица измерения (скопирована из unit)
ПРИМЕР ИСПОЛЬЗОВАНИЯ (в CLI):
-----------------------------
>>> reagents = [
... {'name': 'Глюкоза', 'percentage': 2.0, 'unit': 'г', 'conversion_factor': 1.0},
... {'name': 'Пептон', 'percentage': 1.0, 'unit': 'г', 'conversion_factor': 1.0}
... ]
>>> result = calculate_medium_composition(1000, 'мл', reagents)
>>> print(f"Растворитель: {result['solvent_volume']} мл")
Растворитель: 970.0 мл
>>> for r in result['reagents']:
... print(f"{r['name']}: {r['calculated_amount']} {r['unit']}")
Глюкоза: 20.0 г
Пептон: 10.0 г
"""
# Проверка входных данных
if total_volume <= 0:
raise ValueError(f"Общий объём должен быть положительным: {total_volume}")
if volume_unit not in VOLUME_UNITS:
raise ValueError(f"Неизвестная единица объёма: {volume_unit}")
# Суммируем проценты всех реагентов
total_percentage = sum(r.get('percentage', 0) for r in reagents)
if total_percentage > 100:
raise ValueError(
f"Сумма процентов ({total_percentage:.2f}%) превышает 100%"
)
# Конвертируем общий объём в базовую единицу (мкл)
total_base = convert_units(total_volume, volume_unit)
results = []
total_diluted_volume_base = 0 # объём разбавленных реагентов в мкл
for reagent in reagents:
# Извлекаем параметры с значениями по умолчанию
percentage = reagent.get('percentage', 0)
unit = reagent.get('unit', 'мг')
# print ("unit = ",unit)
# conversion_factor = reagent.get('conversion_factor', 1.0)
dilution_factor = reagent.get('dilution_factor', 1.0)
# Проверяем, является ли реагент жидкостью (объём) или твёрдым веществом (масса)
is_volume = unit in VOLUME_UNITS
# 1. Объём реагента в среде (исходя из процента)
amount_in_base = (percentage / 100) * total_base
# print ("amount_in_base = ",amount_in_base)
# 2. Применяем коэффициент конверсии
# adjusted_amount_base = amount_in_base * conversion_factor
# 3. Конвертируем в нужную единицу (без учёта разбавления)
# undiluted_amount = convert_units(adjusted_amount_base, volume_unit, unit)
undiluted_amount = convert_units(amount_in_base, 'мкл', unit)
# print ("volume_unit = ",volume_unit)
# 4. Применяем разбавление
if dilution_factor <= 0:
dilution_factor = 1.0
diluted_amount = undiluted_amount * dilution_factor
# print ("diluted_amount = ", diluted_amount)
# 5. Для объёмных реагентов учитываем в расчёте растворителя
if is_volume:
reagent_volume_base = convert_units(diluted_amount, unit)
total_diluted_volume_base += reagent_volume_base
# Сохраняем результат
reagent_result = reagent.copy()
reagent_result['calculated_amount'] = diluted_amount
reagent_result['undiluted_amount'] = undiluted_amount
reagent_result['amount_unit'] = unit
results.append(reagent_result)
# Рассчитываем объём растворителя
solvent_volume_base = total_base - total_diluted_volume_base
if solvent_volume_base < 0:
solvent_volume_base = 0
solvent_volume = convert_units(solvent_volume_base, 'мкл', volume_unit)
solvent_percentage = 100 - total_percentage
return {
'total_volume': total_volume,
'total_unit': volume_unit,
'solvent_name': solvent_name,
'solvent_volume': solvent_volume,
'solvent_percentage': solvent_percentage,
'reagents': results
}
+208
View File
@@ -0,0 +1,208 @@
"""
Модель данных проекта для сохранения/загрузки в JSON
"""
from typing import List, Dict, Optional
from dataclasses import dataclass, asdict
from datetime import datetime
import json
@dataclass
class ReagentData:
"""Данные реагента"""
name: str
percentage: float
unit: str
conversion_factor: float
dilution_factor: float
def to_dict(self) -> Dict:
return asdict(self)
@classmethod
def from_dict(cls, data: Dict) -> 'ReagentData':
return cls(
name=data['name'],
percentage=data['percentage'],
unit=data['unit'],
conversion_factor=data.get('conversion_factor', 1.0),
dilution_factor=data.get('dilution_factor', 1.0)
)
@dataclass
class FactorData:
"""Данные фактора эксперимента"""
name: str
center: float
low: float
high: float
step: float
step_type: str
unit: str
percentage: Optional[float] = None
dilution_factor: Optional[float] = None
def to_dict(self) -> Dict:
d = asdict(self)
# Удаляем None значения
return {k: v for k, v in d.items() if v is not None}
@classmethod
def from_dict(cls, data: Dict) -> 'FactorData':
return cls(
name=data['name'],
center=data['center'],
low=data['low'],
high=data['high'],
step=data.get('step', 0),
step_type=data.get('step_type', 'абс'),
unit=data.get('unit', ''),
percentage=data.get('percentage'),
dilution_factor=data.get('dilution_factor')
)
@dataclass
class ResponseData:
"""Данные отклика эксперимента"""
name: str
unit: str
def to_dict(self) -> Dict:
return asdict(self)
@classmethod
def from_dict(cls, data: Dict) -> 'ResponseData':
return cls(
name=data['name'],
unit=data.get('unit', '')
)
@dataclass
class ExperimentResultsData:
"""Результаты эксперимента"""
design: List[Dict] # План эксперимента
results: List[List[float]] # Результаты измерений
responses: List[ResponseData] # Отклики
def to_dict(self) -> Dict:
return {
'design': self.design,
'results': self.results,
'responses': [r.to_dict() for r in self.responses]
}
@classmethod
def from_dict(cls, data: Dict) -> 'ExperimentResultsData':
return cls(
design=data['design'],
results=data['results'],
responses=[ResponseData.from_dict(r) for r in data['responses']]
)
@dataclass
class ProjectData:
"""Полные данные проекта"""
# Информация о проекте
project_name: str
created_at: str
modified_at: str
version: str = "1.0"
# Данные калькулятора сред
medium_total_volume: float = 1000.0
medium_volume_unit: str = "мл"
medium_solvent: str = "Вода"
medium_reagents: List[ReagentData] = None
# Данные эксперимента
experiment_factors: List[FactorData] = None
experiment_responses: List[ResponseData] = None
experiment_center_points: int = 3
experiment_randomize: bool = True
experiment_results: Optional[ExperimentResultsData] = None
def __post_init__(self):
if self.medium_reagents is None:
self.medium_reagents = []
if self.experiment_factors is None:
self.experiment_factors = []
if self.experiment_responses is None:
self.experiment_responses = []
def to_dict(self) -> Dict:
"""Конвертирует в словарь для JSON"""
return {
'project_info': {
'name': self.project_name,
'created_at': self.created_at,
'modified_at': self.modified_at,
'version': self.version
},
'medium_calculator': {
'total_volume': self.medium_total_volume,
'volume_unit': self.medium_volume_unit,
'solvent': self.medium_solvent,
'reagents': [r.to_dict() for r in self.medium_reagents]
},
'experiment': {
'factors': [f.to_dict() for f in self.experiment_factors],
'responses': [r.to_dict() for r in self.experiment_responses],
'center_points': self.experiment_center_points,
'randomize': self.experiment_randomize,
'results': self.experiment_results.to_dict() if self.experiment_results else None
}
}
@classmethod
def from_dict(cls, data: Dict) -> 'ProjectData':
"""Создаёт объект из словаря"""
project_info = data.get('project_info', {})
medium = data.get('medium_calculator', {})
experiment = data.get('experiment', {})
# Создаём объект
obj = cls(
project_name=project_info.get('name', 'Новый проект'),
created_at=project_info.get('created_at', datetime.now().isoformat()),
modified_at=project_info.get('modified_at', datetime.now().isoformat()),
version=project_info.get('version', '1.0'),
medium_total_volume=medium.get('total_volume', 1000.0),
medium_volume_unit=medium.get('volume_unit', 'мл'),
medium_solvent=medium.get('solvent', 'Вода'),
medium_reagents=[ReagentData.from_dict(r) for r in medium.get('reagents', [])],
experiment_factors=[FactorData.from_dict(f) for f in experiment.get('factors', [])],
experiment_responses=[ResponseData.from_dict(r) for r in experiment.get('responses', [])],
experiment_center_points=experiment.get('center_points', 3),
experiment_randomize=experiment.get('randomize', True),
experiment_results=ExperimentResultsData.from_dict(experiment['results'])
if experiment.get('results') else None
)
return obj
def save_to_file(self, filename: str):
"""Сохраняет проект в JSON файл"""
self.modified_at = datetime.now().isoformat()
with open(filename, 'w', encoding='utf-8') as f:
json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
@classmethod
def load_from_file(cls, filename: str) -> 'ProjectData':
"""Загружает проект из JSON файла"""
with open(filename, 'r', encoding='utf-8') as f:
data = json.load(f)
return cls.from_dict(data)
def create_new_project(name: str = "Новый проект") -> ProjectData:
"""Создаёт новый проект с текущей датой"""
now = datetime.now().isoformat()
return ProjectData(
project_name=name,
created_at=now,
modified_at=now
)