Реализация функционала полнофакторного эксперимента

This commit is contained in:
2026-05-06 23:10:55 +05:00
parent 361b934e8a
commit 15193d2403
5 changed files with 751 additions and 212 deletions
+12 -16
View File
@@ -1,7 +1,8 @@
from PyQt5.QtWidgets import QMessageBox, QFileDialog, QTableWidgetItem, QComboBox, QLineEdit
from PyQt5.QtWidgets import QMessageBox, QFileDialog, QTableWidgetItem, QComboBox, QLineEdit, QDoubleSpinBox
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
from model import Model
from view import MainWindow as MediumCalculatorView
from view import MediumCalculatorWindow
import json
from reagent import Reagent
@@ -9,10 +10,9 @@ from reagent import Reagent
class Controller:
def __init__(self):
self.model = Model()
self.view = MediumCalculatorView()
self.view = MediumCalculatorWindow()
self._connect_signals()
# Убираем автоматический показ окна - теперь он вызывается из главного меню
# self.view.show()
def _connect_signals(self):
"""Подключает обработчики событий интерфейса"""
@@ -118,26 +118,22 @@ class Controller:
self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}"))
# Единица - QComboBox
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}"))
# Создаём поле для разбавления (QLineEdit для ручного ввода)
dilution_edit = QLineEdit()
dilution_edit.setText(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}")
dilution_edit.setAlignment(Qt.AlignRight)
dilution_edit.setToolTip("Во сколько раз разбавить (1 = без разбавления)")
self.view.table.setCellWidget(row, 4, dilution_edit)
# Разбавление - обычная ячейка
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()
def save_composition(self):
"""Сохраняет состав среды в JSON-файл"""
filename, _ = QFileDialog.getSaveFileName(
+437 -80
View File
@@ -2,16 +2,19 @@ from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QWidget, QMessageBox,
QTableWidget, QTableWidgetItem, QGroupBox,
QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit,
QTextEdit, QTabWidget, QFormLayout)
QTextEdit, QTabWidget, QFormLayout, QCheckBox,
QScrollArea, QFileDialog)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from PyQt5.QtGui import QColor, QFont
import numpy as np
import csv
class ExperimentDesignWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика")
self.setGeometry(200, 100, 1000, 750)
self.setGeometry(200, 100, 1200, 800)
self.setStyleSheet("""
QMainWindow {
background-color: #f5f5f5;
@@ -27,6 +30,7 @@ class ExperimentDesignWindow(QMainWindow):
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #1565C0;
}
QPushButton {
background-color: #4CAF50;
@@ -42,6 +46,13 @@ class ExperimentDesignWindow(QMainWindow):
QTableWidget {
gridline-color: #ddd;
}
QHeaderView::section {
background-color: #1565C0;
color: white;
padding: 8px;
border: none;
font-weight: bold;
}
""")
self._init_ui()
@@ -71,26 +82,41 @@ class ExperimentDesignWindow(QMainWindow):
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(4)
self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нижний уровень (-1)", "Верхний уровень (+1)", "Единица измерения"])
self.factors_table.setRowCount(3)
# Новый порядок колонок: Фактор, Нулевой уровень, Шаг, Верхний (+1), Нижний (-1), Единица измерения
self.factors_table.setColumnCount(6)
self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг",
"Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"])
self.factors_table.setRowCount(2)
# Пример данных
sample_factors = [
["Температура", "25", "37", "°C"],
["pH", "6.5", "7.5", ""],
["Концентрация глюкозы", "5", "20", "г"]
["Температура", "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):
self.factors_table.setItem(i, j, QTableWidgetItem(value))
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)
@@ -108,6 +134,27 @@ class ExperimentDesignWindow(QMainWindow):
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)
self.center_points_spin.setToolTip("Повторные опыты в нулевой точке для оценки дисперсии")
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()
@@ -128,7 +175,6 @@ class ExperimentDesignWindow(QMainWindow):
responses_layout.addWidget(self.responses_table)
# Кнопки для управления откликами
response_buttons = QHBoxLayout()
add_response_btn = QPushButton("+ Добавить отклик")
add_response_btn.clicked.connect(self.add_response_row)
@@ -148,17 +194,38 @@ class ExperimentDesignWindow(QMainWindow):
plan_tab = QWidget()
plan_layout = QVBoxLayout(plan_tab)
plan_info = QLabel("Полнофакторный план эксперимента (Full Factorial Design)")
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()
plan_layout.addWidget(self.design_matrix)
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)
plan_layout.addWidget(generate_btn)
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, "📊 Матрица планирования")
@@ -166,58 +233,29 @@ class ExperimentDesignWindow(QMainWindow):
analysis_tab = QWidget()
analysis_layout = QVBoxLayout(analysis_tab)
analysis_info = QLabel("Регрессионный анализ и визуализация результатов")
analysis_info = QLabel("Введите результаты экспериментов для анализа")
analysis_info.setAlignment(Qt.AlignCenter)
analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
analysis_layout.addWidget(analysis_info)
# Заглушка для анализа
placeholder = QLabel("Здесь будет:\n\n"
"• Множественная линейная регрессия\n"
"• Анализ взаимодействий факторов\n"
"• ANOVA (дисперсионный анализ)\n"
"• Построение поверхностей отклика\n"
"• Графики главных эффектов\n"
"• Оптимизация параметров")
placeholder.setAlignment(Qt.AlignCenter)
placeholder.setStyleSheet("color: #666; font-size: 12px; border: 1px dashed #ccc; padding: 20px;")
analysis_layout.addWidget(placeholder)
self.results_table = QTableWidget()
analysis_layout.addWidget(self.results_table)
analyze_btn = QPushButton("📈 Провести анализ (в разработке)")
analyze_btn.clicked.connect(self.show_placeholder_message)
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, "📐 Анализ результатов")
# Вкладка 4: Визуализация
viz_tab = QWidget()
viz_layout = QVBoxLayout(viz_tab)
viz_info = QLabel("Интерактивная визуализация данных")
viz_info.setAlignment(Qt.AlignCenter)
viz_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
viz_layout.addWidget(viz_info)
viz_placeholder = QLabel("Здесь будут:\n\n"
"• 2D и 3D графики поверхностей отклика\n"
"• Контурные графики\n"
"• Диаграммы Парето\n"
"• Графики нормальной вероятности")
viz_placeholder.setAlignment(Qt.AlignCenter)
viz_placeholder.setStyleSheet("color: #666; font-size: 12px; border: 1px dashed #ccc; padding: 20px;")
viz_layout.addWidget(viz_placeholder)
tabs.addTab(viz_tab, "📈 Визуализация")
layout.addWidget(tabs)
# Кнопки управления
btn_layout = QHBoxLayout()
export_btn = QPushButton("💾 Экспорт отчёта (PDF/Excel)")
export_btn.clicked.connect(self.show_placeholder_message)
btn_layout.addWidget(export_btn)
save_btn = QPushButton("💿 Сохранить проект")
save_btn.clicked.connect(self.show_placeholder_message)
btn_layout.addWidget(save_btn)
@@ -234,6 +272,36 @@ class ExperimentDesignWindow(QMainWindow):
layout.addLayout(btn_layout)
def on_factor_changed(self, item):
"""При изменении нулевого уровня или шага пересчитываем верхний и нижний уровни"""
row = item.row()
col = item.column()
# Если изменили нулевой уровень (колонка 1) или шаг (колонка 2)
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)
high_item = self.factors_table.item(row, 3)
if high_item:
high_item.setText(f"{high:.3f}".rstrip('0').rstrip('.'))
low_item = self.factors_table.item(row, 4)
if low_item:
low_item.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()
@@ -241,7 +309,19 @@ class ExperimentDesignWindow(QMainWindow):
self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}"))
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
self.factors_table.setItem(row, 3, QTableWidgetItem(""))
# Верхний и нижний уровни - только для чтения
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):
"""Удаляет последнюю строку факторов"""
@@ -260,53 +340,330 @@ class ExperimentDesignWindow(QMainWindow):
if self.responses_table.rowCount() > 1:
self.responses_table.removeRow(self.responses_table.rowCount() - 1)
def generate_design_matrix(self):
"""Генерирует матрицу планирования (заглушка)"""
n_factors = self.factors_table.rowCount()
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):
"""Генерирует полнофакторный план 2^k с центральными точками"""
k = len(factors)
if k == 0:
return []
if n_factors == 0:
# Генерируем 2^k комбинаций
n_factorial = 2 ** k
design = []
for i in range(n_factorial):
experiment = {}
for j in range(k):
# Кодированный уровень (-1 или +1)
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1
# Переводим в натуральные значения
if coded_level == -1:
natural_value = factors[j]['low']
else:
natural_value = 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():
import random
random.shuffle(design)
return design
def generate_design_matrix(self):
"""Генерирует и отображает матрицу планирования"""
factors = self.get_factors_data()
if len(factors) == 0:
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
return
# Полнофакторный план: 2^k опытов
n_experiments = 2 ** n_factors
# Генерируем план
design = self.calculate_factorial_design(factors)
# Количество опытов
n_experiments = len(design)
n_factors = len(factors)
# Настройка таблицы
self.design_matrix.setRowCount(n_experiments)
self.design_matrix.setColumnCount(n_factors + 1)
self.design_matrix.setColumnCount(n_factors + 2)
# Заголовки
headers = ["Опыт"] + [self.factors_table.item(i, 0).text() if self.factors_table.item(i, 0) else f"Фактор_{i+1}"
for i in range(n_factors)]
headers = ["опыта"] + [f["name"] for f in factors] + ["Тип точки"]
self.design_matrix.setHorizontalHeaderLabels(headers)
# Заполняем матрицу (простой 2^k план)
for exp in range(n_experiments):
# Заполняем матрицу
for exp_idx, experiment in enumerate(design):
# Номер опыта
self.design_matrix.setItem(exp, 0, QTableWidgetItem(str(exp + 1)))
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
# Кодированные уровни факторов (-1 или +1)
for factor in range(n_factors):
level = -1 if (exp // (2 ** factor)) % 2 == 0 else 1
self.design_matrix.setItem(exp, factor + 1, QTableWidgetItem(str(level)))
# Значения факторов
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"
f"Общее количество опытов: {n_experiments}")
f"Факторных точек: {n_factorial}\n"
f"Центральных точек: {n_center}\n"
f"Всего опытов: {n_experiments}\n\n"
f"Центральные точки позволяют оценить дисперсию воспроизводимости")
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):
"""Экспортирует матрицу планирования в CSV"""
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 = []
for j in range(self.design_matrix.columnCount()):
item = self.design_matrix.item(i, j)
row.append(item.text() if item else "")
writer.writerow(row)
QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}")
def perform_analysis(self):
"""Проводит регрессионный анализ"""
n_responses = self.responses_table.rowCount()
if n_responses == 0:
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)
# Проверяем, что все результаты введены
missing = False
for i, row in enumerate(results):
for j, val in enumerate(row):
if val is None:
missing = True
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.calculate_factorial_design(factors)
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)
self.analysis_output.append(f"Среднее значение: {mean_y:.4f}")
# Дисперсия
variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0
self.analysis_output.append(f"Общая дисперсия: {variance:.4f}")
# Стандартное отклонение
std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0
self.analysis_output.append(f"Стандартное отклонение: {std_dev:.4f}")
# Коэффициент вариации
if mean_y != 0:
cv = (std_dev / mean_y) * 100
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)
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}")
self.analysis_output.append(f" Стандартное отклонение: {np.std(center_y, ddof=1):.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_placeholder_message(self):
"""Показывает сообщение о том, что функция в разработке"""
QMessageBox.information(
self,
"В разработке",
"🧪 Биотехнологические инструменты в стадии активной разработки!\n\n"
"В ближайшее время здесь появится:\n\n"
"✅ Полнофакторный план (2^k факторный дизайн)\n"
"✅ Регрессионный анализ и ANOVA\n"
"🧪 Функция в стадии разработки!\n\nБлижайшие обновления:\n"
"✅ Экспорт в Excel\n"
"✅ Построение поверхностей отклика\n"
"Оптимизация параметров\n"
"Экспорт в Excel и PDF\n"
"✅ Визуализация 2D/3D графиков\n\n"
"Следите за обновлениями!"
"Графики главных эффектов\n"
"Полный регрессионный анализ"
)
+6 -3
View File
@@ -6,9 +6,12 @@ from main_window import MainWindow
def main():
app = QApplication(sys.argv)
# Создаём главное окно с выбором режима
main_window = MainWindow()
main_window.show()
# Устанавливаем стиль приложения
app.setStyle('Fusion')
# Создаём главное окно цифрового помощника биохимика
assistant = MainWindow()
assistant.show()
# Запускаем цикл обработки событий
sys.exit(app.exec_())
+6 -6
View File
@@ -1,12 +1,12 @@
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QWidget, QFrame)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QIcon
from PyQt5.QtGui import QFont
from controller import Controller
from experiment_design import ExperimentDesignWindow
class DigitalBiochemistAssistant(QMainWindow):
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Цифровой помощник биохимика - Главное меню")
@@ -48,7 +48,7 @@ class DigitalBiochemistAssistant(QMainWindow):
layout.setContentsMargins(50, 50, 50, 50)
# Заголовок
title_label = QLabel("🧬 Цифровой помощник биохимика 🧪")
title_label = QLabel("Цифровой помощник биохимика")
title_font = QFont()
title_font.setPointSize(20)
title_font.setBold(True)
@@ -69,7 +69,7 @@ class DigitalBiochemistAssistant(QMainWindow):
layout.addSpacing(20)
# Кнопка 1: Калькулятор питательных сред
btn_medium = QPushButton("🥼 Калькулятор питательных сред")
btn_medium = QPushButton("Калькулятор питательных сред")
btn_medium.setMinimumHeight(80)
btn_medium.clicked.connect(self.open_medium_calculator)
layout.addWidget(btn_medium)
@@ -85,7 +85,7 @@ class DigitalBiochemistAssistant(QMainWindow):
layout.addSpacing(15)
# Кнопка 2: Планирование эксперимента
btn_experiment = QPushButton("📊 Планирование эксперимента (DoE)")
btn_experiment = QPushButton("Планирование эксперимента (DoE)")
btn_experiment.setMinimumHeight(80)
btn_experiment.clicked.connect(self.open_experiment_designer)
layout.addWidget(btn_experiment)
@@ -110,7 +110,7 @@ class DigitalBiochemistAssistant(QMainWindow):
bottom_layout = QHBoxLayout()
# Информация о версии
version_label = QLabel("Версия 2.0 | © 2024 Цифровой помощник биохимика")
version_label = QLabel("Версия alpha 0.1.2 | © 2026 Цифровой помощник биохимика")
version_label.setStyleSheet("color: #999; font-size: 10px;")
bottom_layout.addWidget(version_label)
+290 -107
View File
@@ -1,239 +1,422 @@
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QPushButton,
QLabel, QDoubleSpinBox, QComboBox, QLineEdit,
QWidget, QMessageBox)
QWidget, QMessageBox, QGroupBox, QFrame, QHeaderView)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QColor
class MainWindow(QMainWindow):
class MediumCalculatorWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Калькулятор питательных сред")
self.setGeometry(100, 100, 1000, 600)
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;
}
QTableWidget {
gridline-color: #ddd;
background-color: white;
alternate-background-color: #f9f9f9;
}
QHeaderView::section {
background-color: #1565C0;
color: white;
padding: 8px;
border: none;
font-weight: bold;
}
QPushButton {
background-color: #2196F3;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #1976D2;
}
QPushButton:pressed {
background-color: #0D47A1;
}
QPushButton#danger {
background-color: #f44336;
}
QPushButton#danger:hover {
background-color: #da190b;
}
QPushButton#success {
background-color: #4CAF50;
}
QPushButton#success:hover {
background-color: #45a049;
}
QDoubleSpinBox, QLineEdit {
padding: 4px;
border: 1px solid #ccc;
border-radius: 3px;
background-color: white;
color: black;
font-size: 12px;
min-height: 20px;
}
QComboBox {
border: 1px solid #ccc;
border-radius: 3px;
background-color: white;
color: black;
font-size: 12px;
min-height: 20px;
padding: 2px;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox QAbstractItemView {
background-color: white;
color: black;
selection-background-color: #2196F3;
selection-color: white;
}
QLabel {
color: black;
font-size: 12px;
}
QLabel#title {
font-size: 18px;
font-weight: bold;
color: #0D47A1;
}
QLabel#info {
color: #1565C0;
font-size: 11px;
}
""")
self._init_ui()
def _init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout()
layout = QVBoxLayout(central_widget)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
# Верхняя панель: параметры среды
top_layout = QHBoxLayout()
top_layout.addWidget(QLabel("Общее количество:"))
title_label = QLabel("Калькулятор питательных сред")
title_label.setObjectName("title")
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()
params_layout.setSpacing(20)
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)
top_layout.addWidget(self.amount_input)
self.amount_input.setMinimumWidth(150)
amount_layout.addWidget(self.amount_input)
self.amount_unit_combo = QComboBox()
self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"])
self.amount_unit_combo.setCurrentText("мл")
top_layout.addWidget(self.amount_unit_combo)
self.amount_unit_combo.setMinimumWidth(80)
amount_layout.addWidget(self.amount_unit_combo)
params_layout.addLayout(amount_layout)
top_layout.addWidget(QLabel("Растворитель:"))
solvent_layout = QHBoxLayout()
solvent_layout.addWidget(QLabel("Растворитель:"))
self.solvent_input = QLineEdit("Вода")
top_layout.addWidget(self.solvent_input)
layout.addLayout(top_layout)
self.solvent_input.setMinimumWidth(150)
solvent_layout.addWidget(self.solvent_input)
params_layout.addLayout(solvent_layout)
# Таблица реагентов (с растворителем в первой строке)
layout.addWidget(QLabel("Состав среды:"))
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) # Увеличиваем до 6 колонок
self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Результат"])
layout.addWidget(self.table)
self.table.setColumnCount(6)
self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"])
self.table.setAlternatingRowColors(True)
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed)
self.table.verticalHeader().setVisible(False)
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()
btn_layout.setSpacing(10)
self.add_row_btn = QPushButton("Добавить реагент")
self.remove_row_btn = QPushButton("Удалить реагент")
self.calculate_btn = QPushButton("Рассчитать")
self.save_btn = QPushButton("Сохранить")
self.load_btn = QPushButton("Загрузить")
self.add_row_btn.setMinimumWidth(150)
btn_layout.addWidget(self.add_row_btn)
btn_layout.addWidget(self.remove_row_btn)
btn_layout.addWidget(self.calculate_btn)
btn_layout.addWidget(self.save_btn)
btn_layout.addWidget(self.load_btn)
layout.addLayout(btn_layout)
central_widget.setLayout(layout)
self.remove_row_btn = QPushButton("Удалить реагент")
self.remove_row_btn.setObjectName("danger")
self.remove_row_btn.setMinimumWidth(150)
btn_layout.addWidget(self.remove_row_btn)
btn_layout.addStretch()
self.calculate_btn = QPushButton("Рассчитать")
self.calculate_btn.setObjectName("success")
self.calculate_btn.setMinimumWidth(150)
btn_layout.addWidget(self.calculate_btn)
self.save_btn = QPushButton("Сохранить")
self.save_btn.setMinimumWidth(150)
btn_layout.addWidget(self.save_btn)
self.load_btn = QPushButton("Загрузить")
self.load_btn.setMinimumWidth(150)
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_label.setObjectName("info")
info_layout.addWidget(info_label)
info_layout.addStretch()
layout.addWidget(info_frame)
self.add_initial_rows()
def add_initial_rows(self):
"""Добавляет начальные строки: растворитель и первый реагент"""
# Добавляем строку растворителя (первая, нередактируемая)
self.add_solvent_row()
# Добавляем строку для первого реагента
self.add_new_row()
def add_solvent_row(self):
"""Добавляет строку растворителя (нередактируемая)"""
row_count = self.table.rowCount()
self.table.insertRow(row_count)
# Название растворителя (берём из поля ввода)
self.table.setRowHeight(row_count, 30)
solvent_name = self.solvent_input.text()
solvent_item = QTableWidgetItem(solvent_name)
solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable)
solvent_item.setBackground(Qt.lightGray)
solvent_item.setBackground(QColor(230, 230, 230))
solvent_item.setForeground(QColor(0, 0, 0))
font = QFont()
font.setBold(True)
solvent_item.setFont(font)
self.table.setItem(row_count, 0, solvent_item)
# Процент (будет рассчитан автоматически)
percent_item = QTableWidgetItem("")
percent_item.setFlags(percent_item.flags() & ~Qt.ItemIsEditable)
percent_item.setBackground(Qt.lightGray)
percent_item.setBackground(QColor(230, 230, 230))
percent_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 1, percent_item)
# Единица измерения (не используется для растворителя)
unit_item = QTableWidgetItem("-")
unit_item = QTableWidgetItem(self.amount_unit_combo.currentText())
unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable)
unit_item.setBackground(Qt.lightGray)
unit_item.setBackground(QColor(230, 230, 230))
unit_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 2, unit_item)
# Коэффициент (не используется для растворителя)
coeff_item = QTableWidgetItem("-")
coeff_item.setFlags(coeff_item.flags() & ~Qt.ItemIsEditable)
coeff_item.setBackground(Qt.lightGray)
coeff_item.setBackground(QColor(230, 230, 230))
coeff_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 3, coeff_item)
# Разбавление (не используется для растворителя)
dilution_item = QTableWidgetItem("-")
dilution_item.setFlags(dilution_item.flags() & ~Qt.ItemIsEditable)
dilution_item.setBackground(Qt.lightGray)
dilution_item.setBackground(QColor(230, 230, 230))
dilution_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 4, dilution_item)
# Результат (будет заполнен при расчёте)
result_item = QTableWidgetItem("")
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
result_item.setBackground(Qt.lightGray)
result_item.setBackground(QColor(240, 240, 240))
result_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 5, result_item)
def update_solvent_name(self):
"""Обновляет название растворителя в первой строке таблицы"""
solvent_name = self.solvent_input.text()
name_item = self.table.item(0, 0)
if name_item:
name_item.setText(solvent_name)
def format_number(self, value):
if value == int(value):
return str(int(value))
else:
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 add_new_row(self):
"""Добавляет новую строку для реагента"""
row_count = self.table.rowCount()
self.table.insertRow(row_count)
self.table.setRowHeight(row_count, 30)
self.table.setItem(row_count, 0, QTableWidgetItem(f"Реагент_{row_count}"))
self.table.setItem(row_count, 1, QTableWidgetItem("0.0"))
name_item = QTableWidgetItem(f"Реагент_{row_count}")
name_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 0, name_item)
percent_item = QTableWidgetItem("0")
percent_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 1, percent_item)
unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText("г")
unit_combo.setCurrentText("мг")
self.table.setCellWidget(row_count, 2, unit_combo)
self.table.setItem(row_count, 3, QTableWidgetItem("1.0"))
# Разбавление - обычное текстовое поле
dilution_edit = QLineEdit("1.0")
dilution_edit.setAlignment(Qt.AlignRight)
dilution_edit.setToolTip("Во сколько раз разбавить (1 = без разбавления)")
self.table.setCellWidget(row_count, 4, dilution_edit)
self.table.setItem(row_count, 5, QTableWidgetItem(""))
coeff_item = QTableWidgetItem("1")
coeff_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 3, coeff_item)
dilution_item = QTableWidgetItem("1")
dilution_item.setForeground(QColor(0, 0, 0))
dilution_item.setFlags(dilution_item.flags() | Qt.ItemIsEditable)
self.table.setItem(row_count, 4, dilution_item)
result_item = QTableWidgetItem("")
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
result_item.setBackground(QColor(250, 250, 250))
result_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 5, result_item)
def remove_selected_row(self):
"""Удаляет выделенную строку из таблицы (кроме строки растворителя)"""
selected_rows = set()
for item in self.table.selectedItems():
selected_rows.add(item.row())
# Удаляем строки в обратном порядке, пропуская строку растворителя (индекс 0)
for row in sorted(selected_rows, reverse=True):
if row > 0: # Не удаляем строку растворителя
if row > 0:
self.table.removeRow(row)
def get_table_data(self) -> list:
"""Возвращает данные таблицы в виде списка списков (только реагенты, без растворителя)"""
data = []
# Начинаем с 1 строки (пропускаем растворитель)
for row in range(1, self.table.rowCount()):
row_data = []
# Название (колонка 0)
name_item = self.table.item(row, 0)
row_data.append(name_item.text() if name_item else "")
# Процент (колонка 1)
percent_item = self.table.item(row, 1)
row_data.append(percent_item.text() if percent_item else "0")
# Единица измерения (колонка 2 - комбобокс)
unit_widget = self.table.cellWidget(row, 2)
if unit_widget and isinstance(unit_widget, QComboBox):
row_data.append(unit_widget.currentText())
else:
row_data.append("мг")
# Коэффициент (колонка 3)
coeff_item = self.table.item(row, 3)
row_data.append(coeff_item.text() if coeff_item else "1.0")
# Разбавление (колонка 4 - spinbox)
dilution_widget = self.table.cellWidget(row, 4)
if dilution_widget and isinstance(dilution_widget, QDoubleSpinBox):
dilution_factor = dilution_widget.value()
row_data.append(coeff_item.text() if coeff_item else "1")
dilution_item = self.table.item(row, 4)
if dilution_item:
try:
dilution_factor = float(dilution_item.text())
except ValueError:
dilution_factor = 1.0
row_data.append(dilution_factor)
else:
row_data.append(1.0)
data.append(row_data)
return data
def update_solvent_percent(self, solvent_percent: float):
"""Обновляет процент растворителя в первой строке"""
percent_item = self.table.item(0, 1)
if percent_item:
percent_item.setText(f"{solvent_percent:.2f}")
percent_item.setText(self.format_number(solvent_percent))
def show_error(self, message: str):
"""Показывает сообщение об ошибке"""
QMessageBox.critical(self, "Ошибка", message)
def update_results(self, results: list):
"""Обновляет столбец результатов (индекс 5) в таблице"""
# Начинаем с 1 строки (реагенты), 0 строка - растворитель
for row, amount in enumerate(results, start=1):
if row < self.table.rowCount():
formatted_amount = f"{amount:.4f}"
self.table.setItem(row, 5, QTableWidgetItem(formatted_amount))
formatted_amount = self.format_number(amount)
result_item = QTableWidgetItem(formatted_amount)
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
result_item.setBackground(QColor(220, 255, 220))
result_item.setForeground(QColor(0, 0, 0))
font = QFont()
font.setBold(True)
result_item.setFont(font)
self.table.setItem(row, 5, result_item)
def update_solvent_result(self, solvent_amount: float, unit: str):
"""Обновляет результат для растворителя в первой строке"""
formatted_amount = f"{solvent_amount:.4f}"
formatted_amount = self.format_number(solvent_amount)
result_item = self.table.item(0, 5)
if result_item:
result_item.setText(formatted_amount)
# Также обновляем единицу измерения в колонке 2 для информации
result_item.setBackground(QColor(220, 255, 220))
result_item.setForeground(QColor(0, 0, 0))
unit_item = self.table.item(0, 2)
if unit_item:
unit_item.setText(unit)
def update_display(self, solvent: str, total_amount: float, amount_unit: str):
"""Обновляет отображение растворителя и общего количества среды"""
self.solvent_input.setText(solvent)
self.update_solvent_name()
self.amount_input.setValue(total_amount)
self.amount_unit_combo.setCurrentText(amount_unit)
def clear_results(self):
"""Очищает столбец результатов для всех строк"""
for row in range(self.table.rowCount()):
self.table.setItem(row, 5, QTableWidgetItem(""))
# Очищаем процент растворителя
result_item = self.table.item(row, 5)
if result_item:
result_item.setText("")
if row == 0:
result_item.setBackground(QColor(230, 230, 230))
else:
result_item.setBackground(QColor(250, 250, 250))
percent_item = self.table.item(0, 1)
if percent_item:
percent_item.setText("")
# Очищаем единицу измерения растворителя
unit_item = self.table.item(0, 2)
if unit_item:
unit_item.setText("-")
unit_item.setText(self.amount_unit_combo.currentText())