Теперь подсветка факторов эксперимента подсвечиваются соответственно значениям: + зелёным - красным, 0 (факторы с шагом в 0) - светлосерым. Убран лишний столбец в матрице факторного эксперимента

This commit is contained in:
2026-05-29 08:55:43 +05:00
parent 6400f04f1c
commit 2392735641
3 changed files with 113 additions and 208 deletions
+10 -95
View File
@@ -84,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)
@@ -143,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}"] = {
@@ -165,7 +119,6 @@ def generate_factorial_design(
# Добавление центральных точек # Добавление центральных точек
for i in range(center_points): for i in range(center_points):
center_experiment = {} center_experiment = {}
for j in range(k): for j in range(k):
center_experiment[f"Фактор_{j+1}"] = { center_experiment[f"Фактор_{j+1}"] = {
'coded': 0, 'coded': 0,
@@ -177,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)
@@ -294,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,
+79 -101
View File
@@ -553,10 +553,9 @@ class MainWindow(QMainWindow):
"""Генерирует план эксперимента""" """Генерирует план эксперимента"""
all_factors = self._get_factors_from_table() all_factors = self._get_factors_from_table()
factors = get_active_factors(all_factors) factors = get_active_factors(all_factors)
# Выбираем только те факторы, шаг которых не равен нулю
i_factors = get_inactive_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
@@ -567,75 +566,14 @@ class MainWindow(QMainWindow):
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)
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 + 4)
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):
solvent = convert_units(total_volume,solvent_unit)
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']
solvent -= convert_units(value,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, item)
if n_i_factors>0:
for f_idx in range(n_i_factors):
value = i_factors[f_idx]['center']
unit = i_factors[f_idx]['unit']
display = self._format_number(value)
if unit:
display += f" {unit}"
item = QTableWidgetItem(display)
item.setBackground(QColor(255, 255, 200))
self.design_matrix.setItem(exp_idx, n_factors + f_idx, item)
solvent = convert_units(solvent, "мкл", solvent_unit)
display = self._format_number(solvent)
if solvent_unit:
display += f" {solvent_unit}"
item = QTableWidgetItem(display)
self.design_matrix.setItem(exp_idx, n_factors+n_i_factors, 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+n_i_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):
"""Настраивает таблицу результатов для ввода данных""" """Настраивает таблицу результатов для ввода данных"""
# Получаем отклики (для простоты используем один отклик) # Получаем отклики (для простоты используем один отклик)
@@ -979,43 +917,15 @@ class MainWindow(QMainWindow):
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)
@@ -1028,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):
+12
View File
@@ -0,0 +1,12 @@
Меласса (разб. ×5.00),Глицерин,Этанол,Лимонная кислота,Вода,Тип,Отклик
2.25 мл,0.45 г,0.3 мл,0.06 г,26.94 мл,Факторная,
5.25 мл,0.45 г,0.3 мл,0.06 г,23.94 мл,Факторная,
2.25 мл,0.75 г,0.3 мл,0.06 г,26.64 мл,Факторная,
5.25 мл,0.75 г,0.3 мл,0.06 г,23.64 мл,Факторная,
2.25 мл,0.45 г,0.6 мл,0.06 г,26.64 мл,Факторная,
5.25 мл,0.45 г,0.6 мл,0.06 г,23.64 мл,Факторная,
2.25 мл,0.75 г,0.6 мл,0.06 г,26.34 мл,Факторная,
5.25 мл,0.75 г,0.6 мл,0.06 г,23.34 мл,Факторная,
3.75 мл,0.6 г,0.45 мл,0.06 г,25.14 мл,Центр #1,
3.75 мл,0.6 г,0.45 мл,0.06 г,25.14 мл,Центр #2,
3.75 мл,0.6 г,0.45 мл,0.06 г,25.14 мл,Центр #3,
1 Меласса (разб. ×5.00) Глицерин Этанол Лимонная кислота Вода Тип Отклик
2 2.25 мл 0.45 г 0.3 мл 0.06 г 26.94 мл Факторная
3 5.25 мл 0.45 г 0.3 мл 0.06 г 23.94 мл Факторная
4 2.25 мл 0.75 г 0.3 мл 0.06 г 26.64 мл Факторная
5 5.25 мл 0.75 г 0.3 мл 0.06 г 23.64 мл Факторная
6 2.25 мл 0.45 г 0.6 мл 0.06 г 26.64 мл Факторная
7 5.25 мл 0.45 г 0.6 мл 0.06 г 23.64 мл Факторная
8 2.25 мл 0.75 г 0.6 мл 0.06 г 26.34 мл Факторная
9 5.25 мл 0.75 г 0.6 мл 0.06 г 23.34 мл Факторная
10 3.75 мл 0.6 г 0.45 мл 0.06 г 25.14 мл Центр #1
11 3.75 мл 0.6 г 0.45 мл 0.06 г 25.14 мл Центр #2
12 3.75 мл 0.6 г 0.45 мл 0.06 г 25.14 мл Центр #3