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

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.QtCore import Qt
from PyQt5.QtGui import QColor
from model import Model from model import Model
from view import MainWindow as MediumCalculatorView from view import MediumCalculatorWindow
import json import json
from reagent import Reagent from reagent import Reagent
@@ -9,10 +10,9 @@ from reagent import Reagent
class Controller: class Controller:
def __init__(self): def __init__(self):
self.model = Model() self.model = Model()
self.view = MediumCalculatorView() self.view = MediumCalculatorWindow()
self._connect_signals() self._connect_signals()
# Убираем автоматический показ окна - теперь он вызывается из главного меню
# self.view.show()
def _connect_signals(self): def _connect_signals(self):
"""Подключает обработчики событий интерфейса""" """Подключает обработчики событий интерфейса"""
@@ -118,26 +118,22 @@ class Controller:
self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name)) self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}")) self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}"))
# Единица - QComboBox
unit_combo = QComboBox() unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText(reagent.unit) unit_combo.setCurrentText(reagent.unit)
self.view.table.setCellWidget(row, 2, unit_combo) self.view.table.setCellWidget(row, 2, unit_combo)
self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}")) self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}"))
# Создаём поле для разбавления (QLineEdit для ручного ввода) # Разбавление - обычная ячейка
dilution_edit = QLineEdit() self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}"))
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, 5, QTableWidgetItem("")) self.view.table.setItem(row, 5, QTableWidgetItem(""))
# Очищаем результаты # Очищаем результаты
self.view.clear_results() self.view.clear_results()
def save_composition(self): def save_composition(self):
"""Сохраняет состав среды в JSON-файл""" """Сохраняет состав среды в JSON-файл"""
filename, _ = QFileDialog.getSaveFileName( filename, _ = QFileDialog.getSaveFileName(
+437 -80
View File
@@ -2,16 +2,19 @@ from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QWidget, QMessageBox, QPushButton, QLabel, QWidget, QMessageBox,
QTableWidget, QTableWidgetItem, QGroupBox, QTableWidget, QTableWidgetItem, QGroupBox,
QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit,
QTextEdit, QTabWidget, QFormLayout) QTextEdit, QTabWidget, QFormLayout, QCheckBox,
QScrollArea, QFileDialog)
from PyQt5.QtCore import Qt 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): class ExperimentDesignWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика") self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика")
self.setGeometry(200, 100, 1000, 750) self.setGeometry(200, 100, 1200, 800)
self.setStyleSheet(""" self.setStyleSheet("""
QMainWindow { QMainWindow {
background-color: #f5f5f5; background-color: #f5f5f5;
@@ -27,6 +30,7 @@ class ExperimentDesignWindow(QMainWindow):
subcontrol-origin: margin; subcontrol-origin: margin;
left: 10px; left: 10px;
padding: 0 5px 0 5px; padding: 0 5px 0 5px;
color: #1565C0;
} }
QPushButton { QPushButton {
background-color: #4CAF50; background-color: #4CAF50;
@@ -42,6 +46,13 @@ class ExperimentDesignWindow(QMainWindow):
QTableWidget { QTableWidget {
gridline-color: #ddd; gridline-color: #ddd;
} }
QHeaderView::section {
background-color: #1565C0;
color: white;
padding: 8px;
border: none;
font-weight: bold;
}
""") """)
self._init_ui() self._init_ui()
@@ -71,26 +82,41 @@ class ExperimentDesignWindow(QMainWindow):
factors_group = QGroupBox("Факторы эксперимента (независимые переменные)") factors_group = QGroupBox("Факторы эксперимента (независимые переменные)")
factors_layout = QVBoxLayout() factors_layout = QVBoxLayout()
# Информация
info_label = QLabel("Определите факторы, которые влияют на ваш эксперимент:") info_label = QLabel("Определите факторы, которые влияют на ваш эксперимент:")
info_label.setStyleSheet("color: #555; font-weight: normal;") info_label.setStyleSheet("color: #555; font-weight: normal;")
factors_layout.addWidget(info_label) factors_layout.addWidget(info_label)
self.factors_table = QTableWidget() self.factors_table = QTableWidget()
self.factors_table.setColumnCount(4) # Новый порядок колонок: Фактор, Нулевой уровень, Шаг, Верхний (+1), Нижний (-1), Единица измерения
self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нижний уровень (-1)", "Верхний уровень (+1)", "Единица измерения"]) self.factors_table.setColumnCount(6)
self.factors_table.setRowCount(3) self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг",
"Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"])
self.factors_table.setRowCount(2)
# Пример данных # Пример данных
sample_factors = [ sample_factors = [
["Температура", "25", "37", "°C"], ["Температура", "31", "6", "37", "25", "°C"],
["pH", "6.5", "7.5", ""], ["pH", "7.0", "0.5", "7.5", "6.5", ""],
["Концентрация глюкозы", "5", "20", "г"]
] ]
for i, factor in enumerate(sample_factors): for i, factor in enumerate(sample_factors):
for j, value in enumerate(factor): 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) factors_layout.addWidget(self.factors_table)
@@ -108,6 +134,27 @@ class ExperimentDesignWindow(QMainWindow):
factors_group.setLayout(factors_layout) factors_group.setLayout(factors_layout)
params_layout.addWidget(factors_group) 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_group = QGroupBox("Отклики (зависимые переменные)")
responses_layout = QVBoxLayout() responses_layout = QVBoxLayout()
@@ -128,7 +175,6 @@ class ExperimentDesignWindow(QMainWindow):
responses_layout.addWidget(self.responses_table) responses_layout.addWidget(self.responses_table)
# Кнопки для управления откликами
response_buttons = QHBoxLayout() response_buttons = QHBoxLayout()
add_response_btn = QPushButton("+ Добавить отклик") add_response_btn = QPushButton("+ Добавить отклик")
add_response_btn.clicked.connect(self.add_response_row) add_response_btn.clicked.connect(self.add_response_row)
@@ -148,17 +194,38 @@ class ExperimentDesignWindow(QMainWindow):
plan_tab = QWidget() plan_tab = QWidget()
plan_layout = QVBoxLayout(plan_tab) plan_layout = QVBoxLayout(plan_tab)
plan_info = QLabel("Полнофакторный план эксперимента (Full Factorial Design)") plan_info = QLabel("Полнофакторный план эксперимента с центральными точками")
plan_info.setAlignment(Qt.AlignCenter) plan_info.setAlignment(Qt.AlignCenter)
plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
plan_layout.addWidget(plan_info) plan_layout.addWidget(plan_info)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
matrix_widget = QWidget()
matrix_layout = QVBoxLayout(matrix_widget)
self.design_matrix = QTableWidget() 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 = QPushButton("🔄 Сгенерировать план эксперимента")
generate_btn.clicked.connect(self.generate_design_matrix) 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, "📊 Матрица планирования") tabs.addTab(plan_tab, "📊 Матрица планирования")
@@ -166,58 +233,29 @@ class ExperimentDesignWindow(QMainWindow):
analysis_tab = QWidget() analysis_tab = QWidget()
analysis_layout = QVBoxLayout(analysis_tab) analysis_layout = QVBoxLayout(analysis_tab)
analysis_info = QLabel("Регрессионный анализ и визуализация результатов") analysis_info = QLabel("Введите результаты экспериментов для анализа")
analysis_info.setAlignment(Qt.AlignCenter) analysis_info.setAlignment(Qt.AlignCenter)
analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
analysis_layout.addWidget(analysis_info) analysis_layout.addWidget(analysis_info)
# Заглушка для анализа self.results_table = QTableWidget()
placeholder = QLabel("Здесь будет:\n\n" analysis_layout.addWidget(self.results_table)
"• Множественная линейная регрессия\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)
analyze_btn = QPushButton("📈 Провести анализ (в разработке)") analyze_btn = QPushButton("📈 Провести регрессионный анализ")
analyze_btn.clicked.connect(self.show_placeholder_message) analyze_btn.clicked.connect(self.perform_analysis)
analysis_layout.addWidget(analyze_btn) 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, "📐 Анализ результатов") 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) layout.addWidget(tabs)
# Кнопки управления
btn_layout = QHBoxLayout() 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 = QPushButton("💿 Сохранить проект")
save_btn.clicked.connect(self.show_placeholder_message) save_btn.clicked.connect(self.show_placeholder_message)
btn_layout.addWidget(save_btn) btn_layout.addWidget(save_btn)
@@ -234,6 +272,36 @@ class ExperimentDesignWindow(QMainWindow):
layout.addLayout(btn_layout) 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): def add_factor_row(self):
"""Добавляет строку для нового фактора""" """Добавляет строку для нового фактора"""
row = self.factors_table.rowCount() 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, 0, QTableWidgetItem(f"Фактор_{row+1}"))
self.factors_table.setItem(row, 1, QTableWidgetItem("0")) self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
self.factors_table.setItem(row, 2, QTableWidgetItem("1")) 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): def remove_factor_row(self):
"""Удаляет последнюю строку факторов""" """Удаляет последнюю строку факторов"""
@@ -260,53 +340,330 @@ class ExperimentDesignWindow(QMainWindow):
if self.responses_table.rowCount() > 1: if self.responses_table.rowCount() > 1:
self.responses_table.removeRow(self.responses_table.rowCount() - 1) self.responses_table.removeRow(self.responses_table.rowCount() - 1)
def generate_design_matrix(self): def get_factors_data(self):
"""Генерирует матрицу планирования (заглушка)""" """Получает данные о факторах"""
n_factors = self.factors_table.rowCount() 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, "Предупреждение", "Добавьте хотя бы один фактор!") QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
return 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.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}" headers = ["опыта"] + [f["name"] for f in factors] + ["Тип точки"]
for i in range(n_factors)]
self.design_matrix.setHorizontalHeaderLabels(headers) 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): for factor_idx in range(n_factors):
level = -1 if (exp // (2 ** factor)) % 2 == 0 else 1 factor_key = f"Фактор_{factor_idx + 1}"
self.design_matrix.setItem(exp, factor + 1, QTableWidgetItem(str(level))) 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() 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, "Успех", QMessageBox.information(self, "Успех",
f"Сгенерирован план для {n_factors} факторов\n" 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): def show_placeholder_message(self):
"""Показывает сообщение о том, что функция в разработке""" """Показывает сообщение о том, что функция в разработке"""
QMessageBox.information( QMessageBox.information(
self, self,
"В разработке", "В разработке",
"🧪 Биотехнологические инструменты в стадии активной разработки!\n\n" "🧪 Функция в стадии разработки!\n\nБлижайшие обновления:\n"
"В ближайшее время здесь появится:\n\n" "✅ Экспорт в Excel\n"
"✅ Полнофакторный план (2^k факторный дизайн)\n"
"✅ Регрессионный анализ и ANOVA\n"
"✅ Построение поверхностей отклика\n" "✅ Построение поверхностей отклика\n"
"Оптимизация параметров\n" "Графики главных эффектов\n"
"Экспорт в Excel и PDF\n" "Полный регрессионный анализ"
"✅ Визуализация 2D/3D графиков\n\n"
"Следите за обновлениями!"
) )
+6 -3
View File
@@ -6,9 +6,12 @@ from main_window import MainWindow
def main(): def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
# Создаём главное окно с выбором режима # Устанавливаем стиль приложения
main_window = MainWindow() app.setStyle('Fusion')
main_window.show()
# Создаём главное окно цифрового помощника биохимика
assistant = MainWindow()
assistant.show()
# Запускаем цикл обработки событий # Запускаем цикл обработки событий
sys.exit(app.exec_()) sys.exit(app.exec_())
+6 -6
View File
@@ -1,12 +1,12 @@
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QWidget, QFrame) QPushButton, QLabel, QWidget, QFrame)
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QIcon from PyQt5.QtGui import QFont
from controller import Controller from controller import Controller
from experiment_design import ExperimentDesignWindow from experiment_design import ExperimentDesignWindow
class DigitalBiochemistAssistant(QMainWindow): class MainWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("Цифровой помощник биохимика - Главное меню") self.setWindowTitle("Цифровой помощник биохимика - Главное меню")
@@ -48,7 +48,7 @@ class DigitalBiochemistAssistant(QMainWindow):
layout.setContentsMargins(50, 50, 50, 50) layout.setContentsMargins(50, 50, 50, 50)
# Заголовок # Заголовок
title_label = QLabel("🧬 Цифровой помощник биохимика 🧪") title_label = QLabel("Цифровой помощник биохимика")
title_font = QFont() title_font = QFont()
title_font.setPointSize(20) title_font.setPointSize(20)
title_font.setBold(True) title_font.setBold(True)
@@ -69,7 +69,7 @@ class DigitalBiochemistAssistant(QMainWindow):
layout.addSpacing(20) layout.addSpacing(20)
# Кнопка 1: Калькулятор питательных сред # Кнопка 1: Калькулятор питательных сред
btn_medium = QPushButton("🥼 Калькулятор питательных сред") btn_medium = QPushButton("Калькулятор питательных сред")
btn_medium.setMinimumHeight(80) btn_medium.setMinimumHeight(80)
btn_medium.clicked.connect(self.open_medium_calculator) btn_medium.clicked.connect(self.open_medium_calculator)
layout.addWidget(btn_medium) layout.addWidget(btn_medium)
@@ -85,7 +85,7 @@ class DigitalBiochemistAssistant(QMainWindow):
layout.addSpacing(15) layout.addSpacing(15)
# Кнопка 2: Планирование эксперимента # Кнопка 2: Планирование эксперимента
btn_experiment = QPushButton("📊 Планирование эксперимента (DoE)") btn_experiment = QPushButton("Планирование эксперимента (DoE)")
btn_experiment.setMinimumHeight(80) btn_experiment.setMinimumHeight(80)
btn_experiment.clicked.connect(self.open_experiment_designer) btn_experiment.clicked.connect(self.open_experiment_designer)
layout.addWidget(btn_experiment) layout.addWidget(btn_experiment)
@@ -110,7 +110,7 @@ class DigitalBiochemistAssistant(QMainWindow):
bottom_layout = QHBoxLayout() 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;") version_label.setStyleSheet("color: #999; font-size: 10px;")
bottom_layout.addWidget(version_label) bottom_layout.addWidget(version_label)
+290 -107
View File
@@ -1,239 +1,422 @@
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QPushButton, QTableWidget, QTableWidgetItem, QPushButton,
QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QLabel, QDoubleSpinBox, QComboBox, QLineEdit,
QWidget, QMessageBox) QWidget, QMessageBox, QGroupBox, QFrame, QHeaderView)
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QColor
class MainWindow(QMainWindow):
class MediumCalculatorWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("Калькулятор питательных сред") self.setWindowTitle("Калькулятор питательных сред - Цифровой помощник биохимика")
self.setGeometry(100, 100, 1000, 600) 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() self._init_ui()
def _init_ui(self): def _init_ui(self):
central_widget = QWidget() central_widget = QWidget()
self.setCentralWidget(central_widget) self.setCentralWidget(central_widget)
layout = QVBoxLayout() layout = QVBoxLayout(central_widget)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
# Верхняя панель: параметры среды title_label = QLabel("Калькулятор питательных сред")
top_layout = QHBoxLayout() title_label.setObjectName("title")
top_layout.addWidget(QLabel("Общее количество:")) title_label.setAlignment(Qt.AlignCenter)
title_font = QFont()
title_font.setPointSize(18)
title_font.setBold(True)
title_label.setFont(title_font)
layout.addWidget(title_label)
params_group = QGroupBox("Параметры среды")
params_layout = QHBoxLayout()
params_layout.setSpacing(20)
amount_layout = QHBoxLayout()
amount_layout.addWidget(QLabel("Общее количество:"))
self.amount_input = QDoubleSpinBox() self.amount_input = QDoubleSpinBox()
self.amount_input.setRange(0.001, 1000000.0) self.amount_input.setRange(0.001, 1000000.0)
self.amount_input.setValue(1000.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 = QComboBox()
self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"]) self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"])
self.amount_unit_combo.setCurrentText("мл") 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("Вода") self.solvent_input = QLineEdit("Вода")
top_layout.addWidget(self.solvent_input) self.solvent_input.setMinimumWidth(150)
layout.addLayout(top_layout) solvent_layout.addWidget(self.solvent_input)
params_layout.addLayout(solvent_layout)
# Таблица реагентов (с растворителем в первой строке) params_layout.addStretch()
layout.addWidget(QLabel("Состав среды:")) params_group.setLayout(params_layout)
layout.addWidget(params_group)
table_group = QGroupBox("Состав среды")
table_layout = QVBoxLayout()
self.table = QTableWidget() self.table = QTableWidget()
self.table.setColumnCount(6) # Увеличиваем до 6 колонок self.table.setColumnCount(6)
self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Результат"]) self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"])
layout.addWidget(self.table) 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 = QHBoxLayout()
btn_layout.setSpacing(10)
self.add_row_btn = QPushButton("Добавить реагент") self.add_row_btn = QPushButton("Добавить реагент")
self.remove_row_btn = QPushButton("Удалить реагент") self.add_row_btn.setMinimumWidth(150)
self.calculate_btn = QPushButton("Рассчитать")
self.save_btn = QPushButton("Сохранить")
self.load_btn = QPushButton("Загрузить")
btn_layout.addWidget(self.add_row_btn) 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() self.add_initial_rows()
def add_initial_rows(self): def add_initial_rows(self):
"""Добавляет начальные строки: растворитель и первый реагент"""
# Добавляем строку растворителя (первая, нередактируемая)
self.add_solvent_row() self.add_solvent_row()
# Добавляем строку для первого реагента
self.add_new_row() self.add_new_row()
def add_solvent_row(self): def add_solvent_row(self):
"""Добавляет строку растворителя (нередактируемая)"""
row_count = self.table.rowCount() row_count = self.table.rowCount()
self.table.insertRow(row_count) self.table.insertRow(row_count)
self.table.setRowHeight(row_count, 30)
# Название растворителя (берём из поля ввода)
solvent_name = self.solvent_input.text() solvent_name = self.solvent_input.text()
solvent_item = QTableWidgetItem(solvent_name) solvent_item = QTableWidgetItem(solvent_name)
solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable) 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) self.table.setItem(row_count, 0, solvent_item)
# Процент (будет рассчитан автоматически)
percent_item = QTableWidgetItem("") percent_item = QTableWidgetItem("")
percent_item.setFlags(percent_item.flags() & ~Qt.ItemIsEditable) 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) self.table.setItem(row_count, 1, percent_item)
# Единица измерения (не используется для растворителя) unit_item = QTableWidgetItem(self.amount_unit_combo.currentText())
unit_item = QTableWidgetItem("-")
unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable) 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) self.table.setItem(row_count, 2, unit_item)
# Коэффициент (не используется для растворителя)
coeff_item = QTableWidgetItem("-") coeff_item = QTableWidgetItem("-")
coeff_item.setFlags(coeff_item.flags() & ~Qt.ItemIsEditable) 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) self.table.setItem(row_count, 3, coeff_item)
# Разбавление (не используется для растворителя)
dilution_item = QTableWidgetItem("-") dilution_item = QTableWidgetItem("-")
dilution_item.setFlags(dilution_item.flags() & ~Qt.ItemIsEditable) 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) self.table.setItem(row_count, 4, dilution_item)
# Результат (будет заполнен при расчёте)
result_item = QTableWidgetItem("") result_item = QTableWidgetItem("")
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable) 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) self.table.setItem(row_count, 5, result_item)
def update_solvent_name(self): def update_solvent_name(self):
"""Обновляет название растворителя в первой строке таблицы"""
solvent_name = self.solvent_input.text() solvent_name = self.solvent_input.text()
name_item = self.table.item(0, 0) name_item = self.table.item(0, 0)
if name_item: if name_item:
name_item.setText(solvent_name) 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): def add_new_row(self):
"""Добавляет новую строку для реагента"""
row_count = self.table.rowCount() row_count = self.table.rowCount()
self.table.insertRow(row_count) self.table.insertRow(row_count)
self.table.setRowHeight(row_count, 30)
self.table.setItem(row_count, 0, QTableWidgetItem(f"Реагент_{row_count}")) name_item = QTableWidgetItem(f"Реагент_{row_count}")
self.table.setItem(row_count, 1, QTableWidgetItem("0.0")) 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 = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText("г") unit_combo.setCurrentText("мг")
self.table.setCellWidget(row_count, 2, unit_combo) self.table.setCellWidget(row_count, 2, unit_combo)
self.table.setItem(row_count, 3, QTableWidgetItem("1.0")) coeff_item = QTableWidgetItem("1")
coeff_item.setForeground(QColor(0, 0, 0))
# Разбавление - обычное текстовое поле self.table.setItem(row_count, 3, coeff_item)
dilution_edit = QLineEdit("1.0")
dilution_edit.setAlignment(Qt.AlignRight) dilution_item = QTableWidgetItem("1")
dilution_edit.setToolTip("Во сколько раз разбавить (1 = без разбавления)") dilution_item.setForeground(QColor(0, 0, 0))
self.table.setCellWidget(row_count, 4, dilution_edit) dilution_item.setFlags(dilution_item.flags() | Qt.ItemIsEditable)
self.table.setItem(row_count, 4, dilution_item)
self.table.setItem(row_count, 5, QTableWidgetItem(""))
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): def remove_selected_row(self):
"""Удаляет выделенную строку из таблицы (кроме строки растворителя)"""
selected_rows = set() selected_rows = set()
for item in self.table.selectedItems(): for item in self.table.selectedItems():
selected_rows.add(item.row()) selected_rows.add(item.row())
# Удаляем строки в обратном порядке, пропуская строку растворителя (индекс 0)
for row in sorted(selected_rows, reverse=True): for row in sorted(selected_rows, reverse=True):
if row > 0: # Не удаляем строку растворителя if row > 0:
self.table.removeRow(row) self.table.removeRow(row)
def get_table_data(self) -> list: def get_table_data(self) -> list:
"""Возвращает данные таблицы в виде списка списков (только реагенты, без растворителя)"""
data = [] data = []
# Начинаем с 1 строки (пропускаем растворитель)
for row in range(1, self.table.rowCount()): for row in range(1, self.table.rowCount()):
row_data = [] row_data = []
# Название (колонка 0)
name_item = self.table.item(row, 0) name_item = self.table.item(row, 0)
row_data.append(name_item.text() if name_item else "") row_data.append(name_item.text() if name_item else "")
# Процент (колонка 1)
percent_item = self.table.item(row, 1) percent_item = self.table.item(row, 1)
row_data.append(percent_item.text() if percent_item else "0") row_data.append(percent_item.text() if percent_item else "0")
# Единица измерения (колонка 2 - комбобокс)
unit_widget = self.table.cellWidget(row, 2) unit_widget = self.table.cellWidget(row, 2)
if unit_widget and isinstance(unit_widget, QComboBox): if unit_widget and isinstance(unit_widget, QComboBox):
row_data.append(unit_widget.currentText()) row_data.append(unit_widget.currentText())
else: else:
row_data.append("мг") row_data.append("мг")
# Коэффициент (колонка 3)
coeff_item = self.table.item(row, 3) coeff_item = self.table.item(row, 3)
row_data.append(coeff_item.text() if coeff_item else "1.0") row_data.append(coeff_item.text() if coeff_item else "1")
# Разбавление (колонка 4 - spinbox) dilution_item = self.table.item(row, 4)
dilution_widget = self.table.cellWidget(row, 4) if dilution_item:
if dilution_widget and isinstance(dilution_widget, QDoubleSpinBox): try:
dilution_factor = dilution_widget.value() dilution_factor = float(dilution_item.text())
except ValueError:
dilution_factor = 1.0
row_data.append(dilution_factor) row_data.append(dilution_factor)
else: else:
row_data.append(1.0) row_data.append(1.0)
data.append(row_data) data.append(row_data)
return data return data
def update_solvent_percent(self, solvent_percent: float): def update_solvent_percent(self, solvent_percent: float):
"""Обновляет процент растворителя в первой строке"""
percent_item = self.table.item(0, 1) percent_item = self.table.item(0, 1)
if percent_item: if percent_item:
percent_item.setText(f"{solvent_percent:.2f}") percent_item.setText(self.format_number(solvent_percent))
def show_error(self, message: str): def show_error(self, message: str):
"""Показывает сообщение об ошибке"""
QMessageBox.critical(self, "Ошибка", message) QMessageBox.critical(self, "Ошибка", message)
def update_results(self, results: list): def update_results(self, results: list):
"""Обновляет столбец результатов (индекс 5) в таблице"""
# Начинаем с 1 строки (реагенты), 0 строка - растворитель
for row, amount in enumerate(results, start=1): for row, amount in enumerate(results, start=1):
if row < self.table.rowCount(): if row < self.table.rowCount():
formatted_amount = f"{amount:.4f}" formatted_amount = self.format_number(amount)
self.table.setItem(row, 5, QTableWidgetItem(formatted_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): def update_solvent_result(self, solvent_amount: float, unit: str):
"""Обновляет результат для растворителя в первой строке""" formatted_amount = self.format_number(solvent_amount)
formatted_amount = f"{solvent_amount:.4f}"
result_item = self.table.item(0, 5) result_item = self.table.item(0, 5)
if result_item: if result_item:
result_item.setText(formatted_amount) result_item.setText(formatted_amount)
result_item.setBackground(QColor(220, 255, 220))
# Также обновляем единицу измерения в колонке 2 для информации result_item.setForeground(QColor(0, 0, 0))
unit_item = self.table.item(0, 2) unit_item = self.table.item(0, 2)
if unit_item: if unit_item:
unit_item.setText(unit) unit_item.setText(unit)
def update_display(self, solvent: str, total_amount: float, amount_unit: str): def update_display(self, solvent: str, total_amount: float, amount_unit: str):
"""Обновляет отображение растворителя и общего количества среды"""
self.solvent_input.setText(solvent) self.solvent_input.setText(solvent)
self.update_solvent_name() self.update_solvent_name()
self.amount_input.setValue(total_amount) self.amount_input.setValue(total_amount)
self.amount_unit_combo.setCurrentText(amount_unit) self.amount_unit_combo.setCurrentText(amount_unit)
def clear_results(self): def clear_results(self):
"""Очищает столбец результатов для всех строк"""
for row in range(self.table.rowCount()): 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) percent_item = self.table.item(0, 1)
if percent_item: if percent_item:
percent_item.setText("") percent_item.setText("")
# Очищаем единицу измерения растворителя
unit_item = self.table.item(0, 2) unit_item = self.table.item(0, 2)
if unit_item: if unit_item:
unit_item.setText("-") unit_item.setText(self.amount_unit_combo.currentText())