Files
help_lab/gui.py
T

1043 lines
47 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Единый графический интерфейс для калькулятора сред и DoE
"""
from theme import Colors, Fonts, Spacing, ButtonStyles, get_full_stylesheet, apply_theme, TitleStyles
import sys
from typing import List, Dict, Optional
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTabWidget, QGroupBox, QLabel, QPushButton, QTableWidget,
QTableWidgetItem, QDoubleSpinBox, QComboBox, QLineEdit,
QMessageBox, QSpinBox, QCheckBox, QTextEdit, QFileDialog,
QScrollArea, QHeaderView, QToolBar
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QColor
# Импорты расчётов
from calculations.medium import (
calculate_medium_composition,
convert_units,
VOLUME_UNITS,
MASS_UNITS
)
from calculations.doe import (
generate_factorial_design,
analyze_experiment,
calculate_factor_levels,
get_active_factors,
get_inactive_factors,
FACTOR_TYPES
)
# Импорт моделей для JSON
try:
from calculations.models.project_data import (
ProjectData, ReagentData, FactorData,
ResponseData, ExperimentResultsData, create_new_project
)
JSON_SUPPORT = True
except ImportError:
JSON_SUPPORT = False
print("Предупреждение: Модуль project_data не найден, JSON поддержка отключена")
class MainWindow(QMainWindow):
"""Главное окно с калькулятором сред и планировщиком эксперимента"""
def __init__(self):
super().__init__()
self.setWindowTitle("Биохимический помощник")
self.setStyleSheet(get_full_stylesheet())
self.setGeometry(100, 100, 1300, 800)
self.setStyleSheet(self._get_stylesheet())
self.generated_design = None
self.last_medium_result = None
self.tab_widget = None
self._setup_ui()
self._add_file_toolbar()
def _get_stylesheet(self):
return get_full_stylesheet()
def _add_file_toolbar(self):
"""Добавляет панель инструментов с кнопками сохранения/загрузки"""
toolbar = QToolBar("Файл")
toolbar.setMovable(False)
self.addToolBar(toolbar)
if JSON_SUPPORT:
save_action = toolbar.addAction("💾 Сохранить проект")
save_action.triggered.connect(self.save_project_to_json)
load_action = toolbar.addAction("📂 Загрузить проект")
load_action.triggered.connect(self.load_project_from_json)
toolbar.addSeparator()
def _setup_ui(self):
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
# Заголовок
title = QLabel("Цифровой помощник биохимика")
title.setObjectName("mainTitle") # Стиль подтянется из theme
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
# Вкладки
self.tab_widget = QTabWidget()
self.tab_widget.addTab(self._create_medium_tab(), "🧪 Калькулятор сред")
self.tab_widget.addTab(self._create_factors_tab(), "📊 Факторы эксперимента")
self.tab_widget.addTab(self._create_design_tab(), "📋 Матрица планирования")
self.tab_widget.addTab(self._create_analysis_tab(), "📈 Анализ результатов")
layout.addWidget(self.tab_widget)
# Кнопка закрытия
close_btn = QPushButton("Закрыть")
close_btn.setObjectName("danger")
close_btn.clicked.connect(self.close)
close_btn.setMaximumWidth(150)
btn_layout = QHBoxLayout()
btn_layout.addStretch()
btn_layout.addWidget(close_btn)
layout.addLayout(btn_layout)
# ========== ВКЛАДКА 1: КАЛЬКУЛЯТОР СРЕД ==========
def _create_medium_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
# Параметры
params_box = QGroupBox("Параметры среды")
params_layout = QHBoxLayout()
self.total_volume_spin = QDoubleSpinBox()
self.total_volume_spin.setRange(0.001, 1000000)
self.total_volume_spin.setValue(100)
self.total_volume_spin.setSuffix(" ")
self.volume_unit_combo = QComboBox()
self.volume_unit_combo.addItems(["мл", "л", "мкл", "нл"])
self.volume_unit_combo.setCurrentText("мл")
params_layout.addWidget(QLabel("Общий объём:"))
params_layout.addWidget(self.total_volume_spin)
params_layout.addWidget(self.volume_unit_combo)
params_layout.addSpacing(30)
params_layout.addWidget(QLabel("Растворитель:"))
self.solvent_input = QLineEdit("Вода")
params_layout.addWidget(self.solvent_input)
params_layout.addStretch()
params_box.setLayout(params_layout)
layout.addWidget(params_box)
# Таблица реагентов
reagents_box = QGroupBox("Состав среды")
reagents_layout = QVBoxLayout()
self.reagents_table = QTableWidget()
self.reagents_table.setColumnCount(5)
self.reagents_table.setHorizontalHeaderLabels(
["Реагент", "%", "Единица", "Разбавление", "Количество"]
)
self.reagents_table.setAlternatingRowColors(True)
self.reagents_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
reagents_layout.addWidget(self.reagents_table)
# Кнопки управления реагентами
btn_layout = QHBoxLayout()
add_btn = QPushButton(" Добавить реагент")
add_btn.clicked.connect(self._add_reagent_row)
remove_btn = QPushButton(" Удалить реагент")
remove_btn.setObjectName("danger")
remove_btn.clicked.connect(self._remove_reagent_row)
calculate_btn = QPushButton("🧮 Рассчитать")
calculate_btn.setObjectName("success")
calculate_btn.clicked.connect(self._calculate_medium)
btn_layout.addWidget(add_btn)
btn_layout.addWidget(remove_btn)
btn_layout.addStretch()
btn_layout.addWidget(calculate_btn)
reagents_layout.addLayout(btn_layout)
reagents_box.setLayout(reagents_layout)
layout.addWidget(reagents_box)
# Информационная панель
self.info_label = QLabel("ℹ️ Нажмите «Рассчитать» для получения количеств реагентов")
self.info_label.setStyleSheet("background-color: #ecf0f1; padding: 8px; border-radius: 5px;")
layout.addWidget(self.info_label)
# Добавляем начальные строки
self._add_reagent_row()
return tab
def _add_reagent_row(self, name = "", percentage = "0", unit = "мл", dilution = "1", amount = ""):
row = self.reagents_table.rowCount()
self.reagents_table.insertRow(row)
if name == "":
self.reagents_table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row+1}"))
else:
self.reagents_table.setItem(row, 0, QTableWidgetItem(name))
self.reagents_table.setItem(row, 1, QTableWidgetItem(percentage))
unit_combo = QComboBox()
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л"])
unit_combo.setCurrentText(unit)
self.reagents_table.setCellWidget(row, 2, unit_combo)
self.reagents_table.setItem(row, 3, QTableWidgetItem(dilution))
self.reagents_table.setItem(row, 4, QTableWidgetItem(amount))
def _remove_reagent_row(self):
for row in sorted(set(i.row() for i in self.reagents_table.selectedItems()), reverse=True):
if row >= 0:
self.reagents_table.removeRow(row)
def _get_reagents_from_table(self) -> List[Dict]:
"""Собирает данные реагентов из таблицы"""
reagents = []
for row in range(self.reagents_table.rowCount()):
name_item = self.reagents_table.item(row, 0)
percent_item = self.reagents_table.item(row, 1)
unit_widget = self.reagents_table.cellWidget(row, 2)
dilution_item = self.reagents_table.item(row, 3)
if not name_item or not percent_item:
continue
try:
percent_text = percent_item.text().strip()
if not percent_text:
continue
reagent = {
'name': name_item.text(),
'percentage': float(percent_text),
'unit': unit_widget.currentText() if unit_widget else "мг",
'dilution_factor': float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0
}
reagents.append(reagent)
except ValueError as e:
print(f"Ошибка в строке {row}: {e}")
continue
return reagents
def _calculate_medium(self):
"""Выполняет расчёт питательной среды"""
try:
reagents = self._get_reagents_from_table()
if not reagents:
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один реагент")
return
result = calculate_medium_composition(
total_volume=self.total_volume_spin.value(),
volume_unit=self.volume_unit_combo.currentText(),
reagents=reagents,
solvent_name=self.solvent_input.text()
)
# Отображаем результаты в таблице
for row, reagent in enumerate(result['reagents']):
if row < self.reagents_table.rowCount():
amount_text = self._format_number(reagent['calculated_amount'])
self.reagents_table.setItem(row, 4, QTableWidgetItem(amount_text))
# Подсветка
item = self.reagents_table.item(row, 4)
if item:
item.setBackground(QColor(220, 255, 220))
# Информация о растворителе
solvent_text = (
f"✅ Растворитель: {self._format_number(result['solvent_volume'])} {result['total_unit']} "
f"({result['solvent_percentage']:.1f}%)"
)
self.info_label.setText(solvent_text)
self.info_label.setStyleSheet("background-color: #d5f5e3; padding: 8px; border-radius: 5px;")
print(result)
# Сохраняем результаты для передачи в DoE
self.last_medium_result = result
# QMessageBox.information(self, "Успех", "Расчёт выполнен успешно!")
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
# ========== ВКЛАДКА 2: ФАКТОРЫ ЭКСПЕРИМЕНТА ==========
def _create_factors_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
# Кнопка импорта из калькулятора
import_box = QGroupBox("Импорт данных")
import_layout = QHBoxLayout()
self.import_btn = QPushButton("📥 Импортировать реагенты из калькулятора")
self.import_btn.setObjectName("success")
self.import_btn.clicked.connect(self._import_reagents_to_factors)
import_layout.addWidget(self.import_btn)
import_layout.addStretch()
import_box.setLayout(import_layout)
layout.addWidget(import_box)
# Таблица факторов
factors_box = QGroupBox("Факторы эксперимента")
factors_layout = QVBoxLayout()
self.factors_table = QTableWidget()
self.factors_table.setColumnCount(9)
self.factors_table.setHorizontalHeaderLabels([
"Фактор", "%", "Разбавление", "Нулевой уровень", "Шаг",
"Тип шага", "Верхний (+1)", "Нижний (-1)", "Ед. изм."
])
#self.factors_table.setAlternatingRowColors(True)
factors_layout.addWidget(self.factors_table)
btn_layout = QHBoxLayout()
add_factor_btn = QPushButton(" Добавить фактор")
add_factor_btn.clicked.connect(self._add_factor_row)
remove_factor_btn = QPushButton(" Удалить фактор")
remove_factor_btn.setObjectName("danger")
remove_factor_btn.clicked.connect(self._remove_factor_row)
btn_layout.addWidget(add_factor_btn)
btn_layout.addWidget(remove_factor_btn)
btn_layout.addStretch()
factors_layout.addLayout(btn_layout)
factors_box.setLayout(factors_layout)
layout.addWidget(factors_box)
# Настройки эксперимента
settings_box = QGroupBox("Настройки эксперимента")
settings_layout = QVBoxLayout()
# Параметры среды (из калькулятора)
env_layout = QHBoxLayout()
env_layout.addWidget(QLabel("Общий объём:"))
self.exp_total_volume = QDoubleSpinBox()
self.exp_total_volume.setRange(0.001, 1000000)
self.exp_total_volume.setValue(100)
self.exp_total_volume.setSuffix(" ")
env_layout.addWidget(self.exp_total_volume)
self.exp_volume_unit = QComboBox()
self.exp_volume_unit.addItems(["мл", "л", "мкл", "нл"])
self.exp_volume_unit.setCurrentText("мл")
env_layout.addWidget(self.exp_volume_unit)
env_layout.addSpacing(20)
env_layout.addWidget(QLabel("Растворитель:"))
self.exp_solvent = QLineEdit("Вода")
env_layout.addWidget(self.exp_solvent)
env_layout.addStretch()
settings_layout.addLayout(env_layout)
# Настройки эксперимента
exp_layout = QHBoxLayout()
exp_layout.addWidget(QLabel("Центральных точек:"))
self.center_points_spin = QSpinBox()
self.center_points_spin.setRange(0, 10)
self.center_points_spin.setValue(3)
exp_layout.addWidget(self.center_points_spin)
exp_layout.addSpacing(20)
self.randomize_check = QCheckBox("Рандомизировать порядок")
self.randomize_check.setChecked(False)
exp_layout.addWidget(self.randomize_check)
exp_layout.addStretch()
settings_layout.addLayout(exp_layout)
settings_box.setLayout(settings_layout)
layout.addWidget(settings_box)
# Кнопка генерации
generate_btn = QPushButton("🎲 Сгенерировать план эксперимента")
generate_btn.clicked.connect(self._generate_design)
layout.addWidget(generate_btn)
# Добавляем начальный фактор
self._add_factor_row()
# ПОДКЛЮЧАЕМ СИГНАЛ ИЗМЕНЕНИЯ ЯЧЕЕК
self.factors_table.cellChanged.connect(self._on_factor_changed)
return tab
def _add_factor_row(self, name = "", percentage = "0", dilution = "0", center = "0", step ="0", step_type = "%", high = "", low = "", unit= "г"):
row = self.factors_table.rowCount()
self.factors_table.insertRow(row)
if name == "":
self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}"))
else:
self.factors_table.setItem(row, 0, QTableWidgetItem(name))
self.factors_table.setItem(row, 1, QTableWidgetItem(percentage))
self.factors_table.setItem(row, 2, QTableWidgetItem(dilution))
self.factors_table.setItem(row, 3, QTableWidgetItem(center))
self.factors_table.setItem(row, 4, QTableWidgetItem(step))
step_type_combo = QComboBox()
step_type_combo.addItems(["%", "ед."])
step_type_combo.setCurrentText(step_type)
step_type_combo.currentTextChanged.connect(lambda: self._on_factor_changed(row, 5))
self.factors_table.setCellWidget(row, 5, step_type_combo)
high_item = QTableWidgetItem(high)
high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable)
high_item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(row, 6, high_item)
low_item = QTableWidgetItem(low)
low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable)
low_item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(row, 7, low_item)
unit_measure = QComboBox()
unit_measure.addItems(["мкл", "мл", "л", "мг", "г", "кг"])
unit_measure.setCurrentText(unit)
unit_measure.currentTextChanged.connect(lambda: self._on_factor_changed(row, 8))
self.factors_table.setCellWidget(row, 8, unit_measure)
return row
def _remove_factor_row(self):
for row in sorted(set(i.row() for i in self.factors_table.selectedItems()), reverse=True):
if row >= 0:
self.factors_table.removeRow(row)
def _import_reagents_to_factors(self):
"""Импортирует реагенты из калькулятора как факторы"""
if not hasattr(self, 'last_medium_result') or self.last_medium_result is None:
QMessageBox.warning(self, "Предупреждение",
"Сначала выполните расчёт в калькуляторе сред!")
return
result = self.last_medium_result
# Очищаем таблицу факторов
self.factors_table.setRowCount(0)
self.exp_total_volume.setValue(result['total_volume'])
self.exp_volume_unit.setCurrentText(result['total_unit'])
self.exp_solvent.setText(result['solvent_name'])
for reagent in result['reagents']:
name = reagent['name']
if reagent.get('dilution_factor', 1.0) != 1.0:
name += f"(разб. ×{reagent['dilution_factor']:g})"
percentage = f"{reagent['percentage']:g}"
dilution = f"{reagent.get('dilution_factor', 1.0):g}"
center = str(reagent.get('calculated_amount'))
step = ""
step_type = "%"
high = ""
low = ""
unit = reagent["unit"]
self._add_factor_row(name, percentage, dilution, center, step, step_type, high, low, unit)
# QMessageBox.information(self, "Успех",
# f"Импортировано {len(result['reagents'])} факторов из калькулятора сред")
def _get_factors_from_table(self) -> List[Dict]:
"""Собирает данные факторов из таблицы"""
factors = []
for row in range(self.factors_table.rowCount()):
try:
name_item = self.factors_table.item(row, 0)
center_item = self.factors_table.item(row, 3)
step_item = self.factors_table.item(row, 4)
high_item = self.factors_table.item(row, 6)
low_item = self.factors_table.item(row, 7)
unit_item = self.factors_table.cellWidget(row, 8)
step_combo = self.factors_table.cellWidget(row, 5)
if not all([name_item, center_item, high_item, low_item]):
continue
center_text = center_item.text().strip()
high_text = high_item.text().strip()
low_text = low_item.text().strip()
if not center_text or not high_text or not low_text:
continue
factor = {
'name': name_item.text(),
'center': float(center_text),
'high': float(high_text),
'low': float(low_text),
'unit': unit_item.currentText() if unit_item else "г",
'step': float(step_item.text()) if step_item and step_item.text() else 0,
'step_type': step_combo.currentText() if step_combo else "ед."
}
factors.append(factor)
except (ValueError, AttributeError) as e:
print(f"Ошибка в строке {row}: {e}")
continue
return factors
def _on_factor_changed(self, row, column):
"""При изменении ячейки пересчитываем уровни"""
if getattr(self, '_loading_data', False) or column not in [3, 4, 5, 8]:
return
try:
# Получаем данные
percentage = float(self.factors_table.item(row, 1).text())
dilution = float(self.factors_table.item(row, 2).text())
center = float(self.factors_table.item(row, 3).text())
step = float(self.factors_table.item(row, 4).text())
step_type = self.factors_table.cellWidget(row, 5).currentText()
unit = self.factors_table.cellWidget(row, 8).currentText()
name = self.factors_table.item(row, 0).text()
# Рассчитываем
factor = FactorData(percentage=percentage, dilution_factor=dilution, name=name, center=center, low=0, high=0, step=step, step_type=step_type, unit=unit)
result = calculate_factor_levels(factor, self.total_volume_spin.value(), self.volume_unit_combo.currentText())
if result:
self.factors_table.blockSignals(True)
self.factors_table.item(row, 6).setText(self._format_number(result.high))
self.factors_table.item(row, 7).setText(self._format_number(result.low))
self.factors_table.blockSignals(False)
except (ValueError, AttributeError):
pass
# ========== ВКЛАДКА 3: МАТРИЦА ПЛАНИРОВАНИЯ ==========
def _create_design_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
# Скролл-область для большой матрицы
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll_widget = QWidget()
scroll_layout = QVBoxLayout(scroll_widget)
self.design_matrix = QTableWidget()
scroll_layout.addWidget(self.design_matrix)
scroll.setWidget(scroll_widget)
layout.addWidget(scroll)
btn_layout = QHBoxLayout()
self.export_csv_btn = QPushButton("📊 Экспорт в CSV")
self.export_csv_btn.clicked.connect(self._export_design_csv)
self.export_csv_btn.setEnabled(False)
btn_layout.addWidget(self.export_csv_btn)
btn_layout.addStretch()
layout.addLayout(btn_layout)
self.design_info = QLabel("")
self.design_info.setStyleSheet("color: #666; padding: 5px;")
layout.addWidget(self.design_info)
return tab
def _generate_design(self):
"""Генерирует план эксперимента"""
all_factors = self._get_factors_from_table()
factors = get_active_factors(all_factors)
# Выбираем только те факторы, шаг которых не равен нулю
i_factors = get_inactive_factors(all_factors)
# Отдельно сохраняем неактивные факторы
if len(factors) == 0:
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
return
try:
design = generate_factorial_design(
factors=factors,
center_points=self.center_points_spin.value(),
randomize=self.randomize_check.isChecked()
)
self.generated_design = design
n_exp = len(design)
n_factors = len(factors)
n_i_factors = len(i_factors)
self.design_matrix.setRowCount(n_exp)
self.design_matrix.setColumnCount(n_factors + 3)
headers = [f['name'] for f in factors] + [f['name'] for f in i_factors] + ["Растворитель"] +["Тип"] +["Отклик"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, exp in enumerate(design):
for f_idx in range(n_factors):
key = f"Фактор_{f_idx+1}"
if key not in exp:
continue
value = exp[key]['natural']
unit = factors[f_idx]['unit']
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
if exp.get('is_center', False):
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, f_idx, item)
if n_i_factors>0:
for f_idx in range(n_i_factors):
value = i_factors[f_idx]['center']
unit = i_factors[f_idx]['unit']
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, n_factors + f_idx, item)
if exp.get('is_center', False):
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
type_item.setBackground(QColor(255, 255, 200))
else:
type_item = QTableWidgetItem("Факторная")
self.design_matrix.setItem(exp_idx, n_factors+n_i_factors, type_item)
self.design_matrix.resizeColumnsToContents()
n_factorial = 2 ** n_factors
n_center = self.center_points_spin.value()
self.design_info.setText(
f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}"
)
self.export_csv_btn.setEnabled(True)
self._setup_results_table(n_exp)
QMessageBox.information(self, "Успех",
f"Сгенерирован план для {n_factors} факторов ({n_exp} опытов)")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Ошибка генерации плана: {str(e)}")
def _setup_results_table(self, n_experiments: int):
"""Настраивает таблицу результатов для ввода данных"""
# Получаем отклики (для простоты используем один отклик)
self.results_table.setRowCount(n_experiments)
self.results_table.setColumnCount(2)
self.results_table.setHorizontalHeaderLabels(["№ опыта", "Результат"])
for i in range(n_experiments):
self.results_table.setItem(i, 0, QTableWidgetItem(str(i)))
def _export_design_csv(self):
"""Экспортирует матрицу планирования в CSV"""
if not self.generated_design or self.design_matrix.rowCount() == 0:
QMessageBox.warning(self, "Предупреждение", "Нет данных для экспорта")
return
filename, _ = QFileDialog.getSaveFileName(
self, "Сохранить план", "", "CSV (*.csv);;Все файлы (*)"
)
if filename:
if not filename.endswith('.csv'):
filename += '.csv'
try:
import csv
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.writer(f)
# Заголовки
headers = []
for j in range(self.design_matrix.columnCount()):
header = self.design_matrix.horizontalHeaderItem(j)
headers.append(header.text() if header else f"Колонка_{j+1}")
writer.writerow(headers)
# Данные
for i in range(self.design_matrix.rowCount()):
row = []
for j in range(self.design_matrix.columnCount()):
item = self.design_matrix.item(i, j)
row.append(item.text() if item else "")
writer.writerow(row)
QMessageBox.information(self, "Успех", f"План сохранён в {filename}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить: {str(e)}")
# ========== ВКЛАДКА 4: АНАЛИЗ ==========
def _create_analysis_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
# Таблица для ввода результатов
results_box = QGroupBox("Результаты экспериментов")
results_layout = QVBoxLayout()
self.results_table = QTableWidget()
results_layout.addWidget(self.results_table)
results_box.setLayout(results_layout)
layout.addWidget(results_box)
# Кнопка анализа
self.analyze_btn = QPushButton("📈 Провести анализ")
self.analyze_btn.clicked.connect(self._perform_analysis)
layout.addWidget(self.analyze_btn)
# Вывод анализа
self.analysis_text = QTextEdit()
self.analysis_text.setReadOnly(True)
self.analysis_text.setMaximumHeight(250)
layout.addWidget(self.analysis_text)
return tab
def _perform_analysis(self):
"""Выполняет анализ результатов эксперимента"""
if not self.generated_design:
QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!")
return
# Получаем отклики
responses = [{'name': 'Отклик', 'unit': ''}]
# Собираем результаты
results = []
for i in range(self.results_table.rowCount()):
item = self.results_table.item(i, 1)
if item and item.text():
try:
results.append([float(item.text())])
except ValueError:
QMessageBox.warning(self, "Ошибка",
f"Неверное значение в опыте {i+1}")
return
else:
QMessageBox.warning(self, "Ошибка",
f"Не введены данные для опыта {i+1}")
return
if len(results) != len(self.generated_design):
QMessageBox.warning(self, "Ошибка",
f"Введено {len(results)} результатов, ожидается {len(self.generated_design)}")
return
try:
# Выполняем анализ
analysis = analyze_experiment(results, self.generated_design, responses)
# Выводим результаты
self.analysis_text.clear()
self.analysis_text.append("=" * 60)
self.analysis_text.append("РЕЗУЛЬТАТЫ АНАЛИЗА ЭКСПЕРИМЕНТА")
self.analysis_text.append("=" * 60)
for resp_name, stats in analysis.items():
self.analysis_text.append(f"\n📊 {resp_name}")
self.analysis_text.append("-" * 40)
self.analysis_text.append(f"Среднее значение: {stats['mean']:.4f}")
self.analysis_text.append(f"Дисперсия: {stats['variance']:.4f}")
self.analysis_text.append(f"Ст. отклонение: {stats['std_dev']:.4f}")
self.analysis_text.append(f"Коэф. вариации: {stats['cv']:.2f}%")
if stats['n_center'] > 1:
self.analysis_text.append(f"\nЦентральные точки (n={stats['n_center']}):")
self.analysis_text.append(f" Дисперсия воспроизводимости: {stats['center_variance']:.4f}")
if stats.get('fisher_ratio'):
self.analysis_text.append(f"\nКритерий Фишера: {stats['fisher_ratio']:.4f}")
if stats.get('model_adequate'):
self.analysis_text.append("✅ Модель адекватна")
else:
self.analysis_text.append("⚠️ Модель может быть неадекватна")
self.analysis_text.append("\n" + "=" * 60)
self.analysis_text.append("Анализ завершен")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Ошибка анализа: {str(e)}")
# ========== МЕТОДЫ РАБОТЫ С JSON ==========
def save_project_to_json(self):
"""Сохраняет весь проект в JSON файл"""
if not JSON_SUPPORT:
QMessageBox.warning(self, "Предупреждение", "JSON поддержка не доступна")
return
filename, _ = QFileDialog.getSaveFileName(
self, "Сохранить проект", "", "JSON Files (*.json);;All Files (*)"
)
if not filename:
return
if not filename.endswith('.json'):
filename += '.json'
try:
project = self._collect_current_data()
project.save_to_file(filename)
QMessageBox.information(self, "Успех", f"Проект сохранён в {filename}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить проект: {str(e)}")
def load_project_from_json(self):
"""Загружает проект из JSON файла"""
if not JSON_SUPPORT:
QMessageBox.warning(self, "Предупреждение", "JSON поддержка не доступна")
return
filename, _ = QFileDialog.getOpenFileName(
self, "Загрузить проект", "", "JSON Files (*.json);;All Files (*)"
)
if not filename:
return
try:
project = ProjectData.load_from_file(filename)
self._apply_project_data(project)
# QMessageBox.information(self, "Успех", f"Проект загружен из {filename}")
except FileNotFoundError:
QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить проект: {str(e)}")
def _collect_current_data(self):
"""Собирает текущие данные из всех виджетов"""
# Данные калькулятора сред
reagents = []
for row in range(self.reagents_table.rowCount()):
name_item = self.reagents_table.item(row, 0)
percent_item = self.reagents_table.item(row, 1)
unit_widget = self.reagents_table.cellWidget(row, 2)
coeff_item = self.reagents_table.item(row, 3)
dilution_item = self.reagents_table.item(row, 4)
if name_item and percent_item and percent_item.text():
try:
reagent = ReagentData(
name=name_item.text(),
percentage=float(percent_item.text()),
unit=unit_widget.currentText() if unit_widget else "мг",
conversion_factor=float(coeff_item.text()) if coeff_item and coeff_item.text() else 1.0,
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0
)
reagents.append(reagent)
except ValueError:
pass
# Данные факторов эксперимента
factors = []
for row in range(self.factors_table.rowCount()):
name_item = self.factors_table.item(row, 0)
center_item = self.factors_table.item(row, 3)
low_item = self.factors_table.item(row, 7)
high_item = self.factors_table.item(row, 6)
step_item = self.factors_table.item(row, 4)
unit_item = self.factors_table.cellWidget(row, 8)
step_combo = self.factors_table.cellWidget(row, 5)
percent_item = self.factors_table.item(row, 1)
dilution_item = self.factors_table.item(row, 2)
if name_item and center_item and center_item.text():
try:
factor = FactorData(
name=name_item.text(),
center=float(center_item.text()),
low=float(low_item.text()) if low_item and low_item.text() else 0,
high=float(high_item.text()) if high_item and high_item.text() else 0,
step=float(step_item.text()) if step_item and step_item.text() else 0,
step_type=step_combo.currentText() if step_combo else "ед.",
unit=unit_item.currentText() if unit_item else "",
percentage=float(percent_item.text()) if percent_item and percent_item.text() else None,
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None
)
factors.append(factor)
except ValueError:
pass
# Данные результатов (если есть)
results_data = None
if hasattr(self, 'generated_design') and self.generated_design:
results = []
for i in range(self.results_table.rowCount()):
item = self.results_table.item(i, 1)
if item and item.text():
try:
results.append([float(item.text())])
except ValueError:
pass
if results:
responses = [ResponseData(name="Отклик", unit="")]
results_data = ExperimentResultsData(
design=self.generated_design,
results=results,
responses=responses
)
# Создаём проект
project = create_new_project("Мой проект")
project.medium_total_volume = self.total_volume_spin.value()
project.medium_volume_unit = self.volume_unit_combo.currentText()
project.medium_solvent = self.solvent_input.text()
project.medium_reagents = reagents
project.experiment_factors = factors
project.experiment_center_points = self.center_points_spin.value()
project.experiment_randomize = self.randomize_check.isChecked()
project.experiment_results = results_data
return project
def _apply_project_data(self, project):
"""Применяет загруженные данные к GUI"""
self._loading_data = True # <- ОТКЛЮЧАЕМ ОБРАБОТЧИК
try:
# Применяем данные калькулятора сред
self.total_volume_spin.setValue(project.medium_total_volume)
index = self.volume_unit_combo.findText(project.medium_volume_unit)
if index >= 0:
self.volume_unit_combo.setCurrentIndex(index)
self.solvent_input.setText(project.medium_solvent)
# Очищаем и заполняем таблицу реагентов
self.reagents_table.setRowCount(0)
for reagent in project.medium_reagents:
row = self.reagents_table.rowCount()
self.reagents_table.insertRow(row)
self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage)))
unit_combo = QComboBox()
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
unit_combo.setCurrentText(reagent.unit)
self.reagents_table.setCellWidget(row, 2, unit_combo)
self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor)))
self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor)))
self.reagents_table.setItem(row, 5, QTableWidgetItem(""))
# Применяем данные факторов эксперимента
self.exp_total_volume.setValue(project.experiment_total_volume)
self.exp_volume_unit.setCurrentText(project.experiment_volume_unit)
self.exp_solvent.setText(project.experiment_solvent)
self.factors_table.setRowCount(0)
for factor in project.experiment_factors:
if factor.percentage is not None:
percentage = str(factor.percentage)
if factor.dilution_factor is not None:
dilution = str(factor.dilution_factor)
self._add_factor_row(factor.name, percentage, dilution, str(factor.center),
str(factor.step), factor.step_type, str(factor.high),
str(factor.low), factor.unit)
# Применяем настройки эксперимента
self.center_points_spin.setValue(project.experiment_center_points)
self.randomize_check.setChecked(project.experiment_randomize)
# Если есть результаты, загружаем их
if project.experiment_results and project.experiment_results.design:
self.generated_design = project.experiment_results.design
# Обновляем отображение матрицы
self._refresh_design_matrix()
# Загружаем результаты в таблицу
if project.experiment_results.results:
for i, row_results in enumerate(project.experiment_results.results):
if i < self.results_table.rowCount() and row_results:
self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0])))
# Переключаемся на вкладку с экспериментом
if self.tab_widget:
self.tab_widget.setCurrentIndex(0)
finally:
self._loading_data = False # <- ВКЛЮЧАЕМ ОБРАТНО
def _refresh_design_matrix(self):
"""Обновляет отображение матрицы планирования"""
if not self.generated_design:
return
factors = self._get_factors_from_table()
n_exp = len(self.generated_design)
n_factors = len(factors)
self.design_matrix.setRowCount(n_exp)
self.design_matrix.setColumnCount(n_factors + 2)
headers = [""] + [f['name'] for f in factors] + ["Тип"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, exp in enumerate(self.generated_design):
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
for f_idx in range(n_factors):
key = f"Фактор_{f_idx + 1}"
if key not in exp:
continue
value = exp[key]['natural']
unit = factors[f_idx]['unit']
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
if exp.get('is_center', False):
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, f_idx + 1, item)
if exp.get('is_center', False):
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
type_item.setBackground(QColor(255, 255, 200))
else:
type_item = QTableWidgetItem("Факторная")
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
self.design_matrix.resizeColumnsToContents()
if hasattr(self, 'export_csv_btn'):
self.export_csv_btn.setEnabled(True)
n_factorial = 2 ** n_factors
n_center = self.center_points_spin.value()
self.design_info.setText(
f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}"
)
self._setup_results_table(n_exp)
# ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========
def _format_number(self, value: float) -> str:
"""Форматирует число для отображения"""
if value == int(value):
return str(int(value))
# Убираем лишние нули
formatted = f"{value:.6f}".rstrip('0').rstrip('.')
if '.' in formatted and len(formatted.split('.')[1]) > 4:
formatted = f"{value:.4f}".rstrip('0').rstrip('.')
return formatted
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()