diff --git a/calculations/doe.py b/calculations/doe.py index 15884c5..bd8420f 100644 --- a/calculations/doe.py +++ b/calculations/doe.py @@ -13,7 +13,13 @@ from typing import List, Dict, Tuple, Optional import random 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 = { 'absolute': 'ед.', # абсолютный шаг @@ -22,41 +28,49 @@ FACTOR_TYPES = { def calculate_factor_levels( - center_value: float, - step_value: float, - step_type: str, - base_value: float = None -) -> Tuple[float, float]: - """ - РАССЧИТЫВАЕТ ВЕРХНИЙ И НИЖНИЙ УРОВНИ ФАКТОРА - - Параметры: - center_value: нулевой уровень фактора (центральная точка) - step_value: значение шага - step_type: тип шага ("ед." - абсолютный, "%" - относительный) - base_value: базовое значение для относительного шага (если None, используется center_value) - - Возвращает: - (high_level, low_level): верхний и нижний уровни - - Пример: - >>> calculate_factor_levels(100, 10, "%") - (110.0, 90.0) - >>> calculate_factor_levels(100, 20, "ед.") - (120.0, 80.0) - """ - # Определяем абсолютное значение шага - if step_type == "%": - 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 - low_level = center_value - step_abs - - return high_level, low_level + factor: 'FactorData', + total_volume: float = 1000, + volume_unit: str = "мл" +) -> 'FactorData': + """Рассчитывает high/low уровни фактора через калькулятор сред""" + # Функция для расчёта количества реагента при заданном проценте + def calc_amount(percentage: float) -> float: + reagents = [{ + 'name': factor.name, + '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'] + + + # Определяем проценты для верхнего и нижнего уровней + if factor.step_type == "%": + low = calc_amount(factor.percentage - factor.step) # low - нижний уровень + high = calc_amount(factor.percentage + factor.step) # high - верхний уровень + else: # "ед." + # Конвертируем абсолютный шаг в проценты + low = calc_amount(factor.percentage) - factor.step + high = calc_amount(factor.percentage) + factor.step + + # ВОЗВРАЩАЕМ НОВЫЙ ОБЪЕКТ FactorData + 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 + ) + +def calculate_all_factors_levels(factors: List['FactorData'], **kwargs) -> List['FactorData']: + """Рассчитывает уровни для всех факторов""" + return [calculate_factor_levels(f, **kwargs) for f in factors] def generate_factorial_design( factors: List[Dict], diff --git a/calculations/medium.py b/calculations/medium.py index 03d008f..773e4b2 100644 --- a/calculations/medium.py +++ b/calculations/medium.py @@ -52,11 +52,8 @@ def convert_units(value: float, from_unit: str, to_unit: str = None) -> float: units_map = MASS_UNITS else: raise ValueError(f"Неизвестная единица измерения: {from_unit}") - print("units_map = ",units_map) # Конвертируем в базовую единицу (мкл для объёма, мг для массы) value_in_base = value * units_map[from_unit] - print ("units_map[$from_unit]",units_map[from_unit]) - print ("value_in_base = ",value_in_base) # Если нужна конвертация в другую единицу if from_unit and to_unit in units_map: return value_in_base / units_map[to_unit] @@ -165,7 +162,6 @@ def calculate_medium_composition( base_unit = "мг" else: raise ValueError(f"Неизвестная единица измерения: {from_unit}") - print ("unit = ",unit) # conversion_factor = reagent.get('conversion_factor', 1.0) dilution_factor = reagent.get('dilution_factor', 1.0) @@ -174,19 +170,16 @@ def calculate_medium_composition( # 1. Объём реагента в среде (исходя из процента) amount_in_base = (percentage / 100) * total_base - print ("amount_in_base = ",amount_in_base) # 2. Применяем коэффициент конверсии # adjusted_amount_base = amount_in_base * conversion_factor # 3. Конвертируем в нужную единицу (без учёта разбавления) # undiluted_amount = convert_units(adjusted_amount_base, volume_unit, unit) undiluted_amount = convert_units(amount_in_base, base_unit, unit) - print ("undiluted_amount = ",undiluted_amount) # 4. Применяем разбавление if dilution_factor <= 0: dilution_factor = 1.0 diluted_amount = undiluted_amount * dilution_factor - print ("diluted_amount = ", diluted_amount) # 5. Для объёмных реагентов учитываем в расчёте растворителя if is_volume: reagent_volume_base = convert_units(diluted_amount, unit) diff --git a/gui.py b/gui.py index a03134f..5a11742 100644 --- a/gui.py +++ b/gui.py @@ -30,7 +30,6 @@ from calculations.doe import ( calculate_factor_levels, FACTOR_TYPES ) - # Импорт моделей для JSON try: from calculations.models.project_data import ( @@ -355,7 +354,8 @@ class MainWindow(QMainWindow): # Добавляем начальный фактор self._add_factor_row() - + # ПОДКЛЮЧАЕМ СИГНАЛ ИЗМЕНЕНИЯ ЯЧЕЕК + self.factors_table.cellChanged.connect(self._on_factor_changed) return tab def _add_factor_row(self, name = "", percentage = "0", dilution = "0", center = "0", step ="0", step_type = "%", high = "", low = "", unit= "г"): @@ -374,6 +374,7 @@ class MainWindow(QMainWindow): step_type_combo = QComboBox() step_type_combo.addItems(["%", "ед."]) step_type_combo.setCurrentText(step_type) + step_type_combo.currentTextChanged.connect(lambda: self._on_factor_changed(row, 5)) self.factors_table.setCellWidget(row, 5, step_type_combo) high_item = QTableWidgetItem(high) @@ -388,7 +389,11 @@ class MainWindow(QMainWindow): unit_measure = QComboBox() unit_measure.addItems(["мкл", "мл", "л", "мг", "г", "кг"]) unit_measure.setCurrentText(unit) + unit_measure.currentTextChanged.connect(lambda: self._on_factor_changed(row, 8)) + self.factors_table.setCellWidget(row, 8, unit_measure) + + return row def _remove_factor_row(self): @@ -411,8 +416,8 @@ class MainWindow(QMainWindow): for reagent in result['reagents']: name = reagent['name'] if reagent.get('dilution_factor', 1.0) != 1.0: - name += f" (разб. ×{reagent['dilution_factor']:.g})" - percentage = f"{reagent['percentage']:.g}" + name += f"(разб. ×{reagent['dilution_factor']:g})" + percentage = f"{reagent['percentage']:g}" dilution = f"{reagent.get('dilution_factor', 1.0):g}" center = str(reagent.get('calculated_amount')) step = "" @@ -462,7 +467,33 @@ class MainWindow(QMainWindow): continue return factors - + def _on_factor_changed(self, row, column): + """При изменении ячейки пересчитываем уровни""" + if getattr(self, '_loading_data', False) or column not in [3, 4, 5, 8]: + return + + try: + # Получаем данные + percentage = float(self.factors_table.item(row, 1).text()) + dilution = float(self.factors_table.item(row, 2).text()) + center = float(self.factors_table.item(row, 3).text()) + step = float(self.factors_table.item(row, 4).text()) + step_type = self.factors_table.cellWidget(row, 5).currentText() + unit = self.factors_table.cellWidget(row, 8).currentText() + name = self.factors_table.item(row, 0).text() + + # Рассчитываем + factor = FactorData(percentage=percentage, dilution_factor=dilution, name=name, center=center, low=0, high=0, step=step, step_type=step_type, unit=unit) + result = calculate_factor_levels(factor, self.total_volume_spin.value(), self.volume_unit_combo.currentText()) + if result: + self.factors_table.blockSignals(True) + self.factors_table.item(row, 6).setText(self._format_number(result.high)) + self.factors_table.item(row, 7).setText(self._format_number(result.low)) + self.factors_table.blockSignals(False) + + except (ValueError, AttributeError): + pass + # ========== ВКЛАДКА 3: МАТРИЦА ПЛАНИРОВАНИЯ ========== def _create_design_tab(self): @@ -833,61 +864,66 @@ class MainWindow(QMainWindow): def _apply_project_data(self, project): """Применяет загруженные данные к 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: - if factor.percentage is not None: - percentage = str(factor.percentage) - if factor.dilution_factor is not None: - dilution = str(factor.dilution_factor) - self._add_factor_row(factor.name, percentage, dilution, str(factor.center), - str(factor.step), factor.step_type, str(factor.high), - str(factor.low), factor.unit) - # Применяем настройки эксперимента - self.center_points_spin.setValue(project.experiment_center_points) - self.randomize_check.setChecked(project.experiment_randomize) - - # Если есть результаты, загружаем их - if project.experiment_results and project.experiment_results.design: - self.generated_design = project.experiment_results.design - # Обновляем отображение матрицы - self._refresh_design_matrix() - - # Загружаем результаты в таблицу - if project.experiment_results.results: - for i, row_results in enumerate(project.experiment_results.results): - if i < self.results_table.rowCount() and row_results: - self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0]))) - - # Переключаемся на вкладку с экспериментом - if self.tab_widget: - self.tab_widget.setCurrentIndex(0) + self._loading_data = True # <- ОТКЛЮЧАЕМ ОБРАБОТЧИК + try: + + # Применяем данные калькулятора сред + self.total_volume_spin.setValue(project.medium_total_volume) + + index = self.volume_unit_combo.findText(project.medium_volume_unit) + if index >= 0: + self.volume_unit_combo.setCurrentIndex(index) + + self.solvent_input.setText(project.medium_solvent) + + # Очищаем и заполняем таблицу реагентов + self.reagents_table.setRowCount(0) + for reagent in project.medium_reagents: + row = self.reagents_table.rowCount() + self.reagents_table.insertRow(row) + self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name)) + self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage))) + + unit_combo = QComboBox() + unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"]) + unit_combo.setCurrentText(reagent.unit) + self.reagents_table.setCellWidget(row, 2, unit_combo) + + self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor))) + self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor))) + self.reagents_table.setItem(row, 5, QTableWidgetItem("")) + + # Применяем данные факторов эксперимента + self.factors_table.setRowCount(0) + for factor in project.experiment_factors: + if factor.percentage is not None: + percentage = str(factor.percentage) + if factor.dilution_factor is not None: + dilution = str(factor.dilution_factor) + self._add_factor_row(factor.name, percentage, dilution, str(factor.center), + str(factor.step), factor.step_type, str(factor.high), + str(factor.low), factor.unit) + # Применяем настройки эксперимента + self.center_points_spin.setValue(project.experiment_center_points) + self.randomize_check.setChecked(project.experiment_randomize) + + # Если есть результаты, загружаем их + if project.experiment_results and project.experiment_results.design: + self.generated_design = project.experiment_results.design + # Обновляем отображение матрицы + self._refresh_design_matrix() + + # Загружаем результаты в таблицу + if project.experiment_results.results: + for i, row_results in enumerate(project.experiment_results.results): + if i < self.results_table.rowCount() and row_results: + self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0]))) + + # Переключаемся на вкладку с экспериментом + if self.tab_widget: + self.tab_widget.setCurrentIndex(0) + finally: + self._loading_data = False # <- ВКЛЮЧАЕМ ОБРАТНО def _refresh_design_matrix(self): """Обновляет отображение матрицы планирования"""