From 3ddd05acffb58264d4acd11f71ed240baa763a2b Mon Sep 17 00:00:00 2001 From: Artemiy Date: Mon, 18 May 2026 15:53:24 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D0=BD=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84?= =?UTF-8?q?=D0=B5=D0=B9=D1=81=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=BC=D1=8B,=20=D1=80=D0=B0=D1=81=D1=87=D1=91=D1=82=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BD=D1=87=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=D0=B5=20=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + README.md | 219 -- backup_20260507_145331/src/__init__.py | 3 - .../src/controllers/__init__.py | 4 - .../src/controllers/experiment_controller.py | 23 - .../src/controllers/medium_controller.py | 118 -- backup_20260507_145331/src/models/__init__.py | 5 - .../src/models/experiment_model.py | 80 - .../src/models/medium_model.py | 113 -- backup_20260507_145331/src/models/reagent.py | 10 - backup_20260507_145331/src/utils/__init__.py | 3 - backup_20260507_145331/src/views/__init__.py | 5 - .../src/views/experiment_view.py | 470 ----- .../src/views/main_window.py | 101 - .../src/views/medium_view.py | 207 -- calculations/__init__.py | 28 + calculations/doe.py | 352 ++++ calculations/medium.py | 211 ++ calculations/models/project_data.py | 208 ++ cli.py | 152 ++ gui.py | 1005 ++++++++++ main.py | 16 +- patch.sh | 1761 ++++++++--------- requirements.txt | 2 +- run.sh | 2 - src/__init__.py | 3 - src/controllers/__init__.py | 4 - src/controllers/experiment_controller.py | 23 - src/controllers/medium_controller.py | 143 -- src/models/__init__.py | 5 - src/models/experiment_model.py | 80 - src/models/medium_model.py | 113 -- src/models/reagent.py | 10 - src/utils/__init__.py | 3 - src/views/__init__.py | 5 - src/views/experiment_view.py | 532 ----- src/views/main_window.py | 101 - src/views/medium_view.py | 251 --- 38 files changed, 2820 insertions(+), 3555 deletions(-) delete mode 100644 README.md delete mode 100644 backup_20260507_145331/src/__init__.py delete mode 100644 backup_20260507_145331/src/controllers/__init__.py delete mode 100644 backup_20260507_145331/src/controllers/experiment_controller.py delete mode 100644 backup_20260507_145331/src/controllers/medium_controller.py delete mode 100644 backup_20260507_145331/src/models/__init__.py delete mode 100644 backup_20260507_145331/src/models/experiment_model.py delete mode 100644 backup_20260507_145331/src/models/medium_model.py delete mode 100644 backup_20260507_145331/src/models/reagent.py delete mode 100644 backup_20260507_145331/src/utils/__init__.py delete mode 100644 backup_20260507_145331/src/views/__init__.py delete mode 100644 backup_20260507_145331/src/views/experiment_view.py delete mode 100644 backup_20260507_145331/src/views/main_window.py delete mode 100644 backup_20260507_145331/src/views/medium_view.py create mode 100644 calculations/__init__.py create mode 100644 calculations/doe.py create mode 100644 calculations/medium.py create mode 100644 calculations/models/project_data.py create mode 100644 cli.py create mode 100644 gui.py mode change 100755 => 100644 main.py delete mode 100755 run.sh delete mode 100644 src/__init__.py delete mode 100644 src/controllers/__init__.py delete mode 100644 src/controllers/experiment_controller.py delete mode 100644 src/controllers/medium_controller.py delete mode 100644 src/models/__init__.py delete mode 100644 src/models/experiment_model.py delete mode 100644 src/models/medium_model.py delete mode 100644 src/models/reagent.py delete mode 100644 src/utils/__init__.py delete mode 100644 src/views/__init__.py delete mode 100644 src/views/experiment_view.py delete mode 100644 src/views/main_window.py delete mode 100644 src/views/medium_view.py diff --git a/.gitignore b/.gitignore index 9188cee..b31f2a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Python backup +Backup *.json __pycache__/ *.py[cod] @@ -46,3 +47,6 @@ Thumbs.db # Локальные настройки *.local + +*.sh +*.bash diff --git a/README.md b/README.md deleted file mode 100644 index 887710e..0000000 --- a/README.md +++ /dev/null @@ -1,219 +0,0 @@ -# 🧪 Цифровой помощник биохимика - -Биотехнологические инструменты для лаборатории: калькулятор питательных сред и планирование полнофакторного эксперимента (DoE). - ---- - -## 📋 Содержание - -- [Возможности](#-возможности) -- [Установка](#-установка) -- [Запуск](#-запуск) -- [Структура проекта](#-структура-проекта) -- [Калькулятор питательных сред](#-калькулятор-питательных-сред) -- [Планирование эксперимента](#-планирование-эксперимента) -- [Сохранение и загрузка](#-сохранение-и-загрузка) -- [Требования](#-требования) -- [Лицензия](#-лицензия) - ---- - -## ✨ Возможности - -### 🔬 Калькулятор питательных сред -- Расчёт состава питательной среды по процентному содержанию компонентов -- Поддержка массовых (нг, мкг, мг, г, кг) и объёмных (нл, мкл, мл, л) единиц -- Учёт коэффициента пересчёта для каждого реагента -- Учёт разбавления реагентов (фактор разбавления) -- Автоматический расчёт необходимого количества растворителя -- Сохранение и загрузка рецептов в JSON - -### 📊 Планирование эксперимента (DoE) -- Полнофакторный план 2ᵏ (k факторов) -- Генерация матрицы планирования с центральными точками -- Рэндомизация порядка опытов -- Ввод и анализ результатов экспериментов -- Регрессионный анализ (среднее, дисперсия, стандартное отклонение, CV) -- Критерий Фишера для проверки адекватности модели -- Экспорт матрицы в CSV - ---- - -## 🚀 Установка - -### 1. Клонирование репозитория - -```bash -git clone -cd nutrient_medium_pyqt -2. Создание виртуального окружения (рекомендуется) -bash -python3 -m venv venv -source venv/bin/activate # Linux/Mac -# или -venv\Scripts\activate # Windows -``` -3. Установка зависимостей -```bash -pip install -r requirements.txt -▶️ Запуск -Из командной строки -bash -python3 main.py -Через скрипт -bash -chmod +x run.sh -./run.sh -``` -📁 Структура проекта -text -nutrient_medium_pyqt/ -├── main.py # Точка входа -├── run.sh # Скрипт запуска -├── requirements.txt # Зависимости -├── README.md # Документация -│ -└── src/ # Исходный код - ├── models/ # Модели данных - │ ├── reagent.py # Класс Reagent - │ ├── medium_model.py # Модель расчёта сред - │ └── experiment_model.py # Модель планирования эксперимента - │ - ├── views/ # GUI компоненты - │ ├── main_window.py # Главное окно - │ ├── medium_view.py # Окно калькулятора сред - │ └── experiment_view.py # Окно планирования эксперимента - │ - └── controllers/ # Контроллеры - ├── medium_controller.py # Логика калькулятора - └── experiment_controller.py # Логика планирования - -## 🧪 Калькулятор питательных сред -### Основные поля -Поле Описание -Общее количество Общий объём/масса готовой среды -Растворитель Название растворителя (вода, буфер и т.д.) -Название Имя реагента -% Процентное содержание в среде -Единица Единица измерения реагента -Коэфф. Коэффициент пересчёта (например, для солей-гидратов) -Разбавление (x) Во сколько раз разбавлен исходный раствор -### Пример использования -Укажите общий объём среды (например, 1000 мл) - -Добавьте реагенты с их процентным содержанием - -При необходимости укажите коэффициент пересчёта и разбавление - -Нажмите "Рассчитать" - -В столбце "Количество" отобразятся необходимые объёмы/массы - -#📈 Планирование эксперимента -### Вкладка "Параметры эксперимента" -Факторы -Фактор — название независимой переменной - -Нулевой уровень (0) — базовое значение - -Шаг — интервал варьирования - -Верхний уровень (+1) = нулевой уровень + шаг (вычисляется автоматически) - -Нижний уровень (-1) = нулевой уровень – шаг (вычисляется автоматически) - -Единица измерения — °C, pH, г/л и т.д. - -### Отклики -Зависимые переменные (OD600, концентрация продукта и т.д.) - -Настройки -Количество центральных точек — для оценки дисперсии воспроизводимости - -Рэндомизация порядка опытов — случайный порядок выполнения - -Вкладка "Матрица планирования" -Отображает сгенерированный план эксперимента - -Факторные точки помечены комбинацией уровней (+/–) - -Центральные точки выделены жёлтым цветом - -Кнопка "Экспорт в CSV" сохраняет матрицу в файл - -### Вкладка "Анализ результатов" -Введите результаты экспериментов в таблицу - -Нажмите "Провести регрессионный анализ" - -Получите: - -Среднее значение отклика - -Общую дисперсию - -Стандартное отклонение - -Коэффициент вариации - -Дисперсию воспроизводимости (по центральным точкам) - -Критерий Фишера для проверки адекватности модели - -💾 Сохранение и загрузка - Калькулятор сред -💾 Сохранить — сохранить рецепт в JSON-файл - -📂 Загрузить — загрузить сохранённый рецепт - -Планирование эксперимента -📊 Экспорт в CSV — сохранить матрицу планирования - -## 📦 Требования -Пакет Версия Назначение -PyQt5 ≥ 5.15.0 Графический интерфейс -numpy ≥ 1.19.0 Математические вычисления -Проверка установки -```bash -python3 -c "import PyQt5; import numpy; print('OK')" -``` -📝 Формат JSON (калькулятор сред) - -```json -{ - "total_amount": 1000.0, - "amount_unit": "мл", - "solvent": "Вода", - "reagents": [ - { - "name": "Глюкоза", - "percentage": 2.0, - "unit": "г", - "conversion_factor": 1.0, - "dilution_factor": 1.0 - } - ] -} -``` -## 🐛 Устранение неполадок - -Ошибка "ModuleNotFoundError: No module named 'PyQt5'" -```bash -pip install PyQt5 -Ошибка "No module named 'numpy'" -bash -pip install numpy -Проблемы с отображением кириллицы -Убедитесь, что в системе установлены русские шрифты -``` -📄 Лицензия -© 2026 Цифровой помощник биохимика - -Версия: 1.0.0 - -🙏 Благодарности -Разработано с использованием: - -PyQt5 — GUI framework - -NumPy — численные вычисления diff --git a/backup_20260507_145331/src/__init__.py b/backup_20260507_145331/src/__init__.py deleted file mode 100644 index e878fc2..0000000 --- a/backup_20260507_145331/src/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Цифровой помощник биохимика - основная библиотека""" - -__version__ = "1.0.0" diff --git a/backup_20260507_145331/src/controllers/__init__.py b/backup_20260507_145331/src/controllers/__init__.py deleted file mode 100644 index 37f75f8..0000000 --- a/backup_20260507_145331/src/controllers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .medium_controller import MediumController -from .experiment_controller import ExperimentController - -__all__ = ['MediumController', 'ExperimentController'] diff --git a/backup_20260507_145331/src/controllers/experiment_controller.py b/backup_20260507_145331/src/controllers/experiment_controller.py deleted file mode 100644 index 023e7fa..0000000 --- a/backup_20260507_145331/src/controllers/experiment_controller.py +++ /dev/null @@ -1,23 +0,0 @@ -from ..models.experiment_model import ExperimentModel - -class ExperimentController: - def __init__(self, view): - self.model = ExperimentModel() - self.view = view - - def update_model_from_view(self): - factors = self.view.get_factors_data() - self.model.set_factors(factors) - - responses = [] - for row in range(self.view.responses_table.rowCount()): - name_item = self.view.responses_table.item(row, 0) - unit_item = self.view.responses_table.item(row, 1) - if name_item: - responses.append({ - 'name': name_item.text(), - 'unit': unit_item.text() if unit_item else "" - }) - self.model.set_responses(responses) - self.model.set_center_points(self.view.center_points_spin.value()) - self.model.set_randomize(self.view.randomize_check.isChecked()) diff --git a/backup_20260507_145331/src/controllers/medium_controller.py b/backup_20260507_145331/src/controllers/medium_controller.py deleted file mode 100644 index 6b4ff94..0000000 --- a/backup_20260507_145331/src/controllers/medium_controller.py +++ /dev/null @@ -1,118 +0,0 @@ -from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QColor -import json -from ..models.medium_model import MediumModel -from ..models.reagent import Reagent - -class MediumController: - def __init__(self, view): - self.model = MediumModel() - self.view = view - self._connect_signals() - self._setup_initial_data() - - def _connect_signals(self): - self.view.add_row_btn.clicked.connect(self.add_reagent_row) - self.view.remove_row_btn.clicked.connect(self.remove_reagent_row) - self.view.calculate_btn.clicked.connect(self._perform_calculation) - self.view.save_btn.clicked.connect(self.save_composition) - self.view.load_btn.clicked.connect(self.load_composition) - self.view.solvent_input.textChanged.connect(self.view.update_solvent_name) - - def _setup_initial_data(self): - self.view.add_initial_rows() - - def add_reagent_row(self): - self.view.add_new_row() - - def remove_reagent_row(self): - self.view.remove_selected_row() - - def _perform_calculation(self): - try: - self._update_model_from_view() - results, solvent_amount, solvent_percentage = self.model.calculate_amounts() - self.view.update_results(results) - self.view.update_solvent_result(solvent_amount, self.model.amount_unit) - self.view.update_solvent_percent(solvent_percentage) - except ValueError as e: - self.view.show_error(f"Ошибка в данных: {str(e)}") - except Exception as e: - self.view.show_error(f"Неожиданная ошибка: {str(e)}") - - def _update_model_from_view(self): - self.model.reagents.clear() - self.model.total_amount = self.view.amount_input.value() - self.model.amount_unit = self.view.amount_unit_combo.currentText() - self.model.solvent = self.view.solvent_input.text() - for row in range(1, self.view.table.rowCount()): - name_item = self.view.table.item(row, 0) - percentage_item = self.view.table.item(row, 1) - unit_widget = self.view.table.cellWidget(row, 2) - conversion_item = self.view.table.item(row, 3) - dilution_item = self.view.table.item(row, 4) - if not all([name_item, percentage_item, conversion_item]): - continue - try: - name = name_item.text() - percentage = float(percentage_item.text()) - unit = unit_widget.currentText() if unit_widget else "мг" - conversion_factor = float(conversion_item.text()) - dilution_factor = float(dilution_item.text()) if dilution_item else 1.0 - reagent = Reagent(name, percentage, unit, conversion_factor) - reagent.dilution_factor = dilution_factor - self.model.reagents.append(reagent) - except ValueError as e: - raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}") - - def save_composition(self): - filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)") - if filename: - if not filename.lower().endswith('.json'): - filename += '.json' - try: - self._update_model_from_view() - self.model.save_to_file(filename) - QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!") - except Exception as e: - self.view.show_error(f"Ошибка сохранения: {str(e)}") - - def load_composition(self): - filename, _ = QFileDialog.getOpenFileName(self.view, "Загрузить состав среды", "", "JSON Files (*.json);;All Files (*)") - if filename: - try: - self.model.load_from_file(filename) - self._update_view_from_model() - QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен") - except FileNotFoundError: - QMessageBox.critical(self.view, "Ошибка", f"Файл не найден: {filename}") - except json.JSONDecodeError as e: - QMessageBox.critical(self.view, "Ошибка", f"Неверный формат JSON-файла: {str(e)}") - except Exception as e: - QMessageBox.critical(self.view, "Ошибка", f"Ошибка при загрузке состава: {str(e)}") - - def _update_view_from_model(self): - while self.view.table.rowCount() > 1: - self.view.table.removeRow(1) - if self.view.table.rowCount() == 0: - self.view.add_solvent_row() - self.view.amount_input.setValue(self.model.total_amount) - index = self.view.amount_unit_combo.findText(self.model.amount_unit) - if index >= 0: - self.view.amount_unit_combo.setCurrentIndex(index) - self.view.solvent_input.setText(self.model.solvent) - self.view.update_solvent_name() - for reagent in self.model.reagents: - row = self.view.table.rowCount() - self.view.table.insertRow(row) - self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name)) - self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}")) - unit_combo = QComboBox() - unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) - unit_combo.setCurrentText(reagent.unit) - self.view.table.setCellWidget(row, 2, unit_combo) - self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}")) - self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}")) - self.view.table.setItem(row, 5, QTableWidgetItem("")) - self.view.clear_results() diff --git a/backup_20260507_145331/src/models/__init__.py b/backup_20260507_145331/src/models/__init__.py deleted file mode 100644 index 3bcdcad..0000000 --- a/backup_20260507_145331/src/models/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .reagent import Reagent -from .medium_model import MediumModel -from .experiment_model import ExperimentModel - -__all__ = ['Reagent', 'MediumModel', 'ExperimentModel'] diff --git a/backup_20260507_145331/src/models/experiment_model.py b/backup_20260507_145331/src/models/experiment_model.py deleted file mode 100644 index fe5d322..0000000 --- a/backup_20260507_145331/src/models/experiment_model.py +++ /dev/null @@ -1,80 +0,0 @@ -import numpy as np -from typing import List, Dict, Any - -class ExperimentModel: - def __init__(self): - self.factors = [] - self.responses = [] - self.center_points = 3 - self.randomize = True - - def set_factors(self, factors: List[Dict]): - self.factors = factors - - def set_responses(self, responses: List[Dict]): - self.responses = responses - - def set_center_points(self, n: int): - self.center_points = n - - def set_randomize(self, value: bool): - self.randomize = value - - def calculate_factorial_design(self) -> List[Dict]: - k = len(self.factors) - if k == 0: - return [] - n_factorial = 2 ** k - design = [] - for i in range(n_factorial): - experiment = {} - for j in range(k): - coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1 - natural_value = self.factors[j]['low'] if coded_level == -1 else self.factors[j]['high'] - experiment[f"Фактор_{j+1}"] = { - 'coded': coded_level, - 'natural': natural_value, - 'name': self.factors[j]['name'], - 'unit': self.factors[j]['unit'] - } - design.append(experiment) - for i in range(self.center_points): - center_experiment = {} - for j in range(k): - center_experiment[f"Фактор_{j+1}"] = { - 'coded': 0, - 'natural': self.factors[j]['center'], - 'name': self.factors[j]['name'], - 'unit': self.factors[j]['unit'] - } - center_experiment['is_center'] = True - center_experiment['center_num'] = i + 1 - design.append(center_experiment) - if self.randomize: - import random - random.shuffle(design) - return design - - def analyze_results(self, results: List[List[float]], design: List[Dict]) -> Dict: - analysis = {} - for resp_idx, response in enumerate(self.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 - 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) - } - return analysis diff --git a/backup_20260507_145331/src/models/medium_model.py b/backup_20260507_145331/src/models/medium_model.py deleted file mode 100644 index 48e91f8..0000000 --- a/backup_20260507_145331/src/models/medium_model.py +++ /dev/null @@ -1,113 +0,0 @@ -import json -from typing import List, Tuple -from .reagent import Reagent - -VOLUME_UNITS = { - 'нл': 0.001, 'мкл': 1.0, 'мл': 1000.0, 'л': 1000000.0 -} - -MASS_UNITS = { - 'нг': 0.000001, 'мкг': 0.001, 'мг': 1.0, 'г': 1000.0, 'кг': 1000000.0 -} - -class MediumModel: - def __init__(self): - self.total_amount = 100.0 - self.amount_unit = 'мл' - self.solvent = 'Вода' - self.reagents = [] - - def convert_amount(self, amount_base: float, target_unit: str, is_volume: bool) -> float: - if is_volume: - conversion_factor = VOLUME_UNITS.get(target_unit, 1.0) - else: - conversion_factor = MASS_UNITS.get(target_unit, 1.0) - return amount_base / conversion_factor - - def calculate_amounts(self) -> Tuple[List[float], float, float]: - results = [] - if not self.reagents: - return results, self.total_amount, 100.0 - - total_percentage = sum(r.percentage for r in self.reagents) - if total_percentage > 100: - raise ValueError(f"Сумма процентов реагентов ({total_percentage:.2f}%) превышает 100%") - - total_in_base = self.total_amount * VOLUME_UNITS[self.amount_unit] - - undiluted_amounts = [] - for reagent in self.reagents: - amount_in_base = (reagent.percentage / 100) * total_in_base - is_volume = reagent.unit in VOLUME_UNITS - adjusted_amount = amount_in_base * reagent.conversion_factor - final_amount = self.convert_amount(adjusted_amount, reagent.unit, is_volume) - undiluted_amounts.append(final_amount) - - diluted_amounts = [] - total_diluted_volume_base = 0 - for i, reagent in enumerate(self.reagents): - dilution_factor = getattr(reagent, 'dilution_factor', 1.0) - if dilution_factor <= 0: - dilution_factor = 1.0 - diluted_amount = undiluted_amounts[i] * dilution_factor - diluted_amounts.append(diluted_amount) - is_volume = reagent.unit in VOLUME_UNITS - if is_volume: - reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.unit] - else: - reagent_volume_base = 0 - total_diluted_volume_base += reagent_volume_base - - solvent_volume_base = total_in_base - total_diluted_volume_base - solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit] - if solvent_amount < 0: - solvent_amount = 0 - solvent_percentage = 100 - total_percentage - - return diluted_amounts, solvent_amount, solvent_percentage - - def save_to_file(self, filename: str): - data = { - 'total_amount': self.total_amount, - 'amount_unit': self.amount_unit, - 'solvent': self.solvent, - 'reagents': [{'name': r.name, 'percentage': r.percentage, 'unit': r.unit, - 'conversion_factor': r.conversion_factor, 'dilution_factor': getattr(r, 'dilution_factor', 1.0)} - for r in self.reagents] - } - with open(filename, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - def load_from_file(self, filename: str): - with open(filename, 'r', encoding='utf-8') as f: - data = json.load(f) - self.total_amount = data['total_amount'] - self.amount_unit = data['amount_unit'] - self.solvent = data['solvent'] - self.reagents.clear() - for r_data in data['reagents']: - reagent = Reagent(r_data['name'], r_data['percentage'], r_data['unit'], r_data.get('conversion_factor', 1.0)) - reagent.dilution_factor = r_data.get('dilution_factor', 1.0) - self.reagents.append(reagent) - - def add_reagent(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0, dilution_factor: float = 1.0): - reagent = Reagent(name, percentage, unit, conversion_factor) - reagent.dilution_factor = dilution_factor - self.reagents.append(reagent) - - def remove_reagent(self, index: int): - if 0 <= index < len(self.reagents): - del self.reagents[index] - - def clear_reagents(self): - self.reagents.clear() - - def get_reagent_count(self) -> int: - return len(self.reagents) - - def set_total_amount(self, amount: float, unit: str): - self.total_amount = amount - self.amount_unit = unit - - def set_solvent(self, solvent_name: str): - self.solvent = solvent_name diff --git a/backup_20260507_145331/src/models/reagent.py b/backup_20260507_145331/src/models/reagent.py deleted file mode 100644 index 160892b..0000000 --- a/backup_20260507_145331/src/models/reagent.py +++ /dev/null @@ -1,10 +0,0 @@ -class Reagent: - def __init__(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0): - self.name = name - self.percentage = percentage - self.unit = unit - self.conversion_factor = conversion_factor - self.dilution_factor = 1.0 - - def __repr__(self): - return f"Reagent(name={self.name}, percentage={self.percentage}, unit={self.unit})" diff --git a/backup_20260507_145331/src/utils/__init__.py b/backup_20260507_145331/src/utils/__init__.py deleted file mode 100644 index a42179a..0000000 --- a/backup_20260507_145331/src/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Утилиты и вспомогательные функции""" - -__all__ = [] diff --git a/backup_20260507_145331/src/views/__init__.py b/backup_20260507_145331/src/views/__init__.py deleted file mode 100644 index e6fcdd9..0000000 --- a/backup_20260507_145331/src/views/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main_window import MainWindow -from .medium_view import MediumCalculatorWindow -from .experiment_view import ExperimentDesignWindow - -__all__ = ['MainWindow', 'MediumCalculatorWindow', 'ExperimentDesignWindow'] diff --git a/backup_20260507_145331/src/views/experiment_view.py b/backup_20260507_145331/src/views/experiment_view.py deleted file mode 100644 index 8d6b2a6..0000000 --- a/backup_20260507_145331/src/views/experiment_view.py +++ /dev/null @@ -1,470 +0,0 @@ -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, - QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox, - QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, QTextEdit, - QTabWidget, QFormLayout, QCheckBox, QScrollArea, QFileDialog) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QColor, QFont -import csv -import random -import numpy as np - -class ExperimentDesignWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика") - self.setGeometry(200, 100, 1200, 800) - self.setStyleSheet(""" - QMainWindow { background-color: #f5f5f5; } - QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; } - QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; } - QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; } - QPushButton:hover { background-color: #45a049; } - QPushButton#danger { background-color: #f44336; } - QPushButton#danger:hover { background-color: #da190b; } - QTableWidget { gridline-color: #ddd; } - QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } - """) - self._init_ui() - - def _init_ui(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) - layout = QVBoxLayout(central_widget) - - title_label = QLabel("Планирование полнофакторного эксперимента (DoE)") - title_font = QFont() - title_font.setPointSize(18) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet("color: #2E7D32;") - layout.addWidget(title_label) - - tabs = QTabWidget() - - # Вкладка параметров - params_tab = QWidget() - params_layout = QVBoxLayout(params_tab) - - factors_group = QGroupBox("Факторы эксперимента (независимые переменные)") - factors_layout = QVBoxLayout() - - info_label = QLabel("Определите факторы, которые влияют на ваш эксперимент:") - info_label.setStyleSheet("color: #555; font-weight: normal;") - factors_layout.addWidget(info_label) - - self.factors_table = QTableWidget() - self.factors_table.setColumnCount(6) - self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг", - "Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"]) - self.factors_table.setRowCount(2) - - sample_factors = [["Температура", "31", "6", "37", "25", "°C"], ["pH", "7.0", "0.5", "7.5", "6.5", ""]] - for i, factor in enumerate(sample_factors): - for j, value in enumerate(factor): - item = QTableWidgetItem(value) - if j in [3, 4]: - item.setFlags(item.flags() & ~Qt.ItemIsEditable) - item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(i, j, item) - - self.factors_table.setColumnWidth(0, 150) - self.factors_table.setColumnWidth(1, 120) - self.factors_table.setColumnWidth(2, 80) - self.factors_table.setColumnWidth(3, 120) - self.factors_table.setColumnWidth(4, 120) - self.factors_table.setColumnWidth(5, 100) - - self.factors_table.itemChanged.connect(self.on_factor_changed) - factors_layout.addWidget(self.factors_table) - - factor_buttons = QHBoxLayout() - add_factor_btn = QPushButton("+ Добавить фактор") - add_factor_btn.clicked.connect(self.add_factor_row) - remove_factor_btn = QPushButton("- Удалить последний") - remove_factor_btn.clicked.connect(self.remove_factor_row) - factor_buttons.addWidget(add_factor_btn) - factor_buttons.addWidget(remove_factor_btn) - factor_buttons.addStretch() - factors_layout.addLayout(factor_buttons) - factors_group.setLayout(factors_layout) - params_layout.addWidget(factors_group) - - settings_group = QGroupBox("Настройки эксперимента") - settings_layout = QHBoxLayout() - center_layout = QHBoxLayout() - center_layout.addWidget(QLabel("Количество центральных точек:")) - self.center_points_spin = QSpinBox() - self.center_points_spin.setRange(0, 10) - self.center_points_spin.setValue(3) - center_layout.addWidget(self.center_points_spin) - settings_layout.addLayout(center_layout) - self.randomize_check = QCheckBox("Рэндомизировать порядок опытов") - self.randomize_check.setChecked(True) - settings_layout.addWidget(self.randomize_check) - settings_layout.addStretch() - settings_group.setLayout(settings_layout) - params_layout.addWidget(settings_group) - - responses_group = QGroupBox("Отклики (зависимые переменные)") - responses_layout = QVBoxLayout() - self.responses_table = QTableWidget() - self.responses_table.setColumnCount(2) - self.responses_table.setHorizontalHeaderLabels(["Отклик", "Единица измерения"]) - self.responses_table.setRowCount(2) - sample_responses = [["Оптическая плотность (OD600)", ""], ["Концентрация целевого продукта", "мг/мл"]] - for i, response in enumerate(sample_responses): - for j, value in enumerate(response): - self.responses_table.setItem(i, j, QTableWidgetItem(value)) - responses_layout.addWidget(self.responses_table) - - response_buttons = QHBoxLayout() - add_response_btn = QPushButton("+ Добавить отклик") - add_response_btn.clicked.connect(self.add_response_row) - remove_response_btn = QPushButton("- Удалить последний") - remove_response_btn.clicked.connect(self.remove_response_row) - response_buttons.addWidget(add_response_btn) - response_buttons.addWidget(remove_response_btn) - response_buttons.addStretch() - responses_layout.addLayout(response_buttons) - responses_group.setLayout(responses_layout) - params_layout.addWidget(responses_group) - tabs.addTab(params_tab, "📝 Параметры эксперимента") - - # Вкладка матрицы планирования - plan_tab = QWidget() - plan_layout = QVBoxLayout(plan_tab) - plan_info = QLabel("Полнофакторный план эксперимента с центральными точками") - plan_info.setAlignment(Qt.AlignCenter) - plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") - plan_layout.addWidget(plan_info) - - scroll = QScrollArea() - scroll.setWidgetResizable(True) - matrix_widget = QWidget() - matrix_layout = QVBoxLayout(matrix_widget) - self.design_matrix = QTableWidget() - matrix_layout.addWidget(self.design_matrix) - scroll.setWidget(matrix_widget) - plan_layout.addWidget(scroll) - - buttons_layout = QHBoxLayout() - generate_btn = QPushButton("Сгенерировать план эксперимента") - generate_btn.clicked.connect(self.generate_design_matrix) - buttons_layout.addWidget(generate_btn) - export_btn = QPushButton("📊 Экспорт в CSV") - export_btn.clicked.connect(self.export_to_csv) - buttons_layout.addWidget(export_btn) - buttons_layout.addStretch() - plan_layout.addLayout(buttons_layout) - - self.plan_info_label = QLabel("") - self.plan_info_label.setStyleSheet("color: #666; font-size: 12px; padding: 5px;") - plan_layout.addWidget(self.plan_info_label) - tabs.addTab(plan_tab, "📊 Матрица планирования") - - # Вкладка анализа - analysis_tab = QWidget() - analysis_layout = QVBoxLayout(analysis_tab) - analysis_info = QLabel("Введите результаты экспериментов для анализа") - analysis_info.setAlignment(Qt.AlignCenter) - analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") - analysis_layout.addWidget(analysis_info) - self.results_table = QTableWidget() - analysis_layout.addWidget(self.results_table) - analyze_btn = QPushButton("Провести регрессионный анализ") - analyze_btn.clicked.connect(self.perform_analysis) - analysis_layout.addWidget(analyze_btn) - self.analysis_output = QTextEdit() - self.analysis_output.setReadOnly(True) - self.analysis_output.setMaximumHeight(200) - analysis_layout.addWidget(self.analysis_output) - tabs.addTab(analysis_tab, "📈 Анализ результатов") - - layout.addWidget(tabs) - - btn_layout = QHBoxLayout() - close_btn = QPushButton("Закрыть") - close_btn.clicked.connect(self.close) - btn_layout.addStretch() - btn_layout.addWidget(close_btn) - layout.addLayout(btn_layout) - - self.generated_design = None - self.factors_data = None - - def on_factor_changed(self, item): - row = item.row() - col = item.column() - if col in [1, 2]: - try: - center = float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0 - step = float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0 - high = center + step - low = center - step - self.factors_table.blockSignals(True) - if self.factors_table.item(row, 3): - self.factors_table.item(row, 3).setText(f"{high:.3f}".rstrip('0').rstrip('.')) - if self.factors_table.item(row, 4): - self.factors_table.item(row, 4).setText(f"{low:.3f}".rstrip('0').rstrip('.')) - self.factors_table.blockSignals(False) - except ValueError: - pass - - def add_factor_row(self): - row = self.factors_table.rowCount() - self.factors_table.insertRow(row) - self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}")) - self.factors_table.setItem(row, 1, QTableWidgetItem("0")) - self.factors_table.setItem(row, 2, QTableWidgetItem("1")) - high_item = QTableWidgetItem("1") - high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable) - high_item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(row, 3, high_item) - low_item = QTableWidgetItem("-1") - low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable) - low_item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(row, 4, low_item) - self.factors_table.setItem(row, 5, QTableWidgetItem("")) - - def remove_factor_row(self): - if self.factors_table.rowCount() > 1: - self.factors_table.removeRow(self.factors_table.rowCount() - 1) - - def add_response_row(self): - row = self.responses_table.rowCount() - self.responses_table.insertRow(row) - self.responses_table.setItem(row, 0, QTableWidgetItem(f"Отклик_{row+1}")) - self.responses_table.setItem(row, 1, QTableWidgetItem("")) - - def remove_response_row(self): - if self.responses_table.rowCount() > 1: - self.responses_table.removeRow(self.responses_table.rowCount() - 1) - - def get_factors_data(self): - factors = [] - for row in range(self.factors_table.rowCount()): - try: - factor = { - 'name': self.factors_table.item(row, 0).text() if self.factors_table.item(row, 0) else "", - 'center': float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0, - 'step': float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0, - 'high': float(self.factors_table.item(row, 3).text()) if self.factors_table.item(row, 3) else 0, - 'low': float(self.factors_table.item(row, 4).text()) if self.factors_table.item(row, 4) else 0, - 'unit': self.factors_table.item(row, 5).text() if self.factors_table.item(row, 5) else "" - } - factors.append(factor) - except (ValueError, AttributeError): - continue - return factors - - def calculate_factorial_design(self, factors): - k = len(factors) - if k == 0: - return [] - n_factorial = 2 ** k - design = [] - for i in range(n_factorial): - experiment = {} - for j in range(k): - 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]['unit'] - } - design.append(experiment) - - n_center = self.center_points_spin.value() - for i in range(n_center): - 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]['unit'] - } - center_experiment['is_center'] = True - center_experiment['center_num'] = i + 1 - design.append(center_experiment) - - if self.randomize_check.isChecked(): - random.shuffle(design) - return design - - def generate_design_matrix(self): - factors = self.get_factors_data() - if len(factors) == 0: - QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!") - return - - self.factors_data = factors - design = self.calculate_factorial_design(factors) - self.generated_design = design - - n_experiments = len(design) - n_factors = len(factors) - - self.design_matrix.setRowCount(n_experiments) - self.design_matrix.setColumnCount(n_factors + 2) - headers = ["№ опыта"] + [f["name"] for f in factors] + ["Тип точки"] - self.design_matrix.setHorizontalHeaderLabels(headers) - - for exp_idx, experiment in enumerate(design): - self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1))) - for factor_idx in range(n_factors): - factor_key = f"Фактор_{factor_idx + 1}" - value = experiment[factor_key]['natural'] - unit = factors[factor_idx]['unit'] - if isinstance(value, float): - if value == int(value): - display_value = str(int(value)) - else: - display_value = f"{value:.3f}".rstrip('0').rstrip('.') - else: - display_value = str(value) - if unit: - display_value += f" {unit}" - item = QTableWidgetItem(display_value) - if experiment.get('is_center', False): - item.setBackground(QColor(255, 255, 200)) - self.design_matrix.setItem(exp_idx, factor_idx + 1, item) - - if experiment.get('is_center', False): - type_item = QTableWidgetItem(f"Центральная #{experiment['center_num']}") - type_item.setBackground(QColor(255, 255, 200)) - else: - levels = [] - for factor_idx in range(n_factors): - factor_key = f"Фактор_{factor_idx + 1}" - coded = experiment[factor_key]['coded'] - levels.append("+" if coded == 1 else "-") - type_item = QTableWidgetItem(f"Факторная ({''.join(levels)})") - self.design_matrix.setItem(exp_idx, n_factors + 1, type_item) - - self.design_matrix.resizeColumnsToContents() - n_factorial = 2 ** n_factors - n_center = self.center_points_spin.value() - self.plan_info_label.setText(f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов") - self.setup_results_table(n_experiments) - QMessageBox.information(self, "Успех", f"Сгенерирован план для {n_factors} факторов\nФакторных точек: {n_factorial}\nЦентральных точек: {n_center}\nВсего опытов: {n_experiments}") - - def setup_results_table(self, n_experiments): - n_responses = self.responses_table.rowCount() - self.results_table.setRowCount(n_experiments) - self.results_table.setColumnCount(n_responses + 1) - headers = ["№ опыта"] + [self.responses_table.item(i, 0).text() if self.responses_table.item(i, 0) else f"Отклик_{i+1}" for i in range(n_responses)] - self.results_table.setHorizontalHeaderLabels(headers) - for i in range(n_experiments): - self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1))) - self.results_table.setColumnWidth(0, 80) - for i in range(n_responses): - self.results_table.setColumnWidth(i + 1, 150) - - def export_to_csv(self): - if self.design_matrix.rowCount() == 0: - QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") - return - filename, _ = QFileDialog.getSaveFileName(self, "Сохранить план эксперимента", "", "CSV Files (*.csv);;All Files (*)") - if filename: - if not filename.lower().endswith('.csv'): - filename += '.csv' - try: - with open(filename, 'w', newline='', encoding='utf-8-sig') as f: - writer = csv.writer(f) - headers = [] - for j in range(self.design_matrix.columnCount()): - header_item = self.design_matrix.horizontalHeaderItem(j) - headers.append(header_item.text() if header_item else f"Колонка_{j+1}") - writer.writerow(headers) - for i in range(self.design_matrix.rowCount()): - row = [self.design_matrix.item(i, j).text() if self.design_matrix.item(i, j) else "" for j in range(self.design_matrix.columnCount())] - writer.writerow(row) - QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}") - except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}") - - def perform_analysis(self): - n_responses = self.responses_table.rowCount() - if n_responses == 0: - QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один отклик!") - return - if self.generated_design is None: - QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") - return - - results = [] - for i in range(self.results_table.rowCount()): - row_results = [] - for j in range(1, self.results_table.columnCount()): - item = self.results_table.item(i, j) - if item and item.text(): - try: - row_results.append(float(item.text())) - except ValueError: - row_results.append(None) - else: - row_results.append(None) - results.append(row_results) - - for i, row in enumerate(results): - for j, val in enumerate(row): - if val is None: - self.analysis_output.setText(f"Ошибка: Не введены результаты для опыта {i+1}, отклик {j+1}") - return - - self.analysis_output.clear() - self.analysis_output.append("=" * 60) - self.analysis_output.append("РЕЗУЛЬТАТЫ РЕГРЕССИОННОГО АНАЛИЗА") - self.analysis_output.append("=" * 60) - - factors = self.get_factors_data() - design = self.generated_design - - for resp_idx in range(n_responses): - resp_name = self.responses_table.item(resp_idx, 0).text() - self.analysis_output.append(f"\n📊 Отклик: {resp_name}") - self.analysis_output.append("-" * 40) - - 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 - - self.analysis_output.append(f"Среднее значение: {mean_y:.4f}") - self.analysis_output.append(f"Общая дисперсия: {variance:.4f}") - self.analysis_output.append(f"Стандартное отклонение: {std_dev:.4f}") - self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%") - - 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]) - - if len(center_y) > 1: - center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0 - self.analysis_output.append(f"\nЦентральные точки (n={len(center_y)}):") - self.analysis_output.append(f" Среднее: {np.mean(center_y):.4f}") - self.analysis_output.append(f" Дисперсия воспроизводимости: {center_variance:.4f}") - if len(factorial_y) > 0 and center_variance > 0: - factorial_variance = np.var(factorial_y, ddof=1) if len(factorial_y) > 1 else 0 - if factorial_variance > 0: - fisher = max(factorial_variance, center_variance) / min(factorial_variance, center_variance) - self.analysis_output.append(f"\nКритерий Фишера (F-отношение): {fisher:.4f}") - if fisher < 4.0: - self.analysis_output.append("✅ Модель адекватна экспериментальным данным") - else: - self.analysis_output.append("⚠️ Модель может быть неадекватна, требуется проверка") - - self.analysis_output.append("\n" + "=" * 60) - self.analysis_output.append("Анализ завершен") - - def show_error(self, message: str): - QMessageBox.critical(self, "Ошибка", message) diff --git a/backup_20260507_145331/src/views/main_window.py b/backup_20260507_145331/src/views/main_window.py deleted file mode 100644 index 85ab15f..0000000 --- a/backup_20260507_145331/src/views/main_window.py +++ /dev/null @@ -1,101 +0,0 @@ -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QFrame) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Цифровой помощник биохимика - Главное меню") - self.setGeometry(300, 200, 700, 500) - self.setStyleSheet(""" - QMainWindow { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #e8f4f8, stop:1 #f0f0f0); } - QPushButton { background-color: #2196F3; color: white; border: none; padding: 15px; font-size: 16px; font-weight: bold; border-radius: 8px; } - QPushButton:hover { background-color: #1976D2; } - QLabel { color: #333; font-size: 14px; } - """) - self._init_ui() - self.medium_calculator = None - self.experiment_window = None - - def _init_ui(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) - layout = QVBoxLayout(central_widget) - layout.setSpacing(20) - layout.setContentsMargins(50, 50, 50, 50) - - title_label = QLabel("Цифровой помощник биохимика") - title_font = QFont() - title_font.setPointSize(20) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet("color: #1565C0;") - layout.addWidget(title_label) - - subtitle_label = QLabel("Биотехнологические инструменты для лаборатории") - subtitle_font = QFont() - subtitle_font.setPointSize(12) - subtitle_label.setFont(subtitle_font) - subtitle_label.setAlignment(Qt.AlignCenter) - subtitle_label.setStyleSheet("color: #666;") - layout.addWidget(subtitle_label) - - layout.addSpacing(20) - - btn_medium = QPushButton("🧪 Калькулятор питательных сред") - btn_medium.setMinimumHeight(80) - btn_medium.clicked.connect(self.open_medium_calculator) - layout.addWidget(btn_medium) - - desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\nразбавления реагентов и автоматическим расчётом растворителя") - desc1_label.setAlignment(Qt.AlignCenter) - desc1_label.setWordWrap(True) - desc1_label.setStyleSheet("color: #555; font-size: 11px;") - layout.addWidget(desc1_label) - - layout.addSpacing(15) - - btn_experiment = QPushButton("📊 Планирование эксперимента (DoE)") - btn_experiment.setMinimumHeight(80) - btn_experiment.clicked.connect(self.open_experiment_designer) - layout.addWidget(btn_experiment) - - desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\nмногомерный анализ и визуализация") - desc2_label.setAlignment(Qt.AlignCenter) - desc2_label.setWordWrap(True) - desc2_label.setStyleSheet("color: #555; font-size: 11px;") - layout.addWidget(desc2_label) - - layout.addSpacing(15) - - line = QFrame() - line.setFrameShape(QFrame.HLine) - line.setFrameShadow(QFrame.Sunken) - layout.addWidget(line) - - bottom_layout = QHBoxLayout() - version_label = QLabel("Версия 1.0.0 | © 2026 Цифровой помощник биохимика") - version_label.setStyleSheet("color: #999; font-size: 10px;") - bottom_layout.addWidget(version_label) - bottom_layout.addStretch() - btn_exit = QPushButton("Выход") - btn_exit.setMaximumWidth(150) - btn_exit.setStyleSheet("QPushButton { background-color: #f44336; padding: 8px; font-size: 14px; } QPushButton:hover { background-color: #da190b; }") - btn_exit.clicked.connect(self.close) - bottom_layout.addWidget(btn_exit) - layout.addLayout(bottom_layout) - - def open_medium_calculator(self): - from .medium_view import MediumCalculatorWindow - from ..controllers.medium_controller import MediumController - self.medium_calculator = MediumCalculatorWindow() - self.medium_controller = MediumController(self.medium_calculator) - self.medium_calculator.show() - - def open_experiment_designer(self): - from .experiment_view import ExperimentDesignWindow - from ..controllers.experiment_controller import ExperimentController - self.experiment_window = ExperimentDesignWindow() - self.experiment_controller = ExperimentController(self.experiment_window) - self.experiment_window.show() diff --git a/backup_20260507_145331/src/views/medium_view.py b/backup_20260507_145331/src/views/medium_view.py deleted file mode 100644 index f1f1c2f..0000000 --- a/backup_20260507_145331/src/views/medium_view.py +++ /dev/null @@ -1,207 +0,0 @@ -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, - QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget, - QMessageBox, QGroupBox, QFrame) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont, QColor - -class MediumCalculatorWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Калькулятор питательных сред") - self.setGeometry(200, 100, 1200, 700) - self.setStyleSheet(""" - QMainWindow { background-color: #f5f5f5; } - QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; background-color: white; } - QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; } - QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 15px; border-radius: 4px; font-weight: bold; } - QPushButton:hover { background-color: #1976D2; } - QPushButton#danger { background-color: #f44336; } - QPushButton#success { background-color: #4CAF50; } - QTableWidget { gridline-color: #ddd; background-color: white; } - QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } - """) - self._init_ui() - - def _init_ui(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) - layout = QVBoxLayout(central_widget) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) - - title_label = QLabel("Калькулятор питательных сред") - title_label.setAlignment(Qt.AlignCenter) - title_font = QFont() - title_font.setPointSize(18) - title_font.setBold(True) - title_label.setFont(title_font) - layout.addWidget(title_label) - - params_group = QGroupBox("Параметры среды") - params_layout = QHBoxLayout() - amount_layout = QHBoxLayout() - amount_layout.addWidget(QLabel("Общее количество:")) - self.amount_input = QDoubleSpinBox() - self.amount_input.setRange(0.001, 1000000.0) - self.amount_input.setValue(1000.0) - amount_layout.addWidget(self.amount_input) - self.amount_unit_combo = QComboBox() - self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"]) - self.amount_unit_combo.setCurrentText("мл") - amount_layout.addWidget(self.amount_unit_combo) - params_layout.addLayout(amount_layout) - solvent_layout = QHBoxLayout() - solvent_layout.addWidget(QLabel("Растворитель:")) - self.solvent_input = QLineEdit("Вода") - solvent_layout.addWidget(self.solvent_input) - params_layout.addLayout(solvent_layout) - params_layout.addStretch() - params_group.setLayout(params_layout) - layout.addWidget(params_group) - - table_group = QGroupBox("Состав среды") - table_layout = QVBoxLayout() - self.table = QTableWidget() - self.table.setColumnCount(6) - self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"]) - self.table.setAlternatingRowColors(True) - self.table.setSelectionBehavior(QTableWidget.SelectRows) - self.table.setColumnWidth(0, 180) - self.table.setColumnWidth(1, 70) - self.table.setColumnWidth(2, 90) - self.table.setColumnWidth(3, 70) - self.table.setColumnWidth(4, 100) - self.table.setColumnWidth(5, 120) - table_layout.addWidget(self.table) - table_group.setLayout(table_layout) - layout.addWidget(table_group) - - btn_group = QGroupBox("Управление") - btn_layout = QHBoxLayout() - self.add_row_btn = QPushButton("➕ Добавить реагент") - self.remove_row_btn = QPushButton("➖ Удалить реагент") - self.remove_row_btn.setObjectName("danger") - btn_layout.addWidget(self.add_row_btn) - btn_layout.addWidget(self.remove_row_btn) - btn_layout.addStretch() - self.calculate_btn = QPushButton("🧮 Рассчитать") - self.calculate_btn.setObjectName("success") - self.save_btn = QPushButton("💾 Сохранить") - self.load_btn = QPushButton("📂 Загрузить") - btn_layout.addWidget(self.calculate_btn) - btn_layout.addWidget(self.save_btn) - btn_layout.addWidget(self.load_btn) - btn_group.setLayout(btn_layout) - layout.addWidget(btn_group) - - info_frame = QFrame() - info_frame.setFrameShape(QFrame.StyledPanel) - info_frame.setStyleSheet("background-color: #e3f2fd; border-radius: 5px;") - info_layout = QHBoxLayout(info_frame) - info_label = QLabel("ℹ️ Подсказка: Реагенты в массовых единицах не учитываются при расчёте объёма растворителя") - info_layout.addWidget(info_label) - info_layout.addStretch() - layout.addWidget(info_frame) - - def add_initial_rows(self): - self.add_solvent_row() - self.add_new_row() - - def add_solvent_row(self): - row = self.table.rowCount() - self.table.insertRow(row) - solvent_item = QTableWidgetItem(self.solvent_input.text()) - solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable) - solvent_item.setBackground(QColor(230, 230, 230)) - font = QFont() - font.setBold(True) - solvent_item.setFont(font) - self.table.setItem(row, 0, solvent_item) - for col in [1, 3, 4]: - item = QTableWidgetItem("-") - item.setFlags(item.flags() & ~Qt.ItemIsEditable) - item.setBackground(QColor(230, 230, 230)) - self.table.setItem(row, col, item) - unit_item = QTableWidgetItem(self.amount_unit_combo.currentText()) - unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable) - unit_item.setBackground(QColor(230, 230, 230)) - self.table.setItem(row, 2, unit_item) - result_item = QTableWidgetItem("") - result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable) - self.table.setItem(row, 5, result_item) - - def update_solvent_name(self): - if self.table.rowCount() > 0 and self.table.item(0, 0): - self.table.item(0, 0).setText(self.solvent_input.text()) - - def add_new_row(self): - row = self.table.rowCount() - self.table.insertRow(row) - self.table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row}")) - self.table.setItem(row, 1, QTableWidgetItem("0")) - unit_combo = QComboBox() - unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) - unit_combo.setCurrentText("мг") - self.table.setCellWidget(row, 2, unit_combo) - self.table.setItem(row, 3, QTableWidgetItem("1")) - self.table.setItem(row, 4, QTableWidgetItem("1")) - self.table.setItem(row, 5, QTableWidgetItem("")) - - def remove_selected_row(self): - for row in sorted(set(item.row() for item in self.table.selectedItems()), reverse=True): - if row > 0: - self.table.removeRow(row) - - def get_table_data(self): - data = [] - for row in range(1, self.table.rowCount()): - name = self.table.item(row, 0).text() if self.table.item(row, 0) else "" - percent = self.table.item(row, 1).text() if self.table.item(row, 1) else "0" - unit_widget = self.table.cellWidget(row, 2) - unit = unit_widget.currentText() if unit_widget else "мг" - coeff = self.table.item(row, 3).text() if self.table.item(row, 3) else "1" - dilution = self.table.item(row, 4).text() if self.table.item(row, 4) else "1" - data.append([name, percent, unit, coeff, dilution]) - return data - - def update_solvent_percent(self, solvent_percent: float): - if self.table.rowCount() > 0 and self.table.item(0, 1): - self.table.item(0, 1).setText(self.format_number(solvent_percent)) - - def update_solvent_result(self, solvent_amount: float, unit: str): - if self.table.rowCount() > 0: - if self.table.item(0, 5): - self.table.item(0, 5).setText(self.format_number(solvent_amount)) - self.table.item(0, 5).setBackground(QColor(220, 255, 220)) - if self.table.item(0, 2): - self.table.item(0, 2).setText(unit) - - def update_results(self, results: list): - for row, amount in enumerate(results, start=1): - if row < self.table.rowCount() and self.table.item(row, 5): - self.table.item(row, 5).setText(self.format_number(amount)) - self.table.item(row, 5).setBackground(QColor(220, 255, 220)) - - def clear_results(self): - for row in range(self.table.rowCount()): - if self.table.item(row, 5): - self.table.item(row, 5).setText("") - if row == 0: - self.table.item(row, 5).setBackground(QColor(230, 230, 230)) - else: - self.table.item(row, 5).setBackground(QColor(250, 250, 250)) - if self.table.rowCount() > 0 and self.table.item(0, 1): - self.table.item(0, 1).setText("") - if self.table.rowCount() > 0 and self.table.item(0, 2): - self.table.item(0, 2).setText(self.amount_unit_combo.currentText()) - - def format_number(self, value): - if value == int(value): - return str(int(value)) - formatted = f"{value:.6f}".rstrip('0').rstrip('.') - if '.' in formatted and len(formatted.split('.')[1]) > 4: - formatted = f"{value:.4f}".rstrip('0').rstrip('.') - return formatted - - def show_error(self, message: str): - QMessageBox.critical(self, "Ошибка", message) diff --git a/calculations/__init__.py b/calculations/__init__.py new file mode 100644 index 0000000..b555b0b --- /dev/null +++ b/calculations/__init__.py @@ -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', +] diff --git a/calculations/doe.py b/calculations/doe.py new file mode 100644 index 0000000..06305fe --- /dev/null +++ b/calculations/doe.py @@ -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) + } diff --git a/calculations/medium.py b/calculations/medium.py new file mode 100644 index 0000000..b7268c1 --- /dev/null +++ b/calculations/medium.py @@ -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 + } diff --git a/calculations/models/project_data.py b/calculations/models/project_data.py new file mode 100644 index 0000000..a83fc69 --- /dev/null +++ b/calculations/models/project_data.py @@ -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 + ) diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..dd33d8e --- /dev/null +++ b/cli.py @@ -0,0 +1,152 @@ +""" +Интерфейс командной строки для использования вычислительных функций +Позволяет использовать расчёты без графического интерфейса +""" + +import json +import argparse +from typing import List, Dict + +from calculations import ( + calculate_medium_composition, + generate_factorial_design, + analyze_experiment, + convert_units, + VOLUME_UNITS, + MASS_UNITS +) + + +def demo_medium_calculation(): + """Демонстрация расчёта питательной среды""" + print("\n" + "=" * 60) + print("ДЕМОНСТРАЦИЯ РАСЧЁТА ПИТАТЕЛЬНОЙ СРЕДЫ") + print("=" * 60) + + # Пример реагентов + reagents = [ + { + 'name': 'Глюкоза', + 'percentage': 2.0, + 'unit': 'г', + 'conversion_factor': 1.0, + 'dilution_factor': 1.0 + }, + { + 'name': 'Пептон', + 'percentage': 1.0, + 'unit': 'г', + 'conversion_factor': 1.0, + 'dilution_factor': 1.0 + }, + { + 'name': 'Дрожжевой экстракт', + 'percentage': 0.5, + 'unit': 'г', + 'conversion_factor': 1.0, + 'dilution_factor': 1.0 + } + ] + + print("\nИсходные данные:") + print(f"Общий объём: 1000 мл") + print("Реагенты:") + for r in reagents: + print(f" - {r['name']}: {r['percentage']}%") + + # Расчёт + result = calculate_medium_composition( + total_volume=1000, + volume_unit='мл', + reagents=reagents, + solvent_name='Вода' + ) + + print("\nРезультаты расчёта:") + print(f"Растворитель ({result['solvent_name']}): {result['solvent_volume']:.2f} {result['total_unit']} " + f"({result['solvent_percentage']:.1f}%)") + print("\nКоличества реагентов:") + for r in result['reagents']: + print(f" - {r['name']}: {r['calculated_amount']:.4f} {r['unit']}") + + +def demo_doe_calculation(): + """Демонстрация планирования эксперимента""" + print("\n" + "=" * 60) + print("ДЕМОНСТРАЦИЯ ПЛАНИРОВАНИЯ ЭКСПЕРИМЕНТА") + print("=" * 60) + + # Факторы + factors = [ + {'name': 'Температура', 'low': 25, 'high': 37, 'center': 31, 'unit': '°C'}, + {'name': 'pH', 'low': 6.5, 'high': 7.5, 'center': 7.0, 'unit': ''} + ] + + print("\nФакторы эксперимента:") + for f in factors: + print(f" - {f['name']}: {f['low']} – {f['high']} {f['unit']} (центр: {f['center']})") + + # Генерация плана + design = generate_factorial_design(factors, center_points=3, randomize=True) + + print(f"\nСгенерировано {len(design)} опытов:") + for i, exp in enumerate(design): + is_center = exp.get('is_center', False) + type_str = "Центральная" if is_center else "Факторная" + + values = [] + for key in sorted(exp.keys()): + if key.startswith('Фактор_'): + values.append(f"{exp[key]['natural']}{exp[key]['unit']}") + + print(f" {i+1}. {type_str}: {', '.join(values)}") + + +def demo_unit_conversion(): + """Демонстрация конвертации единиц""" + print("\n" + "=" * 60) + print("ДЕМОНСТРАЦИЯ КОНВЕРТАЦИИ ЕДИНИЦ") + print("=" * 60) + + print("\nОбъёмные единицы (база: мкл):") + for unit, factor in VOLUME_UNITS.items(): + print(f" 1 {unit} = {factor} мкл") + + print("\nМассовые единицы (база: мг):") + for unit, factor in MASS_UNITS.items(): + print(f" 1 {unit} = {factor} мг") + + print("\nПримеры конвертации:") + print(f" 100 мл = {convert_units(100, 'мл', 'мкл'):.0f} мкл") + print(f" 5000 мкл = {convert_units(5000, 'мкл', 'мл'):.2f} мл") + print(f" 2 г = {convert_units(2, 'г', 'мг'):.0f} мг") + print(f" 500 мг = {convert_units(500, 'мг', 'г'):.2f} г") + + +def main(): + parser = argparse.ArgumentParser(description='Биохимический помощник - командная строка') + parser.add_argument('--demo', choices=['medium', 'doe', 'units', 'all'], + default='all', help='Демонстрация расчётов') + + args = parser.parse_args() + + if args.demo in ['medium', 'all']: + demo_medium_calculation() + + if args.demo in ['doe', 'all']: + demo_doe_calculation() + + if args.demo in ['units', 'all']: + demo_unit_conversion() + + print("\n" + "=" * 60) + print("Советы по использованию библиотеки:") + print(" from calculations import *") + print(" result = calculate_medium_composition(...)") + print(" design = generate_factorial_design(...)") + print(" analysis = analyze_experiment(...)") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..cd10165 --- /dev/null +++ b/gui.py @@ -0,0 +1,1005 @@ +#!/usr/bin/env python3 +""" +Единый графический интерфейс для калькулятора сред и DoE +""" + +import sys +from typing import List, Dict, Optional + +from PyQt5.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QTabWidget, QGroupBox, QLabel, QPushButton, QTableWidget, + QTableWidgetItem, QDoubleSpinBox, QComboBox, QLineEdit, + QMessageBox, QSpinBox, QCheckBox, QTextEdit, QFileDialog, + QScrollArea, QHeaderView, QToolBar +) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont, QColor + +# Импорты расчётов +from calculations.medium import ( + calculate_medium_composition, + convert_units, + VOLUME_UNITS, + MASS_UNITS +) + +from calculations.doe import ( + generate_factorial_design, + analyze_experiment, + calculate_factor_levels, + FACTOR_TYPES +) + +# Импорт моделей для JSON +try: + from calculations.models.project_data import ( + ProjectData, ReagentData, FactorData, + ResponseData, ExperimentResultsData, create_new_project + ) + JSON_SUPPORT = True +except ImportError: + JSON_SUPPORT = False + print("Предупреждение: Модуль project_data не найден, JSON поддержка отключена") + + +class MainWindow(QMainWindow): + """Главное окно с калькулятором сред и планировщиком эксперимента""" + + def __init__(self): + super().__init__() + self.setWindowTitle("Биохимический помощник") + self.setGeometry(100, 100, 1300, 800) + self.setStyleSheet(self._get_stylesheet()) + + self.generated_design = None + self.last_medium_result = None + self.tab_widget = None + + self._setup_ui() + self._add_file_toolbar() + + def _get_stylesheet(self): + return """ + QMainWindow { background-color: #f0f0f0; } + QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 8px; margin-top: 12px; padding-top: 12px; } + QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; color: #2c3e50; } + QPushButton { background-color: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 5px; font-weight: bold; } + QPushButton:hover { background-color: #2980b9; } + QPushButton#success { background-color: #27ae60; } + QPushButton#success:hover { background-color: #219a52; } + QPushButton#danger { background-color: #e74c3c; } + QPushButton#danger:hover { background-color: #c0392b; } + QTableWidget { gridline-color: #ddd; background-color: white; alternate-background-color: #f9f9f9; } + QHeaderView::section { background-color: #34495e; color: white; padding: 6px; font-weight: bold; } + QTabWidget::pane { border: 1px solid #ccc; border-radius: 5px; } + QTabBar::tab { background-color: #ecf0f1; padding: 8px 16px; margin-right: 2px; } + QTabBar::tab:selected { background-color: #3498db; color: white; } + """ + + def _add_file_toolbar(self): + """Добавляет панель инструментов с кнопками сохранения/загрузки""" + toolbar = QToolBar("Файл") + toolbar.setMovable(False) + self.addToolBar(toolbar) + + if JSON_SUPPORT: + save_action = toolbar.addAction("💾 Сохранить проект") + save_action.triggered.connect(self.save_project_to_json) + + load_action = toolbar.addAction("📂 Загрузить проект") + load_action.triggered.connect(self.load_project_from_json) + + toolbar.addSeparator() + + def _setup_ui(self): + central = QWidget() + self.setCentralWidget(central) + layout = QVBoxLayout(central) + + # Заголовок + title = QLabel("Цифровой помощник биохимика") + title_font = QFont() + title_font.setPointSize(18) + title_font.setBold(True) + title.setFont(title_font) + title.setAlignment(Qt.AlignCenter) + title.setStyleSheet("color: #2c3e50; padding: 10px;") + layout.addWidget(title) + + # Вкладки + self.tab_widget = QTabWidget() + self.tab_widget.addTab(self._create_medium_tab(), "🧪 Калькулятор сред") + self.tab_widget.addTab(self._create_factors_tab(), "📊 Факторы эксперимента") + self.tab_widget.addTab(self._create_design_tab(), "📋 Матрица планирования") + self.tab_widget.addTab(self._create_analysis_tab(), "📈 Анализ результатов") + + layout.addWidget(self.tab_widget) + + # Кнопка закрытия + close_btn = QPushButton("Закрыть") + close_btn.setObjectName("danger") + close_btn.clicked.connect(self.close) + close_btn.setMaximumWidth(150) + + btn_layout = QHBoxLayout() + btn_layout.addStretch() + btn_layout.addWidget(close_btn) + layout.addLayout(btn_layout) + + # ========== ВКЛАДКА 1: КАЛЬКУЛЯТОР СРЕД ========== + + def _create_medium_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # Параметры + params_box = QGroupBox("Параметры среды") + params_layout = QHBoxLayout() + + self.total_volume_spin = QDoubleSpinBox() + self.total_volume_spin.setRange(0.001, 1000000) + self.total_volume_spin.setValue(100) + self.total_volume_spin.setSuffix(" ") + + self.volume_unit_combo = QComboBox() + self.volume_unit_combo.addItems(["мл", "л", "мкл", "нл"]) + self.volume_unit_combo.setCurrentText("мл") + + params_layout.addWidget(QLabel("Общий объём:")) + params_layout.addWidget(self.total_volume_spin) + params_layout.addWidget(self.volume_unit_combo) + params_layout.addSpacing(30) + + params_layout.addWidget(QLabel("Растворитель:")) + self.solvent_input = QLineEdit("Вода") + params_layout.addWidget(self.solvent_input) + params_layout.addStretch() + + params_box.setLayout(params_layout) + layout.addWidget(params_box) + + # Таблица реагентов + reagents_box = QGroupBox("Состав среды") + reagents_layout = QVBoxLayout() + + self.reagents_table = QTableWidget() + self.reagents_table.setColumnCount(5) + self.reagents_table.setHorizontalHeaderLabels( + ["Реагент", "%", "Единица", "Разбавление", "Количество"] + ) + self.reagents_table.setAlternatingRowColors(True) + self.reagents_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) + + reagents_layout.addWidget(self.reagents_table) + + # Кнопки управления реагентами + btn_layout = QHBoxLayout() + add_btn = QPushButton("➕ Добавить реагент") + add_btn.clicked.connect(self._add_reagent_row) + remove_btn = QPushButton("➖ Удалить реагент") + remove_btn.setObjectName("danger") + remove_btn.clicked.connect(self._remove_reagent_row) + calculate_btn = QPushButton("🧮 Рассчитать") + calculate_btn.setObjectName("success") + calculate_btn.clicked.connect(self._calculate_medium) + + btn_layout.addWidget(add_btn) + btn_layout.addWidget(remove_btn) + btn_layout.addStretch() + btn_layout.addWidget(calculate_btn) + reagents_layout.addLayout(btn_layout) + + reagents_box.setLayout(reagents_layout) + layout.addWidget(reagents_box) + + # Информационная панель + self.info_label = QLabel("ℹ️ Нажмите «Рассчитать» для получения количеств реагентов") + self.info_label.setStyleSheet("background-color: #ecf0f1; padding: 8px; border-radius: 5px;") + layout.addWidget(self.info_label) + + # Добавляем начальные строки + self._add_reagent_row() + + return tab + + def _add_reagent_row(self): + row = self.reagents_table.rowCount() + self.reagents_table.insertRow(row) + self.reagents_table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row+1}")) + self.reagents_table.setItem(row, 1, QTableWidgetItem("0")) + + unit_combo = QComboBox() + unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"]) + unit_combo.setCurrentText("мл") + self.reagents_table.setCellWidget(row, 2, unit_combo) + + self.reagents_table.setItem(row, 3, QTableWidgetItem("1")) + self.reagents_table.setItem(row, 4, QTableWidgetItem("")) + + def _remove_reagent_row(self): + for row in sorted(set(i.row() for i in self.reagents_table.selectedItems()), reverse=True): + if row >= 0: + self.reagents_table.removeRow(row) + + def _get_reagents_from_table(self) -> List[Dict]: + """Собирает данные реагентов из таблицы""" + reagents = [] + for row in range(self.reagents_table.rowCount()): + name_item = self.reagents_table.item(row, 0) + percent_item = self.reagents_table.item(row, 1) + unit_widget = self.reagents_table.cellWidget(row, 2) + dilution_item = self.reagents_table.item(row, 3) + + if not name_item or not percent_item: + continue + + try: + percent_text = percent_item.text().strip() + if not percent_text: + continue + + reagent = { + 'name': name_item.text(), + 'percentage': float(percent_text), + 'unit': unit_widget.currentText() if unit_widget else "мг", + 'dilution_factor': float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0 + } + reagents.append(reagent) + except ValueError as e: + print(f"Ошибка в строке {row}: {e}") + continue + + return reagents + + def _calculate_medium(self): + """Выполняет расчёт питательной среды""" + try: + reagents = self._get_reagents_from_table() + + if not reagents: + QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один реагент") + return + + result = calculate_medium_composition( + total_volume=self.total_volume_spin.value(), + volume_unit=self.volume_unit_combo.currentText(), + reagents=reagents, + solvent_name=self.solvent_input.text() + ) + + # Отображаем результаты в таблице + for row, reagent in enumerate(result['reagents']): + if row < self.reagents_table.rowCount(): + amount_text = self._format_number(reagent['calculated_amount']) + self.reagents_table.setItem(row, 4, QTableWidgetItem(amount_text)) + # Подсветка + item = self.reagents_table.item(row, 4) + if item: + item.setBackground(QColor(220, 255, 220)) + + # Информация о растворителе + solvent_text = ( + f"✅ Растворитель: {self._format_number(result['solvent_volume'])} {result['total_unit']} " + f"({result['solvent_percentage']:.1f}%)" + ) + self.info_label.setText(solvent_text) + self.info_label.setStyleSheet("background-color: #d5f5e3; padding: 8px; border-radius: 5px;") + + # Сохраняем результаты для передачи в DoE + self.last_medium_result = result + + QMessageBox.information(self, "Успех", "Расчёт выполнен успешно!") + + except Exception as e: + QMessageBox.critical(self, "Ошибка", str(e)) + + # ========== ВКЛАДКА 2: ФАКТОРЫ ЭКСПЕРИМЕНТА ========== + + def _create_factors_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # Кнопка импорта из калькулятора + import_box = QGroupBox("Импорт данных") + import_layout = QHBoxLayout() + + self.import_btn = QPushButton("📥 Импортировать реагенты из калькулятора") + self.import_btn.setObjectName("success") + self.import_btn.clicked.connect(self._import_reagents_to_factors) + import_layout.addWidget(self.import_btn) + import_layout.addStretch() + import_box.setLayout(import_layout) + layout.addWidget(import_box) + + # Таблица факторов + factors_box = QGroupBox("Факторы эксперимента") + factors_layout = QVBoxLayout() + + self.factors_table = QTableWidget() + self.factors_table.setColumnCount(9) + self.factors_table.setHorizontalHeaderLabels([ + "Фактор", "%", "Разбавление", "Нулевой уровень", "Шаг", + "Тип шага", "Верхний (+1)", "Нижний (-1)", "Ед. изм." + ]) + self.factors_table.setAlternatingRowColors(True) + + factors_layout.addWidget(self.factors_table) + + btn_layout = QHBoxLayout() + add_factor_btn = QPushButton("➕ Добавить фактор") + add_factor_btn.clicked.connect(self._add_factor_row) + remove_factor_btn = QPushButton("➖ Удалить фактор") + remove_factor_btn.setObjectName("danger") + remove_factor_btn.clicked.connect(self._remove_factor_row) + + btn_layout.addWidget(add_factor_btn) + btn_layout.addWidget(remove_factor_btn) + btn_layout.addStretch() + + factors_layout.addLayout(btn_layout) + factors_box.setLayout(factors_layout) + layout.addWidget(factors_box) + + # Настройки эксперимента + settings_box = QGroupBox("Настройки эксперимента") + settings_layout = QHBoxLayout() + + settings_layout.addWidget(QLabel("Центральных точек:")) + self.center_points_spin = QSpinBox() + self.center_points_spin.setRange(0, 10) + self.center_points_spin.setValue(3) + settings_layout.addWidget(self.center_points_spin) + settings_layout.addSpacing(20) + + self.randomize_check = QCheckBox("Рэндомизировать порядок") + self.randomize_check.setChecked(True) + settings_layout.addWidget(self.randomize_check) + settings_layout.addStretch() + + settings_box.setLayout(settings_layout) + layout.addWidget(settings_box) + + # Кнопка генерации + generate_btn = QPushButton("🎲 Сгенерировать план эксперимента") + generate_btn.clicked.connect(self._generate_design) + layout.addWidget(generate_btn) + + # Добавляем начальный фактор + self._add_factor_row() + + return tab + + def _add_factor_row(self): + row = self.factors_table.rowCount() + self.factors_table.insertRow(row) + self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}")) + self.factors_table.setItem(row, 1, QTableWidgetItem("0")) + self.factors_table.setItem(row, 2, QTableWidgetItem("1")) + self.factors_table.setItem(row, 3, QTableWidgetItem("0")) + self.factors_table.setItem(row, 4, QTableWidgetItem("1")) + + step_type_combo = QComboBox() + step_type_combo.addItems(["абс", "%"]) + self.factors_table.setCellWidget(row, 5, step_type_combo) + + high_item = QTableWidgetItem("1") + high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable) + high_item.setBackground(QColor(240, 240, 240)) + self.factors_table.setItem(row, 6, high_item) + + low_item = QTableWidgetItem("-1") + low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable) + low_item.setBackground(QColor(240, 240, 240)) + self.factors_table.setItem(row, 7, low_item) + + self.factors_table.setItem(row, 8, QTableWidgetItem("")) + + def _remove_factor_row(self): + for row in sorted(set(i.row() for i in self.factors_table.selectedItems()), reverse=True): + if row >= 0: + self.factors_table.removeRow(row) + + def _import_reagents_to_factors(self): + """Импортирует реагенты из калькулятора как факторы""" + if not hasattr(self, 'last_medium_result') or self.last_medium_result is None: + QMessageBox.warning(self, "Предупреждение", + "Сначала выполните расчёт в калькуляторе сред!") + return + + result = self.last_medium_result + + # Очищаем таблицу факторов + self.factors_table.setRowCount(0) + + for reagent in result['reagents']: + row = self.factors_table.rowCount() + self.factors_table.insertRow(row) + + name = reagent['name'] + if reagent.get('dilution_factor', 1.0) != 1.0: + name += f" (разб. ×{reagent['dilution_factor']:.2f})" + + self.factors_table.setItem(row, 0, QTableWidgetItem(name)) + self.factors_table.setItem(row, 1, QTableWidgetItem(f"{reagent['percentage']:.2f}")) + self.factors_table.setItem(row, 2, QTableWidgetItem(f"{reagent.get('dilution_factor', 1.0):.3f}")) + + # Нулевой уровень - исходное количество (неразбавленное) + center = reagent.get('undiluted_amount', reagent['calculated_amount']) + self.factors_table.setItem(row, 3, QTableWidgetItem(self._format_number(center))) + + # Шаг - 10% от нулевого уровня + step = center * 0.1 + self.factors_table.setItem(row, 4, QTableWidgetItem(self._format_number(step))) + + # Тип шага - абсолютный + step_combo = self.factors_table.cellWidget(row, 5) + if step_combo: + step_combo.setCurrentText("абс") + + # Верхний и нижний уровни + self.factors_table.setItem(row, 6, QTableWidgetItem(self._format_number(center + step))) + self.factors_table.setItem(row, 7, QTableWidgetItem(self._format_number(center - step))) + self.factors_table.setItem(row, 8, QTableWidgetItem(result['total_unit'])) + + QMessageBox.information(self, "Успех", + f"Импортировано {len(result['reagents'])} факторов из калькулятора сред") + + def _get_factors_from_table(self) -> List[Dict]: + """Собирает данные факторов из таблицы""" + factors = [] + for row in range(self.factors_table.rowCount()): + try: + name_item = self.factors_table.item(row, 0) + center_item = self.factors_table.item(row, 3) + step_item = self.factors_table.item(row, 4) + high_item = self.factors_table.item(row, 6) + low_item = self.factors_table.item(row, 7) + unit_item = self.factors_table.item(row, 8) + step_combo = self.factors_table.cellWidget(row, 5) + + if not all([name_item, center_item, high_item, low_item]): + continue + + center_text = center_item.text().strip() + high_text = high_item.text().strip() + low_text = low_item.text().strip() + + if not center_text or not high_text or not low_text: + continue + + factor = { + 'name': name_item.text(), + 'center': float(center_text), + 'high': float(high_text), + 'low': float(low_text), + 'unit': unit_item.text() if unit_item else "", + 'step': float(step_item.text()) if step_item and step_item.text() else 0, + 'step_type': step_combo.currentText() if step_combo else "абс" + } + factors.append(factor) + except (ValueError, AttributeError) as e: + print(f"Ошибка в строке {row}: {e}") + continue + + return factors + + # ========== ВКЛАДКА 3: МАТРИЦА ПЛАНИРОВАНИЯ ========== + + def _create_design_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # Скролл-область для большой матрицы + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll_widget = QWidget() + scroll_layout = QVBoxLayout(scroll_widget) + + self.design_matrix = QTableWidget() + scroll_layout.addWidget(self.design_matrix) + scroll.setWidget(scroll_widget) + layout.addWidget(scroll) + + btn_layout = QHBoxLayout() + self.export_csv_btn = QPushButton("📊 Экспорт в CSV") + self.export_csv_btn.clicked.connect(self._export_design_csv) + self.export_csv_btn.setEnabled(False) + btn_layout.addWidget(self.export_csv_btn) + btn_layout.addStretch() + layout.addLayout(btn_layout) + + self.design_info = QLabel("") + self.design_info.setStyleSheet("color: #666; padding: 5px;") + layout.addWidget(self.design_info) + + return tab + + def _generate_design(self): + """Генерирует план эксперимента""" + factors = self._get_factors_from_table() + + if len(factors) == 0: + QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!") + return + + try: + design = generate_factorial_design( + factors=factors, + center_points=self.center_points_spin.value(), + randomize=self.randomize_check.isChecked() + ) + + self.generated_design = design + + n_exp = len(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(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() + + 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} опытов)") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка генерации плана: {str(e)}") + + def _setup_results_table(self, n_experiments: int): + """Настраивает таблицу результатов для ввода данных""" + # Получаем отклики (для простоты используем один отклик) + self.results_table.setRowCount(n_experiments) + self.results_table.setColumnCount(2) + self.results_table.setHorizontalHeaderLabels(["№ опыта", "Результат"]) + + for i in range(n_experiments): + self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1))) + + def _export_design_csv(self): + """Экспортирует матрицу планирования в CSV""" + if not self.generated_design or self.design_matrix.rowCount() == 0: + QMessageBox.warning(self, "Предупреждение", "Нет данных для экспорта") + return + + filename, _ = QFileDialog.getSaveFileName( + self, "Сохранить план", "", "CSV (*.csv);;Все файлы (*)" + ) + if filename: + if not filename.endswith('.csv'): + filename += '.csv' + + try: + import csv + with open(filename, 'w', newline='', encoding='utf-8-sig') as f: + writer = csv.writer(f) + # Заголовки + headers = [] + for j in range(self.design_matrix.columnCount()): + header = self.design_matrix.horizontalHeaderItem(j) + headers.append(header.text() if header else f"Колонка_{j+1}") + writer.writerow(headers) + + # Данные + for i in range(self.design_matrix.rowCount()): + row = [] + for j in range(self.design_matrix.columnCount()): + item = self.design_matrix.item(i, j) + row.append(item.text() if item else "") + writer.writerow(row) + + QMessageBox.information(self, "Успех", f"План сохранён в {filename}") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить: {str(e)}") + + # ========== ВКЛАДКА 4: АНАЛИЗ ========== + + def _create_analysis_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # Таблица для ввода результатов + results_box = QGroupBox("Результаты экспериментов") + results_layout = QVBoxLayout() + self.results_table = QTableWidget() + results_layout.addWidget(self.results_table) + results_box.setLayout(results_layout) + layout.addWidget(results_box) + + # Кнопка анализа + self.analyze_btn = QPushButton("📈 Провести анализ") + self.analyze_btn.clicked.connect(self._perform_analysis) + layout.addWidget(self.analyze_btn) + + # Вывод анализа + self.analysis_text = QTextEdit() + self.analysis_text.setReadOnly(True) + self.analysis_text.setMaximumHeight(250) + layout.addWidget(self.analysis_text) + + return tab + + def _perform_analysis(self): + """Выполняет анализ результатов эксперимента""" + if not self.generated_design: + QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") + return + + # Получаем отклики + responses = [{'name': 'Отклик', 'unit': ''}] + + # Собираем результаты + results = [] + for i in range(self.results_table.rowCount()): + item = self.results_table.item(i, 1) + if item and item.text(): + try: + results.append([float(item.text())]) + except ValueError: + QMessageBox.warning(self, "Ошибка", + f"Неверное значение в опыте {i+1}") + return + else: + QMessageBox.warning(self, "Ошибка", + f"Не введены данные для опыта {i+1}") + return + + if len(results) != len(self.generated_design): + QMessageBox.warning(self, "Ошибка", + f"Введено {len(results)} результатов, ожидается {len(self.generated_design)}") + return + + try: + # Выполняем анализ + analysis = analyze_experiment(results, self.generated_design, responses) + + # Выводим результаты + self.analysis_text.clear() + self.analysis_text.append("=" * 60) + self.analysis_text.append("РЕЗУЛЬТАТЫ АНАЛИЗА ЭКСПЕРИМЕНТА") + self.analysis_text.append("=" * 60) + + for resp_name, stats in analysis.items(): + self.analysis_text.append(f"\n📊 {resp_name}") + self.analysis_text.append("-" * 40) + self.analysis_text.append(f"Среднее значение: {stats['mean']:.4f}") + self.analysis_text.append(f"Дисперсия: {stats['variance']:.4f}") + self.analysis_text.append(f"Ст. отклонение: {stats['std_dev']:.4f}") + self.analysis_text.append(f"Коэф. вариации: {stats['cv']:.2f}%") + + if stats['n_center'] > 1: + self.analysis_text.append(f"\nЦентральные точки (n={stats['n_center']}):") + self.analysis_text.append(f" Дисперсия воспроизводимости: {stats['center_variance']:.4f}") + + if stats.get('fisher_ratio'): + self.analysis_text.append(f"\nКритерий Фишера: {stats['fisher_ratio']:.4f}") + if stats.get('model_adequate'): + self.analysis_text.append("✅ Модель адекватна") + else: + self.analysis_text.append("⚠️ Модель может быть неадекватна") + + self.analysis_text.append("\n" + "=" * 60) + self.analysis_text.append("Анализ завершен") + + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка анализа: {str(e)}") + + # ========== МЕТОДЫ РАБОТЫ С JSON ========== + + def save_project_to_json(self): + """Сохраняет весь проект в JSON файл""" + if not JSON_SUPPORT: + QMessageBox.warning(self, "Предупреждение", "JSON поддержка не доступна") + return + + filename, _ = QFileDialog.getSaveFileName( + self, "Сохранить проект", "", "JSON Files (*.json);;All Files (*)" + ) + if not filename: + return + + if not filename.endswith('.json'): + filename += '.json' + + try: + project = self._collect_current_data() + project.save_to_file(filename) + QMessageBox.information(self, "Успех", f"Проект сохранён в {filename}") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить проект: {str(e)}") + + def load_project_from_json(self): + """Загружает проект из JSON файла""" + if not JSON_SUPPORT: + QMessageBox.warning(self, "Предупреждение", "JSON поддержка не доступна") + return + + filename, _ = QFileDialog.getOpenFileName( + self, "Загрузить проект", "", "JSON Files (*.json);;All Files (*)" + ) + if not filename: + return + + try: + project = ProjectData.load_from_file(filename) + self._apply_project_data(project) + QMessageBox.information(self, "Успех", f"Проект загружен из {filename}") + except FileNotFoundError: + QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить проект: {str(e)}") + + def _collect_current_data(self): + """Собирает текущие данные из всех виджетов""" + # Данные калькулятора сред + reagents = [] + for row in range(self.reagents_table.rowCount()): + name_item = self.reagents_table.item(row, 0) + percent_item = self.reagents_table.item(row, 1) + unit_widget = self.reagents_table.cellWidget(row, 2) + coeff_item = self.reagents_table.item(row, 3) + dilution_item = self.reagents_table.item(row, 4) + + if name_item and percent_item and percent_item.text(): + try: + reagent = ReagentData( + name=name_item.text(), + percentage=float(percent_item.text()), + unit=unit_widget.currentText() if unit_widget else "мг", + conversion_factor=float(coeff_item.text()) if coeff_item and coeff_item.text() else 1.0, + dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0 + ) + reagents.append(reagent) + except ValueError: + pass + + # Данные факторов эксперимента + factors = [] + for row in range(self.factors_table.rowCount()): + name_item = self.factors_table.item(row, 0) + center_item = self.factors_table.item(row, 3) + low_item = self.factors_table.item(row, 7) + high_item = self.factors_table.item(row, 6) + step_item = self.factors_table.item(row, 4) + unit_item = self.factors_table.item(row, 8) + step_combo = self.factors_table.cellWidget(row, 5) + percent_item = self.factors_table.item(row, 1) + dilution_item = self.factors_table.item(row, 2) + + if name_item and center_item and center_item.text(): + try: + factor = FactorData( + name=name_item.text(), + center=float(center_item.text()), + low=float(low_item.text()) if low_item and low_item.text() else 0, + high=float(high_item.text()) if high_item and high_item.text() else 0, + step=float(step_item.text()) if step_item and step_item.text() else 0, + step_type=step_combo.currentText() if step_combo else "абс", + unit=unit_item.text() if unit_item else "", + percentage=float(percent_item.text()) if percent_item and percent_item.text() else None, + dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None + ) + factors.append(factor) + except ValueError: + pass + + # Данные результатов (если есть) + results_data = None + if hasattr(self, 'generated_design') and self.generated_design: + results = [] + for i in range(self.results_table.rowCount()): + item = self.results_table.item(i, 1) + if item and item.text(): + try: + results.append([float(item.text())]) + except ValueError: + pass + + if results: + responses = [ResponseData(name="Отклик", unit="")] + results_data = ExperimentResultsData( + design=self.generated_design, + results=results, + responses=responses + ) + + # Создаём проект + project = create_new_project("Мой проект") + project.medium_total_volume = self.total_volume_spin.value() + project.medium_volume_unit = self.volume_unit_combo.currentText() + project.medium_solvent = self.solvent_input.text() + project.medium_reagents = reagents + project.experiment_factors = factors + project.experiment_center_points = self.center_points_spin.value() + project.experiment_randomize = self.randomize_check.isChecked() + project.experiment_results = results_data + + return project + + def _apply_project_data(self, project): + """Применяет загруженные данные к GUI""" + # Применяем данные калькулятора сред + self.total_volume_spin.setValue(project.medium_total_volume) + + index = self.volume_unit_combo.findText(project.medium_volume_unit) + if index >= 0: + self.volume_unit_combo.setCurrentIndex(index) + + self.solvent_input.setText(project.medium_solvent) + + # Очищаем и заполняем таблицу реагентов + self.reagents_table.setRowCount(0) + for reagent in project.medium_reagents: + row = self.reagents_table.rowCount() + self.reagents_table.insertRow(row) + self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name)) + self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage))) + + unit_combo = QComboBox() + unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"]) + unit_combo.setCurrentText(reagent.unit) + self.reagents_table.setCellWidget(row, 2, unit_combo) + + self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor))) + self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor))) + self.reagents_table.setItem(row, 5, QTableWidgetItem("")) + + # Применяем данные факторов эксперимента + self.factors_table.setRowCount(0) + for factor in project.experiment_factors: + row = self.factors_table.rowCount() + self.factors_table.insertRow(row) + self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name)) + + if factor.percentage is not None: + self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage))) + else: + self.factors_table.setItem(row, 1, QTableWidgetItem("0")) + + if factor.dilution_factor is not None: + self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor))) + else: + self.factors_table.setItem(row, 2, QTableWidgetItem("1")) + + self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center))) + self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step))) + + step_combo = QComboBox() + step_combo.addItems(["абс", "%"]) + step_combo.setCurrentText(factor.step_type) + self.factors_table.setCellWidget(row, 5, step_combo) + + self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high))) + self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low))) + self.factors_table.setItem(row, 8, QTableWidgetItem(factor.unit)) + + # Применяем настройки эксперимента + self.center_points_spin.setValue(project.experiment_center_points) + self.randomize_check.setChecked(project.experiment_randomize) + + # Если есть результаты, загружаем их + if project.experiment_results and project.experiment_results.design: + self.generated_design = project.experiment_results.design + # Обновляем отображение матрицы + self._refresh_design_matrix() + + # Загружаем результаты в таблицу + if project.experiment_results.results: + for i, row_results in enumerate(project.experiment_results.results): + if i < self.results_table.rowCount() and row_results: + self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0]))) + + # Переключаемся на вкладку с факторами + if self.tab_widget: + self.tab_widget.setCurrentIndex(1) + + def _refresh_design_matrix(self): + """Обновляет отображение матрицы планирования""" + if not self.generated_design: + return + + factors = self._get_factors_from_table() + 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( + f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}" + ) + self._setup_results_table(n_exp) + + # ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ========== + + def _format_number(self, value: float) -> str: + """Форматирует число для отображения""" + if value == int(value): + return str(int(value)) + # Убираем лишние нули + formatted = f"{value:.6f}".rstrip('0').rstrip('.') + if '.' in formatted and len(formatted.split('.')[1]) > 4: + formatted = f"{value:.4f}".rstrip('0').rstrip('.') + return formatted + + +def main(): + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() diff --git a/main.py b/main.py old mode 100755 new mode 100644 index 93edf39..85189b3 --- a/main.py +++ b/main.py @@ -1,16 +1,14 @@ +""" +Биохимический помощник - точка входа в приложение +""" + import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) -from PyQt5.QtWidgets import QApplication -from src.views import MainWindow +# Добавляем текущую директорию в путь +sys.path.insert(0, os.path.dirname(__file__)) -def main(): - app = QApplication(sys.argv) - app.setStyle('Fusion') - assistant = MainWindow() - assistant.show() - sys.exit(app.exec_()) +from gui import main if __name__ == "__main__": main() diff --git a/patch.sh b/patch.sh index b13148c..d99727a 100644 --- a/patch.sh +++ b/patch.sh @@ -1,961 +1,904 @@ #!/bin/bash -# Цветной вывод -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' +# Скрипт для добавления функционала сохранения/загрузки данных в JSON +# Запускать из корневой директории проекта -echo -e "${GREEN}=== Обновление программы: добавление передачи данных в DoE ===${NC}" +set -e -# Создаём бэкап -BACKUP_DIR="backup_$(date +%Y%m%d_%H%M%S)" -mkdir -p "$BACKUP_DIR" -cp -r src "$BACKUP_DIR/" 2>/dev/null -echo -e "${GREEN}✓ Бэкап создан в $BACKUP_DIR${NC}" +echo "📁 Добавление функционала сохранения/загрузки данных в JSON..." +echo "================================================================" -# Обновляем medium_view.py - добавляем кнопку -cat > src/views/medium_view.py << 'EOF' -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, - QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget, - QMessageBox, QGroupBox, QFrame, QToolTip) -from PyQt5.QtCore import Qt, QPoint -from PyQt5.QtGui import QFont, QColor +# Создаём директорию для моделей, если её нет +mkdir -p calculations/models -class MediumCalculatorWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Калькулятор питательных сред") - self.setGeometry(200, 100, 1200, 700) - self.setStyleSheet(""" - QMainWindow { background-color: #f5f5f5; } - QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; background-color: white; } - QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; } - QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 15px; border-radius: 4px; font-weight: bold; } - QPushButton:hover { background-color: #1976D2; } - QPushButton#danger { background-color: #f44336; } - QPushButton#danger:hover { background-color: #da190b; } - QPushButton#success { background-color: #4CAF50; } - QPushButton#success:hover { background-color: #45a049; } - QPushButton#doe { background-color: #9C27B0; } - QPushButton#doe:hover { background-color: #7B1FA2; } - QTableWidget { gridline-color: #ddd; background-color: white; } - QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } - """) - self._init_ui() - self.doe_window = None - - def _init_ui(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) - layout = QVBoxLayout(central_widget) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) - - title_label = QLabel("Калькулятор питательных сред") - title_label.setAlignment(Qt.AlignCenter) - title_font = QFont() - title_font.setPointSize(18) - title_font.setBold(True) - title_label.setFont(title_font) - layout.addWidget(title_label) - - params_group = QGroupBox("Параметры среды") - params_layout = QHBoxLayout() - amount_layout = QHBoxLayout() - amount_layout.addWidget(QLabel("Общее количество:")) - self.amount_input = QDoubleSpinBox() - self.amount_input.setRange(0.001, 1000000.0) - self.amount_input.setValue(1000.0) - amount_layout.addWidget(self.amount_input) - self.amount_unit_combo = QComboBox() - self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"]) - self.amount_unit_combo.setCurrentText("мл") - amount_layout.addWidget(self.amount_unit_combo) - params_layout.addLayout(amount_layout) - solvent_layout = QHBoxLayout() - solvent_layout.addWidget(QLabel("Растворитель:")) - self.solvent_input = QLineEdit("Вода") - solvent_layout.addWidget(self.solvent_input) - params_layout.addLayout(solvent_layout) - params_layout.addStretch() - params_group.setLayout(params_layout) - layout.addWidget(params_group) - - table_group = QGroupBox("Состав среды") - table_layout = QVBoxLayout() - self.table = QTableWidget() - self.table.setColumnCount(6) - self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"]) - self.table.setAlternatingRowColors(True) - self.table.setSelectionBehavior(QTableWidget.SelectRows) - self.table.setColumnWidth(0, 180) - self.table.setColumnWidth(1, 70) - self.table.setColumnWidth(2, 90) - self.table.setColumnWidth(3, 70) - self.table.setColumnWidth(4, 100) - self.table.setColumnWidth(5, 120) - table_layout.addWidget(self.table) - table_group.setLayout(table_layout) - layout.addWidget(table_group) - - btn_group = QGroupBox("Управление") - btn_layout = QHBoxLayout() - self.add_row_btn = QPushButton("➕ Добавить реагент") - self.remove_row_btn = QPushButton("➖ Удалить реагент") - self.remove_row_btn.setObjectName("danger") - btn_layout.addWidget(self.add_row_btn) - btn_layout.addWidget(self.remove_row_btn) - btn_layout.addStretch() - self.calculate_btn = QPushButton("🧮 Рассчитать") - self.calculate_btn.setObjectName("success") - self.save_btn = QPushButton("💾 Сохранить") - self.load_btn = QPushButton("📂 Загрузить") - - # Новая кнопка для передачи в DoE - self.to_doe_btn = QPushButton("🎯 В DoE") - self.to_doe_btn.setObjectName("doe") - self.to_doe_btn.setToolTip("Передать состав реагентов в планировщик эксперимента\n" - "Реагенты станут факторами, их концентрации — нулевыми уровнями") - - btn_layout.addWidget(self.to_doe_btn) - btn_layout.addWidget(self.calculate_btn) - btn_layout.addWidget(self.save_btn) - btn_layout.addWidget(self.load_btn) - btn_group.setLayout(btn_layout) - layout.addWidget(btn_group) - - info_frame = QFrame() - info_frame.setFrameShape(QFrame.StyledPanel) - info_frame.setStyleSheet("background-color: #e3f2fd; border-radius: 5px;") - info_layout = QHBoxLayout(info_frame) - info_label = QLabel("ℹ️ Подсказка: Реагенты в массовых единицах не учитываются при расчёте объёма растворителя") - info_layout.addWidget(info_label) - info_layout.addStretch() - layout.addWidget(info_frame) - - def add_initial_rows(self): - self.add_solvent_row() - self.add_new_row() - - def add_solvent_row(self): - row = self.table.rowCount() - self.table.insertRow(row) - solvent_item = QTableWidgetItem(self.solvent_input.text()) - solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable) - solvent_item.setBackground(QColor(230, 230, 230)) - font = QFont() - font.setBold(True) - solvent_item.setFont(font) - self.table.setItem(row, 0, solvent_item) - for col in [1, 3, 4]: - item = QTableWidgetItem("-") - item.setFlags(item.flags() & ~Qt.ItemIsEditable) - item.setBackground(QColor(230, 230, 230)) - self.table.setItem(row, col, item) - unit_item = QTableWidgetItem(self.amount_unit_combo.currentText()) - unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable) - unit_item.setBackground(QColor(230, 230, 230)) - self.table.setItem(row, 2, unit_item) - result_item = QTableWidgetItem("") - result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable) - self.table.setItem(row, 5, result_item) - - def update_solvent_name(self): - if self.table.rowCount() > 0 and self.table.item(0, 0): - self.table.item(0, 0).setText(self.solvent_input.text()) - - def add_new_row(self): - row = self.table.rowCount() - self.table.insertRow(row) - self.table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row}")) - self.table.setItem(row, 1, QTableWidgetItem("0")) - unit_combo = QComboBox() - unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) - unit_combo.setCurrentText("мг") - self.table.setCellWidget(row, 2, unit_combo) - self.table.setItem(row, 3, QTableWidgetItem("1")) - self.table.setItem(row, 4, QTableWidgetItem("1")) - self.table.setItem(row, 5, QTableWidgetItem("")) - - def remove_selected_row(self): - for row in sorted(set(item.row() for item in self.table.selectedItems()), reverse=True): - if row > 0: - self.table.removeRow(row) - - def get_reagents_data(self): - """Возвращает список реагентов для передачи в DoE""" - reagents = [] - for row in range(1, self.table.rowCount()): - name_item = self.table.item(row, 0) - percent_item = self.table.item(row, 1) - unit_widget = self.table.cellWidget(row, 2) - coeff_item = self.table.item(row, 3) - - if not all([name_item, percent_item]): - continue - - try: - name = name_item.text() - percentage = float(percent_item.text()) - unit = unit_widget.currentText() if unit_widget else "мг" - conversion_factor = float(coeff_item.text()) if coeff_item else 1.0 - - reagents.append({ - 'name': name, - 'percentage': percentage, - 'unit': unit, - 'conversion_factor': conversion_factor - }) - except ValueError: - continue - return reagents - - def get_table_data(self): - data = [] - for row in range(1, self.table.rowCount()): - name = self.table.item(row, 0).text() if self.table.item(row, 0) else "" - percent = self.table.item(row, 1).text() if self.table.item(row, 1) else "0" - unit_widget = self.table.cellWidget(row, 2) - unit = unit_widget.currentText() if unit_widget else "мг" - coeff = self.table.item(row, 3).text() if self.table.item(row, 3) else "1" - dilution = self.table.item(row, 4).text() if self.table.item(row, 4) else "1" - data.append([name, percent, unit, coeff, dilution]) - return data - - def update_solvent_percent(self, solvent_percent: float): - if self.table.rowCount() > 0 and self.table.item(0, 1): - self.table.item(0, 1).setText(self.format_number(solvent_percent)) - - def update_solvent_result(self, solvent_amount: float, unit: str): - if self.table.rowCount() > 0: - if self.table.item(0, 5): - self.table.item(0, 5).setText(self.format_number(solvent_amount)) - self.table.item(0, 5).setBackground(QColor(220, 255, 220)) - if self.table.item(0, 2): - self.table.item(0, 2).setText(unit) - - def update_results(self, results: list): - for row, amount in enumerate(results, start=1): - if row < self.table.rowCount() and self.table.item(row, 5): - self.table.item(row, 5).setText(self.format_number(amount)) - self.table.item(row, 5).setBackground(QColor(220, 255, 220)) - - def clear_results(self): - for row in range(self.table.rowCount()): - if self.table.item(row, 5): - self.table.item(row, 5).setText("") - if row == 0: - self.table.item(row, 5).setBackground(QColor(230, 230, 230)) - else: - self.table.item(row, 5).setBackground(QColor(250, 250, 250)) - if self.table.rowCount() > 0 and self.table.item(0, 1): - self.table.item(0, 1).setText("") - if self.table.rowCount() > 0 and self.table.item(0, 2): - self.table.item(0, 2).setText(self.amount_unit_combo.currentText()) - - def format_number(self, value): - if value == int(value): - return str(int(value)) - formatted = f"{value:.6f}".rstrip('0').rstrip('.') - if '.' in formatted and len(formatted.split('.')[1]) > 4: - formatted = f"{value:.4f}".rstrip('0').rstrip('.') - return formatted - - def show_error(self, message: str): - QMessageBox.critical(self, "Ошибка", message) - - def show_info(self, message: str): - QMessageBox.information(self, "Информация", message) -EOF +# 1. СОЗДАЁМ МОДЕЛЬ ДАННЫХ ДЛЯ СОХРАНЕНИЯ +cat > calculations/models/project_data.py << 'EOF' +""" +Модель данных проекта для сохранения/загрузки в JSON +""" -# Обновляем medium_controller.py - добавляем метод передачи в DoE -cat > src/controllers/medium_controller.py << 'EOF' -from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QColor +from typing import List, Dict, Optional +from dataclasses import dataclass, asdict +from datetime import datetime import json -from ..models.medium_model import MediumModel -from ..models.reagent import Reagent -from ..views.experiment_view import ExperimentDesignWindow -class MediumController: - def __init__(self, view): - self.model = MediumModel() - self.view = view - self.doe_window = None - self._connect_signals() - self._setup_initial_data() + +@dataclass +class ReagentData: + """Данные реагента""" + name: str + percentage: float + unit: str + conversion_factor: float + dilution_factor: float - def _connect_signals(self): - self.view.add_row_btn.clicked.connect(self.add_reagent_row) - self.view.remove_row_btn.clicked.connect(self.remove_reagent_row) - self.view.calculate_btn.clicked.connect(self._perform_calculation) - self.view.save_btn.clicked.connect(self.save_composition) - self.view.load_btn.clicked.connect(self.load_composition) - self.view.to_doe_btn.clicked.connect(self.send_to_doe) - self.view.solvent_input.textChanged.connect(self.view.update_solvent_name) + def to_dict(self) -> Dict: + return asdict(self) - def _setup_initial_data(self): - self.view.add_initial_rows() + @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 add_reagent_row(self): - self.view.add_new_row() + def to_dict(self) -> Dict: + d = asdict(self) + # Удаляем None значения + return {k: v for k, v in d.items() if v is not None} - def remove_reagent_row(self): - self.view.remove_selected_row() + @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 _perform_calculation(self): - try: - self._update_model_from_view() - results, solvent_amount, solvent_percentage = self.model.calculate_amounts() - self.view.update_results(results) - self.view.update_solvent_result(solvent_amount, self.model.amount_unit) - self.view.update_solvent_percent(solvent_percentage) - except ValueError as e: - self.view.show_error(f"Ошибка в данных: {str(e)}") - except Exception as e: - self.view.show_error(f"Неожиданная ошибка: {str(e)}") + def to_dict(self) -> Dict: + return asdict(self) - def _update_model_from_view(self): - self.model.reagents.clear() - self.model.total_amount = self.view.amount_input.value() - self.model.amount_unit = self.view.amount_unit_combo.currentText() - self.model.solvent = self.view.solvent_input.text() - for row in range(1, self.view.table.rowCount()): - name_item = self.view.table.item(row, 0) - percentage_item = self.view.table.item(row, 1) - unit_widget = self.view.table.cellWidget(row, 2) - conversion_item = self.view.table.item(row, 3) - dilution_item = self.view.table.item(row, 4) - if not all([name_item, percentage_item, conversion_item]): - continue - try: - name = name_item.text() - percentage = float(percentage_item.text()) - unit = unit_widget.currentText() if unit_widget else "мг" - conversion_factor = float(conversion_item.text()) - dilution_factor = float(dilution_item.text()) if dilution_item else 1.0 - reagent = Reagent(name, percentage, unit, conversion_factor) - reagent.dilution_factor = dilution_factor - self.model.reagents.append(reagent) - except ValueError as e: - raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}") + @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 send_to_doe(self): - """Передаёт данные о реагентах в окно планирования эксперимента""" - reagents = self.view.get_reagents_data() + 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', {}) - if len(reagents) == 0: - self.view.show_error("Нет реагентов для передачи в планировщик эксперимента!") - return - - # Создаём или показываем существующее окно DoE - if self.doe_window is None: - self.doe_window = ExperimentDesignWindow() - - # Передаём данные в окно DoE - self.doe_window.load_factors_from_reagents(reagents) - self.doe_window.show() - self.doe_window.raise_() - self.doe_window.activateWindow() - - self.view.show_info(f"Передано {len(reagents)} реагентов в планировщик эксперимента\n" - f"Каждый реагент добавлен как фактор.\n" - f"Его концентрация (%) установлена как нулевой уровень.") + # Создаём объект + 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_composition(self): - filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)") - if filename: - if not filename.lower().endswith('.json'): - filename += '.json' - try: - self._update_model_from_view() - self.model.save_to_file(filename) - QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!") - except Exception as e: - self.view.show_error(f"Ошибка сохранения: {str(e)}") + 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) - def load_composition(self): - filename, _ = QFileDialog.getOpenFileName(self.view, "Загрузить состав среды", "", "JSON Files (*.json);;All Files (*)") - if filename: - try: - self.model.load_from_file(filename) - self._update_view_from_model() - QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен") - except FileNotFoundError: - QMessageBox.critical(self.view, "Ошибка", f"Файл не найден: {filename}") - except json.JSONDecodeError as e: - QMessageBox.critical(self.view, "Ошибка", f"Неверный формат JSON-файла: {str(e)}") - except Exception as e: - QMessageBox.critical(self.view, "Ошибка", f"Ошибка при загрузке состава: {str(e)}") - - def _update_view_from_model(self): - while self.view.table.rowCount() > 1: - self.view.table.removeRow(1) - if self.view.table.rowCount() == 0: - self.view.add_solvent_row() - self.view.amount_input.setValue(self.model.total_amount) - index = self.view.amount_unit_combo.findText(self.model.amount_unit) - if index >= 0: - self.view.amount_unit_combo.setCurrentIndex(index) - self.view.solvent_input.setText(self.model.solvent) - self.view.update_solvent_name() - for reagent in self.model.reagents: - row = self.view.table.rowCount() - self.view.table.insertRow(row) - self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name)) - self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}")) - unit_combo = QComboBox() - unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) - unit_combo.setCurrentText(reagent.unit) - self.view.table.setCellWidget(row, 2, unit_combo) - self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}")) - self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}")) - self.view.table.setItem(row, 5, QTableWidgetItem("")) - self.view.clear_results() + @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 + ) EOF -# Обновляем experiment_view.py - добавляем метод загрузки факторов из реагентов -cat > src/views/experiment_view.py << 'EOF' -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, - QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox, - QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, QTextEdit, - QTabWidget, QFormLayout, QCheckBox, QScrollArea, QFileDialog) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QColor, QFont -import csv -import random -import numpy as np +echo "✅ Создан calculations/models/project_data.py" -class ExperimentDesignWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика") - self.setGeometry(200, 100, 1200, 800) - self.setStyleSheet(""" - QMainWindow { background-color: #f5f5f5; } - QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; } - QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; } - QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; } - QPushButton:hover { background-color: #45a049; } - QPushButton#danger { background-color: #f44336; } - QPushButton#danger:hover { background-color: #da190b; } - QPushButton#doe { background-color: #9C27B0; } - QPushButton#doe:hover { background-color: #7B1FA2; } - QTableWidget { gridline-color: #ddd; } - QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } - """) - self._init_ui() - - def _init_ui(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) - layout = QVBoxLayout(central_widget) - - title_label = QLabel("Планирование полнофакторного эксперимента (DoE)") - title_font = QFont() - title_font.setPointSize(18) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet("color: #2E7D32;") - layout.addWidget(title_label) - - tabs = QTabWidget() - - # Вкладка параметров - params_tab = QWidget() - params_layout = QVBoxLayout(params_tab) - - factors_group = QGroupBox("Факторы эксперимента (независимые переменные)") - factors_layout = QVBoxLayout() - - info_label = QLabel("Определите факторы, которые влияют на ваш эксперимент:") - info_label.setStyleSheet("color: #555; font-weight: normal;") - factors_layout.addWidget(info_label) - - self.factors_table = QTableWidget() - self.factors_table.setColumnCount(6) - self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг", - "Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"]) - self.factors_table.setRowCount(2) - - sample_factors = [["Температура", "31", "6", "37", "25", "°C"], ["pH", "7.0", "0.5", "7.5", "6.5", ""]] - for i, factor in enumerate(sample_factors): - for j, value in enumerate(factor): - item = QTableWidgetItem(value) - if j in [3, 4]: - item.setFlags(item.flags() & ~Qt.ItemIsEditable) - item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(i, j, item) - - self.factors_table.setColumnWidth(0, 150) - self.factors_table.setColumnWidth(1, 120) - self.factors_table.setColumnWidth(2, 80) - self.factors_table.setColumnWidth(3, 120) - self.factors_table.setColumnWidth(4, 120) - self.factors_table.setColumnWidth(5, 100) - - self.factors_table.itemChanged.connect(self.on_factor_changed) - factors_layout.addWidget(self.factors_table) - - factor_buttons = QHBoxLayout() - add_factor_btn = QPushButton("+ Добавить фактор") - add_factor_btn.clicked.connect(self.add_factor_row) - remove_factor_btn = QPushButton("- Удалить последний") - remove_factor_btn.clicked.connect(self.remove_factor_row) - factor_buttons.addWidget(add_factor_btn) - factor_buttons.addWidget(remove_factor_btn) - factor_buttons.addStretch() - factors_layout.addLayout(factor_buttons) - factors_group.setLayout(factors_layout) - params_layout.addWidget(factors_group) - - settings_group = QGroupBox("Настройки эксперимента") - settings_layout = QHBoxLayout() - center_layout = QHBoxLayout() - center_layout.addWidget(QLabel("Количество центральных точек:")) - self.center_points_spin = QSpinBox() - self.center_points_spin.setRange(0, 10) - self.center_points_spin.setValue(3) - center_layout.addWidget(self.center_points_spin) - settings_layout.addLayout(center_layout) - self.randomize_check = QCheckBox("Рэндомизировать порядок опытов") - self.randomize_check.setChecked(True) - settings_layout.addWidget(self.randomize_check) - settings_layout.addStretch() - settings_group.setLayout(settings_layout) - params_layout.addWidget(settings_group) - - responses_group = QGroupBox("Отклики (зависимые переменные)") - responses_layout = QVBoxLayout() - self.responses_table = QTableWidget() - self.responses_table.setColumnCount(2) - self.responses_table.setHorizontalHeaderLabels(["Отклик", "Единица измерения"]) - self.responses_table.setRowCount(2) - sample_responses = [["Оптическая плотность (OD600)", ""], ["Концентрация целевого продукта", "мг/мл"]] - for i, response in enumerate(sample_responses): - for j, value in enumerate(response): - self.responses_table.setItem(i, j, QTableWidgetItem(value)) - responses_layout.addWidget(self.responses_table) - - response_buttons = QHBoxLayout() - add_response_btn = QPushButton("+ Добавить отклик") - add_response_btn.clicked.connect(self.add_response_row) - remove_response_btn = QPushButton("- Удалить последний") - remove_response_btn.clicked.connect(self.remove_response_row) - response_buttons.addWidget(add_response_btn) - response_buttons.addWidget(remove_response_btn) - response_buttons.addStretch() - responses_layout.addLayout(response_buttons) - responses_group.setLayout(responses_layout) - params_layout.addWidget(responses_group) - tabs.addTab(params_tab, "📝 Параметры эксперимента") - - # Вкладка матрицы планирования - plan_tab = QWidget() - plan_layout = QVBoxLayout(plan_tab) - plan_info = QLabel("Полнофакторный план эксперимента с центральными точками") - plan_info.setAlignment(Qt.AlignCenter) - plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") - plan_layout.addWidget(plan_info) - - scroll = QScrollArea() - scroll.setWidgetResizable(True) - matrix_widget = QWidget() - matrix_layout = QVBoxLayout(matrix_widget) - self.design_matrix = QTableWidget() - matrix_layout.addWidget(self.design_matrix) - scroll.setWidget(matrix_widget) - plan_layout.addWidget(scroll) - - buttons_layout = QHBoxLayout() - generate_btn = QPushButton("Сгенерировать план эксперимента") - generate_btn.clicked.connect(self.generate_design_matrix) - buttons_layout.addWidget(generate_btn) - export_btn = QPushButton("📊 Экспорт в CSV") - export_btn.clicked.connect(self.export_to_csv) - buttons_layout.addWidget(export_btn) - buttons_layout.addStretch() - plan_layout.addLayout(buttons_layout) - - self.plan_info_label = QLabel("") - self.plan_info_label.setStyleSheet("color: #666; font-size: 12px; padding: 5px;") - plan_layout.addWidget(self.plan_info_label) - tabs.addTab(plan_tab, "📊 Матрица планирования") - - # Вкладка анализа - analysis_tab = QWidget() - analysis_layout = QVBoxLayout(analysis_tab) - analysis_info = QLabel("Введите результаты экспериментов для анализа") - analysis_info.setAlignment(Qt.AlignCenter) - analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") - analysis_layout.addWidget(analysis_info) - self.results_table = QTableWidget() - analysis_layout.addWidget(self.results_table) - analyze_btn = QPushButton("Провести регрессионный анализ") - analyze_btn.clicked.connect(self.perform_analysis) - analysis_layout.addWidget(analyze_btn) - self.analysis_output = QTextEdit() - self.analysis_output.setReadOnly(True) - self.analysis_output.setMaximumHeight(200) - analysis_layout.addWidget(self.analysis_output) - tabs.addTab(analysis_tab, "📈 Анализ результатов") - - layout.addWidget(tabs) - - btn_layout = QHBoxLayout() - close_btn = QPushButton("Закрыть") - close_btn.clicked.connect(self.close) - btn_layout.addStretch() - btn_layout.addWidget(close_btn) - layout.addLayout(btn_layout) - - self.generated_design = None - self.factors_data = None - - def load_factors_from_reagents(self, reagents): - """Загружает факторы из списка реагентов калькулятора""" - if not reagents: +# 2. ДОБАВЛЯЕМ ФУНКЦИИ В GUI ДЛЯ РАБОТЫ С JSON +# Создаём патч для gui.py +cat > gui_patch.py << 'EOF' +# ДОБАВИТЬ В gui.py ПОСЛЕ ИМПОРТОВ: + +from calculations.models.project_data import ( + ProjectData, ReagentData, FactorData, + ResponseData, ExperimentResultsData, create_new_project +) + +# ДОБАВИТЬ В КЛАСС MainWindow НОВЫЕ МЕТОДЫ: + + def save_project_to_json(self): + """Сохраняет весь проект в JSON файл""" + filename, _ = QFileDialog.getSaveFileName( + self, "Сохранить проект", "", "JSON Files (*.json);;All Files (*)" + ) + if not filename: return - # Очищаем таблицу факторов - self.factors_table.setRowCount(0) + if not filename.endswith('.json'): + filename += '.json' - # Добавляем каждый реагент как фактор - for reagent in reagents: - row = self.factors_table.rowCount() - self.factors_table.insertRow(row) - - # Название фактора = название реагента - self.factors_table.setItem(row, 0, QTableWidgetItem(reagent['name'])) - - # Нулевой уровень = процентное содержание - percentage = reagent['percentage'] - self.factors_table.setItem(row, 1, QTableWidgetItem(f"{percentage:.2f}")) - - # Шаг = 10% от нулевого уровня (или 1, если нулевой уровень 0) - step = max(percentage * 0.1, 1.0) if percentage > 0 else 1.0 - self.factors_table.setItem(row, 2, QTableWidgetItem(f"{step:.2f}")) - - # Единица измерения - unit = reagent['unit'] - # Преобразуем единицы измерения в понятные для факторов - if unit in ['нг', 'мкг', 'мг', 'г', 'кг']: - display_unit = unit - elif unit in ['нл', 'мкл', 'мл', 'л']: - display_unit = unit - else: - display_unit = "%" - self.factors_table.setItem(row, 5, QTableWidgetItem(display_unit)) - - # Верхний и нижний уровни (вычисляются автоматически) - high = percentage + step - low = percentage - step - - high_item = QTableWidgetItem(f"{high:.2f}") - high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable) - high_item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(row, 3, high_item) - - low_item = QTableWidgetItem(f"{low:.2f}") - low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable) - low_item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(row, 4, low_item) - - # Показываем сообщение об успешной загрузке - QMessageBox.information( - self, - "Загрузка из калькулятора", - f"Загружено {len(reagents)} факторов из калькулятора питательных сред.\n\n" - f"Каждый реагент стал фактором.\n" - f"Нулевой уровень = концентрация реагента (%)\n" - f"Шаг = 10% от нулевого уровня\n\n" - f"При необходимости отредактируйте шаг и единицы измерения." + try: + # Собираем данные из GUI + project = self._collect_current_data() + project.save_to_file(filename) + QMessageBox.information(self, "Успех", f"Проект сохранён в {filename}") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить проект: {str(e)}") + + def load_project_from_json(self): + """Загружает проект из JSON файла""" + filename, _ = QFileDialog.getOpenFileName( + self, "Загрузить проект", "", "JSON Files (*.json);;All Files (*)" ) + if not filename: + return + + try: + project = ProjectData.load_from_file(filename) + self._apply_project_data(project) + QMessageBox.information(self, "Успех", f"Проект загружен из {filename}") + except FileNotFoundError: + QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить проект: {str(e)}") - def on_factor_changed(self, item): - row = item.row() - col = item.column() - if col in [1, 2]: - try: - center = float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0 - step = float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0 - high = center + step - low = center - step - self.factors_table.blockSignals(True) - if self.factors_table.item(row, 3): - self.factors_table.item(row, 3).setText(f"{high:.3f}".rstrip('0').rstrip('.')) - if self.factors_table.item(row, 4): - self.factors_table.item(row, 4).setText(f"{low:.3f}".rstrip('0').rstrip('.')) - self.factors_table.blockSignals(False) - except ValueError: - pass - - def add_factor_row(self): - row = self.factors_table.rowCount() - self.factors_table.insertRow(row) - self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}")) - self.factors_table.setItem(row, 1, QTableWidgetItem("0")) - self.factors_table.setItem(row, 2, QTableWidgetItem("1")) - high_item = QTableWidgetItem("1") - high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable) - high_item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(row, 3, high_item) - low_item = QTableWidgetItem("-1") - low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable) - low_item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(row, 4, low_item) - self.factors_table.setItem(row, 5, QTableWidgetItem("")) - - def remove_factor_row(self): - if self.factors_table.rowCount() > 1: - self.factors_table.removeRow(self.factors_table.rowCount() - 1) - - def add_response_row(self): - row = self.responses_table.rowCount() - self.responses_table.insertRow(row) - self.responses_table.setItem(row, 0, QTableWidgetItem(f"Отклик_{row+1}")) - self.responses_table.setItem(row, 1, QTableWidgetItem("")) - - def remove_response_row(self): - if self.responses_table.rowCount() > 1: - self.responses_table.removeRow(self.responses_table.rowCount() - 1) - - def get_factors_data(self): + def _collect_current_data(self) -> ProjectData: + """Собирает текущие данные из всех виджетов""" + # Данные калькулятора сред + reagents = [] + for row in range(self.reagents_table.rowCount()): + name_item = self.reagents_table.item(row, 0) + percent_item = self.reagents_table.item(row, 1) + unit_widget = self.reagents_table.cellWidget(row, 2) + coeff_item = self.reagents_table.item(row, 3) + dilution_item = self.reagents_table.item(row, 4) + + if name_item and percent_item: + try: + reagent = ReagentData( + name=name_item.text(), + percentage=float(percent_item.text()) if percent_item.text() else 0, + unit=unit_widget.currentText() if unit_widget else "мг", + conversion_factor=float(coeff_item.text()) if coeff_item and coeff_item.text() else 1.0, + dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0 + ) + reagents.append(reagent) + except ValueError: + pass + + # Данные факторов эксперимента factors = [] for row in range(self.factors_table.rowCount()): - try: - factor = { - 'name': self.factors_table.item(row, 0).text() if self.factors_table.item(row, 0) else "", - 'center': float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0, - 'step': float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0, - 'high': float(self.factors_table.item(row, 3).text()) if self.factors_table.item(row, 3) else 0, - 'low': float(self.factors_table.item(row, 4).text()) if self.factors_table.item(row, 4) else 0, - 'unit': self.factors_table.item(row, 5).text() if self.factors_table.item(row, 5) else "" - } - factors.append(factor) - except (ValueError, AttributeError): - continue - return factors - - def calculate_factorial_design(self, factors): - k = len(factors) - if k == 0: - return [] - n_factorial = 2 ** k - design = [] - for i in range(n_factorial): - experiment = {} - for j in range(k): - 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]['unit'] - } - design.append(experiment) + name_item = self.factors_table.item(row, 0) + center_item = self.factors_table.item(row, 3) + low_item = self.factors_table.item(row, 7) + high_item = self.factors_table.item(row, 6) + step_item = self.factors_table.item(row, 4) + unit_item = self.factors_table.item(row, 8) + step_combo = self.factors_table.cellWidget(row, 5) + percent_item = self.factors_table.item(row, 1) + dilution_item = self.factors_table.item(row, 2) + + if name_item and center_item: + try: + factor = FactorData( + name=name_item.text(), + center=float(center_item.text()) if center_item.text() else 0, + low=float(low_item.text()) if low_item and low_item.text() else 0, + high=float(high_item.text()) if high_item and high_item.text() else 0, + step=float(step_item.text()) if step_item and step_item.text() else 0, + step_type=step_combo.currentText() if step_combo else "абс", + unit=unit_item.text() if unit_item else "", + percentage=float(percent_item.text()) if percent_item and percent_item.text() else None, + dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None + ) + factors.append(factor) + except ValueError: + pass - n_center = self.center_points_spin.value() - for i in range(n_center): - 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]['unit'] - } - center_experiment['is_center'] = True - center_experiment['center_num'] = i + 1 - design.append(center_experiment) + # Данные результатов (если есть) + results_data = None + if hasattr(self, 'generated_design') and self.generated_design: + results = [] + for i in range(self.results_table.rowCount()): + item = self.results_table.item(i, 1) + if item and item.text(): + try: + results.append([float(item.text())]) + except ValueError: + pass + + if results: + responses = [ResponseData(name="Отклик", unit="")] + results_data = ExperimentResultsData( + design=self.generated_design, + results=results, + responses=responses + ) - if self.randomize_check.isChecked(): - random.shuffle(design) - return design + # Создаём проект + project = create_new_project("Мой проект") + project.medium_total_volume = self.total_volume_spin.value() + project.medium_volume_unit = self.volume_unit_combo.currentText() + project.medium_solvent = self.solvent_input.text() + project.medium_reagents = reagents + project.experiment_factors = factors + project.experiment_center_points = self.center_points_spin.value() + project.experiment_randomize = self.randomize_check.isChecked() + project.experiment_results = results_data + + return project - def generate_design_matrix(self): - factors = self.get_factors_data() - if len(factors) == 0: - QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!") + def _apply_project_data(self, project: ProjectData): + """Применяет загруженные данные к GUI""" + # Применяем данные калькулятора сред + self.total_volume_spin.setValue(project.medium_total_volume) + + index = self.volume_unit_combo.findText(project.medium_volume_unit) + if index >= 0: + self.volume_unit_combo.setCurrentIndex(index) + + self.solvent_input.setText(project.medium_solvent) + + # Очищаем и заполняем таблицу реагентов + self.reagents_table.setRowCount(0) + for reagent in project.medium_reagents: + row = self.reagents_table.rowCount() + self.reagents_table.insertRow(row) + self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name)) + self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage))) + + unit_combo = QComboBox() + unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"]) + unit_combo.setCurrentText(reagent.unit) + self.reagents_table.setCellWidget(row, 2, unit_combo) + + self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor))) + self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor))) + self.reagents_table.setItem(row, 5, QTableWidgetItem("")) + + # Применяем данные факторов эксперимента + self.factors_table.setRowCount(0) + for factor in project.experiment_factors: + row = self.factors_table.rowCount() + self.factors_table.insertRow(row) + self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name)) + + if factor.percentage is not None: + self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage))) + else: + self.factors_table.setItem(row, 1, QTableWidgetItem("0")) + + if factor.dilution_factor is not None: + self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor))) + else: + self.factors_table.setItem(row, 2, QTableWidgetItem("1")) + + self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center))) + self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step))) + + step_combo = QComboBox() + step_combo.addItems(["абс", "%"]) + step_combo.setCurrentText(factor.step_type) + self.factors_table.setCellWidget(row, 5, step_combo) + + self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high))) + self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low))) + self.factors_table.setItem(row, 8, QTableWidgetItem(factor.unit)) + + # Применяем настройки эксперимента + self.center_points_spin.setValue(project.experiment_center_points) + self.randomize_check.setChecked(project.experiment_randomize) + + # Если есть результаты, загружаем их + if project.experiment_results and project.experiment_results.design: + self.generated_design = project.experiment_results.design + self._display_design_matrix() + + # Загружаем результаты в таблицу + if project.experiment_results.results: + for i, row_results in enumerate(project.experiment_results.results): + if i < self.results_table.rowCount() and row_results: + self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0]))) + + # Переключаемся на вкладку с факторами + self.tab_widget.setCurrentIndex(1) + + def _display_design_matrix(self): + """Отображает матрицу планирования (добавить в _generate_design или отдельно)""" + if not self.generated_design: return - self.factors_data = factors - design = self.calculate_factorial_design(factors) - self.generated_design = design + n_exp = len(self.generated_design) + n_factors = len(self._get_factors_from_table()) - n_experiments = len(design) - n_factors = len(factors) - - self.design_matrix.setRowCount(n_experiments) + self.design_matrix.setRowCount(n_exp) self.design_matrix.setColumnCount(n_factors + 2) - headers = ["№ опыта"] + [f["name"] for f in factors] + ["Тип точки"] + headers = ["№"] + [f['name'] for f in self._get_factors_from_table()] + ["Тип"] self.design_matrix.setHorizontalHeaderLabels(headers) - for exp_idx, experiment in enumerate(design): + for exp_idx, exp in enumerate(self.generated_design): self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1))) - for factor_idx in range(n_factors): - factor_key = f"Фактор_{factor_idx + 1}" - value = experiment[factor_key]['natural'] - unit = factors[factor_idx]['unit'] - if isinstance(value, float): - if value == int(value): - display_value = str(int(value)) - else: - display_value = f"{value:.3f}".rstrip('0').rstrip('.') - else: - display_value = str(value) - if unit: - display_value += f" {unit}" - item = QTableWidgetItem(display_value) - if experiment.get('is_center', False): - item.setBackground(QColor(255, 255, 200)) - self.design_matrix.setItem(exp_idx, factor_idx + 1, item) - if experiment.get('is_center', False): - type_item = QTableWidgetItem(f"Центральная #{experiment['center_num']}") + for f_idx in range(n_factors): + key = f"Фактор_{f_idx + 1}" + if key not in exp: + continue + value = exp[key]['natural'] + unit = self._get_factors_from_table()[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: - levels = [] - for factor_idx in range(n_factors): - factor_key = f"Фактор_{factor_idx + 1}" - coded = experiment[factor_key]['coded'] - levels.append("+" if coded == 1 else "-") - type_item = QTableWidgetItem(f"Факторная ({''.join(levels)})") + type_item = QTableWidgetItem("Факторная") self.design_matrix.setItem(exp_idx, n_factors + 1, type_item) self.design_matrix.resizeColumnsToContents() - n_factorial = 2 ** n_factors - n_center = self.center_points_spin.value() - self.plan_info_label.setText(f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов") - self.setup_results_table(n_experiments) - QMessageBox.information(self, "Успех", f"Сгенерирован план для {n_factors} факторов\nФакторных точек: {n_factorial}\nЦентральных точек: {n_center}\nВсего опытов: {n_experiments}") - - def setup_results_table(self, n_experiments): - n_responses = self.responses_table.rowCount() - self.results_table.setRowCount(n_experiments) - self.results_table.setColumnCount(n_responses + 1) - headers = ["№ опыта"] + [self.responses_table.item(i, 0).text() if self.responses_table.item(i, 0) else f"Отклик_{i+1}" for i in range(n_responses)] - self.results_table.setHorizontalHeaderLabels(headers) - for i in range(n_experiments): - self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1))) - self.results_table.setColumnWidth(0, 80) - for i in range(n_responses): - self.results_table.setColumnWidth(i + 1, 150) - - def export_to_csv(self): - if self.design_matrix.rowCount() == 0: - QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") - return - filename, _ = QFileDialog.getSaveFileName(self, "Сохранить план эксперимента", "", "CSV Files (*.csv);;All Files (*)") - if filename: - if not filename.lower().endswith('.csv'): - filename += '.csv' - try: - with open(filename, 'w', newline='', encoding='utf-8-sig') as f: - writer = csv.writer(f) - headers = [] - for j in range(self.design_matrix.columnCount()): - header_item = self.design_matrix.horizontalHeaderItem(j) - headers.append(header_item.text() if header_item else f"Колонка_{j+1}") - writer.writerow(headers) - for i in range(self.design_matrix.rowCount()): - row = [self.design_matrix.item(i, j).text() if self.design_matrix.item(i, j) else "" for j in range(self.design_matrix.columnCount())] - writer.writerow(row) - QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}") - except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}") - - def perform_analysis(self): - n_responses = self.responses_table.rowCount() - if n_responses == 0: - QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один отклик!") - return - if self.generated_design is None: - QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") - return + self.export_csv_btn.setEnabled(True) + self._setup_results_table(n_exp) + +# ДОБАВИТЬ В МЕТОД _setup_ui ПОСЛЕ СОЗДАНИЯ ВКЛАДОК: + + # Добавляем кнопки сохранения/загрузки в тулбар + toolbar = self.addToolBar("Файл") + toolbar.setMovable(False) - results = [] - for i in range(self.results_table.rowCount()): - row_results = [] - for j in range(1, self.results_table.columnCount()): - item = self.results_table.item(i, j) - if item and item.text(): - try: - row_results.append(float(item.text())) - except ValueError: - row_results.append(None) - else: - row_results.append(None) - results.append(row_results) + save_action = toolbar.addAction("💾 Сохранить проект") + save_action.triggered.connect(self.save_project_to_json) - for i, row in enumerate(results): - for j, val in enumerate(row): - if val is None: - self.analysis_output.setText(f"Ошибка: Не введены результаты для опыта {i+1}, отклик {j+1}") - return + load_action = toolbar.addAction("📂 Загрузить проект") + load_action.triggered.connect(self.load_project_from_json) - self.analysis_output.clear() - self.analysis_output.append("=" * 60) - self.analysis_output.append("РЕЗУЛЬТАТЫ РЕГРЕССИОННОГО АНАЛИЗА") - self.analysis_output.append("=" * 60) + toolbar.addSeparator() - factors = self.get_factors_data() - design = self.generated_design - - for resp_idx in range(n_responses): - resp_name = self.responses_table.item(resp_idx, 0).text() - self.analysis_output.append(f"\n📊 Отклик: {resp_name}") - self.analysis_output.append("-" * 40) - - 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 - - self.analysis_output.append(f"Среднее значение: {mean_y:.4f}") - self.analysis_output.append(f"Общая дисперсия: {variance:.4f}") - self.analysis_output.append(f"Стандартное отклонение: {std_dev:.4f}") - self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%") - - 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]) - - if len(center_y) > 1: - center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0 - self.analysis_output.append(f"\nЦентральные точки (n={len(center_y)}):") - self.analysis_output.append(f" Среднее: {np.mean(center_y):.4f}") - self.analysis_output.append(f" Дисперсия воспроизводимости: {center_variance:.4f}") - if len(factorial_y) > 0 and center_variance > 0: - factorial_variance = np.var(factorial_y, ddof=1) if len(factorial_y) > 1 else 0 - if factorial_variance > 0: - fisher = max(factorial_variance, center_variance) / min(factorial_variance, center_variance) - self.analysis_output.append(f"\nКритерий Фишера (F-отношение): {fisher:.4f}") - if fisher < 4.0: - self.analysis_output.append("✅ Модель адекватна экспериментальным данным") - else: - self.analysis_output.append("⚠️ Модель может быть неадекватна, требуется проверка") - - self.analysis_output.append("\n" + "=" * 60) - self.analysis_output.append("Анализ завершен") - - def show_error(self, message: str): - QMessageBox.critical(self, "Ошибка", message) + # Сохраняем ссылку на tab_widget для переключения вкладок + self.tab_widget = tabs EOF -echo -e "${GREEN}========================================${NC}" -echo -e "${GREEN}Обновление завершено!${NC}" -echo -e "${GREEN}========================================${NC}" -echo -e "${YELLOW}Добавлена кнопка «🎯 В DoE» в калькулятор питательных сред${NC}" -echo -e "${YELLOW}При нажатии реагенты передаются в планировщик как факторы${NC}" +echo "✅ Создан патч gui_patch.py" + +# 3. ПРИМЕНЯЕМ ПАТЧ К GUI.PY +# Создаём резервную копию +cp gui.py gui.py.backup +echo "✅ Создана резервная копия gui.py.backup" + +# Вставляем импорты после существующих импортов +sed -i '/from calculations.doe import/a\ +from calculations.models.project_data import (\ + ProjectData, ReagentData, FactorData, \ + ResponseData, ExperimentResultsData, create_new_project\ +)' gui.py + +# Находим строку с self._setup_ui() и добавляем после неё toolbar +sed -i '/self._setup_ui()/a\ self._add_file_toolbar()' gui.py + +# Добавляем новый метод _add_file_toolbar после _setup_ui +cat >> gui.py.new_method << 'EOF' + + def _add_file_toolbar(self): + """Добавляет панель инструментов с кнопками сохранения/загрузки""" + toolbar = self.addToolBar("Файл") + toolbar.setMovable(False) + + save_action = toolbar.addAction("💾 Сохранить проект") + save_action.triggered.connect(self.save_project_to_json) + + load_action = toolbar.addAction("📂 Загрузить проект") + load_action.triggered.connect(self.load_project_from_json) + + toolbar.addSeparator() +EOF + +# Вставляем новый метод после _setup_ui +sed -i '/def _setup_ui(self):/,/def _get_stylesheet(self):/ { + /def _get_stylesheet(self):/ { + r gui.py.new_method + } +}' gui.py + +rm gui.py.new_method + +# Добавляем методы сохранения/загрузки в конец файла перед if __name__ +cat >> gui.py << 'EOF' + + # ========== МЕТОДЫ РАБОТЫ С JSON ========== + + def save_project_to_json(self): + """Сохраняет весь проект в JSON файл""" + from PyQt5.QtWidgets import QFileDialog, QMessageBox + + filename, _ = QFileDialog.getSaveFileName( + self, "Сохранить проект", "", "JSON Files (*.json);;All Files (*)" + ) + if not filename: + return + + if not filename.endswith('.json'): + filename += '.json' + + try: + project = self._collect_current_data() + project.save_to_file(filename) + QMessageBox.information(self, "Успех", f"Проект сохранён в {filename}") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить проект: {str(e)}") + + def load_project_from_json(self): + """Загружает проект из JSON файла""" + from PyQt5.QtWidgets import QFileDialog, QMessageBox, QComboBox, QTableWidgetItem + + filename, _ = QFileDialog.getOpenFileName( + self, "Загрузить проект", "", "JSON Files (*.json);;All Files (*)" + ) + if not filename: + return + + try: + project = ProjectData.load_from_file(filename) + self._apply_project_data(project) + QMessageBox.information(self, "Успех", f"Проект загружен из {filename}") + except FileNotFoundError: + QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить проект: {str(e)}") + + def _collect_current_data(self) -> ProjectData: + """Собирает текущие данные из всех виджетов""" + from PyQt5.QtWidgets import QComboBox + + # Данные калькулятора сред + reagents = [] + for row in range(self.reagents_table.rowCount()): + name_item = self.reagents_table.item(row, 0) + percent_item = self.reagents_table.item(row, 1) + unit_widget = self.reagents_table.cellWidget(row, 2) + coeff_item = self.reagents_table.item(row, 3) + dilution_item = self.reagents_table.item(row, 4) + + if name_item and percent_item and percent_item.text(): + try: + reagent = ReagentData( + name=name_item.text(), + percentage=float(percent_item.text()), + unit=unit_widget.currentText() if unit_widget else "мг", + conversion_factor=float(coeff_item.text()) if coeff_item and coeff_item.text() else 1.0, + dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0 + ) + reagents.append(reagent) + except ValueError: + pass + + # Данные факторов эксперимента + factors = [] + for row in range(self.factors_table.rowCount()): + name_item = self.factors_table.item(row, 0) + center_item = self.factors_table.item(row, 3) + low_item = self.factors_table.item(row, 7) + high_item = self.factors_table.item(row, 6) + step_item = self.factors_table.item(row, 4) + unit_item = self.factors_table.item(row, 8) + step_combo = self.factors_table.cellWidget(row, 5) + percent_item = self.factors_table.item(row, 1) + dilution_item = self.factors_table.item(row, 2) + + if name_item and center_item and center_item.text(): + try: + factor = FactorData( + name=name_item.text(), + center=float(center_item.text()), + low=float(low_item.text()) if low_item and low_item.text() else 0, + high=float(high_item.text()) if high_item and high_item.text() else 0, + step=float(step_item.text()) if step_item and step_item.text() else 0, + step_type=step_combo.currentText() if step_combo else "абс", + unit=unit_item.text() if unit_item else "", + percentage=float(percent_item.text()) if percent_item and percent_item.text() else None, + dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None + ) + factors.append(factor) + except ValueError: + pass + + # Данные результатов (если есть) + results_data = None + if hasattr(self, 'generated_design') and self.generated_design: + results = [] + for i in range(self.results_table.rowCount()): + item = self.results_table.item(i, 1) + if item and item.text(): + try: + results.append([float(item.text())]) + except ValueError: + pass + + if results: + responses = [ResponseData(name="Отклик", unit="")] + results_data = ExperimentResultsData( + design=self.generated_design, + results=results, + responses=responses + ) + + # Создаём проект + project = create_new_project("Мой проект") + project.medium_total_volume = self.total_volume_spin.value() + project.medium_volume_unit = self.volume_unit_combo.currentText() + project.medium_solvent = self.solvent_input.text() + project.medium_reagents = reagents + project.experiment_factors = factors + project.experiment_center_points = self.center_points_spin.value() + project.experiment_randomize = self.randomize_check.isChecked() + project.experiment_results = results_data + + return project + + def _apply_project_data(self, project: ProjectData): + """Применяет загруженные данные к GUI""" + from PyQt5.QtWidgets import QComboBox, QTableWidgetItem + from PyQt5.QtCore import Qt + + # Применяем данные калькулятора сред + self.total_volume_spin.setValue(project.medium_total_volume) + + index = self.volume_unit_combo.findText(project.medium_volume_unit) + if index >= 0: + self.volume_unit_combo.setCurrentIndex(index) + + self.solvent_input.setText(project.medium_solvent) + + # Очищаем и заполняем таблицу реагентов + self.reagents_table.setRowCount(0) + for reagent in project.medium_reagents: + row = self.reagents_table.rowCount() + self.reagents_table.insertRow(row) + self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name)) + self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage))) + + unit_combo = QComboBox() + unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"]) + unit_combo.setCurrentText(reagent.unit) + self.reagents_table.setCellWidget(row, 2, unit_combo) + + self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor))) + self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor))) + self.reagents_table.setItem(row, 5, QTableWidgetItem("")) + + # Применяем данные факторов эксперимента + self.factors_table.setRowCount(0) + for factor in project.experiment_factors: + row = self.factors_table.rowCount() + self.factors_table.insertRow(row) + self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name)) + + if factor.percentage is not None: + self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage))) + else: + self.factors_table.setItem(row, 1, QTableWidgetItem("0")) + + if factor.dilution_factor is not None: + self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor))) + else: + self.factors_table.setItem(row, 2, QTableWidgetItem("1")) + + self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center))) + self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step))) + + step_combo = QComboBox() + step_combo.addItems(["абс", "%"]) + step_combo.setCurrentText(factor.step_type) + self.factors_table.setCellWidget(row, 5, step_combo) + + self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high))) + self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low))) + self.factors_table.setItem(row, 8, QTableWidgetItem(factor.unit)) + + # Применяем настройки эксперимента + self.center_points_spin.setValue(project.experiment_center_points) + self.randomize_check.setChecked(project.experiment_randomize) + + # Если есть результаты, загружаем их + if project.experiment_results and project.experiment_results.design: + self.generated_design = project.experiment_results.design + # Обновляем отображение матрицы + self._refresh_design_matrix() + + # Загружаем результаты в таблицу + if project.experiment_results.results: + for i, row_results in enumerate(project.experiment_results.results): + if i < self.results_table.rowCount() and row_results: + self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0]))) + + # Переключаемся на вкладку с факторами + if hasattr(self, 'tab_widget'): + self.tab_widget.setCurrentIndex(1) + + def _refresh_design_matrix(self): + """Обновляет отображение матрицы планирования""" + if not self.generated_design: + return + + factors = self._get_factors_from_table() + 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( + f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}" + ) + self._setup_results_table(n_exp) +EOF + +echo "✅ Добавлены методы работы с JSON в gui.py" + +# 4. СОЗДАЁМ ПРИМЕР JSON ФАЙЛА +cat > example_project.json << 'EOF' +{ + "project_info": { + "name": "Пример проекта", + "created_at": "2026-05-14T16:00:00", + "modified_at": "2026-05-14T16:00:00", + "version": "1.0" + }, + "medium_calculator": { + "total_volume": 1000, + "volume_unit": "мл", + "solvent": "Вода", + "reagents": [ + { + "name": "Глюкоза", + "percentage": 2.0, + "unit": "г", + "conversion_factor": 1.0, + "dilution_factor": 1.0 + }, + { + "name": "Пептон", + "percentage": 1.0, + "unit": "г", + "conversion_factor": 1.0, + "dilution_factor": 1.0 + } + ] + }, + "experiment": { + "factors": [ + { + "name": "Глюкоза", + "center": 20.0, + "low": 18.0, + "high": 22.0, + "step": 2.0, + "step_type": "абс", + "unit": "г", + "percentage": 2.0, + "dilution_factor": 1.0 + }, + { + "name": "Пептон", + "center": 10.0, + "low": 9.0, + "high": 11.0, + "step": 1.0, + "step_type": "абс", + "unit": "г", + "percentage": 1.0, + "dilution_factor": 1.0 + } + ], + "responses": [ + {"name": "Отклик", "unit": ""} + ], + "center_points": 3, + "randomize": true, + "results": null + } +} +EOF + +echo "✅ Создан example_project.json" + +# 5. СОЗДАЁМ requirements.txt ЕСЛИ ЕГО НЕТ +if [ ! -f requirements.txt ]; then + cat > requirements.txt << 'EOF' +PyQt5>=5.15.0 +numpy>=1.21.0 +EOF + echo "✅ Создан requirements.txt" +fi + echo "" -echo -e "${GREEN}Запуск: python3 main.py${NC}" +echo "================================================================" +echo "✅ ГОТОВО! Функционал сохранения/загрузки JSON добавлен!" +echo "================================================================" +echo "" +echo "Что было добавлено:" +echo " 1. Модель данных в calculations/models/project_data.py" +echo " 2. Кнопки 'Сохранить проект' и 'Загрузить проект' в toolbar" +echo " 3. Методы сохранения/загрузки в gui.py" +echo " 4. Пример JSON файла example_project.json" +echo "" +echo "Для запуска программы:" +echo " python main.py" +echo "" +echo "Для восстановления оригинального gui.py (если нужно):" +echo " cp gui.py.backup gui.py" +echo "" +echo "Чтобы применить изменения, перезапустите программу" +echo "================================================================" diff --git a/requirements.txt b/requirements.txt index b7803ce..f69da05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ PyQt5>=5.15.0 -numpy>=1.19.0 +numpy>=1.21.0 diff --git a/run.sh b/run.sh deleted file mode 100755 index e1f6228..0000000 --- a/run.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -python3 main.py diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e878fc2..0000000 --- a/src/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Цифровой помощник биохимика - основная библиотека""" - -__version__ = "1.0.0" diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py deleted file mode 100644 index 37f75f8..0000000 --- a/src/controllers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .medium_controller import MediumController -from .experiment_controller import ExperimentController - -__all__ = ['MediumController', 'ExperimentController'] diff --git a/src/controllers/experiment_controller.py b/src/controllers/experiment_controller.py deleted file mode 100644 index 023e7fa..0000000 --- a/src/controllers/experiment_controller.py +++ /dev/null @@ -1,23 +0,0 @@ -from ..models.experiment_model import ExperimentModel - -class ExperimentController: - def __init__(self, view): - self.model = ExperimentModel() - self.view = view - - def update_model_from_view(self): - factors = self.view.get_factors_data() - self.model.set_factors(factors) - - responses = [] - for row in range(self.view.responses_table.rowCount()): - name_item = self.view.responses_table.item(row, 0) - unit_item = self.view.responses_table.item(row, 1) - if name_item: - responses.append({ - 'name': name_item.text(), - 'unit': unit_item.text() if unit_item else "" - }) - self.model.set_responses(responses) - self.model.set_center_points(self.view.center_points_spin.value()) - self.model.set_randomize(self.view.randomize_check.isChecked()) diff --git a/src/controllers/medium_controller.py b/src/controllers/medium_controller.py deleted file mode 100644 index 5888bb5..0000000 --- a/src/controllers/medium_controller.py +++ /dev/null @@ -1,143 +0,0 @@ -from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QColor -import json -from ..models.medium_model import MediumModel -from ..models.reagent import Reagent -from ..views.experiment_view import ExperimentDesignWindow - -class MediumController: - def __init__(self, view): - self.model = MediumModel() - self.view = view - self.doe_window = None - self._connect_signals() - self._setup_initial_data() - - def _connect_signals(self): - self.view.add_row_btn.clicked.connect(self.add_reagent_row) - self.view.remove_row_btn.clicked.connect(self.remove_reagent_row) - self.view.calculate_btn.clicked.connect(self._perform_calculation) - self.view.save_btn.clicked.connect(self.save_composition) - self.view.load_btn.clicked.connect(self.load_composition) - self.view.to_doe_btn.clicked.connect(self.send_to_doe) - self.view.solvent_input.textChanged.connect(self.view.update_solvent_name) - - def _setup_initial_data(self): - self.view.add_initial_rows() - - def add_reagent_row(self): - self.view.add_new_row() - - def remove_reagent_row(self): - self.view.remove_selected_row() - - def _perform_calculation(self): - try: - self._update_model_from_view() - results, solvent_amount, solvent_percentage = self.model.calculate_amounts() - self.view.update_results(results) - self.view.update_solvent_result(solvent_amount, self.model.amount_unit) - self.view.update_solvent_percent(solvent_percentage) - except ValueError as e: - self.view.show_error(f"Ошибка в данных: {str(e)}") - except Exception as e: - self.view.show_error(f"Неожиданная ошибка: {str(e)}") - - def _update_model_from_view(self): - self.model.reagents.clear() - self.model.total_amount = self.view.amount_input.value() - self.model.amount_unit = self.view.amount_unit_combo.currentText() - self.model.solvent = self.view.solvent_input.text() - for row in range(1, self.view.table.rowCount()): - name_item = self.view.table.item(row, 0) - percentage_item = self.view.table.item(row, 1) - unit_widget = self.view.table.cellWidget(row, 2) - conversion_item = self.view.table.item(row, 3) - dilution_item = self.view.table.item(row, 4) - if not all([name_item, percentage_item, conversion_item]): - continue - try: - name = name_item.text() - percentage = float(percentage_item.text()) - unit = unit_widget.currentText() if unit_widget else "мг" - conversion_factor = float(conversion_item.text()) - dilution_factor = float(dilution_item.text()) if dilution_item else 1.0 - reagent = Reagent(name, percentage, unit, conversion_factor) - reagent.dilution_factor = dilution_factor - self.model.reagents.append(reagent) - except ValueError as e: - raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}") - - def send_to_doe(self): - """Передаёт данные о реагентах в окно планирования эксперимента""" - reagents = self.view.get_reagents_data() - - if len(reagents) == 0: - self.view.show_error("Нет реагентов для передачи в планировщик эксперимента!") - return - - # Создаём или показываем существующее окно DoE - if self.doe_window is None: - self.doe_window = ExperimentDesignWindow() - - # Передаём данные в окно DoE - self.doe_window.load_factors_from_reagents(reagents) - self.doe_window.show() - self.doe_window.raise_() - self.doe_window.activateWindow() - - self.view.show_info(f"Передано {len(reagents)} реагентов в планировщик эксперимента\n" - f"Каждый реагент добавлен как фактор.\n" - f"Его концентрация (%) установлена как нулевой уровень.") - - def save_composition(self): - filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)") - if filename: - if not filename.lower().endswith('.json'): - filename += '.json' - try: - self._update_model_from_view() - self.model.save_to_file(filename) - QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!") - except Exception as e: - self.view.show_error(f"Ошибка сохранения: {str(e)}") - - def load_composition(self): - filename, _ = QFileDialog.getOpenFileName(self.view, "Загрузить состав среды", "", "JSON Files (*.json);;All Files (*)") - if filename: - try: - self.model.load_from_file(filename) - self._update_view_from_model() - QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен") - except FileNotFoundError: - QMessageBox.critical(self.view, "Ошибка", f"Файл не найден: {filename}") - except json.JSONDecodeError as e: - QMessageBox.critical(self.view, "Ошибка", f"Неверный формат JSON-файла: {str(e)}") - except Exception as e: - QMessageBox.critical(self.view, "Ошибка", f"Ошибка при загрузке состава: {str(e)}") - - def _update_view_from_model(self): - while self.view.table.rowCount() > 1: - self.view.table.removeRow(1) - if self.view.table.rowCount() == 0: - self.view.add_solvent_row() - self.view.amount_input.setValue(self.model.total_amount) - index = self.view.amount_unit_combo.findText(self.model.amount_unit) - if index >= 0: - self.view.amount_unit_combo.setCurrentIndex(index) - self.view.solvent_input.setText(self.model.solvent) - self.view.update_solvent_name() - for reagent in self.model.reagents: - row = self.view.table.rowCount() - self.view.table.insertRow(row) - self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name)) - self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}")) - unit_combo = QComboBox() - unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) - unit_combo.setCurrentText(reagent.unit) - self.view.table.setCellWidget(row, 2, unit_combo) - self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}")) - self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}")) - self.view.table.setItem(row, 5, QTableWidgetItem("")) - self.view.clear_results() diff --git a/src/models/__init__.py b/src/models/__init__.py deleted file mode 100644 index 3bcdcad..0000000 --- a/src/models/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .reagent import Reagent -from .medium_model import MediumModel -from .experiment_model import ExperimentModel - -__all__ = ['Reagent', 'MediumModel', 'ExperimentModel'] diff --git a/src/models/experiment_model.py b/src/models/experiment_model.py deleted file mode 100644 index fe5d322..0000000 --- a/src/models/experiment_model.py +++ /dev/null @@ -1,80 +0,0 @@ -import numpy as np -from typing import List, Dict, Any - -class ExperimentModel: - def __init__(self): - self.factors = [] - self.responses = [] - self.center_points = 3 - self.randomize = True - - def set_factors(self, factors: List[Dict]): - self.factors = factors - - def set_responses(self, responses: List[Dict]): - self.responses = responses - - def set_center_points(self, n: int): - self.center_points = n - - def set_randomize(self, value: bool): - self.randomize = value - - def calculate_factorial_design(self) -> List[Dict]: - k = len(self.factors) - if k == 0: - return [] - n_factorial = 2 ** k - design = [] - for i in range(n_factorial): - experiment = {} - for j in range(k): - coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1 - natural_value = self.factors[j]['low'] if coded_level == -1 else self.factors[j]['high'] - experiment[f"Фактор_{j+1}"] = { - 'coded': coded_level, - 'natural': natural_value, - 'name': self.factors[j]['name'], - 'unit': self.factors[j]['unit'] - } - design.append(experiment) - for i in range(self.center_points): - center_experiment = {} - for j in range(k): - center_experiment[f"Фактор_{j+1}"] = { - 'coded': 0, - 'natural': self.factors[j]['center'], - 'name': self.factors[j]['name'], - 'unit': self.factors[j]['unit'] - } - center_experiment['is_center'] = True - center_experiment['center_num'] = i + 1 - design.append(center_experiment) - if self.randomize: - import random - random.shuffle(design) - return design - - def analyze_results(self, results: List[List[float]], design: List[Dict]) -> Dict: - analysis = {} - for resp_idx, response in enumerate(self.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 - 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) - } - return analysis diff --git a/src/models/medium_model.py b/src/models/medium_model.py deleted file mode 100644 index 48e91f8..0000000 --- a/src/models/medium_model.py +++ /dev/null @@ -1,113 +0,0 @@ -import json -from typing import List, Tuple -from .reagent import Reagent - -VOLUME_UNITS = { - 'нл': 0.001, 'мкл': 1.0, 'мл': 1000.0, 'л': 1000000.0 -} - -MASS_UNITS = { - 'нг': 0.000001, 'мкг': 0.001, 'мг': 1.0, 'г': 1000.0, 'кг': 1000000.0 -} - -class MediumModel: - def __init__(self): - self.total_amount = 100.0 - self.amount_unit = 'мл' - self.solvent = 'Вода' - self.reagents = [] - - def convert_amount(self, amount_base: float, target_unit: str, is_volume: bool) -> float: - if is_volume: - conversion_factor = VOLUME_UNITS.get(target_unit, 1.0) - else: - conversion_factor = MASS_UNITS.get(target_unit, 1.0) - return amount_base / conversion_factor - - def calculate_amounts(self) -> Tuple[List[float], float, float]: - results = [] - if not self.reagents: - return results, self.total_amount, 100.0 - - total_percentage = sum(r.percentage for r in self.reagents) - if total_percentage > 100: - raise ValueError(f"Сумма процентов реагентов ({total_percentage:.2f}%) превышает 100%") - - total_in_base = self.total_amount * VOLUME_UNITS[self.amount_unit] - - undiluted_amounts = [] - for reagent in self.reagents: - amount_in_base = (reagent.percentage / 100) * total_in_base - is_volume = reagent.unit in VOLUME_UNITS - adjusted_amount = amount_in_base * reagent.conversion_factor - final_amount = self.convert_amount(adjusted_amount, reagent.unit, is_volume) - undiluted_amounts.append(final_amount) - - diluted_amounts = [] - total_diluted_volume_base = 0 - for i, reagent in enumerate(self.reagents): - dilution_factor = getattr(reagent, 'dilution_factor', 1.0) - if dilution_factor <= 0: - dilution_factor = 1.0 - diluted_amount = undiluted_amounts[i] * dilution_factor - diluted_amounts.append(diluted_amount) - is_volume = reagent.unit in VOLUME_UNITS - if is_volume: - reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.unit] - else: - reagent_volume_base = 0 - total_diluted_volume_base += reagent_volume_base - - solvent_volume_base = total_in_base - total_diluted_volume_base - solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit] - if solvent_amount < 0: - solvent_amount = 0 - solvent_percentage = 100 - total_percentage - - return diluted_amounts, solvent_amount, solvent_percentage - - def save_to_file(self, filename: str): - data = { - 'total_amount': self.total_amount, - 'amount_unit': self.amount_unit, - 'solvent': self.solvent, - 'reagents': [{'name': r.name, 'percentage': r.percentage, 'unit': r.unit, - 'conversion_factor': r.conversion_factor, 'dilution_factor': getattr(r, 'dilution_factor', 1.0)} - for r in self.reagents] - } - with open(filename, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - def load_from_file(self, filename: str): - with open(filename, 'r', encoding='utf-8') as f: - data = json.load(f) - self.total_amount = data['total_amount'] - self.amount_unit = data['amount_unit'] - self.solvent = data['solvent'] - self.reagents.clear() - for r_data in data['reagents']: - reagent = Reagent(r_data['name'], r_data['percentage'], r_data['unit'], r_data.get('conversion_factor', 1.0)) - reagent.dilution_factor = r_data.get('dilution_factor', 1.0) - self.reagents.append(reagent) - - def add_reagent(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0, dilution_factor: float = 1.0): - reagent = Reagent(name, percentage, unit, conversion_factor) - reagent.dilution_factor = dilution_factor - self.reagents.append(reagent) - - def remove_reagent(self, index: int): - if 0 <= index < len(self.reagents): - del self.reagents[index] - - def clear_reagents(self): - self.reagents.clear() - - def get_reagent_count(self) -> int: - return len(self.reagents) - - def set_total_amount(self, amount: float, unit: str): - self.total_amount = amount - self.amount_unit = unit - - def set_solvent(self, solvent_name: str): - self.solvent = solvent_name diff --git a/src/models/reagent.py b/src/models/reagent.py deleted file mode 100644 index 160892b..0000000 --- a/src/models/reagent.py +++ /dev/null @@ -1,10 +0,0 @@ -class Reagent: - def __init__(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0): - self.name = name - self.percentage = percentage - self.unit = unit - self.conversion_factor = conversion_factor - self.dilution_factor = 1.0 - - def __repr__(self): - return f"Reagent(name={self.name}, percentage={self.percentage}, unit={self.unit})" diff --git a/src/utils/__init__.py b/src/utils/__init__.py deleted file mode 100644 index a42179a..0000000 --- a/src/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Утилиты и вспомогательные функции""" - -__all__ = [] diff --git a/src/views/__init__.py b/src/views/__init__.py deleted file mode 100644 index e6fcdd9..0000000 --- a/src/views/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main_window import MainWindow -from .medium_view import MediumCalculatorWindow -from .experiment_view import ExperimentDesignWindow - -__all__ = ['MainWindow', 'MediumCalculatorWindow', 'ExperimentDesignWindow'] diff --git a/src/views/experiment_view.py b/src/views/experiment_view.py deleted file mode 100644 index cc533a3..0000000 --- a/src/views/experiment_view.py +++ /dev/null @@ -1,532 +0,0 @@ -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, - QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox, - QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, QTextEdit, - QTabWidget, QFormLayout, QCheckBox, QScrollArea, QFileDialog) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QColor, QFont -import csv -import random -import numpy as np - -class ExperimentDesignWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика") - self.setGeometry(200, 100, 1200, 800) - self.setStyleSheet(""" - QMainWindow { background-color: #f5f5f5; } - QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; } - QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; } - QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; } - QPushButton:hover { background-color: #45a049; } - QPushButton#danger { background-color: #f44336; } - QPushButton#danger:hover { background-color: #da190b; } - QPushButton#doe { background-color: #9C27B0; } - QPushButton#doe:hover { background-color: #7B1FA2; } - QTableWidget { gridline-color: #ddd; } - QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } - """) - self._init_ui() - - def _init_ui(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) - layout = QVBoxLayout(central_widget) - - title_label = QLabel("Планирование полнофакторного эксперимента (DoE)") - title_font = QFont() - title_font.setPointSize(18) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet("color: #2E7D32;") - layout.addWidget(title_label) - - tabs = QTabWidget() - - # Вкладка параметров - params_tab = QWidget() - params_layout = QVBoxLayout(params_tab) - - factors_group = QGroupBox("Факторы эксперимента (независимые переменные)") - factors_layout = QVBoxLayout() - - info_label = QLabel("Определите факторы, которые влияют на ваш эксперимент:") - info_label.setStyleSheet("color: #555; font-weight: normal;") - factors_layout.addWidget(info_label) - - self.factors_table = QTableWidget() - self.factors_table.setColumnCount(6) - self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг", - "Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"]) - self.factors_table.setRowCount(2) - - sample_factors = [["Температура", "31", "6", "37", "25", "°C"], ["pH", "7.0", "0.5", "7.5", "6.5", ""]] - for i, factor in enumerate(sample_factors): - for j, value in enumerate(factor): - item = QTableWidgetItem(value) - if j in [3, 4]: - item.setFlags(item.flags() & ~Qt.ItemIsEditable) - item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(i, j, item) - - self.factors_table.setColumnWidth(0, 150) - self.factors_table.setColumnWidth(1, 120) - self.factors_table.setColumnWidth(2, 80) - self.factors_table.setColumnWidth(3, 120) - self.factors_table.setColumnWidth(4, 120) - self.factors_table.setColumnWidth(5, 100) - - self.factors_table.itemChanged.connect(self.on_factor_changed) - factors_layout.addWidget(self.factors_table) - - factor_buttons = QHBoxLayout() - add_factor_btn = QPushButton("+ Добавить фактор") - add_factor_btn.clicked.connect(self.add_factor_row) - remove_factor_btn = QPushButton("- Удалить последний") - remove_factor_btn.clicked.connect(self.remove_factor_row) - factor_buttons.addWidget(add_factor_btn) - factor_buttons.addWidget(remove_factor_btn) - factor_buttons.addStretch() - factors_layout.addLayout(factor_buttons) - factors_group.setLayout(factors_layout) - params_layout.addWidget(factors_group) - - settings_group = QGroupBox("Настройки эксперимента") - settings_layout = QHBoxLayout() - center_layout = QHBoxLayout() - center_layout.addWidget(QLabel("Количество центральных точек:")) - self.center_points_spin = QSpinBox() - self.center_points_spin.setRange(0, 10) - self.center_points_spin.setValue(3) - center_layout.addWidget(self.center_points_spin) - settings_layout.addLayout(center_layout) - self.randomize_check = QCheckBox("Рэндомизировать порядок опытов") - self.randomize_check.setChecked(True) - settings_layout.addWidget(self.randomize_check) - settings_layout.addStretch() - settings_group.setLayout(settings_layout) - params_layout.addWidget(settings_group) - - responses_group = QGroupBox("Отклики (зависимые переменные)") - responses_layout = QVBoxLayout() - self.responses_table = QTableWidget() - self.responses_table.setColumnCount(2) - self.responses_table.setHorizontalHeaderLabels(["Отклик", "Единица измерения"]) - self.responses_table.setRowCount(2) - sample_responses = [["Оптическая плотность (OD600)", ""], ["Концентрация целевого продукта", "мг/мл"]] - for i, response in enumerate(sample_responses): - for j, value in enumerate(response): - self.responses_table.setItem(i, j, QTableWidgetItem(value)) - responses_layout.addWidget(self.responses_table) - - response_buttons = QHBoxLayout() - add_response_btn = QPushButton("+ Добавить отклик") - add_response_btn.clicked.connect(self.add_response_row) - remove_response_btn = QPushButton("- Удалить последний") - remove_response_btn.clicked.connect(self.remove_response_row) - response_buttons.addWidget(add_response_btn) - response_buttons.addWidget(remove_response_btn) - response_buttons.addStretch() - responses_layout.addLayout(response_buttons) - responses_group.setLayout(responses_layout) - params_layout.addWidget(responses_group) - tabs.addTab(params_tab, "📝 Параметры эксперимента") - - # Вкладка матрицы планирования - plan_tab = QWidget() - plan_layout = QVBoxLayout(plan_tab) - plan_info = QLabel("Полнофакторный план эксперимента с центральными точками") - plan_info.setAlignment(Qt.AlignCenter) - plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") - plan_layout.addWidget(plan_info) - - scroll = QScrollArea() - scroll.setWidgetResizable(True) - matrix_widget = QWidget() - matrix_layout = QVBoxLayout(matrix_widget) - self.design_matrix = QTableWidget() - matrix_layout.addWidget(self.design_matrix) - scroll.setWidget(matrix_widget) - plan_layout.addWidget(scroll) - - buttons_layout = QHBoxLayout() - generate_btn = QPushButton("Сгенерировать план эксперимента") - generate_btn.clicked.connect(self.generate_design_matrix) - buttons_layout.addWidget(generate_btn) - export_btn = QPushButton("📊 Экспорт в CSV") - export_btn.clicked.connect(self.export_to_csv) - buttons_layout.addWidget(export_btn) - buttons_layout.addStretch() - plan_layout.addLayout(buttons_layout) - - self.plan_info_label = QLabel("") - self.plan_info_label.setStyleSheet("color: #666; font-size: 12px; padding: 5px;") - plan_layout.addWidget(self.plan_info_label) - tabs.addTab(plan_tab, "📊 Матрица планирования") - - # Вкладка анализа - analysis_tab = QWidget() - analysis_layout = QVBoxLayout(analysis_tab) - analysis_info = QLabel("Введите результаты экспериментов для анализа") - analysis_info.setAlignment(Qt.AlignCenter) - analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") - analysis_layout.addWidget(analysis_info) - self.results_table = QTableWidget() - analysis_layout.addWidget(self.results_table) - analyze_btn = QPushButton("Провести регрессионный анализ") - analyze_btn.clicked.connect(self.perform_analysis) - analysis_layout.addWidget(analyze_btn) - self.analysis_output = QTextEdit() - self.analysis_output.setReadOnly(True) - self.analysis_output.setMaximumHeight(200) - analysis_layout.addWidget(self.analysis_output) - tabs.addTab(analysis_tab, "📈 Анализ результатов") - - layout.addWidget(tabs) - - btn_layout = QHBoxLayout() - close_btn = QPushButton("Закрыть") - close_btn.clicked.connect(self.close) - btn_layout.addStretch() - btn_layout.addWidget(close_btn) - layout.addLayout(btn_layout) - - self.generated_design = None - self.factors_data = None - - def load_factors_from_reagents(self, reagents): - """Загружает факторы из списка реагентов калькулятора""" - if not reagents: - return - - # Очищаем таблицу факторов - self.factors_table.setRowCount(0) - - # Добавляем каждый реагент как фактор - for reagent in reagents: - row = self.factors_table.rowCount() - self.factors_table.insertRow(row) - - # Название фактора = название реагента - self.factors_table.setItem(row, 0, QTableWidgetItem(reagent['name'])) - - # Нулевой уровень = процентное содержание - percentage = reagent['percentage'] - self.factors_table.setItem(row, 1, QTableWidgetItem(f"{percentage:.2f}")) - - # Шаг = 10% от нулевого уровня (или 1, если нулевой уровень 0) - step = max(percentage * 0.1, 1.0) if percentage > 0 else 1.0 - self.factors_table.setItem(row, 2, QTableWidgetItem(f"{step:.2f}")) - - # Единица измерения - unit = reagent['unit'] - # Преобразуем единицы измерения в понятные для факторов - if unit in ['нг', 'мкг', 'мг', 'г', 'кг']: - display_unit = unit - elif unit in ['нл', 'мкл', 'мл', 'л']: - display_unit = unit - else: - display_unit = "%" - self.factors_table.setItem(row, 5, QTableWidgetItem(display_unit)) - - # Верхний и нижний уровни (вычисляются автоматически) - high = percentage + step - low = percentage - step - - high_item = QTableWidgetItem(f"{high:.2f}") - high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable) - high_item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(row, 3, high_item) - - low_item = QTableWidgetItem(f"{low:.2f}") - low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable) - low_item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(row, 4, low_item) - - # Показываем сообщение об успешной загрузке - QMessageBox.information( - self, - "Загрузка из калькулятора", - f"Загружено {len(reagents)} факторов из калькулятора питательных сред.\n\n" - f"Каждый реагент стал фактором.\n" - f"Нулевой уровень = концентрация реагента (%)\n" - f"Шаг = 10% от нулевого уровня\n\n" - f"При необходимости отредактируйте шаг и единицы измерения." - ) - - def on_factor_changed(self, item): - row = item.row() - col = item.column() - if col in [1, 2]: - try: - center = float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0 - step = float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0 - high = center + step - low = center - step - self.factors_table.blockSignals(True) - if self.factors_table.item(row, 3): - self.factors_table.item(row, 3).setText(f"{high:.3f}".rstrip('0').rstrip('.')) - if self.factors_table.item(row, 4): - self.factors_table.item(row, 4).setText(f"{low:.3f}".rstrip('0').rstrip('.')) - self.factors_table.blockSignals(False) - except ValueError: - pass - - def add_factor_row(self): - row = self.factors_table.rowCount() - self.factors_table.insertRow(row) - self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}")) - self.factors_table.setItem(row, 1, QTableWidgetItem("0")) - self.factors_table.setItem(row, 2, QTableWidgetItem("1")) - high_item = QTableWidgetItem("1") - high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable) - high_item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(row, 3, high_item) - low_item = QTableWidgetItem("-1") - low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable) - low_item.setBackground(QColor(240, 240, 240)) - self.factors_table.setItem(row, 4, low_item) - self.factors_table.setItem(row, 5, QTableWidgetItem("")) - - def remove_factor_row(self): - if self.factors_table.rowCount() > 1: - self.factors_table.removeRow(self.factors_table.rowCount() - 1) - - def add_response_row(self): - row = self.responses_table.rowCount() - self.responses_table.insertRow(row) - self.responses_table.setItem(row, 0, QTableWidgetItem(f"Отклик_{row+1}")) - self.responses_table.setItem(row, 1, QTableWidgetItem("")) - - def remove_response_row(self): - if self.responses_table.rowCount() > 1: - self.responses_table.removeRow(self.responses_table.rowCount() - 1) - - def get_factors_data(self): - factors = [] - for row in range(self.factors_table.rowCount()): - try: - factor = { - 'name': self.factors_table.item(row, 0).text() if self.factors_table.item(row, 0) else "", - 'center': float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0, - 'step': float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0, - 'high': float(self.factors_table.item(row, 3).text()) if self.factors_table.item(row, 3) else 0, - 'low': float(self.factors_table.item(row, 4).text()) if self.factors_table.item(row, 4) else 0, - 'unit': self.factors_table.item(row, 5).text() if self.factors_table.item(row, 5) else "" - } - factors.append(factor) - except (ValueError, AttributeError): - continue - return factors - - def calculate_factorial_design(self, factors): - k = len(factors) - if k == 0: - return [] - n_factorial = 2 ** k - design = [] - for i in range(n_factorial): - experiment = {} - for j in range(k): - 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]['unit'] - } - design.append(experiment) - - n_center = self.center_points_spin.value() - for i in range(n_center): - 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]['unit'] - } - center_experiment['is_center'] = True - center_experiment['center_num'] = i + 1 - design.append(center_experiment) - - if self.randomize_check.isChecked(): - random.shuffle(design) - return design - - def generate_design_matrix(self): - factors = self.get_factors_data() - if len(factors) == 0: - QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!") - return - - self.factors_data = factors - design = self.calculate_factorial_design(factors) - self.generated_design = design - - n_experiments = len(design) - n_factors = len(factors) - - self.design_matrix.setRowCount(n_experiments) - self.design_matrix.setColumnCount(n_factors + 2) - headers = ["№ опыта"] + [f["name"] for f in factors] + ["Тип точки"] - self.design_matrix.setHorizontalHeaderLabels(headers) - - for exp_idx, experiment in enumerate(design): - self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1))) - for factor_idx in range(n_factors): - factor_key = f"Фактор_{factor_idx + 1}" - value = experiment[factor_key]['natural'] - unit = factors[factor_idx]['unit'] - if isinstance(value, float): - if value == int(value): - display_value = str(int(value)) - else: - display_value = f"{value:.3f}".rstrip('0').rstrip('.') - else: - display_value = str(value) - if unit: - display_value += f" {unit}" - item = QTableWidgetItem(display_value) - if experiment.get('is_center', False): - item.setBackground(QColor(255, 255, 200)) - self.design_matrix.setItem(exp_idx, factor_idx + 1, item) - - if experiment.get('is_center', False): - type_item = QTableWidgetItem(f"Центральная #{experiment['center_num']}") - type_item.setBackground(QColor(255, 255, 200)) - else: - levels = [] - for factor_idx in range(n_factors): - factor_key = f"Фактор_{factor_idx + 1}" - coded = experiment[factor_key]['coded'] - levels.append("+" if coded == 1 else "-") - type_item = QTableWidgetItem(f"Факторная ({''.join(levels)})") - self.design_matrix.setItem(exp_idx, n_factors + 1, type_item) - - self.design_matrix.resizeColumnsToContents() - n_factorial = 2 ** n_factors - n_center = self.center_points_spin.value() - self.plan_info_label.setText(f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов") - self.setup_results_table(n_experiments) - QMessageBox.information(self, "Успех", f"Сгенерирован план для {n_factors} факторов\nФакторных точек: {n_factorial}\nЦентральных точек: {n_center}\nВсего опытов: {n_experiments}") - - def setup_results_table(self, n_experiments): - n_responses = self.responses_table.rowCount() - self.results_table.setRowCount(n_experiments) - self.results_table.setColumnCount(n_responses + 1) - headers = ["№ опыта"] + [self.responses_table.item(i, 0).text() if self.responses_table.item(i, 0) else f"Отклик_{i+1}" for i in range(n_responses)] - self.results_table.setHorizontalHeaderLabels(headers) - for i in range(n_experiments): - self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1))) - self.results_table.setColumnWidth(0, 80) - for i in range(n_responses): - self.results_table.setColumnWidth(i + 1, 150) - - def export_to_csv(self): - if self.design_matrix.rowCount() == 0: - QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") - return - filename, _ = QFileDialog.getSaveFileName(self, "Сохранить план эксперимента", "", "CSV Files (*.csv);;All Files (*)") - if filename: - if not filename.lower().endswith('.csv'): - filename += '.csv' - try: - with open(filename, 'w', newline='', encoding='utf-8-sig') as f: - writer = csv.writer(f) - headers = [] - for j in range(self.design_matrix.columnCount()): - header_item = self.design_matrix.horizontalHeaderItem(j) - headers.append(header_item.text() if header_item else f"Колонка_{j+1}") - writer.writerow(headers) - for i in range(self.design_matrix.rowCount()): - row = [self.design_matrix.item(i, j).text() if self.design_matrix.item(i, j) else "" for j in range(self.design_matrix.columnCount())] - writer.writerow(row) - QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}") - except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}") - - def perform_analysis(self): - n_responses = self.responses_table.rowCount() - if n_responses == 0: - QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один отклик!") - return - if self.generated_design is None: - QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") - return - - results = [] - for i in range(self.results_table.rowCount()): - row_results = [] - for j in range(1, self.results_table.columnCount()): - item = self.results_table.item(i, j) - if item and item.text(): - try: - row_results.append(float(item.text())) - except ValueError: - row_results.append(None) - else: - row_results.append(None) - results.append(row_results) - - for i, row in enumerate(results): - for j, val in enumerate(row): - if val is None: - self.analysis_output.setText(f"Ошибка: Не введены результаты для опыта {i+1}, отклик {j+1}") - return - - self.analysis_output.clear() - self.analysis_output.append("=" * 60) - self.analysis_output.append("РЕЗУЛЬТАТЫ РЕГРЕССИОННОГО АНАЛИЗА") - self.analysis_output.append("=" * 60) - - factors = self.get_factors_data() - design = self.generated_design - - for resp_idx in range(n_responses): - resp_name = self.responses_table.item(resp_idx, 0).text() - self.analysis_output.append(f"\n📊 Отклик: {resp_name}") - self.analysis_output.append("-" * 40) - - 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 - - self.analysis_output.append(f"Среднее значение: {mean_y:.4f}") - self.analysis_output.append(f"Общая дисперсия: {variance:.4f}") - self.analysis_output.append(f"Стандартное отклонение: {std_dev:.4f}") - self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%") - - 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]) - - if len(center_y) > 1: - center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0 - self.analysis_output.append(f"\nЦентральные точки (n={len(center_y)}):") - self.analysis_output.append(f" Среднее: {np.mean(center_y):.4f}") - self.analysis_output.append(f" Дисперсия воспроизводимости: {center_variance:.4f}") - if len(factorial_y) > 0 and center_variance > 0: - factorial_variance = np.var(factorial_y, ddof=1) if len(factorial_y) > 1 else 0 - if factorial_variance > 0: - fisher = max(factorial_variance, center_variance) / min(factorial_variance, center_variance) - self.analysis_output.append(f"\nКритерий Фишера (F-отношение): {fisher:.4f}") - if fisher < 4.0: - self.analysis_output.append("✅ Модель адекватна экспериментальным данным") - else: - self.analysis_output.append("⚠️ Модель может быть неадекватна, требуется проверка") - - self.analysis_output.append("\n" + "=" * 60) - self.analysis_output.append("Анализ завершен") - - def show_error(self, message: str): - QMessageBox.critical(self, "Ошибка", message) diff --git a/src/views/main_window.py b/src/views/main_window.py deleted file mode 100644 index 85ab15f..0000000 --- a/src/views/main_window.py +++ /dev/null @@ -1,101 +0,0 @@ -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QFrame) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Цифровой помощник биохимика - Главное меню") - self.setGeometry(300, 200, 700, 500) - self.setStyleSheet(""" - QMainWindow { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #e8f4f8, stop:1 #f0f0f0); } - QPushButton { background-color: #2196F3; color: white; border: none; padding: 15px; font-size: 16px; font-weight: bold; border-radius: 8px; } - QPushButton:hover { background-color: #1976D2; } - QLabel { color: #333; font-size: 14px; } - """) - self._init_ui() - self.medium_calculator = None - self.experiment_window = None - - def _init_ui(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) - layout = QVBoxLayout(central_widget) - layout.setSpacing(20) - layout.setContentsMargins(50, 50, 50, 50) - - title_label = QLabel("Цифровой помощник биохимика") - title_font = QFont() - title_font.setPointSize(20) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet("color: #1565C0;") - layout.addWidget(title_label) - - subtitle_label = QLabel("Биотехнологические инструменты для лаборатории") - subtitle_font = QFont() - subtitle_font.setPointSize(12) - subtitle_label.setFont(subtitle_font) - subtitle_label.setAlignment(Qt.AlignCenter) - subtitle_label.setStyleSheet("color: #666;") - layout.addWidget(subtitle_label) - - layout.addSpacing(20) - - btn_medium = QPushButton("🧪 Калькулятор питательных сред") - btn_medium.setMinimumHeight(80) - btn_medium.clicked.connect(self.open_medium_calculator) - layout.addWidget(btn_medium) - - desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\nразбавления реагентов и автоматическим расчётом растворителя") - desc1_label.setAlignment(Qt.AlignCenter) - desc1_label.setWordWrap(True) - desc1_label.setStyleSheet("color: #555; font-size: 11px;") - layout.addWidget(desc1_label) - - layout.addSpacing(15) - - btn_experiment = QPushButton("📊 Планирование эксперимента (DoE)") - btn_experiment.setMinimumHeight(80) - btn_experiment.clicked.connect(self.open_experiment_designer) - layout.addWidget(btn_experiment) - - desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\nмногомерный анализ и визуализация") - desc2_label.setAlignment(Qt.AlignCenter) - desc2_label.setWordWrap(True) - desc2_label.setStyleSheet("color: #555; font-size: 11px;") - layout.addWidget(desc2_label) - - layout.addSpacing(15) - - line = QFrame() - line.setFrameShape(QFrame.HLine) - line.setFrameShadow(QFrame.Sunken) - layout.addWidget(line) - - bottom_layout = QHBoxLayout() - version_label = QLabel("Версия 1.0.0 | © 2026 Цифровой помощник биохимика") - version_label.setStyleSheet("color: #999; font-size: 10px;") - bottom_layout.addWidget(version_label) - bottom_layout.addStretch() - btn_exit = QPushButton("Выход") - btn_exit.setMaximumWidth(150) - btn_exit.setStyleSheet("QPushButton { background-color: #f44336; padding: 8px; font-size: 14px; } QPushButton:hover { background-color: #da190b; }") - btn_exit.clicked.connect(self.close) - bottom_layout.addWidget(btn_exit) - layout.addLayout(bottom_layout) - - def open_medium_calculator(self): - from .medium_view import MediumCalculatorWindow - from ..controllers.medium_controller import MediumController - self.medium_calculator = MediumCalculatorWindow() - self.medium_controller = MediumController(self.medium_calculator) - self.medium_calculator.show() - - def open_experiment_designer(self): - from .experiment_view import ExperimentDesignWindow - from ..controllers.experiment_controller import ExperimentController - self.experiment_window = ExperimentDesignWindow() - self.experiment_controller = ExperimentController(self.experiment_window) - self.experiment_window.show() diff --git a/src/views/medium_view.py b/src/views/medium_view.py deleted file mode 100644 index e84a9cd..0000000 --- a/src/views/medium_view.py +++ /dev/null @@ -1,251 +0,0 @@ -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, - QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget, - QMessageBox, QGroupBox, QFrame, QToolTip) -from PyQt5.QtCore import Qt, QPoint -from PyQt5.QtGui import QFont, QColor - -class MediumCalculatorWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Калькулятор питательных сред") - self.setGeometry(200, 100, 1200, 700) - self.setStyleSheet(""" - QMainWindow { background-color: #f5f5f5; } - QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; background-color: white; } - QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; } - QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 15px; border-radius: 4px; font-weight: bold; } - QPushButton:hover { background-color: #1976D2; } - QPushButton#danger { background-color: #f44336; } - QPushButton#danger:hover { background-color: #da190b; } - QPushButton#success { background-color: #4CAF50; } - QPushButton#success:hover { background-color: #45a049; } - QPushButton#doe { background-color: #9C27B0; } - QPushButton#doe:hover { background-color: #7B1FA2; } - QTableWidget { gridline-color: #ddd; background-color: white; } - QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } - """) - self._init_ui() - self.doe_window = None - - def _init_ui(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) - layout = QVBoxLayout(central_widget) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) - - title_label = QLabel("Калькулятор питательных сред") - title_label.setAlignment(Qt.AlignCenter) - title_font = QFont() - title_font.setPointSize(18) - title_font.setBold(True) - title_label.setFont(title_font) - layout.addWidget(title_label) - - params_group = QGroupBox("Параметры среды") - params_layout = QHBoxLayout() - amount_layout = QHBoxLayout() - amount_layout.addWidget(QLabel("Общее количество:")) - self.amount_input = QDoubleSpinBox() - self.amount_input.setRange(0.001, 1000000.0) - self.amount_input.setValue(1000.0) - amount_layout.addWidget(self.amount_input) - self.amount_unit_combo = QComboBox() - self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"]) - self.amount_unit_combo.setCurrentText("мл") - amount_layout.addWidget(self.amount_unit_combo) - params_layout.addLayout(amount_layout) - solvent_layout = QHBoxLayout() - solvent_layout.addWidget(QLabel("Растворитель:")) - self.solvent_input = QLineEdit("Вода") - solvent_layout.addWidget(self.solvent_input) - params_layout.addLayout(solvent_layout) - params_layout.addStretch() - params_group.setLayout(params_layout) - layout.addWidget(params_group) - - table_group = QGroupBox("Состав среды") - table_layout = QVBoxLayout() - self.table = QTableWidget() - self.table.setColumnCount(6) - self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"]) - self.table.setAlternatingRowColors(True) - self.table.setSelectionBehavior(QTableWidget.SelectRows) - self.table.setColumnWidth(0, 180) - self.table.setColumnWidth(1, 70) - self.table.setColumnWidth(2, 90) - self.table.setColumnWidth(3, 70) - self.table.setColumnWidth(4, 100) - self.table.setColumnWidth(5, 120) - table_layout.addWidget(self.table) - table_group.setLayout(table_layout) - layout.addWidget(table_group) - - btn_group = QGroupBox("Управление") - btn_layout = QHBoxLayout() - self.add_row_btn = QPushButton("➕ Добавить реагент") - self.remove_row_btn = QPushButton("➖ Удалить реагент") - self.remove_row_btn.setObjectName("danger") - btn_layout.addWidget(self.add_row_btn) - btn_layout.addWidget(self.remove_row_btn) - btn_layout.addStretch() - self.calculate_btn = QPushButton("🧮 Рассчитать") - self.calculate_btn.setObjectName("success") - self.save_btn = QPushButton("💾 Сохранить") - self.load_btn = QPushButton("📂 Загрузить") - - # Новая кнопка для передачи в DoE - self.to_doe_btn = QPushButton("🎯 В DoE") - self.to_doe_btn.setObjectName("doe") - self.to_doe_btn.setToolTip("Передать состав реагентов в планировщик эксперимента\n" - "Реагенты станут факторами, их концентрации — нулевыми уровнями") - - btn_layout.addWidget(self.to_doe_btn) - btn_layout.addWidget(self.calculate_btn) - btn_layout.addWidget(self.save_btn) - btn_layout.addWidget(self.load_btn) - btn_group.setLayout(btn_layout) - layout.addWidget(btn_group) - - info_frame = QFrame() - info_frame.setFrameShape(QFrame.StyledPanel) - info_frame.setStyleSheet("background-color: #e3f2fd; border-radius: 5px;") - info_layout = QHBoxLayout(info_frame) - info_label = QLabel("ℹ️ Подсказка: Реагенты в массовых единицах не учитываются при расчёте объёма растворителя") - info_layout.addWidget(info_label) - info_layout.addStretch() - layout.addWidget(info_frame) - - def add_initial_rows(self): - self.add_solvent_row() - self.add_new_row() - - def add_solvent_row(self): - row = self.table.rowCount() - self.table.insertRow(row) - solvent_item = QTableWidgetItem(self.solvent_input.text()) - solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable) - solvent_item.setBackground(QColor(230, 230, 230)) - font = QFont() - font.setBold(True) - solvent_item.setFont(font) - self.table.setItem(row, 0, solvent_item) - for col in [1, 3, 4]: - item = QTableWidgetItem("-") - item.setFlags(item.flags() & ~Qt.ItemIsEditable) - item.setBackground(QColor(230, 230, 230)) - self.table.setItem(row, col, item) - unit_item = QTableWidgetItem(self.amount_unit_combo.currentText()) - unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable) - unit_item.setBackground(QColor(230, 230, 230)) - self.table.setItem(row, 2, unit_item) - result_item = QTableWidgetItem("") - result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable) - self.table.setItem(row, 5, result_item) - - def update_solvent_name(self): - if self.table.rowCount() > 0 and self.table.item(0, 0): - self.table.item(0, 0).setText(self.solvent_input.text()) - - def add_new_row(self): - row = self.table.rowCount() - self.table.insertRow(row) - self.table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row}")) - self.table.setItem(row, 1, QTableWidgetItem("0")) - unit_combo = QComboBox() - unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) - unit_combo.setCurrentText("мг") - self.table.setCellWidget(row, 2, unit_combo) - self.table.setItem(row, 3, QTableWidgetItem("1")) - self.table.setItem(row, 4, QTableWidgetItem("1")) - self.table.setItem(row, 5, QTableWidgetItem("")) - - def remove_selected_row(self): - for row in sorted(set(item.row() for item in self.table.selectedItems()), reverse=True): - if row > 0: - self.table.removeRow(row) - - def get_reagents_data(self): - """Возвращает список реагентов для передачи в DoE""" - reagents = [] - for row in range(1, self.table.rowCount()): - name_item = self.table.item(row, 0) - percent_item = self.table.item(row, 1) - unit_widget = self.table.cellWidget(row, 2) - coeff_item = self.table.item(row, 3) - - if not all([name_item, percent_item]): - continue - - try: - name = name_item.text() - percentage = float(percent_item.text()) - unit = unit_widget.currentText() if unit_widget else "мг" - conversion_factor = float(coeff_item.text()) if coeff_item else 1.0 - - reagents.append({ - 'name': name, - 'percentage': percentage, - 'unit': unit, - 'conversion_factor': conversion_factor - }) - except ValueError: - continue - return reagents - - def get_table_data(self): - data = [] - for row in range(1, self.table.rowCount()): - name = self.table.item(row, 0).text() if self.table.item(row, 0) else "" - percent = self.table.item(row, 1).text() if self.table.item(row, 1) else "0" - unit_widget = self.table.cellWidget(row, 2) - unit = unit_widget.currentText() if unit_widget else "мг" - coeff = self.table.item(row, 3).text() if self.table.item(row, 3) else "1" - dilution = self.table.item(row, 4).text() if self.table.item(row, 4) else "1" - data.append([name, percent, unit, coeff, dilution]) - return data - - def update_solvent_percent(self, solvent_percent: float): - if self.table.rowCount() > 0 and self.table.item(0, 1): - self.table.item(0, 1).setText(self.format_number(solvent_percent)) - - def update_solvent_result(self, solvent_amount: float, unit: str): - if self.table.rowCount() > 0: - if self.table.item(0, 5): - self.table.item(0, 5).setText(self.format_number(solvent_amount)) - self.table.item(0, 5).setBackground(QColor(220, 255, 220)) - if self.table.item(0, 2): - self.table.item(0, 2).setText(unit) - - def update_results(self, results: list): - for row, amount in enumerate(results, start=1): - if row < self.table.rowCount() and self.table.item(row, 5): - self.table.item(row, 5).setText(self.format_number(amount)) - self.table.item(row, 5).setBackground(QColor(220, 255, 220)) - - def clear_results(self): - for row in range(self.table.rowCount()): - if self.table.item(row, 5): - self.table.item(row, 5).setText("") - if row == 0: - self.table.item(row, 5).setBackground(QColor(230, 230, 230)) - else: - self.table.item(row, 5).setBackground(QColor(250, 250, 250)) - if self.table.rowCount() > 0 and self.table.item(0, 1): - self.table.item(0, 1).setText("") - if self.table.rowCount() > 0 and self.table.item(0, 2): - self.table.item(0, 2).setText(self.amount_unit_combo.currentText()) - - def format_number(self, value): - if value == int(value): - return str(int(value)) - formatted = f"{value:.6f}".rstrip('0').rstrip('.') - if '.' in formatted and len(formatted.split('.')[1]) > 4: - formatted = f"{value:.4f}".rstrip('0').rstrip('.') - return formatted - - def show_error(self, message: str): - QMessageBox.critical(self, "Ошибка", message) - - def show_info(self, message: str): - QMessageBox.information(self, "Информация", message)