Реализация функционала полнофакторного эксперимента
This commit is contained in:
+12
-16
@@ -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
@@ -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,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
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user