Compare commits

..

2 Commits

3 changed files with 104 additions and 208 deletions
+3
View File
@@ -1,6 +1,9 @@
# Python # Python
backup backup
Backup Backup
*.csv
*.xls
*.xlsx
*.json *.json
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
+18 -103
View File
@@ -84,76 +84,30 @@ def generate_factorial_design(
randomize: bool = True randomize: bool = True
) -> List[Dict]: ) -> List[Dict]:
""" """
ГЕНЕРИРУЕТ ПОЛНОФАКТОРНЫЙ ПЛАН ЭКСПЕРИМЕНТА Генерирует полнофакторный план 2^k с правильным порядком изменения факторов:
- фактор 1 меняется через 1 эксперимент (2^0)
Создаёт матрицу планирования для 2^k полнофакторного эксперимента - фактор 2 меняется через 2 эксперимента (2^1)
с добавлением центральных точек. - фактор 3 меняется через 4 эксперимента (2^2)
- и т.д.
ПАРАМЕТРЫ:
----------
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) k = len(factors)
if k == 0: if k == 0:
return [] return []
n_factorial = 2 ** k n_factorial = 2 ** k
design = [] design = []
# Генерация факторных точек (все комбинации уровней) # Генерация факторных точек в правильном порядке (двоичный счётчик)
for i in range(n_factorial): for i in range(n_factorial):
experiment = {} experiment = {}
for j in range(k): for j in range(k):
# Кодированный уровень: -1 для 0, +1 для 1 в бите # Правильный порядок битов: младший бит - первый фактор
# (k-1-j) для правильного порядка факторов # (j) - для прямого порядка: фактор 1 меняется чаще всего
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1 coded_level = -1 if (i >> j) & 1 == 0 else 1
# Натуральное значение
natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high'] natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high']
experiment[f"Фактор_{j+1}"] = { experiment[f"Фактор_{j+1}"] = {
'coded': coded_level, 'coded': coded_level,
'natural': natural_value, 'natural': natural_value,
@@ -161,11 +115,10 @@ def generate_factorial_design(
'unit': factors[j].get('unit', '') 'unit': factors[j].get('unit', '')
} }
design.append(experiment) design.append(experiment)
# Добавление центральных точек # Добавление центральных точек
for i in range(center_points): for i in range(center_points):
center_experiment = {} center_experiment = {}
for j in range(k): for j in range(k):
center_experiment[f"Фактор_{j+1}"] = { center_experiment[f"Фактор_{j+1}"] = {
'coded': 0, 'coded': 0,
@@ -176,11 +129,11 @@ def generate_factorial_design(
center_experiment['is_center'] = True center_experiment['is_center'] = True
center_experiment['center_num'] = i + 1 center_experiment['center_num'] = i + 1
design.append(center_experiment) design.append(center_experiment)
# Перемешивание порядка # Перемешивание порядка (опционально)
if randomize: if randomize:
random.shuffle(design) random.shuffle(design)
return design return design
@@ -294,44 +247,6 @@ def analyze_experiment(
return analysis 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( def create_factor_from_reagent(
reagent: Dict, reagent: Dict,
+83 -105
View File
@@ -553,13 +553,12 @@ class MainWindow(QMainWindow):
"""Генерирует план эксперимента""" """Генерирует план эксперимента"""
all_factors = self._get_factors_from_table() all_factors = self._get_factors_from_table()
factors = get_active_factors(all_factors) factors = get_active_factors(all_factors)
# Выбираем только те факторы, шаг которых не равен нулю
i_factors = get_inactive_factors(all_factors) i_factors = get_inactive_factors(all_factors)
# Отдельно сохраняем неактивные факторы
if len(factors) == 0: if not factors:
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!") QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
return return
try: try:
design = generate_factorial_design( design = generate_factorial_design(
factors=factors, factors=factors,
@@ -567,74 +566,13 @@ class MainWindow(QMainWindow):
randomize=self.randomize_check.isChecked() randomize=self.randomize_check.isChecked()
) )
self.generated_design = design self.generated_design = design
self._fill_design_matrix(design, factors, i_factors)
n_exp = len(design)
n_factors = len(factors)
n_i_factors = len(i_factors)
solvent_name = self.exp_solvent.text()
total_volume = self.exp_total_volume.value()
solvent_unit = self.exp_volume_unit.currentText()
self.design_matrix.setRowCount(n_exp)
self.design_matrix.setColumnCount(n_factors + 4)
headers = [f['name'] for f in factors] + [f['name'] for f in i_factors] + [solvent_name] +["Тип"] +["Отклик"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, exp in enumerate(design):
solvent = convert_units(total_volume,solvent_unit)
for f_idx in range(n_factors):
key = f"Фактор_{f_idx+1}"
if key not in exp:
continue
value = exp[key]['natural']
unit = factors[f_idx]['unit']
solvent -= convert_units(value,unit)
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
if exp.get('is_center', False):
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, f_idx, item)
if n_i_factors>0:
for f_idx in range(n_i_factors):
value = i_factors[f_idx]['center']
unit = i_factors[f_idx]['unit']
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, n_factors + f_idx, item)
solvent = convert_units(solvent, "мкл", solvent_unit)
display = self._format_number(solvent)
if solvent_unit:
display += f" {solvent_unit}"
item = QTableWidgetItem(display)
self.design_matrix.setItem(exp_idx, n_factors+n_i_factors, item)
if exp.get('is_center', False):
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
type_item.setBackground(QColor(255, 255, 200))
else:
type_item = QTableWidgetItem("Факторная")
self.design_matrix.setItem(exp_idx, n_factors+n_i_factors + 1, type_item)
self.design_matrix.resizeColumnsToContents()
n_factorial = 2 ** n_factors
n_center = self.center_points_spin.value()
self.design_info.setText(
f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}"
)
self.export_csv_btn.setEnabled(True) self.export_csv_btn.setEnabled(True)
self._setup_results_table(n_exp) QMessageBox.information(self, "Успех", f"Сгенерирован план для {len(factors)} факторов ({len(design)} опытов)")
QMessageBox.information(self, "Успех",
f"Сгенерирован план для {n_factors} факторов ({n_exp} опытов)")
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Ошибка генерации плана: {str(e)}") QMessageBox.critical(self, "Ошибка", f"Ошибка генерации плана: {str(e)}")
def _setup_results_table(self, n_experiments: int): def _setup_results_table(self, n_experiments: int):
"""Настраивает таблицу результатов для ввода данных""" """Настраивает таблицу результатов для ввода данных"""
@@ -978,47 +916,19 @@ class MainWindow(QMainWindow):
"""Обновляет отображение матрицы планирования""" """Обновляет отображение матрицы планирования"""
if not self.generated_design: if not self.generated_design:
return return
factors = self._get_factors_from_table() all_factors = self._get_factors_from_table()
factors = get_active_factors(all_factors)
i_factors = get_inactive_factors(all_factors)
self._fill_design_matrix(self.generated_design, factors, i_factors)
n_exp = len(self.generated_design) n_exp = len(self.generated_design)
n_factors = len(factors) n_factors = len(factors)
self.design_matrix.setRowCount(n_exp)
self.design_matrix.setColumnCount(n_factors + 2)
headers = [""] + [f['name'] for f in factors] + ["Тип"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, exp in enumerate(self.generated_design):
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
for f_idx in range(n_factors):
key = f"Фактор_{f_idx + 1}"
if key not in exp:
continue
value = exp[key]['natural']
unit = factors[f_idx]['unit']
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
if exp.get('is_center', False):
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, f_idx + 1, item)
if exp.get('is_center', False):
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
type_item.setBackground(QColor(255, 255, 200))
else:
type_item = QTableWidgetItem("Факторная")
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
self.design_matrix.resizeColumnsToContents()
if hasattr(self, 'export_csv_btn'): if hasattr(self, 'export_csv_btn'):
self.export_csv_btn.setEnabled(True) self.export_csv_btn.setEnabled(True)
n_factorial = 2 ** n_factors n_factorial = 2 ** n_factors
n_center = self.center_points_spin.value() n_center = self.center_points_spin.value()
self.design_info.setText( self.design_info.setText(
@@ -1027,6 +937,74 @@ class MainWindow(QMainWindow):
self._setup_results_table(n_exp) self._setup_results_table(n_exp)
# ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ========== # ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========
def _fill_design_matrix(self, design, factors, i_factors):
"""Заполняет матрицу планирования (общая логика)"""
n_exp = len(design)
n_factors = len(factors)
n_i_factors = len(i_factors)
solvent_name = self.exp_solvent.text()
total_volume = self.exp_total_volume.value()
solvent_unit = self.exp_volume_unit.currentText()
self.design_matrix.setRowCount(n_exp)
self.design_matrix.setColumnCount(n_factors + n_i_factors + 3)
headers = [f['name'] for f in factors] + [f['name'] for f in i_factors] + [solvent_name] + ["Тип"] + ["Отклик"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, exp in enumerate(design):
remaining = convert_units(total_volume, solvent_unit)
# Активные факторы
for f_idx in range(n_factors):
value = exp[f"Фактор_{f_idx+1}"]['natural']
unit = factors[f_idx]['unit']
remaining -= convert_units(value, unit)
item = self._create_item(value, unit, exp.get('is_center', False))
if value == factors[f_idx]['high']:
item.setBackground(QColor(200, 250, 200))
elif value ==factors[f_idx]['low']:
item.setBackground(QColor(250, 200, 200))
else:
item.setBackground(QColor(230, 230, 230))
self.design_matrix.setItem(exp_idx, f_idx, item)
# Неактивные факторы
for f_idx in range(n_i_factors):
value = i_factors[f_idx]['center']
unit = i_factors[f_idx]['unit']
remaining -= convert_units(value, unit)
item = self._create_item(value, unit, True)
item.setBackground(QColor(230, 230, 230))
self.design_matrix.setItem(exp_idx, n_factors + f_idx, item)
# Растворитель
remaining = convert_units(remaining, 'мкл', solvent_unit)
item = self._create_item(remaining, solvent_unit, False)
item.setBackground(QColor(200, 200, 255))
self.design_matrix.setItem(exp_idx, n_factors + n_i_factors, item)
# Тип опыта
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}" if exp.get('is_center') else "Факторная")
if exp.get('is_center'):
type_item.setBackground(QColor(200, 200, 200))
self.design_matrix.setItem(exp_idx, n_factors + n_i_factors + 1, type_item)
# Отклик
self.design_matrix.setItem(exp_idx, n_factors + n_i_factors + 2, QTableWidgetItem(""))
self.design_matrix.resizeColumnsToContents()
def _create_item(self, value, unit, is_center):
"""Создаёт QTableWidgetItem с форматированием"""
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
if is_center:
item.setBackground(QColor(255, 255, 200))
return item
def _format_number(self, value: float) -> str: def _format_number(self, value: float) -> str:
"""Форматирует число для отображения""" """Форматирует число для отображения"""