Программа перекомпанована

This commit is contained in:
2026-05-07 14:22:14 +05:00
parent 0b54165e99
commit cc56475d79
24 changed files with 1873 additions and 1168 deletions
BIN
View File
Binary file not shown.
Binary file not shown.
+2
View File
@@ -1,4 +1,6 @@
# Python
backup
*.json
__pycache__/
*.py[cod]
*$py.class
+3 -71
View File
@@ -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. Профессиональный вид - современный интерфейс, подходящий для лаборатории
## Структура проекта
+4 -8
View File
@@ -1,19 +1,15 @@
# main.py
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from PyQt5.QtWidgets import QApplication
from main_window import MainWindow
from src.views import MainWindow
def main():
app = QApplication(sys.argv)
# Устанавливаем стиль приложения
app.setStyle('Fusion')
# Создаём главное окно цифрового помощника биохимика
assistant = MainWindow()
assistant.show()
# Запускаем цикл обработки событий
sys.exit(app.exec_())
if __name__ == "__main__":
-192
View File
@@ -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
+1247
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -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
View File
@@ -1 +1,2 @@
PyQt5>=5.15.0
numpy>=1.19.0
Executable
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
python3 main.py
+3
View File
@@ -0,0 +1,3 @@
"""Цифровой помощник биохимика - основная библиотека"""
__version__ = "1.0.0"
+4
View File
@@ -0,0 +1,4 @@
from .medium_controller import MediumController
from .experiment_controller import ExperimentController
__all__ = ['MediumController', 'ExperimentController']
+23
View File
@@ -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,21 +1,18 @@
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.QtGui import QColor
from model import Model
from view import MediumCalculatorWindow
import json
from reagent import Reagent
from ..models.medium_model import MediumModel
from ..models.reagent import Reagent
class Controller:
def __init__(self):
self.model = Model()
self.view = MediumCalculatorWindow()
class MediumController:
def __init__(self, view):
self.model = MediumModel()
self.view = view
self._connect_signals()
self._setup_initial_data()
def _connect_signals(self):
"""Подключает обработчики событий интерфейса"""
self.view.add_row_btn.clicked.connect(self.add_reagent_row)
self.view.remove_row_btn.clicked.connect(self.remove_reagent_row)
self.view.calculate_btn.clicked.connect(self._perform_calculation)
@@ -23,120 +20,57 @@ class Controller:
self.view.load_btn.clicked.connect(self.load_composition)
self.view.solvent_input.textChanged.connect(self.view.update_solvent_name)
def _setup_initial_data(self):
self.view.add_initial_rows()
def add_reagent_row(self):
"""Добавляет новую строку для реагента"""
self.view.add_new_row()
def remove_reagent_row(self):
"""Удаляет выбранную строку реагента"""
self.view.remove_selected_row()
def _perform_calculation(self):
"""Выполняет расчёт и обновляет интерфейс"""
try:
self._update_model_from_view()
results, solvent_amount, solvent_percentage = self.model.calculate_amounts()
self.view.update_results(results)
# Обновляем информацию о растворителе в первой строке таблицы
self.view.update_solvent_result(solvent_amount, self.model.amount_unit)
self.view.update_solvent_percent(solvent_percentage)
except ValueError as e:
self.view.show_error(f"Ошибка в данных: {str(e)}")
except Exception as e:
self.view.show_error(f"Неожиданная ошибка: {str(e)}")
def _update_model_from_view(self):
"""Обновляет модель данными из интерфейса (только реагенты, без растворителя)"""
# Очищаем список реагентов в модели
self.model.reagents.clear()
# Обновляем общее количество и единицу измерения
self.model.total_amount = self.view.amount_input.value()
self.model.amount_unit = self.view.amount_unit_combo.currentText()
self.model.solvent = self.view.solvent_input.text()
# Заполняем реагенты из таблицы (начиная с 1 строки, пропуская растворитель)
for row in range(1, self.view.table.rowCount()):
name_item = self.view.table.item(row, 0)
percentage_item = self.view.table.item(row, 1)
unit_widget = self.view.table.cellWidget(row, 2)
conversion_item = self.view.table.item(row, 3)
dilution_item = self.view.table.item(row, 4)
# Пропускаем строку, если какие-то обязательные поля отсутствуют
if not all([name_item, percentage_item, conversion_item]):
continue
try:
name = name_item.text()
percentage = float(percentage_item.text())
unit = unit_widget.currentText() if unit_widget else "мг"
conversion_factor = float(conversion_item.text())
dilution_factor = float(dilution_item.text())
self.model.add_reagent(name, percentage, unit, conversion_factor, dilution_factor)
dilution_factor = float(dilution_item.text()) if dilution_item else 1.0
reagent = Reagent(name, percentage, unit, conversion_factor)
reagent.dilution_factor = dilution_factor
self.model.reagents.append(reagent)
except ValueError as e:
raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}")
def _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):
"""Сохраняет состав среды в JSON-файл"""
filename, _ = QFileDialog.getSaveFileName(
self.view,
"Сохранить состав среды",
"",
"JSON Files (*.json);;All Files (*)"
)
filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)")
if filename:
if not filename.lower().endswith('.json'):
filename += '.json'
try:
self._update_model_from_view()
self.model.save_to_file(filename)
@@ -145,38 +79,40 @@ class Controller:
self.view.show_error(f"Ошибка сохранения: {str(e)}")
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:
try:
self.model.load_from_file(filename)
self._update_view_from_model()
QMessageBox.information(
self.view,
"Успех",
"Состав среды успешно загружен"
)
QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен")
except FileNotFoundError:
QMessageBox.critical(
self.view,
"Ошибка",
f"Файл не найден: {filename}"
)
QMessageBox.critical(self.view, "Ошибка", f"Файл не найден: {filename}")
except json.JSONDecodeError as e:
QMessageBox.critical(
self.view,
"Ошибка",
f"Неверный формат JSON-файла: {str(e)}"
)
QMessageBox.critical(self.view, "Ошибка", f"Неверный формат JSON-файла: {str(e)}")
except Exception as e:
QMessageBox.critical(
self.view,
"Ошибка",
f"Ошибка при загрузке состава: {str(e)}"
)
QMessageBox.critical(self.view, "Ошибка", f"Ошибка при загрузке состава: {str(e)}")
def _update_view_from_model(self):
while self.view.table.rowCount() > 1:
self.view.table.removeRow(1)
if self.view.table.rowCount() == 0:
self.view.add_solvent_row()
self.view.amount_input.setValue(self.model.total_amount)
index = self.view.amount_unit_combo.findText(self.model.amount_unit)
if index >= 0:
self.view.amount_unit_combo.setCurrentIndex(index)
self.view.solvent_input.setText(self.model.solvent)
self.view.update_solvent_name()
for reagent in self.model.reagents:
row = self.view.table.rowCount()
self.view.table.insertRow(row)
self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}"))
unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText(reagent.unit)
self.view.table.setCellWidget(row, 2, unit_combo)
self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}"))
self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}"))
self.view.table.setItem(row, 5, QTableWidgetItem(""))
self.view.clear_results()
+5
View File
@@ -0,0 +1,5 @@
from .reagent import Reagent
from .medium_model import MediumModel
from .experiment_model import ExperimentModel
__all__ = ['Reagent', 'MediumModel', 'ExperimentModel']
+80
View File
@@ -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
+113
View File
@@ -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
+10
View File
@@ -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})"
+3
View File
@@ -0,0 +1,3 @@
"""Утилиты и вспомогательные функции"""
__all__ = []
+5
View File
@@ -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,
QPushButton, QLabel, QWidget, QMessageBox,
QTableWidget, QTableWidgetItem, QGroupBox,
QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit,
QTextEdit, QTabWidget, QFormLayout, QCheckBox,
QScrollArea, QFileDialog)
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox,
QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, QTextEdit,
QTabWidget, QFormLayout, QCheckBox, QScrollArea, QFileDialog)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QFont
import numpy as np
import csv
import random
import numpy as np
class ExperimentDesignWindow(QMainWindow):
def __init__(self):
@@ -16,43 +14,15 @@ class ExperimentDesignWindow(QMainWindow):
self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика")
self.setGeometry(200, 100, 1200, 800)
self.setStyleSheet("""
QMainWindow {
background-color: #f5f5f5;
}
QGroupBox {
font-weight: bold;
border: 2px solid #ccc;
border-radius: 5px;
margin-top: 10px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #1565C0;
}
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #45a049;
}
QTableWidget {
gridline-color: #ddd;
}
QHeaderView::section {
background-color: #1565C0;
color: white;
padding: 8px;
border: none;
font-weight: bold;
}
QMainWindow { background-color: #f5f5f5; }
QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; }
QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; }
QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; }
QPushButton:hover { background-color: #45a049; }
QPushButton#danger { background-color: #f44336; }
QPushButton#danger:hover { background-color: #da190b; }
QTableWidget { gridline-color: #ddd; }
QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; }
""")
self._init_ui()
@@ -61,8 +31,7 @@ class ExperimentDesignWindow(QMainWindow):
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# Заголовок
title_label = QLabel("📈 Планирование полнофакторного эксперимента (DoE)")
title_label = QLabel("Планирование полнофакторного эксперимента (DoE)")
title_font = QFont()
title_font.setPointSize(18)
title_font.setBold(True)
@@ -71,14 +40,12 @@ class ExperimentDesignWindow(QMainWindow):
title_label.setStyleSheet("color: #2E7D32;")
layout.addWidget(title_label)
# Вкладки
tabs = QTabWidget()
# Вкладка 1: Параметры эксперимента
# Вкладка параметров
params_tab = QWidget()
params_layout = QVBoxLayout(params_tab)
# Группа факторов
factors_group = QGroupBox("Факторы эксперимента (независимые переменные)")
factors_layout = QVBoxLayout()
@@ -87,27 +54,20 @@ class ExperimentDesignWindow(QMainWindow):
factors_layout.addWidget(info_label)
self.factors_table = QTableWidget()
# Новый порядок колонок: Фактор, Нулевой уровень, Шаг, Верхний (+1), Нижний (-1), Единица измерения
self.factors_table.setColumnCount(6)
self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг",
"Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"])
self.factors_table.setRowCount(2)
# Пример данных
sample_factors = [
["Температура", "31", "6", "37", "25", "°C"],
["pH", "7.0", "0.5", "7.5", "6.5", ""],
]
sample_factors = [["Температура", "31", "6", "37", "25", "°C"], ["pH", "7.0", "0.5", "7.5", "6.5", ""]]
for i, factor in enumerate(sample_factors):
for j, value in enumerate(factor):
item = QTableWidgetItem(value)
if j in [3, 4]: # Верхний и нижний уровень - только для чтения
if j in [3, 4]:
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(i, j, item)
# Настройка ширины колонок
self.factors_table.setColumnWidth(0, 150)
self.factors_table.setColumnWidth(1, 120)
self.factors_table.setColumnWidth(2, 80)
@@ -115,12 +75,9 @@ class ExperimentDesignWindow(QMainWindow):
self.factors_table.setColumnWidth(4, 120)
self.factors_table.setColumnWidth(5, 100)
# Подключаем сигналы изменения ячеек
self.factors_table.itemChanged.connect(self.on_factor_changed)
factors_layout.addWidget(self.factors_table)
# Кнопки для управления факторами
factor_buttons = QHBoxLayout()
add_factor_btn = QPushButton("+ Добавить фактор")
add_factor_btn.clicked.connect(self.add_factor_row)
@@ -130,49 +87,35 @@ class ExperimentDesignWindow(QMainWindow):
factor_buttons.addWidget(remove_factor_btn)
factor_buttons.addStretch()
factors_layout.addLayout(factor_buttons)
factors_group.setLayout(factors_layout)
params_layout.addWidget(factors_group)
# Группа настроек эксперимента
settings_group = QGroupBox("Настройки эксперимента")
settings_layout = QHBoxLayout()
center_layout = QHBoxLayout()
center_layout.addWidget(QLabel("Количество центральных точек:"))
self.center_points_spin = QSpinBox()
self.center_points_spin.setRange(0, 10)
self.center_points_spin.setValue(3)
self.center_points_spin.setToolTip("Повторные опыты в нулевой точке для оценки дисперсии")
center_layout.addWidget(self.center_points_spin)
settings_layout.addLayout(center_layout)
self.randomize_check = QCheckBox("Рэндомизировать порядок опытов")
self.randomize_check.setChecked(True)
settings_layout.addWidget(self.randomize_check)
settings_layout.addStretch()
settings_group.setLayout(settings_layout)
params_layout.addWidget(settings_group)
# Группа откликов
responses_group = QGroupBox("Отклики (зависимые переменные)")
responses_layout = QVBoxLayout()
self.responses_table = QTableWidget()
self.responses_table.setColumnCount(2)
self.responses_table.setHorizontalHeaderLabels(["Отклик", "Единица измерения"])
self.responses_table.setRowCount(2)
sample_responses = [
["Оптическая плотность (OD600)", ""],
["Концентрация целевого продукта", "мг/мл"]
]
sample_responses = [["Оптическая плотность (OD600)", ""], ["Концентрация целевого продукта", "мг/мл"]]
for i, response in enumerate(sample_responses):
for j, value in enumerate(response):
self.responses_table.setItem(i, j, QTableWidgetItem(value))
responses_layout.addWidget(self.responses_table)
response_buttons = QHBoxLayout()
@@ -184,16 +127,13 @@ class ExperimentDesignWindow(QMainWindow):
response_buttons.addWidget(remove_response_btn)
response_buttons.addStretch()
responses_layout.addLayout(response_buttons)
responses_group.setLayout(responses_layout)
params_layout.addWidget(responses_group)
tabs.addTab(params_tab, "📝 Параметры эксперимента")
# Вкладка 2: Матрица планирования
# Вкладка матрицы планирования
plan_tab = QWidget()
plan_layout = QVBoxLayout(plan_tab)
plan_info = QLabel("Полнофакторный план эксперимента с центральными точками")
plan_info.setAlignment(Qt.AlignCenter)
plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
@@ -203,145 +143,105 @@ class ExperimentDesignWindow(QMainWindow):
scroll.setWidgetResizable(True)
matrix_widget = QWidget()
matrix_layout = QVBoxLayout(matrix_widget)
self.design_matrix = QTableWidget()
matrix_layout.addWidget(self.design_matrix)
scroll.setWidget(matrix_widget)
plan_layout.addWidget(scroll)
buttons_layout = QHBoxLayout()
generate_btn = QPushButton("🔄 Сгенерировать план эксперимента")
generate_btn = QPushButton("Сгенерировать план эксперимента")
generate_btn.clicked.connect(self.generate_design_matrix)
buttons_layout.addWidget(generate_btn)
export_btn = QPushButton("📊 Экспорт в CSV")
export_btn.clicked.connect(self.export_to_csv)
buttons_layout.addWidget(export_btn)
buttons_layout.addStretch()
plan_layout.addLayout(buttons_layout)
self.plan_info_label = QLabel("")
self.plan_info_label.setStyleSheet("color: #666; font-size: 12px; padding: 5px;")
plan_layout.addWidget(self.plan_info_label)
tabs.addTab(plan_tab, "📊 Матрица планирования")
# Вкладка 3: Анализ результатов
# Вкладка анализа
analysis_tab = QWidget()
analysis_layout = QVBoxLayout(analysis_tab)
analysis_info = QLabel("Введите результаты экспериментов для анализа")
analysis_info.setAlignment(Qt.AlignCenter)
analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
analysis_layout.addWidget(analysis_info)
self.results_table = QTableWidget()
analysis_layout.addWidget(self.results_table)
analyze_btn = QPushButton("📈 Провести регрессионный анализ")
analyze_btn = QPushButton("Провести регрессионный анализ")
analyze_btn.clicked.connect(self.perform_analysis)
analysis_layout.addWidget(analyze_btn)
self.analysis_output = QTextEdit()
self.analysis_output.setReadOnly(True)
self.analysis_output.setMaximumHeight(200)
analysis_layout.addWidget(self.analysis_output)
tabs.addTab(analysis_tab, "📐 Анализ результатов")
tabs.addTab(analysis_tab, "📈 Анализ результатов")
layout.addWidget(tabs)
btn_layout = QHBoxLayout()
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 = QPushButton("Закрыть")
close_btn.clicked.connect(self.close)
btn_layout.addStretch()
btn_layout.addWidget(close_btn)
layout.addLayout(btn_layout)
self.generated_design = None
self.factors_data = None
def on_factor_changed(self, item):
"""При изменении нулевого уровня или шага пересчитываем верхний и нижний уровни"""
row = item.row()
col = item.column()
# Если изменили нулевой уровень (колонка 1) или шаг (колонка 2)
if col in [1, 2]:
try:
center = float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0
step = float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0
# Пересчитываем верхний и нижний уровни
high = center + step
low = center - step
# Обновляем ячейки (временно отключаем сигнал)
self.factors_table.blockSignals(True)
high_item = self.factors_table.item(row, 3)
if high_item:
high_item.setText(f"{high:.3f}".rstrip('0').rstrip('.'))
low_item = self.factors_table.item(row, 4)
if low_item:
low_item.setText(f"{low:.3f}".rstrip('0').rstrip('.'))
if self.factors_table.item(row, 3):
self.factors_table.item(row, 3).setText(f"{high:.3f}".rstrip('0').rstrip('.'))
if self.factors_table.item(row, 4):
self.factors_table.item(row, 4).setText(f"{low:.3f}".rstrip('0').rstrip('.'))
self.factors_table.blockSignals(False)
except ValueError:
pass
def add_factor_row(self):
"""Добавляет строку для нового фактора"""
row = self.factors_table.rowCount()
self.factors_table.insertRow(row)
self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}"))
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
# Верхний и нижний уровни - только для чтения
high_item = QTableWidgetItem("1")
high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable)
high_item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(row, 3, high_item)
low_item = QTableWidgetItem("-1")
low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable)
low_item.setBackground(QColor(240, 240, 240))
self.factors_table.setItem(row, 4, low_item)
self.factors_table.setItem(row, 5, QTableWidgetItem(""))
def remove_factor_row(self):
"""Удаляет последнюю строку факторов"""
if self.factors_table.rowCount() > 1:
self.factors_table.removeRow(self.factors_table.rowCount() - 1)
def add_response_row(self):
"""Добавляет строку для нового отклика"""
row = self.responses_table.rowCount()
self.responses_table.insertRow(row)
self.responses_table.setItem(row, 0, QTableWidgetItem(f"Отклик_{row+1}"))
self.responses_table.setItem(row, 1, QTableWidgetItem(""))
def remove_response_row(self):
"""Удаляет последнюю строку откликов"""
if self.responses_table.rowCount() > 1:
self.responses_table.removeRow(self.responses_table.rowCount() - 1)
def get_factors_data(self):
"""Получает данные о факторах"""
factors = []
for row in range(self.factors_table.rowCount()):
try:
@@ -359,27 +259,16 @@ class ExperimentDesignWindow(QMainWindow):
return factors
def calculate_factorial_design(self, factors):
"""Генерирует полнофакторный план 2^k с центральными точками"""
k = len(factors)
if k == 0:
return []
# Генерируем 2^k комбинаций
n_factorial = 2 ** k
design = []
for i in range(n_factorial):
experiment = {}
for j in range(k):
# Кодированный уровень (-1 или +1)
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1
# Переводим в натуральные значения
if coded_level == -1:
natural_value = factors[j]['low']
else:
natural_value = factors[j]['high']
natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high']
experiment[f"Фактор_{j+1}"] = {
'coded': coded_level,
'natural': natural_value,
@@ -388,7 +277,6 @@ class ExperimentDesignWindow(QMainWindow):
}
design.append(experiment)
# Добавляем центральные точки
n_center = self.center_points_spin.value()
for i in range(n_center):
center_experiment = {}
@@ -403,48 +291,34 @@ class ExperimentDesignWindow(QMainWindow):
center_experiment['center_num'] = i + 1
design.append(center_experiment)
# Рэндомизация порядка опытов
if self.randomize_check.isChecked():
import random
random.shuffle(design)
return design
def generate_design_matrix(self):
"""Генерирует и отображает матрицу планирования"""
factors = self.get_factors_data()
if len(factors) == 0:
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
return
# Генерируем план
self.factors_data = factors
design = self.calculate_factorial_design(factors)
self.generated_design = design
# Количество опытов
n_experiments = len(design)
n_factors = len(factors)
# Настройка таблицы
self.design_matrix.setRowCount(n_experiments)
self.design_matrix.setColumnCount(n_factors + 2)
# Заголовки
headers = ["№ опыта"] + [f["name"] for f in factors] + ["Тип точки"]
self.design_matrix.setHorizontalHeaderLabels(headers)
# Заполняем матрицу
for exp_idx, experiment in enumerate(design):
# Номер опыта
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
# Значения факторов
for factor_idx in range(n_factors):
factor_key = f"Фактор_{factor_idx + 1}"
value = experiment[factor_key]['natural']
unit = factors[factor_idx]['unit']
# Форматируем значение
if isinstance(value, float):
if value == int(value):
display_value = str(int(value))
@@ -452,123 +326,76 @@ class ExperimentDesignWindow(QMainWindow):
display_value = f"{value:.3f}".rstrip('0').rstrip('.')
else:
display_value = str(value)
if unit:
display_value += f" {unit}"
item = QTableWidgetItem(display_value)
# Подсветка центральных точек
if experiment.get('is_center', False):
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, factor_idx + 1, item)
# Тип точки
if experiment.get('is_center', False):
type_item = QTableWidgetItem(f"Центральная #{experiment['center_num']}")
type_item.setBackground(QColor(255, 255, 200))
else:
# Показываем комбинацию уровней
levels = []
for factor_idx in range(n_factors):
factor_key = f"Фактор_{factor_idx + 1}"
coded = experiment[factor_key]['coded']
levels.append("+" if coded == 1 else "-")
type_item = QTableWidgetItem(f"Факторная ({''.join(levels)})")
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
# Настройка ширины колонок
self.design_matrix.resizeColumnsToContents()
# Обновляем информацию
n_factorial = 2 ** n_factors
n_center = self.center_points_spin.value()
self.plan_info_label.setText(
f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов"
)
# Настраиваем таблицу для ввода результатов
self.plan_info_label.setText(f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов")
self.setup_results_table(n_experiments)
QMessageBox.information(self, "Успех",
f"Сгенерирован план для {n_factors} факторов\n"
f"Факторных точек: {n_factorial}\n"
f"Центральных точек: {n_center}\n"
f"Всего опытов: {n_experiments}\n\n"
f"Центральные точки позволяют оценить дисперсию воспроизводимости")
QMessageBox.information(self, "Успех", f"Сгенерирован план для {n_factors} факторов\nФакторных точек: {n_factorial}\nЦентральных точек: {n_center}\nВсего опытов: {n_experiments}")
def setup_results_table(self, n_experiments):
"""Настраивает таблицу для ввода результатов"""
n_responses = self.responses_table.rowCount()
self.results_table.setRowCount(n_experiments)
self.results_table.setColumnCount(n_responses + 1)
# Заголовки
headers = ["№ опыта"] + [self.responses_table.item(i, 0).text() if self.responses_table.item(i, 0) else f"Отклик_{i+1}"
for i in range(n_responses)]
headers = ["№ опыта"] + [self.responses_table.item(i, 0).text() if self.responses_table.item(i, 0) else f"Отклик_{i+1}" for i in range(n_responses)]
self.results_table.setHorizontalHeaderLabels(headers)
# Заполняем номера опытов
for i in range(n_experiments):
self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1)))
# Настройка ширины колонок
self.results_table.setColumnWidth(0, 80)
for i in range(n_responses):
self.results_table.setColumnWidth(i + 1, 150)
def export_to_csv(self):
"""Экспортирует матрицу планирования в CSV"""
if self.design_matrix.rowCount() == 0:
QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!")
return
filename, _ = QFileDialog.getSaveFileName(
self,
"Сохранить план эксперимента",
"",
"CSV Files (*.csv);;All Files (*)"
)
filename, _ = QFileDialog.getSaveFileName(self, "Сохранить план эксперимента", "", "CSV Files (*.csv);;All Files (*)")
if filename:
if not filename.lower().endswith('.csv'):
filename += '.csv'
try:
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.writer(f)
# Заголовки
headers = []
for j in range(self.design_matrix.columnCount()):
header_item = self.design_matrix.horizontalHeaderItem(j)
headers.append(header_item.text() if header_item else f"Колонка_{j+1}")
writer.writerow(headers)
# Данные
for i in range(self.design_matrix.rowCount()):
row = []
for j in range(self.design_matrix.columnCount()):
item = self.design_matrix.item(i, j)
row.append(item.text() if item else "")
row = [self.design_matrix.item(i, j).text() if self.design_matrix.item(i, j) else "" for j in range(self.design_matrix.columnCount())]
writer.writerow(row)
QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}")
def perform_analysis(self):
"""Проводит регрессионный анализ"""
n_responses = self.responses_table.rowCount()
if n_responses == 0:
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один отклик!")
return
if self.generated_design is None:
QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!")
return
# Собираем результаты
results = []
for i in range(self.results_table.rowCount()):
row_results = []
@@ -583,50 +410,36 @@ class ExperimentDesignWindow(QMainWindow):
row_results.append(None)
results.append(row_results)
# Проверяем, что все результаты введены
missing = False
for i, row in enumerate(results):
for j, val in enumerate(row):
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
# Анализ
self.analysis_output.clear()
self.analysis_output.append("=" * 60)
self.analysis_output.append("РЕЗУЛЬТАТЫ РЕГРЕССИОННОГО АНАЛИЗА")
self.analysis_output.append("=" * 60)
factors = self.get_factors_data()
design = self.calculate_factorial_design(factors)
design = self.generated_design
for resp_idx in range(n_responses):
resp_name = self.responses_table.item(resp_idx, 0).text()
self.analysis_output.append(f"\n📊 Отклик: {resp_name}")
self.analysis_output.append("-" * 40)
# Собираем значения отклика
y_values = [results[i][resp_idx] for i in range(len(results))]
# Среднее значение
mean_y = np.mean(y_values)
self.analysis_output.append(f"Среднее значение: {mean_y:.4f}")
# Дисперсия
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
self.analysis_output.append(f"Стандартное отклонение: {std_dev:.4f}")
cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0
# Коэффициент вариации
if mean_y != 0:
cv = (std_dev / mean_y) * 100
self.analysis_output.append(f"Среднее значение: {mean_y:.4f}")
self.analysis_output.append(f"Общая дисперсия: {variance:.4f}")
self.analysis_output.append(f"Стандартное отклонение: {std_dev:.4f}")
self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%")
# Разделяем факторные и центральные точки
factorial_y = []
center_y = []
for i, exp in enumerate(design):
@@ -636,34 +449,22 @@ class ExperimentDesignWindow(QMainWindow):
factorial_y.append(y_values[i])
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" Среднее: {np.mean(center_y):.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:
factorial_variance = np.var(factorial_y, ddof=1) if len(factorial_y) > 1 else 0
if factorial_variance > 0:
fisher = max(factorial_variance, center_variance) / min(factorial_variance, center_variance)
self.analysis_output.append(f"\nКритерий Фишера (F-отношение): {fisher:.4f}")
if fisher < 4.0:
self.analysis_output.append(" ✅ Модель адекватна экспериментальным данным")
self.analysis_output.append("✅ Модель адекватна экспериментальным данным")
else:
self.analysis_output.append(" ⚠️ Модель может быть неадекватна, требуется проверка")
self.analysis_output.append("⚠️ Модель может быть неадекватна, требуется проверка")
self.analysis_output.append("\n" + "=" * 60)
self.analysis_output.append("Анализ завершен")
self.analysis_output.append("Анализ завершен")
def show_placeholder_message(self):
"""Показывает сообщение о том, что функция в разработке"""
QMessageBox.information(
self,
"В разработке",
"🧪 Функция в стадии разработки!\n\nБлижайшие обновления:\n"
"✅ Экспорт в Excel\n"
"✅ Построение поверхностей отклика\n"
"✅ Графики главных эффектов\n"
"✅ Полный регрессионный анализ"
)
def show_error(self, message: str):
QMessageBox.critical(self, "Ошибка", message)
+21 -65
View File
@@ -1,10 +1,6 @@
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QWidget, QFrame)
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QFrame)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from controller import Controller
from experiment_design import ExperimentDesignWindow
class MainWindow(QMainWindow):
def __init__(self):
@@ -12,33 +8,14 @@ class MainWindow(QMainWindow):
self.setWindowTitle("Цифровой помощник биохимика - Главное меню")
self.setGeometry(300, 200, 700, 500)
self.setStyleSheet("""
QMainWindow {
background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #e8f4f8, stop:1 #f0f0f0);
}
QPushButton {
background-color: #2196F3;
color: white;
border: none;
padding: 15px;
font-size: 16px;
font-weight: bold;
border-radius: 8px;
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;
}
QMainWindow { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #e8f4f8, stop:1 #f0f0f0); }
QPushButton { background-color: #2196F3; color: white; border: none; padding: 15px; font-size: 16px; font-weight: bold; border-radius: 8px; }
QPushButton:hover { background-color: #1976D2; }
QLabel { color: #333; font-size: 14px; }
""")
self._init_ui()
self.medium_calculator = None
self.experiment_window = None
def _init_ui(self):
central_widget = QWidget()
@@ -47,7 +24,6 @@ class MainWindow(QMainWindow):
layout.setSpacing(20)
layout.setContentsMargins(50, 50, 50, 50)
# Заголовок
title_label = QLabel("Цифровой помощник биохимика")
title_font = QFont()
title_font.setPointSize(20)
@@ -57,7 +33,6 @@ class MainWindow(QMainWindow):
title_label.setStyleSheet("color: #1565C0;")
layout.addWidget(title_label)
# Подзаголовок
subtitle_label = QLabel("Биотехнологические инструменты для лаборатории")
subtitle_font = QFont()
subtitle_font.setPointSize(12)
@@ -68,15 +43,12 @@ class MainWindow(QMainWindow):
layout.addSpacing(20)
# Кнопка 1: Калькулятор питательных сред
btn_medium = QPushButton("Калькулятор питательных сред")
btn_medium = QPushButton("🧪 Калькулятор питательных сред")
btn_medium.setMinimumHeight(80)
btn_medium.clicked.connect(self.open_medium_calculator)
layout.addWidget(btn_medium)
# Описание кнопки 1
desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\n"
"разбавления реагентов и автоматическим расчётом растворителя")
desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\nразбавления реагентов и автоматическим расчётом растворителя")
desc1_label.setAlignment(Qt.AlignCenter)
desc1_label.setWordWrap(True)
desc1_label.setStyleSheet("color: #555; font-size: 11px;")
@@ -84,15 +56,12 @@ class MainWindow(QMainWindow):
layout.addSpacing(15)
# Кнопка 2: Планирование эксперимента
btn_experiment = QPushButton("Планирование эксперимента (DoE)")
btn_experiment = QPushButton("📊 Планирование эксперимента (DoE)")
btn_experiment.setMinimumHeight(80)
btn_experiment.clicked.connect(self.open_experiment_designer)
layout.addWidget(btn_experiment)
# Описание кнопки 2
desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\n"
"многомерный анализ и визуализация")
desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\nмногомерный анализ и визуализация")
desc2_label.setAlignment(Qt.AlignCenter)
desc2_label.setWordWrap(True)
desc2_label.setStyleSheet("color: #555; font-size: 11px;")
@@ -100,46 +69,33 @@ class MainWindow(QMainWindow):
layout.addSpacing(15)
# Линия-разделитель
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
layout.addWidget(line)
# Нижняя панель
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;")
bottom_layout.addWidget(version_label)
bottom_layout.addStretch()
# Кнопка выхода
btn_exit = QPushButton("Выход")
btn_exit.setMaximumWidth(150)
btn_exit.setStyleSheet("""
QPushButton {
background-color: #f44336;
padding: 8px;
font-size: 14px;
}
QPushButton:hover {
background-color: #da190b;
}
""")
btn_exit.setStyleSheet("QPushButton { background-color: #f44336; padding: 8px; font-size: 14px; } QPushButton:hover { background-color: #da190b; }")
btn_exit.clicked.connect(self.close)
bottom_layout.addWidget(btn_exit)
layout.addLayout(bottom_layout)
def open_medium_calculator(self):
"""Открывает калькулятор питательной среды"""
self.medium_calculator = Controller()
self.medium_calculator.view.show()
from .medium_view import MediumCalculatorWindow
from ..controllers.medium_controller import MediumController
self.medium_calculator = MediumCalculatorWindow()
self.medium_controller = MediumController(self.medium_calculator)
self.medium_calculator.show()
def open_experiment_designer(self):
"""Открывает окно планирования эксперимента"""
from .experiment_view import ExperimentDesignWindow
from ..controllers.experiment_controller import ExperimentController
self.experiment_window = ExperimentDesignWindow()
self.experiment_controller = ExperimentController(self.experiment_window)
self.experiment_window.show()
+207
View File
@@ -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)
-422
View File
@@ -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())