Compare commits

..

9 Commits

Author SHA1 Message Date
artemiy 85e6aa6c72 Убраны лишние файлы 2026-05-29 08:57:28 +05:00
artemiy 2392735641 Теперь подсветка факторов эксперимента подсвечиваются соответственно значениям: + зелёным - красным, 0 (факторы с шагом в 0) - светлосерым. Убран лишний столбец в матрице факторного эксперимента 2026-05-29 08:55:43 +05:00
artemiy 6400f04f1c Реализован расчёт растворителя в матрице планирования 2026-05-27 23:32:11 +05:00
artemiy acf3ad0dd5 Добавленна поддержка отображения, сохранинения, и загрузки растворителя во вкладке факторнго эксперимента. Убрана галочка рАндомизировать по умолчанию 2026-05-27 15:41:09 +05:00
artemiy a2bc606336 Теперь фактор, шаг которого равен нулю, не учитывается, но отображается отдельным столбцом с однаковым значением. 2026-05-27 12:40:47 +05:00
artemiy f479674332 Исправлен расчёт +1 и -1 факторов эксперимента 2026-05-27 01:09:11 +05:00
artemiy 1ddfe20a8d Оптимизмрован код, исправлены параметры отображения факторов экспримента 2026-05-26 22:51:11 +05:00
artemiy 63b5f0a49f Добавлен файл темы. Исправлен счёт веса реагентов 2026-05-22 14:26:58 +05:00
artemiy 75b710bf59 Очищены лишние файлы 2026-05-18 23:23:53 +05:00
8 changed files with 1155 additions and 1316 deletions
+3
View File
@@ -1,6 +1,9 @@
# Python # Python
backup backup
Backup Backup
*.csv
*.xls
*.xlsx
*.json *.json
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
+62 -126
View File
@@ -13,50 +13,70 @@
from typing import List, Dict, Tuple, Optional from typing import List, Dict, Tuple, Optional
import random import random
import numpy as np import numpy as np
from calculations.medium import (
calculate_medium_composition,
convert_units,
VOLUME_UNITS,
MASS_UNITS
)
from calculations.models.project_data import FactorData
# Типы расчёта шага # Типы расчёта шага
FACTOR_TYPES = { FACTOR_TYPES = {
'absolute': 'абс', # абсолютный шаг 'absolute': 'ед.', # абсолютный шаг
'relative': '%', # относительный шаг (процент от нулевого уровня) 'relative': '%', # относительный шаг (процент от нулевого уровня)
} }
def calculate_factor_levels( def calculate_factor_levels(
center_value: float, factor: 'FactorData',
step_value: float, total_volume: float = 1000,
step_type: str, volume_unit: str = "мл"
base_value: float = None ) -> 'FactorData':
) -> Tuple[float, float]: """Рассчитывает high/low уровни фактора через калькулятор сред"""
"""
РАССЧИТЫВАЕТ ВЕРХНИЙ И НИЖНИЙ УРОВНИ ФАКТОРА
Параметры: # Функция для расчёта количества реагента при заданном проценте
center_value: нулевой уровень фактора (центральная точка) def calc_amount(percentage: float) -> float:
step_value: значение шага reagents = [{
step_type: тип шага ("абс" - абсолютный, "%" - относительный) 'name': factor.name,
base_value: базовое значение для относительного шага (если None, используется center_value) 'percentage': max(0, percentage),
'unit': factor.unit,
'dilution_factor': factor.dilution_factor or 1.0
}]
result = calculate_medium_composition(total_volume, volume_unit, reagents)
return result['reagents'][0]['calculated_amount']
Возвращает:
(high_level, low_level): верхний и нижний уровни
Пример: # Определяем проценты для верхнего и нижнего уровней
>>> calculate_factor_levels(100, 10, "%") if factor.step_type == "%":
(110.0, 90.0) low = calc_amount(factor.percentage - factor.step) # low - нижний уровень
>>> calculate_factor_levels(100, 20, "абс") high = calc_amount(factor.percentage + factor.step) # high - верхний уровень
(120.0, 80.0) else: # "ед."
""" # Конвертируем абсолютный шаг в проценты
# Определяем абсолютное значение шага low = calc_amount(factor.percentage) - factor.step
if step_type == "%": high = calc_amount(factor.center) + factor.step
base = base_value if base_value is not None else center_value
step_abs = center_value * step_value / 100
else: # "абс" или "absolute"
step_abs = step_value
high_level = center_value + step_abs # ВОЗВРАЩАЕМ НОВЫЙ ОБЪЕКТ FactorData
low_level = center_value - step_abs return FactorData(
name=factor.name,
center=factor.center,
low=low, # low - нижний уровень
high=high, # high - верхний уровень
step=factor.step,
step_type=factor.step_type,
unit=factor.unit,
percentage=factor.percentage or factor.center,
dilution_factor=factor.dilution_factor
)
return high_level, low_level def calculate_all_factors_levels(factors: List['FactorData'], **kwargs) -> List['FactorData']:
"""Рассчитывает уровни для всех факторов"""
return [calculate_factor_levels(f, **kwargs) for f in factors]
def get_active_factors(factors):
return [f for f in factors if f['step'] != 0]
def get_inactive_factors(factors):
return [f for f in factors if f['step'] == 0]
def generate_factorial_design( def generate_factorial_design(
factors: List[Dict], factors: List[Dict],
@@ -64,56 +84,11 @@ def generate_factorial_design(
randomize: bool = True randomize: bool = True
) -> List[Dict]: ) -> List[Dict]:
""" """
ГЕНЕРИРУЕТ ПОЛНОФАКТОРНЫЙ ПЛАН ЭКСПЕРИМЕНТА Генерирует полнофакторный план 2^k с правильным порядком изменения факторов:
- фактор 1 меняется через 1 эксперимент (2^0)
Создаёт матрицу планирования для 2^k полнофакторного эксперимента - фактор 2 меняется через 2 эксперимента (2^1)
с добавлением центральных точек. - фактор 3 меняется через 4 эксперимента (2^2)
- и т.д.
ПАРАМЕТРЫ:
----------
factors : List[Dict]
Список факторов. Каждый фактор - словарь с ключами:
- name (str): название фактора
- low (float): нижний уровень (-1)
- high (float): верхний уровень (+1)
- center (float): нулевой уровень (0)
- unit (str): единица измерения
- step (float, опционально): шаг варьирования
- step_type (str, опционально): тип шага ("абс" или "%")
Минимально необходимые ключи: name, low, high, center, unit
center_points : int
Количество центральных точек (повторений в центре плана)
По умолчанию 3
randomize : bool
Перемешивать ли порядок опытов случайным образом
По умолчанию True
ВОЗВРАЩАЕТ:
-----------
List[Dict]
Список экспериментов. Каждый эксперимент - словарь:
- для каждого фактора: "Фактор_N" с полями:
- coded: кодированное значение (-1, 0, +1)
- natural: натуральное значение
- name: название фактора
- unit: единица измерения
- is_center (bool): является ли точка центральной
- center_num (int): номер центральной точки (если is_center)
ПРИМЕР ИСПОЛЬЗОВАНИЯ:
---------------------
>>> factors = [
... {'name': 'Температура', 'low': 25, 'high': 37, 'center': 31, 'unit': '°C'},
... {'name': 'pH', 'low': 6.5, 'high': 7.5, 'center': 7.0, 'unit': ''}
... ]
>>> design = generate_factorial_design(factors, center_points=2)
>>> print(len(design)) # 2^2 + 2 = 6
6
>>> design[0]['Фактор_1']['coded'] # первый фактор в первом опыте
-1
""" """
k = len(factors) k = len(factors)
@@ -123,15 +98,14 @@ def generate_factorial_design(
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 для 0, +1 для 1 в бите # Правильный порядок битов: младший бит - первый фактор
# (k-1-j) для правильного порядка факторов # (j) - для прямого порядка: фактор 1 меняется чаще всего
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1 coded_level = -1 if (i >> j) & 1 == 0 else 1
# Натуральное значение
natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high'] natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high']
experiment[f"Фактор_{j+1}"] = { experiment[f"Фактор_{j+1}"] = {
@@ -156,7 +130,7 @@ def generate_factorial_design(
center_experiment['center_num'] = i + 1 center_experiment['center_num'] = i + 1
design.append(center_experiment) design.append(center_experiment)
# Перемешивание порядка # Перемешивание порядка (опционально)
if randomize: if randomize:
random.shuffle(design) random.shuffle(design)
@@ -273,44 +247,6 @@ def analyze_experiment(
return analysis return analysis
def create_factor_from_reagent(
reagent: Dict,
total_volume: float,
volume_unit: str,
step_percent: float = 10.0
) -> Dict:
"""
СОЗДАЁТ ФАКТОР ИЗ РЕАГЕНТА (для интеграции калькулятора и DoE)
Преобразует рассчитанный реагент в фактор для планирования эксперимента.
Параметры:
reagent: рассчитанный реагент (из calculate_medium_composition)
total_volume: общий объём среды
volume_unit: единица объёма
step_percent: шаг варьирования в процентах от нулевого уровня
Возвращает:
Dict: фактор для использования в generate_factorial_design()
"""
center_value = reagent.get('undiluted_amount', reagent.get('calculated_amount', 0))
step_value = center_value * step_percent / 100
high_level, low_level = calculate_factor_levels(
center_value, step_value, "абс"
)
return {
'name': reagent['name'],
'center': center_value,
'low': low_level,
'high': high_level,
'step': step_value,
'step_type': 'абс',
'unit': reagent.get('unit', volume_unit),
'percentage': reagent.get('percentage', 0),
'dilution_factor': reagent.get('dilution_factor', 1.0)
}
def create_factor_from_reagent( def create_factor_from_reagent(
reagent: Dict, reagent: Dict,
@@ -336,7 +272,7 @@ def create_factor_from_reagent(
step_value = center_value * step_percent / 100 step_value = center_value * step_percent / 100
high_level, low_level = calculate_factor_levels( high_level, low_level = calculate_factor_levels(
center_value, step_value, "абс" center_value, step_value, "ед."
) )
return { return {
+8 -9
View File
@@ -52,12 +52,10 @@ def convert_units(value: float, from_unit: str, to_unit: str = None) -> float:
units_map = MASS_UNITS units_map = MASS_UNITS
else: else:
raise ValueError(f"Неизвестная единица измерения: {from_unit}") raise ValueError(f"Неизвестная единица измерения: {from_unit}")
# Конвертируем в базовую единицу (мкл для объёма, мг для массы) # Конвертируем в базовую единицу (мкл для объёма, мг для массы)
value_in_base = value * units_map[from_unit] value_in_base = value * units_map[from_unit]
# Если нужна конвертация в другую единицу # Если нужна конвертация в другую единицу
if to_unit and to_unit in units_map: if from_unit and to_unit in units_map:
return value_in_base / units_map[to_unit] return value_in_base / units_map[to_unit]
return value_in_base return value_in_base
@@ -158,7 +156,12 @@ def calculate_medium_composition(
# Извлекаем параметры с значениями по умолчанию # Извлекаем параметры с значениями по умолчанию
percentage = reagent.get('percentage', 0) percentage = reagent.get('percentage', 0)
unit = reagent.get('unit', 'мг') unit = reagent.get('unit', 'мг')
# print ("unit = ",unit) if unit in VOLUME_UNITS:
base_unit = "мкл"
elif unit in MASS_UNITS:
base_unit = "мг"
else:
raise ValueError(f"Неизвестная единица измерения: {from_unit}")
# conversion_factor = reagent.get('conversion_factor', 1.0) # conversion_factor = reagent.get('conversion_factor', 1.0)
dilution_factor = reagent.get('dilution_factor', 1.0) dilution_factor = reagent.get('dilution_factor', 1.0)
@@ -167,20 +170,16 @@ def calculate_medium_composition(
# 1. Объём реагента в среде (исходя из процента) # 1. Объём реагента в среде (исходя из процента)
amount_in_base = (percentage / 100) * total_base amount_in_base = (percentage / 100) * total_base
# print ("amount_in_base = ",amount_in_base)
# 2. Применяем коэффициент конверсии # 2. Применяем коэффициент конверсии
# adjusted_amount_base = amount_in_base * conversion_factor # adjusted_amount_base = amount_in_base * conversion_factor
# 3. Конвертируем в нужную единицу (без учёта разбавления) # 3. Конвертируем в нужную единицу (без учёта разбавления)
# undiluted_amount = convert_units(adjusted_amount_base, volume_unit, unit) # undiluted_amount = convert_units(adjusted_amount_base, volume_unit, unit)
undiluted_amount = convert_units(amount_in_base, 'мкл', unit) undiluted_amount = convert_units(amount_in_base, base_unit, unit)
# print ("volume_unit = ",volume_unit)
# 4. Применяем разбавление # 4. Применяем разбавление
if dilution_factor <= 0: if dilution_factor <= 0:
dilution_factor = 1.0 dilution_factor = 1.0
diluted_amount = undiluted_amount * dilution_factor diluted_amount = undiluted_amount * dilution_factor
# print ("diluted_amount = ", diluted_amount)
# 5. Для объёмных реагентов учитываем в расчёте растворителя # 5. Для объёмных реагентов учитываем в расчёте растворителя
if is_volume: if is_volume:
reagent_volume_base = convert_units(diluted_amount, unit) reagent_volume_base = convert_units(diluted_amount, unit)
+17 -2
View File
@@ -7,7 +7,13 @@ from dataclasses import dataclass, asdict
from datetime import datetime from datetime import datetime
import json import json
VERSION="alpha_0.3"
"""
Условно
0.1 - Разработка калькулятора сред
0.2 - Разработка факторов эксперимента
0.3 - разработка матрицы планирования
"""
@dataclass @dataclass
class ReagentData: class ReagentData:
"""Данные реагента""" """Данные реагента"""
@@ -111,7 +117,7 @@ class ProjectData:
project_name: str project_name: str
created_at: str created_at: str
modified_at: str modified_at: str
version: str = "1.0" version: str = VERSION
# Данные калькулятора сред # Данные калькулятора сред
medium_total_volume: float = 1000.0 medium_total_volume: float = 1000.0
@@ -120,6 +126,9 @@ class ProjectData:
medium_reagents: List[ReagentData] = None medium_reagents: List[ReagentData] = None
# Данные эксперимента # Данные эксперимента
experiment_total_volume: float = 1000.0
experiment_volume_unit: str = "мл"
experiment_solvent: str = "Вода"
experiment_factors: List[FactorData] = None experiment_factors: List[FactorData] = None
experiment_responses: List[ResponseData] = None experiment_responses: List[ResponseData] = None
experiment_center_points: int = 3 experiment_center_points: int = 3
@@ -150,6 +159,9 @@ class ProjectData:
'reagents': [r.to_dict() for r in self.medium_reagents] 'reagents': [r.to_dict() for r in self.medium_reagents]
}, },
'experiment': { 'experiment': {
'total_volume': self.medium_total_volume,
'volume_unit': self.medium_volume_unit,
'solvent': self.medium_solvent,
'factors': [f.to_dict() for f in self.experiment_factors], 'factors': [f.to_dict() for f in self.experiment_factors],
'responses': [r.to_dict() for r in self.experiment_responses], 'responses': [r.to_dict() for r in self.experiment_responses],
'center_points': self.experiment_center_points, 'center_points': self.experiment_center_points,
@@ -175,6 +187,9 @@ class ProjectData:
medium_volume_unit=medium.get('volume_unit', 'мл'), medium_volume_unit=medium.get('volume_unit', 'мл'),
medium_solvent=medium.get('solvent', 'Вода'), medium_solvent=medium.get('solvent', 'Вода'),
medium_reagents=[ReagentData.from_dict(r) for r in medium.get('reagents', [])], medium_reagents=[ReagentData.from_dict(r) for r in medium.get('reagents', [])],
experiment_total_volume=experiment.get('total_volume', 1000.0),
experiment_volume_unit=experiment.get('volume_unit', 'мл'),
experiment_solvent=experiment.get('solvent', 'Вода'),
experiment_factors=[FactorData.from_dict(f) for f in experiment.get('factors', [])], experiment_factors=[FactorData.from_dict(f) for f in experiment.get('factors', [])],
experiment_responses=[ResponseData.from_dict(r) for r in experiment.get('responses', [])], experiment_responses=[ResponseData.from_dict(r) for r in experiment.get('responses', [])],
experiment_center_points=experiment.get('center_points', 3), experiment_center_points=experiment.get('center_points', 3),
+259 -236
View File
@@ -2,10 +2,9 @@
""" """
Единый графический интерфейс для калькулятора сред и DoE Единый графический интерфейс для калькулятора сред и DoE
""" """
from theme import Colors, Fonts, Spacing, ButtonStyles, get_full_stylesheet, apply_theme, TitleStyles
import sys import sys
from typing import List, Dict, Optional from typing import List, Dict, Optional
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTabWidget, QGroupBox, QLabel, QPushButton, QTableWidget, QTabWidget, QGroupBox, QLabel, QPushButton, QTableWidget,
@@ -28,9 +27,10 @@ from calculations.doe import (
generate_factorial_design, generate_factorial_design,
analyze_experiment, analyze_experiment,
calculate_factor_levels, calculate_factor_levels,
get_active_factors,
get_inactive_factors,
FACTOR_TYPES FACTOR_TYPES
) )
# Импорт моделей для JSON # Импорт моделей для JSON
try: try:
from calculations.models.project_data import ( from calculations.models.project_data import (
@@ -49,6 +49,7 @@ class MainWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("Биохимический помощник") self.setWindowTitle("Биохимический помощник")
self.setStyleSheet(get_full_stylesheet())
self.setGeometry(100, 100, 1300, 800) self.setGeometry(100, 100, 1300, 800)
self.setStyleSheet(self._get_stylesheet()) self.setStyleSheet(self._get_stylesheet())
@@ -60,22 +61,7 @@ class MainWindow(QMainWindow):
self._add_file_toolbar() self._add_file_toolbar()
def _get_stylesheet(self): def _get_stylesheet(self):
return """ return get_full_stylesheet()
QMainWindow { background-color: #f0f0f0; }
QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 8px; margin-top: 12px; padding-top: 12px; }
QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; color: #2c3e50; }
QPushButton { background-color: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 5px; font-weight: bold; }
QPushButton:hover { background-color: #2980b9; }
QPushButton#success { background-color: #27ae60; }
QPushButton#success:hover { background-color: #219a52; }
QPushButton#danger { background-color: #e74c3c; }
QPushButton#danger:hover { background-color: #c0392b; }
QTableWidget { gridline-color: #ddd; background-color: white; alternate-background-color: #f9f9f9; }
QHeaderView::section { background-color: #34495e; color: white; padding: 6px; font-weight: bold; }
QTabWidget::pane { border: 1px solid #ccc; border-radius: 5px; }
QTabBar::tab { background-color: #ecf0f1; padding: 8px 16px; margin-right: 2px; }
QTabBar::tab:selected { background-color: #3498db; color: white; }
"""
def _add_file_toolbar(self): def _add_file_toolbar(self):
"""Добавляет панель инструментов с кнопками сохранения/загрузки""" """Добавляет панель инструментов с кнопками сохранения/загрузки"""
@@ -99,12 +85,8 @@ class MainWindow(QMainWindow):
# Заголовок # Заголовок
title = QLabel("Цифровой помощник биохимика") title = QLabel("Цифровой помощник биохимика")
title_font = QFont() title.setObjectName("mainTitle") # Стиль подтянется из theme
title_font.setPointSize(18)
title_font.setBold(True)
title.setFont(title_font)
title.setAlignment(Qt.AlignCenter) title.setAlignment(Qt.AlignCenter)
title.setStyleSheet("color: #2c3e50; padding: 10px;")
layout.addWidget(title) layout.addWidget(title)
# Вкладки # Вкладки
@@ -203,19 +185,22 @@ class MainWindow(QMainWindow):
return tab return tab
def _add_reagent_row(self): def _add_reagent_row(self, name = "", percentage = "0", unit = "мл", dilution = "1", amount = ""):
row = self.reagents_table.rowCount() row = self.reagents_table.rowCount()
self.reagents_table.insertRow(row) self.reagents_table.insertRow(row)
self.reagents_table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row+1}")) if name == "":
self.reagents_table.setItem(row, 1, QTableWidgetItem("0")) self.reagents_table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row+1}"))
else:
self.reagents_table.setItem(row, 0, QTableWidgetItem(name))
self.reagents_table.setItem(row, 1, QTableWidgetItem(percentage))
unit_combo = QComboBox() unit_combo = QComboBox()
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"]) unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л"])
unit_combo.setCurrentText("мл") unit_combo.setCurrentText(unit)
self.reagents_table.setCellWidget(row, 2, unit_combo) self.reagents_table.setCellWidget(row, 2, unit_combo)
self.reagents_table.setItem(row, 3, QTableWidgetItem("1")) self.reagents_table.setItem(row, 3, QTableWidgetItem(dilution))
self.reagents_table.setItem(row, 4, QTableWidgetItem("")) self.reagents_table.setItem(row, 4, QTableWidgetItem(amount))
def _remove_reagent_row(self): def _remove_reagent_row(self):
for row in sorted(set(i.row() for i in self.reagents_table.selectedItems()), reverse=True): for row in sorted(set(i.row() for i in self.reagents_table.selectedItems()), reverse=True):
@@ -285,17 +270,15 @@ class MainWindow(QMainWindow):
) )
self.info_label.setText(solvent_text) self.info_label.setText(solvent_text)
self.info_label.setStyleSheet("background-color: #d5f5e3; padding: 8px; border-radius: 5px;") self.info_label.setStyleSheet("background-color: #d5f5e3; padding: 8px; border-radius: 5px;")
# Сохраняем результаты для передачи в DoE # Сохраняем результаты для передачи в DoE
self.last_medium_result = result self.last_medium_result = result
QMessageBox.information(self, "Успех", "Расчёт выполнен успешно!") # QMessageBox.information(self, "Успех", "Расчёт выполнен успешно!")
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e)) QMessageBox.critical(self, "Ошибка", str(e))
# ========== ВКЛАДКА 2: ФАКТОРЫ ЭКСПЕРИМЕНТА ========== # ========== ВКЛАДКА 2: ФАКТОРЫ ЭКСПЕРИМЕНТА ==========
def _create_factors_tab(self): def _create_factors_tab(self):
tab = QWidget() tab = QWidget()
layout = QVBoxLayout(tab) layout = QVBoxLayout(tab)
@@ -322,7 +305,7 @@ class MainWindow(QMainWindow):
"Фактор", "%", "Разбавление", "Нулевой уровень", "Шаг", "Фактор", "%", "Разбавление", "Нулевой уровень", "Шаг",
"Тип шага", "Верхний (+1)", "Нижний (-1)", "Ед. изм." "Тип шага", "Верхний (+1)", "Нижний (-1)", "Ед. изм."
]) ])
self.factors_table.setAlternatingRowColors(True) #self.factors_table.setAlternatingRowColors(True)
factors_layout.addWidget(self.factors_table) factors_layout.addWidget(self.factors_table)
@@ -343,19 +326,43 @@ class MainWindow(QMainWindow):
# Настройки эксперимента # Настройки эксперимента
settings_box = QGroupBox("Настройки эксперимента") settings_box = QGroupBox("Настройки эксперимента")
settings_layout = QHBoxLayout() settings_layout = QVBoxLayout()
settings_layout.addWidget(QLabel("Центральных точек:")) # Параметры среды (из калькулятора)
env_layout = QHBoxLayout()
env_layout.addWidget(QLabel("Общий объём:"))
self.exp_total_volume = QDoubleSpinBox()
self.exp_total_volume.setRange(0.001, 1000000)
self.exp_total_volume.setValue(100)
self.exp_total_volume.setSuffix(" ")
env_layout.addWidget(self.exp_total_volume)
self.exp_volume_unit = QComboBox()
self.exp_volume_unit.addItems(["мл", "л", "мкл", "нл"])
self.exp_volume_unit.setCurrentText("мл")
env_layout.addWidget(self.exp_volume_unit)
env_layout.addSpacing(20)
env_layout.addWidget(QLabel("Растворитель:"))
self.exp_solvent = QLineEdit("Вода")
env_layout.addWidget(self.exp_solvent)
env_layout.addStretch()
settings_layout.addLayout(env_layout)
# Настройки эксперимента
exp_layout = QHBoxLayout()
exp_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)
settings_layout.addWidget(self.center_points_spin) exp_layout.addWidget(self.center_points_spin)
settings_layout.addSpacing(20) exp_layout.addSpacing(20)
self.randomize_check = QCheckBox("Рэндомизировать порядок") self.randomize_check = QCheckBox("Рандомизировать порядок")
self.randomize_check.setChecked(True) self.randomize_check.setChecked(False)
settings_layout.addWidget(self.randomize_check) exp_layout.addWidget(self.randomize_check)
settings_layout.addStretch() exp_layout.addStretch()
settings_layout.addLayout(exp_layout)
settings_box.setLayout(settings_layout) settings_box.setLayout(settings_layout)
layout.addWidget(settings_box) layout.addWidget(settings_box)
@@ -367,33 +374,47 @@ class MainWindow(QMainWindow):
# Добавляем начальный фактор # Добавляем начальный фактор
self._add_factor_row() self._add_factor_row()
# ПОДКЛЮЧАЕМ СИГНАЛ ИЗМЕНЕНИЯ ЯЧЕЕК
self.factors_table.cellChanged.connect(self._on_factor_changed)
return tab return tab
def _add_factor_row(self): def _add_factor_row(self, name = "", percentage = "0", dilution = "0", center = "0", step ="0", step_type = "%", high = "", low = "", unit= "г"):
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}")) if name == "":
self.factors_table.setItem(row, 1, QTableWidgetItem("0")) self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}"))
self.factors_table.setItem(row, 2, QTableWidgetItem("1")) else:
self.factors_table.setItem(row, 3, QTableWidgetItem("0")) self.factors_table.setItem(row, 0, QTableWidgetItem(name))
self.factors_table.setItem(row, 4, QTableWidgetItem("1")) self.factors_table.setItem(row, 1, QTableWidgetItem(percentage))
self.factors_table.setItem(row, 2, QTableWidgetItem(dilution))
self.factors_table.setItem(row, 3, QTableWidgetItem(center))
self.factors_table.setItem(row, 4, QTableWidgetItem(step))
step_type_combo = QComboBox() step_type_combo = QComboBox()
step_type_combo.addItems(["абс", "%"]) step_type_combo.addItems(["%", "ед."])
step_type_combo.setCurrentText(step_type)
step_type_combo.currentTextChanged.connect(lambda: self._on_factor_changed(row, 5))
self.factors_table.setCellWidget(row, 5, step_type_combo) self.factors_table.setCellWidget(row, 5, step_type_combo)
high_item = QTableWidgetItem("1") high_item = QTableWidgetItem(high)
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, 6, high_item) self.factors_table.setItem(row, 6, high_item)
low_item = QTableWidgetItem("-1") low_item = QTableWidgetItem(low)
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, 7, low_item) self.factors_table.setItem(row, 7, low_item)
unit_measure = QComboBox()
unit_measure.addItems(["мкл", "мл", "л", "мг", "г", "кг"])
unit_measure.setCurrentText(unit)
unit_measure.currentTextChanged.connect(lambda: self._on_factor_changed(row, 8))
self.factors_table.setItem(row, 8, QTableWidgetItem("")) self.factors_table.setCellWidget(row, 8, unit_measure)
return row
def _remove_factor_row(self): def _remove_factor_row(self):
for row in sorted(set(i.row() for i in self.factors_table.selectedItems()), reverse=True): for row in sorted(set(i.row() for i in self.factors_table.selectedItems()), reverse=True):
@@ -412,38 +433,25 @@ class MainWindow(QMainWindow):
# Очищаем таблицу факторов # Очищаем таблицу факторов
self.factors_table.setRowCount(0) self.factors_table.setRowCount(0)
for reagent in result['reagents']:
row = self.factors_table.rowCount()
self.factors_table.insertRow(row)
self.exp_total_volume.setValue(result['total_volume'])
self.exp_volume_unit.setCurrentText(result['total_unit'])
self.exp_solvent.setText(result['solvent_name'])
for reagent in result['reagents']:
name = reagent['name'] name = reagent['name']
if reagent.get('dilution_factor', 1.0) != 1.0: if reagent.get('dilution_factor', 1.0) != 1.0:
name += f" (разб. ×{reagent['dilution_factor']:.2f})" name += f"(разб. ×{reagent['dilution_factor']:g})"
percentage = f"{reagent['percentage']:g}"
self.factors_table.setItem(row, 0, QTableWidgetItem(name)) dilution = f"{reagent.get('dilution_factor', 1.0):g}"
self.factors_table.setItem(row, 1, QTableWidgetItem(f"{reagent['percentage']:.2f}")) center = str(reagent.get('calculated_amount'))
self.factors_table.setItem(row, 2, QTableWidgetItem(f"{reagent.get('dilution_factor', 1.0):.3f}")) step = ""
step_type = "%"
# Нулевой уровень - исходное количество (неразбавленное) high = ""
center = reagent.get('undiluted_amount', reagent['calculated_amount']) low = ""
self.factors_table.setItem(row, 3, QTableWidgetItem(self._format_number(center))) unit = reagent["unit"]
self._add_factor_row(name, percentage, dilution, center, step, step_type, high, low, unit)
# Шаг - 10% от нулевого уровня # QMessageBox.information(self, "Успех",
step = center * 0.1 # f"Импортировано {len(result['reagents'])} факторов из калькулятора сред")
self.factors_table.setItem(row, 4, QTableWidgetItem(self._format_number(step)))
# Тип шага - абсолютный
step_combo = self.factors_table.cellWidget(row, 5)
if step_combo:
step_combo.setCurrentText("абс")
# Верхний и нижний уровни
self.factors_table.setItem(row, 6, QTableWidgetItem(self._format_number(center + step)))
self.factors_table.setItem(row, 7, QTableWidgetItem(self._format_number(center - step)))
self.factors_table.setItem(row, 8, QTableWidgetItem(result['total_unit']))
QMessageBox.information(self, "Успех",
f"Импортировано {len(result['reagents'])} факторов из калькулятора сред")
def _get_factors_from_table(self) -> List[Dict]: def _get_factors_from_table(self) -> List[Dict]:
"""Собирает данные факторов из таблицы""" """Собирает данные факторов из таблицы"""
@@ -455,7 +463,7 @@ class MainWindow(QMainWindow):
step_item = self.factors_table.item(row, 4) step_item = self.factors_table.item(row, 4)
high_item = self.factors_table.item(row, 6) high_item = self.factors_table.item(row, 6)
low_item = self.factors_table.item(row, 7) low_item = self.factors_table.item(row, 7)
unit_item = self.factors_table.item(row, 8) unit_item = self.factors_table.cellWidget(row, 8)
step_combo = self.factors_table.cellWidget(row, 5) step_combo = self.factors_table.cellWidget(row, 5)
if not all([name_item, center_item, high_item, low_item]): if not all([name_item, center_item, high_item, low_item]):
@@ -473,9 +481,9 @@ class MainWindow(QMainWindow):
'center': float(center_text), 'center': float(center_text),
'high': float(high_text), 'high': float(high_text),
'low': float(low_text), 'low': float(low_text),
'unit': unit_item.text() if unit_item else "", 'unit': unit_item.currentText() if unit_item else "г",
'step': float(step_item.text()) if step_item and step_item.text() else 0, 'step': float(step_item.text()) if step_item and step_item.text() else 0,
'step_type': step_combo.currentText() if step_combo else "абс" 'step_type': step_combo.currentText() if step_combo else "ед."
} }
factors.append(factor) factors.append(factor)
except (ValueError, AttributeError) as e: except (ValueError, AttributeError) as e:
@@ -483,6 +491,32 @@ class MainWindow(QMainWindow):
continue continue
return factors return factors
def _on_factor_changed(self, row, column):
"""При изменении ячейки пересчитываем уровни"""
if getattr(self, '_loading_data', False) or column not in [3, 4, 5, 8]:
return
try:
# Получаем данные
percentage = float(self.factors_table.item(row, 1).text())
dilution = float(self.factors_table.item(row, 2).text())
center = float(self.factors_table.item(row, 3).text())
step = float(self.factors_table.item(row, 4).text())
step_type = self.factors_table.cellWidget(row, 5).currentText()
unit = self.factors_table.cellWidget(row, 8).currentText()
name = self.factors_table.item(row, 0).text()
# Рассчитываем
factor = FactorData(percentage=percentage, dilution_factor=dilution, name=name, center=center, low=0, high=0, step=step, step_type=step_type, unit=unit)
result = calculate_factor_levels(factor, self.total_volume_spin.value(), self.volume_unit_combo.currentText())
if result:
self.factors_table.blockSignals(True)
self.factors_table.item(row, 6).setText(self._format_number(result.high))
self.factors_table.item(row, 7).setText(self._format_number(result.low))
self.factors_table.blockSignals(False)
except (ValueError, AttributeError):
pass
# ========== ВКЛАДКА 3: МАТРИЦА ПЛАНИРОВАНИЯ ========== # ========== ВКЛАДКА 3: МАТРИЦА ПЛАНИРОВАНИЯ ==========
@@ -517,9 +551,11 @@ class MainWindow(QMainWindow):
def _generate_design(self): def _generate_design(self):
"""Генерирует план эксперимента""" """Генерирует план эксперимента"""
factors = self._get_factors_from_table() all_factors = self._get_factors_from_table()
factors = get_active_factors(all_factors)
i_factors = get_inactive_factors(all_factors)
if len(factors) == 0: if not factors:
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!") QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
return return
@@ -529,59 +565,15 @@ class MainWindow(QMainWindow):
center_points=self.center_points_spin.value(), center_points=self.center_points_spin.value(),
randomize=self.randomize_check.isChecked() randomize=self.randomize_check.isChecked()
) )
self.generated_design = design self.generated_design = design
self._fill_design_matrix(design, factors, i_factors)
n_exp = len(design)
n_factors = len(factors)
self.design_matrix.setRowCount(n_exp)
self.design_matrix.setColumnCount(n_factors + 2)
headers = [""] + [f['name'] for f in factors] + ["Тип"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, exp in enumerate(design):
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
for f_idx in range(n_factors):
key = f"Фактор_{f_idx + 1}"
if key not in exp:
continue
value = exp[key]['natural']
unit = factors[f_idx]['unit']
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
if exp.get('is_center', False):
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, f_idx + 1, item)
if exp.get('is_center', False):
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
type_item.setBackground(QColor(255, 255, 200))
else:
type_item = QTableWidgetItem("Факторная")
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
self.design_matrix.resizeColumnsToContents()
n_factorial = 2 ** n_factors
n_center = self.center_points_spin.value()
self.design_info.setText(
f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}"
)
self.export_csv_btn.setEnabled(True) self.export_csv_btn.setEnabled(True)
self._setup_results_table(n_exp) QMessageBox.information(self, "Успех", f"Сгенерирован план для {len(factors)} факторов ({len(design)} опытов)")
QMessageBox.information(self, "Успех",
f"Сгенерирован план для {n_factors} факторов ({n_exp} опытов)")
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Ошибка генерации плана: {str(e)}") QMessageBox.critical(self, "Ошибка", f"Ошибка генерации плана: {str(e)}")
def _setup_results_table(self, n_experiments: int): def _setup_results_table(self, n_experiments: int):
"""Настраивает таблицу результатов для ввода данных""" """Настраивает таблицу результатов для ввода данных"""
# Получаем отклики (для простоты используем один отклик) # Получаем отклики (для простоты используем один отклик)
@@ -590,7 +582,7 @@ class MainWindow(QMainWindow):
self.results_table.setHorizontalHeaderLabels(["№ опыта", "Результат"]) self.results_table.setHorizontalHeaderLabels(["№ опыта", "Результат"])
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)))
def _export_design_csv(self): def _export_design_csv(self):
"""Экспортирует матрицу планирования в CSV""" """Экспортирует матрицу планирования в CSV"""
@@ -759,7 +751,7 @@ class MainWindow(QMainWindow):
try: try:
project = ProjectData.load_from_file(filename) project = ProjectData.load_from_file(filename)
self._apply_project_data(project) self._apply_project_data(project)
QMessageBox.information(self, "Успех", f"Проект загружен из {filename}") # QMessageBox.information(self, "Успех", f"Проект загружен из {filename}")
except FileNotFoundError: except FileNotFoundError:
QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}") QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}")
except Exception as e: except Exception as e:
@@ -797,7 +789,7 @@ class MainWindow(QMainWindow):
low_item = self.factors_table.item(row, 7) low_item = self.factors_table.item(row, 7)
high_item = self.factors_table.item(row, 6) high_item = self.factors_table.item(row, 6)
step_item = self.factors_table.item(row, 4) step_item = self.factors_table.item(row, 4)
unit_item = self.factors_table.item(row, 8) unit_item = self.factors_table.cellWidget(row, 8)
step_combo = self.factors_table.cellWidget(row, 5) step_combo = self.factors_table.cellWidget(row, 5)
percent_item = self.factors_table.item(row, 1) percent_item = self.factors_table.item(row, 1)
dilution_item = self.factors_table.item(row, 2) dilution_item = self.factors_table.item(row, 2)
@@ -810,8 +802,8 @@ class MainWindow(QMainWindow):
low=float(low_item.text()) if low_item and low_item.text() else 0, low=float(low_item.text()) if low_item and low_item.text() else 0,
high=float(high_item.text()) if high_item and high_item.text() else 0, high=float(high_item.text()) if high_item and high_item.text() else 0,
step=float(step_item.text()) if step_item and step_item.text() else 0, step=float(step_item.text()) if step_item and step_item.text() else 0,
step_type=step_combo.currentText() if step_combo else "абс", step_type=step_combo.currentText() if step_combo else "ед.",
unit=unit_item.text() if unit_item else "", unit=unit_item.currentText() if unit_item else "",
percentage=float(percent_item.text()) if percent_item and percent_item.text() else None, percentage=float(percent_item.text()) if percent_item and percent_item.text() else None,
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None
) )
@@ -854,123 +846,86 @@ class MainWindow(QMainWindow):
def _apply_project_data(self, project): def _apply_project_data(self, project):
"""Применяет загруженные данные к GUI""" """Применяет загруженные данные к GUI"""
# Применяем данные калькулятора сред self._loading_data = True # <- ОТКЛЮЧАЕМ ОБРАБОТЧИК
self.total_volume_spin.setValue(project.medium_total_volume) try:
index = self.volume_unit_combo.findText(project.medium_volume_unit) # Применяем данные калькулятора сред
if index >= 0: self.total_volume_spin.setValue(project.medium_total_volume)
self.volume_unit_combo.setCurrentIndex(index)
self.solvent_input.setText(project.medium_solvent) index = self.volume_unit_combo.findText(project.medium_volume_unit)
if index >= 0:
self.volume_unit_combo.setCurrentIndex(index)
# Очищаем и заполняем таблицу реагентов self.solvent_input.setText(project.medium_solvent)
self.reagents_table.setRowCount(0)
for reagent in project.medium_reagents:
row = self.reagents_table.rowCount()
self.reagents_table.insertRow(row)
self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage)))
unit_combo = QComboBox() # Очищаем и заполняем таблицу реагентов
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"]) self.reagents_table.setRowCount(0)
unit_combo.setCurrentText(reagent.unit) for reagent in project.medium_reagents:
self.reagents_table.setCellWidget(row, 2, unit_combo) row = self.reagents_table.rowCount()
self.reagents_table.insertRow(row)
self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage)))
self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor))) unit_combo = QComboBox()
self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor))) unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
self.reagents_table.setItem(row, 5, QTableWidgetItem("")) unit_combo.setCurrentText(reagent.unit)
self.reagents_table.setCellWidget(row, 2, unit_combo)
# Применяем данные факторов эксперимента self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor)))
self.factors_table.setRowCount(0) self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor)))
for factor in project.experiment_factors: self.reagents_table.setItem(row, 5, QTableWidgetItem(""))
row = self.factors_table.rowCount()
self.factors_table.insertRow(row)
self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name))
if factor.percentage is not None:
self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage)))
else:
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
if factor.dilution_factor is not None:
self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor)))
else:
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center))) # Применяем данные факторов эксперимента
self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step))) self.exp_total_volume.setValue(project.experiment_total_volume)
self.exp_volume_unit.setCurrentText(project.experiment_volume_unit)
self.exp_solvent.setText(project.experiment_solvent)
self.factors_table.setRowCount(0)
for factor in project.experiment_factors:
if factor.percentage is not None:
percentage = str(factor.percentage)
if factor.dilution_factor is not None:
dilution = str(factor.dilution_factor)
self._add_factor_row(factor.name, percentage, dilution, str(factor.center),
str(factor.step), factor.step_type, str(factor.high),
str(factor.low), factor.unit)
# Применяем настройки эксперимента
self.center_points_spin.setValue(project.experiment_center_points)
self.randomize_check.setChecked(project.experiment_randomize)
step_combo = QComboBox() # Если есть результаты, загружаем их
step_combo.addItems(["абс", "%"]) if project.experiment_results and project.experiment_results.design:
step_combo.setCurrentText(factor.step_type) self.generated_design = project.experiment_results.design
self.factors_table.setCellWidget(row, 5, step_combo) # Обновляем отображение матрицы
self._refresh_design_matrix()
self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high))) # Загружаем результаты в таблицу
self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low))) if project.experiment_results.results:
self.factors_table.setItem(row, 8, QTableWidgetItem(factor.unit)) for i, row_results in enumerate(project.experiment_results.results):
if i < self.results_table.rowCount() and row_results:
self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0])))
# Применяем настройки эксперимента # Переключаемся на вкладку с экспериментом
self.center_points_spin.setValue(project.experiment_center_points) if self.tab_widget:
self.randomize_check.setChecked(project.experiment_randomize) self.tab_widget.setCurrentIndex(0)
finally:
# Если есть результаты, загружаем их self._loading_data = False # <- ВКЛЮЧАЕМ ОБРАТНО
if project.experiment_results and project.experiment_results.design:
self.generated_design = project.experiment_results.design
# Обновляем отображение матрицы
self._refresh_design_matrix()
# Загружаем результаты в таблицу
if project.experiment_results.results:
for i, row_results in enumerate(project.experiment_results.results):
if i < self.results_table.rowCount() and row_results:
self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0])))
# Переключаемся на вкладку с факторами
if self.tab_widget:
self.tab_widget.setCurrentIndex(1)
def _refresh_design_matrix(self): def _refresh_design_matrix(self):
"""Обновляет отображение матрицы планирования""" """Обновляет отображение матрицы планирования"""
if not self.generated_design: if not self.generated_design:
return return
factors = self._get_factors_from_table() all_factors = self._get_factors_from_table()
factors = get_active_factors(all_factors)
i_factors = get_inactive_factors(all_factors)
self._fill_design_matrix(self.generated_design, factors, i_factors)
n_exp = len(self.generated_design) n_exp = len(self.generated_design)
n_factors = len(factors) n_factors = len(factors)
self.design_matrix.setRowCount(n_exp)
self.design_matrix.setColumnCount(n_factors + 2)
headers = [""] + [f['name'] for f in factors] + ["Тип"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, exp in enumerate(self.generated_design):
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
for f_idx in range(n_factors):
key = f"Фактор_{f_idx + 1}"
if key not in exp:
continue
value = exp[key]['natural']
unit = factors[f_idx]['unit']
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
if exp.get('is_center', False):
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, f_idx + 1, item)
if exp.get('is_center', False):
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
type_item.setBackground(QColor(255, 255, 200))
else:
type_item = QTableWidgetItem("Факторная")
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
self.design_matrix.resizeColumnsToContents()
if hasattr(self, 'export_csv_btn'): if hasattr(self, 'export_csv_btn'):
self.export_csv_btn.setEnabled(True) self.export_csv_btn.setEnabled(True)
@@ -983,6 +938,74 @@ class MainWindow(QMainWindow):
# ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ========== # ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========
def _fill_design_matrix(self, design, factors, i_factors):
"""Заполняет матрицу планирования (общая логика)"""
n_exp = len(design)
n_factors = len(factors)
n_i_factors = len(i_factors)
solvent_name = self.exp_solvent.text()
total_volume = self.exp_total_volume.value()
solvent_unit = self.exp_volume_unit.currentText()
self.design_matrix.setRowCount(n_exp)
self.design_matrix.setColumnCount(n_factors + n_i_factors + 3)
headers = [f['name'] for f in factors] + [f['name'] for f in i_factors] + [solvent_name] + ["Тип"] + ["Отклик"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, exp in enumerate(design):
remaining = convert_units(total_volume, solvent_unit)
# Активные факторы
for f_idx in range(n_factors):
value = exp[f"Фактор_{f_idx+1}"]['natural']
unit = factors[f_idx]['unit']
remaining -= convert_units(value, unit)
item = self._create_item(value, unit, exp.get('is_center', False))
if value == factors[f_idx]['high']:
item.setBackground(QColor(200, 250, 200))
elif value ==factors[f_idx]['low']:
item.setBackground(QColor(250, 200, 200))
else:
item.setBackground(QColor(230, 230, 230))
self.design_matrix.setItem(exp_idx, f_idx, item)
# Неактивные факторы
for f_idx in range(n_i_factors):
value = i_factors[f_idx]['center']
unit = i_factors[f_idx]['unit']
remaining -= convert_units(value, unit)
item = self._create_item(value, unit, True)
item.setBackground(QColor(230, 230, 230))
self.design_matrix.setItem(exp_idx, n_factors + f_idx, item)
# Растворитель
remaining = convert_units(remaining, 'мкл', solvent_unit)
item = self._create_item(remaining, solvent_unit, False)
item.setBackground(QColor(200, 200, 255))
self.design_matrix.setItem(exp_idx, n_factors + n_i_factors, item)
# Тип опыта
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}" if exp.get('is_center') else "Факторная")
if exp.get('is_center'):
type_item.setBackground(QColor(200, 200, 200))
self.design_matrix.setItem(exp_idx, n_factors + n_i_factors + 1, type_item)
# Отклик
self.design_matrix.setItem(exp_idx, n_factors + n_i_factors + 2, QTableWidgetItem(""))
self.design_matrix.resizeColumnsToContents()
def _create_item(self, value, unit, is_center):
"""Создаёт QTableWidgetItem с форматированием"""
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
if is_center:
item.setBackground(QColor(255, 255, 200))
return item
def _format_number(self, value: float) -> str: def _format_number(self, value: float) -> str:
"""Форматирует число для отображения""" """Форматирует число для отображения"""
if value == int(value): if value == int(value):
+23 -1
View File
@@ -1,14 +1,36 @@
# main.py
""" """
Биохимический помощник - точка входа в приложение Биохимический помощник - точка входа в приложение
TODO:
- Добавить информацию о количестве раствора в DOE ОК
- Не считать фактор, если его шаг 0 ОК
- Добавить столбец в матрицу планирования с информацией о количестве добавленного растворителя, учитывая все реагенты
- Начать делать анализ
""" """
import sys import sys
import os import os
from PyQt5.QtWidgets import QApplication
from gui import MainWindow
from theme import Fonts, setup_emoji_support
# Добавляем текущую директорию в путь # Добавляем текущую директорию в путь
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
from gui import main
def main():
app = QApplication(sys.argv)
# Настраиваем поддержку эмодзи
setup_emoji_support(app)
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__": if __name__ == "__main__":
main() main()
-904
View File
@@ -1,904 +0,0 @@
#!/bin/bash
# Скрипт для добавления функционала сохранения/загрузки данных в JSON
# Запускать из корневой директории проекта
set -e
echo "📁 Добавление функционала сохранения/загрузки данных в JSON..."
echo "================================================================"
# Создаём директорию для моделей, если её нет
mkdir -p calculations/models
# 1. СОЗДАЁМ МОДЕЛЬ ДАННЫХ ДЛЯ СОХРАНЕНИЯ
cat > calculations/models/project_data.py << 'EOF'
"""
Модель данных проекта для сохранения/загрузки в JSON
"""
from typing import List, Dict, Optional
from dataclasses import dataclass, asdict
from datetime import datetime
import json
@dataclass
class ReagentData:
"""Данные реагента"""
name: str
percentage: float
unit: str
conversion_factor: float
dilution_factor: float
def to_dict(self) -> Dict:
return asdict(self)
@classmethod
def from_dict(cls, data: Dict) -> 'ReagentData':
return cls(
name=data['name'],
percentage=data['percentage'],
unit=data['unit'],
conversion_factor=data.get('conversion_factor', 1.0),
dilution_factor=data.get('dilution_factor', 1.0)
)
@dataclass
class FactorData:
"""Данные фактора эксперимента"""
name: str
center: float
low: float
high: float
step: float
step_type: str
unit: str
percentage: Optional[float] = None
dilution_factor: Optional[float] = None
def to_dict(self) -> Dict:
d = asdict(self)
# Удаляем None значения
return {k: v for k, v in d.items() if v is not None}
@classmethod
def from_dict(cls, data: Dict) -> 'FactorData':
return cls(
name=data['name'],
center=data['center'],
low=data['low'],
high=data['high'],
step=data.get('step', 0),
step_type=data.get('step_type', 'абс'),
unit=data.get('unit', ''),
percentage=data.get('percentage'),
dilution_factor=data.get('dilution_factor')
)
@dataclass
class ResponseData:
"""Данные отклика эксперимента"""
name: str
unit: str
def to_dict(self) -> Dict:
return asdict(self)
@classmethod
def from_dict(cls, data: Dict) -> 'ResponseData':
return cls(
name=data['name'],
unit=data.get('unit', '')
)
@dataclass
class ExperimentResultsData:
"""Результаты эксперимента"""
design: List[Dict] # План эксперимента
results: List[List[float]] # Результаты измерений
responses: List[ResponseData] # Отклики
def to_dict(self) -> Dict:
return {
'design': self.design,
'results': self.results,
'responses': [r.to_dict() for r in self.responses]
}
@classmethod
def from_dict(cls, data: Dict) -> 'ExperimentResultsData':
return cls(
design=data['design'],
results=data['results'],
responses=[ResponseData.from_dict(r) for r in data['responses']]
)
@dataclass
class ProjectData:
"""Полные данные проекта"""
# Информация о проекте
project_name: str
created_at: str
modified_at: str
version: str = "1.0"
# Данные калькулятора сред
medium_total_volume: float = 1000.0
medium_volume_unit: str = "мл"
medium_solvent: str = "Вода"
medium_reagents: List[ReagentData] = None
# Данные эксперимента
experiment_factors: List[FactorData] = None
experiment_responses: List[ResponseData] = None
experiment_center_points: int = 3
experiment_randomize: bool = True
experiment_results: Optional[ExperimentResultsData] = None
def __post_init__(self):
if self.medium_reagents is None:
self.medium_reagents = []
if self.experiment_factors is None:
self.experiment_factors = []
if self.experiment_responses is None:
self.experiment_responses = []
def to_dict(self) -> Dict:
"""Конвертирует в словарь для JSON"""
return {
'project_info': {
'name': self.project_name,
'created_at': self.created_at,
'modified_at': self.modified_at,
'version': self.version
},
'medium_calculator': {
'total_volume': self.medium_total_volume,
'volume_unit': self.medium_volume_unit,
'solvent': self.medium_solvent,
'reagents': [r.to_dict() for r in self.medium_reagents]
},
'experiment': {
'factors': [f.to_dict() for f in self.experiment_factors],
'responses': [r.to_dict() for r in self.experiment_responses],
'center_points': self.experiment_center_points,
'randomize': self.experiment_randomize,
'results': self.experiment_results.to_dict() if self.experiment_results else None
}
}
@classmethod
def from_dict(cls, data: Dict) -> 'ProjectData':
"""Создаёт объект из словаря"""
project_info = data.get('project_info', {})
medium = data.get('medium_calculator', {})
experiment = data.get('experiment', {})
# Создаём объект
obj = cls(
project_name=project_info.get('name', 'Новый проект'),
created_at=project_info.get('created_at', datetime.now().isoformat()),
modified_at=project_info.get('modified_at', datetime.now().isoformat()),
version=project_info.get('version', '1.0'),
medium_total_volume=medium.get('total_volume', 1000.0),
medium_volume_unit=medium.get('volume_unit', 'мл'),
medium_solvent=medium.get('solvent', 'Вода'),
medium_reagents=[ReagentData.from_dict(r) for r in medium.get('reagents', [])],
experiment_factors=[FactorData.from_dict(f) for f in experiment.get('factors', [])],
experiment_responses=[ResponseData.from_dict(r) for r in experiment.get('responses', [])],
experiment_center_points=experiment.get('center_points', 3),
experiment_randomize=experiment.get('randomize', True),
experiment_results=ExperimentResultsData.from_dict(experiment['results'])
if experiment.get('results') else None
)
return obj
def save_to_file(self, filename: str):
"""Сохраняет проект в JSON файл"""
self.modified_at = datetime.now().isoformat()
with open(filename, 'w', encoding='utf-8') as f:
json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
@classmethod
def load_from_file(cls, filename: str) -> 'ProjectData':
"""Загружает проект из JSON файла"""
with open(filename, 'r', encoding='utf-8') as f:
data = json.load(f)
return cls.from_dict(data)
def create_new_project(name: str = "Новый проект") -> ProjectData:
"""Создаёт новый проект с текущей датой"""
now = datetime.now().isoformat()
return ProjectData(
project_name=name,
created_at=now,
modified_at=now
)
EOF
echo "✅ Создан calculations/models/project_data.py"
# 2. ДОБАВЛЯЕМ ФУНКЦИИ В GUI ДЛЯ РАБОТЫ С JSON
# Создаём патч для gui.py
cat > gui_patch.py << 'EOF'
# ДОБАВИТЬ В gui.py ПОСЛЕ ИМПОРТОВ:
from calculations.models.project_data import (
ProjectData, ReagentData, FactorData,
ResponseData, ExperimentResultsData, create_new_project
)
# ДОБАВИТЬ В КЛАСС MainWindow НОВЫЕ МЕТОДЫ:
def save_project_to_json(self):
"""Сохраняет весь проект в JSON файл"""
filename, _ = QFileDialog.getSaveFileName(
self, "Сохранить проект", "", "JSON Files (*.json);;All Files (*)"
)
if not filename:
return
if not filename.endswith('.json'):
filename += '.json'
try:
# Собираем данные из GUI
project = self._collect_current_data()
project.save_to_file(filename)
QMessageBox.information(self, "Успех", f"Проект сохранён в {filename}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить проект: {str(e)}")
def load_project_from_json(self):
"""Загружает проект из JSON файла"""
filename, _ = QFileDialog.getOpenFileName(
self, "Загрузить проект", "", "JSON Files (*.json);;All Files (*)"
)
if not filename:
return
try:
project = ProjectData.load_from_file(filename)
self._apply_project_data(project)
QMessageBox.information(self, "Успех", f"Проект загружен из {filename}")
except FileNotFoundError:
QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить проект: {str(e)}")
def _collect_current_data(self) -> ProjectData:
"""Собирает текущие данные из всех виджетов"""
# Данные калькулятора сред
reagents = []
for row in range(self.reagents_table.rowCount()):
name_item = self.reagents_table.item(row, 0)
percent_item = self.reagents_table.item(row, 1)
unit_widget = self.reagents_table.cellWidget(row, 2)
coeff_item = self.reagents_table.item(row, 3)
dilution_item = self.reagents_table.item(row, 4)
if name_item and percent_item:
try:
reagent = ReagentData(
name=name_item.text(),
percentage=float(percent_item.text()) if percent_item.text() else 0,
unit=unit_widget.currentText() if unit_widget else "мг",
conversion_factor=float(coeff_item.text()) if coeff_item and coeff_item.text() else 1.0,
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0
)
reagents.append(reagent)
except ValueError:
pass
# Данные факторов эксперимента
factors = []
for row in range(self.factors_table.rowCount()):
name_item = self.factors_table.item(row, 0)
center_item = self.factors_table.item(row, 3)
low_item = self.factors_table.item(row, 7)
high_item = self.factors_table.item(row, 6)
step_item = self.factors_table.item(row, 4)
unit_item = self.factors_table.item(row, 8)
step_combo = self.factors_table.cellWidget(row, 5)
percent_item = self.factors_table.item(row, 1)
dilution_item = self.factors_table.item(row, 2)
if name_item and center_item:
try:
factor = FactorData(
name=name_item.text(),
center=float(center_item.text()) if center_item.text() else 0,
low=float(low_item.text()) if low_item and low_item.text() else 0,
high=float(high_item.text()) if high_item and high_item.text() else 0,
step=float(step_item.text()) if step_item and step_item.text() else 0,
step_type=step_combo.currentText() if step_combo else "абс",
unit=unit_item.text() if unit_item else "",
percentage=float(percent_item.text()) if percent_item and percent_item.text() else None,
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None
)
factors.append(factor)
except ValueError:
pass
# Данные результатов (если есть)
results_data = None
if hasattr(self, 'generated_design') and self.generated_design:
results = []
for i in range(self.results_table.rowCount()):
item = self.results_table.item(i, 1)
if item and item.text():
try:
results.append([float(item.text())])
except ValueError:
pass
if results:
responses = [ResponseData(name="Отклик", unit="")]
results_data = ExperimentResultsData(
design=self.generated_design,
results=results,
responses=responses
)
# Создаём проект
project = create_new_project("Мой проект")
project.medium_total_volume = self.total_volume_spin.value()
project.medium_volume_unit = self.volume_unit_combo.currentText()
project.medium_solvent = self.solvent_input.text()
project.medium_reagents = reagents
project.experiment_factors = factors
project.experiment_center_points = self.center_points_spin.value()
project.experiment_randomize = self.randomize_check.isChecked()
project.experiment_results = results_data
return project
def _apply_project_data(self, project: ProjectData):
"""Применяет загруженные данные к GUI"""
# Применяем данные калькулятора сред
self.total_volume_spin.setValue(project.medium_total_volume)
index = self.volume_unit_combo.findText(project.medium_volume_unit)
if index >= 0:
self.volume_unit_combo.setCurrentIndex(index)
self.solvent_input.setText(project.medium_solvent)
# Очищаем и заполняем таблицу реагентов
self.reagents_table.setRowCount(0)
for reagent in project.medium_reagents:
row = self.reagents_table.rowCount()
self.reagents_table.insertRow(row)
self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage)))
unit_combo = QComboBox()
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
unit_combo.setCurrentText(reagent.unit)
self.reagents_table.setCellWidget(row, 2, unit_combo)
self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor)))
self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor)))
self.reagents_table.setItem(row, 5, QTableWidgetItem(""))
# Применяем данные факторов эксперимента
self.factors_table.setRowCount(0)
for factor in project.experiment_factors:
row = self.factors_table.rowCount()
self.factors_table.insertRow(row)
self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name))
if factor.percentage is not None:
self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage)))
else:
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
if factor.dilution_factor is not None:
self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor)))
else:
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center)))
self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step)))
step_combo = QComboBox()
step_combo.addItems(["абс", "%"])
step_combo.setCurrentText(factor.step_type)
self.factors_table.setCellWidget(row, 5, step_combo)
self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high)))
self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low)))
self.factors_table.setItem(row, 8, QTableWidgetItem(factor.unit))
# Применяем настройки эксперимента
self.center_points_spin.setValue(project.experiment_center_points)
self.randomize_check.setChecked(project.experiment_randomize)
# Если есть результаты, загружаем их
if project.experiment_results and project.experiment_results.design:
self.generated_design = project.experiment_results.design
self._display_design_matrix()
# Загружаем результаты в таблицу
if project.experiment_results.results:
for i, row_results in enumerate(project.experiment_results.results):
if i < self.results_table.rowCount() and row_results:
self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0])))
# Переключаемся на вкладку с факторами
self.tab_widget.setCurrentIndex(1)
def _display_design_matrix(self):
"""Отображает матрицу планирования (добавить в _generate_design или отдельно)"""
if not self.generated_design:
return
n_exp = len(self.generated_design)
n_factors = len(self._get_factors_from_table())
self.design_matrix.setRowCount(n_exp)
self.design_matrix.setColumnCount(n_factors + 2)
headers = ["№"] + [f['name'] for f in self._get_factors_from_table()] + ["Тип"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, exp in enumerate(self.generated_design):
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
for f_idx in range(n_factors):
key = f"Фактор_{f_idx + 1}"
if key not in exp:
continue
value = exp[key]['natural']
unit = self._get_factors_from_table()[f_idx]['unit']
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
if exp.get('is_center', False):
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, f_idx + 1, item)
if exp.get('is_center', False):
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
type_item.setBackground(QColor(255, 255, 200))
else:
type_item = QTableWidgetItem("Факторная")
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
self.design_matrix.resizeColumnsToContents()
self.export_csv_btn.setEnabled(True)
self._setup_results_table(n_exp)
# ДОБАВИТЬ В МЕТОД _setup_ui ПОСЛЕ СОЗДАНИЯ ВКЛАДОК:
# Добавляем кнопки сохранения/загрузки в тулбар
toolbar = self.addToolBar("Файл")
toolbar.setMovable(False)
save_action = toolbar.addAction("💾 Сохранить проект")
save_action.triggered.connect(self.save_project_to_json)
load_action = toolbar.addAction("📂 Загрузить проект")
load_action.triggered.connect(self.load_project_from_json)
toolbar.addSeparator()
# Сохраняем ссылку на tab_widget для переключения вкладок
self.tab_widget = tabs
EOF
echo "✅ Создан патч gui_patch.py"
# 3. ПРИМЕНЯЕМ ПАТЧ К GUI.PY
# Создаём резервную копию
cp gui.py gui.py.backup
echo "✅ Создана резервная копия gui.py.backup"
# Вставляем импорты после существующих импортов
sed -i '/from calculations.doe import/a\
from calculations.models.project_data import (\
ProjectData, ReagentData, FactorData, \
ResponseData, ExperimentResultsData, create_new_project\
)' gui.py
# Находим строку с self._setup_ui() и добавляем после неё toolbar
sed -i '/self._setup_ui()/a\ self._add_file_toolbar()' gui.py
# Добавляем новый метод _add_file_toolbar после _setup_ui
cat >> gui.py.new_method << 'EOF'
def _add_file_toolbar(self):
"""Добавляет панель инструментов с кнопками сохранения/загрузки"""
toolbar = self.addToolBar("Файл")
toolbar.setMovable(False)
save_action = toolbar.addAction("💾 Сохранить проект")
save_action.triggered.connect(self.save_project_to_json)
load_action = toolbar.addAction("📂 Загрузить проект")
load_action.triggered.connect(self.load_project_from_json)
toolbar.addSeparator()
EOF
# Вставляем новый метод после _setup_ui
sed -i '/def _setup_ui(self):/,/def _get_stylesheet(self):/ {
/def _get_stylesheet(self):/ {
r gui.py.new_method
}
}' gui.py
rm gui.py.new_method
# Добавляем методы сохранения/загрузки в конец файла перед if __name__
cat >> gui.py << 'EOF'
# ========== МЕТОДЫ РАБОТЫ С JSON ==========
def save_project_to_json(self):
"""Сохраняет весь проект в JSON файл"""
from PyQt5.QtWidgets import QFileDialog, QMessageBox
filename, _ = QFileDialog.getSaveFileName(
self, "Сохранить проект", "", "JSON Files (*.json);;All Files (*)"
)
if not filename:
return
if not filename.endswith('.json'):
filename += '.json'
try:
project = self._collect_current_data()
project.save_to_file(filename)
QMessageBox.information(self, "Успех", f"Проект сохранён в {filename}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить проект: {str(e)}")
def load_project_from_json(self):
"""Загружает проект из JSON файла"""
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QComboBox, QTableWidgetItem
filename, _ = QFileDialog.getOpenFileName(
self, "Загрузить проект", "", "JSON Files (*.json);;All Files (*)"
)
if not filename:
return
try:
project = ProjectData.load_from_file(filename)
self._apply_project_data(project)
QMessageBox.information(self, "Успех", f"Проект загружен из {filename}")
except FileNotFoundError:
QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить проект: {str(e)}")
def _collect_current_data(self) -> ProjectData:
"""Собирает текущие данные из всех виджетов"""
from PyQt5.QtWidgets import QComboBox
# Данные калькулятора сред
reagents = []
for row in range(self.reagents_table.rowCount()):
name_item = self.reagents_table.item(row, 0)
percent_item = self.reagents_table.item(row, 1)
unit_widget = self.reagents_table.cellWidget(row, 2)
coeff_item = self.reagents_table.item(row, 3)
dilution_item = self.reagents_table.item(row, 4)
if name_item and percent_item and percent_item.text():
try:
reagent = ReagentData(
name=name_item.text(),
percentage=float(percent_item.text()),
unit=unit_widget.currentText() if unit_widget else "мг",
conversion_factor=float(coeff_item.text()) if coeff_item and coeff_item.text() else 1.0,
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0
)
reagents.append(reagent)
except ValueError:
pass
# Данные факторов эксперимента
factors = []
for row in range(self.factors_table.rowCount()):
name_item = self.factors_table.item(row, 0)
center_item = self.factors_table.item(row, 3)
low_item = self.factors_table.item(row, 7)
high_item = self.factors_table.item(row, 6)
step_item = self.factors_table.item(row, 4)
unit_item = self.factors_table.item(row, 8)
step_combo = self.factors_table.cellWidget(row, 5)
percent_item = self.factors_table.item(row, 1)
dilution_item = self.factors_table.item(row, 2)
if name_item and center_item and center_item.text():
try:
factor = FactorData(
name=name_item.text(),
center=float(center_item.text()),
low=float(low_item.text()) if low_item and low_item.text() else 0,
high=float(high_item.text()) if high_item and high_item.text() else 0,
step=float(step_item.text()) if step_item and step_item.text() else 0,
step_type=step_combo.currentText() if step_combo else "абс",
unit=unit_item.text() if unit_item else "",
percentage=float(percent_item.text()) if percent_item and percent_item.text() else None,
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None
)
factors.append(factor)
except ValueError:
pass
# Данные результатов (если есть)
results_data = None
if hasattr(self, 'generated_design') and self.generated_design:
results = []
for i in range(self.results_table.rowCount()):
item = self.results_table.item(i, 1)
if item and item.text():
try:
results.append([float(item.text())])
except ValueError:
pass
if results:
responses = [ResponseData(name="Отклик", unit="")]
results_data = ExperimentResultsData(
design=self.generated_design,
results=results,
responses=responses
)
# Создаём проект
project = create_new_project("Мой проект")
project.medium_total_volume = self.total_volume_spin.value()
project.medium_volume_unit = self.volume_unit_combo.currentText()
project.medium_solvent = self.solvent_input.text()
project.medium_reagents = reagents
project.experiment_factors = factors
project.experiment_center_points = self.center_points_spin.value()
project.experiment_randomize = self.randomize_check.isChecked()
project.experiment_results = results_data
return project
def _apply_project_data(self, project: ProjectData):
"""Применяет загруженные данные к GUI"""
from PyQt5.QtWidgets import QComboBox, QTableWidgetItem
from PyQt5.QtCore import Qt
# Применяем данные калькулятора сред
self.total_volume_spin.setValue(project.medium_total_volume)
index = self.volume_unit_combo.findText(project.medium_volume_unit)
if index >= 0:
self.volume_unit_combo.setCurrentIndex(index)
self.solvent_input.setText(project.medium_solvent)
# Очищаем и заполняем таблицу реагентов
self.reagents_table.setRowCount(0)
for reagent in project.medium_reagents:
row = self.reagents_table.rowCount()
self.reagents_table.insertRow(row)
self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage)))
unit_combo = QComboBox()
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
unit_combo.setCurrentText(reagent.unit)
self.reagents_table.setCellWidget(row, 2, unit_combo)
self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor)))
self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor)))
self.reagents_table.setItem(row, 5, QTableWidgetItem(""))
# Применяем данные факторов эксперимента
self.factors_table.setRowCount(0)
for factor in project.experiment_factors:
row = self.factors_table.rowCount()
self.factors_table.insertRow(row)
self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name))
if factor.percentage is not None:
self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage)))
else:
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
if factor.dilution_factor is not None:
self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor)))
else:
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center)))
self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step)))
step_combo = QComboBox()
step_combo.addItems(["абс", "%"])
step_combo.setCurrentText(factor.step_type)
self.factors_table.setCellWidget(row, 5, step_combo)
self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high)))
self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low)))
self.factors_table.setItem(row, 8, QTableWidgetItem(factor.unit))
# Применяем настройки эксперимента
self.center_points_spin.setValue(project.experiment_center_points)
self.randomize_check.setChecked(project.experiment_randomize)
# Если есть результаты, загружаем их
if project.experiment_results and project.experiment_results.design:
self.generated_design = project.experiment_results.design
# Обновляем отображение матрицы
self._refresh_design_matrix()
# Загружаем результаты в таблицу
if project.experiment_results.results:
for i, row_results in enumerate(project.experiment_results.results):
if i < self.results_table.rowCount() and row_results:
self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0])))
# Переключаемся на вкладку с факторами
if hasattr(self, 'tab_widget'):
self.tab_widget.setCurrentIndex(1)
def _refresh_design_matrix(self):
"""Обновляет отображение матрицы планирования"""
if not self.generated_design:
return
factors = self._get_factors_from_table()
n_exp = len(self.generated_design)
n_factors = len(factors)
self.design_matrix.setRowCount(n_exp)
self.design_matrix.setColumnCount(n_factors + 2)
headers = ["№"] + [f['name'] for f in factors] + ["Тип"]
self.design_matrix.setHorizontalHeaderLabels(headers)
for exp_idx, exp in enumerate(self.generated_design):
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
for f_idx in range(n_factors):
key = f"Фактор_{f_idx + 1}"
if key not in exp:
continue
value = exp[key]['natural']
unit = factors[f_idx]['unit']
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
if exp.get('is_center', False):
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, f_idx + 1, item)
if exp.get('is_center', False):
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
type_item.setBackground(QColor(255, 255, 200))
else:
type_item = QTableWidgetItem("Факторная")
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
self.design_matrix.resizeColumnsToContents()
if hasattr(self, 'export_csv_btn'):
self.export_csv_btn.setEnabled(True)
n_factorial = 2 ** n_factors
n_center = self.center_points_spin.value()
self.design_info.setText(
f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}"
)
self._setup_results_table(n_exp)
EOF
echo "✅ Добавлены методы работы с JSON в gui.py"
# 4. СОЗДАЁМ ПРИМЕР JSON ФАЙЛА
cat > example_project.json << 'EOF'
{
"project_info": {
"name": "Пример проекта",
"created_at": "2026-05-14T16:00:00",
"modified_at": "2026-05-14T16:00:00",
"version": "1.0"
},
"medium_calculator": {
"total_volume": 1000,
"volume_unit": "мл",
"solvent": "Вода",
"reagents": [
{
"name": "Глюкоза",
"percentage": 2.0,
"unit": "г",
"conversion_factor": 1.0,
"dilution_factor": 1.0
},
{
"name": "Пептон",
"percentage": 1.0,
"unit": "г",
"conversion_factor": 1.0,
"dilution_factor": 1.0
}
]
},
"experiment": {
"factors": [
{
"name": "Глюкоза",
"center": 20.0,
"low": 18.0,
"high": 22.0,
"step": 2.0,
"step_type": "абс",
"unit": "г",
"percentage": 2.0,
"dilution_factor": 1.0
},
{
"name": "Пептон",
"center": 10.0,
"low": 9.0,
"high": 11.0,
"step": 1.0,
"step_type": "абс",
"unit": "г",
"percentage": 1.0,
"dilution_factor": 1.0
}
],
"responses": [
{"name": "Отклик", "unit": ""}
],
"center_points": 3,
"randomize": true,
"results": null
}
}
EOF
echo "✅ Создан example_project.json"
# 5. СОЗДАЁМ requirements.txt ЕСЛИ ЕГО НЕТ
if [ ! -f requirements.txt ]; then
cat > requirements.txt << 'EOF'
PyQt5>=5.15.0
numpy>=1.21.0
EOF
echo "✅ Создан requirements.txt"
fi
echo ""
echo "================================================================"
echo "✅ ГОТОВО! Функционал сохранения/загрузки JSON добавлен!"
echo "================================================================"
echo ""
echo "Что было добавлено:"
echo " 1. Модель данных в calculations/models/project_data.py"
echo " 2. Кнопки 'Сохранить проект' и 'Загрузить проект' в toolbar"
echo " 3. Методы сохранения/загрузки в gui.py"
echo " 4. Пример JSON файла example_project.json"
echo ""
echo "Для запуска программы:"
echo " python main.py"
echo ""
echo "Для восстановления оригинального gui.py (если нужно):"
echo " cp gui.py.backup gui.py"
echo ""
echo "Чтобы применить изменения, перезапустите программу"
echo "================================================================"
+745
View File
@@ -0,0 +1,745 @@
# theme.py
"""
Настройки темы и цветовой схемы приложения
Централизованное управление стилями для единообразного внешнего вида
"""
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFontDatabase, QFont
# ========== ОСНОВНЫЕ ЦВЕТА ==========
class Colors:
"""Цветовая палитра приложения"""
# Основные цвета (Primary)
PRIMARY = "#3498db" # Синий - основной акцент
PRIMARY_DARK = "#777777" # Тёмно-синий (наведение)
PRIMARY_LIGHT = "#5dade2" # Светло-синий
PRIMARY_BG = "#ebf5fb" # Фоновый для primary элементов
# Успех/Позитив (Success)
SUCCESS = "#27ae60" # Зелёный
SUCCESS_DARK = "#219a52" # Тёмно-зелёный
SUCCESS_LIGHT = "#2ecc71" # Светло-зелёный
SUCCESS_BG = "#d5f5e3" # Фоновый для успеха
# Опасность/Ошибка (Danger)
DANGER = "#e74c3c" # Красный
DANGER_DARK = "#c0392b" # Тёмно-красный
DANGER_LIGHT = "#ec7063" # Светло-красный
DANGER_BG = "#fadbd8" # Фоновый для ошибок
# Предупреждение (Warning)
WARNING = "#f39c12" # Оранжевый
WARNING_DARK = "#e67e22" # Тёмно-оранжевый
WARNING_LIGHT = "#f5b041" # Светло-оранжевый
WARNING_BG = "#fef9e7" # Фоновый для предупреждений
# Информация (Info)
INFO = "#34495e" # Тёмно-синий/серый
INFO_LIGHT = "#5d6d7e" # Светлый вариант
# Нейтральные цвета (Neutral)
WHITE = "#ffffff"
BLACK = "#000000"
GRAY_100 = "#f8f9fa"
GRAY_200 = "#ecf0f1"
GRAY_300 = "#dee2e6"
GRAY_400 = "#ced4da"
GRAY_500 = "#adb5bd"
GRAY_600 = "#6c757d"
GRAY_700 = "#495057"
GRAY_800 = "#343a40"
GRAY_900 = "#212529"
# Цвета для таблиц
TABLE_ALTERNATE_ROW = "#f9f9f9"
TABLE_CENTER_POINT = "#ffffc8" # Жёлтый для центральных точек
TABLE_HIGHLIGHT = "#d4efdf" # Светло-зелёный для подсветки
# Цвета для текста
TEXT_PRIMARY = GRAY_900
TEXT_SECONDARY = GRAY_600
TEXT_MUTED = GRAY_500
TEXT_ON_PRIMARY = WHITE
TEXT_ON_DARK = WHITE
# Цвета для границ
BORDER_LIGHT = GRAY_300
BORDER_DEFAULT = GRAY_400
BORDER_DARK = GRAY_600
# Цвет для заголовка
TITLE_COLOR = "#2c3e50"
# Прозрачность
TRANSPARENT = "transparent"
OVERLAY = "rgba(0, 0, 0, 0.5)"
OVERLAY_LIGHT = "rgba(0, 0, 0, 0.1)"
# ========== НАСТРОЙКИ ШРИФТОВ ==========
class Fonts:
"""Настройки шрифтов"""
FAMILY_PRIMARY = "Segoe UI, Arial, sans-serif"
FAMILY_MONO = "Consolas, Monaco, monospace"
# Размеры
SIZE_TINY = 10
SIZE_SMALL = 11
SIZE_NORMAL = 12
SIZE_MEDIUM = 14
SIZE_LARGE = 16
SIZE_XLARGE = 18
SIZE_XXLARGE = 24
# Вес (жирность)
WEIGHT_NORMAL = 400
WEIGHT_MEDIUM = 500
WEIGHT_SEMIBOLD = 600
WEIGHT_BOLD = 700
@classmethod
def get_title_font(cls):
"""Возвращает шрифт для заголовков"""
font = QFont(cls.FAMILY_PRIMARY.split(',')[0])
font.setPointSize(cls.SIZE_XXLARGE)
font.setBold(True)
return font
@classmethod
def get_heading_font(cls, size=SIZE_LARGE):
"""Возвращает шрифт для подзаголовков"""
font = QFont(cls.FAMILY_PRIMARY.split(',')[0])
font.setPointSize(size)
font.setBold(True)
return font
@classmethod
def get_normal_font(cls):
"""Возвращает обычный шрифт"""
font = QFont(cls.FAMILY_PRIMARY.split(',')[0])
font.setPointSize(cls.SIZE_NORMAL)
return font
@classmethod
def get_emoji_font(cls):
"""Возвращает шрифт с поддержкой эмодзи"""
# Получаем список доступных шрифтов
available_fonts = QFontDatabase().families()
# Приоритетный список шрифтов с поддержкой эмодзи
emoji_fonts = [
"Segoe UI Emoji", # Windows 10/11
"Apple Color Emoji", # macOS
"Noto Color Emoji", # Linux
"EmojiOne Color", # Альтернативный
"Twemoji Mozilla", # Firefox
"Symbola", # Основные эмодзи
"Segoe UI Symbol", # Windows (старые версии)
]
# Ищем первый доступный шрифт
for font_name in emoji_fonts:
if font_name in available_fonts:
font = QFont(font_name)
font.setPointSize(cls.SIZE_NORMAL)
return font
# Если шрифты с эмодзи не найдены, возвращаем обычный шрифт
# и добавляем fallback для эмодзи через семейство шрифтов
fallback_font = cls.get_normal_font()
fallback_font.setFamily(f"{cls.FAMILY_PRIMARY}, Segoe UI Emoji, Apple Color Emoji")
return fallback_font
def setup_emoji_support(app: QApplication):
"""
Настраивает поддержку эмодзи во всём приложении
Параметры:
app: экземпляр QApplication
"""
# Устанавливаем шрифт по умолчанию с поддержкой эмодзи
font = Fonts.get_emoji_font()
app.setFont(font)
# Дополнительно настраиваем атрибуты шрифта для лучшего отображения
font.setStyleStrategy(QFont.PreferAntialias)
# Для Windows: включаем поддержку DirectWrite для лучшего отображения
try:
app.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
app.setAttribute(Qt.AA_EnableHighDpiScaling, True)
except:
pass # Не все версии PyQt5 поддерживают эти атрибуты
# ========== ОТСТУПЫ И РАЗМЕРЫ ==========
class Spacing:
"""Отступы и размеры элементов"""
XS = 4 # Очень маленький
SM = 8 # Маленький
MD = 12 # Средний
LG = 16 # Большой
XL = 24 # Очень большой
XXL = 32 # Максимальный
# Размеры элементов
BUTTON_HEIGHT = 32
INPUT_HEIGHT = 30
TABLE_ROW_HEIGHT = 25
# Скругления
BORDER_RADIUS_SM = 4
BORDER_RADIUS_MD = 6
BORDER_RADIUS_LG = 8
BORDER_RADIUS_XL = 12
# ========== СТИЛЬ ЗАГОЛОВКА ==========
class TitleStyles:
@staticmethod
def main_title():
return f"""
color: {Colors.TITLE_COLOR};
padding: {Spacing.XL}px;
font-size: {Fonts.SIZE_XLARGE}px;
font-weight: {Fonts.WEIGHT_BOLD};
"""
# ========== СТИЛИ КНОПОК ==========
class ButtonStyles:
"""Стили для разных типов кнопок"""
@staticmethod
def _base_style():
return f"""
border: none;
border-radius: {Spacing.BORDER_RADIUS_MD}px;
padding: {Spacing.SM}px {Spacing.LG}px;
font-weight: {Fonts.WEIGHT_SEMIBOLD};
font-size: {Fonts.SIZE_NORMAL}px;
"""
@staticmethod
def primary():
"""Основная кнопка (синяя)"""
return f"""
QPushButton {{
background-color: {Colors.PRIMARY};
color: {Colors.TEXT_ON_PRIMARY};
{ButtonStyles._base_style()}
}}
QPushButton:hover {{
background-color: {Colors.PRIMARY_LIGHT};
}}
QPushButton:pressed {{
background-color: {Colors.PRIMARY_DARK};
}}
QPushButton:disabled {{
background-color: {Colors.GRAY_400};
color: {Colors.GRAY_600};
}}
"""
@staticmethod
def success():
"""Кнопка успеха (зелёная)"""
return f"""
QPushButton {{
background-color: {Colors.SUCCESS};
color: {Colors.TEXT_ON_PRIMARY};
{ButtonStyles._base_style()}
}}
QPushButton:hover {{
background-color: {Colors.SUCCESS_DARK};
}}
QPushButton:pressed {{
background-color: {Colors.SUCCESS_LIGHT};
}}
QPushButton:disabled {{
background-color: {Colors.GRAY_400};
color: {Colors.GRAY_600};
}}
"""
@staticmethod
def danger():
"""Кнопка опасности/удаления (красная)"""
return f"""
QPushButton {{
background-color: {Colors.DANGER};
color: {Colors.TEXT_ON_PRIMARY};
{ButtonStyles._base_style()}
}}
QPushButton:hover {{
background-color: {Colors.DANGER_DARK};
}}
QPushButton:pressed {{
background-color: {Colors.DANGER_LIGHT};
}}
"""
@staticmethod
def warning():
"""Кнопка предупреждения (оранжевая)"""
return f"""
QPushButton {{
background-color: {Colors.WARNING};
color: {Colors.TEXT_ON_PRIMARY};
{ButtonStyles._base_style()}
}}
QPushButton:hover {{
background-color: {Colors.WARNING_DARK};
}}
QPushButton:pressed {{
background-color: {Colors.WARNING_LIGHT};
}}
"""
@staticmethod
def outline():
"""Контурная кнопка"""
return f"""
QPushButton {{
background-color: {Colors.TRANSPARENT};
color: {Colors.PRIMARY};
border: 1px solid {Colors.PRIMARY};
border-radius: {Spacing.BORDER_RADIUS_MD}px;
padding: {Spacing.SM}px {Spacing.LG}px;
font-weight: {Fonts.WEIGHT_SEMIBOLD};
}}
QPushButton:hover {{
background-color: {Colors.PRIMARY_BG};
}}
QPushButton:pressed {{
background-color: {Colors.PRIMARY_LIGHT};
color: {Colors.TEXT_ON_PRIMARY};
}}
"""
@staticmethod
def ghost():
"""Прозрачная кнопка (только текст)"""
return f"""
QPushButton {{
background-color: {Colors.TRANSPARENT};
color: {Colors.PRIMARY};
border: none;
padding: {Spacing.SM}px {Spacing.LG}px;
}}
QPushButton:hover {{
background-color: {Colors.PRIMARY_BG};
border-radius: {Spacing.BORDER_RADIUS_MD}px;
}}
"""
# ========== СТИЛИ ПОЛЕЙ ВВОДА ==========
class InputStyles:
"""Стили для полей ввода"""
@staticmethod
def default():
return f"""
QComboBox {{
border: 1px solid {Colors.BORDER_DEFAULT};
border-radius: {Spacing.BORDER_RADIUS_SM}px;
padding: 1px;
background-color: {Colors.WHITE};
min-height: {Spacing.INPUT_HEIGHT - 16}px;
}}
QLineEdit, QDoubleSpinBox, QSpinBox {{
border: 1px solid {Colors.BORDER_DEFAULT};
border-radius: {Spacing.BORDER_RADIUS_SM}px;
padding: {Spacing.SM}px;
background-color: {Colors.WHITE};
min-height: {Spacing.INPUT_HEIGHT - 16}px;
}}
QLineEdit:focus, QDoubleSpinBox:focus, QSpinBox:focus, QComboBox:focus {{
border: 1px solid {Colors.PRIMARY};
outline: none;
}}
QLineEdit:disabled, QDoubleSpinBox:disabled, QSpinBox:disabled {{
background-color: {Colors.GRAY_200};
color: {Colors.GRAY_600};
}}
"""
@staticmethod
def error():
return f"""
QLineEdit, QDoubleSpinBox, QSpinBox {{
border: 1px solid {Colors.DANGER};
border-radius: {Spacing.BORDER_RADIUS_SM}px;
padding: {Spacing.SM}px;
background-color: {Colors.DANGER_BG};
}}
"""
@staticmethod
def success_border():
return f"""
QLineEdit, QDoubleSpinBox, QSpinBox {{
border: 1px solid {Colors.SUCCESS};
border-radius: {Spacing.BORDER_RADIUS_SM}px;
padding: {Spacing.SM}px;
background-color: {Colors.SUCCESS_BG};
}}
"""
# ========== СТИЛИ ТАБЛИЦ ==========
class TableStyles:
"""Стили для таблиц"""
@staticmethod
def default():
return f"""
QTableWidget {{
gridline-color: {Colors.BORDER_LIGHT};
background-color: {Colors.WHITE};
alternate-background-color: {Colors.TABLE_ALTERNATE_ROW};
selection-background-color: {Colors.PRIMARY_BG};
selection-color: {Colors.TEXT_PRIMARY};
}}
QHeaderView::section {{
background-color: {Colors.INFO};
color: {Colors.TEXT_ON_DARK};
padding: {Spacing.SM}px;
font-weight: {Fonts.WEIGHT_BOLD};
border: none;
}}
QTableWidget::item {{
padding: {Spacing.XS}px;
}}
QTableWidget::item:selected {{
background-color: {Colors.PRIMARY_BG};
color: {Colors.TEXT_PRIMARY};
}}
"""
@staticmethod
def compact():
"""Компактный стиль таблицы"""
return f"""
QTableWidget {{
gridline-color: {Colors.BORDER_LIGHT};
background-color: {Colors.WHITE};
}}
QHeaderView::section {{
background-color: {Colors.INFO};
color: {Colors.TEXT_ON_DARK};
padding: {Spacing.XS}px;
font-weight: {Fonts.WEIGHT_BOLD};
}}
QTableWidget::item {{
padding: {Spacing.XS}px;
}}
"""
# ========== СТИЛИ ВКЛАДОК ==========
class TabStyles:
"""Стили для вкладок (табов)"""
@staticmethod
def default():
return f"""
QTabWidget::pane {{
border: 1px solid {Colors.BORDER_DEFAULT};
border-radius: {Spacing.BORDER_RADIUS_MD}px;
background-color: {Colors.WHITE};
}}
QTabBar::tab {{
background-color: {Colors.GRAY_200};
padding: {Spacing.SM}px {Spacing.XL}px;
margin-right: {Spacing.XS}px;
border-top-left-radius: {Spacing.BORDER_RADIUS_SM}px;
border-top-right-radius: {Spacing.BORDER_RADIUS_SM}px;
}}
QTabBar::tab:selected {{
background-color: {Colors.PRIMARY};
color: {Colors.TEXT_ON_PRIMARY};
}}
QTabBar::tab:hover:!selected {{
background-color: {Colors.PRIMARY_BG};
}}
"""
# ========== СТИЛИ ГРУПП ==========
class GroupBoxStyles:
"""Стили для групп (GroupBox)"""
@staticmethod
def default():
return f"""
QGroupBox {{
font-weight: {Fonts.WEIGHT_BOLD};
border: 2px solid {Colors.BORDER_DEFAULT};
border-radius: {Spacing.BORDER_RADIUS_MD}px;
margin-top: {Spacing.LG}px;
padding-top: {Spacing.LG}px;
font-size: {Fonts.SIZE_MEDIUM}px;
}}
QGroupBox::title {{
subcontrol-origin: margin;
left: {Spacing.LG}px;
padding: 10 {Spacing.MD}px;
color: {Colors.INFO};
}}
"""
@staticmethod
def card():
"""Стиль карточки"""
return f"""
QGroupBox {{
background-color: {Colors.WHITE};
border: 1px solid {Colors.BORDER_DEFAULT};
border-radius: {Spacing.BORDER_RADIUS_LG}px;
margin-top: {Spacing.LG}px;
padding-top: {Spacing.LG}px;
}}
QGroupBox::title {{
subcontrol-origin: margin;
left: {Spacing.LG}px;
padding: 10 {Spacing.MD}px;
color: {Colors.PRIMARY};
}}
"""
# ========== СТИЛИ ПРОГРЕССА ==========
class ProgressStyles:
"""Стили для индикаторов прогресса"""
@staticmethod
def default():
return f"""
QProgressBar {{
border: 1px solid {Colors.BORDER_DEFAULT};
border-radius: {Spacing.BORDER_RADIUS_SM}px;
text-align: center;
background-color: {Colors.GRAY_200};
}}
QProgressBar::chunk {{
background-color: {Colors.PRIMARY};
border-radius: {Spacing.BORDER_RADIUS_SM}px;
}}
"""
@staticmethod
def success():
return f"""
QProgressBar::chunk {{
background-color: {Colors.SUCCESS};
}}
"""
# ========== СТИЛИ СТАТУСОВ ==========
class StatusStyles:
"""Стили для статусных сообщений"""
@staticmethod
def info():
return f"""
background-color: {Colors.PRIMARY_BG};
color: {Colors.PRIMARY_DARK};
padding: {Spacing.MD}px;
border-radius: {Spacing.BORDER_RADIUS_MD}px;
border-left: 4px solid {Colors.PRIMARY};
"""
@staticmethod
def success():
return f"""
background-color: {Colors.SUCCESS_BG};
color: {Colors.SUCCESS_DARK};
padding: {Spacing.MD}px;
border-radius: {Spacing.BORDER_RADIUS_MD}px;
border-left: 4px solid {Colors.SUCCESS};
"""
@staticmethod
def warning():
return f"""
background-color: {Colors.WARNING_BG};
color: {Colors.WARNING_DARK};
padding: {Spacing.MD}px;
border-radius: {Spacing.BORDER_RADIUS_MD}px;
border-left: 4px solid {Colors.WARNING};
"""
@staticmethod
def error():
return f"""
background-color: {Colors.DANGER_BG};
color: {Colors.DANGER_DARK};
padding: {Spacing.MD}px;
border-radius: {Spacing.BORDER_RADIUS_MD}px;
border-left: 4px solid {Colors.DANGER};
"""
# ========== ПОЛНАЯ ТЕМА ==========
def get_full_stylesheet():
"""
Возвращает полную таблицу стилей для приложения
"""
return f"""
/* СТИЛИ ДЛЯ ВСЕХ КНОПОК ПО УМОЛЧАНИЮ */
QPushButton {{
background-color: {Colors.PRIMARY};
color: {Colors.TEXT_ON_PRIMARY};
border: none;
border-radius: {Spacing.BORDER_RADIUS_MD}px;
padding: {Spacing.SM}px {Spacing.LG}px;
font-weight: {Fonts.WEIGHT_SEMIBOLD};
font-size: {Fonts.SIZE_NORMAL}px;
}}
QPushButton:hover {{
background-color: {Colors.PRIMARY_LIGHT};
}}
QPushButton:pressed {{
background-color: {Colors.PRIMARY_DARK};
}}
/* Глобальные стили */
QLabel#mainTitle {{
{TitleStyles.main_title()}
}}
QMainWindow {{
background-color: {Colors.GRAY_200};
}}
QWidget {{
font-family: {Fonts.FAMILY_PRIMARY};
font-size: {Fonts.SIZE_NORMAL}px;
color: {Colors.TEXT_PRIMARY};
}}
QLabel {{
color: {Colors.TEXT_PRIMARY};
}}
/* Кнопки с идентификаторами */
QPushButton#primary {{
{ButtonStyles.primary()}
}}
QPushButton#success {{
{ButtonStyles.success()}
}}
QPushButton#danger {{
{ButtonStyles.danger()}
}}
QPushButton#warning {{
{ButtonStyles.warning()}
}}
QPushButton#outline {{
{ButtonStyles.outline()}
}}
/* Группы */
{GroupBoxStyles.default()}
/* Таблицы */
{TableStyles.default()}
/* Вкладки */
{TabStyles.default()}
/* Поля ввода */
{InputStyles.default()}
/* ScrollArea */
QScrollArea {{
border: none;
background-color: {Colors.TRANSPARENT};
}}
QScrollBar:vertical {{
border: none;
background-color: {Colors.GRAY_200};
width: {Spacing.LG}px;
border-radius: {Spacing.BORDER_RADIUS_SM}px;
}}
QScrollBar::handle:vertical {{
background-color: {Colors.GRAY_500};
min-height: {Spacing.XL}px;
border-radius: {Spacing.BORDER_RADIUS_SM}px;
}}
QScrollBar::handle:vertical:hover {{
background-color: {Colors.GRAY_600};
}}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
border: none;
background: none;
}}
/* ToolBar */
QToolBar {{
background-color: {Colors.WHITE};
border-bottom: 1px solid {Colors.BORDER_DEFAULT};
padding: {Spacing.SM}px;
spacing: {Spacing.SM}px;
}}
QToolBar QToolButton {{
background-color: {Colors.TRANSPARENT};
padding: {Spacing.SM}px {Spacing.LG}px;
border-radius: {Spacing.BORDER_RADIUS_MD}px;
}}
QToolBar QToolButton:hover {{
background-color: {Colors.GRAY_200};
}}
/* Message Box */
QMessageBox {{
background-color: {Colors.WHITE};
}}
QMessageBox QPushButton {{
min-width: 80px;
padding: {Spacing.SM}px;
}}
"""
# ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==========
def apply_theme(widget):
"""
Применяет тему к виджету и всем его дочерним элементам
"""
from PyQt5.QtWidgets import QApplication
# Устанавливаем стиль приложения
app = QApplication.instance()
if app:
app.setStyleSheet(get_full_stylesheet())
# Дополнительно устанавливаем виджету
if widget:
widget.setStyleSheet(get_full_stylesheet())
def get_primary_color():
"""Возвращает основной цвет приложения"""
return Colors.PRIMARY
def get_success_color():
"""Возвращает цвет успеха"""
return Colors.SUCCESS
def get_danger_color():
"""Возвращает цвет опасности"""
return Colors.DANGER
def get_warning_color():
"""Возвращает цвет предупреждения"""
return Colors.WARNING