#!/bin/bash # Цветной вывод RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' echo -e "${GREEN}=== Обновление программы: добавление передачи данных в DoE ===${NC}" # Создаём бэкап BACKUP_DIR="backup_$(date +%Y%m%d_%H%M%S)" mkdir -p "$BACKUP_DIR" cp -r src "$BACKUP_DIR/" 2>/dev/null echo -e "${GREEN}✓ Бэкап создан в $BACKUP_DIR${NC}" # Обновляем 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, QToolTip) from PyQt5.QtCore import Qt, QPoint 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#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() 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("📂 Загрузить") # Новая кнопка для передачи в 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) 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_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()): 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) def show_info(self, message: str): QMessageBox.information(self, "Информация", message) EOF # Обновляем 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, 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; } 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; } """) 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 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() 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) EOF echo -e "${GREEN}========================================${NC}" echo -e "${GREEN}Обновление завершено!${NC}" echo -e "${GREEN}========================================${NC}" echo -e "${YELLOW}Добавлена кнопка «🎯 В DoE» в калькулятор питательных сред${NC}" echo -e "${YELLOW}При нажатии реагенты передаются в планировщик как факторы${NC}" echo "" echo -e "${GREEN}Запуск: python3 main.py${NC}"