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

This commit is contained in:
2026-05-18 15:53:24 +05:00
parent e6e86e50a3
commit 3ddd05acff
38 changed files with 2820 additions and 3555 deletions
+4
View File
@@ -1,5 +1,6 @@
# Python
backup
Backup
*.json
__pycache__/
*.py[cod]
@@ -46,3 +47,6 @@ Thumbs.db
# Локальные настройки
*.local
*.sh
*.bash
-219
View File
@@ -1,219 +0,0 @@
# 🧪 Цифровой помощник биохимика
Биотехнологические инструменты для лаборатории: калькулятор питательных сред и планирование полнофакторного эксперимента (DoE).
---
## 📋 Содержание
- [Возможности](#-возможности)
- [Установка](#-установка)
- [Запуск](#-запуск)
- [Структура проекта](#-структура-проекта)
- [Калькулятор питательных сред](#-калькулятор-питательных-сред)
- [Планирование эксперимента](#-планирование-эксперимента)
- [Сохранение и загрузка](#-сохранение-и-загрузка)
- [Требования](#-требования)
- [Лицензия](#-лицензия)
---
## ✨ Возможности
### 🔬 Калькулятор питательных сред
- Расчёт состава питательной среды по процентному содержанию компонентов
- Поддержка массовых (нг, мкг, мг, г, кг) и объёмных (нл, мкл, мл, л) единиц
- Учёт коэффициента пересчёта для каждого реагента
- Учёт разбавления реагентов (фактор разбавления)
- Автоматический расчёт необходимого количества растворителя
- Сохранение и загрузка рецептов в JSON
### 📊 Планирование эксперимента (DoE)
- Полнофакторный план 2ᵏ (k факторов)
- Генерация матрицы планирования с центральными точками
- Рэндомизация порядка опытов
- Ввод и анализ результатов экспериментов
- Регрессионный анализ (среднее, дисперсия, стандартное отклонение, CV)
- Критерий Фишера для проверки адекватности модели
- Экспорт матрицы в CSV
---
## 🚀 Установка
### 1. Клонирование репозитория
```bash
git clone <repository-url>
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 — численные вычисления
-3
View File
@@ -1,3 +0,0 @@
"""Цифровой помощник биохимика - основная библиотека"""
__version__ = "1.0.0"
@@ -1,4 +0,0 @@
from .medium_controller import MediumController
from .experiment_controller import ExperimentController
__all__ = ['MediumController', 'ExperimentController']
@@ -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())
@@ -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()
@@ -1,5 +0,0 @@
from .reagent import Reagent
from .medium_model import MediumModel
from .experiment_model import ExperimentModel
__all__ = ['Reagent', 'MediumModel', 'ExperimentModel']
@@ -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
@@ -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
@@ -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})"
@@ -1,3 +0,0 @@
"""Утилиты и вспомогательные функции"""
__all__ = []
@@ -1,5 +0,0 @@
from .main_window import MainWindow
from .medium_view import MediumCalculatorWindow
from .experiment_view import ExperimentDesignWindow
__all__ = ['MainWindow', 'MediumCalculatorWindow', 'ExperimentDesignWindow']
@@ -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)
@@ -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()
@@ -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)
+28
View File
@@ -0,0 +1,28 @@
"""Библиотека расчётов для биохимика"""
from .medium import (
calculate_medium_composition,
convert_units,
VOLUME_UNITS,
MASS_UNITS
)
from .doe import (
generate_factorial_design,
analyze_experiment,
calculate_factor_levels,
create_factor_from_reagent, # Добавлено
FACTOR_TYPES
)
__all__ = [
'calculate_medium_composition',
'convert_units',
'VOLUME_UNITS',
'MASS_UNITS',
'generate_factorial_design',
'analyze_experiment',
'calculate_factor_levels',
'create_factor_from_reagent',
'FACTOR_TYPES',
]
+352
View File
@@ -0,0 +1,352 @@
"""
Планирование эксперимента (DoE)
Модуль для генерации полнофакторных планов экспериментов
и анализа результатов.
Основные функции:
- generate_factorial_design() - генерация плана
- analyze_experiment() - статистический анализ
- calculate_factor_levels() - расчёт уровней факторов
"""
from typing import List, Dict, Tuple, Optional
import random
import numpy as np
# Типы расчёта шага
FACTOR_TYPES = {
'absolute': 'абс', # абсолютный шаг
'relative': '%', # относительный шаг (процент от нулевого уровня)
}
def calculate_factor_levels(
center_value: float,
step_value: float,
step_type: str,
base_value: float = None
) -> Tuple[float, float]:
"""
РАССЧИТЫВАЕТ ВЕРХНИЙ И НИЖНИЙ УРОВНИ ФАКТОРА
Параметры:
center_value: нулевой уровень фактора (центральная точка)
step_value: значение шага
step_type: тип шага ("абс" - абсолютный, "%" - относительный)
base_value: базовое значение для относительного шага (если None, используется center_value)
Возвращает:
(high_level, low_level): верхний и нижний уровни
Пример:
>>> calculate_factor_levels(100, 10, "%")
(110.0, 90.0)
>>> calculate_factor_levels(100, 20, "абс")
(120.0, 80.0)
"""
# Определяем абсолютное значение шага
if step_type == "%":
base = base_value if base_value is not None else center_value
step_abs = center_value * step_value / 100
else: # "абс" или "absolute"
step_abs = step_value
high_level = center_value + step_abs
low_level = center_value - step_abs
return high_level, low_level
def generate_factorial_design(
factors: List[Dict],
center_points: int = 3,
randomize: bool = True
) -> List[Dict]:
"""
ГЕНЕРИРУЕТ ПОЛНОФАКТОРНЫЙ ПЛАН ЭКСПЕРИМЕНТА
Создаёт матрицу планирования для 2^k полнофакторного эксперимента
с добавлением центральных точек.
ПАРАМЕТРЫ:
----------
factors : List[Dict]
Список факторов. Каждый фактор - словарь с ключами:
- name (str): название фактора
- low (float): нижний уровень (-1)
- high (float): верхний уровень (+1)
- center (float): нулевой уровень (0)
- unit (str): единица измерения
- step (float, опционально): шаг варьирования
- step_type (str, опционально): тип шага ("абс" или "%")
Минимально необходимые ключи: name, low, high, center, unit
center_points : int
Количество центральных точек (повторений в центре плана)
По умолчанию 3
randomize : bool
Перемешивать ли порядок опытов случайным образом
По умолчанию True
ВОЗВРАЩАЕТ:
-----------
List[Dict]
Список экспериментов. Каждый эксперимент - словарь:
- для каждого фактора: "Фактор_N" с полями:
- coded: кодированное значение (-1, 0, +1)
- natural: натуральное значение
- name: название фактора
- unit: единица измерения
- is_center (bool): является ли точка центральной
- center_num (int): номер центральной точки (если is_center)
ПРИМЕР ИСПОЛЬЗОВАНИЯ:
---------------------
>>> factors = [
... {'name': 'Температура', 'low': 25, 'high': 37, 'center': 31, 'unit': '°C'},
... {'name': 'pH', 'low': 6.5, 'high': 7.5, 'center': 7.0, 'unit': ''}
... ]
>>> design = generate_factorial_design(factors, center_points=2)
>>> print(len(design)) # 2^2 + 2 = 6
6
>>> design[0]['Фактор_1']['coded'] # первый фактор в первом опыте
-1
"""
k = len(factors)
if k == 0:
return []
n_factorial = 2 ** k
design = []
# Генерация факторных точек (все комбинации уровней)
for i in range(n_factorial):
experiment = {}
for j in range(k):
# Кодированный уровень: -1 для 0, +1 для 1 в бите
# (k-1-j) для правильного порядка факторов
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1
# Натуральное значение
natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high']
experiment[f"Фактор_{j+1}"] = {
'coded': coded_level,
'natural': natural_value,
'name': factors[j]['name'],
'unit': factors[j].get('unit', '')
}
design.append(experiment)
# Добавление центральных точек
for i in range(center_points):
center_experiment = {}
for j in range(k):
center_experiment[f"Фактор_{j+1}"] = {
'coded': 0,
'natural': factors[j]['center'],
'name': factors[j]['name'],
'unit': factors[j].get('unit', '')
}
center_experiment['is_center'] = True
center_experiment['center_num'] = i + 1
design.append(center_experiment)
# Перемешивание порядка
if randomize:
random.shuffle(design)
return design
def analyze_experiment(
results: List[List[float]],
design: List[Dict],
responses: List[Dict]
) -> Dict:
"""
ПРОВОДИТ СТАТИСТИЧЕСКИЙ АНАЛИЗ РЕЗУЛЬТАТОВ ЭКСПЕРИМЕНТА
ПАРАМЕТРЫ:
----------
results : List[List[float]]
Матрица результатов. Каждая строка - эксперимент,
каждый столбец - отклик.
Размер: [n_experiments, n_responses]
design : List[Dict]
План эксперимента, возвращённый generate_factorial_design()
responses : List[Dict]
Список откликов. Каждый отклик - словарь с ключами:
- name (str): название отклика
- unit (str): единица измерения
ВОЗВРАЩАЕТ:
-----------
Dict
Словарь с анализом для каждого отклика:
{
'Название отклика': {
'mean': float, # среднее значение всех опытов
'variance': float, # общая дисперсия
'std_dev': float, # стандартное отклонение
'cv': float, # коэффициент вариации (%)
'factorial_values': list, # значения в факторных точках
'center_values': list, # значения в центральных точках
'center_variance': float, # дисперсия воспроизводимости
'n_factorial': int, # количество факторных точек
'n_center': int, # количество центральных точек
'fisher_ratio': float, # критерий Фишера (если применимо)
'model_adequate': bool|None # адекватность модели
}
}
ПРИМЕР ИСПОЛЬЗОВАНИЯ:
---------------------
>>> factors = [{'name': 'X', 'low': 0, 'high': 10, 'center': 5, 'unit': ''}]
>>> design = generate_factorial_design(factors, center_points=2)
>>> results = [[2.0], [8.0], [5.1], [4.9]] # 2 факторные + 2 центральные
>>> responses = [{'name': 'Выход', 'unit': '%'}]
>>> analysis = analyze_experiment(results, design, responses)
>>> print(analysis['Выход']['mean'])
5.0
"""
analysis = {}
for resp_idx, response in enumerate(responses):
resp_name = response.get('name', f'Отклик_{resp_idx+1}')
# Собираем все значения для этого отклика
y_values = [results[i][resp_idx] for i in range(len(results))]
# Базовые статистики
mean_y = np.mean(y_values)
variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0
std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0
cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0
# Разделяем факторные и центральные точки
factorial_y = []
center_y = []
for i, exp in enumerate(design):
if exp.get('is_center', False):
center_y.append(y_values[i])
else:
factorial_y.append(y_values[i])
# Анализ воспроизводимости
center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0
# Критерий Фишера для проверки адекватности модели
fisher_ratio = None
model_adequate = None
if len(center_y) > 1 and len(factorial_y) > 1:
factorial_variance = np.var(factorial_y, ddof=1)
if factorial_variance > 0 and center_variance > 0:
fisher_ratio = max(factorial_variance, center_variance) / min(factorial_variance, center_variance)
# Критическое значение F (приблизительное, для p=0.05)
# Более точное требует знания степеней свободы
model_adequate = fisher_ratio < 4.0
analysis[resp_name] = {
'mean': mean_y,
'variance': variance,
'std_dev': std_dev,
'cv': cv,
'factorial_values': factorial_y,
'center_values': center_y,
'center_variance': center_variance,
'n_factorial': len(factorial_y),
'n_center': len(center_y),
'fisher_ratio': fisher_ratio,
'model_adequate': model_adequate
}
return analysis
def create_factor_from_reagent(
reagent: Dict,
total_volume: float,
volume_unit: str,
step_percent: float = 10.0
) -> Dict:
"""
СОЗДАЁТ ФАКТОР ИЗ РЕАГЕНТА (для интеграции калькулятора и DoE)
Преобразует рассчитанный реагент в фактор для планирования эксперимента.
Параметры:
reagent: рассчитанный реагент (из calculate_medium_composition)
total_volume: общий объём среды
volume_unit: единица объёма
step_percent: шаг варьирования в процентах от нулевого уровня
Возвращает:
Dict: фактор для использования в generate_factorial_design()
"""
center_value = reagent.get('undiluted_amount', reagent.get('calculated_amount', 0))
step_value = center_value * step_percent / 100
high_level, low_level = calculate_factor_levels(
center_value, step_value, "абс"
)
return {
'name': reagent['name'],
'center': center_value,
'low': low_level,
'high': high_level,
'step': step_value,
'step_type': 'абс',
'unit': reagent.get('unit', volume_unit),
'percentage': reagent.get('percentage', 0),
'dilution_factor': reagent.get('dilution_factor', 1.0)
}
def create_factor_from_reagent(
reagent: Dict,
total_volume: float,
volume_unit: str,
step_percent: float = 10.0
) -> Dict:
"""
СОЗДАЁТ ФАКТОР ИЗ РЕАГЕНТА (для интеграции калькулятора и DoE)
Преобразует рассчитанный реагент в фактор для планирования эксперимента.
Параметры:
reagent: рассчитанный реагент (из calculate_medium_composition)
total_volume: общий объём среды
volume_unit: единица объёма
step_percent: шаг варьирования в процентах от нулевого уровня
Возвращает:
Dict: фактор для использования в generate_factorial_design()
"""
center_value = reagent.get('undiluted_amount', reagent.get('calculated_amount', 0))
step_value = center_value * step_percent / 100
high_level, low_level = calculate_factor_levels(
center_value, step_value, "абс"
)
return {
'name': reagent['name'],
'center': center_value,
'low': low_level,
'high': high_level,
'step': step_value,
'step_type': 'абс',
'unit': reagent.get('unit', volume_unit),
'percentage': reagent.get('percentage', 0),
'dilution_factor': reagent.get('dilution_factor', 1.0)
}
+211
View File
@@ -0,0 +1,211 @@
"""
Расчёт питательных сред
Этот модуль содержит функции для расчёта состава питательных сред
на основе процентов, коэффициентов конверсии и разбавления.
Основная функция: calculate_medium_composition()
"""
from typing import List, Dict, Tuple
# Константы для конверсии единиц измерения
# Базовые единицы: мкл для объёма, мг для массы
VOLUME_UNITS = {
'нл': 0.001, # нанолитры -> микролитры
'мкл': 1.0, # микролитры (база)
'мл': 1000.0, # миллилитры -> микролитры
'л': 1000000.0, # литры -> микролитры
}
MASS_UNITS = {
'нг': 0.000001, # нанограммы -> миллиграммы
'мкг': 0.001, # микрограммы -> миллиграммы
'мг': 1.0, # миллиграммы (база)
'г': 1000.0, # граммы -> миллиграммы
'кг': 1000000.0, # килограммы -> миллиграммы
}
def convert_units(value: float, from_unit: str, to_unit: str = None) -> float:
"""
Конвертирует значение между единицами объёма или массы
Параметры:
value: числовое значение
from_unit: исходная единица (например "мл" или "мг")
to_unit: целевая единица (если None, конвертирует в базовую)
Возвращает:
float: сконвертированное значение
Пример:
>>> convert_units(100, 'мл') # конвертирует в базовую (мкл)
100000.0
>>> convert_units(500, 'мкл', 'мл')
0.5
"""
# Определяем тип единицы (объём или масса)
if from_unit in VOLUME_UNITS:
units_map = VOLUME_UNITS
elif from_unit in MASS_UNITS:
units_map = MASS_UNITS
else:
raise ValueError(f"Неизвестная единица измерения: {from_unit}")
# Конвертируем в базовую единицу (мкл для объёма, мг для массы)
value_in_base = value * units_map[from_unit]
# Если нужна конвертация в другую единицу
if to_unit and to_unit in units_map:
return value_in_base / units_map[to_unit]
return value_in_base
def calculate_medium_composition(
total_volume: float,
volume_unit: str,
reagents: List[Dict],
solvent_name: str = "Вода"
) -> Dict:
"""
РАССЧИТЫВАЕТ СОСТАВ ПИТАТЕЛЬНОЙ СРЕДЫ
Эта функция является основной для расчёта питательных сред.
ВХОДНЫЕ ПАРАМЕТРЫ:
---------------
total_volume : float
Общий объём/количество среды (например 1000)
volume_unit : str
Единица измерения общего объёма: "нл", "мкл", "мл", "л"
reagents : List[Dict]
Список реагентов. Каждый реагент - словарь с ключами:
- name (str): название реагента
- percentage (float): процентное содержание в среде (0-100)
- unit (str): единица измерения реагента (нг, мкг, мг, г, кг, нл, мкл, мл, л)
- dilution_factor (float): фактор разбавления (необяз., по умолч. 1.0)
Пример реагента:
{
'name': 'Глюкоза',
'percentage': 2.5,
'unit': 'г',
'dilution_factor': 1.0
}
solvent_name : str, optional
Название растворителя (по умолчанию "Вода")
ВОЗВРАЩАЕМЫЙ СЛОВАРЬ:
--------------------
{
'total_volume': float, # Исходный объём
'total_unit': str, # Единица измерения
'solvent_name': str, # Название растворителя
'solvent_volume': float, # Объём растворителя
'solvent_percentage': float, # Процент растворителя
'reagents': List[Dict] # Список реагентов с рассчитанными количествами
}
Каждый реагент в возвращаемом списке содержит:
- все исходные поля
- calculated_amount (float): рассчитанное количество реагента
- undiluted_amount (float): количество до разбавления
- amount_unit (str): единица измерения (скопирована из unit)
ПРИМЕР ИСПОЛЬЗОВАНИЯ (в CLI):
-----------------------------
>>> reagents = [
... {'name': 'Глюкоза', 'percentage': 2.0, 'unit': 'г', 'conversion_factor': 1.0},
... {'name': 'Пептон', 'percentage': 1.0, 'unit': 'г', 'conversion_factor': 1.0}
... ]
>>> result = calculate_medium_composition(1000, 'мл', reagents)
>>> print(f"Растворитель: {result['solvent_volume']} мл")
Растворитель: 970.0 мл
>>> for r in result['reagents']:
... print(f"{r['name']}: {r['calculated_amount']} {r['unit']}")
Глюкоза: 20.0 г
Пептон: 10.0 г
"""
# Проверка входных данных
if total_volume <= 0:
raise ValueError(f"Общий объём должен быть положительным: {total_volume}")
if volume_unit not in VOLUME_UNITS:
raise ValueError(f"Неизвестная единица объёма: {volume_unit}")
# Суммируем проценты всех реагентов
total_percentage = sum(r.get('percentage', 0) for r in reagents)
if total_percentage > 100:
raise ValueError(
f"Сумма процентов ({total_percentage:.2f}%) превышает 100%"
)
# Конвертируем общий объём в базовую единицу (мкл)
total_base = convert_units(total_volume, volume_unit)
results = []
total_diluted_volume_base = 0 # объём разбавленных реагентов в мкл
for reagent in reagents:
# Извлекаем параметры с значениями по умолчанию
percentage = reagent.get('percentage', 0)
unit = reagent.get('unit', 'мг')
# print ("unit = ",unit)
# conversion_factor = reagent.get('conversion_factor', 1.0)
dilution_factor = reagent.get('dilution_factor', 1.0)
# Проверяем, является ли реагент жидкостью (объём) или твёрдым веществом (масса)
is_volume = unit in VOLUME_UNITS
# 1. Объём реагента в среде (исходя из процента)
amount_in_base = (percentage / 100) * total_base
# print ("amount_in_base = ",amount_in_base)
# 2. Применяем коэффициент конверсии
# adjusted_amount_base = amount_in_base * conversion_factor
# 3. Конвертируем в нужную единицу (без учёта разбавления)
# undiluted_amount = convert_units(adjusted_amount_base, volume_unit, unit)
undiluted_amount = convert_units(amount_in_base, 'мкл', unit)
# print ("volume_unit = ",volume_unit)
# 4. Применяем разбавление
if dilution_factor <= 0:
dilution_factor = 1.0
diluted_amount = undiluted_amount * dilution_factor
# print ("diluted_amount = ", diluted_amount)
# 5. Для объёмных реагентов учитываем в расчёте растворителя
if is_volume:
reagent_volume_base = convert_units(diluted_amount, unit)
total_diluted_volume_base += reagent_volume_base
# Сохраняем результат
reagent_result = reagent.copy()
reagent_result['calculated_amount'] = diluted_amount
reagent_result['undiluted_amount'] = undiluted_amount
reagent_result['amount_unit'] = unit
results.append(reagent_result)
# Рассчитываем объём растворителя
solvent_volume_base = total_base - total_diluted_volume_base
if solvent_volume_base < 0:
solvent_volume_base = 0
solvent_volume = convert_units(solvent_volume_base, 'мкл', volume_unit)
solvent_percentage = 100 - total_percentage
return {
'total_volume': total_volume,
'total_unit': volume_unit,
'solvent_name': solvent_name,
'solvent_volume': solvent_volume,
'solvent_percentage': solvent_percentage,
'reagents': results
}
+208
View File
@@ -0,0 +1,208 @@
"""
Модель данных проекта для сохранения/загрузки в JSON
"""
from typing import List, Dict, Optional
from dataclasses import dataclass, asdict
from datetime import datetime
import json
@dataclass
class ReagentData:
"""Данные реагента"""
name: str
percentage: float
unit: str
conversion_factor: float
dilution_factor: float
def to_dict(self) -> Dict:
return asdict(self)
@classmethod
def from_dict(cls, data: Dict) -> 'ReagentData':
return cls(
name=data['name'],
percentage=data['percentage'],
unit=data['unit'],
conversion_factor=data.get('conversion_factor', 1.0),
dilution_factor=data.get('dilution_factor', 1.0)
)
@dataclass
class FactorData:
"""Данные фактора эксперимента"""
name: str
center: float
low: float
high: float
step: float
step_type: str
unit: str
percentage: Optional[float] = None
dilution_factor: Optional[float] = None
def to_dict(self) -> Dict:
d = asdict(self)
# Удаляем None значения
return {k: v for k, v in d.items() if v is not None}
@classmethod
def from_dict(cls, data: Dict) -> 'FactorData':
return cls(
name=data['name'],
center=data['center'],
low=data['low'],
high=data['high'],
step=data.get('step', 0),
step_type=data.get('step_type', 'абс'),
unit=data.get('unit', ''),
percentage=data.get('percentage'),
dilution_factor=data.get('dilution_factor')
)
@dataclass
class ResponseData:
"""Данные отклика эксперимента"""
name: str
unit: str
def to_dict(self) -> Dict:
return asdict(self)
@classmethod
def from_dict(cls, data: Dict) -> 'ResponseData':
return cls(
name=data['name'],
unit=data.get('unit', '')
)
@dataclass
class ExperimentResultsData:
"""Результаты эксперимента"""
design: List[Dict] # План эксперимента
results: List[List[float]] # Результаты измерений
responses: List[ResponseData] # Отклики
def to_dict(self) -> Dict:
return {
'design': self.design,
'results': self.results,
'responses': [r.to_dict() for r in self.responses]
}
@classmethod
def from_dict(cls, data: Dict) -> 'ExperimentResultsData':
return cls(
design=data['design'],
results=data['results'],
responses=[ResponseData.from_dict(r) for r in data['responses']]
)
@dataclass
class ProjectData:
"""Полные данные проекта"""
# Информация о проекте
project_name: str
created_at: str
modified_at: str
version: str = "1.0"
# Данные калькулятора сред
medium_total_volume: float = 1000.0
medium_volume_unit: str = "мл"
medium_solvent: str = "Вода"
medium_reagents: List[ReagentData] = None
# Данные эксперимента
experiment_factors: List[FactorData] = None
experiment_responses: List[ResponseData] = None
experiment_center_points: int = 3
experiment_randomize: bool = True
experiment_results: Optional[ExperimentResultsData] = None
def __post_init__(self):
if self.medium_reagents is None:
self.medium_reagents = []
if self.experiment_factors is None:
self.experiment_factors = []
if self.experiment_responses is None:
self.experiment_responses = []
def to_dict(self) -> Dict:
"""Конвертирует в словарь для JSON"""
return {
'project_info': {
'name': self.project_name,
'created_at': self.created_at,
'modified_at': self.modified_at,
'version': self.version
},
'medium_calculator': {
'total_volume': self.medium_total_volume,
'volume_unit': self.medium_volume_unit,
'solvent': self.medium_solvent,
'reagents': [r.to_dict() for r in self.medium_reagents]
},
'experiment': {
'factors': [f.to_dict() for f in self.experiment_factors],
'responses': [r.to_dict() for r in self.experiment_responses],
'center_points': self.experiment_center_points,
'randomize': self.experiment_randomize,
'results': self.experiment_results.to_dict() if self.experiment_results else None
}
}
@classmethod
def from_dict(cls, data: Dict) -> 'ProjectData':
"""Создаёт объект из словаря"""
project_info = data.get('project_info', {})
medium = data.get('medium_calculator', {})
experiment = data.get('experiment', {})
# Создаём объект
obj = cls(
project_name=project_info.get('name', 'Новый проект'),
created_at=project_info.get('created_at', datetime.now().isoformat()),
modified_at=project_info.get('modified_at', datetime.now().isoformat()),
version=project_info.get('version', '1.0'),
medium_total_volume=medium.get('total_volume', 1000.0),
medium_volume_unit=medium.get('volume_unit', 'мл'),
medium_solvent=medium.get('solvent', 'Вода'),
medium_reagents=[ReagentData.from_dict(r) for r in medium.get('reagents', [])],
experiment_factors=[FactorData.from_dict(f) for f in experiment.get('factors', [])],
experiment_responses=[ResponseData.from_dict(r) for r in experiment.get('responses', [])],
experiment_center_points=experiment.get('center_points', 3),
experiment_randomize=experiment.get('randomize', True),
experiment_results=ExperimentResultsData.from_dict(experiment['results'])
if experiment.get('results') else None
)
return obj
def save_to_file(self, filename: str):
"""Сохраняет проект в JSON файл"""
self.modified_at = datetime.now().isoformat()
with open(filename, 'w', encoding='utf-8') as f:
json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
@classmethod
def load_from_file(cls, filename: str) -> 'ProjectData':
"""Загружает проект из JSON файла"""
with open(filename, 'r', encoding='utf-8') as f:
data = json.load(f)
return cls.from_dict(data)
def create_new_project(name: str = "Новый проект") -> ProjectData:
"""Создаёт новый проект с текущей датой"""
now = datetime.now().isoformat()
return ProjectData(
project_name=name,
created_at=now,
modified_at=now
)
+152
View File
@@ -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()
+1005
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+7 -9
View File
@@ -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()
+858 -915
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,2 +1,2 @@
PyQt5>=5.15.0
numpy>=1.19.0
numpy>=1.21.0
-2
View File
@@ -1,2 +0,0 @@
#!/bin/bash
python3 main.py
-3
View File
@@ -1,3 +0,0 @@
"""Цифровой помощник биохимика - основная библиотека"""
__version__ = "1.0.0"
-4
View File
@@ -1,4 +0,0 @@
from .medium_controller import MediumController
from .experiment_controller import ExperimentController
__all__ = ['MediumController', 'ExperimentController']
-23
View File
@@ -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())
-143
View File
@@ -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()
-5
View File
@@ -1,5 +0,0 @@
from .reagent import Reagent
from .medium_model import MediumModel
from .experiment_model import ExperimentModel
__all__ = ['Reagent', 'MediumModel', 'ExperimentModel']
-80
View File
@@ -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
-113
View File
@@ -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
-10
View File
@@ -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})"
-3
View File
@@ -1,3 +0,0 @@
"""Утилиты и вспомогательные функции"""
__all__ = []
-5
View File
@@ -1,5 +0,0 @@
from .main_window import MainWindow
from .medium_view import MediumCalculatorWindow
from .experiment_view import ExperimentDesignWindow
__all__ = ['MainWindow', 'MediumCalculatorWindow', 'ExperimentDesignWindow']
-532
View File
@@ -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)
-101
View File
@@ -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()
-251
View File
@@ -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)