diff --git a/backup_20260507_145331/src/__init__.py b/backup_20260507_145331/src/__init__.py new file mode 100644 index 0000000..e878fc2 --- /dev/null +++ b/backup_20260507_145331/src/__init__.py @@ -0,0 +1,3 @@ +"""Цифровой помощник биохимика - основная библиотека""" + +__version__ = "1.0.0" diff --git a/backup_20260507_145331/src/controllers/__init__.py b/backup_20260507_145331/src/controllers/__init__.py new file mode 100644 index 0000000..37f75f8 --- /dev/null +++ b/backup_20260507_145331/src/controllers/__init__.py @@ -0,0 +1,4 @@ +from .medium_controller import MediumController +from .experiment_controller import ExperimentController + +__all__ = ['MediumController', 'ExperimentController'] diff --git a/backup_20260507_145331/src/controllers/experiment_controller.py b/backup_20260507_145331/src/controllers/experiment_controller.py new file mode 100644 index 0000000..023e7fa --- /dev/null +++ b/backup_20260507_145331/src/controllers/experiment_controller.py @@ -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()) diff --git a/backup_20260507_145331/src/controllers/medium_controller.py b/backup_20260507_145331/src/controllers/medium_controller.py new file mode 100644 index 0000000..6b4ff94 --- /dev/null +++ b/backup_20260507_145331/src/controllers/medium_controller.py @@ -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() diff --git a/backup_20260507_145331/src/models/__init__.py b/backup_20260507_145331/src/models/__init__.py new file mode 100644 index 0000000..3bcdcad --- /dev/null +++ b/backup_20260507_145331/src/models/__init__.py @@ -0,0 +1,5 @@ +from .reagent import Reagent +from .medium_model import MediumModel +from .experiment_model import ExperimentModel + +__all__ = ['Reagent', 'MediumModel', 'ExperimentModel'] diff --git a/backup_20260507_145331/src/models/experiment_model.py b/backup_20260507_145331/src/models/experiment_model.py new file mode 100644 index 0000000..fe5d322 --- /dev/null +++ b/backup_20260507_145331/src/models/experiment_model.py @@ -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 diff --git a/backup_20260507_145331/src/models/medium_model.py b/backup_20260507_145331/src/models/medium_model.py new file mode 100644 index 0000000..48e91f8 --- /dev/null +++ b/backup_20260507_145331/src/models/medium_model.py @@ -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 diff --git a/backup_20260507_145331/src/models/reagent.py b/backup_20260507_145331/src/models/reagent.py new file mode 100644 index 0000000..160892b --- /dev/null +++ b/backup_20260507_145331/src/models/reagent.py @@ -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})" diff --git a/backup_20260507_145331/src/utils/__init__.py b/backup_20260507_145331/src/utils/__init__.py new file mode 100644 index 0000000..a42179a --- /dev/null +++ b/backup_20260507_145331/src/utils/__init__.py @@ -0,0 +1,3 @@ +"""Утилиты и вспомогательные функции""" + +__all__ = [] diff --git a/backup_20260507_145331/src/views/__init__.py b/backup_20260507_145331/src/views/__init__.py new file mode 100644 index 0000000..e6fcdd9 --- /dev/null +++ b/backup_20260507_145331/src/views/__init__.py @@ -0,0 +1,5 @@ +from .main_window import MainWindow +from .medium_view import MediumCalculatorWindow +from .experiment_view import ExperimentDesignWindow + +__all__ = ['MainWindow', 'MediumCalculatorWindow', 'ExperimentDesignWindow'] diff --git a/backup_20260507_145331/src/views/experiment_view.py b/backup_20260507_145331/src/views/experiment_view.py new file mode 100644 index 0000000..8d6b2a6 --- /dev/null +++ b/backup_20260507_145331/src/views/experiment_view.py @@ -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) diff --git a/backup_20260507_145331/src/views/main_window.py b/backup_20260507_145331/src/views/main_window.py new file mode 100644 index 0000000..85ab15f --- /dev/null +++ b/backup_20260507_145331/src/views/main_window.py @@ -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() diff --git a/backup_20260507_145331/src/views/medium_view.py b/backup_20260507_145331/src/views/medium_view.py new file mode 100644 index 0000000..f1f1c2f --- /dev/null +++ b/backup_20260507_145331/src/views/medium_view.py @@ -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) diff --git a/patch.sh b/patch.sh index 44ae0fa..b13148c 100644 --- a/patch.sh +++ b/patch.sh @@ -6,366 +6,20 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' -echo -e "${GREEN}=== Реорганизация программы Цифровой помощник биохимика ===${NC}" +echo -e "${GREEN}=== Обновление программы: добавление передачи данных в DoE ===${NC}" -# Создаём бэкап текущей версии +# Создаём бэкап BACKUP_DIR="backup_$(date +%Y%m%d_%H%M%S)" -echo -e "${YELLOW}Создание бэкапа в $BACKUP_DIR...${NC}" mkdir -p "$BACKUP_DIR" -cp *.py "$BACKUP_DIR/" 2>/dev/null -echo -e "${GREEN}✓ Бэкап создан${NC}" +cp -r src "$BACKUP_DIR/" 2>/dev/null +echo -e "${GREEN}✓ Бэкап создан в $BACKUP_DIR${NC}" -# Создаём структуру папок -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 +# Обновляем medium_view.py - добавляем кнопку cat > src/views/medium_view.py << 'EOF' from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget, - QMessageBox, QGroupBox, QFrame) -from PyQt5.QtCore import Qt + QMessageBox, QGroupBox, QFrame, QToolTip) +from PyQt5.QtCore import Qt, QPoint from PyQt5.QtGui import QFont, QColor 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:hover { background-color: #1976D2; } QPushButton#danger { background-color: #f44336; } + QPushButton#danger:hover { background-color: #da190b; } 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; } QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } """) self._init_ui() + self.doe_window = None def _init_ui(self): central_widget = QWidget() @@ -452,6 +111,14 @@ class MediumCalculatorWindow(QMainWindow): self.calculate_btn.setObjectName("success") self.save_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.save_btn) btn_layout.addWidget(self.load_btn) @@ -516,6 +183,34 @@ class MediumCalculatorWindow(QMainWindow): if row > 0: 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): data = [] for row in range(1, self.table.rowCount()): @@ -569,9 +264,159 @@ class MediumCalculatorWindow(QMainWindow): def show_error(self, message: str): QMessageBox.critical(self, "Ошибка", message) + + def show_info(self, message: str): + QMessageBox.information(self, "Информация", message) 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' from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox, @@ -596,6 +441,8 @@ class ExperimentDesignWindow(QMainWindow): QPushButton:hover { background-color: #45a049; } QPushButton#danger { background-color: #f44336; } QPushButton#danger:hover { background-color: #da190b; } + QPushButton#doe { background-color: #9C27B0; } + QPushButton#doe:hover { background-color: #7B1FA2; } QTableWidget { gridline-color: #ddd; } QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } """) @@ -768,6 +615,66 @@ class ExperimentDesignWindow(QMainWindow): self.generated_design = 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): row = item.row() col = item.column() @@ -1045,203 +952,10 @@ class ExperimentDesignWindow(QMainWindow): QMessageBox.critical(self, "Ошибка", message) 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 "${YELLOW}Для запуска обновлённой версии:${NC}" -echo -e "${GREEN}python3 main.py${NC}" +echo -e "${YELLOW}Добавлена кнопка «🎯 В DoE» в калькулятор питательных сред${NC}" +echo -e "${YELLOW}При нажатии реагенты передаются в планировщик как факторы${NC}" echo "" -echo -e "${GREEN}или${NC}" -echo -e "${GREEN}./run.sh${NC}" -echo "" -echo -e "${YELLOW}Бэкап старых файлов:${NC} $BACKUP_DIR" +echo -e "${GREEN}Запуск: python3 main.py${NC}" diff --git a/src/controllers/medium_controller.py b/src/controllers/medium_controller.py index 6b4ff94..5888bb5 100644 --- a/src/controllers/medium_controller.py +++ b/src/controllers/medium_controller.py @@ -4,11 +4,13 @@ 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() @@ -18,6 +20,7 @@ class MediumController: 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): @@ -66,6 +69,28 @@ class MediumController: 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: diff --git a/src/views/experiment_view.py b/src/views/experiment_view.py index 8d6b2a6..cc533a3 100644 --- a/src/views/experiment_view.py +++ b/src/views/experiment_view.py @@ -21,6 +21,8 @@ class ExperimentDesignWindow(QMainWindow): QPushButton:hover { background-color: #45a049; } QPushButton#danger { background-color: #f44336; } QPushButton#danger:hover { background-color: #da190b; } + QPushButton#doe { background-color: #9C27B0; } + QPushButton#doe:hover { background-color: #7B1FA2; } QTableWidget { gridline-color: #ddd; } QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } """) @@ -193,6 +195,66 @@ class ExperimentDesignWindow(QMainWindow): self.generated_design = 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): row = item.row() col = item.column() diff --git a/src/views/medium_view.py b/src/views/medium_view.py index f1f1c2f..e84a9cd 100644 --- a/src/views/medium_view.py +++ b/src/views/medium_view.py @@ -1,7 +1,7 @@ from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget, - QMessageBox, QGroupBox, QFrame) -from PyQt5.QtCore import Qt + QMessageBox, QGroupBox, QFrame, QToolTip) +from PyQt5.QtCore import Qt, QPoint from PyQt5.QtGui import QFont, QColor 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:hover { background-color: #1976D2; } QPushButton#danger { background-color: #f44336; } + QPushButton#danger:hover { background-color: #da190b; } 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; } QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } """) self._init_ui() + self.doe_window = None def _init_ui(self): central_widget = QWidget() @@ -88,6 +93,14 @@ class MediumCalculatorWindow(QMainWindow): self.calculate_btn.setObjectName("success") self.save_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.save_btn) btn_layout.addWidget(self.load_btn) @@ -152,6 +165,34 @@ class MediumCalculatorWindow(QMainWindow): if row > 0: 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): data = [] for row in range(1, self.table.rowCount()): @@ -205,3 +246,6 @@ class MediumCalculatorWindow(QMainWindow): def show_error(self, message: str): QMessageBox.critical(self, "Ошибка", message) + + def show_info(self, message: str): + QMessageBox.information(self, "Информация", message)