From 23927356415a319e540a311175368f840d8e7396 Mon Sep 17 00:00:00 2001 From: Artemiy Date: Fri, 29 May 2026 08:55:43 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A2=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D1=81=D0=B2=D0=B5=D1=82=D0=BA=D0=B0=20=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=BE=D0=B2=20=D1=8D=D0=BA=D1=81=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B8=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D1=81=D0=B2=D0=B5=D1=87=D0=B8=D0=B2=D0=B0=D1=8E=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D1=81=D0=BE=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D0=BD=D0=BD=D0=BE=20=D0=B7=D0=BD=D0=B0=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC:=20+=20=D0=B7=D0=B5=D0=BB=D1=91?= =?UTF-8?q?=D0=BD=D1=8B=D0=BC=20-=20=D0=BA=D1=80=D0=B0=D1=81=D0=BD=D1=8B?= =?UTF-8?q?=D0=BC,=200=20(=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D1=8B=20?= =?UTF-8?q?=D1=81=20=D1=88=D0=B0=D0=B3=D0=BE=D0=BC=20=D0=B2=200)=20-=20?= =?UTF-8?q?=D1=81=D0=B2=D0=B5=D1=82=D0=BB=D0=BE=D1=81=D0=B5=D1=80=D1=8B?= =?UTF-8?q?=D0=BC.=20=D0=A3=D0=B1=D1=80=D0=B0=D0=BD=20=D0=BB=D0=B8=D1=88?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=20=D1=81=D1=82=D0=BE=D0=BB=D0=B1=D0=B5=D1=86?= =?UTF-8?q?=20=D0=B2=20=D0=BC=D0=B0=D1=82=D1=80=D0=B8=D1=86=D0=B5=20=D1=84?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=81=D0=BF=D0=B5=D1=80=D0=B8=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- calculations/doe.py | 121 +++++----------------------- gui.py | 188 +++++++++++++++++++------------------------- matrix.csv | 12 +++ 3 files changed, 113 insertions(+), 208 deletions(-) create mode 100644 matrix.csv diff --git a/calculations/doe.py b/calculations/doe.py index cac8410..4918141 100644 --- a/calculations/doe.py +++ b/calculations/doe.py @@ -84,76 +84,30 @@ def generate_factorial_design( 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 + Генерирует полнофакторный план 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): - # Кодированный уровень: -1 для 0, +1 для 1 в бите - # (k-1-j) для правильного порядка факторов - coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1 - - # Натуральное значение + # Правильный порядок битов: младший бит - первый фактор + # (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, @@ -161,11 +115,10 @@ def generate_factorial_design( '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, @@ -176,11 +129,11 @@ def generate_factorial_design( center_experiment['is_center'] = True center_experiment['center_num'] = i + 1 design.append(center_experiment) - - # Перемешивание порядка + + # Перемешивание порядка (опционально) if randomize: random.shuffle(design) - + return design @@ -294,44 +247,6 @@ def analyze_experiment( 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, diff --git a/gui.py b/gui.py index 935580e..eaa5bcc 100644 --- a/gui.py +++ b/gui.py @@ -553,13 +553,12 @@ class MainWindow(QMainWindow): """Генерирует план эксперимента""" all_factors = self._get_factors_from_table() factors = get_active_factors(all_factors) - # Выбираем только те факторы, шаг которых не равен нулю i_factors = get_inactive_factors(all_factors) - # Отдельно сохраняем неактивные факторы - if len(factors) == 0: + + if not factors: QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!") return - + try: design = generate_factorial_design( factors=factors, @@ -567,74 +566,13 @@ class MainWindow(QMainWindow): randomize=self.randomize_check.isChecked() ) self.generated_design = design - - 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) + self._fill_design_matrix(design, factors, i_factors) - 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._setup_results_table(n_exp) - - QMessageBox.information(self, "Успех", - f"Сгенерирован план для {n_factors} факторов ({n_exp} опытов)") + QMessageBox.information(self, "Успех", f"Сгенерирован план для {len(factors)} факторов ({len(design)} опытов)") except Exception as e: QMessageBox.critical(self, "Ошибка", f"Ошибка генерации плана: {str(e)}") + def _setup_results_table(self, n_experiments: int): """Настраивает таблицу результатов для ввода данных""" @@ -978,47 +916,19 @@ class MainWindow(QMainWindow): """Обновляет отображение матрицы планирования""" if not self.generated_design: 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_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'): self.export_csv_btn.setEnabled(True) - + n_factorial = 2 ** n_factors n_center = self.center_points_spin.value() self.design_info.setText( @@ -1027,6 +937,74 @@ class MainWindow(QMainWindow): 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: """Форматирует число для отображения""" diff --git a/matrix.csv b/matrix.csv new file mode 100644 index 0000000..7cf898f --- /dev/null +++ b/matrix.csv @@ -0,0 +1,12 @@ +Меласса (разб. ×5.00),Глицерин,Этанол,Лимонная кислота,Вода,Тип,Отклик +2.25 мл,0.45 г,0.3 мл,0.06 г,26.94 мл,Факторная, +5.25 мл,0.45 г,0.3 мл,0.06 г,23.94 мл,Факторная, +2.25 мл,0.75 г,0.3 мл,0.06 г,26.64 мл,Факторная, +5.25 мл,0.75 г,0.3 мл,0.06 г,23.64 мл,Факторная, +2.25 мл,0.45 г,0.6 мл,0.06 г,26.64 мл,Факторная, +5.25 мл,0.45 г,0.6 мл,0.06 г,23.64 мл,Факторная, +2.25 мл,0.75 г,0.6 мл,0.06 г,26.34 мл,Факторная, +5.25 мл,0.75 г,0.6 мл,0.06 г,23.34 мл,Факторная, +3.75 мл,0.6 г,0.45 мл,0.06 г,25.14 мл,Центр #1, +3.75 мл,0.6 г,0.45 мл,0.06 г,25.14 мл,Центр #2, +3.75 мл,0.6 г,0.45 мл,0.06 г,25.14 мл,Центр #3,