#!/usr/bin/env python3 """ Единый графический интерфейс для калькулятора сред и DoE """ from theme import Colors, Fonts, Spacing, ButtonStyles, get_full_stylesheet, apply_theme import sys from typing import List, Dict, Optional from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QGroupBox, QLabel, QPushButton, QTableWidget, QTableWidgetItem, QDoubleSpinBox, QComboBox, QLineEdit, QMessageBox, QSpinBox, QCheckBox, QTextEdit, QFileDialog, QScrollArea, QHeaderView, QToolBar ) from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont, QColor # Импорты расчётов from calculations.medium import ( calculate_medium_composition, convert_units, VOLUME_UNITS, MASS_UNITS ) from calculations.doe import ( generate_factorial_design, analyze_experiment, calculate_factor_levels, FACTOR_TYPES ) # Импорт моделей для JSON try: from calculations.models.project_data import ( ProjectData, ReagentData, FactorData, ResponseData, ExperimentResultsData, create_new_project ) JSON_SUPPORT = True except ImportError: JSON_SUPPORT = False print("Предупреждение: Модуль project_data не найден, JSON поддержка отключена") class MainWindow(QMainWindow): """Главное окно с калькулятором сред и планировщиком эксперимента""" def __init__(self): super().__init__() self.setWindowTitle("Биохимический помощник") self.setStyleSheet(get_full_stylesheet()) self.setGeometry(100, 100, 1300, 800) self.setStyleSheet(self._get_stylesheet()) self.generated_design = None self.last_medium_result = None self.tab_widget = None self._setup_ui() self._add_file_toolbar() def _get_stylesheet(self): return get_full_stylesheet() def _add_file_toolbar(self): """Добавляет панель инструментов с кнопками сохранения/загрузки""" toolbar = QToolBar("Файл") toolbar.setMovable(False) self.addToolBar(toolbar) if JSON_SUPPORT: save_action = toolbar.addAction("💾 Сохранить проект") save_action.triggered.connect(self.save_project_to_json) load_action = toolbar.addAction("📂 Загрузить проект") load_action.triggered.connect(self.load_project_from_json) toolbar.addSeparator() def _setup_ui(self): central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) # Заголовок title = QLabel("Цифровой помощник биохимика") title_font = QFont() title_font.setPointSize(18) title_font.setBold(True) title.setFont(title_font) title.setAlignment(Qt.AlignCenter) title.setStyleSheet("color: #2c3e50; padding: 10px;") layout.addWidget(title) # Вкладки self.tab_widget = QTabWidget() self.tab_widget.addTab(self._create_medium_tab(), "🧪 Калькулятор сред") self.tab_widget.addTab(self._create_factors_tab(), "📊 Факторы эксперимента") self.tab_widget.addTab(self._create_design_tab(), "📋 Матрица планирования") self.tab_widget.addTab(self._create_analysis_tab(), "📈 Анализ результатов") layout.addWidget(self.tab_widget) # Кнопка закрытия close_btn = QPushButton("Закрыть") close_btn.setObjectName("danger") close_btn.clicked.connect(self.close) close_btn.setMaximumWidth(150) btn_layout = QHBoxLayout() btn_layout.addStretch() btn_layout.addWidget(close_btn) layout.addLayout(btn_layout) # ========== ВКЛАДКА 1: КАЛЬКУЛЯТОР СРЕД ========== def _create_medium_tab(self): tab = QWidget() layout = QVBoxLayout(tab) # Параметры params_box = QGroupBox("Параметры среды") params_layout = QHBoxLayout() self.total_volume_spin = QDoubleSpinBox() self.total_volume_spin.setRange(0.001, 1000000) self.total_volume_spin.setValue(100) self.total_volume_spin.setSuffix(" ") self.volume_unit_combo = QComboBox() self.volume_unit_combo.addItems(["мл", "л", "мкл", "нл"]) self.volume_unit_combo.setCurrentText("мл") params_layout.addWidget(QLabel("Общий объём:")) params_layout.addWidget(self.total_volume_spin) params_layout.addWidget(self.volume_unit_combo) params_layout.addSpacing(30) params_layout.addWidget(QLabel("Растворитель:")) self.solvent_input = QLineEdit("Вода") params_layout.addWidget(self.solvent_input) params_layout.addStretch() params_box.setLayout(params_layout) layout.addWidget(params_box) # Таблица реагентов reagents_box = QGroupBox("Состав среды") reagents_layout = QVBoxLayout() self.reagents_table = QTableWidget() self.reagents_table.setColumnCount(5) self.reagents_table.setHorizontalHeaderLabels( ["Реагент", "%", "Единица", "Разбавление", "Количество"] ) self.reagents_table.setAlternatingRowColors(True) self.reagents_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) reagents_layout.addWidget(self.reagents_table) # Кнопки управления реагентами btn_layout = QHBoxLayout() add_btn = QPushButton("➕ Добавить реагент") add_btn.clicked.connect(self._add_reagent_row) remove_btn = QPushButton("➖ Удалить реагент") remove_btn.setObjectName("danger") remove_btn.clicked.connect(self._remove_reagent_row) calculate_btn = QPushButton("🧮 Рассчитать") calculate_btn.setObjectName("success") calculate_btn.clicked.connect(self._calculate_medium) btn_layout.addWidget(add_btn) btn_layout.addWidget(remove_btn) btn_layout.addStretch() btn_layout.addWidget(calculate_btn) reagents_layout.addLayout(btn_layout) reagents_box.setLayout(reagents_layout) layout.addWidget(reagents_box) # Информационная панель self.info_label = QLabel("ℹ️ Нажмите «Рассчитать» для получения количеств реагентов") self.info_label.setStyleSheet("background-color: #ecf0f1; padding: 8px; border-radius: 5px;") layout.addWidget(self.info_label) # Добавляем начальные строки self._add_reagent_row() return tab def _add_reagent_row(self, name = "", percentage = "0", unit = "мл", dilution = "1", amount = ""): row = self.reagents_table.rowCount() self.reagents_table.insertRow(row) if name == "": self.reagents_table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row+1}")) else: self.reagents_table.setItem(row, 0, QTableWidgetItem(name)) self.reagents_table.setItem(row, 1, QTableWidgetItem(percentage)) unit_combo = QComboBox() unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л"]) unit_combo.setCurrentText(unit) self.reagents_table.setCellWidget(row, 2, unit_combo) self.reagents_table.setItem(row, 3, QTableWidgetItem(dilution)) self.reagents_table.setItem(row, 4, QTableWidgetItem(amount)) def _remove_reagent_row(self): for row in sorted(set(i.row() for i in self.reagents_table.selectedItems()), reverse=True): if row >= 0: self.reagents_table.removeRow(row) def _get_reagents_from_table(self) -> List[Dict]: """Собирает данные реагентов из таблицы""" reagents = [] for row in range(self.reagents_table.rowCount()): name_item = self.reagents_table.item(row, 0) percent_item = self.reagents_table.item(row, 1) unit_widget = self.reagents_table.cellWidget(row, 2) dilution_item = self.reagents_table.item(row, 3) if not name_item or not percent_item: continue try: percent_text = percent_item.text().strip() if not percent_text: continue reagent = { 'name': name_item.text(), 'percentage': float(percent_text), 'unit': unit_widget.currentText() if unit_widget else "мг", 'dilution_factor': float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0 } reagents.append(reagent) except ValueError as e: print(f"Ошибка в строке {row}: {e}") continue return reagents def _calculate_medium(self): """Выполняет расчёт питательной среды""" try: reagents = self._get_reagents_from_table() if not reagents: QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один реагент") return result = calculate_medium_composition( total_volume=self.total_volume_spin.value(), volume_unit=self.volume_unit_combo.currentText(), reagents=reagents, solvent_name=self.solvent_input.text() ) # Отображаем результаты в таблице for row, reagent in enumerate(result['reagents']): if row < self.reagents_table.rowCount(): amount_text = self._format_number(reagent['calculated_amount']) self.reagents_table.setItem(row, 4, QTableWidgetItem(amount_text)) # Подсветка item = self.reagents_table.item(row, 4) if item: item.setBackground(QColor(220, 255, 220)) # Информация о растворителе solvent_text = ( f"✅ Растворитель: {self._format_number(result['solvent_volume'])} {result['total_unit']} " f"({result['solvent_percentage']:.1f}%)" ) self.info_label.setText(solvent_text) self.info_label.setStyleSheet("background-color: #d5f5e3; padding: 8px; border-radius: 5px;") # Сохраняем результаты для передачи в DoE self.last_medium_result = result # QMessageBox.information(self, "Успех", "Расчёт выполнен успешно!") except Exception as e: QMessageBox.critical(self, "Ошибка", str(e)) # ========== ВКЛАДКА 2: ФАКТОРЫ ЭКСПЕРИМЕНТА ========== def _create_factors_tab(self): tab = QWidget() layout = QVBoxLayout(tab) # Кнопка импорта из калькулятора import_box = QGroupBox("Импорт данных") import_layout = QHBoxLayout() self.import_btn = QPushButton("📥 Импортировать реагенты из калькулятора") self.import_btn.setObjectName("success") self.import_btn.clicked.connect(self._import_reagents_to_factors) import_layout.addWidget(self.import_btn) import_layout.addStretch() import_box.setLayout(import_layout) layout.addWidget(import_box) # Таблица факторов factors_box = QGroupBox("Факторы эксперимента") factors_layout = QVBoxLayout() self.factors_table = QTableWidget() self.factors_table.setColumnCount(9) self.factors_table.setHorizontalHeaderLabels([ "Фактор", "%", "Разбавление", "Нулевой уровень", "Шаг", "Тип шага", "Верхний (+1)", "Нижний (-1)", "Ед. изм." ]) #self.factors_table.setAlternatingRowColors(True) factors_layout.addWidget(self.factors_table) btn_layout = QHBoxLayout() add_factor_btn = QPushButton("➕ Добавить фактор") add_factor_btn.clicked.connect(self._add_factor_row) remove_factor_btn = QPushButton("➖ Удалить фактор") remove_factor_btn.setObjectName("danger") remove_factor_btn.clicked.connect(self._remove_factor_row) btn_layout.addWidget(add_factor_btn) btn_layout.addWidget(remove_factor_btn) btn_layout.addStretch() factors_layout.addLayout(btn_layout) factors_box.setLayout(factors_layout) layout.addWidget(factors_box) # Настройки эксперимента settings_box = QGroupBox("Настройки эксперимента") settings_layout = QHBoxLayout() settings_layout.addWidget(QLabel("Центральных точек:")) self.center_points_spin = QSpinBox() self.center_points_spin.setRange(0, 10) self.center_points_spin.setValue(3) settings_layout.addWidget(self.center_points_spin) settings_layout.addSpacing(20) self.randomize_check = QCheckBox("Рэндомизировать порядок") self.randomize_check.setChecked(True) settings_layout.addWidget(self.randomize_check) settings_layout.addStretch() settings_box.setLayout(settings_layout) layout.addWidget(settings_box) # Кнопка генерации generate_btn = QPushButton("🎲 Сгенерировать план эксперимента") generate_btn.clicked.connect(self._generate_design) layout.addWidget(generate_btn) # Добавляем начальный фактор self._add_factor_row() # ПОДКЛЮЧАЕМ СИГНАЛ ИЗМЕНЕНИЯ ЯЧЕЕК self.factors_table.cellChanged.connect(self._on_factor_changed) return tab def _add_factor_row(self, name = "", percentage = "0", dilution = "0", center = "0", step ="0", step_type = "%", high = "", low = "", unit= "г"): row = self.factors_table.rowCount() self.factors_table.insertRow(row) if name == "": self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}")) else: self.factors_table.setItem(row, 0, QTableWidgetItem(name)) self.factors_table.setItem(row, 1, QTableWidgetItem(percentage)) self.factors_table.setItem(row, 2, QTableWidgetItem(dilution)) self.factors_table.setItem(row, 3, QTableWidgetItem(center)) self.factors_table.setItem(row, 4, QTableWidgetItem(step)) step_type_combo = QComboBox() step_type_combo.addItems(["%", "ед."]) step_type_combo.setCurrentText(step_type) step_type_combo.currentTextChanged.connect(lambda: self._on_factor_changed(row, 5)) self.factors_table.setCellWidget(row, 5, step_type_combo) high_item = QTableWidgetItem(high) high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable) high_item.setBackground(QColor(240, 240, 240)) self.factors_table.setItem(row, 6, high_item) low_item = QTableWidgetItem(low) low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable) low_item.setBackground(QColor(240, 240, 240)) self.factors_table.setItem(row, 7, low_item) unit_measure = QComboBox() unit_measure.addItems(["мкл", "мл", "л", "мг", "г", "кг"]) unit_measure.setCurrentText(unit) unit_measure.currentTextChanged.connect(lambda: self._on_factor_changed(row, 8)) self.factors_table.setCellWidget(row, 8, unit_measure) return row def _remove_factor_row(self): for row in sorted(set(i.row() for i in self.factors_table.selectedItems()), reverse=True): if row >= 0: self.factors_table.removeRow(row) def _import_reagents_to_factors(self): """Импортирует реагенты из калькулятора как факторы""" if not hasattr(self, 'last_medium_result') or self.last_medium_result is None: QMessageBox.warning(self, "Предупреждение", "Сначала выполните расчёт в калькуляторе сред!") return result = self.last_medium_result # Очищаем таблицу факторов self.factors_table.setRowCount(0) for reagent in result['reagents']: name = reagent['name'] if reagent.get('dilution_factor', 1.0) != 1.0: name += f"(разб. ×{reagent['dilution_factor']:g})" percentage = f"{reagent['percentage']:g}" dilution = f"{reagent.get('dilution_factor', 1.0):g}" center = str(reagent.get('calculated_amount')) step = "" step_type = "%" high = "" low = "" unit = reagent["unit"] self._add_factor_row(name, percentage, dilution, center, step, step_type, high, low, unit) # QMessageBox.information(self, "Успех", # f"Импортировано {len(result['reagents'])} факторов из калькулятора сред") def _get_factors_from_table(self) -> List[Dict]: """Собирает данные факторов из таблицы""" factors = [] for row in range(self.factors_table.rowCount()): try: name_item = self.factors_table.item(row, 0) center_item = self.factors_table.item(row, 3) step_item = self.factors_table.item(row, 4) high_item = self.factors_table.item(row, 6) low_item = self.factors_table.item(row, 7) unit_item = self.factors_table.item(row, 8) step_combo = self.factors_table.cellWidget(row, 5) if not all([name_item, center_item, high_item, low_item]): continue center_text = center_item.text().strip() high_text = high_item.text().strip() low_text = low_item.text().strip() if not center_text or not high_text or not low_text: continue factor = { 'name': name_item.text(), 'center': float(center_text), 'high': float(high_text), 'low': float(low_text), 'unit': unit_item.text() if unit_item else "", 'step': float(step_item.text()) if step_item and step_item.text() else 0, 'step_type': step_combo.currentText() if step_combo else "ед." } factors.append(factor) except (ValueError, AttributeError) as e: print(f"Ошибка в строке {row}: {e}") continue return factors def _on_factor_changed(self, row, column): """При изменении ячейки пересчитываем уровни""" if getattr(self, '_loading_data', False) or column not in [3, 4, 5, 8]: return try: # Получаем данные percentage = float(self.factors_table.item(row, 1).text()) dilution = float(self.factors_table.item(row, 2).text()) center = float(self.factors_table.item(row, 3).text()) step = float(self.factors_table.item(row, 4).text()) step_type = self.factors_table.cellWidget(row, 5).currentText() unit = self.factors_table.cellWidget(row, 8).currentText() name = self.factors_table.item(row, 0).text() # Рассчитываем factor = FactorData(percentage=percentage, dilution_factor=dilution, name=name, center=center, low=0, high=0, step=step, step_type=step_type, unit=unit) result = calculate_factor_levels(factor, self.total_volume_spin.value(), self.volume_unit_combo.currentText()) if result: self.factors_table.blockSignals(True) self.factors_table.item(row, 6).setText(self._format_number(result.high)) self.factors_table.item(row, 7).setText(self._format_number(result.low)) self.factors_table.blockSignals(False) except (ValueError, AttributeError): pass # ========== ВКЛАДКА 3: МАТРИЦА ПЛАНИРОВАНИЯ ========== def _create_design_tab(self): tab = QWidget() layout = QVBoxLayout(tab) # Скролл-область для большой матрицы scroll = QScrollArea() scroll.setWidgetResizable(True) scroll_widget = QWidget() scroll_layout = QVBoxLayout(scroll_widget) self.design_matrix = QTableWidget() scroll_layout.addWidget(self.design_matrix) scroll.setWidget(scroll_widget) layout.addWidget(scroll) btn_layout = QHBoxLayout() self.export_csv_btn = QPushButton("📊 Экспорт в CSV") self.export_csv_btn.clicked.connect(self._export_design_csv) self.export_csv_btn.setEnabled(False) btn_layout.addWidget(self.export_csv_btn) btn_layout.addStretch() layout.addLayout(btn_layout) self.design_info = QLabel("") self.design_info.setStyleSheet("color: #666; padding: 5px;") layout.addWidget(self.design_info) return tab def _generate_design(self): """Генерирует план эксперимента""" factors = self._get_factors_from_table() if len(factors) == 0: QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!") return try: design = generate_factorial_design( factors=factors, center_points=self.center_points_spin.value(), randomize=self.randomize_check.isChecked() ) self.generated_design = design n_exp = len(design) n_factors = len(factors) self.design_matrix.setRowCount(n_exp) self.design_matrix.setColumnCount(n_factors + 2) headers = ["№"] + [f['name'] for f in factors] + ["Тип"] self.design_matrix.setHorizontalHeaderLabels(headers) for exp_idx, exp in enumerate(design): self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1))) for f_idx in range(n_factors): key = f"Фактор_{f_idx + 1}" if key not in exp: continue value = exp[key]['natural'] unit = factors[f_idx]['unit'] display = self._format_number(value) if unit: display += f" {unit}" item = QTableWidgetItem(display) if exp.get('is_center', False): item.setBackground(QColor(255, 255, 200)) self.design_matrix.setItem(exp_idx, f_idx + 1, item) if exp.get('is_center', False): type_item = QTableWidgetItem(f"Центр #{exp['center_num']}") type_item.setBackground(QColor(255, 255, 200)) else: type_item = QTableWidgetItem("Факторная") 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.design_info.setText( f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}" ) self.export_csv_btn.setEnabled(True) self._setup_results_table(n_exp) QMessageBox.information(self, "Успех", f"Сгенерирован план для {n_factors} факторов ({n_exp} опытов)") except Exception as e: QMessageBox.critical(self, "Ошибка", f"Ошибка генерации плана: {str(e)}") def _setup_results_table(self, n_experiments: int): """Настраивает таблицу результатов для ввода данных""" # Получаем отклики (для простоты используем один отклик) self.results_table.setRowCount(n_experiments) self.results_table.setColumnCount(2) self.results_table.setHorizontalHeaderLabels(["№ опыта", "Результат"]) for i in range(n_experiments): self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1))) def _export_design_csv(self): """Экспортирует матрицу планирования в CSV""" if not self.generated_design or self.design_matrix.rowCount() == 0: QMessageBox.warning(self, "Предупреждение", "Нет данных для экспорта") return filename, _ = QFileDialog.getSaveFileName( self, "Сохранить план", "", "CSV (*.csv);;Все файлы (*)" ) if filename: if not filename.endswith('.csv'): filename += '.csv' try: import csv 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 = self.design_matrix.horizontalHeaderItem(j) headers.append(header.text() if header else f"Колонка_{j+1}") writer.writerow(headers) # Данные for i in range(self.design_matrix.rowCount()): row = [] for j in range(self.design_matrix.columnCount()): item = self.design_matrix.item(i, j) row.append(item.text() if item else "") writer.writerow(row) QMessageBox.information(self, "Успех", f"План сохранён в {filename}") except Exception as e: QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить: {str(e)}") # ========== ВКЛАДКА 4: АНАЛИЗ ========== def _create_analysis_tab(self): tab = QWidget() layout = QVBoxLayout(tab) # Таблица для ввода результатов results_box = QGroupBox("Результаты экспериментов") results_layout = QVBoxLayout() self.results_table = QTableWidget() results_layout.addWidget(self.results_table) results_box.setLayout(results_layout) layout.addWidget(results_box) # Кнопка анализа self.analyze_btn = QPushButton("📈 Провести анализ") self.analyze_btn.clicked.connect(self._perform_analysis) layout.addWidget(self.analyze_btn) # Вывод анализа self.analysis_text = QTextEdit() self.analysis_text.setReadOnly(True) self.analysis_text.setMaximumHeight(250) layout.addWidget(self.analysis_text) return tab def _perform_analysis(self): """Выполняет анализ результатов эксперимента""" if not self.generated_design: QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") return # Получаем отклики responses = [{'name': 'Отклик', 'unit': ''}] # Собираем результаты results = [] for i in range(self.results_table.rowCount()): item = self.results_table.item(i, 1) if item and item.text(): try: results.append([float(item.text())]) except ValueError: QMessageBox.warning(self, "Ошибка", f"Неверное значение в опыте {i+1}") return else: QMessageBox.warning(self, "Ошибка", f"Не введены данные для опыта {i+1}") return if len(results) != len(self.generated_design): QMessageBox.warning(self, "Ошибка", f"Введено {len(results)} результатов, ожидается {len(self.generated_design)}") return try: # Выполняем анализ analysis = analyze_experiment(results, self.generated_design, responses) # Выводим результаты self.analysis_text.clear() self.analysis_text.append("=" * 60) self.analysis_text.append("РЕЗУЛЬТАТЫ АНАЛИЗА ЭКСПЕРИМЕНТА") self.analysis_text.append("=" * 60) for resp_name, stats in analysis.items(): self.analysis_text.append(f"\n📊 {resp_name}") self.analysis_text.append("-" * 40) self.analysis_text.append(f"Среднее значение: {stats['mean']:.4f}") self.analysis_text.append(f"Дисперсия: {stats['variance']:.4f}") self.analysis_text.append(f"Ст. отклонение: {stats['std_dev']:.4f}") self.analysis_text.append(f"Коэф. вариации: {stats['cv']:.2f}%") if stats['n_center'] > 1: self.analysis_text.append(f"\nЦентральные точки (n={stats['n_center']}):") self.analysis_text.append(f" Дисперсия воспроизводимости: {stats['center_variance']:.4f}") if stats.get('fisher_ratio'): self.analysis_text.append(f"\nКритерий Фишера: {stats['fisher_ratio']:.4f}") if stats.get('model_adequate'): self.analysis_text.append("✅ Модель адекватна") else: self.analysis_text.append("⚠️ Модель может быть неадекватна") self.analysis_text.append("\n" + "=" * 60) self.analysis_text.append("Анализ завершен") except Exception as e: QMessageBox.critical(self, "Ошибка", f"Ошибка анализа: {str(e)}") # ========== МЕТОДЫ РАБОТЫ С JSON ========== def save_project_to_json(self): """Сохраняет весь проект в JSON файл""" if not JSON_SUPPORT: QMessageBox.warning(self, "Предупреждение", "JSON поддержка не доступна") return filename, _ = QFileDialog.getSaveFileName( self, "Сохранить проект", "", "JSON Files (*.json);;All Files (*)" ) if not filename: return if not filename.endswith('.json'): filename += '.json' try: project = self._collect_current_data() project.save_to_file(filename) QMessageBox.information(self, "Успех", f"Проект сохранён в {filename}") except Exception as e: QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить проект: {str(e)}") def load_project_from_json(self): """Загружает проект из JSON файла""" if not JSON_SUPPORT: QMessageBox.warning(self, "Предупреждение", "JSON поддержка не доступна") return filename, _ = QFileDialog.getOpenFileName( self, "Загрузить проект", "", "JSON Files (*.json);;All Files (*)" ) if not filename: return try: project = ProjectData.load_from_file(filename) self._apply_project_data(project) # QMessageBox.information(self, "Успех", f"Проект загружен из {filename}") except FileNotFoundError: QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}") except Exception as e: QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить проект: {str(e)}") def _collect_current_data(self): """Собирает текущие данные из всех виджетов""" # Данные калькулятора сред reagents = [] for row in range(self.reagents_table.rowCount()): name_item = self.reagents_table.item(row, 0) percent_item = self.reagents_table.item(row, 1) unit_widget = self.reagents_table.cellWidget(row, 2) coeff_item = self.reagents_table.item(row, 3) dilution_item = self.reagents_table.item(row, 4) if name_item and percent_item and percent_item.text(): try: reagent = ReagentData( 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 and coeff_item.text() else 1.0, dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0 ) reagents.append(reagent) except ValueError: pass # Данные факторов эксперимента factors = [] for row in range(self.factors_table.rowCount()): name_item = self.factors_table.item(row, 0) center_item = self.factors_table.item(row, 3) low_item = self.factors_table.item(row, 7) high_item = self.factors_table.item(row, 6) step_item = self.factors_table.item(row, 4) unit_item = self.factors_table.cellWidget(row, 8) step_combo = self.factors_table.cellWidget(row, 5) percent_item = self.factors_table.item(row, 1) dilution_item = self.factors_table.item(row, 2) if name_item and center_item and center_item.text(): try: factor = FactorData( name=name_item.text(), center=float(center_item.text()), low=float(low_item.text()) if low_item and low_item.text() else 0, high=float(high_item.text()) if high_item and high_item.text() else 0, step=float(step_item.text()) if step_item and step_item.text() else 0, step_type=step_combo.currentText() if step_combo else "ед.", unit=unit_item.currentText() if unit_item else "", percentage=float(percent_item.text()) if percent_item and percent_item.text() else None, dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None ) factors.append(factor) except ValueError: pass # Данные результатов (если есть) results_data = None if hasattr(self, 'generated_design') and self.generated_design: results = [] for i in range(self.results_table.rowCount()): item = self.results_table.item(i, 1) if item and item.text(): try: results.append([float(item.text())]) except ValueError: pass if results: responses = [ResponseData(name="Отклик", unit="")] results_data = ExperimentResultsData( design=self.generated_design, results=results, responses=responses ) # Создаём проект project = create_new_project("Мой проект") project.medium_total_volume = self.total_volume_spin.value() project.medium_volume_unit = self.volume_unit_combo.currentText() project.medium_solvent = self.solvent_input.text() project.medium_reagents = reagents project.experiment_factors = factors project.experiment_center_points = self.center_points_spin.value() project.experiment_randomize = self.randomize_check.isChecked() project.experiment_results = results_data return project def _apply_project_data(self, project): """Применяет загруженные данные к GUI""" self._loading_data = True # <- ОТКЛЮЧАЕМ ОБРАБОТЧИК try: # Применяем данные калькулятора сред self.total_volume_spin.setValue(project.medium_total_volume) index = self.volume_unit_combo.findText(project.medium_volume_unit) if index >= 0: self.volume_unit_combo.setCurrentIndex(index) self.solvent_input.setText(project.medium_solvent) # Очищаем и заполняем таблицу реагентов self.reagents_table.setRowCount(0) for reagent in project.medium_reagents: row = self.reagents_table.rowCount() self.reagents_table.insertRow(row) self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name)) self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage))) unit_combo = QComboBox() unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"]) unit_combo.setCurrentText(reagent.unit) self.reagents_table.setCellWidget(row, 2, unit_combo) self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor))) self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor))) self.reagents_table.setItem(row, 5, QTableWidgetItem("")) # Применяем данные факторов эксперимента self.factors_table.setRowCount(0) for factor in project.experiment_factors: if factor.percentage is not None: percentage = str(factor.percentage) if factor.dilution_factor is not None: dilution = str(factor.dilution_factor) self._add_factor_row(factor.name, percentage, dilution, str(factor.center), str(factor.step), factor.step_type, str(factor.high), str(factor.low), factor.unit) # Применяем настройки эксперимента self.center_points_spin.setValue(project.experiment_center_points) self.randomize_check.setChecked(project.experiment_randomize) # Если есть результаты, загружаем их if project.experiment_results and project.experiment_results.design: self.generated_design = project.experiment_results.design # Обновляем отображение матрицы self._refresh_design_matrix() # Загружаем результаты в таблицу if project.experiment_results.results: for i, row_results in enumerate(project.experiment_results.results): if i < self.results_table.rowCount() and row_results: self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0]))) # Переключаемся на вкладку с экспериментом if self.tab_widget: self.tab_widget.setCurrentIndex(0) finally: self._loading_data = False # <- ВКЛЮЧАЕМ ОБРАТНО def _refresh_design_matrix(self): """Обновляет отображение матрицы планирования""" if not self.generated_design: return factors = self._get_factors_from_table() n_exp = len(self.generated_design) n_factors = len(factors) self.design_matrix.setRowCount(n_exp) self.design_matrix.setColumnCount(n_factors + 2) headers = ["№"] + [f['name'] for f in factors] + ["Тип"] self.design_matrix.setHorizontalHeaderLabels(headers) for exp_idx, exp in enumerate(self.generated_design): self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1))) for f_idx in range(n_factors): key = f"Фактор_{f_idx + 1}" if key not in exp: continue value = exp[key]['natural'] unit = factors[f_idx]['unit'] display = self._format_number(value) if unit: display += f" {unit}" item = QTableWidgetItem(display) if exp.get('is_center', False): item.setBackground(QColor(255, 255, 200)) self.design_matrix.setItem(exp_idx, f_idx + 1, item) if exp.get('is_center', False): type_item = QTableWidgetItem(f"Центр #{exp['center_num']}") type_item.setBackground(QColor(255, 255, 200)) else: type_item = QTableWidgetItem("Факторная") self.design_matrix.setItem(exp_idx, n_factors + 1, type_item) self.design_matrix.resizeColumnsToContents() if hasattr(self, 'export_csv_btn'): self.export_csv_btn.setEnabled(True) n_factorial = 2 ** n_factors n_center = self.center_points_spin.value() self.design_info.setText( f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}" ) self._setup_results_table(n_exp) # ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ========== def _format_number(self, value: float) -> str: """Форматирует число для отображения""" 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 main(): app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_()) if __name__ == "__main__": main()