Очищены лишние файлы
This commit is contained in:
@@ -1,904 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Скрипт для добавления функционала сохранения/загрузки данных в JSON
|
|
||||||
# Запускать из корневой директории проекта
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "📁 Добавление функционала сохранения/загрузки данных в JSON..."
|
|
||||||
echo "================================================================"
|
|
||||||
|
|
||||||
# Создаём директорию для моделей, если её нет
|
|
||||||
mkdir -p calculations/models
|
|
||||||
|
|
||||||
# 1. СОЗДАЁМ МОДЕЛЬ ДАННЫХ ДЛЯ СОХРАНЕНИЯ
|
|
||||||
cat > calculations/models/project_data.py << 'EOF'
|
|
||||||
"""
|
|
||||||
Модель данных проекта для сохранения/загрузки в JSON
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import List, Dict, Optional
|
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
from datetime import datetime
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ReagentData:
|
|
||||||
"""Данные реагента"""
|
|
||||||
name: str
|
|
||||||
percentage: float
|
|
||||||
unit: str
|
|
||||||
conversion_factor: float
|
|
||||||
dilution_factor: float
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: Dict) -> 'ReagentData':
|
|
||||||
return cls(
|
|
||||||
name=data['name'],
|
|
||||||
percentage=data['percentage'],
|
|
||||||
unit=data['unit'],
|
|
||||||
conversion_factor=data.get('conversion_factor', 1.0),
|
|
||||||
dilution_factor=data.get('dilution_factor', 1.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FactorData:
|
|
||||||
"""Данные фактора эксперимента"""
|
|
||||||
name: str
|
|
||||||
center: float
|
|
||||||
low: float
|
|
||||||
high: float
|
|
||||||
step: float
|
|
||||||
step_type: str
|
|
||||||
unit: str
|
|
||||||
percentage: Optional[float] = None
|
|
||||||
dilution_factor: Optional[float] = None
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
|
||||||
d = asdict(self)
|
|
||||||
# Удаляем None значения
|
|
||||||
return {k: v for k, v in d.items() if v is not None}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: Dict) -> 'FactorData':
|
|
||||||
return cls(
|
|
||||||
name=data['name'],
|
|
||||||
center=data['center'],
|
|
||||||
low=data['low'],
|
|
||||||
high=data['high'],
|
|
||||||
step=data.get('step', 0),
|
|
||||||
step_type=data.get('step_type', 'абс'),
|
|
||||||
unit=data.get('unit', ''),
|
|
||||||
percentage=data.get('percentage'),
|
|
||||||
dilution_factor=data.get('dilution_factor')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ResponseData:
|
|
||||||
"""Данные отклика эксперимента"""
|
|
||||||
name: str
|
|
||||||
unit: str
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: Dict) -> 'ResponseData':
|
|
||||||
return cls(
|
|
||||||
name=data['name'],
|
|
||||||
unit=data.get('unit', '')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ExperimentResultsData:
|
|
||||||
"""Результаты эксперимента"""
|
|
||||||
design: List[Dict] # План эксперимента
|
|
||||||
results: List[List[float]] # Результаты измерений
|
|
||||||
responses: List[ResponseData] # Отклики
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
|
||||||
return {
|
|
||||||
'design': self.design,
|
|
||||||
'results': self.results,
|
|
||||||
'responses': [r.to_dict() for r in self.responses]
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: Dict) -> 'ExperimentResultsData':
|
|
||||||
return cls(
|
|
||||||
design=data['design'],
|
|
||||||
results=data['results'],
|
|
||||||
responses=[ResponseData.from_dict(r) for r in data['responses']]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ProjectData:
|
|
||||||
"""Полные данные проекта"""
|
|
||||||
# Информация о проекте
|
|
||||||
project_name: str
|
|
||||||
created_at: str
|
|
||||||
modified_at: str
|
|
||||||
version: str = "1.0"
|
|
||||||
|
|
||||||
# Данные калькулятора сред
|
|
||||||
medium_total_volume: float = 1000.0
|
|
||||||
medium_volume_unit: str = "мл"
|
|
||||||
medium_solvent: str = "Вода"
|
|
||||||
medium_reagents: List[ReagentData] = None
|
|
||||||
|
|
||||||
# Данные эксперимента
|
|
||||||
experiment_factors: List[FactorData] = None
|
|
||||||
experiment_responses: List[ResponseData] = None
|
|
||||||
experiment_center_points: int = 3
|
|
||||||
experiment_randomize: bool = True
|
|
||||||
experiment_results: Optional[ExperimentResultsData] = None
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.medium_reagents is None:
|
|
||||||
self.medium_reagents = []
|
|
||||||
if self.experiment_factors is None:
|
|
||||||
self.experiment_factors = []
|
|
||||||
if self.experiment_responses is None:
|
|
||||||
self.experiment_responses = []
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
|
||||||
"""Конвертирует в словарь для JSON"""
|
|
||||||
return {
|
|
||||||
'project_info': {
|
|
||||||
'name': self.project_name,
|
|
||||||
'created_at': self.created_at,
|
|
||||||
'modified_at': self.modified_at,
|
|
||||||
'version': self.version
|
|
||||||
},
|
|
||||||
'medium_calculator': {
|
|
||||||
'total_volume': self.medium_total_volume,
|
|
||||||
'volume_unit': self.medium_volume_unit,
|
|
||||||
'solvent': self.medium_solvent,
|
|
||||||
'reagents': [r.to_dict() for r in self.medium_reagents]
|
|
||||||
},
|
|
||||||
'experiment': {
|
|
||||||
'factors': [f.to_dict() for f in self.experiment_factors],
|
|
||||||
'responses': [r.to_dict() for r in self.experiment_responses],
|
|
||||||
'center_points': self.experiment_center_points,
|
|
||||||
'randomize': self.experiment_randomize,
|
|
||||||
'results': self.experiment_results.to_dict() if self.experiment_results else None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: Dict) -> 'ProjectData':
|
|
||||||
"""Создаёт объект из словаря"""
|
|
||||||
project_info = data.get('project_info', {})
|
|
||||||
medium = data.get('medium_calculator', {})
|
|
||||||
experiment = data.get('experiment', {})
|
|
||||||
|
|
||||||
# Создаём объект
|
|
||||||
obj = cls(
|
|
||||||
project_name=project_info.get('name', 'Новый проект'),
|
|
||||||
created_at=project_info.get('created_at', datetime.now().isoformat()),
|
|
||||||
modified_at=project_info.get('modified_at', datetime.now().isoformat()),
|
|
||||||
version=project_info.get('version', '1.0'),
|
|
||||||
medium_total_volume=medium.get('total_volume', 1000.0),
|
|
||||||
medium_volume_unit=medium.get('volume_unit', 'мл'),
|
|
||||||
medium_solvent=medium.get('solvent', 'Вода'),
|
|
||||||
medium_reagents=[ReagentData.from_dict(r) for r in medium.get('reagents', [])],
|
|
||||||
experiment_factors=[FactorData.from_dict(f) for f in experiment.get('factors', [])],
|
|
||||||
experiment_responses=[ResponseData.from_dict(r) for r in experiment.get('responses', [])],
|
|
||||||
experiment_center_points=experiment.get('center_points', 3),
|
|
||||||
experiment_randomize=experiment.get('randomize', True),
|
|
||||||
experiment_results=ExperimentResultsData.from_dict(experiment['results'])
|
|
||||||
if experiment.get('results') else None
|
|
||||||
)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def save_to_file(self, filename: str):
|
|
||||||
"""Сохраняет проект в JSON файл"""
|
|
||||||
self.modified_at = datetime.now().isoformat()
|
|
||||||
with open(filename, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load_from_file(cls, filename: str) -> 'ProjectData':
|
|
||||||
"""Загружает проект из JSON файла"""
|
|
||||||
with open(filename, 'r', encoding='utf-8') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
return cls.from_dict(data)
|
|
||||||
|
|
||||||
|
|
||||||
def create_new_project(name: str = "Новый проект") -> ProjectData:
|
|
||||||
"""Создаёт новый проект с текущей датой"""
|
|
||||||
now = datetime.now().isoformat()
|
|
||||||
return ProjectData(
|
|
||||||
project_name=name,
|
|
||||||
created_at=now,
|
|
||||||
modified_at=now
|
|
||||||
)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Создан calculations/models/project_data.py"
|
|
||||||
|
|
||||||
# 2. ДОБАВЛЯЕМ ФУНКЦИИ В GUI ДЛЯ РАБОТЫ С JSON
|
|
||||||
# Создаём патч для gui.py
|
|
||||||
cat > gui_patch.py << 'EOF'
|
|
||||||
# ДОБАВИТЬ В gui.py ПОСЛЕ ИМПОРТОВ:
|
|
||||||
|
|
||||||
from calculations.models.project_data import (
|
|
||||||
ProjectData, ReagentData, FactorData,
|
|
||||||
ResponseData, ExperimentResultsData, create_new_project
|
|
||||||
)
|
|
||||||
|
|
||||||
# ДОБАВИТЬ В КЛАСС MainWindow НОВЫЕ МЕТОДЫ:
|
|
||||||
|
|
||||||
def save_project_to_json(self):
|
|
||||||
"""Сохраняет весь проект в JSON файл"""
|
|
||||||
filename, _ = QFileDialog.getSaveFileName(
|
|
||||||
self, "Сохранить проект", "", "JSON Files (*.json);;All Files (*)"
|
|
||||||
)
|
|
||||||
if not filename:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not filename.endswith('.json'):
|
|
||||||
filename += '.json'
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Собираем данные из GUI
|
|
||||||
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 файла"""
|
|
||||||
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) -> ProjectData:
|
|
||||||
"""Собирает текущие данные из всех виджетов"""
|
|
||||||
# Данные калькулятора сред
|
|
||||||
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:
|
|
||||||
try:
|
|
||||||
reagent = ReagentData(
|
|
||||||
name=name_item.text(),
|
|
||||||
percentage=float(percent_item.text()) if percent_item.text() else 0,
|
|
||||||
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.item(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:
|
|
||||||
try:
|
|
||||||
factor = FactorData(
|
|
||||||
name=name_item.text(),
|
|
||||||
center=float(center_item.text()) if center_item.text() else 0,
|
|
||||||
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.text() 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: ProjectData):
|
|
||||||
"""Применяет загруженные данные к GUI"""
|
|
||||||
# Применяем данные калькулятора сред
|
|
||||||
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:
|
|
||||||
row = self.factors_table.rowCount()
|
|
||||||
self.factors_table.insertRow(row)
|
|
||||||
self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name))
|
|
||||||
|
|
||||||
if factor.percentage is not None:
|
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage)))
|
|
||||||
else:
|
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
|
|
||||||
|
|
||||||
if factor.dilution_factor is not None:
|
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor)))
|
|
||||||
else:
|
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
|
|
||||||
|
|
||||||
self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center)))
|
|
||||||
self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step)))
|
|
||||||
|
|
||||||
step_combo = QComboBox()
|
|
||||||
step_combo.addItems(["абс", "%"])
|
|
||||||
step_combo.setCurrentText(factor.step_type)
|
|
||||||
self.factors_table.setCellWidget(row, 5, step_combo)
|
|
||||||
|
|
||||||
self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high)))
|
|
||||||
self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low)))
|
|
||||||
self.factors_table.setItem(row, 8, QTableWidgetItem(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._display_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])))
|
|
||||||
|
|
||||||
# Переключаемся на вкладку с факторами
|
|
||||||
self.tab_widget.setCurrentIndex(1)
|
|
||||||
|
|
||||||
def _display_design_matrix(self):
|
|
||||||
"""Отображает матрицу планирования (добавить в _generate_design или отдельно)"""
|
|
||||||
if not self.generated_design:
|
|
||||||
return
|
|
||||||
|
|
||||||
n_exp = len(self.generated_design)
|
|
||||||
n_factors = len(self._get_factors_from_table())
|
|
||||||
|
|
||||||
self.design_matrix.setRowCount(n_exp)
|
|
||||||
self.design_matrix.setColumnCount(n_factors + 2)
|
|
||||||
headers = ["№"] + [f['name'] for f in self._get_factors_from_table()] + ["Тип"]
|
|
||||||
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 = self._get_factors_from_table()[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()
|
|
||||||
self.export_csv_btn.setEnabled(True)
|
|
||||||
self._setup_results_table(n_exp)
|
|
||||||
|
|
||||||
# ДОБАВИТЬ В МЕТОД _setup_ui ПОСЛЕ СОЗДАНИЯ ВКЛАДОК:
|
|
||||||
|
|
||||||
# Добавляем кнопки сохранения/загрузки в тулбар
|
|
||||||
toolbar = self.addToolBar("Файл")
|
|
||||||
toolbar.setMovable(False)
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
# Сохраняем ссылку на tab_widget для переключения вкладок
|
|
||||||
self.tab_widget = tabs
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Создан патч gui_patch.py"
|
|
||||||
|
|
||||||
# 3. ПРИМЕНЯЕМ ПАТЧ К GUI.PY
|
|
||||||
# Создаём резервную копию
|
|
||||||
cp gui.py gui.py.backup
|
|
||||||
echo "✅ Создана резервная копия gui.py.backup"
|
|
||||||
|
|
||||||
# Вставляем импорты после существующих импортов
|
|
||||||
sed -i '/from calculations.doe import/a\
|
|
||||||
from calculations.models.project_data import (\
|
|
||||||
ProjectData, ReagentData, FactorData, \
|
|
||||||
ResponseData, ExperimentResultsData, create_new_project\
|
|
||||||
)' gui.py
|
|
||||||
|
|
||||||
# Находим строку с self._setup_ui() и добавляем после неё toolbar
|
|
||||||
sed -i '/self._setup_ui()/a\ self._add_file_toolbar()' gui.py
|
|
||||||
|
|
||||||
# Добавляем новый метод _add_file_toolbar после _setup_ui
|
|
||||||
cat >> gui.py.new_method << 'EOF'
|
|
||||||
|
|
||||||
def _add_file_toolbar(self):
|
|
||||||
"""Добавляет панель инструментов с кнопками сохранения/загрузки"""
|
|
||||||
toolbar = self.addToolBar("Файл")
|
|
||||||
toolbar.setMovable(False)
|
|
||||||
|
|
||||||
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()
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Вставляем новый метод после _setup_ui
|
|
||||||
sed -i '/def _setup_ui(self):/,/def _get_stylesheet(self):/ {
|
|
||||||
/def _get_stylesheet(self):/ {
|
|
||||||
r gui.py.new_method
|
|
||||||
}
|
|
||||||
}' gui.py
|
|
||||||
|
|
||||||
rm gui.py.new_method
|
|
||||||
|
|
||||||
# Добавляем методы сохранения/загрузки в конец файла перед if __name__
|
|
||||||
cat >> gui.py << 'EOF'
|
|
||||||
|
|
||||||
# ========== МЕТОДЫ РАБОТЫ С JSON ==========
|
|
||||||
|
|
||||||
def save_project_to_json(self):
|
|
||||||
"""Сохраняет весь проект в JSON файл"""
|
|
||||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox
|
|
||||||
|
|
||||||
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 файла"""
|
|
||||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QComboBox, QTableWidgetItem
|
|
||||||
|
|
||||||
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) -> ProjectData:
|
|
||||||
"""Собирает текущие данные из всех виджетов"""
|
|
||||||
from PyQt5.QtWidgets import QComboBox
|
|
||||||
|
|
||||||
# Данные калькулятора сред
|
|
||||||
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.item(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.text() 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: ProjectData):
|
|
||||||
"""Применяет загруженные данные к GUI"""
|
|
||||||
from PyQt5.QtWidgets import QComboBox, QTableWidgetItem
|
|
||||||
from PyQt5.QtCore import Qt
|
|
||||||
|
|
||||||
# Применяем данные калькулятора сред
|
|
||||||
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:
|
|
||||||
row = self.factors_table.rowCount()
|
|
||||||
self.factors_table.insertRow(row)
|
|
||||||
self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name))
|
|
||||||
|
|
||||||
if factor.percentage is not None:
|
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage)))
|
|
||||||
else:
|
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
|
|
||||||
|
|
||||||
if factor.dilution_factor is not None:
|
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor)))
|
|
||||||
else:
|
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
|
|
||||||
|
|
||||||
self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center)))
|
|
||||||
self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step)))
|
|
||||||
|
|
||||||
step_combo = QComboBox()
|
|
||||||
step_combo.addItems(["абс", "%"])
|
|
||||||
step_combo.setCurrentText(factor.step_type)
|
|
||||||
self.factors_table.setCellWidget(row, 5, step_combo)
|
|
||||||
|
|
||||||
self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high)))
|
|
||||||
self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low)))
|
|
||||||
self.factors_table.setItem(row, 8, QTableWidgetItem(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 hasattr(self, 'tab_widget'):
|
|
||||||
self.tab_widget.setCurrentIndex(1)
|
|
||||||
|
|
||||||
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)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Добавлены методы работы с JSON в gui.py"
|
|
||||||
|
|
||||||
# 4. СОЗДАЁМ ПРИМЕР JSON ФАЙЛА
|
|
||||||
cat > example_project.json << 'EOF'
|
|
||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"name": "Пример проекта",
|
|
||||||
"created_at": "2026-05-14T16:00:00",
|
|
||||||
"modified_at": "2026-05-14T16:00:00",
|
|
||||||
"version": "1.0"
|
|
||||||
},
|
|
||||||
"medium_calculator": {
|
|
||||||
"total_volume": 1000,
|
|
||||||
"volume_unit": "мл",
|
|
||||||
"solvent": "Вода",
|
|
||||||
"reagents": [
|
|
||||||
{
|
|
||||||
"name": "Глюкоза",
|
|
||||||
"percentage": 2.0,
|
|
||||||
"unit": "г",
|
|
||||||
"conversion_factor": 1.0,
|
|
||||||
"dilution_factor": 1.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Пептон",
|
|
||||||
"percentage": 1.0,
|
|
||||||
"unit": "г",
|
|
||||||
"conversion_factor": 1.0,
|
|
||||||
"dilution_factor": 1.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"experiment": {
|
|
||||||
"factors": [
|
|
||||||
{
|
|
||||||
"name": "Глюкоза",
|
|
||||||
"center": 20.0,
|
|
||||||
"low": 18.0,
|
|
||||||
"high": 22.0,
|
|
||||||
"step": 2.0,
|
|
||||||
"step_type": "абс",
|
|
||||||
"unit": "г",
|
|
||||||
"percentage": 2.0,
|
|
||||||
"dilution_factor": 1.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Пептон",
|
|
||||||
"center": 10.0,
|
|
||||||
"low": 9.0,
|
|
||||||
"high": 11.0,
|
|
||||||
"step": 1.0,
|
|
||||||
"step_type": "абс",
|
|
||||||
"unit": "г",
|
|
||||||
"percentage": 1.0,
|
|
||||||
"dilution_factor": 1.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": [
|
|
||||||
{"name": "Отклик", "unit": ""}
|
|
||||||
],
|
|
||||||
"center_points": 3,
|
|
||||||
"randomize": true,
|
|
||||||
"results": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Создан example_project.json"
|
|
||||||
|
|
||||||
# 5. СОЗДАЁМ requirements.txt ЕСЛИ ЕГО НЕТ
|
|
||||||
if [ ! -f requirements.txt ]; then
|
|
||||||
cat > requirements.txt << 'EOF'
|
|
||||||
PyQt5>=5.15.0
|
|
||||||
numpy>=1.21.0
|
|
||||||
EOF
|
|
||||||
echo "✅ Создан requirements.txt"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "================================================================"
|
|
||||||
echo "✅ ГОТОВО! Функционал сохранения/загрузки JSON добавлен!"
|
|
||||||
echo "================================================================"
|
|
||||||
echo ""
|
|
||||||
echo "Что было добавлено:"
|
|
||||||
echo " 1. Модель данных в calculations/models/project_data.py"
|
|
||||||
echo " 2. Кнопки 'Сохранить проект' и 'Загрузить проект' в toolbar"
|
|
||||||
echo " 3. Методы сохранения/загрузки в gui.py"
|
|
||||||
echo " 4. Пример JSON файла example_project.json"
|
|
||||||
echo ""
|
|
||||||
echo "Для запуска программы:"
|
|
||||||
echo " python main.py"
|
|
||||||
echo ""
|
|
||||||
echo "Для восстановления оригинального gui.py (если нужно):"
|
|
||||||
echo " cp gui.py.backup gui.py"
|
|
||||||
echo ""
|
|
||||||
echo "Чтобы применить изменения, перезапустите программу"
|
|
||||||
echo "================================================================"
|
|
||||||
Reference in New Issue
Block a user