Добавлена функция передачи данных калькулятора в расчёт полнофакторного анализа

This commit is contained in:
2026-05-07 15:02:28 +05:00
parent e3fbaceb24
commit e6e86e50a3
17 changed files with 1540 additions and 553 deletions
+3
View File
@@ -0,0 +1,3 @@
"""Цифровой помощник биохимика - основная библиотека"""
__version__ = "1.0.0"
@@ -0,0 +1,4 @@
from .medium_controller import MediumController
from .experiment_controller import ExperimentController
__all__ = ['MediumController', 'ExperimentController']
@@ -0,0 +1,23 @@
from ..models.experiment_model import ExperimentModel
class ExperimentController:
def __init__(self, view):
self.model = ExperimentModel()
self.view = view
def update_model_from_view(self):
factors = self.view.get_factors_data()
self.model.set_factors(factors)
responses = []
for row in range(self.view.responses_table.rowCount()):
name_item = self.view.responses_table.item(row, 0)
unit_item = self.view.responses_table.item(row, 1)
if name_item:
responses.append({
'name': name_item.text(),
'unit': unit_item.text() if unit_item else ""
})
self.model.set_responses(responses)
self.model.set_center_points(self.view.center_points_spin.value())
self.model.set_randomize(self.view.randomize_check.isChecked())
@@ -0,0 +1,118 @@
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
import json
from ..models.medium_model import MediumModel
from ..models.reagent import Reagent
class MediumController:
def __init__(self, view):
self.model = MediumModel()
self.view = view
self._connect_signals()
self._setup_initial_data()
def _connect_signals(self):
self.view.add_row_btn.clicked.connect(self.add_reagent_row)
self.view.remove_row_btn.clicked.connect(self.remove_reagent_row)
self.view.calculate_btn.clicked.connect(self._perform_calculation)
self.view.save_btn.clicked.connect(self.save_composition)
self.view.load_btn.clicked.connect(self.load_composition)
self.view.solvent_input.textChanged.connect(self.view.update_solvent_name)
def _setup_initial_data(self):
self.view.add_initial_rows()
def add_reagent_row(self):
self.view.add_new_row()
def remove_reagent_row(self):
self.view.remove_selected_row()
def _perform_calculation(self):
try:
self._update_model_from_view()
results, solvent_amount, solvent_percentage = self.model.calculate_amounts()
self.view.update_results(results)
self.view.update_solvent_result(solvent_amount, self.model.amount_unit)
self.view.update_solvent_percent(solvent_percentage)
except ValueError as e:
self.view.show_error(f"Ошибка в данных: {str(e)}")
except Exception as e:
self.view.show_error(f"Неожиданная ошибка: {str(e)}")
def _update_model_from_view(self):
self.model.reagents.clear()
self.model.total_amount = self.view.amount_input.value()
self.model.amount_unit = self.view.amount_unit_combo.currentText()
self.model.solvent = self.view.solvent_input.text()
for row in range(1, self.view.table.rowCount()):
name_item = self.view.table.item(row, 0)
percentage_item = self.view.table.item(row, 1)
unit_widget = self.view.table.cellWidget(row, 2)
conversion_item = self.view.table.item(row, 3)
dilution_item = self.view.table.item(row, 4)
if not all([name_item, percentage_item, conversion_item]):
continue
try:
name = name_item.text()
percentage = float(percentage_item.text())
unit = unit_widget.currentText() if unit_widget else "мг"
conversion_factor = float(conversion_item.text())
dilution_factor = float(dilution_item.text()) if dilution_item else 1.0
reagent = Reagent(name, percentage, unit, conversion_factor)
reagent.dilution_factor = dilution_factor
self.model.reagents.append(reagent)
except ValueError as e:
raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}")
def save_composition(self):
filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)")
if filename:
if not filename.lower().endswith('.json'):
filename += '.json'
try:
self._update_model_from_view()
self.model.save_to_file(filename)
QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!")
except Exception as e:
self.view.show_error(f"Ошибка сохранения: {str(e)}")
def load_composition(self):
filename, _ = QFileDialog.getOpenFileName(self.view, "Загрузить состав среды", "", "JSON Files (*.json);;All Files (*)")
if filename:
try:
self.model.load_from_file(filename)
self._update_view_from_model()
QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен")
except FileNotFoundError:
QMessageBox.critical(self.view, "Ошибка", f"Файл не найден: {filename}")
except json.JSONDecodeError as e:
QMessageBox.critical(self.view, "Ошибка", f"Неверный формат JSON-файла: {str(e)}")
except Exception as e:
QMessageBox.critical(self.view, "Ошибка", f"Ошибка при загрузке состава: {str(e)}")
def _update_view_from_model(self):
while self.view.table.rowCount() > 1:
self.view.table.removeRow(1)
if self.view.table.rowCount() == 0:
self.view.add_solvent_row()
self.view.amount_input.setValue(self.model.total_amount)
index = self.view.amount_unit_combo.findText(self.model.amount_unit)
if index >= 0:
self.view.amount_unit_combo.setCurrentIndex(index)
self.view.solvent_input.setText(self.model.solvent)
self.view.update_solvent_name()
for reagent in self.model.reagents:
row = self.view.table.rowCount()
self.view.table.insertRow(row)
self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}"))
unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText(reagent.unit)
self.view.table.setCellWidget(row, 2, unit_combo)
self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}"))
self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}"))
self.view.table.setItem(row, 5, QTableWidgetItem(""))
self.view.clear_results()
@@ -0,0 +1,5 @@
from .reagent import Reagent
from .medium_model import MediumModel
from .experiment_model import ExperimentModel
__all__ = ['Reagent', 'MediumModel', 'ExperimentModel']
@@ -0,0 +1,80 @@
import numpy as np
from typing import List, Dict, Any
class ExperimentModel:
def __init__(self):
self.factors = []
self.responses = []
self.center_points = 3
self.randomize = True
def set_factors(self, factors: List[Dict]):
self.factors = factors
def set_responses(self, responses: List[Dict]):
self.responses = responses
def set_center_points(self, n: int):
self.center_points = n
def set_randomize(self, value: bool):
self.randomize = value
def calculate_factorial_design(self) -> List[Dict]:
k = len(self.factors)
if k == 0:
return []
n_factorial = 2 ** k
design = []
for i in range(n_factorial):
experiment = {}
for j in range(k):
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1
natural_value = self.factors[j]['low'] if coded_level == -1 else self.factors[j]['high']
experiment[f"Фактор_{j+1}"] = {
'coded': coded_level,
'natural': natural_value,
'name': self.factors[j]['name'],
'unit': self.factors[j]['unit']
}
design.append(experiment)
for i in range(self.center_points):
center_experiment = {}
for j in range(k):
center_experiment[f"Фактор_{j+1}"] = {
'coded': 0,
'natural': self.factors[j]['center'],
'name': self.factors[j]['name'],
'unit': self.factors[j]['unit']
}
center_experiment['is_center'] = True
center_experiment['center_num'] = i + 1
design.append(center_experiment)
if self.randomize:
import random
random.shuffle(design)
return design
def analyze_results(self, results: List[List[float]], design: List[Dict]) -> Dict:
analysis = {}
for resp_idx, response in enumerate(self.responses):
resp_name = response.get('name', f'Отклик_{resp_idx+1}')
y_values = [results[i][resp_idx] for i in range(len(results))]
mean_y = np.mean(y_values)
variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0
std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0
cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0
factorial_y = []
center_y = []
for i, exp in enumerate(design):
if exp.get('is_center', False):
center_y.append(y_values[i])
else:
factorial_y.append(y_values[i])
center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0
analysis[resp_name] = {
'mean': mean_y, 'variance': variance, 'std_dev': std_dev, 'cv': cv,
'factorial_values': factorial_y, 'center_values': center_y, 'center_variance': center_variance,
'n_factorial': len(factorial_y), 'n_center': len(center_y)
}
return analysis
@@ -0,0 +1,113 @@
import json
from typing import List, Tuple
from .reagent import Reagent
VOLUME_UNITS = {
'нл': 0.001, 'мкл': 1.0, 'мл': 1000.0, 'л': 1000000.0
}
MASS_UNITS = {
'нг': 0.000001, 'мкг': 0.001, 'мг': 1.0, 'г': 1000.0, 'кг': 1000000.0
}
class MediumModel:
def __init__(self):
self.total_amount = 100.0
self.amount_unit = 'мл'
self.solvent = 'Вода'
self.reagents = []
def convert_amount(self, amount_base: float, target_unit: str, is_volume: bool) -> float:
if is_volume:
conversion_factor = VOLUME_UNITS.get(target_unit, 1.0)
else:
conversion_factor = MASS_UNITS.get(target_unit, 1.0)
return amount_base / conversion_factor
def calculate_amounts(self) -> Tuple[List[float], float, float]:
results = []
if not self.reagents:
return results, self.total_amount, 100.0
total_percentage = sum(r.percentage for r in self.reagents)
if total_percentage > 100:
raise ValueError(f"Сумма процентов реагентов ({total_percentage:.2f}%) превышает 100%")
total_in_base = self.total_amount * VOLUME_UNITS[self.amount_unit]
undiluted_amounts = []
for reagent in self.reagents:
amount_in_base = (reagent.percentage / 100) * total_in_base
is_volume = reagent.unit in VOLUME_UNITS
adjusted_amount = amount_in_base * reagent.conversion_factor
final_amount = self.convert_amount(adjusted_amount, reagent.unit, is_volume)
undiluted_amounts.append(final_amount)
diluted_amounts = []
total_diluted_volume_base = 0
for i, reagent in enumerate(self.reagents):
dilution_factor = getattr(reagent, 'dilution_factor', 1.0)
if dilution_factor <= 0:
dilution_factor = 1.0
diluted_amount = undiluted_amounts[i] * dilution_factor
diluted_amounts.append(diluted_amount)
is_volume = reagent.unit in VOLUME_UNITS
if is_volume:
reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.unit]
else:
reagent_volume_base = 0
total_diluted_volume_base += reagent_volume_base
solvent_volume_base = total_in_base - total_diluted_volume_base
solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit]
if solvent_amount < 0:
solvent_amount = 0
solvent_percentage = 100 - total_percentage
return diluted_amounts, solvent_amount, solvent_percentage
def save_to_file(self, filename: str):
data = {
'total_amount': self.total_amount,
'amount_unit': self.amount_unit,
'solvent': self.solvent,
'reagents': [{'name': r.name, 'percentage': r.percentage, 'unit': r.unit,
'conversion_factor': r.conversion_factor, 'dilution_factor': getattr(r, 'dilution_factor', 1.0)}
for r in self.reagents]
}
with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
def load_from_file(self, filename: str):
with open(filename, 'r', encoding='utf-8') as f:
data = json.load(f)
self.total_amount = data['total_amount']
self.amount_unit = data['amount_unit']
self.solvent = data['solvent']
self.reagents.clear()
for r_data in data['reagents']:
reagent = Reagent(r_data['name'], r_data['percentage'], r_data['unit'], r_data.get('conversion_factor', 1.0))
reagent.dilution_factor = r_data.get('dilution_factor', 1.0)
self.reagents.append(reagent)
def add_reagent(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0, dilution_factor: float = 1.0):
reagent = Reagent(name, percentage, unit, conversion_factor)
reagent.dilution_factor = dilution_factor
self.reagents.append(reagent)
def remove_reagent(self, index: int):
if 0 <= index < len(self.reagents):
del self.reagents[index]
def clear_reagents(self):
self.reagents.clear()
def get_reagent_count(self) -> int:
return len(self.reagents)
def set_total_amount(self, amount: float, unit: str):
self.total_amount = amount
self.amount_unit = unit
def set_solvent(self, solvent_name: str):
self.solvent = solvent_name
@@ -0,0 +1,10 @@
class Reagent:
def __init__(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0):
self.name = name
self.percentage = percentage
self.unit = unit
self.conversion_factor = conversion_factor
self.dilution_factor = 1.0
def __repr__(self):
return f"Reagent(name={self.name}, percentage={self.percentage}, unit={self.unit})"
@@ -0,0 +1,3 @@
"""Утилиты и вспомогательные функции"""
__all__ = []
@@ -0,0 +1,5 @@
from .main_window import MainWindow
from .medium_view import MediumCalculatorWindow
from .experiment_view import ExperimentDesignWindow
__all__ = ['MainWindow', 'MediumCalculatorWindow', 'ExperimentDesignWindow']
@@ -0,0 +1,470 @@
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox,
QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, QTextEdit,
QTabWidget, QFormLayout, QCheckBox, QScrollArea, QFileDialog)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QFont
import csv
import random
import numpy as np
class ExperimentDesignWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика")
self.setGeometry(200, 100, 1200, 800)
self.setStyleSheet("""
QMainWindow { background-color: #f5f5f5; }
QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; }
QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; }
QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; }
QPushButton:hover { background-color: #45a049; }
QPushButton#danger { background-color: #f44336; }
QPushButton#danger:hover { background-color: #da190b; }
QTableWidget { gridline-color: #ddd; }
QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; }
""")
self._init_ui()
def _init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
title_label = QLabel("Планирование полнофакторного эксперимента (DoE)")
title_font = QFont()
title_font.setPointSize(18)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("color: #2E7D32;")
layout.addWidget(title_label)
tabs = QTabWidget()
# Вкладка параметров
params_tab = QWidget()
params_layout = QVBoxLayout(params_tab)
factors_group = QGroupBox("Факторы эксперимента (независимые переменные)")
factors_layout = QVBoxLayout()
info_label = QLabel("Определите факторы, которые влияют на ваш эксперимент:")
info_label.setStyleSheet("color: #555; font-weight: normal;")
factors_layout.addWidget(info_label)
self.factors_table = QTableWidget()
self.factors_table.setColumnCount(6)
self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг",
"Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"])
self.factors_table.setRowCount(2)
sample_factors = [["Температура", "31", "6", "37", "25", "°C"], ["pH", "7.0", "0.5", "7.5", "6.5", ""]]
for i, factor in enumerate(sample_factors):
for j, value in enumerate(factor):
item = QTableWidgetItem(value)
if j in [3, 4]:
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(i, j, item)
self.factors_table.setColumnWidth(0, 150)
self.factors_table.setColumnWidth(1, 120)
self.factors_table.setColumnWidth(2, 80)
self.factors_table.setColumnWidth(3, 120)
self.factors_table.setColumnWidth(4, 120)
self.factors_table.setColumnWidth(5, 100)
self.factors_table.itemChanged.connect(self.on_factor_changed)
factors_layout.addWidget(self.factors_table)
factor_buttons = QHBoxLayout()
add_factor_btn = QPushButton("+ Добавить фактор")
add_factor_btn.clicked.connect(self.add_factor_row)
remove_factor_btn = QPushButton("- Удалить последний")
remove_factor_btn.clicked.connect(self.remove_factor_row)
factor_buttons.addWidget(add_factor_btn)
factor_buttons.addWidget(remove_factor_btn)
factor_buttons.addStretch()
factors_layout.addLayout(factor_buttons)
factors_group.setLayout(factors_layout)
params_layout.addWidget(factors_group)
settings_group = QGroupBox("Настройки эксперимента")
settings_layout = QHBoxLayout()
center_layout = QHBoxLayout()
center_layout.addWidget(QLabel("Количество центральных точек:"))
self.center_points_spin = QSpinBox()
self.center_points_spin.setRange(0, 10)
self.center_points_spin.setValue(3)
center_layout.addWidget(self.center_points_spin)
settings_layout.addLayout(center_layout)
self.randomize_check = QCheckBox("Рэндомизировать порядок опытов")
self.randomize_check.setChecked(True)
settings_layout.addWidget(self.randomize_check)
settings_layout.addStretch()
settings_group.setLayout(settings_layout)
params_layout.addWidget(settings_group)
responses_group = QGroupBox("Отклики (зависимые переменные)")
responses_layout = QVBoxLayout()
self.responses_table = QTableWidget()
self.responses_table.setColumnCount(2)
self.responses_table.setHorizontalHeaderLabels(["Отклик", "Единица измерения"])
self.responses_table.setRowCount(2)
sample_responses = [["Оптическая плотность (OD600)", ""], ["Концентрация целевого продукта", "мг/мл"]]
for i, response in enumerate(sample_responses):
for j, value in enumerate(response):
self.responses_table.setItem(i, j, QTableWidgetItem(value))
responses_layout.addWidget(self.responses_table)
response_buttons = QHBoxLayout()
add_response_btn = QPushButton("+ Добавить отклик")
add_response_btn.clicked.connect(self.add_response_row)
remove_response_btn = QPushButton("- Удалить последний")
remove_response_btn.clicked.connect(self.remove_response_row)
response_buttons.addWidget(add_response_btn)
response_buttons.addWidget(remove_response_btn)
response_buttons.addStretch()
responses_layout.addLayout(response_buttons)
responses_group.setLayout(responses_layout)
params_layout.addWidget(responses_group)
tabs.addTab(params_tab, "📝 Параметры эксперимента")
# Вкладка матрицы планирования
plan_tab = QWidget()
plan_layout = QVBoxLayout(plan_tab)
plan_info = QLabel("Полнофакторный план эксперимента с центральными точками")
plan_info.setAlignment(Qt.AlignCenter)
plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
plan_layout.addWidget(plan_info)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
matrix_widget = QWidget()
matrix_layout = QVBoxLayout(matrix_widget)
self.design_matrix = QTableWidget()
matrix_layout.addWidget(self.design_matrix)
scroll.setWidget(matrix_widget)
plan_layout.addWidget(scroll)
buttons_layout = QHBoxLayout()
generate_btn = QPushButton("Сгенерировать план эксперимента")
generate_btn.clicked.connect(self.generate_design_matrix)
buttons_layout.addWidget(generate_btn)
export_btn = QPushButton("📊 Экспорт в CSV")
export_btn.clicked.connect(self.export_to_csv)
buttons_layout.addWidget(export_btn)
buttons_layout.addStretch()
plan_layout.addLayout(buttons_layout)
self.plan_info_label = QLabel("")
self.plan_info_label.setStyleSheet("color: #666; font-size: 12px; padding: 5px;")
plan_layout.addWidget(self.plan_info_label)
tabs.addTab(plan_tab, "📊 Матрица планирования")
# Вкладка анализа
analysis_tab = QWidget()
analysis_layout = QVBoxLayout(analysis_tab)
analysis_info = QLabel("Введите результаты экспериментов для анализа")
analysis_info.setAlignment(Qt.AlignCenter)
analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
analysis_layout.addWidget(analysis_info)
self.results_table = QTableWidget()
analysis_layout.addWidget(self.results_table)
analyze_btn = QPushButton("Провести регрессионный анализ")
analyze_btn.clicked.connect(self.perform_analysis)
analysis_layout.addWidget(analyze_btn)
self.analysis_output = QTextEdit()
self.analysis_output.setReadOnly(True)
self.analysis_output.setMaximumHeight(200)
analysis_layout.addWidget(self.analysis_output)
tabs.addTab(analysis_tab, "📈 Анализ результатов")
layout.addWidget(tabs)
btn_layout = QHBoxLayout()
close_btn = QPushButton("Закрыть")
close_btn.clicked.connect(self.close)
btn_layout.addStretch()
btn_layout.addWidget(close_btn)
layout.addLayout(btn_layout)
self.generated_design = None
self.factors_data = None
def on_factor_changed(self, item):
row = item.row()
col = item.column()
if col in [1, 2]:
try:
center = float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0
step = float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0
high = center + step
low = center - step
self.factors_table.blockSignals(True)
if self.factors_table.item(row, 3):
self.factors_table.item(row, 3).setText(f"{high:.3f}".rstrip('0').rstrip('.'))
if self.factors_table.item(row, 4):
self.factors_table.item(row, 4).setText(f"{low:.3f}".rstrip('0').rstrip('.'))
self.factors_table.blockSignals(False)
except ValueError:
pass
def add_factor_row(self):
row = self.factors_table.rowCount()
self.factors_table.insertRow(row)
self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}"))
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
high_item = QTableWidgetItem("1")
high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable)
high_item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(row, 3, high_item)
low_item = QTableWidgetItem("-1")
low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable)
low_item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(row, 4, low_item)
self.factors_table.setItem(row, 5, QTableWidgetItem(""))
def remove_factor_row(self):
if self.factors_table.rowCount() > 1:
self.factors_table.removeRow(self.factors_table.rowCount() - 1)
def add_response_row(self):
row = self.responses_table.rowCount()
self.responses_table.insertRow(row)
self.responses_table.setItem(row, 0, QTableWidgetItem(f"Отклик_{row+1}"))
self.responses_table.setItem(row, 1, QTableWidgetItem(""))
def remove_response_row(self):
if self.responses_table.rowCount() > 1:
self.responses_table.removeRow(self.responses_table.rowCount() - 1)
def get_factors_data(self):
factors = []
for row in range(self.factors_table.rowCount()):
try:
factor = {
'name': self.factors_table.item(row, 0).text() if self.factors_table.item(row, 0) else "",
'center': float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0,
'step': float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0,
'high': float(self.factors_table.item(row, 3).text()) if self.factors_table.item(row, 3) else 0,
'low': float(self.factors_table.item(row, 4).text()) if self.factors_table.item(row, 4) else 0,
'unit': self.factors_table.item(row, 5).text() if self.factors_table.item(row, 5) else ""
}
factors.append(factor)
except (ValueError, AttributeError):
continue
return factors
def calculate_factorial_design(self, factors):
k = len(factors)
if k == 0:
return []
n_factorial = 2 ** k
design = []
for i in range(n_factorial):
experiment = {}
for j in range(k):
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1
natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high']
experiment[f"Фактор_{j+1}"] = {
'coded': coded_level,
'natural': natural_value,
'name': factors[j]['name'],
'unit': factors[j]['unit']
}
design.append(experiment)
n_center = self.center_points_spin.value()
for i in range(n_center):
center_experiment = {}
for j in range(k):
center_experiment[f"Фактор_{j+1}"] = {
'coded': 0,
'natural': factors[j]['center'],
'name': factors[j]['name'],
'unit': factors[j]['unit']
}
center_experiment['is_center'] = True
center_experiment['center_num'] = i + 1
design.append(center_experiment)
if self.randomize_check.isChecked():
random.shuffle(design)
return design
def generate_design_matrix(self):
factors = self.get_factors_data()
if len(factors) == 0:
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
return
self.factors_data = factors
design = self.calculate_factorial_design(factors)
self.generated_design = design
n_experiments = len(design)
n_factors = len(factors)
self.design_matrix.setRowCount(n_experiments)
self.design_matrix.setColumnCount(n_factors + 2)
headers = ["№ опыта"] + [f["name"] for f in factors] + ["Тип точки"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, experiment in enumerate(design):
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
for factor_idx in range(n_factors):
factor_key = f"Фактор_{factor_idx + 1}"
value = experiment[factor_key]['natural']
unit = factors[factor_idx]['unit']
if isinstance(value, float):
if value == int(value):
display_value = str(int(value))
else:
display_value = f"{value:.3f}".rstrip('0').rstrip('.')
else:
display_value = str(value)
if unit:
display_value += f" {unit}"
item = QTableWidgetItem(display_value)
if experiment.get('is_center', False):
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, factor_idx + 1, item)
if experiment.get('is_center', False):
type_item = QTableWidgetItem(f"Центральная #{experiment['center_num']}")
type_item.setBackground(QColor(255, 255, 200))
else:
levels = []
for factor_idx in range(n_factors):
factor_key = f"Фактор_{factor_idx + 1}"
coded = experiment[factor_key]['coded']
levels.append("+" if coded == 1 else "-")
type_item = QTableWidgetItem(f"Факторная ({''.join(levels)})")
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
self.design_matrix.resizeColumnsToContents()
n_factorial = 2 ** n_factors
n_center = self.center_points_spin.value()
self.plan_info_label.setText(f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов")
self.setup_results_table(n_experiments)
QMessageBox.information(self, "Успех", f"Сгенерирован план для {n_factors} факторов\nФакторных точек: {n_factorial}\nЦентральных точек: {n_center}\nВсего опытов: {n_experiments}")
def setup_results_table(self, n_experiments):
n_responses = self.responses_table.rowCount()
self.results_table.setRowCount(n_experiments)
self.results_table.setColumnCount(n_responses + 1)
headers = ["№ опыта"] + [self.responses_table.item(i, 0).text() if self.responses_table.item(i, 0) else f"Отклик_{i+1}" for i in range(n_responses)]
self.results_table.setHorizontalHeaderLabels(headers)
for i in range(n_experiments):
self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1)))
self.results_table.setColumnWidth(0, 80)
for i in range(n_responses):
self.results_table.setColumnWidth(i + 1, 150)
def export_to_csv(self):
if self.design_matrix.rowCount() == 0:
QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!")
return
filename, _ = QFileDialog.getSaveFileName(self, "Сохранить план эксперимента", "", "CSV Files (*.csv);;All Files (*)")
if filename:
if not filename.lower().endswith('.csv'):
filename += '.csv'
try:
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.writer(f)
headers = []
for j in range(self.design_matrix.columnCount()):
header_item = self.design_matrix.horizontalHeaderItem(j)
headers.append(header_item.text() if header_item else f"Колонка_{j+1}")
writer.writerow(headers)
for i in range(self.design_matrix.rowCount()):
row = [self.design_matrix.item(i, j).text() if self.design_matrix.item(i, j) else "" for j in range(self.design_matrix.columnCount())]
writer.writerow(row)
QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}")
def perform_analysis(self):
n_responses = self.responses_table.rowCount()
if n_responses == 0:
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один отклик!")
return
if self.generated_design is None:
QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!")
return
results = []
for i in range(self.results_table.rowCount()):
row_results = []
for j in range(1, self.results_table.columnCount()):
item = self.results_table.item(i, j)
if item and item.text():
try:
row_results.append(float(item.text()))
except ValueError:
row_results.append(None)
else:
row_results.append(None)
results.append(row_results)
for i, row in enumerate(results):
for j, val in enumerate(row):
if val is None:
self.analysis_output.setText(f"Ошибка: Не введены результаты для опыта {i+1}, отклик {j+1}")
return
self.analysis_output.clear()
self.analysis_output.append("=" * 60)
self.analysis_output.append("РЕЗУЛЬТАТЫ РЕГРЕССИОННОГО АНАЛИЗА")
self.analysis_output.append("=" * 60)
factors = self.get_factors_data()
design = self.generated_design
for resp_idx in range(n_responses):
resp_name = self.responses_table.item(resp_idx, 0).text()
self.analysis_output.append(f"\n📊 Отклик: {resp_name}")
self.analysis_output.append("-" * 40)
y_values = [results[i][resp_idx] for i in range(len(results))]
mean_y = np.mean(y_values)
variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0
std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0
cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0
self.analysis_output.append(f"Среднее значение: {mean_y:.4f}")
self.analysis_output.append(f"Общая дисперсия: {variance:.4f}")
self.analysis_output.append(f"Стандартное отклонение: {std_dev:.4f}")
self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%")
factorial_y = []
center_y = []
for i, exp in enumerate(design):
if exp.get('is_center', False):
center_y.append(y_values[i])
else:
factorial_y.append(y_values[i])
if len(center_y) > 1:
center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0
self.analysis_output.append(f"\nЦентральные точки (n={len(center_y)}):")
self.analysis_output.append(f" Среднее: {np.mean(center_y):.4f}")
self.analysis_output.append(f" Дисперсия воспроизводимости: {center_variance:.4f}")
if len(factorial_y) > 0 and center_variance > 0:
factorial_variance = np.var(factorial_y, ddof=1) if len(factorial_y) > 1 else 0
if factorial_variance > 0:
fisher = max(factorial_variance, center_variance) / min(factorial_variance, center_variance)
self.analysis_output.append(f"\nКритерий Фишера (F-отношение): {fisher:.4f}")
if fisher < 4.0:
self.analysis_output.append("✅ Модель адекватна экспериментальным данным")
else:
self.analysis_output.append("⚠️ Модель может быть неадекватна, требуется проверка")
self.analysis_output.append("\n" + "=" * 60)
self.analysis_output.append("Анализ завершен")
def show_error(self, message: str):
QMessageBox.critical(self, "Ошибка", message)
@@ -0,0 +1,101 @@
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QFrame)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Цифровой помощник биохимика - Главное меню")
self.setGeometry(300, 200, 700, 500)
self.setStyleSheet("""
QMainWindow { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #e8f4f8, stop:1 #f0f0f0); }
QPushButton { background-color: #2196F3; color: white; border: none; padding: 15px; font-size: 16px; font-weight: bold; border-radius: 8px; }
QPushButton:hover { background-color: #1976D2; }
QLabel { color: #333; font-size: 14px; }
""")
self._init_ui()
self.medium_calculator = None
self.experiment_window = None
def _init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
layout.setSpacing(20)
layout.setContentsMargins(50, 50, 50, 50)
title_label = QLabel("Цифровой помощник биохимика")
title_font = QFont()
title_font.setPointSize(20)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("color: #1565C0;")
layout.addWidget(title_label)
subtitle_label = QLabel("Биотехнологические инструменты для лаборатории")
subtitle_font = QFont()
subtitle_font.setPointSize(12)
subtitle_label.setFont(subtitle_font)
subtitle_label.setAlignment(Qt.AlignCenter)
subtitle_label.setStyleSheet("color: #666;")
layout.addWidget(subtitle_label)
layout.addSpacing(20)
btn_medium = QPushButton("🧪 Калькулятор питательных сред")
btn_medium.setMinimumHeight(80)
btn_medium.clicked.connect(self.open_medium_calculator)
layout.addWidget(btn_medium)
desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\nразбавления реагентов и автоматическим расчётом растворителя")
desc1_label.setAlignment(Qt.AlignCenter)
desc1_label.setWordWrap(True)
desc1_label.setStyleSheet("color: #555; font-size: 11px;")
layout.addWidget(desc1_label)
layout.addSpacing(15)
btn_experiment = QPushButton("📊 Планирование эксперимента (DoE)")
btn_experiment.setMinimumHeight(80)
btn_experiment.clicked.connect(self.open_experiment_designer)
layout.addWidget(btn_experiment)
desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\nмногомерный анализ и визуализация")
desc2_label.setAlignment(Qt.AlignCenter)
desc2_label.setWordWrap(True)
desc2_label.setStyleSheet("color: #555; font-size: 11px;")
layout.addWidget(desc2_label)
layout.addSpacing(15)
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
layout.addWidget(line)
bottom_layout = QHBoxLayout()
version_label = QLabel("Версия 1.0.0 | © 2026 Цифровой помощник биохимика")
version_label.setStyleSheet("color: #999; font-size: 10px;")
bottom_layout.addWidget(version_label)
bottom_layout.addStretch()
btn_exit = QPushButton("Выход")
btn_exit.setMaximumWidth(150)
btn_exit.setStyleSheet("QPushButton { background-color: #f44336; padding: 8px; font-size: 14px; } QPushButton:hover { background-color: #da190b; }")
btn_exit.clicked.connect(self.close)
bottom_layout.addWidget(btn_exit)
layout.addLayout(bottom_layout)
def open_medium_calculator(self):
from .medium_view import MediumCalculatorWindow
from ..controllers.medium_controller import MediumController
self.medium_calculator = MediumCalculatorWindow()
self.medium_controller = MediumController(self.medium_calculator)
self.medium_calculator.show()
def open_experiment_designer(self):
from .experiment_view import ExperimentDesignWindow
from ..controllers.experiment_controller import ExperimentController
self.experiment_window = ExperimentDesignWindow()
self.experiment_controller = ExperimentController(self.experiment_window)
self.experiment_window.show()
@@ -0,0 +1,207 @@
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget,
QMessageBox, QGroupBox, QFrame)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QColor
class MediumCalculatorWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Калькулятор питательных сред")
self.setGeometry(200, 100, 1200, 700)
self.setStyleSheet("""
QMainWindow { background-color: #f5f5f5; }
QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; background-color: white; }
QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; }
QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 15px; border-radius: 4px; font-weight: bold; }
QPushButton:hover { background-color: #1976D2; }
QPushButton#danger { background-color: #f44336; }
QPushButton#success { background-color: #4CAF50; }
QTableWidget { gridline-color: #ddd; background-color: white; }
QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; }
""")
self._init_ui()
def _init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
title_label = QLabel("Калькулятор питательных сред")
title_label.setAlignment(Qt.AlignCenter)
title_font = QFont()
title_font.setPointSize(18)
title_font.setBold(True)
title_label.setFont(title_font)
layout.addWidget(title_label)
params_group = QGroupBox("Параметры среды")
params_layout = QHBoxLayout()
amount_layout = QHBoxLayout()
amount_layout.addWidget(QLabel("Общее количество:"))
self.amount_input = QDoubleSpinBox()
self.amount_input.setRange(0.001, 1000000.0)
self.amount_input.setValue(1000.0)
amount_layout.addWidget(self.amount_input)
self.amount_unit_combo = QComboBox()
self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"])
self.amount_unit_combo.setCurrentText("мл")
amount_layout.addWidget(self.amount_unit_combo)
params_layout.addLayout(amount_layout)
solvent_layout = QHBoxLayout()
solvent_layout.addWidget(QLabel("Растворитель:"))
self.solvent_input = QLineEdit("Вода")
solvent_layout.addWidget(self.solvent_input)
params_layout.addLayout(solvent_layout)
params_layout.addStretch()
params_group.setLayout(params_layout)
layout.addWidget(params_group)
table_group = QGroupBox("Состав среды")
table_layout = QVBoxLayout()
self.table = QTableWidget()
self.table.setColumnCount(6)
self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"])
self.table.setAlternatingRowColors(True)
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.table.setColumnWidth(0, 180)
self.table.setColumnWidth(1, 70)
self.table.setColumnWidth(2, 90)
self.table.setColumnWidth(3, 70)
self.table.setColumnWidth(4, 100)
self.table.setColumnWidth(5, 120)
table_layout.addWidget(self.table)
table_group.setLayout(table_layout)
layout.addWidget(table_group)
btn_group = QGroupBox("Управление")
btn_layout = QHBoxLayout()
self.add_row_btn = QPushButton(" Добавить реагент")
self.remove_row_btn = QPushButton(" Удалить реагент")
self.remove_row_btn.setObjectName("danger")
btn_layout.addWidget(self.add_row_btn)
btn_layout.addWidget(self.remove_row_btn)
btn_layout.addStretch()
self.calculate_btn = QPushButton("🧮 Рассчитать")
self.calculate_btn.setObjectName("success")
self.save_btn = QPushButton("💾 Сохранить")
self.load_btn = QPushButton("📂 Загрузить")
btn_layout.addWidget(self.calculate_btn)
btn_layout.addWidget(self.save_btn)
btn_layout.addWidget(self.load_btn)
btn_group.setLayout(btn_layout)
layout.addWidget(btn_group)
info_frame = QFrame()
info_frame.setFrameShape(QFrame.StyledPanel)
info_frame.setStyleSheet("background-color: #e3f2fd; border-radius: 5px;")
info_layout = QHBoxLayout(info_frame)
info_label = QLabel("ℹ️ Подсказка: Реагенты в массовых единицах не учитываются при расчёте объёма растворителя")
info_layout.addWidget(info_label)
info_layout.addStretch()
layout.addWidget(info_frame)
def add_initial_rows(self):
self.add_solvent_row()
self.add_new_row()
def add_solvent_row(self):
row = self.table.rowCount()
self.table.insertRow(row)
solvent_item = QTableWidgetItem(self.solvent_input.text())
solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable)
solvent_item.setBackground(QColor(230, 230, 230))
font = QFont()
font.setBold(True)
solvent_item.setFont(font)
self.table.setItem(row, 0, solvent_item)
for col in [1, 3, 4]:
item = QTableWidgetItem("-")
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
item.setBackground(QColor(230, 230, 230))
self.table.setItem(row, col, item)
unit_item = QTableWidgetItem(self.amount_unit_combo.currentText())
unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable)
unit_item.setBackground(QColor(230, 230, 230))
self.table.setItem(row, 2, unit_item)
result_item = QTableWidgetItem("")
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row, 5, result_item)
def update_solvent_name(self):
if self.table.rowCount() > 0 and self.table.item(0, 0):
self.table.item(0, 0).setText(self.solvent_input.text())
def add_new_row(self):
row = self.table.rowCount()
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row}"))
self.table.setItem(row, 1, QTableWidgetItem("0"))
unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText("мг")
self.table.setCellWidget(row, 2, unit_combo)
self.table.setItem(row, 3, QTableWidgetItem("1"))
self.table.setItem(row, 4, QTableWidgetItem("1"))
self.table.setItem(row, 5, QTableWidgetItem(""))
def remove_selected_row(self):
for row in sorted(set(item.row() for item in self.table.selectedItems()), reverse=True):
if row > 0:
self.table.removeRow(row)
def get_table_data(self):
data = []
for row in range(1, self.table.rowCount()):
name = self.table.item(row, 0).text() if self.table.item(row, 0) else ""
percent = self.table.item(row, 1).text() if self.table.item(row, 1) else "0"
unit_widget = self.table.cellWidget(row, 2)
unit = unit_widget.currentText() if unit_widget else "мг"
coeff = self.table.item(row, 3).text() if self.table.item(row, 3) else "1"
dilution = self.table.item(row, 4).text() if self.table.item(row, 4) else "1"
data.append([name, percent, unit, coeff, dilution])
return data
def update_solvent_percent(self, solvent_percent: float):
if self.table.rowCount() > 0 and self.table.item(0, 1):
self.table.item(0, 1).setText(self.format_number(solvent_percent))
def update_solvent_result(self, solvent_amount: float, unit: str):
if self.table.rowCount() > 0:
if self.table.item(0, 5):
self.table.item(0, 5).setText(self.format_number(solvent_amount))
self.table.item(0, 5).setBackground(QColor(220, 255, 220))
if self.table.item(0, 2):
self.table.item(0, 2).setText(unit)
def update_results(self, results: list):
for row, amount in enumerate(results, start=1):
if row < self.table.rowCount() and self.table.item(row, 5):
self.table.item(row, 5).setText(self.format_number(amount))
self.table.item(row, 5).setBackground(QColor(220, 255, 220))
def clear_results(self):
for row in range(self.table.rowCount()):
if self.table.item(row, 5):
self.table.item(row, 5).setText("")
if row == 0:
self.table.item(row, 5).setBackground(QColor(230, 230, 230))
else:
self.table.item(row, 5).setBackground(QColor(250, 250, 250))
if self.table.rowCount() > 0 and self.table.item(0, 1):
self.table.item(0, 1).setText("")
if self.table.rowCount() > 0 and self.table.item(0, 2):
self.table.item(0, 2).setText(self.amount_unit_combo.currentText())
def format_number(self, value):
if value == int(value):
return str(int(value))
formatted = f"{value:.6f}".rstrip('0').rstrip('.')
if '.' in formatted and len(formatted.split('.')[1]) > 4:
formatted = f"{value:.4f}".rstrip('0').rstrip('.')
return formatted
def show_error(self, message: str):
QMessageBox.critical(self, "Ошибка", message)
+265 -551
View File
@@ -6,366 +6,20 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
NC='\033[0m' NC='\033[0m'
echo -e "${GREEN}=== Реорганизация программы Цифровой помощник биохимика ===${NC}" echo -e "${GREEN}=== Обновление программы: добавление передачи данных в DoE ===${NC}"
# Создаём бэкап текущей версии # Создаём бэкап
BACKUP_DIR="backup_$(date +%Y%m%d_%H%M%S)" BACKUP_DIR="backup_$(date +%Y%m%d_%H%M%S)"
echo -e "${YELLOW}Создание бэкапа в $BACKUP_DIR...${NC}"
mkdir -p "$BACKUP_DIR" mkdir -p "$BACKUP_DIR"
cp *.py "$BACKUP_DIR/" 2>/dev/null cp -r src "$BACKUP_DIR/" 2>/dev/null
echo -e "${GREEN}✓ Бэкап создан${NC}" echo -e "${GREEN}✓ Бэкап создан в $BACKUP_DIR${NC}"
# Создаём структуру папок # Обновляем medium_view.py - добавляем кнопку
echo -e "${YELLOW}Создание структуры проекта...${NC}"
mkdir -p src/models
mkdir -p src/views
mkdir -p src/controllers
mkdir -p src/utils
# Файл: src/models/reagent.py
cat > src/models/reagent.py << 'EOF'
class Reagent:
def __init__(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0):
self.name = name
self.percentage = percentage
self.unit = unit
self.conversion_factor = conversion_factor
self.dilution_factor = 1.0
def __repr__(self):
return f"Reagent(name={self.name}, percentage={self.percentage}, unit={self.unit})"
EOF
# Файл: src/models/__init__.py
cat > src/models/__init__.py << 'EOF'
from .reagent import Reagent
from .medium_model import MediumModel
from .experiment_model import ExperimentModel
__all__ = ['Reagent', 'MediumModel', 'ExperimentModel']
EOF
# Файл: src/models/medium_model.py
cat > src/models/medium_model.py << 'EOF'
import json
from typing import List, Tuple
from .reagent import Reagent
VOLUME_UNITS = {
'нл': 0.001, 'мкл': 1.0, 'мл': 1000.0, 'л': 1000000.0
}
MASS_UNITS = {
'нг': 0.000001, 'мкг': 0.001, 'мг': 1.0, 'г': 1000.0, 'кг': 1000000.0
}
class MediumModel:
def __init__(self):
self.total_amount = 100.0
self.amount_unit = 'мл'
self.solvent = 'Вода'
self.reagents = []
def convert_amount(self, amount_base: float, target_unit: str, is_volume: bool) -> float:
if is_volume:
conversion_factor = VOLUME_UNITS.get(target_unit, 1.0)
else:
conversion_factor = MASS_UNITS.get(target_unit, 1.0)
return amount_base / conversion_factor
def calculate_amounts(self) -> Tuple[List[float], float, float]:
results = []
if not self.reagents:
return results, self.total_amount, 100.0
total_percentage = sum(r.percentage for r in self.reagents)
if total_percentage > 100:
raise ValueError(f"Сумма процентов реагентов ({total_percentage:.2f}%) превышает 100%")
total_in_base = self.total_amount * VOLUME_UNITS[self.amount_unit]
undiluted_amounts = []
for reagent in self.reagents:
amount_in_base = (reagent.percentage / 100) * total_in_base
is_volume = reagent.unit in VOLUME_UNITS
adjusted_amount = amount_in_base * reagent.conversion_factor
final_amount = self.convert_amount(adjusted_amount, reagent.unit, is_volume)
undiluted_amounts.append(final_amount)
diluted_amounts = []
total_diluted_volume_base = 0
for i, reagent in enumerate(self.reagents):
dilution_factor = getattr(reagent, 'dilution_factor', 1.0)
if dilution_factor <= 0:
dilution_factor = 1.0
diluted_amount = undiluted_amounts[i] * dilution_factor
diluted_amounts.append(diluted_amount)
is_volume = reagent.unit in VOLUME_UNITS
if is_volume:
reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.unit]
else:
reagent_volume_base = 0
total_diluted_volume_base += reagent_volume_base
solvent_volume_base = total_in_base - total_diluted_volume_base
solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit]
if solvent_amount < 0:
solvent_amount = 0
solvent_percentage = 100 - total_percentage
return diluted_amounts, solvent_amount, solvent_percentage
def save_to_file(self, filename: str):
data = {
'total_amount': self.total_amount,
'amount_unit': self.amount_unit,
'solvent': self.solvent,
'reagents': [{'name': r.name, 'percentage': r.percentage, 'unit': r.unit,
'conversion_factor': r.conversion_factor, 'dilution_factor': getattr(r, 'dilution_factor', 1.0)}
for r in self.reagents]
}
with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
def load_from_file(self, filename: str):
with open(filename, 'r', encoding='utf-8') as f:
data = json.load(f)
self.total_amount = data['total_amount']
self.amount_unit = data['amount_unit']
self.solvent = data['solvent']
self.reagents.clear()
for r_data in data['reagents']:
reagent = Reagent(r_data['name'], r_data['percentage'], r_data['unit'], r_data.get('conversion_factor', 1.0))
reagent.dilution_factor = r_data.get('dilution_factor', 1.0)
self.reagents.append(reagent)
def add_reagent(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0, dilution_factor: float = 1.0):
reagent = Reagent(name, percentage, unit, conversion_factor)
reagent.dilution_factor = dilution_factor
self.reagents.append(reagent)
def remove_reagent(self, index: int):
if 0 <= index < len(self.reagents):
del self.reagents[index]
def clear_reagents(self):
self.reagents.clear()
def get_reagent_count(self) -> int:
return len(self.reagents)
def set_total_amount(self, amount: float, unit: str):
self.total_amount = amount
self.amount_unit = unit
def set_solvent(self, solvent_name: str):
self.solvent = solvent_name
EOF
# Файл: src/models/experiment_model.py
cat > src/models/experiment_model.py << 'EOF'
import numpy as np
from typing import List, Dict, Any
class ExperimentModel:
def __init__(self):
self.factors = []
self.responses = []
self.center_points = 3
self.randomize = True
def set_factors(self, factors: List[Dict]):
self.factors = factors
def set_responses(self, responses: List[Dict]):
self.responses = responses
def set_center_points(self, n: int):
self.center_points = n
def set_randomize(self, value: bool):
self.randomize = value
def calculate_factorial_design(self) -> List[Dict]:
k = len(self.factors)
if k == 0:
return []
n_factorial = 2 ** k
design = []
for i in range(n_factorial):
experiment = {}
for j in range(k):
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1
natural_value = self.factors[j]['low'] if coded_level == -1 else self.factors[j]['high']
experiment[f"Фактор_{j+1}"] = {
'coded': coded_level,
'natural': natural_value,
'name': self.factors[j]['name'],
'unit': self.factors[j]['unit']
}
design.append(experiment)
for i in range(self.center_points):
center_experiment = {}
for j in range(k):
center_experiment[f"Фактор_{j+1}"] = {
'coded': 0,
'natural': self.factors[j]['center'],
'name': self.factors[j]['name'],
'unit': self.factors[j]['unit']
}
center_experiment['is_center'] = True
center_experiment['center_num'] = i + 1
design.append(center_experiment)
if self.randomize:
import random
random.shuffle(design)
return design
def analyze_results(self, results: List[List[float]], design: List[Dict]) -> Dict:
analysis = {}
for resp_idx, response in enumerate(self.responses):
resp_name = response.get('name', f'Отклик_{resp_idx+1}')
y_values = [results[i][resp_idx] for i in range(len(results))]
mean_y = np.mean(y_values)
variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0
std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0
cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0
factorial_y = []
center_y = []
for i, exp in enumerate(design):
if exp.get('is_center', False):
center_y.append(y_values[i])
else:
factorial_y.append(y_values[i])
center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0
analysis[resp_name] = {
'mean': mean_y, 'variance': variance, 'std_dev': std_dev, 'cv': cv,
'factorial_values': factorial_y, 'center_values': center_y, 'center_variance': center_variance,
'n_factorial': len(factorial_y), 'n_center': len(center_y)
}
return analysis
EOF
# Файл: src/views/__init__.py
cat > src/views/__init__.py << 'EOF'
from .main_window import MainWindow
from .medium_view import MediumCalculatorWindow
from .experiment_view import ExperimentDesignWindow
__all__ = ['MainWindow', 'MediumCalculatorWindow', 'ExperimentDesignWindow']
EOF
# Файл: src/views/main_window.py
cat > src/views/main_window.py << 'EOF'
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QFrame)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Цифровой помощник биохимика - Главное меню")
self.setGeometry(300, 200, 700, 500)
self.setStyleSheet("""
QMainWindow { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #e8f4f8, stop:1 #f0f0f0); }
QPushButton { background-color: #2196F3; color: white; border: none; padding: 15px; font-size: 16px; font-weight: bold; border-radius: 8px; }
QPushButton:hover { background-color: #1976D2; }
QLabel { color: #333; font-size: 14px; }
""")
self._init_ui()
self.medium_calculator = None
self.experiment_window = None
def _init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
layout.setSpacing(20)
layout.setContentsMargins(50, 50, 50, 50)
title_label = QLabel("Цифровой помощник биохимика")
title_font = QFont()
title_font.setPointSize(20)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("color: #1565C0;")
layout.addWidget(title_label)
subtitle_label = QLabel("Биотехнологические инструменты для лаборатории")
subtitle_font = QFont()
subtitle_font.setPointSize(12)
subtitle_label.setFont(subtitle_font)
subtitle_label.setAlignment(Qt.AlignCenter)
subtitle_label.setStyleSheet("color: #666;")
layout.addWidget(subtitle_label)
layout.addSpacing(20)
btn_medium = QPushButton("🧪 Калькулятор питательных сред")
btn_medium.setMinimumHeight(80)
btn_medium.clicked.connect(self.open_medium_calculator)
layout.addWidget(btn_medium)
desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\nразбавления реагентов и автоматическим расчётом растворителя")
desc1_label.setAlignment(Qt.AlignCenter)
desc1_label.setWordWrap(True)
desc1_label.setStyleSheet("color: #555; font-size: 11px;")
layout.addWidget(desc1_label)
layout.addSpacing(15)
btn_experiment = QPushButton("📊 Планирование эксперимента (DoE)")
btn_experiment.setMinimumHeight(80)
btn_experiment.clicked.connect(self.open_experiment_designer)
layout.addWidget(btn_experiment)
desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\nмногомерный анализ и визуализация")
desc2_label.setAlignment(Qt.AlignCenter)
desc2_label.setWordWrap(True)
desc2_label.setStyleSheet("color: #555; font-size: 11px;")
layout.addWidget(desc2_label)
layout.addSpacing(15)
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
layout.addWidget(line)
bottom_layout = QHBoxLayout()
version_label = QLabel("Версия 1.0.0 | © 2026 Цифровой помощник биохимика")
version_label.setStyleSheet("color: #999; font-size: 10px;")
bottom_layout.addWidget(version_label)
bottom_layout.addStretch()
btn_exit = QPushButton("Выход")
btn_exit.setMaximumWidth(150)
btn_exit.setStyleSheet("QPushButton { background-color: #f44336; padding: 8px; font-size: 14px; } QPushButton:hover { background-color: #da190b; }")
btn_exit.clicked.connect(self.close)
bottom_layout.addWidget(btn_exit)
layout.addLayout(bottom_layout)
def open_medium_calculator(self):
from .medium_view import MediumCalculatorWindow
from ..controllers.medium_controller import MediumController
self.medium_calculator = MediumCalculatorWindow()
self.medium_controller = MediumController(self.medium_calculator)
self.medium_calculator.show()
def open_experiment_designer(self):
from .experiment_view import ExperimentDesignWindow
from ..controllers.experiment_controller import ExperimentController
self.experiment_window = ExperimentDesignWindow()
self.experiment_controller = ExperimentController(self.experiment_window)
self.experiment_window.show()
EOF
# Файл: src/views/medium_view.py
cat > src/views/medium_view.py << 'EOF' cat > src/views/medium_view.py << 'EOF'
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget, QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget,
QMessageBox, QGroupBox, QFrame) QMessageBox, QGroupBox, QFrame, QToolTip)
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QFont, QColor from PyQt5.QtGui import QFont, QColor
class MediumCalculatorWindow(QMainWindow): class MediumCalculatorWindow(QMainWindow):
@@ -380,11 +34,16 @@ class MediumCalculatorWindow(QMainWindow):
QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 15px; border-radius: 4px; 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:hover { background-color: #1976D2; }
QPushButton#danger { background-color: #f44336; } QPushButton#danger { background-color: #f44336; }
QPushButton#danger:hover { background-color: #da190b; }
QPushButton#success { background-color: #4CAF50; } QPushButton#success { background-color: #4CAF50; }
QPushButton#success:hover { background-color: #45a049; }
QPushButton#doe { background-color: #9C27B0; }
QPushButton#doe:hover { background-color: #7B1FA2; }
QTableWidget { gridline-color: #ddd; background-color: white; } QTableWidget { gridline-color: #ddd; background-color: white; }
QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; }
""") """)
self._init_ui() self._init_ui()
self.doe_window = None
def _init_ui(self): def _init_ui(self):
central_widget = QWidget() central_widget = QWidget()
@@ -452,6 +111,14 @@ class MediumCalculatorWindow(QMainWindow):
self.calculate_btn.setObjectName("success") self.calculate_btn.setObjectName("success")
self.save_btn = QPushButton("💾 Сохранить") self.save_btn = QPushButton("💾 Сохранить")
self.load_btn = QPushButton("📂 Загрузить") self.load_btn = QPushButton("📂 Загрузить")
# Новая кнопка для передачи в DoE
self.to_doe_btn = QPushButton("🎯 В DoE")
self.to_doe_btn.setObjectName("doe")
self.to_doe_btn.setToolTip("Передать состав реагентов в планировщик эксперимента\n"
"Реагенты станут факторами, их концентрации — нулевыми уровнями")
btn_layout.addWidget(self.to_doe_btn)
btn_layout.addWidget(self.calculate_btn) btn_layout.addWidget(self.calculate_btn)
btn_layout.addWidget(self.save_btn) btn_layout.addWidget(self.save_btn)
btn_layout.addWidget(self.load_btn) btn_layout.addWidget(self.load_btn)
@@ -516,6 +183,34 @@ class MediumCalculatorWindow(QMainWindow):
if row > 0: if row > 0:
self.table.removeRow(row) self.table.removeRow(row)
def get_reagents_data(self):
"""Возвращает список реагентов для передачи в DoE"""
reagents = []
for row in range(1, self.table.rowCount()):
name_item = self.table.item(row, 0)
percent_item = self.table.item(row, 1)
unit_widget = self.table.cellWidget(row, 2)
coeff_item = self.table.item(row, 3)
if not all([name_item, percent_item]):
continue
try:
name = name_item.text()
percentage = float(percent_item.text())
unit = unit_widget.currentText() if unit_widget else "мг"
conversion_factor = float(coeff_item.text()) if coeff_item else 1.0
reagents.append({
'name': name,
'percentage': percentage,
'unit': unit,
'conversion_factor': conversion_factor
})
except ValueError:
continue
return reagents
def get_table_data(self): def get_table_data(self):
data = [] data = []
for row in range(1, self.table.rowCount()): for row in range(1, self.table.rowCount()):
@@ -569,9 +264,159 @@ class MediumCalculatorWindow(QMainWindow):
def show_error(self, message: str): def show_error(self, message: str):
QMessageBox.critical(self, "Ошибка", message) QMessageBox.critical(self, "Ошибка", message)
def show_info(self, message: str):
QMessageBox.information(self, "Информация", message)
EOF EOF
# Файл: src/views/experiment_view.py (полная версия) # Обновляем medium_controller.py - добавляем метод передачи в DoE
cat > src/controllers/medium_controller.py << 'EOF'
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
import json
from ..models.medium_model import MediumModel
from ..models.reagent import Reagent
from ..views.experiment_view import ExperimentDesignWindow
class MediumController:
def __init__(self, view):
self.model = MediumModel()
self.view = view
self.doe_window = None
self._connect_signals()
self._setup_initial_data()
def _connect_signals(self):
self.view.add_row_btn.clicked.connect(self.add_reagent_row)
self.view.remove_row_btn.clicked.connect(self.remove_reagent_row)
self.view.calculate_btn.clicked.connect(self._perform_calculation)
self.view.save_btn.clicked.connect(self.save_composition)
self.view.load_btn.clicked.connect(self.load_composition)
self.view.to_doe_btn.clicked.connect(self.send_to_doe)
self.view.solvent_input.textChanged.connect(self.view.update_solvent_name)
def _setup_initial_data(self):
self.view.add_initial_rows()
def add_reagent_row(self):
self.view.add_new_row()
def remove_reagent_row(self):
self.view.remove_selected_row()
def _perform_calculation(self):
try:
self._update_model_from_view()
results, solvent_amount, solvent_percentage = self.model.calculate_amounts()
self.view.update_results(results)
self.view.update_solvent_result(solvent_amount, self.model.amount_unit)
self.view.update_solvent_percent(solvent_percentage)
except ValueError as e:
self.view.show_error(f"Ошибка в данных: {str(e)}")
except Exception as e:
self.view.show_error(f"Неожиданная ошибка: {str(e)}")
def _update_model_from_view(self):
self.model.reagents.clear()
self.model.total_amount = self.view.amount_input.value()
self.model.amount_unit = self.view.amount_unit_combo.currentText()
self.model.solvent = self.view.solvent_input.text()
for row in range(1, self.view.table.rowCount()):
name_item = self.view.table.item(row, 0)
percentage_item = self.view.table.item(row, 1)
unit_widget = self.view.table.cellWidget(row, 2)
conversion_item = self.view.table.item(row, 3)
dilution_item = self.view.table.item(row, 4)
if not all([name_item, percentage_item, conversion_item]):
continue
try:
name = name_item.text()
percentage = float(percentage_item.text())
unit = unit_widget.currentText() if unit_widget else "мг"
conversion_factor = float(conversion_item.text())
dilution_factor = float(dilution_item.text()) if dilution_item else 1.0
reagent = Reagent(name, percentage, unit, conversion_factor)
reagent.dilution_factor = dilution_factor
self.model.reagents.append(reagent)
except ValueError as e:
raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}")
def send_to_doe(self):
"""Передаёт данные о реагентах в окно планирования эксперимента"""
reagents = self.view.get_reagents_data()
if len(reagents) == 0:
self.view.show_error("Нет реагентов для передачи в планировщик эксперимента!")
return
# Создаём или показываем существующее окно DoE
if self.doe_window is None:
self.doe_window = ExperimentDesignWindow()
# Передаём данные в окно DoE
self.doe_window.load_factors_from_reagents(reagents)
self.doe_window.show()
self.doe_window.raise_()
self.doe_window.activateWindow()
self.view.show_info(f"Передано {len(reagents)} реагентов в планировщик эксперимента\n"
f"Каждый реагент добавлен как фактор.\n"
f"Его концентрация (%) установлена как нулевой уровень.")
def save_composition(self):
filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)")
if filename:
if not filename.lower().endswith('.json'):
filename += '.json'
try:
self._update_model_from_view()
self.model.save_to_file(filename)
QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!")
except Exception as e:
self.view.show_error(f"Ошибка сохранения: {str(e)}")
def load_composition(self):
filename, _ = QFileDialog.getOpenFileName(self.view, "Загрузить состав среды", "", "JSON Files (*.json);;All Files (*)")
if filename:
try:
self.model.load_from_file(filename)
self._update_view_from_model()
QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен")
except FileNotFoundError:
QMessageBox.critical(self.view, "Ошибка", f"Файл не найден: {filename}")
except json.JSONDecodeError as e:
QMessageBox.critical(self.view, "Ошибка", f"Неверный формат JSON-файла: {str(e)}")
except Exception as e:
QMessageBox.critical(self.view, "Ошибка", f"Ошибка при загрузке состава: {str(e)}")
def _update_view_from_model(self):
while self.view.table.rowCount() > 1:
self.view.table.removeRow(1)
if self.view.table.rowCount() == 0:
self.view.add_solvent_row()
self.view.amount_input.setValue(self.model.total_amount)
index = self.view.amount_unit_combo.findText(self.model.amount_unit)
if index >= 0:
self.view.amount_unit_combo.setCurrentIndex(index)
self.view.solvent_input.setText(self.model.solvent)
self.view.update_solvent_name()
for reagent in self.model.reagents:
row = self.view.table.rowCount()
self.view.table.insertRow(row)
self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}"))
unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText(reagent.unit)
self.view.table.setCellWidget(row, 2, unit_combo)
self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}"))
self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}"))
self.view.table.setItem(row, 5, QTableWidgetItem(""))
self.view.clear_results()
EOF
# Обновляем experiment_view.py - добавляем метод загрузки факторов из реагентов
cat > src/views/experiment_view.py << 'EOF' cat > src/views/experiment_view.py << 'EOF'
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox, QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox,
@@ -596,6 +441,8 @@ class ExperimentDesignWindow(QMainWindow):
QPushButton:hover { background-color: #45a049; } QPushButton:hover { background-color: #45a049; }
QPushButton#danger { background-color: #f44336; } QPushButton#danger { background-color: #f44336; }
QPushButton#danger:hover { background-color: #da190b; } QPushButton#danger:hover { background-color: #da190b; }
QPushButton#doe { background-color: #9C27B0; }
QPushButton#doe:hover { background-color: #7B1FA2; }
QTableWidget { gridline-color: #ddd; } QTableWidget { gridline-color: #ddd; }
QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; }
""") """)
@@ -768,6 +615,66 @@ class ExperimentDesignWindow(QMainWindow):
self.generated_design = None self.generated_design = None
self.factors_data = None self.factors_data = None
def load_factors_from_reagents(self, reagents):
"""Загружает факторы из списка реагентов калькулятора"""
if not reagents:
return
# Очищаем таблицу факторов
self.factors_table.setRowCount(0)
# Добавляем каждый реагент как фактор
for reagent in reagents:
row = self.factors_table.rowCount()
self.factors_table.insertRow(row)
# Название фактора = название реагента
self.factors_table.setItem(row, 0, QTableWidgetItem(reagent['name']))
# Нулевой уровень = процентное содержание
percentage = reagent['percentage']
self.factors_table.setItem(row, 1, QTableWidgetItem(f"{percentage:.2f}"))
# Шаг = 10% от нулевого уровня (или 1, если нулевой уровень 0)
step = max(percentage * 0.1, 1.0) if percentage > 0 else 1.0
self.factors_table.setItem(row, 2, QTableWidgetItem(f"{step:.2f}"))
# Единица измерения
unit = reagent['unit']
# Преобразуем единицы измерения в понятные для факторов
if unit in ['нг', 'мкг', 'мг', 'г', 'кг']:
display_unit = unit
elif unit in ['нл', 'мкл', 'мл', 'л']:
display_unit = unit
else:
display_unit = "%"
self.factors_table.setItem(row, 5, QTableWidgetItem(display_unit))
# Верхний и нижний уровни (вычисляются автоматически)
high = percentage + step
low = percentage - step
high_item = QTableWidgetItem(f"{high:.2f}")
high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable)
high_item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(row, 3, high_item)
low_item = QTableWidgetItem(f"{low:.2f}")
low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable)
low_item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(row, 4, low_item)
# Показываем сообщение об успешной загрузке
QMessageBox.information(
self,
"Загрузка из калькулятора",
f"Загружено {len(reagents)} факторов из калькулятора питательных сред.\n\n"
f"Каждый реагент стал фактором.\n"
f"Нулевой уровень = концентрация реагента (%)\n"
f"Шаг = 10% от нулевого уровня\n\n"
f"При необходимости отредактируйте шаг и единицы измерения."
)
def on_factor_changed(self, item): def on_factor_changed(self, item):
row = item.row() row = item.row()
col = item.column() col = item.column()
@@ -1045,203 +952,10 @@ class ExperimentDesignWindow(QMainWindow):
QMessageBox.critical(self, "Ошибка", message) QMessageBox.critical(self, "Ошибка", message)
EOF EOF
# Файл: src/controllers/__init__.py
cat > src/controllers/__init__.py << 'EOF'
from .medium_controller import MediumController
from .experiment_controller import ExperimentController
__all__ = ['MediumController', 'ExperimentController']
EOF
# Файл: src/controllers/medium_controller.py
cat > src/controllers/medium_controller.py << 'EOF'
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
import json
from ..models.medium_model import MediumModel
from ..models.reagent import Reagent
class MediumController:
def __init__(self, view):
self.model = MediumModel()
self.view = view
self._connect_signals()
self._setup_initial_data()
def _connect_signals(self):
self.view.add_row_btn.clicked.connect(self.add_reagent_row)
self.view.remove_row_btn.clicked.connect(self.remove_reagent_row)
self.view.calculate_btn.clicked.connect(self._perform_calculation)
self.view.save_btn.clicked.connect(self.save_composition)
self.view.load_btn.clicked.connect(self.load_composition)
self.view.solvent_input.textChanged.connect(self.view.update_solvent_name)
def _setup_initial_data(self):
self.view.add_initial_rows()
def add_reagent_row(self):
self.view.add_new_row()
def remove_reagent_row(self):
self.view.remove_selected_row()
def _perform_calculation(self):
try:
self._update_model_from_view()
results, solvent_amount, solvent_percentage = self.model.calculate_amounts()
self.view.update_results(results)
self.view.update_solvent_result(solvent_amount, self.model.amount_unit)
self.view.update_solvent_percent(solvent_percentage)
except ValueError as e:
self.view.show_error(f"Ошибка в данных: {str(e)}")
except Exception as e:
self.view.show_error(f"Неожиданная ошибка: {str(e)}")
def _update_model_from_view(self):
self.model.reagents.clear()
self.model.total_amount = self.view.amount_input.value()
self.model.amount_unit = self.view.amount_unit_combo.currentText()
self.model.solvent = self.view.solvent_input.text()
for row in range(1, self.view.table.rowCount()):
name_item = self.view.table.item(row, 0)
percentage_item = self.view.table.item(row, 1)
unit_widget = self.view.table.cellWidget(row, 2)
conversion_item = self.view.table.item(row, 3)
dilution_item = self.view.table.item(row, 4)
if not all([name_item, percentage_item, conversion_item]):
continue
try:
name = name_item.text()
percentage = float(percentage_item.text())
unit = unit_widget.currentText() if unit_widget else "мг"
conversion_factor = float(conversion_item.text())
dilution_factor = float(dilution_item.text()) if dilution_item else 1.0
reagent = Reagent(name, percentage, unit, conversion_factor)
reagent.dilution_factor = dilution_factor
self.model.reagents.append(reagent)
except ValueError as e:
raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}")
def save_composition(self):
filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)")
if filename:
if not filename.lower().endswith('.json'):
filename += '.json'
try:
self._update_model_from_view()
self.model.save_to_file(filename)
QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!")
except Exception as e:
self.view.show_error(f"Ошибка сохранения: {str(e)}")
def load_composition(self):
filename, _ = QFileDialog.getOpenFileName(self.view, "Загрузить состав среды", "", "JSON Files (*.json);;All Files (*)")
if filename:
try:
self.model.load_from_file(filename)
self._update_view_from_model()
QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен")
except FileNotFoundError:
QMessageBox.critical(self.view, "Ошибка", f"Файл не найден: {filename}")
except json.JSONDecodeError as e:
QMessageBox.critical(self.view, "Ошибка", f"Неверный формат JSON-файла: {str(e)}")
except Exception as e:
QMessageBox.critical(self.view, "Ошибка", f"Ошибка при загрузке состава: {str(e)}")
def _update_view_from_model(self):
while self.view.table.rowCount() > 1:
self.view.table.removeRow(1)
if self.view.table.rowCount() == 0:
self.view.add_solvent_row()
self.view.amount_input.setValue(self.model.total_amount)
index = self.view.amount_unit_combo.findText(self.model.amount_unit)
if index >= 0:
self.view.amount_unit_combo.setCurrentIndex(index)
self.view.solvent_input.setText(self.model.solvent)
self.view.update_solvent_name()
for reagent in self.model.reagents:
row = self.view.table.rowCount()
self.view.table.insertRow(row)
self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}"))
unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText(reagent.unit)
self.view.table.setCellWidget(row, 2, unit_combo)
self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}"))
self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}"))
self.view.table.setItem(row, 5, QTableWidgetItem(""))
self.view.clear_results()
EOF
# Файл: src/controllers/experiment_controller.py
cat > src/controllers/experiment_controller.py << 'EOF'
from ..models.experiment_model import ExperimentModel
class ExperimentController:
def __init__(self, view):
self.model = ExperimentModel()
self.view = view
def update_model_from_view(self):
factors = self.view.get_factors_data()
self.model.set_factors(factors)
responses = []
for row in range(self.view.responses_table.rowCount()):
name_item = self.view.responses_table.item(row, 0)
unit_item = self.view.responses_table.item(row, 1)
if name_item:
responses.append({
'name': name_item.text(),
'unit': unit_item.text() if unit_item else ""
})
self.model.set_responses(responses)
self.model.set_center_points(self.view.center_points_spin.value())
self.model.set_randomize(self.view.randomize_check.isChecked())
EOF
# Файл: main.py
cat > main.py << 'EOF'
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from PyQt5.QtWidgets import QApplication
from src.views import MainWindow
def main():
app = QApplication(sys.argv)
app.setStyle('Fusion')
assistant = MainWindow()
assistant.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
EOF
# Файл: run.sh
cat > run.sh << 'EOF'
#!/bin/bash
python3 main.py
EOF
chmod +x run.sh
# Файл: requirements.txt
cat > requirements.txt << 'EOF'
PyQt5>=5.15.0
numpy>=1.19.0
EOF
echo -e "${GREEN}========================================${NC}" echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}Реорганизация завершена!${NC}" echo -e "${GREEN}Обновление завершено!${NC}"
echo -e "${GREEN}========================================${NC}" echo -e "${GREEN}========================================${NC}"
echo -e "${YELLOW}Для запуска обновлённой версии:${NC}" echo -e "${YELLOW}Добавлена кнопка «🎯 В DoE» в калькулятор питательных сред${NC}"
echo -e "${GREEN}python3 main.py${NC}" echo -e "${YELLOW}При нажатии реагенты передаются в планировщик как факторы${NC}"
echo "" echo ""
echo -e "${GREEN}или${NC}" echo -e "${GREEN}Запуск: python3 main.py${NC}"
echo -e "${GREEN}./run.sh${NC}"
echo ""
echo -e "${YELLOW}Бэкап старых файлов:${NC} $BACKUP_DIR"
+25
View File
@@ -4,11 +4,13 @@ from PyQt5.QtGui import QColor
import json import json
from ..models.medium_model import MediumModel from ..models.medium_model import MediumModel
from ..models.reagent import Reagent from ..models.reagent import Reagent
from ..views.experiment_view import ExperimentDesignWindow
class MediumController: class MediumController:
def __init__(self, view): def __init__(self, view):
self.model = MediumModel() self.model = MediumModel()
self.view = view self.view = view
self.doe_window = None
self._connect_signals() self._connect_signals()
self._setup_initial_data() self._setup_initial_data()
@@ -18,6 +20,7 @@ class MediumController:
self.view.calculate_btn.clicked.connect(self._perform_calculation) self.view.calculate_btn.clicked.connect(self._perform_calculation)
self.view.save_btn.clicked.connect(self.save_composition) self.view.save_btn.clicked.connect(self.save_composition)
self.view.load_btn.clicked.connect(self.load_composition) self.view.load_btn.clicked.connect(self.load_composition)
self.view.to_doe_btn.clicked.connect(self.send_to_doe)
self.view.solvent_input.textChanged.connect(self.view.update_solvent_name) self.view.solvent_input.textChanged.connect(self.view.update_solvent_name)
def _setup_initial_data(self): def _setup_initial_data(self):
@@ -66,6 +69,28 @@ class MediumController:
except ValueError as e: except ValueError as e:
raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}") raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}")
def send_to_doe(self):
"""Передаёт данные о реагентах в окно планирования эксперимента"""
reagents = self.view.get_reagents_data()
if len(reagents) == 0:
self.view.show_error("Нет реагентов для передачи в планировщик эксперимента!")
return
# Создаём или показываем существующее окно DoE
if self.doe_window is None:
self.doe_window = ExperimentDesignWindow()
# Передаём данные в окно DoE
self.doe_window.load_factors_from_reagents(reagents)
self.doe_window.show()
self.doe_window.raise_()
self.doe_window.activateWindow()
self.view.show_info(f"Передано {len(reagents)} реагентов в планировщик эксперимента\n"
f"Каждый реагент добавлен как фактор.\n"
f"Его концентрация (%) установлена как нулевой уровень.")
def save_composition(self): def save_composition(self):
filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)") filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)")
if filename: if filename:
+62
View File
@@ -21,6 +21,8 @@ class ExperimentDesignWindow(QMainWindow):
QPushButton:hover { background-color: #45a049; } QPushButton:hover { background-color: #45a049; }
QPushButton#danger { background-color: #f44336; } QPushButton#danger { background-color: #f44336; }
QPushButton#danger:hover { background-color: #da190b; } QPushButton#danger:hover { background-color: #da190b; }
QPushButton#doe { background-color: #9C27B0; }
QPushButton#doe:hover { background-color: #7B1FA2; }
QTableWidget { gridline-color: #ddd; } QTableWidget { gridline-color: #ddd; }
QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; }
""") """)
@@ -193,6 +195,66 @@ class ExperimentDesignWindow(QMainWindow):
self.generated_design = None self.generated_design = None
self.factors_data = None self.factors_data = None
def load_factors_from_reagents(self, reagents):
"""Загружает факторы из списка реагентов калькулятора"""
if not reagents:
return
# Очищаем таблицу факторов
self.factors_table.setRowCount(0)
# Добавляем каждый реагент как фактор
for reagent in reagents:
row = self.factors_table.rowCount()
self.factors_table.insertRow(row)
# Название фактора = название реагента
self.factors_table.setItem(row, 0, QTableWidgetItem(reagent['name']))
# Нулевой уровень = процентное содержание
percentage = reagent['percentage']
self.factors_table.setItem(row, 1, QTableWidgetItem(f"{percentage:.2f}"))
# Шаг = 10% от нулевого уровня (или 1, если нулевой уровень 0)
step = max(percentage * 0.1, 1.0) if percentage > 0 else 1.0
self.factors_table.setItem(row, 2, QTableWidgetItem(f"{step:.2f}"))
# Единица измерения
unit = reagent['unit']
# Преобразуем единицы измерения в понятные для факторов
if unit in ['нг', 'мкг', 'мг', 'г', 'кг']:
display_unit = unit
elif unit in ['нл', 'мкл', 'мл', 'л']:
display_unit = unit
else:
display_unit = "%"
self.factors_table.setItem(row, 5, QTableWidgetItem(display_unit))
# Верхний и нижний уровни (вычисляются автоматически)
high = percentage + step
low = percentage - step
high_item = QTableWidgetItem(f"{high:.2f}")
high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable)
high_item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(row, 3, high_item)
low_item = QTableWidgetItem(f"{low:.2f}")
low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable)
low_item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(row, 4, low_item)
# Показываем сообщение об успешной загрузке
QMessageBox.information(
self,
"Загрузка из калькулятора",
f"Загружено {len(reagents)} факторов из калькулятора питательных сред.\n\n"
f"Каждый реагент стал фактором.\n"
f"Нулевой уровень = концентрация реагента (%)\n"
f"Шаг = 10% от нулевого уровня\n\n"
f"При необходимости отредактируйте шаг и единицы измерения."
)
def on_factor_changed(self, item): def on_factor_changed(self, item):
row = item.row() row = item.row()
col = item.column() col = item.column()
+46 -2
View File
@@ -1,7 +1,7 @@
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget, QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget,
QMessageBox, QGroupBox, QFrame) QMessageBox, QGroupBox, QFrame, QToolTip)
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QFont, QColor from PyQt5.QtGui import QFont, QColor
class MediumCalculatorWindow(QMainWindow): class MediumCalculatorWindow(QMainWindow):
@@ -16,11 +16,16 @@ class MediumCalculatorWindow(QMainWindow):
QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 15px; border-radius: 4px; 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:hover { background-color: #1976D2; }
QPushButton#danger { background-color: #f44336; } QPushButton#danger { background-color: #f44336; }
QPushButton#danger:hover { background-color: #da190b; }
QPushButton#success { background-color: #4CAF50; } QPushButton#success { background-color: #4CAF50; }
QPushButton#success:hover { background-color: #45a049; }
QPushButton#doe { background-color: #9C27B0; }
QPushButton#doe:hover { background-color: #7B1FA2; }
QTableWidget { gridline-color: #ddd; background-color: white; } QTableWidget { gridline-color: #ddd; background-color: white; }
QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; }
""") """)
self._init_ui() self._init_ui()
self.doe_window = None
def _init_ui(self): def _init_ui(self):
central_widget = QWidget() central_widget = QWidget()
@@ -88,6 +93,14 @@ class MediumCalculatorWindow(QMainWindow):
self.calculate_btn.setObjectName("success") self.calculate_btn.setObjectName("success")
self.save_btn = QPushButton("💾 Сохранить") self.save_btn = QPushButton("💾 Сохранить")
self.load_btn = QPushButton("📂 Загрузить") self.load_btn = QPushButton("📂 Загрузить")
# Новая кнопка для передачи в DoE
self.to_doe_btn = QPushButton("🎯 В DoE")
self.to_doe_btn.setObjectName("doe")
self.to_doe_btn.setToolTip("Передать состав реагентов в планировщик эксперимента\n"
"Реагенты станут факторами, их концентрации — нулевыми уровнями")
btn_layout.addWidget(self.to_doe_btn)
btn_layout.addWidget(self.calculate_btn) btn_layout.addWidget(self.calculate_btn)
btn_layout.addWidget(self.save_btn) btn_layout.addWidget(self.save_btn)
btn_layout.addWidget(self.load_btn) btn_layout.addWidget(self.load_btn)
@@ -152,6 +165,34 @@ class MediumCalculatorWindow(QMainWindow):
if row > 0: if row > 0:
self.table.removeRow(row) self.table.removeRow(row)
def get_reagents_data(self):
"""Возвращает список реагентов для передачи в DoE"""
reagents = []
for row in range(1, self.table.rowCount()):
name_item = self.table.item(row, 0)
percent_item = self.table.item(row, 1)
unit_widget = self.table.cellWidget(row, 2)
coeff_item = self.table.item(row, 3)
if not all([name_item, percent_item]):
continue
try:
name = name_item.text()
percentage = float(percent_item.text())
unit = unit_widget.currentText() if unit_widget else "мг"
conversion_factor = float(coeff_item.text()) if coeff_item else 1.0
reagents.append({
'name': name,
'percentage': percentage,
'unit': unit,
'conversion_factor': conversion_factor
})
except ValueError:
continue
return reagents
def get_table_data(self): def get_table_data(self):
data = [] data = []
for row in range(1, self.table.rowCount()): for row in range(1, self.table.rowCount()):
@@ -205,3 +246,6 @@ class MediumCalculatorWindow(QMainWindow):
def show_error(self, message: str): def show_error(self, message: str):
QMessageBox.critical(self, "Ошибка", message) QMessageBox.critical(self, "Ошибка", message)
def show_info(self, message: str):
QMessageBox.information(self, "Информация", message)