Программа перекомпанована
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,6 @@
|
|||||||
# Python
|
# Python
|
||||||
|
backup
|
||||||
|
*.json
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|||||||
@@ -1,73 +1,5 @@
|
|||||||
markdown
|
# Цифровой помощник биохимика
|
||||||
# 🧬 Цифровой помощник биохимика
|
|
||||||
|
|
||||||
**Биотехнологические инструменты для лаборатории**
|
Биотехнологические инструменты для лаборатории.
|
||||||
|
|
||||||
## 📋 О проекте
|
## Структура проекта
|
||||||
|
|
||||||
Цифровой помощник биохимика — это программный комплекс для автоматизации лабораторных расчётов и планирования экспериментов в области биотехнологии и биохимии.
|
|
||||||
|
|
||||||
## 🚀 Возможности
|
|
||||||
|
|
||||||
### 1. 🥼 Калькулятор питательных сред
|
|
||||||
- Расчёт состава питательных сред по процентному содержанию компонентов
|
|
||||||
- Поддержка различных единиц измерения (нг, мкг, мг, г, кг, нл, мкл, мл, л)
|
|
||||||
- Учёт разбавления исходных реактивов
|
|
||||||
- Автоматический расчёт количества растворителя
|
|
||||||
- Сохранение и загрузка рецептов в JSON
|
|
||||||
|
|
||||||
### 2. 📊 Планирование эксперимента (DoE)
|
|
||||||
- Полнофакторный дизайн эксперимента (2^k факторный план)
|
|
||||||
- Интерактивное создание матрицы планирования
|
|
||||||
- Регрессионный анализ (в разработке)
|
|
||||||
- Визуализация результатов (в разработке)
|
|
||||||
|
|
||||||
## 🛠 Технологии
|
|
||||||
|
|
||||||
- Python 3.6+
|
|
||||||
- PyQt5 — графический интерфейс
|
|
||||||
- JSON — хранение данных
|
|
||||||
|
|
||||||
## 📁 Структура проекта
|
|
||||||
digital_biochemist_assistant/
|
|
||||||
├── main.py # Точка входа
|
|
||||||
├── main_window.py # Главное окно с выбором инструментов
|
|
||||||
├── controller.py # Контроллер калькулятора сред
|
|
||||||
├── model.py # Модель расчётов
|
|
||||||
├── view.py # Интерфейс калькулятора
|
|
||||||
├── reagent.py # Класс реагента
|
|
||||||
├── experiment_design.py # Инструмент планирования эксперимента
|
|
||||||
└── README.md # Документация
|
|
||||||
|
|
||||||
text
|
|
||||||
|
|
||||||
## 🎯 Планы развития
|
|
||||||
|
|
||||||
- [ ] Расширенный статистический анализ
|
|
||||||
- [ ] Визуализация поверхностей отклика
|
|
||||||
- [ ] Экспорт в Excel и PDF
|
|
||||||
- [ ] База данных реагентов и рецептов
|
|
||||||
- [ ] Графический редактор плана эксперимента
|
|
||||||
- [ ] Модуль концентраций и разбавлений
|
|
||||||
- [ ] Калькулятор растворов
|
|
||||||
|
|
||||||
## 📄 Лицензия
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
## 👨💻 Автор
|
|
||||||
|
|
||||||
Kolobov artem
|
|
||||||
|
|
||||||
|
|
||||||
Основные изменения:
|
|
||||||
|
|
||||||
1. Новое название - "Цифровой помощник биохимика" (Digital Biotechnologist Assistant)
|
|
||||||
|
|
||||||
2. Современный дизайн - градиентный фон, иконки, улучшенная стилизация
|
|
||||||
|
|
||||||
3. Расширенный DoE модуль - уже умеет генерировать матрицу планирования для 2^k факторного плана
|
|
||||||
|
|
||||||
4. Улучшенная навигация - вкладки, понятные описания
|
|
||||||
|
|
||||||
5. Профессиональный вид - современный интерфейс, подходящий для лаборатории
|
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
# main.py
|
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
from main_window import MainWindow
|
from src.views import MainWindow
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
# Устанавливаем стиль приложения
|
|
||||||
app.setStyle('Fusion')
|
app.setStyle('Fusion')
|
||||||
|
|
||||||
# Создаём главное окно цифрового помощника биохимика
|
|
||||||
assistant = MainWindow()
|
assistant = MainWindow()
|
||||||
assistant.show()
|
assistant.show()
|
||||||
|
|
||||||
# Запускаем цикл обработки событий
|
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
import json
|
|
||||||
from reagent import Reagent
|
|
||||||
|
|
||||||
VOLUME_UNITS = {
|
|
||||||
'нл': 0.001, # 1 нл = 0.001 мкл
|
|
||||||
'мкл': 1.0, # базовая единица объёма
|
|
||||||
'мл': 1000.0, # 1 мл = 1000 мкл
|
|
||||||
'л': 1000000.0 # 1 л = 1 000 000 мкл
|
|
||||||
}
|
|
||||||
|
|
||||||
MASS_UNITS = {
|
|
||||||
'нг': 0.000001, # 1 нг = 0.001 мкг
|
|
||||||
'мкг': 0.001, # базовая единица массы
|
|
||||||
'мг': 1.0, # 1 мг = 1000 мкг
|
|
||||||
'г': 1000.0, # 1 г = 1 000 000 мкг
|
|
||||||
'кг': 1000000.0 # 1 кг = 1 000 000 000 мкг
|
|
||||||
}
|
|
||||||
|
|
||||||
class Model:
|
|
||||||
def __init__(self):
|
|
||||||
self.total_amount = 100.0 # общее количество среды (по умолчанию — 1000 мл)
|
|
||||||
self.amount_unit = 'мл' # единица измерения общего количества
|
|
||||||
self.solvent = 'Вода' # растворитель по умолчанию
|
|
||||||
self.reagents = [] # список реагентов
|
|
||||||
|
|
||||||
def convert_amount(self, amount_base: float, target_unit: str, is_volume: bool) -> float:
|
|
||||||
"""
|
|
||||||
Пересчитывает количество из базовых единиц (мкл/мкг) в целевую единицу.
|
|
||||||
"""
|
|
||||||
if is_volume:
|
|
||||||
conversion_factor = VOLUME_UNITS.get(target_unit, 1.0)
|
|
||||||
else:
|
|
||||||
conversion_factor = MASS_UNITS.get(target_unit, 1.0)
|
|
||||||
|
|
||||||
return amount_base / conversion_factor
|
|
||||||
|
|
||||||
def calculate_amounts(self) -> tuple:
|
|
||||||
"""
|
|
||||||
Рассчитывает:
|
|
||||||
1. Исходное количество реагента для неразбавленного состава
|
|
||||||
2. Количество разбавленного реагента, которое нужно взять
|
|
||||||
3. Количество растворителя с учётом добавленных разбавленных реагентов
|
|
||||||
|
|
||||||
Возвращает: (список количеств разбавленных реагентов, количество растворителя, процент растворителя)
|
|
||||||
"""
|
|
||||||
results = []
|
|
||||||
|
|
||||||
# Проверяем, есть ли реагенты
|
|
||||||
if not self.reagents:
|
|
||||||
return results, self.total_amount, 100.0
|
|
||||||
|
|
||||||
# Суммируем проценты всех реагентов
|
|
||||||
total_percentage = sum(reagent.percentage for reagent in self.reagents)
|
|
||||||
|
|
||||||
|
|
||||||
# Проверяем, что сумма процентов не превышает 100
|
|
||||||
if total_percentage > 100:
|
|
||||||
raise ValueError(f"Сумма процентов реагентов ({total_percentage:.2f}%) превышает 100%")
|
|
||||||
|
|
||||||
# Шаг 1: Рассчитываем общий объём среды в базовых единицах (мкл)
|
|
||||||
total_in_base = self.total_amount * VOLUME_UNITS[self.amount_unit]
|
|
||||||
|
|
||||||
# Шаг 2: Рассчитываем количество каждого реагента для неразбавленного состава
|
|
||||||
undiluted_amounts = []
|
|
||||||
for reagent in self.reagents:
|
|
||||||
|
|
||||||
# Расчёт количества реагента в базовых единицах
|
|
||||||
amount_in_base = (reagent.percentage / 100) * total_in_base
|
|
||||||
|
|
||||||
# Определение типа единицы измерения реагента
|
|
||||||
is_volume = reagent.unit in VOLUME_UNITS
|
|
||||||
|
|
||||||
# Применение коэффициента пересчёта
|
|
||||||
adjusted_amount = amount_in_base * reagent.conversion_factor
|
|
||||||
|
|
||||||
# Пересчёт в целевую единицу измерения
|
|
||||||
final_amount = self.convert_amount(adjusted_amount, reagent.unit, is_volume)
|
|
||||||
|
|
||||||
undiluted_amounts.append(final_amount)
|
|
||||||
|
|
||||||
# Шаг 3: Рассчитываем количество разбавленного реагента, которое нужно взять
|
|
||||||
# и общий объём вносимых разбавленных реагентов (в базовых единицах)
|
|
||||||
diluted_amounts = []
|
|
||||||
total_diluted_volume_base = 0
|
|
||||||
|
|
||||||
for i, reagent in enumerate(self.reagents):
|
|
||||||
dilution_factor = getattr(reagent, 'dilution_factor', 1.0)
|
|
||||||
|
|
||||||
if dilution_factor <= 0:
|
|
||||||
dilution_factor = 1.0
|
|
||||||
|
|
||||||
# Количество разбавленного реагента = исходное количество / фактор разбавления
|
|
||||||
diluted_amount = undiluted_amounts[i] * dilution_factor
|
|
||||||
|
|
||||||
diluted_amounts.append(diluted_amount)
|
|
||||||
|
|
||||||
# Вычисляем объём разбавленного реагента в базовых единицах (мкл)
|
|
||||||
is_volume = reagent.unit in VOLUME_UNITS
|
|
||||||
if is_volume:
|
|
||||||
# Если реагент в объёмных единицах, его объём = diluted_amount
|
|
||||||
reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.unit]
|
|
||||||
else:
|
|
||||||
# Если реагент в массовых единицах, считаем, что он вносит пренебрежимый объём
|
|
||||||
# или можно добавить коэффициент плотности, пока игнорируем
|
|
||||||
reagent_volume_base = 0
|
|
||||||
|
|
||||||
total_diluted_volume_base += reagent_volume_base
|
|
||||||
|
|
||||||
# Шаг 4: Рассчитываем количество растворителя
|
|
||||||
# Растворитель = общий объём - объём всех разбавленных реагентов
|
|
||||||
solvent_volume_base = total_in_base - total_diluted_volume_base
|
|
||||||
|
|
||||||
# Переводим количество растворителя в целевую единицу измерения
|
|
||||||
solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit]
|
|
||||||
|
|
||||||
# Если объём реагентов превышает общий объём, корректируем
|
|
||||||
if solvent_amount < 0:
|
|
||||||
solvent_amount = 0
|
|
||||||
|
|
||||||
# Процент растворителя в неразбавленном составе
|
|
||||||
solvent_percentage = 100 - total_percentage
|
|
||||||
|
|
||||||
return diluted_amounts, solvent_amount, solvent_percentage
|
|
||||||
|
|
||||||
def save_to_file(self, filename: str):
|
|
||||||
"""Сохраняет модель в JSON-файл"""
|
|
||||||
data = {
|
|
||||||
'total_amount': self.total_amount,
|
|
||||||
'amount_unit': self.amount_unit,
|
|
||||||
'solvent': self.solvent,
|
|
||||||
'reagents': [
|
|
||||||
{
|
|
||||||
'name': r.name,
|
|
||||||
'percentage': r.percentage,
|
|
||||||
'unit': r.unit,
|
|
||||||
'conversion_factor': r.conversion_factor,
|
|
||||||
'dilution_factor': getattr(r, 'dilution_factor', 1.0)
|
|
||||||
} for r in self.reagents
|
|
||||||
]
|
|
||||||
}
|
|
||||||
with open(filename, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
|
||||||
|
|
||||||
def load_from_file(self, filename: str):
|
|
||||||
"""Загружает модель из JSON-файла"""
|
|
||||||
with open(filename, 'r', encoding='utf-8') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
self.total_amount = data['total_amount']
|
|
||||||
self.amount_unit = data['amount_unit']
|
|
||||||
self.solvent = data['solvent']
|
|
||||||
self.reagents.clear()
|
|
||||||
|
|
||||||
for r_data in data['reagents']:
|
|
||||||
reagent = Reagent(
|
|
||||||
name=r_data['name'],
|
|
||||||
percentage=r_data['percentage'],
|
|
||||||
unit=r_data['unit'],
|
|
||||||
conversion_factor=r_data.get('conversion_factor', 1.0)
|
|
||||||
)
|
|
||||||
# Добавляем коэффициент разбавления
|
|
||||||
reagent.dilution_factor = r_data.get('dilution_factor', 1.0)
|
|
||||||
self.reagents.append(reagent)
|
|
||||||
|
|
||||||
def add_reagent(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0,
|
|
||||||
dilution_factor: float = 1.0):
|
|
||||||
"""Добавляет новый реагент в модель"""
|
|
||||||
reagent = Reagent(name, percentage, unit, conversion_factor)
|
|
||||||
reagent.dilution_factor = dilution_factor
|
|
||||||
self.reagents.append(reagent)
|
|
||||||
|
|
||||||
def remove_reagent(self, index: int):
|
|
||||||
"""Удаляет реагент по индексу"""
|
|
||||||
if 0 <= index < len(self.reagents):
|
|
||||||
del self.reagents[index]
|
|
||||||
|
|
||||||
def clear_reagents(self):
|
|
||||||
"""Очищает список реагентов"""
|
|
||||||
self.reagents.clear()
|
|
||||||
|
|
||||||
def get_reagent_count(self) -> int:
|
|
||||||
"""Возвращает количество реагентов в модели"""
|
|
||||||
return len(self.reagents)
|
|
||||||
|
|
||||||
def set_total_amount(self, amount: float, unit: str):
|
|
||||||
"""Устанавливает общее количество среды и единицу измерения"""
|
|
||||||
self.total_amount = amount
|
|
||||||
self.amount_unit = unit
|
|
||||||
|
|
||||||
def set_solvent(self, solvent_name: str):
|
|
||||||
"""Устанавливает название растворителя"""
|
|
||||||
self.solvent = solvent_name
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
class Reagent:
|
|
||||||
def __init__(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0):
|
|
||||||
self.name = name
|
|
||||||
self.percentage = percentage
|
|
||||||
self.unit = unit
|
|
||||||
self.conversion_factor = conversion_factor # коэффициент пересчёта
|
|
||||||
self.dilution_factor = 1.0 # во сколько раз разбавляем (1 = без разбавления)
|
|
||||||
@@ -1 +1,2 @@
|
|||||||
PyQt5>=5.15.0
|
PyQt5>=5.15.0
|
||||||
|
numpy>=1.19.0
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""Цифровой помощник биохимика - основная библиотека"""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from .medium_controller import MediumController
|
||||||
|
from .experiment_controller import ExperimentController
|
||||||
|
|
||||||
|
__all__ = ['MediumController', 'ExperimentController']
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from ..models.experiment_model import ExperimentModel
|
||||||
|
|
||||||
|
class ExperimentController:
|
||||||
|
def __init__(self, view):
|
||||||
|
self.model = ExperimentModel()
|
||||||
|
self.view = view
|
||||||
|
|
||||||
|
def update_model_from_view(self):
|
||||||
|
factors = self.view.get_factors_data()
|
||||||
|
self.model.set_factors(factors)
|
||||||
|
|
||||||
|
responses = []
|
||||||
|
for row in range(self.view.responses_table.rowCount()):
|
||||||
|
name_item = self.view.responses_table.item(row, 0)
|
||||||
|
unit_item = self.view.responses_table.item(row, 1)
|
||||||
|
if name_item:
|
||||||
|
responses.append({
|
||||||
|
'name': name_item.text(),
|
||||||
|
'unit': unit_item.text() if unit_item else ""
|
||||||
|
})
|
||||||
|
self.model.set_responses(responses)
|
||||||
|
self.model.set_center_points(self.view.center_points_spin.value())
|
||||||
|
self.model.set_randomize(self.view.randomize_check.isChecked())
|
||||||
@@ -1,182 +1,118 @@
|
|||||||
from PyQt5.QtWidgets import QMessageBox, QFileDialog, QTableWidgetItem, QComboBox, QLineEdit, QDoubleSpinBox
|
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtGui import QColor
|
from PyQt5.QtGui import QColor
|
||||||
from model import Model
|
|
||||||
from view import MediumCalculatorWindow
|
|
||||||
import json
|
import json
|
||||||
from reagent import Reagent
|
from ..models.medium_model import MediumModel
|
||||||
|
from ..models.reagent import Reagent
|
||||||
|
|
||||||
|
class MediumController:
|
||||||
class Controller:
|
def __init__(self, view):
|
||||||
def __init__(self):
|
self.model = MediumModel()
|
||||||
self.model = Model()
|
self.view = view
|
||||||
self.view = MediumCalculatorWindow()
|
|
||||||
self._connect_signals()
|
self._connect_signals()
|
||||||
|
self._setup_initial_data()
|
||||||
|
|
||||||
def _connect_signals(self):
|
def _connect_signals(self):
|
||||||
"""Подключает обработчики событий интерфейса"""
|
|
||||||
self.view.add_row_btn.clicked.connect(self.add_reagent_row)
|
self.view.add_row_btn.clicked.connect(self.add_reagent_row)
|
||||||
self.view.remove_row_btn.clicked.connect(self.remove_reagent_row)
|
self.view.remove_row_btn.clicked.connect(self.remove_reagent_row)
|
||||||
self.view.calculate_btn.clicked.connect(self._perform_calculation)
|
self.view.calculate_btn.clicked.connect(self._perform_calculation)
|
||||||
self.view.save_btn.clicked.connect(self.save_composition)
|
self.view.save_btn.clicked.connect(self.save_composition)
|
||||||
self.view.load_btn.clicked.connect(self.load_composition)
|
self.view.load_btn.clicked.connect(self.load_composition)
|
||||||
self.view.solvent_input.textChanged.connect(self.view.update_solvent_name)
|
self.view.solvent_input.textChanged.connect(self.view.update_solvent_name)
|
||||||
|
|
||||||
|
def _setup_initial_data(self):
|
||||||
|
self.view.add_initial_rows()
|
||||||
|
|
||||||
def add_reagent_row(self):
|
def add_reagent_row(self):
|
||||||
"""Добавляет новую строку для реагента"""
|
|
||||||
self.view.add_new_row()
|
self.view.add_new_row()
|
||||||
|
|
||||||
def remove_reagent_row(self):
|
def remove_reagent_row(self):
|
||||||
"""Удаляет выбранную строку реагента"""
|
|
||||||
self.view.remove_selected_row()
|
self.view.remove_selected_row()
|
||||||
|
|
||||||
def _perform_calculation(self):
|
def _perform_calculation(self):
|
||||||
"""Выполняет расчёт и обновляет интерфейс"""
|
|
||||||
try:
|
try:
|
||||||
self._update_model_from_view()
|
self._update_model_from_view()
|
||||||
results, solvent_amount, solvent_percentage = self.model.calculate_amounts()
|
results, solvent_amount, solvent_percentage = self.model.calculate_amounts()
|
||||||
self.view.update_results(results)
|
self.view.update_results(results)
|
||||||
|
|
||||||
# Обновляем информацию о растворителе в первой строке таблицы
|
|
||||||
self.view.update_solvent_result(solvent_amount, self.model.amount_unit)
|
self.view.update_solvent_result(solvent_amount, self.model.amount_unit)
|
||||||
self.view.update_solvent_percent(solvent_percentage)
|
self.view.update_solvent_percent(solvent_percentage)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
self.view.show_error(f"Ошибка в данных: {str(e)}")
|
self.view.show_error(f"Ошибка в данных: {str(e)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.view.show_error(f"Неожиданная ошибка: {str(e)}")
|
self.view.show_error(f"Неожиданная ошибка: {str(e)}")
|
||||||
|
|
||||||
def _update_model_from_view(self):
|
def _update_model_from_view(self):
|
||||||
"""Обновляет модель данными из интерфейса (только реагенты, без растворителя)"""
|
|
||||||
# Очищаем список реагентов в модели
|
|
||||||
self.model.reagents.clear()
|
self.model.reagents.clear()
|
||||||
|
|
||||||
# Обновляем общее количество и единицу измерения
|
|
||||||
self.model.total_amount = self.view.amount_input.value()
|
self.model.total_amount = self.view.amount_input.value()
|
||||||
self.model.amount_unit = self.view.amount_unit_combo.currentText()
|
self.model.amount_unit = self.view.amount_unit_combo.currentText()
|
||||||
self.model.solvent = self.view.solvent_input.text()
|
self.model.solvent = self.view.solvent_input.text()
|
||||||
|
|
||||||
# Заполняем реагенты из таблицы (начиная с 1 строки, пропуская растворитель)
|
|
||||||
for row in range(1, self.view.table.rowCount()):
|
for row in range(1, self.view.table.rowCount()):
|
||||||
name_item = self.view.table.item(row, 0)
|
name_item = self.view.table.item(row, 0)
|
||||||
percentage_item = self.view.table.item(row, 1)
|
percentage_item = self.view.table.item(row, 1)
|
||||||
unit_widget = self.view.table.cellWidget(row, 2)
|
unit_widget = self.view.table.cellWidget(row, 2)
|
||||||
conversion_item = self.view.table.item(row, 3)
|
conversion_item = self.view.table.item(row, 3)
|
||||||
dilution_item = self.view.table.item(row, 4)
|
dilution_item = self.view.table.item(row, 4)
|
||||||
|
|
||||||
|
|
||||||
# Пропускаем строку, если какие-то обязательные поля отсутствуют
|
|
||||||
if not all([name_item, percentage_item, conversion_item]):
|
if not all([name_item, percentage_item, conversion_item]):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
name = name_item.text()
|
name = name_item.text()
|
||||||
percentage = float(percentage_item.text())
|
percentage = float(percentage_item.text())
|
||||||
unit = unit_widget.currentText() if unit_widget else "мг"
|
unit = unit_widget.currentText() if unit_widget else "мг"
|
||||||
conversion_factor = float(conversion_item.text())
|
conversion_factor = float(conversion_item.text())
|
||||||
dilution_factor = float(dilution_item.text())
|
dilution_factor = float(dilution_item.text()) if dilution_item else 1.0
|
||||||
|
reagent = Reagent(name, percentage, unit, conversion_factor)
|
||||||
self.model.add_reagent(name, percentage, unit, conversion_factor, dilution_factor)
|
reagent.dilution_factor = dilution_factor
|
||||||
|
self.model.reagents.append(reagent)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}")
|
raise ValueError(f"Ошибка в строке {row + 1}: {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}"))
|
|
||||||
|
|
||||||
# Единица - QComboBox
|
|
||||||
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()
|
|
||||||
def save_composition(self):
|
def save_composition(self):
|
||||||
"""Сохраняет состав среды в JSON-файл"""
|
filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)")
|
||||||
filename, _ = QFileDialog.getSaveFileName(
|
|
||||||
self.view,
|
|
||||||
"Сохранить состав среды",
|
|
||||||
"",
|
|
||||||
"JSON Files (*.json);;All Files (*)"
|
|
||||||
)
|
|
||||||
|
|
||||||
if filename:
|
if filename:
|
||||||
if not filename.lower().endswith('.json'):
|
if not filename.lower().endswith('.json'):
|
||||||
filename += '.json'
|
filename += '.json'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._update_model_from_view()
|
self._update_model_from_view()
|
||||||
self.model.save_to_file(filename)
|
self.model.save_to_file(filename)
|
||||||
QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!")
|
QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.view.show_error(f"Ошибка сохранения: {str(e)}")
|
self.view.show_error(f"Ошибка сохранения: {str(e)}")
|
||||||
|
|
||||||
def load_composition(self):
|
def load_composition(self):
|
||||||
"""Загружает состав среды из JSON-файла"""
|
filename, _ = QFileDialog.getOpenFileName(self.view, "Загрузить состав среды", "", "JSON Files (*.json);;All Files (*)")
|
||||||
filename, _ = QFileDialog.getOpenFileName(
|
|
||||||
self.view,
|
|
||||||
"Загрузить состав среды",
|
|
||||||
"",
|
|
||||||
"JSON Files (*.json);;All Files (*)"
|
|
||||||
)
|
|
||||||
|
|
||||||
if filename:
|
if filename:
|
||||||
try:
|
try:
|
||||||
self.model.load_from_file(filename)
|
self.model.load_from_file(filename)
|
||||||
self._update_view_from_model()
|
self._update_view_from_model()
|
||||||
QMessageBox.information(
|
QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен")
|
||||||
self.view,
|
|
||||||
"Успех",
|
|
||||||
"Состав среды успешно загружен"
|
|
||||||
)
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(self.view, "Ошибка", f"Файл не найден: {filename}")
|
||||||
self.view,
|
|
||||||
"Ошибка",
|
|
||||||
f"Файл не найден: {filename}"
|
|
||||||
)
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(self.view, "Ошибка", f"Неверный формат JSON-файла: {str(e)}")
|
||||||
self.view,
|
|
||||||
"Ошибка",
|
|
||||||
f"Неверный формат JSON-файла: {str(e)}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(self.view, "Ошибка", f"Ошибка при загрузке состава: {str(e)}")
|
||||||
self.view,
|
|
||||||
"Ошибка",
|
def _update_view_from_model(self):
|
||||||
f"Ошибка при загрузке состава: {str(e)}"
|
while self.view.table.rowCount() > 1:
|
||||||
)
|
self.view.table.removeRow(1)
|
||||||
|
if self.view.table.rowCount() == 0:
|
||||||
|
self.view.add_solvent_row()
|
||||||
|
self.view.amount_input.setValue(self.model.total_amount)
|
||||||
|
index = self.view.amount_unit_combo.findText(self.model.amount_unit)
|
||||||
|
if index >= 0:
|
||||||
|
self.view.amount_unit_combo.setCurrentIndex(index)
|
||||||
|
self.view.solvent_input.setText(self.model.solvent)
|
||||||
|
self.view.update_solvent_name()
|
||||||
|
for reagent in self.model.reagents:
|
||||||
|
row = self.view.table.rowCount()
|
||||||
|
self.view.table.insertRow(row)
|
||||||
|
self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name))
|
||||||
|
self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}"))
|
||||||
|
unit_combo = QComboBox()
|
||||||
|
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
|
||||||
|
unit_combo.setCurrentText(reagent.unit)
|
||||||
|
self.view.table.setCellWidget(row, 2, unit_combo)
|
||||||
|
self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}"))
|
||||||
|
self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}"))
|
||||||
|
self.view.table.setItem(row, 5, QTableWidgetItem(""))
|
||||||
|
self.view.clear_results()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .reagent import Reagent
|
||||||
|
from .medium_model import MediumModel
|
||||||
|
from .experiment_model import ExperimentModel
|
||||||
|
|
||||||
|
__all__ = ['Reagent', 'MediumModel', 'ExperimentModel']
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import numpy as np
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
class ExperimentModel:
|
||||||
|
def __init__(self):
|
||||||
|
self.factors = []
|
||||||
|
self.responses = []
|
||||||
|
self.center_points = 3
|
||||||
|
self.randomize = True
|
||||||
|
|
||||||
|
def set_factors(self, factors: List[Dict]):
|
||||||
|
self.factors = factors
|
||||||
|
|
||||||
|
def set_responses(self, responses: List[Dict]):
|
||||||
|
self.responses = responses
|
||||||
|
|
||||||
|
def set_center_points(self, n: int):
|
||||||
|
self.center_points = n
|
||||||
|
|
||||||
|
def set_randomize(self, value: bool):
|
||||||
|
self.randomize = value
|
||||||
|
|
||||||
|
def calculate_factorial_design(self) -> List[Dict]:
|
||||||
|
k = len(self.factors)
|
||||||
|
if k == 0:
|
||||||
|
return []
|
||||||
|
n_factorial = 2 ** k
|
||||||
|
design = []
|
||||||
|
for i in range(n_factorial):
|
||||||
|
experiment = {}
|
||||||
|
for j in range(k):
|
||||||
|
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1
|
||||||
|
natural_value = self.factors[j]['low'] if coded_level == -1 else self.factors[j]['high']
|
||||||
|
experiment[f"Фактор_{j+1}"] = {
|
||||||
|
'coded': coded_level,
|
||||||
|
'natural': natural_value,
|
||||||
|
'name': self.factors[j]['name'],
|
||||||
|
'unit': self.factors[j]['unit']
|
||||||
|
}
|
||||||
|
design.append(experiment)
|
||||||
|
for i in range(self.center_points):
|
||||||
|
center_experiment = {}
|
||||||
|
for j in range(k):
|
||||||
|
center_experiment[f"Фактор_{j+1}"] = {
|
||||||
|
'coded': 0,
|
||||||
|
'natural': self.factors[j]['center'],
|
||||||
|
'name': self.factors[j]['name'],
|
||||||
|
'unit': self.factors[j]['unit']
|
||||||
|
}
|
||||||
|
center_experiment['is_center'] = True
|
||||||
|
center_experiment['center_num'] = i + 1
|
||||||
|
design.append(center_experiment)
|
||||||
|
if self.randomize:
|
||||||
|
import random
|
||||||
|
random.shuffle(design)
|
||||||
|
return design
|
||||||
|
|
||||||
|
def analyze_results(self, results: List[List[float]], design: List[Dict]) -> Dict:
|
||||||
|
analysis = {}
|
||||||
|
for resp_idx, response in enumerate(self.responses):
|
||||||
|
resp_name = response.get('name', f'Отклик_{resp_idx+1}')
|
||||||
|
y_values = [results[i][resp_idx] for i in range(len(results))]
|
||||||
|
mean_y = np.mean(y_values)
|
||||||
|
variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0
|
||||||
|
std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0
|
||||||
|
cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0
|
||||||
|
factorial_y = []
|
||||||
|
center_y = []
|
||||||
|
for i, exp in enumerate(design):
|
||||||
|
if exp.get('is_center', False):
|
||||||
|
center_y.append(y_values[i])
|
||||||
|
else:
|
||||||
|
factorial_y.append(y_values[i])
|
||||||
|
center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0
|
||||||
|
analysis[resp_name] = {
|
||||||
|
'mean': mean_y, 'variance': variance, 'std_dev': std_dev, 'cv': cv,
|
||||||
|
'factorial_values': factorial_y, 'center_values': center_y, 'center_variance': center_variance,
|
||||||
|
'n_factorial': len(factorial_y), 'n_center': len(center_y)
|
||||||
|
}
|
||||||
|
return analysis
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import json
|
||||||
|
from typing import List, Tuple
|
||||||
|
from .reagent import Reagent
|
||||||
|
|
||||||
|
VOLUME_UNITS = {
|
||||||
|
'нл': 0.001, 'мкл': 1.0, 'мл': 1000.0, 'л': 1000000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
MASS_UNITS = {
|
||||||
|
'нг': 0.000001, 'мкг': 0.001, 'мг': 1.0, 'г': 1000.0, 'кг': 1000000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediumModel:
|
||||||
|
def __init__(self):
|
||||||
|
self.total_amount = 100.0
|
||||||
|
self.amount_unit = 'мл'
|
||||||
|
self.solvent = 'Вода'
|
||||||
|
self.reagents = []
|
||||||
|
|
||||||
|
def convert_amount(self, amount_base: float, target_unit: str, is_volume: bool) -> float:
|
||||||
|
if is_volume:
|
||||||
|
conversion_factor = VOLUME_UNITS.get(target_unit, 1.0)
|
||||||
|
else:
|
||||||
|
conversion_factor = MASS_UNITS.get(target_unit, 1.0)
|
||||||
|
return amount_base / conversion_factor
|
||||||
|
|
||||||
|
def calculate_amounts(self) -> Tuple[List[float], float, float]:
|
||||||
|
results = []
|
||||||
|
if not self.reagents:
|
||||||
|
return results, self.total_amount, 100.0
|
||||||
|
|
||||||
|
total_percentage = sum(r.percentage for r in self.reagents)
|
||||||
|
if total_percentage > 100:
|
||||||
|
raise ValueError(f"Сумма процентов реагентов ({total_percentage:.2f}%) превышает 100%")
|
||||||
|
|
||||||
|
total_in_base = self.total_amount * VOLUME_UNITS[self.amount_unit]
|
||||||
|
|
||||||
|
undiluted_amounts = []
|
||||||
|
for reagent in self.reagents:
|
||||||
|
amount_in_base = (reagent.percentage / 100) * total_in_base
|
||||||
|
is_volume = reagent.unit in VOLUME_UNITS
|
||||||
|
adjusted_amount = amount_in_base * reagent.conversion_factor
|
||||||
|
final_amount = self.convert_amount(adjusted_amount, reagent.unit, is_volume)
|
||||||
|
undiluted_amounts.append(final_amount)
|
||||||
|
|
||||||
|
diluted_amounts = []
|
||||||
|
total_diluted_volume_base = 0
|
||||||
|
for i, reagent in enumerate(self.reagents):
|
||||||
|
dilution_factor = getattr(reagent, 'dilution_factor', 1.0)
|
||||||
|
if dilution_factor <= 0:
|
||||||
|
dilution_factor = 1.0
|
||||||
|
diluted_amount = undiluted_amounts[i] * dilution_factor
|
||||||
|
diluted_amounts.append(diluted_amount)
|
||||||
|
is_volume = reagent.unit in VOLUME_UNITS
|
||||||
|
if is_volume:
|
||||||
|
reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.unit]
|
||||||
|
else:
|
||||||
|
reagent_volume_base = 0
|
||||||
|
total_diluted_volume_base += reagent_volume_base
|
||||||
|
|
||||||
|
solvent_volume_base = total_in_base - total_diluted_volume_base
|
||||||
|
solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit]
|
||||||
|
if solvent_amount < 0:
|
||||||
|
solvent_amount = 0
|
||||||
|
solvent_percentage = 100 - total_percentage
|
||||||
|
|
||||||
|
return diluted_amounts, solvent_amount, solvent_percentage
|
||||||
|
|
||||||
|
def save_to_file(self, filename: str):
|
||||||
|
data = {
|
||||||
|
'total_amount': self.total_amount,
|
||||||
|
'amount_unit': self.amount_unit,
|
||||||
|
'solvent': self.solvent,
|
||||||
|
'reagents': [{'name': r.name, 'percentage': r.percentage, 'unit': r.unit,
|
||||||
|
'conversion_factor': r.conversion_factor, 'dilution_factor': getattr(r, 'dilution_factor', 1.0)}
|
||||||
|
for r in self.reagents]
|
||||||
|
}
|
||||||
|
with open(filename, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
def load_from_file(self, filename: str):
|
||||||
|
with open(filename, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.total_amount = data['total_amount']
|
||||||
|
self.amount_unit = data['amount_unit']
|
||||||
|
self.solvent = data['solvent']
|
||||||
|
self.reagents.clear()
|
||||||
|
for r_data in data['reagents']:
|
||||||
|
reagent = Reagent(r_data['name'], r_data['percentage'], r_data['unit'], r_data.get('conversion_factor', 1.0))
|
||||||
|
reagent.dilution_factor = r_data.get('dilution_factor', 1.0)
|
||||||
|
self.reagents.append(reagent)
|
||||||
|
|
||||||
|
def add_reagent(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0, dilution_factor: float = 1.0):
|
||||||
|
reagent = Reagent(name, percentage, unit, conversion_factor)
|
||||||
|
reagent.dilution_factor = dilution_factor
|
||||||
|
self.reagents.append(reagent)
|
||||||
|
|
||||||
|
def remove_reagent(self, index: int):
|
||||||
|
if 0 <= index < len(self.reagents):
|
||||||
|
del self.reagents[index]
|
||||||
|
|
||||||
|
def clear_reagents(self):
|
||||||
|
self.reagents.clear()
|
||||||
|
|
||||||
|
def get_reagent_count(self) -> int:
|
||||||
|
return len(self.reagents)
|
||||||
|
|
||||||
|
def set_total_amount(self, amount: float, unit: str):
|
||||||
|
self.total_amount = amount
|
||||||
|
self.amount_unit = unit
|
||||||
|
|
||||||
|
def set_solvent(self, solvent_name: str):
|
||||||
|
self.solvent = solvent_name
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
class Reagent:
|
||||||
|
def __init__(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0):
|
||||||
|
self.name = name
|
||||||
|
self.percentage = percentage
|
||||||
|
self.unit = unit
|
||||||
|
self.conversion_factor = conversion_factor
|
||||||
|
self.dilution_factor = 1.0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Reagent(name={self.name}, percentage={self.percentage}, unit={self.unit})"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""Утилиты и вспомогательные функции"""
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .main_window import MainWindow
|
||||||
|
from .medium_view import MediumCalculatorWindow
|
||||||
|
from .experiment_view import ExperimentDesignWindow
|
||||||
|
|
||||||
|
__all__ = ['MainWindow', 'MediumCalculatorWindow', 'ExperimentDesignWindow']
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
|
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
|
||||||
QPushButton, QLabel, QWidget, QMessageBox,
|
QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox,
|
||||||
QTableWidget, QTableWidgetItem, QGroupBox,
|
QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, QTextEdit,
|
||||||
QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit,
|
QTabWidget, QFormLayout, QCheckBox, QScrollArea, QFileDialog)
|
||||||
QTextEdit, QTabWidget, QFormLayout, QCheckBox,
|
|
||||||
QScrollArea, QFileDialog)
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtGui import QColor, QFont
|
from PyQt5.QtGui import QColor, QFont
|
||||||
import numpy as np
|
|
||||||
import csv
|
import csv
|
||||||
|
import random
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
class ExperimentDesignWindow(QMainWindow):
|
class ExperimentDesignWindow(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -16,53 +14,24 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика")
|
self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика")
|
||||||
self.setGeometry(200, 100, 1200, 800)
|
self.setGeometry(200, 100, 1200, 800)
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
QMainWindow {
|
QMainWindow { background-color: #f5f5f5; }
|
||||||
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; }
|
||||||
QGroupBox {
|
QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; }
|
||||||
font-weight: bold;
|
QPushButton:hover { background-color: #45a049; }
|
||||||
border: 2px solid #ccc;
|
QPushButton#danger { background-color: #f44336; }
|
||||||
border-radius: 5px;
|
QPushButton#danger:hover { background-color: #da190b; }
|
||||||
margin-top: 10px;
|
QTableWidget { gridline-color: #ddd; }
|
||||||
padding-top: 10px;
|
QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; }
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
QTableWidget {
|
|
||||||
gridline-color: #ddd;
|
|
||||||
}
|
|
||||||
QHeaderView::section {
|
|
||||||
background-color: #1565C0;
|
|
||||||
color: white;
|
|
||||||
padding: 8px;
|
|
||||||
border: none;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
""")
|
""")
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
|
|
||||||
def _init_ui(self):
|
def _init_ui(self):
|
||||||
central_widget = QWidget()
|
central_widget = QWidget()
|
||||||
self.setCentralWidget(central_widget)
|
self.setCentralWidget(central_widget)
|
||||||
layout = QVBoxLayout(central_widget)
|
layout = QVBoxLayout(central_widget)
|
||||||
|
|
||||||
# Заголовок
|
title_label = QLabel("Планирование полнофакторного эксперимента (DoE)")
|
||||||
title_label = QLabel("📈 Планирование полнофакторного эксперимента (DoE)")
|
|
||||||
title_font = QFont()
|
title_font = QFont()
|
||||||
title_font.setPointSize(18)
|
title_font.setPointSize(18)
|
||||||
title_font.setBold(True)
|
title_font.setBold(True)
|
||||||
@@ -70,15 +39,13 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
title_label.setAlignment(Qt.AlignCenter)
|
title_label.setAlignment(Qt.AlignCenter)
|
||||||
title_label.setStyleSheet("color: #2E7D32;")
|
title_label.setStyleSheet("color: #2E7D32;")
|
||||||
layout.addWidget(title_label)
|
layout.addWidget(title_label)
|
||||||
|
|
||||||
# Вкладки
|
|
||||||
tabs = QTabWidget()
|
tabs = QTabWidget()
|
||||||
|
|
||||||
# Вкладка 1: Параметры эксперимента
|
# Вкладка параметров
|
||||||
params_tab = QWidget()
|
params_tab = QWidget()
|
||||||
params_layout = QVBoxLayout(params_tab)
|
params_layout = QVBoxLayout(params_tab)
|
||||||
|
|
||||||
# Группа факторов
|
|
||||||
factors_group = QGroupBox("Факторы эксперимента (независимые переменные)")
|
factors_group = QGroupBox("Факторы эксперимента (независимые переменные)")
|
||||||
factors_layout = QVBoxLayout()
|
factors_layout = QVBoxLayout()
|
||||||
|
|
||||||
@@ -87,27 +54,20 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
factors_layout.addWidget(info_label)
|
factors_layout.addWidget(info_label)
|
||||||
|
|
||||||
self.factors_table = QTableWidget()
|
self.factors_table = QTableWidget()
|
||||||
# Новый порядок колонок: Фактор, Нулевой уровень, Шаг, Верхний (+1), Нижний (-1), Единица измерения
|
|
||||||
self.factors_table.setColumnCount(6)
|
self.factors_table.setColumnCount(6)
|
||||||
self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг",
|
self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг",
|
||||||
"Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"])
|
"Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"])
|
||||||
self.factors_table.setRowCount(2)
|
self.factors_table.setRowCount(2)
|
||||||
|
|
||||||
# Пример данных
|
sample_factors = [["Температура", "31", "6", "37", "25", "°C"], ["pH", "7.0", "0.5", "7.5", "6.5", ""]]
|
||||||
sample_factors = [
|
|
||||||
["Температура", "31", "6", "37", "25", "°C"],
|
|
||||||
["pH", "7.0", "0.5", "7.5", "6.5", ""],
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, factor in enumerate(sample_factors):
|
for i, factor in enumerate(sample_factors):
|
||||||
for j, value in enumerate(factor):
|
for j, value in enumerate(factor):
|
||||||
item = QTableWidgetItem(value)
|
item = QTableWidgetItem(value)
|
||||||
if j in [3, 4]: # Верхний и нижний уровень - только для чтения
|
if j in [3, 4]:
|
||||||
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
||||||
item.setBackground(QColor(240, 240, 240))
|
item.setBackground(QColor(240, 240, 240))
|
||||||
self.factors_table.setItem(i, j, item)
|
self.factors_table.setItem(i, j, item)
|
||||||
|
|
||||||
# Настройка ширины колонок
|
|
||||||
self.factors_table.setColumnWidth(0, 150)
|
self.factors_table.setColumnWidth(0, 150)
|
||||||
self.factors_table.setColumnWidth(1, 120)
|
self.factors_table.setColumnWidth(1, 120)
|
||||||
self.factors_table.setColumnWidth(2, 80)
|
self.factors_table.setColumnWidth(2, 80)
|
||||||
@@ -115,12 +75,9 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
self.factors_table.setColumnWidth(4, 120)
|
self.factors_table.setColumnWidth(4, 120)
|
||||||
self.factors_table.setColumnWidth(5, 100)
|
self.factors_table.setColumnWidth(5, 100)
|
||||||
|
|
||||||
# Подключаем сигналы изменения ячеек
|
|
||||||
self.factors_table.itemChanged.connect(self.on_factor_changed)
|
self.factors_table.itemChanged.connect(self.on_factor_changed)
|
||||||
|
|
||||||
factors_layout.addWidget(self.factors_table)
|
factors_layout.addWidget(self.factors_table)
|
||||||
|
|
||||||
# Кнопки для управления факторами
|
|
||||||
factor_buttons = QHBoxLayout()
|
factor_buttons = QHBoxLayout()
|
||||||
add_factor_btn = QPushButton("+ Добавить фактор")
|
add_factor_btn = QPushButton("+ Добавить фактор")
|
||||||
add_factor_btn.clicked.connect(self.add_factor_row)
|
add_factor_btn.clicked.connect(self.add_factor_row)
|
||||||
@@ -130,49 +87,35 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
factor_buttons.addWidget(remove_factor_btn)
|
factor_buttons.addWidget(remove_factor_btn)
|
||||||
factor_buttons.addStretch()
|
factor_buttons.addStretch()
|
||||||
factors_layout.addLayout(factor_buttons)
|
factors_layout.addLayout(factor_buttons)
|
||||||
|
|
||||||
factors_group.setLayout(factors_layout)
|
factors_group.setLayout(factors_layout)
|
||||||
params_layout.addWidget(factors_group)
|
params_layout.addWidget(factors_group)
|
||||||
|
|
||||||
# Группа настроек эксперимента
|
|
||||||
settings_group = QGroupBox("Настройки эксперимента")
|
settings_group = QGroupBox("Настройки эксперимента")
|
||||||
settings_layout = QHBoxLayout()
|
settings_layout = QHBoxLayout()
|
||||||
|
|
||||||
center_layout = QHBoxLayout()
|
center_layout = QHBoxLayout()
|
||||||
center_layout.addWidget(QLabel("Количество центральных точек:"))
|
center_layout.addWidget(QLabel("Количество центральных точек:"))
|
||||||
self.center_points_spin = QSpinBox()
|
self.center_points_spin = QSpinBox()
|
||||||
self.center_points_spin.setRange(0, 10)
|
self.center_points_spin.setRange(0, 10)
|
||||||
self.center_points_spin.setValue(3)
|
self.center_points_spin.setValue(3)
|
||||||
self.center_points_spin.setToolTip("Повторные опыты в нулевой точке для оценки дисперсии")
|
|
||||||
center_layout.addWidget(self.center_points_spin)
|
center_layout.addWidget(self.center_points_spin)
|
||||||
settings_layout.addLayout(center_layout)
|
settings_layout.addLayout(center_layout)
|
||||||
|
|
||||||
self.randomize_check = QCheckBox("Рэндомизировать порядок опытов")
|
self.randomize_check = QCheckBox("Рэндомизировать порядок опытов")
|
||||||
self.randomize_check.setChecked(True)
|
self.randomize_check.setChecked(True)
|
||||||
settings_layout.addWidget(self.randomize_check)
|
settings_layout.addWidget(self.randomize_check)
|
||||||
|
|
||||||
settings_layout.addStretch()
|
settings_layout.addStretch()
|
||||||
settings_group.setLayout(settings_layout)
|
settings_group.setLayout(settings_layout)
|
||||||
params_layout.addWidget(settings_group)
|
params_layout.addWidget(settings_group)
|
||||||
|
|
||||||
# Группа откликов
|
|
||||||
responses_group = QGroupBox("Отклики (зависимые переменные)")
|
responses_group = QGroupBox("Отклики (зависимые переменные)")
|
||||||
responses_layout = QVBoxLayout()
|
responses_layout = QVBoxLayout()
|
||||||
|
|
||||||
self.responses_table = QTableWidget()
|
self.responses_table = QTableWidget()
|
||||||
self.responses_table.setColumnCount(2)
|
self.responses_table.setColumnCount(2)
|
||||||
self.responses_table.setHorizontalHeaderLabels(["Отклик", "Единица измерения"])
|
self.responses_table.setHorizontalHeaderLabels(["Отклик", "Единица измерения"])
|
||||||
self.responses_table.setRowCount(2)
|
self.responses_table.setRowCount(2)
|
||||||
|
sample_responses = [["Оптическая плотность (OD600)", ""], ["Концентрация целевого продукта", "мг/мл"]]
|
||||||
sample_responses = [
|
|
||||||
["Оптическая плотность (OD600)", ""],
|
|
||||||
["Концентрация целевого продукта", "мг/мл"]
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, response in enumerate(sample_responses):
|
for i, response in enumerate(sample_responses):
|
||||||
for j, value in enumerate(response):
|
for j, value in enumerate(response):
|
||||||
self.responses_table.setItem(i, j, QTableWidgetItem(value))
|
self.responses_table.setItem(i, j, QTableWidgetItem(value))
|
||||||
|
|
||||||
responses_layout.addWidget(self.responses_table)
|
responses_layout.addWidget(self.responses_table)
|
||||||
|
|
||||||
response_buttons = QHBoxLayout()
|
response_buttons = QHBoxLayout()
|
||||||
@@ -184,16 +127,13 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
response_buttons.addWidget(remove_response_btn)
|
response_buttons.addWidget(remove_response_btn)
|
||||||
response_buttons.addStretch()
|
response_buttons.addStretch()
|
||||||
responses_layout.addLayout(response_buttons)
|
responses_layout.addLayout(response_buttons)
|
||||||
|
|
||||||
responses_group.setLayout(responses_layout)
|
responses_group.setLayout(responses_layout)
|
||||||
params_layout.addWidget(responses_group)
|
params_layout.addWidget(responses_group)
|
||||||
|
|
||||||
tabs.addTab(params_tab, "📝 Параметры эксперимента")
|
tabs.addTab(params_tab, "📝 Параметры эксперимента")
|
||||||
|
|
||||||
# Вкладка 2: Матрица планирования
|
# Вкладка матрицы планирования
|
||||||
plan_tab = QWidget()
|
plan_tab = QWidget()
|
||||||
plan_layout = QVBoxLayout(plan_tab)
|
plan_layout = QVBoxLayout(plan_tab)
|
||||||
|
|
||||||
plan_info = QLabel("Полнофакторный план эксперимента с центральными точками")
|
plan_info = QLabel("Полнофакторный план эксперимента с центральными точками")
|
||||||
plan_info.setAlignment(Qt.AlignCenter)
|
plan_info.setAlignment(Qt.AlignCenter)
|
||||||
plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
|
plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
|
||||||
@@ -203,145 +143,105 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
scroll.setWidgetResizable(True)
|
scroll.setWidgetResizable(True)
|
||||||
matrix_widget = QWidget()
|
matrix_widget = QWidget()
|
||||||
matrix_layout = QVBoxLayout(matrix_widget)
|
matrix_layout = QVBoxLayout(matrix_widget)
|
||||||
|
|
||||||
self.design_matrix = QTableWidget()
|
self.design_matrix = QTableWidget()
|
||||||
matrix_layout.addWidget(self.design_matrix)
|
matrix_layout.addWidget(self.design_matrix)
|
||||||
|
|
||||||
scroll.setWidget(matrix_widget)
|
scroll.setWidget(matrix_widget)
|
||||||
plan_layout.addWidget(scroll)
|
plan_layout.addWidget(scroll)
|
||||||
|
|
||||||
buttons_layout = QHBoxLayout()
|
buttons_layout = QHBoxLayout()
|
||||||
|
generate_btn = QPushButton("Сгенерировать план эксперимента")
|
||||||
generate_btn = QPushButton("🔄 Сгенерировать план эксперимента")
|
|
||||||
generate_btn.clicked.connect(self.generate_design_matrix)
|
generate_btn.clicked.connect(self.generate_design_matrix)
|
||||||
buttons_layout.addWidget(generate_btn)
|
buttons_layout.addWidget(generate_btn)
|
||||||
|
|
||||||
export_btn = QPushButton("📊 Экспорт в CSV")
|
export_btn = QPushButton("📊 Экспорт в CSV")
|
||||||
export_btn.clicked.connect(self.export_to_csv)
|
export_btn.clicked.connect(self.export_to_csv)
|
||||||
buttons_layout.addWidget(export_btn)
|
buttons_layout.addWidget(export_btn)
|
||||||
|
|
||||||
buttons_layout.addStretch()
|
buttons_layout.addStretch()
|
||||||
plan_layout.addLayout(buttons_layout)
|
plan_layout.addLayout(buttons_layout)
|
||||||
|
|
||||||
self.plan_info_label = QLabel("")
|
self.plan_info_label = QLabel("")
|
||||||
self.plan_info_label.setStyleSheet("color: #666; font-size: 12px; padding: 5px;")
|
self.plan_info_label.setStyleSheet("color: #666; font-size: 12px; padding: 5px;")
|
||||||
plan_layout.addWidget(self.plan_info_label)
|
plan_layout.addWidget(self.plan_info_label)
|
||||||
|
|
||||||
tabs.addTab(plan_tab, "📊 Матрица планирования")
|
tabs.addTab(plan_tab, "📊 Матрица планирования")
|
||||||
|
|
||||||
# Вкладка 3: Анализ результатов
|
# Вкладка анализа
|
||||||
analysis_tab = QWidget()
|
analysis_tab = QWidget()
|
||||||
analysis_layout = QVBoxLayout(analysis_tab)
|
analysis_layout = QVBoxLayout(analysis_tab)
|
||||||
|
|
||||||
analysis_info = QLabel("Введите результаты экспериментов для анализа")
|
analysis_info = QLabel("Введите результаты экспериментов для анализа")
|
||||||
analysis_info.setAlignment(Qt.AlignCenter)
|
analysis_info.setAlignment(Qt.AlignCenter)
|
||||||
analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
|
analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
|
||||||
analysis_layout.addWidget(analysis_info)
|
analysis_layout.addWidget(analysis_info)
|
||||||
|
|
||||||
self.results_table = QTableWidget()
|
self.results_table = QTableWidget()
|
||||||
analysis_layout.addWidget(self.results_table)
|
analysis_layout.addWidget(self.results_table)
|
||||||
|
analyze_btn = QPushButton("Провести регрессионный анализ")
|
||||||
analyze_btn = QPushButton("📈 Провести регрессионный анализ")
|
|
||||||
analyze_btn.clicked.connect(self.perform_analysis)
|
analyze_btn.clicked.connect(self.perform_analysis)
|
||||||
analysis_layout.addWidget(analyze_btn)
|
analysis_layout.addWidget(analyze_btn)
|
||||||
|
|
||||||
self.analysis_output = QTextEdit()
|
self.analysis_output = QTextEdit()
|
||||||
self.analysis_output.setReadOnly(True)
|
self.analysis_output.setReadOnly(True)
|
||||||
self.analysis_output.setMaximumHeight(200)
|
self.analysis_output.setMaximumHeight(200)
|
||||||
analysis_layout.addWidget(self.analysis_output)
|
analysis_layout.addWidget(self.analysis_output)
|
||||||
|
tabs.addTab(analysis_tab, "📈 Анализ результатов")
|
||||||
tabs.addTab(analysis_tab, "📐 Анализ результатов")
|
|
||||||
|
|
||||||
layout.addWidget(tabs)
|
layout.addWidget(tabs)
|
||||||
|
|
||||||
btn_layout = QHBoxLayout()
|
btn_layout = QHBoxLayout()
|
||||||
|
close_btn = QPushButton("Закрыть")
|
||||||
save_btn = QPushButton("💿 Сохранить проект")
|
|
||||||
save_btn.clicked.connect(self.show_placeholder_message)
|
|
||||||
btn_layout.addWidget(save_btn)
|
|
||||||
|
|
||||||
load_btn = QPushButton("📂 Загрузить проект")
|
|
||||||
load_btn.clicked.connect(self.show_placeholder_message)
|
|
||||||
btn_layout.addWidget(load_btn)
|
|
||||||
|
|
||||||
btn_layout.addStretch()
|
|
||||||
|
|
||||||
close_btn = QPushButton("❌ Закрыть")
|
|
||||||
close_btn.clicked.connect(self.close)
|
close_btn.clicked.connect(self.close)
|
||||||
|
btn_layout.addStretch()
|
||||||
btn_layout.addWidget(close_btn)
|
btn_layout.addWidget(close_btn)
|
||||||
|
|
||||||
layout.addLayout(btn_layout)
|
layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
self.generated_design = None
|
||||||
|
self.factors_data = None
|
||||||
|
|
||||||
def on_factor_changed(self, item):
|
def on_factor_changed(self, item):
|
||||||
"""При изменении нулевого уровня или шага пересчитываем верхний и нижний уровни"""
|
|
||||||
row = item.row()
|
row = item.row()
|
||||||
col = item.column()
|
col = item.column()
|
||||||
|
|
||||||
# Если изменили нулевой уровень (колонка 1) или шаг (колонка 2)
|
|
||||||
if col in [1, 2]:
|
if col in [1, 2]:
|
||||||
try:
|
try:
|
||||||
center = float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0
|
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
|
step = float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0
|
||||||
|
|
||||||
# Пересчитываем верхний и нижний уровни
|
|
||||||
high = center + step
|
high = center + step
|
||||||
low = center - step
|
low = center - step
|
||||||
|
|
||||||
# Обновляем ячейки (временно отключаем сигнал)
|
|
||||||
self.factors_table.blockSignals(True)
|
self.factors_table.blockSignals(True)
|
||||||
|
if self.factors_table.item(row, 3):
|
||||||
high_item = self.factors_table.item(row, 3)
|
self.factors_table.item(row, 3).setText(f"{high:.3f}".rstrip('0').rstrip('.'))
|
||||||
if high_item:
|
if self.factors_table.item(row, 4):
|
||||||
high_item.setText(f"{high:.3f}".rstrip('0').rstrip('.'))
|
self.factors_table.item(row, 4).setText(f"{low:.3f}".rstrip('0').rstrip('.'))
|
||||||
|
|
||||||
low_item = self.factors_table.item(row, 4)
|
|
||||||
if low_item:
|
|
||||||
low_item.setText(f"{low:.3f}".rstrip('0').rstrip('.'))
|
|
||||||
|
|
||||||
self.factors_table.blockSignals(False)
|
self.factors_table.blockSignals(False)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_factor_row(self):
|
def add_factor_row(self):
|
||||||
"""Добавляет строку для нового фактора"""
|
|
||||||
row = self.factors_table.rowCount()
|
row = self.factors_table.rowCount()
|
||||||
self.factors_table.insertRow(row)
|
self.factors_table.insertRow(row)
|
||||||
self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}"))
|
self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}"))
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
|
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
|
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
|
||||||
|
|
||||||
# Верхний и нижний уровни - только для чтения
|
|
||||||
high_item = QTableWidgetItem("1")
|
high_item = QTableWidgetItem("1")
|
||||||
high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable)
|
high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable)
|
||||||
high_item.setBackground(QColor(240, 240, 240))
|
high_item.setBackground(QColor(240, 240, 240))
|
||||||
self.factors_table.setItem(row, 3, high_item)
|
self.factors_table.setItem(row, 3, high_item)
|
||||||
|
|
||||||
low_item = QTableWidgetItem("-1")
|
low_item = QTableWidgetItem("-1")
|
||||||
low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable)
|
low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable)
|
||||||
low_item.setBackground(QColor(240, 240, 240))
|
low_item.setBackground(QColor(240, 240, 240))
|
||||||
self.factors_table.setItem(row, 4, low_item)
|
self.factors_table.setItem(row, 4, low_item)
|
||||||
|
|
||||||
self.factors_table.setItem(row, 5, QTableWidgetItem(""))
|
self.factors_table.setItem(row, 5, QTableWidgetItem(""))
|
||||||
|
|
||||||
def remove_factor_row(self):
|
def remove_factor_row(self):
|
||||||
"""Удаляет последнюю строку факторов"""
|
|
||||||
if self.factors_table.rowCount() > 1:
|
if self.factors_table.rowCount() > 1:
|
||||||
self.factors_table.removeRow(self.factors_table.rowCount() - 1)
|
self.factors_table.removeRow(self.factors_table.rowCount() - 1)
|
||||||
|
|
||||||
def add_response_row(self):
|
def add_response_row(self):
|
||||||
"""Добавляет строку для нового отклика"""
|
|
||||||
row = self.responses_table.rowCount()
|
row = self.responses_table.rowCount()
|
||||||
self.responses_table.insertRow(row)
|
self.responses_table.insertRow(row)
|
||||||
self.responses_table.setItem(row, 0, QTableWidgetItem(f"Отклик_{row+1}"))
|
self.responses_table.setItem(row, 0, QTableWidgetItem(f"Отклик_{row+1}"))
|
||||||
self.responses_table.setItem(row, 1, QTableWidgetItem(""))
|
self.responses_table.setItem(row, 1, QTableWidgetItem(""))
|
||||||
|
|
||||||
def remove_response_row(self):
|
def remove_response_row(self):
|
||||||
"""Удаляет последнюю строку откликов"""
|
|
||||||
if self.responses_table.rowCount() > 1:
|
if self.responses_table.rowCount() > 1:
|
||||||
self.responses_table.removeRow(self.responses_table.rowCount() - 1)
|
self.responses_table.removeRow(self.responses_table.rowCount() - 1)
|
||||||
|
|
||||||
def get_factors_data(self):
|
def get_factors_data(self):
|
||||||
"""Получает данные о факторах"""
|
|
||||||
factors = []
|
factors = []
|
||||||
for row in range(self.factors_table.rowCount()):
|
for row in range(self.factors_table.rowCount()):
|
||||||
try:
|
try:
|
||||||
@@ -357,29 +257,18 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
continue
|
continue
|
||||||
return factors
|
return factors
|
||||||
|
|
||||||
def calculate_factorial_design(self, factors):
|
def calculate_factorial_design(self, factors):
|
||||||
"""Генерирует полнофакторный план 2^k с центральными точками"""
|
|
||||||
k = len(factors)
|
k = len(factors)
|
||||||
if k == 0:
|
if k == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Генерируем 2^k комбинаций
|
|
||||||
n_factorial = 2 ** k
|
n_factorial = 2 ** k
|
||||||
design = []
|
design = []
|
||||||
|
|
||||||
for i in range(n_factorial):
|
for i in range(n_factorial):
|
||||||
experiment = {}
|
experiment = {}
|
||||||
for j in range(k):
|
for j in range(k):
|
||||||
# Кодированный уровень (-1 или +1)
|
|
||||||
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1
|
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']
|
||||||
# Переводим в натуральные значения
|
|
||||||
if coded_level == -1:
|
|
||||||
natural_value = factors[j]['low']
|
|
||||||
else:
|
|
||||||
natural_value = factors[j]['high']
|
|
||||||
|
|
||||||
experiment[f"Фактор_{j+1}"] = {
|
experiment[f"Фактор_{j+1}"] = {
|
||||||
'coded': coded_level,
|
'coded': coded_level,
|
||||||
'natural': natural_value,
|
'natural': natural_value,
|
||||||
@@ -388,7 +277,6 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
}
|
}
|
||||||
design.append(experiment)
|
design.append(experiment)
|
||||||
|
|
||||||
# Добавляем центральные точки
|
|
||||||
n_center = self.center_points_spin.value()
|
n_center = self.center_points_spin.value()
|
||||||
for i in range(n_center):
|
for i in range(n_center):
|
||||||
center_experiment = {}
|
center_experiment = {}
|
||||||
@@ -403,48 +291,34 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
center_experiment['center_num'] = i + 1
|
center_experiment['center_num'] = i + 1
|
||||||
design.append(center_experiment)
|
design.append(center_experiment)
|
||||||
|
|
||||||
# Рэндомизация порядка опытов
|
|
||||||
if self.randomize_check.isChecked():
|
if self.randomize_check.isChecked():
|
||||||
import random
|
|
||||||
random.shuffle(design)
|
random.shuffle(design)
|
||||||
|
|
||||||
return design
|
return design
|
||||||
|
|
||||||
def generate_design_matrix(self):
|
def generate_design_matrix(self):
|
||||||
"""Генерирует и отображает матрицу планирования"""
|
|
||||||
factors = self.get_factors_data()
|
factors = self.get_factors_data()
|
||||||
|
|
||||||
if len(factors) == 0:
|
if len(factors) == 0:
|
||||||
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
|
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Генерируем план
|
self.factors_data = factors
|
||||||
design = self.calculate_factorial_design(factors)
|
design = self.calculate_factorial_design(factors)
|
||||||
|
self.generated_design = design
|
||||||
|
|
||||||
# Количество опытов
|
|
||||||
n_experiments = len(design)
|
n_experiments = len(design)
|
||||||
n_factors = len(factors)
|
n_factors = len(factors)
|
||||||
|
|
||||||
# Настройка таблицы
|
|
||||||
self.design_matrix.setRowCount(n_experiments)
|
self.design_matrix.setRowCount(n_experiments)
|
||||||
self.design_matrix.setColumnCount(n_factors + 2)
|
self.design_matrix.setColumnCount(n_factors + 2)
|
||||||
|
|
||||||
# Заголовки
|
|
||||||
headers = ["№ опыта"] + [f["name"] for f in factors] + ["Тип точки"]
|
headers = ["№ опыта"] + [f["name"] for f in factors] + ["Тип точки"]
|
||||||
self.design_matrix.setHorizontalHeaderLabels(headers)
|
self.design_matrix.setHorizontalHeaderLabels(headers)
|
||||||
|
|
||||||
# Заполняем матрицу
|
|
||||||
for exp_idx, experiment in enumerate(design):
|
for exp_idx, experiment in enumerate(design):
|
||||||
# Номер опыта
|
|
||||||
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
|
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
|
||||||
|
|
||||||
# Значения факторов
|
|
||||||
for factor_idx in range(n_factors):
|
for factor_idx in range(n_factors):
|
||||||
factor_key = f"Фактор_{factor_idx + 1}"
|
factor_key = f"Фактор_{factor_idx + 1}"
|
||||||
value = experiment[factor_key]['natural']
|
value = experiment[factor_key]['natural']
|
||||||
unit = factors[factor_idx]['unit']
|
unit = factors[factor_idx]['unit']
|
||||||
|
|
||||||
# Форматируем значение
|
|
||||||
if isinstance(value, float):
|
if isinstance(value, float):
|
||||||
if value == int(value):
|
if value == int(value):
|
||||||
display_value = str(int(value))
|
display_value = str(int(value))
|
||||||
@@ -452,123 +326,76 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
display_value = f"{value:.3f}".rstrip('0').rstrip('.')
|
display_value = f"{value:.3f}".rstrip('0').rstrip('.')
|
||||||
else:
|
else:
|
||||||
display_value = str(value)
|
display_value = str(value)
|
||||||
|
|
||||||
if unit:
|
if unit:
|
||||||
display_value += f" {unit}"
|
display_value += f" {unit}"
|
||||||
|
|
||||||
item = QTableWidgetItem(display_value)
|
item = QTableWidgetItem(display_value)
|
||||||
|
|
||||||
# Подсветка центральных точек
|
|
||||||
if experiment.get('is_center', False):
|
if experiment.get('is_center', False):
|
||||||
item.setBackground(QColor(255, 255, 200))
|
item.setBackground(QColor(255, 255, 200))
|
||||||
|
|
||||||
self.design_matrix.setItem(exp_idx, factor_idx + 1, item)
|
self.design_matrix.setItem(exp_idx, factor_idx + 1, item)
|
||||||
|
|
||||||
# Тип точки
|
|
||||||
if experiment.get('is_center', False):
|
if experiment.get('is_center', False):
|
||||||
type_item = QTableWidgetItem(f"Центральная #{experiment['center_num']}")
|
type_item = QTableWidgetItem(f"Центральная #{experiment['center_num']}")
|
||||||
type_item.setBackground(QColor(255, 255, 200))
|
type_item.setBackground(QColor(255, 255, 200))
|
||||||
else:
|
else:
|
||||||
# Показываем комбинацию уровней
|
|
||||||
levels = []
|
levels = []
|
||||||
for factor_idx in range(n_factors):
|
for factor_idx in range(n_factors):
|
||||||
factor_key = f"Фактор_{factor_idx + 1}"
|
factor_key = f"Фактор_{factor_idx + 1}"
|
||||||
coded = experiment[factor_key]['coded']
|
coded = experiment[factor_key]['coded']
|
||||||
levels.append("+" if coded == 1 else "-")
|
levels.append("+" if coded == 1 else "-")
|
||||||
type_item = QTableWidgetItem(f"Факторная ({''.join(levels)})")
|
type_item = QTableWidgetItem(f"Факторная ({''.join(levels)})")
|
||||||
|
|
||||||
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
|
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
|
||||||
|
|
||||||
# Настройка ширины колонок
|
|
||||||
self.design_matrix.resizeColumnsToContents()
|
self.design_matrix.resizeColumnsToContents()
|
||||||
|
|
||||||
# Обновляем информацию
|
|
||||||
n_factorial = 2 ** n_factors
|
n_factorial = 2 ** n_factors
|
||||||
n_center = self.center_points_spin.value()
|
n_center = self.center_points_spin.value()
|
||||||
self.plan_info_label.setText(
|
self.plan_info_label.setText(f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов")
|
||||||
f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Настраиваем таблицу для ввода результатов
|
|
||||||
self.setup_results_table(n_experiments)
|
self.setup_results_table(n_experiments)
|
||||||
|
QMessageBox.information(self, "Успех", f"Сгенерирован план для {n_factors} факторов\nФакторных точек: {n_factorial}\nЦентральных точек: {n_center}\nВсего опытов: {n_experiments}")
|
||||||
QMessageBox.information(self, "Успех",
|
|
||||||
f"Сгенерирован план для {n_factors} факторов\n"
|
|
||||||
f"Факторных точек: {n_factorial}\n"
|
|
||||||
f"Центральных точек: {n_center}\n"
|
|
||||||
f"Всего опытов: {n_experiments}\n\n"
|
|
||||||
f"Центральные точки позволяют оценить дисперсию воспроизводимости")
|
|
||||||
|
|
||||||
def setup_results_table(self, n_experiments):
|
def setup_results_table(self, n_experiments):
|
||||||
"""Настраивает таблицу для ввода результатов"""
|
|
||||||
n_responses = self.responses_table.rowCount()
|
n_responses = self.responses_table.rowCount()
|
||||||
|
|
||||||
self.results_table.setRowCount(n_experiments)
|
self.results_table.setRowCount(n_experiments)
|
||||||
self.results_table.setColumnCount(n_responses + 1)
|
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)]
|
||||||
# Заголовки
|
|
||||||
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)
|
self.results_table.setHorizontalHeaderLabels(headers)
|
||||||
|
|
||||||
# Заполняем номера опытов
|
|
||||||
for i in range(n_experiments):
|
for i in range(n_experiments):
|
||||||
self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1)))
|
self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1)))
|
||||||
|
|
||||||
# Настройка ширины колонок
|
|
||||||
self.results_table.setColumnWidth(0, 80)
|
self.results_table.setColumnWidth(0, 80)
|
||||||
for i in range(n_responses):
|
for i in range(n_responses):
|
||||||
self.results_table.setColumnWidth(i + 1, 150)
|
self.results_table.setColumnWidth(i + 1, 150)
|
||||||
|
|
||||||
def export_to_csv(self):
|
def export_to_csv(self):
|
||||||
"""Экспортирует матрицу планирования в CSV"""
|
|
||||||
if self.design_matrix.rowCount() == 0:
|
if self.design_matrix.rowCount() == 0:
|
||||||
QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!")
|
QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!")
|
||||||
return
|
return
|
||||||
|
filename, _ = QFileDialog.getSaveFileName(self, "Сохранить план эксперимента", "", "CSV Files (*.csv);;All Files (*)")
|
||||||
filename, _ = QFileDialog.getSaveFileName(
|
|
||||||
self,
|
|
||||||
"Сохранить план эксперимента",
|
|
||||||
"",
|
|
||||||
"CSV Files (*.csv);;All Files (*)"
|
|
||||||
)
|
|
||||||
|
|
||||||
if filename:
|
if filename:
|
||||||
if not filename.lower().endswith('.csv'):
|
if not filename.lower().endswith('.csv'):
|
||||||
filename += '.csv'
|
filename += '.csv'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
|
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
|
||||||
writer = csv.writer(f)
|
writer = csv.writer(f)
|
||||||
|
|
||||||
# Заголовки
|
|
||||||
headers = []
|
headers = []
|
||||||
for j in range(self.design_matrix.columnCount()):
|
for j in range(self.design_matrix.columnCount()):
|
||||||
header_item = self.design_matrix.horizontalHeaderItem(j)
|
header_item = self.design_matrix.horizontalHeaderItem(j)
|
||||||
headers.append(header_item.text() if header_item else f"Колонка_{j+1}")
|
headers.append(header_item.text() if header_item else f"Колонка_{j+1}")
|
||||||
writer.writerow(headers)
|
writer.writerow(headers)
|
||||||
|
|
||||||
# Данные
|
|
||||||
for i in range(self.design_matrix.rowCount()):
|
for i in range(self.design_matrix.rowCount()):
|
||||||
row = []
|
row = [self.design_matrix.item(i, j).text() if self.design_matrix.item(i, j) else "" for j in range(self.design_matrix.columnCount())]
|
||||||
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)
|
writer.writerow(row)
|
||||||
|
|
||||||
QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}")
|
QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}")
|
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}")
|
||||||
|
|
||||||
def perform_analysis(self):
|
def perform_analysis(self):
|
||||||
"""Проводит регрессионный анализ"""
|
|
||||||
n_responses = self.responses_table.rowCount()
|
n_responses = self.responses_table.rowCount()
|
||||||
|
|
||||||
if n_responses == 0:
|
if n_responses == 0:
|
||||||
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один отклик!")
|
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один отклик!")
|
||||||
return
|
return
|
||||||
|
if self.generated_design is None:
|
||||||
|
QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!")
|
||||||
|
return
|
||||||
|
|
||||||
# Собираем результаты
|
|
||||||
results = []
|
results = []
|
||||||
for i in range(self.results_table.rowCount()):
|
for i in range(self.results_table.rowCount()):
|
||||||
row_results = []
|
row_results = []
|
||||||
@@ -583,50 +410,36 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
row_results.append(None)
|
row_results.append(None)
|
||||||
results.append(row_results)
|
results.append(row_results)
|
||||||
|
|
||||||
# Проверяем, что все результаты введены
|
|
||||||
missing = False
|
|
||||||
for i, row in enumerate(results):
|
for i, row in enumerate(results):
|
||||||
for j, val in enumerate(row):
|
for j, val in enumerate(row):
|
||||||
if val is None:
|
if val is None:
|
||||||
missing = True
|
self.analysis_output.setText(f"Ошибка: Не введены результаты для опыта {i+1}, отклик {j+1}")
|
||||||
self.analysis_output.setText(f"❌ Ошибка: Не введены результаты для опыта {i+1}, отклик {j+1}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Анализ
|
|
||||||
self.analysis_output.clear()
|
self.analysis_output.clear()
|
||||||
self.analysis_output.append("=" * 60)
|
self.analysis_output.append("=" * 60)
|
||||||
self.analysis_output.append("РЕЗУЛЬТАТЫ РЕГРЕССИОННОГО АНАЛИЗА")
|
self.analysis_output.append("РЕЗУЛЬТАТЫ РЕГРЕССИОННОГО АНАЛИЗА")
|
||||||
self.analysis_output.append("=" * 60)
|
self.analysis_output.append("=" * 60)
|
||||||
|
|
||||||
factors = self.get_factors_data()
|
factors = self.get_factors_data()
|
||||||
design = self.calculate_factorial_design(factors)
|
design = self.generated_design
|
||||||
|
|
||||||
for resp_idx in range(n_responses):
|
for resp_idx in range(n_responses):
|
||||||
resp_name = self.responses_table.item(resp_idx, 0).text()
|
resp_name = self.responses_table.item(resp_idx, 0).text()
|
||||||
self.analysis_output.append(f"\n📊 Отклик: {resp_name}")
|
self.analysis_output.append(f"\n📊 Отклик: {resp_name}")
|
||||||
self.analysis_output.append("-" * 40)
|
self.analysis_output.append("-" * 40)
|
||||||
|
|
||||||
# Собираем значения отклика
|
|
||||||
y_values = [results[i][resp_idx] for i in range(len(results))]
|
y_values = [results[i][resp_idx] for i in range(len(results))]
|
||||||
|
|
||||||
# Среднее значение
|
|
||||||
mean_y = np.mean(y_values)
|
mean_y = np.mean(y_values)
|
||||||
self.analysis_output.append(f"Среднее значение: {mean_y:.4f}")
|
|
||||||
|
|
||||||
# Дисперсия
|
|
||||||
variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0
|
variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0
|
||||||
self.analysis_output.append(f"Общая дисперсия: {variance:.4f}")
|
|
||||||
|
|
||||||
# Стандартное отклонение
|
|
||||||
std_dev = np.std(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"Стандартное отклонение: {std_dev:.4f}")
|
||||||
|
self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%")
|
||||||
|
|
||||||
# Коэффициент вариации
|
|
||||||
if mean_y != 0:
|
|
||||||
cv = (std_dev / mean_y) * 100
|
|
||||||
self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%")
|
|
||||||
|
|
||||||
# Разделяем факторные и центральные точки
|
|
||||||
factorial_y = []
|
factorial_y = []
|
||||||
center_y = []
|
center_y = []
|
||||||
for i, exp in enumerate(design):
|
for i, exp in enumerate(design):
|
||||||
@@ -636,34 +449,22 @@ class ExperimentDesignWindow(QMainWindow):
|
|||||||
factorial_y.append(y_values[i])
|
factorial_y.append(y_values[i])
|
||||||
|
|
||||||
if len(center_y) > 1:
|
if len(center_y) > 1:
|
||||||
center_variance = np.var(center_y, ddof=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"\nЦентральные точки (n={len(center_y)}):")
|
||||||
self.analysis_output.append(f" Среднее: {np.mean(center_y):.4f}")
|
self.analysis_output.append(f" Среднее: {np.mean(center_y):.4f}")
|
||||||
self.analysis_output.append(f" Дисперсия воспроизводимости: {center_variance:.4f}")
|
self.analysis_output.append(f" Дисперсия воспроизводимости: {center_variance:.4f}")
|
||||||
self.analysis_output.append(f" Стандартное отклонение: {np.std(center_y, ddof=1):.4f}")
|
|
||||||
|
|
||||||
# Критерий Фишера для проверки адекватности
|
|
||||||
if len(factorial_y) > 0 and center_variance > 0:
|
if len(factorial_y) > 0 and center_variance > 0:
|
||||||
factorial_variance = np.var(factorial_y, ddof=1) if len(factorial_y) > 1 else 0
|
factorial_variance = np.var(factorial_y, ddof=1) if len(factorial_y) > 1 else 0
|
||||||
if factorial_variance > 0:
|
if factorial_variance > 0:
|
||||||
fisher = max(factorial_variance, center_variance) / min(factorial_variance, center_variance)
|
fisher = max(factorial_variance, center_variance) / min(factorial_variance, center_variance)
|
||||||
self.analysis_output.append(f"\nКритерий Фишера (F-отношение): {fisher:.4f}")
|
self.analysis_output.append(f"\nКритерий Фишера (F-отношение): {fisher:.4f}")
|
||||||
if fisher < 4.0:
|
if fisher < 4.0:
|
||||||
self.analysis_output.append(" ✅ Модель адекватна экспериментальным данным")
|
self.analysis_output.append("✅ Модель адекватна экспериментальным данным")
|
||||||
else:
|
else:
|
||||||
self.analysis_output.append(" ⚠️ Модель может быть неадекватна, требуется проверка")
|
self.analysis_output.append("⚠️ Модель может быть неадекватна, требуется проверка")
|
||||||
|
|
||||||
self.analysis_output.append("\n" + "=" * 60)
|
self.analysis_output.append("\n" + "=" * 60)
|
||||||
self.analysis_output.append("✅ Анализ завершен")
|
self.analysis_output.append("Анализ завершен")
|
||||||
|
|
||||||
def show_placeholder_message(self):
|
def show_error(self, message: str):
|
||||||
"""Показывает сообщение о том, что функция в разработке"""
|
QMessageBox.critical(self, "Ошибка", message)
|
||||||
QMessageBox.information(
|
|
||||||
self,
|
|
||||||
"В разработке",
|
|
||||||
"🧪 Функция в стадии разработки!\n\nБлижайшие обновления:\n"
|
|
||||||
"✅ Экспорт в Excel\n"
|
|
||||||
"✅ Построение поверхностей отклика\n"
|
|
||||||
"✅ Графики главных эффектов\n"
|
|
||||||
"✅ Полный регрессионный анализ"
|
|
||||||
)
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
|
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QFrame)
|
||||||
QPushButton, QLabel, QWidget, QFrame)
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt5.QtGui import QFont
|
||||||
from controller import Controller
|
|
||||||
from experiment_design import ExperimentDesignWindow
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -12,42 +8,22 @@ class MainWindow(QMainWindow):
|
|||||||
self.setWindowTitle("Цифровой помощник биохимика - Главное меню")
|
self.setWindowTitle("Цифровой помощник биохимика - Главное меню")
|
||||||
self.setGeometry(300, 200, 700, 500)
|
self.setGeometry(300, 200, 700, 500)
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
QMainWindow {
|
QMainWindow { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #e8f4f8, stop:1 #f0f0f0); }
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
QPushButton { background-color: #2196F3; color: white; border: none; padding: 15px; font-size: 16px; font-weight: bold; border-radius: 8px; }
|
||||||
stop:0 #e8f4f8, stop:1 #f0f0f0);
|
QPushButton:hover { background-color: #1976D2; }
|
||||||
}
|
QLabel { color: #333; font-size: 14px; }
|
||||||
QPushButton {
|
|
||||||
background-color: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-family: 'Segoe UI', Arial;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: #1976D2;
|
|
||||||
}
|
|
||||||
QPushButton:pressed {
|
|
||||||
background-color: #0D47A1;
|
|
||||||
}
|
|
||||||
QLabel {
|
|
||||||
color: #333;
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: 'Segoe UI', Arial;
|
|
||||||
}
|
|
||||||
""")
|
""")
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
|
self.medium_calculator = None
|
||||||
|
self.experiment_window = None
|
||||||
|
|
||||||
def _init_ui(self):
|
def _init_ui(self):
|
||||||
central_widget = QWidget()
|
central_widget = QWidget()
|
||||||
self.setCentralWidget(central_widget)
|
self.setCentralWidget(central_widget)
|
||||||
layout = QVBoxLayout(central_widget)
|
layout = QVBoxLayout(central_widget)
|
||||||
layout.setSpacing(20)
|
layout.setSpacing(20)
|
||||||
layout.setContentsMargins(50, 50, 50, 50)
|
layout.setContentsMargins(50, 50, 50, 50)
|
||||||
|
|
||||||
# Заголовок
|
|
||||||
title_label = QLabel("Цифровой помощник биохимика")
|
title_label = QLabel("Цифровой помощник биохимика")
|
||||||
title_font = QFont()
|
title_font = QFont()
|
||||||
title_font.setPointSize(20)
|
title_font.setPointSize(20)
|
||||||
@@ -56,8 +32,7 @@ class MainWindow(QMainWindow):
|
|||||||
title_label.setAlignment(Qt.AlignCenter)
|
title_label.setAlignment(Qt.AlignCenter)
|
||||||
title_label.setStyleSheet("color: #1565C0;")
|
title_label.setStyleSheet("color: #1565C0;")
|
||||||
layout.addWidget(title_label)
|
layout.addWidget(title_label)
|
||||||
|
|
||||||
# Подзаголовок
|
|
||||||
subtitle_label = QLabel("Биотехнологические инструменты для лаборатории")
|
subtitle_label = QLabel("Биотехнологические инструменты для лаборатории")
|
||||||
subtitle_font = QFont()
|
subtitle_font = QFont()
|
||||||
subtitle_font.setPointSize(12)
|
subtitle_font.setPointSize(12)
|
||||||
@@ -65,81 +40,62 @@ class MainWindow(QMainWindow):
|
|||||||
subtitle_label.setAlignment(Qt.AlignCenter)
|
subtitle_label.setAlignment(Qt.AlignCenter)
|
||||||
subtitle_label.setStyleSheet("color: #666;")
|
subtitle_label.setStyleSheet("color: #666;")
|
||||||
layout.addWidget(subtitle_label)
|
layout.addWidget(subtitle_label)
|
||||||
|
|
||||||
layout.addSpacing(20)
|
layout.addSpacing(20)
|
||||||
|
|
||||||
# Кнопка 1: Калькулятор питательных сред
|
btn_medium = QPushButton("🧪 Калькулятор питательных сред")
|
||||||
btn_medium = QPushButton("Калькулятор питательных сред")
|
|
||||||
btn_medium.setMinimumHeight(80)
|
btn_medium.setMinimumHeight(80)
|
||||||
btn_medium.clicked.connect(self.open_medium_calculator)
|
btn_medium.clicked.connect(self.open_medium_calculator)
|
||||||
layout.addWidget(btn_medium)
|
layout.addWidget(btn_medium)
|
||||||
|
|
||||||
# Описание кнопки 1
|
desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\nразбавления реагентов и автоматическим расчётом растворителя")
|
||||||
desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\n"
|
|
||||||
"разбавления реагентов и автоматическим расчётом растворителя")
|
|
||||||
desc1_label.setAlignment(Qt.AlignCenter)
|
desc1_label.setAlignment(Qt.AlignCenter)
|
||||||
desc1_label.setWordWrap(True)
|
desc1_label.setWordWrap(True)
|
||||||
desc1_label.setStyleSheet("color: #555; font-size: 11px;")
|
desc1_label.setStyleSheet("color: #555; font-size: 11px;")
|
||||||
layout.addWidget(desc1_label)
|
layout.addWidget(desc1_label)
|
||||||
|
|
||||||
layout.addSpacing(15)
|
layout.addSpacing(15)
|
||||||
|
|
||||||
# Кнопка 2: Планирование эксперимента
|
btn_experiment = QPushButton("📊 Планирование эксперимента (DoE)")
|
||||||
btn_experiment = QPushButton("Планирование эксперимента (DoE)")
|
|
||||||
btn_experiment.setMinimumHeight(80)
|
btn_experiment.setMinimumHeight(80)
|
||||||
btn_experiment.clicked.connect(self.open_experiment_designer)
|
btn_experiment.clicked.connect(self.open_experiment_designer)
|
||||||
layout.addWidget(btn_experiment)
|
layout.addWidget(btn_experiment)
|
||||||
|
|
||||||
# Описание кнопки 2
|
desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\nмногомерный анализ и визуализация")
|
||||||
desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\n"
|
|
||||||
"многомерный анализ и визуализация")
|
|
||||||
desc2_label.setAlignment(Qt.AlignCenter)
|
desc2_label.setAlignment(Qt.AlignCenter)
|
||||||
desc2_label.setWordWrap(True)
|
desc2_label.setWordWrap(True)
|
||||||
desc2_label.setStyleSheet("color: #555; font-size: 11px;")
|
desc2_label.setStyleSheet("color: #555; font-size: 11px;")
|
||||||
layout.addWidget(desc2_label)
|
layout.addWidget(desc2_label)
|
||||||
|
|
||||||
layout.addSpacing(15)
|
layout.addSpacing(15)
|
||||||
|
|
||||||
# Линия-разделитель
|
|
||||||
line = QFrame()
|
line = QFrame()
|
||||||
line.setFrameShape(QFrame.HLine)
|
line.setFrameShape(QFrame.HLine)
|
||||||
line.setFrameShadow(QFrame.Sunken)
|
line.setFrameShadow(QFrame.Sunken)
|
||||||
layout.addWidget(line)
|
layout.addWidget(line)
|
||||||
|
|
||||||
# Нижняя панель
|
|
||||||
bottom_layout = QHBoxLayout()
|
|
||||||
|
|
||||||
# Информация о версии
|
bottom_layout = QHBoxLayout()
|
||||||
version_label = QLabel("Версия alpha 0.1.2 | © 2026 Цифровой помощник биохимика")
|
version_label = QLabel("Версия 1.0.0 | © 2026 Цифровой помощник биохимика")
|
||||||
version_label.setStyleSheet("color: #999; font-size: 10px;")
|
version_label.setStyleSheet("color: #999; font-size: 10px;")
|
||||||
bottom_layout.addWidget(version_label)
|
bottom_layout.addWidget(version_label)
|
||||||
|
|
||||||
bottom_layout.addStretch()
|
bottom_layout.addStretch()
|
||||||
|
|
||||||
# Кнопка выхода
|
|
||||||
btn_exit = QPushButton("Выход")
|
btn_exit = QPushButton("Выход")
|
||||||
btn_exit.setMaximumWidth(150)
|
btn_exit.setMaximumWidth(150)
|
||||||
btn_exit.setStyleSheet("""
|
btn_exit.setStyleSheet("QPushButton { background-color: #f44336; padding: 8px; font-size: 14px; } QPushButton:hover { background-color: #da190b; }")
|
||||||
QPushButton {
|
|
||||||
background-color: #f44336;
|
|
||||||
padding: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: #da190b;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
btn_exit.clicked.connect(self.close)
|
btn_exit.clicked.connect(self.close)
|
||||||
bottom_layout.addWidget(btn_exit)
|
bottom_layout.addWidget(btn_exit)
|
||||||
|
|
||||||
layout.addLayout(bottom_layout)
|
layout.addLayout(bottom_layout)
|
||||||
|
|
||||||
def open_medium_calculator(self):
|
def open_medium_calculator(self):
|
||||||
"""Открывает калькулятор питательной среды"""
|
from .medium_view import MediumCalculatorWindow
|
||||||
self.medium_calculator = Controller()
|
from ..controllers.medium_controller import MediumController
|
||||||
self.medium_calculator.view.show()
|
self.medium_calculator = MediumCalculatorWindow()
|
||||||
|
self.medium_controller = MediumController(self.medium_calculator)
|
||||||
|
self.medium_calculator.show()
|
||||||
|
|
||||||
def open_experiment_designer(self):
|
def open_experiment_designer(self):
|
||||||
"""Открывает окно планирования эксперимента"""
|
from .experiment_view import ExperimentDesignWindow
|
||||||
|
from ..controllers.experiment_controller import ExperimentController
|
||||||
self.experiment_window = ExperimentDesignWindow()
|
self.experiment_window = ExperimentDesignWindow()
|
||||||
|
self.experiment_controller = ExperimentController(self.experiment_window)
|
||||||
self.experiment_window.show()
|
self.experiment_window.show()
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
|
||||||
|
QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget,
|
||||||
|
QMessageBox, QGroupBox, QFrame)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QFont, QColor
|
||||||
|
|
||||||
|
class MediumCalculatorWindow(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Калькулятор питательных сред")
|
||||||
|
self.setGeometry(200, 100, 1200, 700)
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QMainWindow { background-color: #f5f5f5; }
|
||||||
|
QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; background-color: white; }
|
||||||
|
QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; }
|
||||||
|
QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 15px; border-radius: 4px; font-weight: bold; }
|
||||||
|
QPushButton:hover { background-color: #1976D2; }
|
||||||
|
QPushButton#danger { background-color: #f44336; }
|
||||||
|
QPushButton#success { background-color: #4CAF50; }
|
||||||
|
QTableWidget { gridline-color: #ddd; background-color: white; }
|
||||||
|
QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; }
|
||||||
|
""")
|
||||||
|
self._init_ui()
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
central_widget = QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
layout = QVBoxLayout(central_widget)
|
||||||
|
layout.setSpacing(15)
|
||||||
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
|
||||||
|
title_label = QLabel("Калькулятор питательных сред")
|
||||||
|
title_label.setAlignment(Qt.AlignCenter)
|
||||||
|
title_font = QFont()
|
||||||
|
title_font.setPointSize(18)
|
||||||
|
title_font.setBold(True)
|
||||||
|
title_label.setFont(title_font)
|
||||||
|
layout.addWidget(title_label)
|
||||||
|
|
||||||
|
params_group = QGroupBox("Параметры среды")
|
||||||
|
params_layout = QHBoxLayout()
|
||||||
|
amount_layout = QHBoxLayout()
|
||||||
|
amount_layout.addWidget(QLabel("Общее количество:"))
|
||||||
|
self.amount_input = QDoubleSpinBox()
|
||||||
|
self.amount_input.setRange(0.001, 1000000.0)
|
||||||
|
self.amount_input.setValue(1000.0)
|
||||||
|
amount_layout.addWidget(self.amount_input)
|
||||||
|
self.amount_unit_combo = QComboBox()
|
||||||
|
self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"])
|
||||||
|
self.amount_unit_combo.setCurrentText("мл")
|
||||||
|
amount_layout.addWidget(self.amount_unit_combo)
|
||||||
|
params_layout.addLayout(amount_layout)
|
||||||
|
solvent_layout = QHBoxLayout()
|
||||||
|
solvent_layout.addWidget(QLabel("Растворитель:"))
|
||||||
|
self.solvent_input = QLineEdit("Вода")
|
||||||
|
solvent_layout.addWidget(self.solvent_input)
|
||||||
|
params_layout.addLayout(solvent_layout)
|
||||||
|
params_layout.addStretch()
|
||||||
|
params_group.setLayout(params_layout)
|
||||||
|
layout.addWidget(params_group)
|
||||||
|
|
||||||
|
table_group = QGroupBox("Состав среды")
|
||||||
|
table_layout = QVBoxLayout()
|
||||||
|
self.table = QTableWidget()
|
||||||
|
self.table.setColumnCount(6)
|
||||||
|
self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"])
|
||||||
|
self.table.setAlternatingRowColors(True)
|
||||||
|
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||||
|
self.table.setColumnWidth(0, 180)
|
||||||
|
self.table.setColumnWidth(1, 70)
|
||||||
|
self.table.setColumnWidth(2, 90)
|
||||||
|
self.table.setColumnWidth(3, 70)
|
||||||
|
self.table.setColumnWidth(4, 100)
|
||||||
|
self.table.setColumnWidth(5, 120)
|
||||||
|
table_layout.addWidget(self.table)
|
||||||
|
table_group.setLayout(table_layout)
|
||||||
|
layout.addWidget(table_group)
|
||||||
|
|
||||||
|
btn_group = QGroupBox("Управление")
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
self.add_row_btn = QPushButton("➕ Добавить реагент")
|
||||||
|
self.remove_row_btn = QPushButton("➖ Удалить реагент")
|
||||||
|
self.remove_row_btn.setObjectName("danger")
|
||||||
|
btn_layout.addWidget(self.add_row_btn)
|
||||||
|
btn_layout.addWidget(self.remove_row_btn)
|
||||||
|
btn_layout.addStretch()
|
||||||
|
self.calculate_btn = QPushButton("🧮 Рассчитать")
|
||||||
|
self.calculate_btn.setObjectName("success")
|
||||||
|
self.save_btn = QPushButton("💾 Сохранить")
|
||||||
|
self.load_btn = QPushButton("📂 Загрузить")
|
||||||
|
btn_layout.addWidget(self.calculate_btn)
|
||||||
|
btn_layout.addWidget(self.save_btn)
|
||||||
|
btn_layout.addWidget(self.load_btn)
|
||||||
|
btn_group.setLayout(btn_layout)
|
||||||
|
layout.addWidget(btn_group)
|
||||||
|
|
||||||
|
info_frame = QFrame()
|
||||||
|
info_frame.setFrameShape(QFrame.StyledPanel)
|
||||||
|
info_frame.setStyleSheet("background-color: #e3f2fd; border-radius: 5px;")
|
||||||
|
info_layout = QHBoxLayout(info_frame)
|
||||||
|
info_label = QLabel("ℹ️ Подсказка: Реагенты в массовых единицах не учитываются при расчёте объёма растворителя")
|
||||||
|
info_layout.addWidget(info_label)
|
||||||
|
info_layout.addStretch()
|
||||||
|
layout.addWidget(info_frame)
|
||||||
|
|
||||||
|
def add_initial_rows(self):
|
||||||
|
self.add_solvent_row()
|
||||||
|
self.add_new_row()
|
||||||
|
|
||||||
|
def add_solvent_row(self):
|
||||||
|
row = self.table.rowCount()
|
||||||
|
self.table.insertRow(row)
|
||||||
|
solvent_item = QTableWidgetItem(self.solvent_input.text())
|
||||||
|
solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable)
|
||||||
|
solvent_item.setBackground(QColor(230, 230, 230))
|
||||||
|
font = QFont()
|
||||||
|
font.setBold(True)
|
||||||
|
solvent_item.setFont(font)
|
||||||
|
self.table.setItem(row, 0, solvent_item)
|
||||||
|
for col in [1, 3, 4]:
|
||||||
|
item = QTableWidgetItem("-")
|
||||||
|
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
||||||
|
item.setBackground(QColor(230, 230, 230))
|
||||||
|
self.table.setItem(row, col, item)
|
||||||
|
unit_item = QTableWidgetItem(self.amount_unit_combo.currentText())
|
||||||
|
unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable)
|
||||||
|
unit_item.setBackground(QColor(230, 230, 230))
|
||||||
|
self.table.setItem(row, 2, unit_item)
|
||||||
|
result_item = QTableWidgetItem("")
|
||||||
|
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
|
||||||
|
self.table.setItem(row, 5, result_item)
|
||||||
|
|
||||||
|
def update_solvent_name(self):
|
||||||
|
if self.table.rowCount() > 0 and self.table.item(0, 0):
|
||||||
|
self.table.item(0, 0).setText(self.solvent_input.text())
|
||||||
|
|
||||||
|
def add_new_row(self):
|
||||||
|
row = self.table.rowCount()
|
||||||
|
self.table.insertRow(row)
|
||||||
|
self.table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row}"))
|
||||||
|
self.table.setItem(row, 1, QTableWidgetItem("0"))
|
||||||
|
unit_combo = QComboBox()
|
||||||
|
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
|
||||||
|
unit_combo.setCurrentText("мг")
|
||||||
|
self.table.setCellWidget(row, 2, unit_combo)
|
||||||
|
self.table.setItem(row, 3, QTableWidgetItem("1"))
|
||||||
|
self.table.setItem(row, 4, QTableWidgetItem("1"))
|
||||||
|
self.table.setItem(row, 5, QTableWidgetItem(""))
|
||||||
|
|
||||||
|
def remove_selected_row(self):
|
||||||
|
for row in sorted(set(item.row() for item in self.table.selectedItems()), reverse=True):
|
||||||
|
if row > 0:
|
||||||
|
self.table.removeRow(row)
|
||||||
|
|
||||||
|
def get_table_data(self):
|
||||||
|
data = []
|
||||||
|
for row in range(1, self.table.rowCount()):
|
||||||
|
name = self.table.item(row, 0).text() if self.table.item(row, 0) else ""
|
||||||
|
percent = self.table.item(row, 1).text() if self.table.item(row, 1) else "0"
|
||||||
|
unit_widget = self.table.cellWidget(row, 2)
|
||||||
|
unit = unit_widget.currentText() if unit_widget else "мг"
|
||||||
|
coeff = self.table.item(row, 3).text() if self.table.item(row, 3) else "1"
|
||||||
|
dilution = self.table.item(row, 4).text() if self.table.item(row, 4) else "1"
|
||||||
|
data.append([name, percent, unit, coeff, dilution])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def update_solvent_percent(self, solvent_percent: float):
|
||||||
|
if self.table.rowCount() > 0 and self.table.item(0, 1):
|
||||||
|
self.table.item(0, 1).setText(self.format_number(solvent_percent))
|
||||||
|
|
||||||
|
def update_solvent_result(self, solvent_amount: float, unit: str):
|
||||||
|
if self.table.rowCount() > 0:
|
||||||
|
if self.table.item(0, 5):
|
||||||
|
self.table.item(0, 5).setText(self.format_number(solvent_amount))
|
||||||
|
self.table.item(0, 5).setBackground(QColor(220, 255, 220))
|
||||||
|
if self.table.item(0, 2):
|
||||||
|
self.table.item(0, 2).setText(unit)
|
||||||
|
|
||||||
|
def update_results(self, results: list):
|
||||||
|
for row, amount in enumerate(results, start=1):
|
||||||
|
if row < self.table.rowCount() and self.table.item(row, 5):
|
||||||
|
self.table.item(row, 5).setText(self.format_number(amount))
|
||||||
|
self.table.item(row, 5).setBackground(QColor(220, 255, 220))
|
||||||
|
|
||||||
|
def clear_results(self):
|
||||||
|
for row in range(self.table.rowCount()):
|
||||||
|
if self.table.item(row, 5):
|
||||||
|
self.table.item(row, 5).setText("")
|
||||||
|
if row == 0:
|
||||||
|
self.table.item(row, 5).setBackground(QColor(230, 230, 230))
|
||||||
|
else:
|
||||||
|
self.table.item(row, 5).setBackground(QColor(250, 250, 250))
|
||||||
|
if self.table.rowCount() > 0 and self.table.item(0, 1):
|
||||||
|
self.table.item(0, 1).setText("")
|
||||||
|
if self.table.rowCount() > 0 and self.table.item(0, 2):
|
||||||
|
self.table.item(0, 2).setText(self.amount_unit_combo.currentText())
|
||||||
|
|
||||||
|
def format_number(self, value):
|
||||||
|
if value == int(value):
|
||||||
|
return str(int(value))
|
||||||
|
formatted = f"{value:.6f}".rstrip('0').rstrip('.')
|
||||||
|
if '.' in formatted and len(formatted.split('.')[1]) > 4:
|
||||||
|
formatted = f"{value:.4f}".rstrip('0').rstrip('.')
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
def show_error(self, message: str):
|
||||||
|
QMessageBox.critical(self, "Ошибка", message)
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
|
|
||||||
QTableWidget, QTableWidgetItem, QPushButton,
|
|
||||||
QLabel, QDoubleSpinBox, QComboBox, QLineEdit,
|
|
||||||
QWidget, QMessageBox, QGroupBox, QFrame, QHeaderView)
|
|
||||||
from PyQt5.QtCore import Qt
|
|
||||||
from PyQt5.QtGui import QFont, QColor
|
|
||||||
|
|
||||||
|
|
||||||
class MediumCalculatorWindow(QMainWindow):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.setWindowTitle("Калькулятор питательных сред - Цифровой помощник биохимика")
|
|
||||||
self.setGeometry(200, 100, 1200, 700)
|
|
||||||
self.setStyleSheet("""
|
|
||||||
QMainWindow {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
QGroupBox {
|
|
||||||
font-weight: bold;
|
|
||||||
border: 2px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-top: 10px;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
QGroupBox::title {
|
|
||||||
subcontrol-origin: margin;
|
|
||||||
left: 10px;
|
|
||||||
padding: 0 5px 0 5px;
|
|
||||||
color: #1565C0;
|
|
||||||
}
|
|
||||||
QTableWidget {
|
|
||||||
gridline-color: #ddd;
|
|
||||||
background-color: white;
|
|
||||||
alternate-background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
QHeaderView::section {
|
|
||||||
background-color: #1565C0;
|
|
||||||
color: white;
|
|
||||||
padding: 8px;
|
|
||||||
border: none;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
QPushButton {
|
|
||||||
background-color: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: #1976D2;
|
|
||||||
}
|
|
||||||
QPushButton:pressed {
|
|
||||||
background-color: #0D47A1;
|
|
||||||
}
|
|
||||||
QPushButton#danger {
|
|
||||||
background-color: #f44336;
|
|
||||||
}
|
|
||||||
QPushButton#danger:hover {
|
|
||||||
background-color: #da190b;
|
|
||||||
}
|
|
||||||
QPushButton#success {
|
|
||||||
background-color: #4CAF50;
|
|
||||||
}
|
|
||||||
QPushButton#success:hover {
|
|
||||||
background-color: #45a049;
|
|
||||||
}
|
|
||||||
QDoubleSpinBox, QLineEdit {
|
|
||||||
padding: 4px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: white;
|
|
||||||
color: black;
|
|
||||||
font-size: 12px;
|
|
||||||
min-height: 20px;
|
|
||||||
}
|
|
||||||
QComboBox {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: white;
|
|
||||||
color: black;
|
|
||||||
font-size: 12px;
|
|
||||||
min-height: 20px;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
QComboBox::drop-down {
|
|
||||||
border: none;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
QComboBox QAbstractItemView {
|
|
||||||
background-color: white;
|
|
||||||
color: black;
|
|
||||||
selection-background-color: #2196F3;
|
|
||||||
selection-color: white;
|
|
||||||
}
|
|
||||||
QLabel {
|
|
||||||
color: black;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
QLabel#title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #0D47A1;
|
|
||||||
}
|
|
||||||
QLabel#info {
|
|
||||||
color: #1565C0;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self._init_ui()
|
|
||||||
|
|
||||||
def _init_ui(self):
|
|
||||||
central_widget = QWidget()
|
|
||||||
self.setCentralWidget(central_widget)
|
|
||||||
layout = QVBoxLayout(central_widget)
|
|
||||||
layout.setSpacing(15)
|
|
||||||
layout.setContentsMargins(20, 20, 20, 20)
|
|
||||||
|
|
||||||
title_label = QLabel("Калькулятор питательных сред")
|
|
||||||
title_label.setObjectName("title")
|
|
||||||
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()
|
|
||||||
params_layout.setSpacing(20)
|
|
||||||
|
|
||||||
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)
|
|
||||||
self.amount_input.setMinimumWidth(150)
|
|
||||||
amount_layout.addWidget(self.amount_input)
|
|
||||||
|
|
||||||
self.amount_unit_combo = QComboBox()
|
|
||||||
self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"])
|
|
||||||
self.amount_unit_combo.setCurrentText("мл")
|
|
||||||
self.amount_unit_combo.setMinimumWidth(80)
|
|
||||||
amount_layout.addWidget(self.amount_unit_combo)
|
|
||||||
params_layout.addLayout(amount_layout)
|
|
||||||
|
|
||||||
solvent_layout = QHBoxLayout()
|
|
||||||
solvent_layout.addWidget(QLabel("Растворитель:"))
|
|
||||||
self.solvent_input = QLineEdit("Вода")
|
|
||||||
self.solvent_input.setMinimumWidth(150)
|
|
||||||
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.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed)
|
|
||||||
|
|
||||||
self.table.verticalHeader().setVisible(False)
|
|
||||||
|
|
||||||
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()
|
|
||||||
btn_layout.setSpacing(10)
|
|
||||||
|
|
||||||
self.add_row_btn = QPushButton("Добавить реагент")
|
|
||||||
self.add_row_btn.setMinimumWidth(150)
|
|
||||||
btn_layout.addWidget(self.add_row_btn)
|
|
||||||
|
|
||||||
self.remove_row_btn = QPushButton("Удалить реагент")
|
|
||||||
self.remove_row_btn.setObjectName("danger")
|
|
||||||
self.remove_row_btn.setMinimumWidth(150)
|
|
||||||
btn_layout.addWidget(self.remove_row_btn)
|
|
||||||
|
|
||||||
btn_layout.addStretch()
|
|
||||||
|
|
||||||
self.calculate_btn = QPushButton("Рассчитать")
|
|
||||||
self.calculate_btn.setObjectName("success")
|
|
||||||
self.calculate_btn.setMinimumWidth(150)
|
|
||||||
btn_layout.addWidget(self.calculate_btn)
|
|
||||||
|
|
||||||
self.save_btn = QPushButton("Сохранить")
|
|
||||||
self.save_btn.setMinimumWidth(150)
|
|
||||||
btn_layout.addWidget(self.save_btn)
|
|
||||||
|
|
||||||
self.load_btn = QPushButton("Загрузить")
|
|
||||||
self.load_btn.setMinimumWidth(150)
|
|
||||||
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_label.setObjectName("info")
|
|
||||||
info_layout.addWidget(info_label)
|
|
||||||
info_layout.addStretch()
|
|
||||||
|
|
||||||
layout.addWidget(info_frame)
|
|
||||||
|
|
||||||
self.add_initial_rows()
|
|
||||||
|
|
||||||
def add_initial_rows(self):
|
|
||||||
self.add_solvent_row()
|
|
||||||
self.add_new_row()
|
|
||||||
|
|
||||||
def add_solvent_row(self):
|
|
||||||
row_count = self.table.rowCount()
|
|
||||||
self.table.insertRow(row_count)
|
|
||||||
self.table.setRowHeight(row_count, 30)
|
|
||||||
|
|
||||||
solvent_name = self.solvent_input.text()
|
|
||||||
solvent_item = QTableWidgetItem(solvent_name)
|
|
||||||
solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable)
|
|
||||||
solvent_item.setBackground(QColor(230, 230, 230))
|
|
||||||
solvent_item.setForeground(QColor(0, 0, 0))
|
|
||||||
font = QFont()
|
|
||||||
font.setBold(True)
|
|
||||||
solvent_item.setFont(font)
|
|
||||||
self.table.setItem(row_count, 0, solvent_item)
|
|
||||||
|
|
||||||
percent_item = QTableWidgetItem("")
|
|
||||||
percent_item.setFlags(percent_item.flags() & ~Qt.ItemIsEditable)
|
|
||||||
percent_item.setBackground(QColor(230, 230, 230))
|
|
||||||
percent_item.setForeground(QColor(0, 0, 0))
|
|
||||||
self.table.setItem(row_count, 1, percent_item)
|
|
||||||
|
|
||||||
unit_item = QTableWidgetItem(self.amount_unit_combo.currentText())
|
|
||||||
unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable)
|
|
||||||
unit_item.setBackground(QColor(230, 230, 230))
|
|
||||||
unit_item.setForeground(QColor(0, 0, 0))
|
|
||||||
self.table.setItem(row_count, 2, unit_item)
|
|
||||||
|
|
||||||
coeff_item = QTableWidgetItem("-")
|
|
||||||
coeff_item.setFlags(coeff_item.flags() & ~Qt.ItemIsEditable)
|
|
||||||
coeff_item.setBackground(QColor(230, 230, 230))
|
|
||||||
coeff_item.setForeground(QColor(0, 0, 0))
|
|
||||||
self.table.setItem(row_count, 3, coeff_item)
|
|
||||||
|
|
||||||
dilution_item = QTableWidgetItem("-")
|
|
||||||
dilution_item.setFlags(dilution_item.flags() & ~Qt.ItemIsEditable)
|
|
||||||
dilution_item.setBackground(QColor(230, 230, 230))
|
|
||||||
dilution_item.setForeground(QColor(0, 0, 0))
|
|
||||||
self.table.setItem(row_count, 4, dilution_item)
|
|
||||||
|
|
||||||
result_item = QTableWidgetItem("")
|
|
||||||
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
|
|
||||||
result_item.setBackground(QColor(240, 240, 240))
|
|
||||||
result_item.setForeground(QColor(0, 0, 0))
|
|
||||||
self.table.setItem(row_count, 5, result_item)
|
|
||||||
|
|
||||||
def update_solvent_name(self):
|
|
||||||
solvent_name = self.solvent_input.text()
|
|
||||||
name_item = self.table.item(0, 0)
|
|
||||||
if name_item:
|
|
||||||
name_item.setText(solvent_name)
|
|
||||||
|
|
||||||
def format_number(self, value):
|
|
||||||
if value == int(value):
|
|
||||||
return str(int(value))
|
|
||||||
else:
|
|
||||||
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 add_new_row(self):
|
|
||||||
row_count = self.table.rowCount()
|
|
||||||
self.table.insertRow(row_count)
|
|
||||||
self.table.setRowHeight(row_count, 30)
|
|
||||||
|
|
||||||
name_item = QTableWidgetItem(f"Реагент_{row_count}")
|
|
||||||
name_item.setForeground(QColor(0, 0, 0))
|
|
||||||
self.table.setItem(row_count, 0, name_item)
|
|
||||||
|
|
||||||
percent_item = QTableWidgetItem("0")
|
|
||||||
percent_item.setForeground(QColor(0, 0, 0))
|
|
||||||
self.table.setItem(row_count, 1, percent_item)
|
|
||||||
|
|
||||||
unit_combo = QComboBox()
|
|
||||||
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
|
|
||||||
unit_combo.setCurrentText("мг")
|
|
||||||
self.table.setCellWidget(row_count, 2, unit_combo)
|
|
||||||
|
|
||||||
coeff_item = QTableWidgetItem("1")
|
|
||||||
coeff_item.setForeground(QColor(0, 0, 0))
|
|
||||||
self.table.setItem(row_count, 3, coeff_item)
|
|
||||||
|
|
||||||
dilution_item = QTableWidgetItem("1")
|
|
||||||
dilution_item.setForeground(QColor(0, 0, 0))
|
|
||||||
dilution_item.setFlags(dilution_item.flags() | Qt.ItemIsEditable)
|
|
||||||
self.table.setItem(row_count, 4, dilution_item)
|
|
||||||
|
|
||||||
result_item = QTableWidgetItem("")
|
|
||||||
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
|
|
||||||
result_item.setBackground(QColor(250, 250, 250))
|
|
||||||
result_item.setForeground(QColor(0, 0, 0))
|
|
||||||
self.table.setItem(row_count, 5, result_item)
|
|
||||||
|
|
||||||
def remove_selected_row(self):
|
|
||||||
selected_rows = set()
|
|
||||||
for item in self.table.selectedItems():
|
|
||||||
selected_rows.add(item.row())
|
|
||||||
|
|
||||||
for row in sorted(selected_rows, reverse=True):
|
|
||||||
if row > 0:
|
|
||||||
self.table.removeRow(row)
|
|
||||||
|
|
||||||
def get_table_data(self) -> list:
|
|
||||||
data = []
|
|
||||||
for row in range(1, self.table.rowCount()):
|
|
||||||
row_data = []
|
|
||||||
|
|
||||||
name_item = self.table.item(row, 0)
|
|
||||||
row_data.append(name_item.text() if name_item else "")
|
|
||||||
|
|
||||||
percent_item = self.table.item(row, 1)
|
|
||||||
row_data.append(percent_item.text() if percent_item else "0")
|
|
||||||
|
|
||||||
unit_widget = self.table.cellWidget(row, 2)
|
|
||||||
if unit_widget and isinstance(unit_widget, QComboBox):
|
|
||||||
row_data.append(unit_widget.currentText())
|
|
||||||
else:
|
|
||||||
row_data.append("мг")
|
|
||||||
|
|
||||||
coeff_item = self.table.item(row, 3)
|
|
||||||
row_data.append(coeff_item.text() if coeff_item else "1")
|
|
||||||
|
|
||||||
dilution_item = self.table.item(row, 4)
|
|
||||||
if dilution_item:
|
|
||||||
try:
|
|
||||||
dilution_factor = float(dilution_item.text())
|
|
||||||
except ValueError:
|
|
||||||
dilution_factor = 1.0
|
|
||||||
row_data.append(dilution_factor)
|
|
||||||
else:
|
|
||||||
row_data.append(1.0)
|
|
||||||
|
|
||||||
data.append(row_data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def update_solvent_percent(self, solvent_percent: float):
|
|
||||||
percent_item = self.table.item(0, 1)
|
|
||||||
if percent_item:
|
|
||||||
percent_item.setText(self.format_number(solvent_percent))
|
|
||||||
|
|
||||||
def show_error(self, message: str):
|
|
||||||
QMessageBox.critical(self, "Ошибка", message)
|
|
||||||
|
|
||||||
def update_results(self, results: list):
|
|
||||||
for row, amount in enumerate(results, start=1):
|
|
||||||
if row < self.table.rowCount():
|
|
||||||
formatted_amount = self.format_number(amount)
|
|
||||||
result_item = QTableWidgetItem(formatted_amount)
|
|
||||||
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
|
|
||||||
result_item.setBackground(QColor(220, 255, 220))
|
|
||||||
result_item.setForeground(QColor(0, 0, 0))
|
|
||||||
font = QFont()
|
|
||||||
font.setBold(True)
|
|
||||||
result_item.setFont(font)
|
|
||||||
self.table.setItem(row, 5, result_item)
|
|
||||||
|
|
||||||
def update_solvent_result(self, solvent_amount: float, unit: str):
|
|
||||||
formatted_amount = self.format_number(solvent_amount)
|
|
||||||
result_item = self.table.item(0, 5)
|
|
||||||
if result_item:
|
|
||||||
result_item.setText(formatted_amount)
|
|
||||||
result_item.setBackground(QColor(220, 255, 220))
|
|
||||||
result_item.setForeground(QColor(0, 0, 0))
|
|
||||||
|
|
||||||
unit_item = self.table.item(0, 2)
|
|
||||||
if unit_item:
|
|
||||||
unit_item.setText(unit)
|
|
||||||
|
|
||||||
def update_display(self, solvent: str, total_amount: float, amount_unit: str):
|
|
||||||
self.solvent_input.setText(solvent)
|
|
||||||
self.update_solvent_name()
|
|
||||||
self.amount_input.setValue(total_amount)
|
|
||||||
self.amount_unit_combo.setCurrentText(amount_unit)
|
|
||||||
|
|
||||||
def clear_results(self):
|
|
||||||
for row in range(self.table.rowCount()):
|
|
||||||
result_item = self.table.item(row, 5)
|
|
||||||
if result_item:
|
|
||||||
result_item.setText("")
|
|
||||||
if row == 0:
|
|
||||||
result_item.setBackground(QColor(230, 230, 230))
|
|
||||||
else:
|
|
||||||
result_item.setBackground(QColor(250, 250, 250))
|
|
||||||
|
|
||||||
percent_item = self.table.item(0, 1)
|
|
||||||
if percent_item:
|
|
||||||
percent_item.setText("")
|
|
||||||
|
|
||||||
unit_item = self.table.item(0, 2)
|
|
||||||
if unit_item:
|
|
||||||
unit_item.setText(self.amount_unit_combo.currentText())
|
|
||||||
Reference in New Issue
Block a user