Initial commit: планирование эксперимента и оптимизация смесей
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.so
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.whl
|
||||||
|
|
||||||
|
# Виртуальное окружение
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Файлы проекта
|
||||||
|
project.json
|
||||||
|
*.csv
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Редакторы
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Планирование эксперимента и оптимизация смесей
|
||||||
|
|
||||||
|
Программа для автоматизированного планирования эксперимента, построения регрессионных моделей, проверки значимости факторов и крутого восхождения (градиентного метода) для поиска оптимума в задачах со смесями.
|
||||||
|
|
||||||
|
## Основные возможности
|
||||||
|
|
||||||
|
- **Редактор состава**: создание и изменение ингредиентов смеси (проценты, разбавление, плотность).
|
||||||
|
- **Планирование эксперимента**: генерация различных планов:
|
||||||
|
- Полный факторный план `2^k`
|
||||||
|
- Центральный композиционный план (ЦКП) с возможностью дробного ядра
|
||||||
|
- Дробный факторный план `2^(k-p)`
|
||||||
|
- План Плакетта-Бермана
|
||||||
|
- План Бокса-Бенкена
|
||||||
|
- **Ввод откликов**: добавление, редактирование, переименование и удаление откликов.
|
||||||
|
- **Анализ значений**:
|
||||||
|
- Построение линейной регрессии (МНК)
|
||||||
|
- Проверка значимости коэффициентов по t-критерию с заданным уровнем α
|
||||||
|
- R², скорректированный R², F-критерий
|
||||||
|
- Дисперсия в центре и доверительный интервал
|
||||||
|
- Исключение незначимых факторов и пересчёт модели
|
||||||
|
- **Крутое восхождение**:
|
||||||
|
- Расчёт коэффициентов модели для выбранного отклика
|
||||||
|
- Генерация точек вдоль градиента (максимизация или минимизация)
|
||||||
|
- Возможность установить выбранную точку как новый состав
|
||||||
|
- **Экспорт данных**: все таблицы можно сохранить в CSV или скопировать в буфер обмена (TSV).
|
||||||
|
|
||||||
|
## Установка и запуск
|
||||||
|
|
||||||
|
1. Клонируйте репозиторий:
|
||||||
|
```bash
|
||||||
|
git clone <url>
|
||||||
|
cd experiment
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
# analysis.py
|
||||||
|
"""
|
||||||
|
Анализ экспериментальных данных: регрессия, проверка значимости, дисперсия в центре.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
from core.table_io import handle_table_output
|
||||||
|
from core.statistics import (
|
||||||
|
compute_regression,
|
||||||
|
get_center_indices,
|
||||||
|
compute_center_stats,
|
||||||
|
chi2_confidence_interval,
|
||||||
|
get_t_critical,
|
||||||
|
regression_summary
|
||||||
|
)
|
||||||
|
|
||||||
|
def analysis_menu(mixture, design, responses):
|
||||||
|
if not design.plan or not responses:
|
||||||
|
print("Нет данных для анализа: план или отклики пусты.")
|
||||||
|
input("Нажмите Enter для продолжения...")
|
||||||
|
return
|
||||||
|
|
||||||
|
active_indices = design._active_indices()
|
||||||
|
k = len(active_indices)
|
||||||
|
if k < 2:
|
||||||
|
print("Для анализа необходимо минимум 2 активных фактора.")
|
||||||
|
input("Нажмите Enter для продолжения...")
|
||||||
|
return
|
||||||
|
|
||||||
|
factor_names = []
|
||||||
|
for idx in active_indices:
|
||||||
|
ing_index = idx + 1
|
||||||
|
if ing_index < len(mixture.ings):
|
||||||
|
factor_names.append(mixture.ings[ing_index].name)
|
||||||
|
else:
|
||||||
|
factor_names.append(f"x{idx+1}")
|
||||||
|
|
||||||
|
print("\nВведите уровень значимости alpha (например, 0.05, 0.01, 0.001):")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
alpha_input = input("alpha (по умолчанию 0.05): ").strip()
|
||||||
|
if alpha_input == "":
|
||||||
|
alpha = 0.05
|
||||||
|
else:
|
||||||
|
alpha = float(alpha_input)
|
||||||
|
if alpha <= 0 or alpha >= 1:
|
||||||
|
print("alpha должна быть в интервале (0, 1).")
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
print("Введите число.")
|
||||||
|
|
||||||
|
plan_matrix = np.array([[row[j] for j in active_indices] for row in design.plan])
|
||||||
|
N = plan_matrix.shape[0]
|
||||||
|
X = np.hstack([np.ones((N, 1)), plan_matrix])
|
||||||
|
df_res = N - (k + 1)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("АНАЛИЗ ЗНАЧЕНИЙ")
|
||||||
|
print(f"Уровень значимости α = {alpha}")
|
||||||
|
print("Доступные отклики:")
|
||||||
|
for idx, resp in enumerate(responses, start=1):
|
||||||
|
print(f"{idx}. {resp['name']}")
|
||||||
|
print(f"{len(responses)+1}. Анализировать все отклики")
|
||||||
|
print("(Enter - выход в главное меню)")
|
||||||
|
print("=" * 50)
|
||||||
|
choice = input("Выберите отклик для анализа: ").strip()
|
||||||
|
if choice == "":
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
idx = int(choice) - 1
|
||||||
|
if idx < 0:
|
||||||
|
print("Неверный номер.")
|
||||||
|
continue
|
||||||
|
if idx < len(responses):
|
||||||
|
analyze_single_response(responses[idx], X, k, df_res, design, alpha, factor_names)
|
||||||
|
input("Нажмите Enter для продолжения...")
|
||||||
|
elif idx == len(responses):
|
||||||
|
for resp in responses:
|
||||||
|
print(f"\n--- Анализ отклика '{resp['name']}' ---")
|
||||||
|
analyze_single_response(resp, X, k, df_res, design, alpha, factor_names)
|
||||||
|
input("Нажмите Enter для продолжения...")
|
||||||
|
else:
|
||||||
|
print("Неверный номер.")
|
||||||
|
except ValueError:
|
||||||
|
print("Введите число.")
|
||||||
|
|
||||||
|
def analyze_single_response(response, X, k, df_res, design, alpha, factor_names):
|
||||||
|
y = np.array(response["values"])
|
||||||
|
N = len(y)
|
||||||
|
if N != X.shape[0]:
|
||||||
|
print(f"Ошибка: число значений отклика ({N}) не соответствует числу опытов ({X.shape[0]})")
|
||||||
|
return
|
||||||
|
|
||||||
|
reg = compute_regression(X, y)
|
||||||
|
beta = reg["beta"]
|
||||||
|
se = reg["se"]
|
||||||
|
t_stats = reg["t_stats"]
|
||||||
|
s2_res = reg["s2_res"]
|
||||||
|
df_res_actual = reg["df_res"]
|
||||||
|
y_pred = reg["y_pred"]
|
||||||
|
|
||||||
|
if np.isnan(beta).any():
|
||||||
|
print("Ошибка при расчёте регрессии: матрица вырождена.")
|
||||||
|
return
|
||||||
|
|
||||||
|
def print_and_get_tcrit(beta, se, t_stats, df_res, y_pred, y, k, factor_names, alpha):
|
||||||
|
summary = regression_summary(y, y_pred, k, df_res)
|
||||||
|
r2 = summary["r2"]
|
||||||
|
r2_adj = summary["r2_adj"]
|
||||||
|
f_stat = summary["f_stat"]
|
||||||
|
f_pvalue = summary["f_pvalue"]
|
||||||
|
|
||||||
|
print("\n=== Регрессионный анализ ===")
|
||||||
|
print(f"Число опытов: {len(y)}")
|
||||||
|
print(f"Число факторов: {k}")
|
||||||
|
print(f"Степени свободы остатков: {df_res}")
|
||||||
|
print(f"Стандартная ошибка регрессии: {np.sqrt(s2_res) if not np.isnan(s2_res) else np.nan:.6f}")
|
||||||
|
print(f"Множественный R: {np.sqrt(r2) if not np.isnan(r2) else np.nan:.6f}")
|
||||||
|
print(f"R-квадрат: {r2:.6f}")
|
||||||
|
print(f"Скорректированный R-квадрат: {r2_adj:.6f}")
|
||||||
|
print(f"F-статистика: {f_stat:.4f}, p-value: {f_pvalue:.6f}")
|
||||||
|
|
||||||
|
t_crit = get_t_critical(df_res, alpha=alpha)
|
||||||
|
print("\nКоэффициенты регрессии:")
|
||||||
|
headers = ["Коэффициент", "Значение", "Станд. ошибка", "t-статистика", f"|t| > t_крит({alpha})?"]
|
||||||
|
rows = []
|
||||||
|
coef_names = ["Свободный член"] + factor_names
|
||||||
|
for i, name in enumerate(coef_names):
|
||||||
|
sig = "Да" if (not np.isnan(t_stats[i]) and abs(t_stats[i]) > t_crit and not np.isnan(t_crit)) else "Нет"
|
||||||
|
rows.append([
|
||||||
|
name,
|
||||||
|
f"{beta[i]:.6f}",
|
||||||
|
f"{se[i]:.6f}",
|
||||||
|
f"{t_stats[i]:.4f}",
|
||||||
|
sig
|
||||||
|
])
|
||||||
|
handle_table_output(headers, rows, title="", ask_each_time=True)
|
||||||
|
if not np.isnan(t_crit):
|
||||||
|
print(f"Критическое значение t (alpha={alpha}, df={df_res}): {t_crit:.4f}")
|
||||||
|
return t_crit
|
||||||
|
|
||||||
|
t_crit = print_and_get_tcrit(beta, se, t_stats, df_res_actual, y_pred, y, k, factor_names, alpha)
|
||||||
|
|
||||||
|
center_indices = get_center_indices(design.plan, design._active_indices())
|
||||||
|
if len(center_indices) >= 2:
|
||||||
|
y_center = y[center_indices]
|
||||||
|
center_mean, center_var = compute_center_stats(y_center)
|
||||||
|
print("\n=== Анализ центральных точек ===")
|
||||||
|
print(f"Количество центральных точек: {len(center_indices)}")
|
||||||
|
print(f"Среднее значение отклика в центре: {center_mean:.6f}")
|
||||||
|
print(f"Дисперсия в центре (несмещённая): {center_var:.6f}")
|
||||||
|
print(f"Стандартное отклонение: {np.sqrt(center_var):.6f}")
|
||||||
|
lower, upper = chi2_confidence_interval(center_var, len(center_indices), alpha=alpha)
|
||||||
|
if not np.isnan(lower):
|
||||||
|
print(f"Доверительный интервал для дисперсии (уровень доверия {1-alpha}): [{lower:.6f}, {upper:.6f}]")
|
||||||
|
else:
|
||||||
|
print("\nНедостаточно центральных точек (нужно минимум 2).")
|
||||||
|
|
||||||
|
if k > 0 and not np.isnan(t_crit):
|
||||||
|
print("\nХотите исключить незначимые факторы и пересчитать модель?")
|
||||||
|
choice = input("(y/n, по умолчанию n): ").strip().lower()
|
||||||
|
if choice == 'y':
|
||||||
|
significant = [True] + [abs(t_stats[i]) > t_crit for i in range(1, k+1)]
|
||||||
|
if all(significant):
|
||||||
|
print("Все факторы значимы. Упрощённая модель не отличается от полной.")
|
||||||
|
return
|
||||||
|
X_new = X[:, significant]
|
||||||
|
reg_new = compute_regression(X_new, y)
|
||||||
|
beta_new = reg_new["beta"]
|
||||||
|
se_new = reg_new["se"]
|
||||||
|
t_stats_new = reg_new["t_stats"]
|
||||||
|
df_res_new = reg_new["df_res"]
|
||||||
|
y_pred_new = reg_new["y_pred"]
|
||||||
|
if np.isnan(beta_new).any():
|
||||||
|
print("Ошибка при пересчёте упрощённой модели.")
|
||||||
|
return
|
||||||
|
factor_names_new = ["Свободный член"] + [factor_names[i-1] for i in range(1, k+1) if significant[i]]
|
||||||
|
k_new = len(factor_names_new) - 1
|
||||||
|
print("\n=== Упрощённая модель (исключены незначимые факторы) ===")
|
||||||
|
print_and_get_tcrit(beta_new, se_new, t_stats_new, df_res_new, y_pred_new, y, k_new, factor_names_new[1:], alpha)
|
||||||
|
print("\nСовет: для использования упрощённой модели в крутом восхождении установите шаги для исключённых факторов равными 0 в планировании эксперимента.")
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# ascent.py
|
||||||
|
"""
|
||||||
|
Крутое восхождение (градиентный метод) для поиска оптимума.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
from core.table_io import handle_table_output
|
||||||
|
from core.statistics import compute_regression, compute_gradient_points
|
||||||
|
|
||||||
|
def ascent_menu(mixture, design, responses):
|
||||||
|
if not design.plan or not responses:
|
||||||
|
print("Нет данных для крутого восхождения: план или отклики пусты.")
|
||||||
|
input("Нажмите Enter для продолжения...")
|
||||||
|
return
|
||||||
|
|
||||||
|
active_indices = design._active_indices()
|
||||||
|
k = len(active_indices)
|
||||||
|
if k < 2:
|
||||||
|
print("Для крутого восхождения необходимо минимум 2 активных фактора.")
|
||||||
|
input("Нажмите Enter для продолжения...")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not mixture.ings or len(mixture.ings) < 2:
|
||||||
|
print("В смеси нет изменяемых ингредиентов.")
|
||||||
|
return
|
||||||
|
|
||||||
|
base_levels = []
|
||||||
|
factor_names = []
|
||||||
|
for idx in active_indices:
|
||||||
|
ing_index = idx + 1
|
||||||
|
if ing_index >= len(mixture.ings):
|
||||||
|
print(f"Ошибка: индекс ингредиента {ing_index} выходит за пределы списка.")
|
||||||
|
return
|
||||||
|
factor_names.append(mixture.ings[ing_index].name)
|
||||||
|
base_levels.append(mixture.ings[ing_index].value)
|
||||||
|
print(f"\nБазовые уровни факторов: {dict(zip(factor_names, base_levels))}")
|
||||||
|
|
||||||
|
print("\nДоступные отклики:")
|
||||||
|
for i, resp in enumerate(responses, start=1):
|
||||||
|
print(f"{i}. {resp['name']}")
|
||||||
|
try:
|
||||||
|
resp_idx = int(input("Выберите номер отклика для оптимизации: ")) - 1
|
||||||
|
if resp_idx < 0 or resp_idx >= len(responses):
|
||||||
|
print("Неверный номер.")
|
||||||
|
return
|
||||||
|
response = responses[resp_idx]
|
||||||
|
except ValueError:
|
||||||
|
print("Введите число.")
|
||||||
|
return
|
||||||
|
|
||||||
|
plan_matrix = np.array([[row[j] for j in active_indices] for row in design.plan])
|
||||||
|
N = plan_matrix.shape[0]
|
||||||
|
X = np.hstack([np.ones((N, 1)), plan_matrix])
|
||||||
|
y = np.array(response["values"])
|
||||||
|
if len(y) != N:
|
||||||
|
print("Ошибка: число значений отклика не совпадает с числом опытов.")
|
||||||
|
return
|
||||||
|
|
||||||
|
reg = compute_regression(X, y)
|
||||||
|
beta = reg["beta"]
|
||||||
|
if np.isnan(beta).any():
|
||||||
|
print("Ошибка расчёта коэффициентов регрессии.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n=== Коэффициенты модели (МНК) ===")
|
||||||
|
headers = ["Фактор", "Коэффициент"]
|
||||||
|
rows = [["Свободный член", f"{beta[0]:.6f}"]]
|
||||||
|
for i, name in enumerate(factor_names):
|
||||||
|
rows.append([name, f"{beta[i+1]:.6f}"])
|
||||||
|
handle_table_output(headers, rows, title="", ask_each_time=True)
|
||||||
|
|
||||||
|
print("\nВведите шаг движения (в % для первого фактора).")
|
||||||
|
print("Шаг для остальных факторов будет пропорционален их коэффициентам.")
|
||||||
|
try:
|
||||||
|
step = float(input("Шаг (положительное число): "))
|
||||||
|
if step <= 0:
|
||||||
|
print("Шаг должен быть положительным.")
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
print("Введите число.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
num_steps = int(input("Количество шагов (например, 5): "))
|
||||||
|
if num_steps <= 0:
|
||||||
|
print("Количество шагов должно быть положительным.")
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
print("Введите целое число.")
|
||||||
|
return
|
||||||
|
|
||||||
|
direction = input("Максимизировать отклик? (y/n, по умолчанию y): ").strip().lower()
|
||||||
|
maximize = direction != 'n'
|
||||||
|
|
||||||
|
try:
|
||||||
|
points = compute_gradient_points(
|
||||||
|
beta, base_levels, step, num_steps,
|
||||||
|
factor_names=factor_names,
|
||||||
|
maximize=maximize
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
headers = ["Шаг"] + factor_names + ["Предсказанный отклик"]
|
||||||
|
rows = []
|
||||||
|
for pt in points:
|
||||||
|
row = [str(pt["Шаг"])] + [f"{pt[name]:.4f}" for name in factor_names] + [f"{pt['Предсказанный отклик']:.4f}"]
|
||||||
|
rows.append(row)
|
||||||
|
handle_table_output(
|
||||||
|
headers, rows,
|
||||||
|
title=f"Точки крутого восхождения (направление: {'максимизация' if maximize else 'минимизация'})"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nВы можете выбрать одну из точек и установить её как новый состав.")
|
||||||
|
choice = input("Установить состав из точки? (введите номер шага или Enter для пропуска): ").strip()
|
||||||
|
if choice:
|
||||||
|
try:
|
||||||
|
step_num = int(choice) - 1
|
||||||
|
if 0 <= step_num < len(points):
|
||||||
|
pt = points[step_num]
|
||||||
|
for name in factor_names:
|
||||||
|
for idx, info in enumerate(mixture.ings):
|
||||||
|
if info.name == name:
|
||||||
|
mixture.ings[idx].value = pt[name]
|
||||||
|
break
|
||||||
|
mixture.comp_solvent_percent()
|
||||||
|
print("Состав обновлён. Текущие проценты:")
|
||||||
|
for info in mixture.ings:
|
||||||
|
print(f" {info.name}: {info.value:.2f}%")
|
||||||
|
else:
|
||||||
|
print("Неверный номер.")
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка ввода.")
|
||||||
|
|
||||||
|
input("Нажмите Enter для продолжения...")
|
||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
# core/classes.py
|
||||||
|
"""
|
||||||
|
Модуль содержит классы для представления ингредиентов и смеси.
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
class Ingredient:
|
||||||
|
"""Представляет ингредиент с именем, единицей измерения и значением."""
|
||||||
|
def __init__(self, name: str = "Water", unit: str = "ml", value: float = 1.0):
|
||||||
|
self.name = name
|
||||||
|
self.unit = unit
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {"name": self.name, "unit": self.unit, "value": self.value}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data):
|
||||||
|
return Ingredient(data["name"], data["unit"], data["value"])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.name}: {self.value} {self.unit}"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class IngredientInfo:
|
||||||
|
"""
|
||||||
|
Обёртка для ингредиента в составе смеси, добавляет разбавление и плотность.
|
||||||
|
"""
|
||||||
|
ingredient: Ingredient
|
||||||
|
dilution: float = 1.0
|
||||||
|
density: float = 1.0
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"ingredient": self.ingredient.to_dict(),
|
||||||
|
"dilution": self.dilution,
|
||||||
|
"density": self.density
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data):
|
||||||
|
ing = Ingredient.from_dict(data["ingredient"])
|
||||||
|
return IngredientInfo(
|
||||||
|
ingredient=ing,
|
||||||
|
dilution=data.get("dilution", 1.0),
|
||||||
|
density=data.get("density", 1.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.ingredient.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit(self):
|
||||||
|
return self.ingredient.unit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self.ingredient.value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, new_value):
|
||||||
|
self.ingredient.value = new_value
|
||||||
|
|
||||||
|
class Mixture:
|
||||||
|
"""
|
||||||
|
Основной класс для работы со смесью.
|
||||||
|
Первый ингредиент считается растворителем (его процент пересчитывается автоматически).
|
||||||
|
"""
|
||||||
|
def __init__(self, solvent=None, ings=None, total_amount=1000.0, total_unit="ml"):
|
||||||
|
all_ings = []
|
||||||
|
if solvent is None:
|
||||||
|
water = Ingredient(name="Water", unit=total_unit, value=100.0)
|
||||||
|
solvent_info = IngredientInfo(ingredient=water)
|
||||||
|
elif isinstance(solvent, Ingredient):
|
||||||
|
solvent_info = IngredientInfo(ingredient=solvent)
|
||||||
|
elif isinstance(solvent, IngredientInfo):
|
||||||
|
solvent_info = solvent
|
||||||
|
else:
|
||||||
|
raise TypeError("solvent must be Ingredient or IngredientInfo")
|
||||||
|
all_ings.append(solvent_info)
|
||||||
|
|
||||||
|
if ings is not None:
|
||||||
|
if not isinstance(ings, list):
|
||||||
|
raise TypeError("ings must be a list")
|
||||||
|
for item in ings:
|
||||||
|
if isinstance(item, Ingredient):
|
||||||
|
all_ings.append(IngredientInfo(ingredient=item))
|
||||||
|
elif isinstance(item, IngredientInfo):
|
||||||
|
all_ings.append(item)
|
||||||
|
else:
|
||||||
|
raise TypeError("Each item in ings must be Ingredient or IngredientInfo")
|
||||||
|
|
||||||
|
self.ings = all_ings
|
||||||
|
self.total_amount = total_amount
|
||||||
|
self.total_unit = total_unit
|
||||||
|
self.comp_solvent_percent()
|
||||||
|
|
||||||
|
def comp_solvent_percent(self) -> float:
|
||||||
|
"""Пересчёт процента растворителя = 100% - сумма остальных."""
|
||||||
|
if not self.ings:
|
||||||
|
return 0.0
|
||||||
|
total_others = sum(info.value for info in self.ings[1:])
|
||||||
|
if total_others > 100.0:
|
||||||
|
raise ValueError("Sum of all ingredient percentages exceeds 100%")
|
||||||
|
self.ings[0].value = 100.0 - total_others
|
||||||
|
return self.ings[0].value
|
||||||
|
|
||||||
|
def add_ingredient(self, ingredient: Ingredient, dilution: float = 1.0, density: float = 1.0):
|
||||||
|
info = IngredientInfo(ingredient, dilution, density)
|
||||||
|
self.ings.append(info)
|
||||||
|
self.comp_solvent_percent()
|
||||||
|
|
||||||
|
def remove_ingredient(self, index: int):
|
||||||
|
if index == 0:
|
||||||
|
raise ValueError("Нельзя удалить растворитель (первый ингредиент).")
|
||||||
|
removed = self.ings.pop(index)
|
||||||
|
self.comp_solvent_percent()
|
||||||
|
return removed
|
||||||
|
|
||||||
|
def get_absolute_ingredients(self):
|
||||||
|
"""Возвращает абсолютные значения ингредиентов (с учётом плотности и разбавления)."""
|
||||||
|
abs_list = []
|
||||||
|
sum_rest = 0.0
|
||||||
|
for info in self.ings[1:]:
|
||||||
|
abs_value = (info.value / 100.0) * self.total_amount * info.dilution * info.density
|
||||||
|
abs_list.append(Ingredient(name=info.name, unit=info.unit, value=abs_value))
|
||||||
|
sum_rest += abs_value
|
||||||
|
if sum_rest > self.total_amount:
|
||||||
|
raise ValueError("Сумма ингредиентов (кроме растворителя) превышает общее количество")
|
||||||
|
solvent_abs = self.total_amount - sum_rest
|
||||||
|
solvent = Ingredient(name=self.ings[0].name, unit=self.ings[0].unit, value=solvent_abs)
|
||||||
|
abs_list.insert(0, solvent)
|
||||||
|
return abs_list
|
||||||
|
|
||||||
|
def get_ingredient_info(self, index: int):
|
||||||
|
return self.ings[index]
|
||||||
|
|
||||||
|
def set_total_amount(self, new_amount: float, new_unit: str):
|
||||||
|
if new_amount <= 0:
|
||||||
|
raise ValueError("Общее количество должно быть положительным.")
|
||||||
|
if not new_unit.strip():
|
||||||
|
raise ValueError("Единица измерения не может быть пустой.")
|
||||||
|
self.total_amount = new_amount
|
||||||
|
self.total_unit = new_unit.strip()
|
||||||
|
|
||||||
|
def set_composition(self, *percentages):
|
||||||
|
expected = len(self.ings) - 1
|
||||||
|
if len(percentages) != expected:
|
||||||
|
raise ValueError(f"Expected {expected} percentages, got {len(percentages)}")
|
||||||
|
total = sum(percentages)
|
||||||
|
if total > 100.0:
|
||||||
|
raise ValueError(f"Sum of percentages ({total:.2f}) exceeds 100")
|
||||||
|
for i, pct in enumerate(percentages, start=1):
|
||||||
|
if pct < 0:
|
||||||
|
raise ValueError(f"Percentage for {self.ings[i].name} cannot be negative")
|
||||||
|
self.ings[i].value = pct
|
||||||
|
self.comp_solvent_percent()
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"ings": [info.to_dict() for info in self.ings],
|
||||||
|
"total_amount": self.total_amount,
|
||||||
|
"total_unit": self.total_unit
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data):
|
||||||
|
ings = [IngredientInfo.from_dict(item) for item in data["ings"]]
|
||||||
|
solvent = ings[0] if ings else None
|
||||||
|
rest = ings[1:] if len(ings) > 1 else None
|
||||||
|
return Mixture(
|
||||||
|
solvent=solvent,
|
||||||
|
ings=rest,
|
||||||
|
total_amount=data["total_amount"],
|
||||||
|
total_unit=data["total_unit"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
parts = []
|
||||||
|
for info in self.ings:
|
||||||
|
parts.append(f"{info.name} {info.value:.1f}% (разб.{info.dilution:.2f}, плотн.{info.density:.2f})")
|
||||||
|
return f"Mixture({', '.join(parts)}, total={self.total_amount} {self.total_unit})"
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
# core/doe.py
|
||||||
|
"""
|
||||||
|
Модуль с функциями для генерации планов эксперимента.
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def ispoz(N):
|
||||||
|
"""Проверяет, является ли N положительным целым числом (для использования в ze)."""
|
||||||
|
return isinstance(N, (int, float)) and not isinstance(N, bool) and N > 0
|
||||||
|
|
||||||
|
def ze(k, n):
|
||||||
|
"""Генерирует матрицу размера n x k, заполненную нулями (центральные точки)."""
|
||||||
|
if n is None or k is None:
|
||||||
|
return []
|
||||||
|
elif ispoz(k) and ispoz(n):
|
||||||
|
return [[0] * k for _ in range(n)]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def ffe(k):
|
||||||
|
"""Полный факторный план 2^k. Возвращает список списков размером 2^k x k со значениями ±1."""
|
||||||
|
if k < 2:
|
||||||
|
raise ValueError("Factor must be at least 2")
|
||||||
|
N = 2 ** k
|
||||||
|
out = []
|
||||||
|
for i in range(N):
|
||||||
|
row = []
|
||||||
|
for j in range(k):
|
||||||
|
bit = ((i >> j) & 1) * 2 - 1
|
||||||
|
row.append(bit)
|
||||||
|
out.append(row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def ccp(k, alpha=1.0, center_points=1, fractional=None):
|
||||||
|
"""
|
||||||
|
Центральный композиционный план (ЦКП) для k факторов.
|
||||||
|
alpha – звёздное плечо.
|
||||||
|
center_points – количество центральных точек.
|
||||||
|
fractional – степень дробления ядра (если None, используется полный факторный план).
|
||||||
|
"""
|
||||||
|
if k < 2:
|
||||||
|
raise ValueError("Для ЦКП необходимо минимум 2 фактора.")
|
||||||
|
if fractional is not None:
|
||||||
|
if fractional <= 0 or fractional >= k:
|
||||||
|
raise ValueError("Степень дробления p должна быть от 1 до k-1")
|
||||||
|
base_k = k - fractional
|
||||||
|
if base_k < 2:
|
||||||
|
raise ValueError(f"Ядро дробного факторного плана должно иметь минимум 2 фактора, а получается {base_k}.")
|
||||||
|
if fractional is not None:
|
||||||
|
from core.doe import fractional_factorial
|
||||||
|
core_plan = fractional_factorial(k, fractional)
|
||||||
|
else:
|
||||||
|
core_plan = ffe(k)
|
||||||
|
star_points = []
|
||||||
|
for i in range(k):
|
||||||
|
row = [0.0] * k
|
||||||
|
row[i] = alpha
|
||||||
|
star_points.append(row)
|
||||||
|
row_neg = [0.0] * k
|
||||||
|
row_neg[i] = -alpha
|
||||||
|
star_points.append(row_neg)
|
||||||
|
center_rows = [[0.0] * k for _ in range(center_points)]
|
||||||
|
plan = core_plan + star_points + center_rows
|
||||||
|
return plan
|
||||||
|
|
||||||
|
def fractional_factorial(k, p=1):
|
||||||
|
"""Дробный факторный план 2^(k-p). p – степень дробления."""
|
||||||
|
if k < 2 or p <= 0 or p >= k:
|
||||||
|
raise ValueError("Некорректные параметры: k>=2, 0<p<k")
|
||||||
|
base_k = k - p
|
||||||
|
if base_k < 2:
|
||||||
|
raise ValueError(f"Ядро должно содержать минимум 2 фактора, а получается {base_k}.")
|
||||||
|
N = 2 ** base_k
|
||||||
|
base_plan = ffe(base_k)
|
||||||
|
plan = []
|
||||||
|
for row in base_plan:
|
||||||
|
new_row = row.copy()
|
||||||
|
for i in range(p):
|
||||||
|
prod = 1
|
||||||
|
for j in range(base_k):
|
||||||
|
prod *= row[j]
|
||||||
|
new_row.append(prod)
|
||||||
|
plan.append(new_row)
|
||||||
|
return plan
|
||||||
|
|
||||||
|
def plackett_burman(k):
|
||||||
|
"""
|
||||||
|
План Плакетта-Бермана для k факторов.
|
||||||
|
Число опытов N – ближайшее кратное 4, >= k+1.
|
||||||
|
Для N = 8, 16, 32 используется рекурсивное построение матрицы Адамара.
|
||||||
|
"""
|
||||||
|
if k < 2:
|
||||||
|
raise ValueError("Требуется минимум 2 фактора.")
|
||||||
|
N = 4 * ((k + 3) // 4)
|
||||||
|
|
||||||
|
def hadamard_rec(n):
|
||||||
|
if n == 1:
|
||||||
|
return np.array([[1]])
|
||||||
|
H = hadamard_rec(n//2)
|
||||||
|
top = np.hstack([H, H])
|
||||||
|
bottom = np.hstack([H, -H])
|
||||||
|
return np.vstack([top, bottom])
|
||||||
|
|
||||||
|
if (N & (N - 1)) == 0: # N — степень двойки
|
||||||
|
H = hadamard_rec(N)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from scipy.linalg import hadamard
|
||||||
|
H = hadamard(N)
|
||||||
|
except ImportError:
|
||||||
|
print("Предупреждение: scipy не установлена. Генерируем случайный план.")
|
||||||
|
plan = []
|
||||||
|
for _ in range(N):
|
||||||
|
row = [random.choice([-1, 1]) for _ in range(k)]
|
||||||
|
plan.append(row)
|
||||||
|
return plan
|
||||||
|
plan = H[:, 1:k+1].tolist()
|
||||||
|
return plan
|
||||||
|
|
||||||
|
def box_behnken(k):
|
||||||
|
"""
|
||||||
|
План Бокса-Бенкена для k факторов (k >= 3).
|
||||||
|
Содержит комбинации на серединах рёбер и центральные точки.
|
||||||
|
"""
|
||||||
|
if k < 3:
|
||||||
|
raise ValueError("Требуется минимум 3 фактора.")
|
||||||
|
plan = []
|
||||||
|
for i in range(k):
|
||||||
|
for j in range(i+1, k):
|
||||||
|
for a, b in [(1,1), (1,-1), (-1,1), (-1,-1)]:
|
||||||
|
row = [0.0] * k
|
||||||
|
row[i] = a
|
||||||
|
row[j] = b
|
||||||
|
plan.append(row)
|
||||||
|
for _ in range(3):
|
||||||
|
plan.append([0.0] * k)
|
||||||
|
return plan
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# core/experiment.py
|
||||||
|
"""
|
||||||
|
Класс ExperimentDesign для хранения шагов и матрицы плана,
|
||||||
|
а также генерации экспериментальных смесей.
|
||||||
|
"""
|
||||||
|
from core.classes import Mixture, Ingredient, IngredientInfo
|
||||||
|
|
||||||
|
class ExperimentDesign:
|
||||||
|
def __init__(self, steps=None, plan=None):
|
||||||
|
self.steps = steps if steps is not None else []
|
||||||
|
self.plan = plan if plan is not None else []
|
||||||
|
|
||||||
|
def set_steps(self, steps):
|
||||||
|
"""
|
||||||
|
Устанавливает шаги для всех факторов (ингредиентов кроме растворителя).
|
||||||
|
Каждый шаг должен быть >= 0. Нулевые шаги означают, что данный ингредиент не является фактором.
|
||||||
|
"""
|
||||||
|
for s in steps:
|
||||||
|
if s < 0:
|
||||||
|
raise ValueError("Step cannot be negative")
|
||||||
|
self.steps = list(steps)
|
||||||
|
|
||||||
|
def _active_indices(self):
|
||||||
|
"""Возвращает список индексов (в self.steps) с шагом > 0."""
|
||||||
|
return [i for i, s in enumerate(self.steps) if s > 0]
|
||||||
|
|
||||||
|
def _active_steps(self):
|
||||||
|
"""Возвращает список шагов > 0."""
|
||||||
|
return [s for s in self.steps if s > 0]
|
||||||
|
|
||||||
|
def generate_plan(self, k=None):
|
||||||
|
"""
|
||||||
|
Генерирует полный факторный план 2^k, где k - число активных факторов (шагов > 0).
|
||||||
|
Если k не указано, вычисляется автоматически.
|
||||||
|
Если активных факторов < 2, выбрасывает ValueError.
|
||||||
|
"""
|
||||||
|
active = self._active_steps()
|
||||||
|
if k is None:
|
||||||
|
k = len(active)
|
||||||
|
if k < 2:
|
||||||
|
raise ValueError(f"Need at least 2 active factors (steps > 0), but got {k}")
|
||||||
|
from core.doe import ffe
|
||||||
|
self.plan = ffe(k)
|
||||||
|
|
||||||
|
def get_experiment_mixture(self, mixture, exp_index):
|
||||||
|
"""
|
||||||
|
Возвращает Mixture для опыта с индексом exp_index (0-based).
|
||||||
|
Изменяются только те ингредиенты, у которых шаг > 0.
|
||||||
|
"""
|
||||||
|
if not self.plan or not self.steps:
|
||||||
|
raise ValueError("Plan or steps not set")
|
||||||
|
if exp_index >= len(self.plan):
|
||||||
|
raise IndexError("Experiment index out of range")
|
||||||
|
|
||||||
|
active_indices = self._active_indices()
|
||||||
|
active_steps = self._active_steps()
|
||||||
|
if len(active_indices) != len(self.plan[0]):
|
||||||
|
raise ValueError("Number of active steps does not match plan columns")
|
||||||
|
|
||||||
|
base_percents = [info.value for info in mixture.ings]
|
||||||
|
new_percents = base_percents.copy()
|
||||||
|
|
||||||
|
for col, idx_in_steps in enumerate(active_indices):
|
||||||
|
factor = self.plan[exp_index][col]
|
||||||
|
ingredient_idx = idx_in_steps + 1
|
||||||
|
delta = active_steps[col] * factor
|
||||||
|
new_percents[ingredient_idx] = base_percents[ingredient_idx] + delta
|
||||||
|
|
||||||
|
new_ings = []
|
||||||
|
for i, info in enumerate(mixture.ings):
|
||||||
|
new_ings.append(
|
||||||
|
IngredientInfo(
|
||||||
|
ingredient=Ingredient(name=info.name, unit=info.unit, value=new_percents[i]),
|
||||||
|
dilution=info.dilution,
|
||||||
|
density=info.density
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
temp_mixture = Mixture(
|
||||||
|
solvent=new_ings[0],
|
||||||
|
ings=new_ings[1:],
|
||||||
|
total_amount=mixture.total_amount,
|
||||||
|
total_unit=mixture.total_unit
|
||||||
|
)
|
||||||
|
return temp_mixture
|
||||||
|
|
||||||
|
def get_all_mixtures(self, mixture):
|
||||||
|
"""Возвращает список Mixture для всех опытов."""
|
||||||
|
return [self.get_experiment_mixture(mixture, i) for i in range(len(self.plan))]
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"steps": self.steps,
|
||||||
|
"plan": self.plan
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data):
|
||||||
|
return cls(steps=data.get("steps", []), plan=data.get("plan", []))
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
# core/fs.py
|
||||||
|
"""
|
||||||
|
Модуль для работы с файлами проекта (сохранение/загрузка в JSON).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from core.classes import Mixture
|
||||||
|
from core.experiment import ExperimentDesign
|
||||||
|
|
||||||
|
PROJECT_FILENAME = "project.json"
|
||||||
|
|
||||||
|
def load_project(filename: str = PROJECT_FILENAME):
|
||||||
|
"""
|
||||||
|
Загружает проект из JSON-файла.
|
||||||
|
Возвращает кортеж (mixture, design, responses).
|
||||||
|
Если файл отсутствует или повреждён, возвращает новые объекты по умолчанию.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(filename, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
mixture = Mixture.from_dict(data.get("mixture", {})) if "mixture" in data else Mixture()
|
||||||
|
design = ExperimentDesign.from_dict(data.get("design", {})) if "design" in data else ExperimentDesign()
|
||||||
|
responses = data.get("responses", [])
|
||||||
|
if isinstance(responses, list) and responses and isinstance(responses[0], (int, float)):
|
||||||
|
responses = [{"name": "Отклик 1", "values": responses}]
|
||||||
|
return mixture, design, responses
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError, KeyError):
|
||||||
|
return Mixture(), ExperimentDesign(), []
|
||||||
|
|
||||||
|
def save_project_partial(filename: str = PROJECT_FILENAME, mixture=None, design=None, responses=None):
|
||||||
|
"""
|
||||||
|
Сохраняет только переданные части проекта, остальные загружаются из существующего файла.
|
||||||
|
Используется для точечного сохранения (например, только смеси).
|
||||||
|
"""
|
||||||
|
cur_mixture, cur_design, cur_responses = load_project(filename)
|
||||||
|
if mixture is not None:
|
||||||
|
cur_mixture = mixture
|
||||||
|
if design is not None:
|
||||||
|
cur_design = design
|
||||||
|
if responses is not None:
|
||||||
|
cur_responses = responses
|
||||||
|
data = {
|
||||||
|
"mixture": cur_mixture.to_dict(),
|
||||||
|
"design": cur_design.to_dict(),
|
||||||
|
"responses": cur_responses
|
||||||
|
}
|
||||||
|
with open(filename, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# core/statistics.py
|
||||||
|
"""
|
||||||
|
Статистические функции для регрессии, проверки значимости, дисперсии и градиента.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
try:
|
||||||
|
from scipy import stats
|
||||||
|
SCIPY_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
SCIPY_AVAILABLE = False
|
||||||
|
|
||||||
|
def compute_regression(X, y):
|
||||||
|
"""МНК-регрессия, возвращает коэффициенты, ошибки, t-статистики."""
|
||||||
|
N, p = X.shape
|
||||||
|
try:
|
||||||
|
XTX = X.T @ X
|
||||||
|
XTX_inv = np.linalg.inv(XTX)
|
||||||
|
beta = XTX_inv @ (X.T @ y)
|
||||||
|
y_pred = X @ beta
|
||||||
|
residuals = y - y_pred
|
||||||
|
df_res = N - p
|
||||||
|
if df_res > 0:
|
||||||
|
s2_res = np.sum(residuals**2) / df_res
|
||||||
|
se = np.sqrt(np.diag(XTX_inv) * s2_res)
|
||||||
|
else:
|
||||||
|
s2_res = np.nan
|
||||||
|
se = [np.nan] * p
|
||||||
|
t_stats = beta / se if not np.isnan(se).any() else [np.nan] * p
|
||||||
|
except np.linalg.LinAlgError:
|
||||||
|
beta = [np.nan] * p
|
||||||
|
se = [np.nan] * p
|
||||||
|
t_stats = [np.nan] * p
|
||||||
|
df_res = 0
|
||||||
|
s2_res = np.nan
|
||||||
|
residuals = np.full(N, np.nan)
|
||||||
|
y_pred = np.full(N, np.nan)
|
||||||
|
XTX_inv = None
|
||||||
|
return {
|
||||||
|
"beta": beta,
|
||||||
|
"se": se,
|
||||||
|
"t_stats": t_stats,
|
||||||
|
"df_res": df_res,
|
||||||
|
"s2_res": s2_res,
|
||||||
|
"residuals": residuals,
|
||||||
|
"y_pred": y_pred,
|
||||||
|
"XTX_inv": XTX_inv
|
||||||
|
}
|
||||||
|
|
||||||
|
def regression_summary(y, y_pred, k, df_res):
|
||||||
|
"""Вычисляет R², скорректированный R², F-статистику и p-value."""
|
||||||
|
n = len(y)
|
||||||
|
ss_total = np.sum((y - np.mean(y))**2)
|
||||||
|
ss_res = np.sum((y - y_pred)**2)
|
||||||
|
r2 = 1 - ss_res / ss_total if ss_total > 0 else np.nan
|
||||||
|
if df_res > 0:
|
||||||
|
r2_adj = 1 - (1 - r2) * (n - 1) / df_res
|
||||||
|
else:
|
||||||
|
r2_adj = np.nan
|
||||||
|
if df_res > 0 and k > 0:
|
||||||
|
f_stat = (r2 / k) / ((1 - r2) / df_res) if r2 < 1 else np.inf
|
||||||
|
else:
|
||||||
|
f_stat = np.nan
|
||||||
|
if SCIPY_AVAILABLE and not np.isnan(f_stat) and f_stat != np.inf:
|
||||||
|
p_value = 1 - stats.f.cdf(f_stat, k, df_res)
|
||||||
|
else:
|
||||||
|
p_value = np.nan
|
||||||
|
return {"r2": r2, "r2_adj": r2_adj, "f_stat": f_stat, "f_pvalue": p_value}
|
||||||
|
|
||||||
|
def get_center_indices(plan, active_indices):
|
||||||
|
"""Возвращает индексы строк плана, где активные факторы равны 0."""
|
||||||
|
if not plan:
|
||||||
|
return []
|
||||||
|
plan_arr = np.array(plan)
|
||||||
|
if active_indices:
|
||||||
|
active_cols = plan_arr[:, active_indices]
|
||||||
|
center_mask = np.all(active_cols == 0, axis=1)
|
||||||
|
return np.where(center_mask)[0].tolist()
|
||||||
|
return []
|
||||||
|
|
||||||
|
def compute_center_stats(y_center):
|
||||||
|
"""Среднее и несмещённая дисперсия для центральных точек."""
|
||||||
|
if len(y_center) < 2:
|
||||||
|
return None, None
|
||||||
|
return np.mean(y_center), np.var(y_center, ddof=1)
|
||||||
|
|
||||||
|
def chi2_confidence_interval(var, n, alpha=0.05):
|
||||||
|
"""Доверительный интервал для дисперсии на основе χ²-распределения."""
|
||||||
|
if not SCIPY_AVAILABLE or n < 2:
|
||||||
|
return np.nan, np.nan
|
||||||
|
df = n - 1
|
||||||
|
chi2_lower = stats.chi2.ppf(alpha / 2, df)
|
||||||
|
chi2_upper = stats.chi2.ppf(1 - alpha / 2, df)
|
||||||
|
return df * var / chi2_upper, df * var / chi2_lower
|
||||||
|
|
||||||
|
def get_t_critical(df, alpha=0.05):
|
||||||
|
"""Критическое значение t (двустороннее)."""
|
||||||
|
if not SCIPY_AVAILABLE or df <= 0:
|
||||||
|
return np.nan
|
||||||
|
return stats.t.ppf(1 - alpha/2, df)
|
||||||
|
|
||||||
|
def compute_gradient_points(beta, base_levels, step_factor, num_steps, factor_names=None, maximize=True):
|
||||||
|
"""
|
||||||
|
Генерирует точки вдоль градиента (крутое восхождение или спуск).
|
||||||
|
beta – коэффициенты модели: [b0, b1, b2, ...]
|
||||||
|
base_levels – список базовых уровней для каждого фактора
|
||||||
|
step_factor – шаг по первому фактору
|
||||||
|
num_steps – количество шагов
|
||||||
|
factor_names – список названий факторов
|
||||||
|
maximize – True для максимизации, False для минимизации
|
||||||
|
"""
|
||||||
|
if len(beta) - 1 != len(base_levels):
|
||||||
|
raise ValueError("Число коэффициентов (без b0) должно совпадать с числом факторов")
|
||||||
|
direction = 1 if maximize else -1
|
||||||
|
coefs = np.array(beta[1:]) * direction
|
||||||
|
max_abs = np.max(np.abs(coefs))
|
||||||
|
if max_abs == 0:
|
||||||
|
raise ValueError("Все коэффициенты равны нулю, направление не определено.")
|
||||||
|
norm_coefs = coefs / max_abs
|
||||||
|
steps = step_factor * norm_coefs
|
||||||
|
|
||||||
|
points = []
|
||||||
|
for step_num in range(1, num_steps + 1):
|
||||||
|
point = {}
|
||||||
|
for i, name in enumerate(factor_names or [f"x{i+1}" for i in range(len(base_levels))]):
|
||||||
|
point[name] = base_levels[i] + steps[i] * step_num
|
||||||
|
predicted = beta[0] + np.sum(beta[1:] * np.array([point[name] for name in factor_names or range(len(base_levels))]))
|
||||||
|
point["Шаг"] = step_num
|
||||||
|
point["Предсказанный отклик"] = predicted
|
||||||
|
points.append(point)
|
||||||
|
return points
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# core/table_io.py
|
||||||
|
"""
|
||||||
|
Модуль для работы с таблицами: вывод, сохранение в CSV, копирование в буфер обмена.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import csv
|
||||||
|
from table import print_table
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyperclip
|
||||||
|
HAS_PYPERCLIP = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PYPERCLIP = False
|
||||||
|
|
||||||
|
def handle_table_output(headers, rows, title="Таблица", ask_each_time=True):
|
||||||
|
"""
|
||||||
|
Выводит таблицу, затем предлагает сохранить в CSV или скопировать в буфер обмена.
|
||||||
|
headers – список заголовков
|
||||||
|
rows – список строк
|
||||||
|
title – заголовок
|
||||||
|
ask_each_time – если False, только выводит таблицу без дополнительных вопросов.
|
||||||
|
"""
|
||||||
|
if title:
|
||||||
|
print(f"\n=== {title} ===")
|
||||||
|
print_table(headers, rows)
|
||||||
|
|
||||||
|
if not ask_each_time:
|
||||||
|
return
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\nДействия с таблицей:")
|
||||||
|
print(" 1. Сохранить в CSV")
|
||||||
|
if HAS_PYPERCLIP:
|
||||||
|
print(" 2. Скопировать в буфер обмена")
|
||||||
|
else:
|
||||||
|
print(" 2. Скопировать в буфер обмена (недоступно, установите pyperclip)")
|
||||||
|
print(" Enter - пропустить")
|
||||||
|
choice = input("Выберите действие: ").strip()
|
||||||
|
|
||||||
|
if choice == "":
|
||||||
|
break
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
save_to_csv(headers, rows)
|
||||||
|
break
|
||||||
|
elif choice == "2":
|
||||||
|
if HAS_PYPERCLIP:
|
||||||
|
copy_to_clipboard(headers, rows)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("pyperclip не установлен. Установите командой: pip install pyperclip")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("Неверный выбор. Введите 1, 2 или Enter.")
|
||||||
|
|
||||||
|
def save_to_csv(headers, rows, default_filename="table.csv"):
|
||||||
|
"""Сохраняет таблицу в CSV-файл."""
|
||||||
|
filename = input(f"Имя файла (по умолчанию {default_filename}): ").strip()
|
||||||
|
if not filename:
|
||||||
|
filename = default_filename
|
||||||
|
if not filename.lower().endswith('.csv'):
|
||||||
|
filename += '.csv'
|
||||||
|
try:
|
||||||
|
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
|
||||||
|
writer = csv.writer(f, delimiter=';')
|
||||||
|
writer.writerow(headers)
|
||||||
|
writer.writerows(rows)
|
||||||
|
print(f"Таблица сохранена в {os.path.abspath(filename)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при сохранении: {e}")
|
||||||
|
|
||||||
|
def copy_to_clipboard(headers, rows):
|
||||||
|
"""Копирует таблицу в буфер обмена в формате TSV."""
|
||||||
|
if not HAS_PYPERCLIP:
|
||||||
|
print("pyperclip не установлен. Установите командой: pip install pyperclip")
|
||||||
|
return
|
||||||
|
lines = []
|
||||||
|
lines.append("\t".join(str(h) for h in headers))
|
||||||
|
for row in rows:
|
||||||
|
lines.append("\t".join(str(cell) for cell in row))
|
||||||
|
text = "\n".join(lines)
|
||||||
|
try:
|
||||||
|
pyperclip.copy(text)
|
||||||
|
print("Таблица скопирована в буфер обмена (TSV-формат).")
|
||||||
|
print("Вставьте её в Excel или другой редактор.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка копирования в буфер обмена: {e}")
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# editor.py
|
||||||
|
"""
|
||||||
|
Редактор состава смеси.
|
||||||
|
Позволяет добавлять, изменять ингредиенты и сохранять состав в проект.
|
||||||
|
"""
|
||||||
|
from core.classes import Ingredient
|
||||||
|
from core.fs import save_project_partial
|
||||||
|
from core.table_io import handle_table_output
|
||||||
|
|
||||||
|
def safe_float(prompt, default=None):
|
||||||
|
"""Безопасный ввод числа с возможностью значения по умолчанию."""
|
||||||
|
while True:
|
||||||
|
val = input(prompt)
|
||||||
|
if val.strip() == "" and default is not None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return float(val)
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка! Введите число.")
|
||||||
|
|
||||||
|
def display_ingredients(mixture):
|
||||||
|
"""Выводит список ингредиентов в виде таблицы."""
|
||||||
|
if not mixture.ings:
|
||||||
|
print("Список ингредиентов пуст.")
|
||||||
|
return
|
||||||
|
headers = ["№", "Название", "%", "Разбавление", "Плотность"]
|
||||||
|
rows = []
|
||||||
|
for i, info in enumerate(mixture.ings, start=1):
|
||||||
|
rows.append([
|
||||||
|
str(i),
|
||||||
|
info.name + (" (растворитель)" if i == 1 else ""),
|
||||||
|
f"{info.value:.1f}",
|
||||||
|
f"{info.dilution:.2f}",
|
||||||
|
f"{info.density:.2f}"
|
||||||
|
])
|
||||||
|
handle_table_output(headers, rows, title="Список ингредиентов")
|
||||||
|
|
||||||
|
def editor_menu(mixture):
|
||||||
|
"""
|
||||||
|
Подменю редактора состава.
|
||||||
|
Изменяет объект mixture напрямую.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("РЕДАКТОР СОСТАВА")
|
||||||
|
print("1. Добавить ингредиент")
|
||||||
|
print("2. Показать все ингредиенты")
|
||||||
|
print("3. Изменить количество ингредиента (%)")
|
||||||
|
print("4. Изменить разбавление и плотность")
|
||||||
|
print("5. Сохранить состав в проект")
|
||||||
|
print("(Enter - выход в главное меню)")
|
||||||
|
print("=" * 50)
|
||||||
|
choice = input("Выберите пункт: ").strip()
|
||||||
|
|
||||||
|
if choice == "":
|
||||||
|
break
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
name = input("Название: ")
|
||||||
|
unit = input("Единица: ")
|
||||||
|
value = safe_float("Количество (%): ")
|
||||||
|
if value < 0:
|
||||||
|
print("Процент не может быть отрицательным!")
|
||||||
|
continue
|
||||||
|
dilution = safe_float("Разбавление (по умолчанию 1): ", default=1.0)
|
||||||
|
density = safe_float("Плотность (по умолчанию 1): ", default=1.0)
|
||||||
|
ing = Ingredient(name, unit, value)
|
||||||
|
mixture.add_ingredient(ing, dilution, density)
|
||||||
|
print(f"Добавлено: {ing.name} = {ing.value:.1f}%")
|
||||||
|
|
||||||
|
elif choice == "2":
|
||||||
|
display_ingredients(mixture)
|
||||||
|
|
||||||
|
elif choice == "3":
|
||||||
|
if len(mixture.ings) <= 1:
|
||||||
|
print("Нельзя изменить единственный ингредиент (растворитель) напрямую.")
|
||||||
|
continue
|
||||||
|
display_ingredients(mixture)
|
||||||
|
try:
|
||||||
|
idx = int(input("Номер ингредиента: ")) - 1
|
||||||
|
if idx == 0:
|
||||||
|
print("Нельзя менять растворитель напрямую.")
|
||||||
|
continue
|
||||||
|
if idx < 0 or idx >= len(mixture.ings):
|
||||||
|
print("Неверный номер!")
|
||||||
|
continue
|
||||||
|
new_val = safe_float(f"Новое количество для {mixture.ings[idx].name} (%): ")
|
||||||
|
if new_val < 0:
|
||||||
|
print("Процент не может быть отрицательным!")
|
||||||
|
continue
|
||||||
|
mixture.ings[idx].value = new_val
|
||||||
|
mixture.comp_solvent_percent()
|
||||||
|
print(f"Обновлено! Растворитель теперь: {mixture.ings[0].value:.1f}%")
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка!")
|
||||||
|
|
||||||
|
elif choice == "4":
|
||||||
|
if not mixture.ings:
|
||||||
|
print("Список пуст.")
|
||||||
|
continue
|
||||||
|
display_ingredients(mixture)
|
||||||
|
try:
|
||||||
|
idx = int(input("Номер ингредиента: ")) - 1
|
||||||
|
if idx < 0 or idx >= len(mixture.ings):
|
||||||
|
print("Неверный номер!")
|
||||||
|
continue
|
||||||
|
info = mixture.ings[idx]
|
||||||
|
new_dil = safe_float(f"Новое разбавление для {info.name} (было {info.dilution:.2f}): ", default=info.dilution)
|
||||||
|
new_den = safe_float(f"Новая плотность для {info.name} (было {info.density:.2f}): ", default=info.density)
|
||||||
|
info.dilution = new_dil
|
||||||
|
info.density = new_den
|
||||||
|
print("Параметры обновлены!")
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка!")
|
||||||
|
|
||||||
|
elif choice == "5":
|
||||||
|
save_project_partial(mixture=mixture)
|
||||||
|
print("Состав сохранён в проект.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Неверный пункт.")
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# main.py
|
||||||
|
"""
|
||||||
|
Главное меню программы.
|
||||||
|
Загружает проект при старте и предоставляет доступ ко всем подменю.
|
||||||
|
"""
|
||||||
|
from core.fs import load_project
|
||||||
|
from editor import editor_menu
|
||||||
|
from plan import plan_menu
|
||||||
|
from response import response_menu
|
||||||
|
from analysis import analysis_menu
|
||||||
|
from ascent import ascent_menu
|
||||||
|
|
||||||
|
PROJECT_FILE = "project.json"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
mixture, design, responses = load_project(PROJECT_FILE)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("ГЛАВНОЕ МЕНЮ")
|
||||||
|
print("1. Редактор состава")
|
||||||
|
print("2. Планирование эксперимента")
|
||||||
|
print("3. Ввод значений отклика")
|
||||||
|
print("4. Анализ значений")
|
||||||
|
print("5. Крутое восхождение")
|
||||||
|
print("6. Выход")
|
||||||
|
print("=" * 50)
|
||||||
|
choice = input("Выберите пункт: ").strip()
|
||||||
|
|
||||||
|
if choice == "6":
|
||||||
|
print("До свидания! (данные не сохранены автоматически)")
|
||||||
|
break
|
||||||
|
elif choice == "1":
|
||||||
|
editor_menu(mixture)
|
||||||
|
elif choice == "2":
|
||||||
|
design = plan_menu(mixture, design)
|
||||||
|
elif choice == "3":
|
||||||
|
responses = response_menu(design, responses)
|
||||||
|
elif choice == "4":
|
||||||
|
analysis_menu(mixture, design, responses)
|
||||||
|
elif choice == "5":
|
||||||
|
ascent_menu(mixture, design, responses)
|
||||||
|
else:
|
||||||
|
print("Неверный пункт.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
# plan.py
|
||||||
|
"""
|
||||||
|
Планирование эксперимента.
|
||||||
|
Позволяет задать шаги для факторов, сгенерировать план различных типов,
|
||||||
|
показать матрицу плана, экспериментальные смеси и сохранить дизайн в проект.
|
||||||
|
"""
|
||||||
|
from core.experiment import ExperimentDesign
|
||||||
|
from core.fs import save_project_partial
|
||||||
|
from core.doe import ze, ccp, fractional_factorial, plackett_burman, box_behnken
|
||||||
|
from core.table_io import handle_table_output
|
||||||
|
|
||||||
|
def plan_menu(mixture, design):
|
||||||
|
if design is None:
|
||||||
|
design = ExperimentDesign()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("ПЛАНИРОВАНИЕ ЭКСПЕРИМЕНТА")
|
||||||
|
print("1. Задать шаги для факторов (ингредиентов, кроме растворителя)")
|
||||||
|
print("2. Сгенерировать план (выбор типа)")
|
||||||
|
print("3. Показать матрицу плана")
|
||||||
|
print("4. Показать экспериментальные смеси (абсолютные значения)")
|
||||||
|
print("5. Сохранить дизайн в проект")
|
||||||
|
print("(Enter - выход в главное меню)")
|
||||||
|
print("=" * 50)
|
||||||
|
choice = input("Выберите пункт: ").strip()
|
||||||
|
|
||||||
|
if choice == "":
|
||||||
|
break
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
num_factors = len(mixture.ings) - 1
|
||||||
|
if num_factors <= 0:
|
||||||
|
print("В смеси нет изменяемых ингредиентов (только растворитель).")
|
||||||
|
continue
|
||||||
|
print(f"Введите шаги для {num_factors} факторов (в %). Введите 0, если фактор не используется:")
|
||||||
|
steps = []
|
||||||
|
for i in range(1, len(mixture.ings)):
|
||||||
|
name = mixture.ings[i].name
|
||||||
|
while True:
|
||||||
|
val = input(f"Шаг для {name} (текущий % = {mixture.ings[i].value:.1f}): ")
|
||||||
|
try:
|
||||||
|
s = float(val)
|
||||||
|
if s < 0:
|
||||||
|
print("Шаг не может быть отрицательным. Введите 0 или положительное число.")
|
||||||
|
continue
|
||||||
|
steps.append(s)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
print("Введите число.")
|
||||||
|
design.set_steps(steps)
|
||||||
|
active = design._active_steps()
|
||||||
|
print(f"Шаги установлены. Активных факторов (шаг > 0): {len(active)}")
|
||||||
|
if len(active) < 2:
|
||||||
|
print("Предупреждение: для генерации плана необходимо минимум 2 активных фактора.")
|
||||||
|
|
||||||
|
elif choice == "2":
|
||||||
|
active_steps = design._active_steps()
|
||||||
|
if len(active_steps) < 2:
|
||||||
|
print(f"Недостаточно активных факторов (шаг > 0): {len(active_steps)}. Требуется минимум 2.")
|
||||||
|
continue
|
||||||
|
k = len(active_steps)
|
||||||
|
|
||||||
|
print("\nВыберите тип плана:")
|
||||||
|
print(" 1. Полный факторный план 2^k")
|
||||||
|
print(" 2. Центральный композиционный план (ЦКП)")
|
||||||
|
print(" 3. Дробный факторный план 2^(k-p)")
|
||||||
|
print(" 4. План Плакетта-Бермана")
|
||||||
|
print(" 5. План Бокса-Бенкена")
|
||||||
|
plan_type = input("Ваш выбор (по умолчанию 1): ").strip()
|
||||||
|
if plan_type == "":
|
||||||
|
plan_type = "1"
|
||||||
|
|
||||||
|
if plan_type == "1":
|
||||||
|
print(f"Активных факторов: {k}. Будет сгенерирован план 2^{k} = {2**k} опытов.")
|
||||||
|
try:
|
||||||
|
design.generate_plan()
|
||||||
|
print(f"Основной план сгенерирован: {len(design.plan)} опытов.")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Ошибка: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
num_center = input("Введите количество дополнительных центральных точек (0, если не нужно): ").strip()
|
||||||
|
if num_center == "":
|
||||||
|
num_center = "0"
|
||||||
|
num_center = int(num_center)
|
||||||
|
if num_center < 0:
|
||||||
|
print("Количество не может быть отрицательным.")
|
||||||
|
elif num_center > 0:
|
||||||
|
center_rows = ze(k, num_center)
|
||||||
|
if center_rows:
|
||||||
|
design.plan.extend(center_rows)
|
||||||
|
print(f"Добавлено {num_center} центральных точек. Всего опытов: {len(design.plan)}")
|
||||||
|
else:
|
||||||
|
print("Ошибка генерации центральных точек.")
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка: введите целое число.")
|
||||||
|
|
||||||
|
elif plan_type == "2":
|
||||||
|
print("Центральный композиционный план (ЦКП)")
|
||||||
|
try:
|
||||||
|
alpha_input = input("Введите значение альфа (звёздное плечо, по умолчанию 1.0): ").strip()
|
||||||
|
alpha = float(alpha_input) if alpha_input else 1.0
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка ввода, используется alpha=1.0")
|
||||||
|
alpha = 1.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
center_input = input("Введите количество центральных точек (по умолчанию 1): ").strip()
|
||||||
|
center_points = int(center_input) if center_input else 1
|
||||||
|
if center_points < 0:
|
||||||
|
print("Количество не может быть отрицательным, установлено 1.")
|
||||||
|
center_points = 1
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка ввода, используется 1 центральная точка.")
|
||||||
|
center_points = 1
|
||||||
|
|
||||||
|
use_fractional = input("Использовать дробное ядро? (y/n, по умолчанию n): ").strip().lower()
|
||||||
|
fractional = None
|
||||||
|
if use_fractional == 'y':
|
||||||
|
try:
|
||||||
|
p = int(input("Введите степень дробления p (1 или больше): ").strip())
|
||||||
|
if 0 < p < k:
|
||||||
|
if k - p < 2:
|
||||||
|
print(f"Ошибка: при k={k} и p={p} ядро будет содержать {k-p} факторов, что меньше 2. Используйте p <= {k-2}.")
|
||||||
|
continue
|
||||||
|
fractional = p
|
||||||
|
print(f"Будет использовано дробное ядро 2^{k-p}.")
|
||||||
|
else:
|
||||||
|
print("Некорректное p, используем полное ядро.")
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка ввода, используем полное ядро.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
design.plan = ccp(k, alpha, center_points, fractional)
|
||||||
|
if fractional:
|
||||||
|
print(f"ЦКП с дробным ядром 2^{k-fractional} сгенерирован. Опытов: {len(design.plan)}")
|
||||||
|
else:
|
||||||
|
print(f"ЦКП с полным ядром сгенерирован. Опытов: {len(design.plan)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка генерации ЦКП: {e}")
|
||||||
|
|
||||||
|
elif plan_type == "3":
|
||||||
|
print("Дробный факторный план 2^(k-p)")
|
||||||
|
try:
|
||||||
|
p_input = input("Введите степень дробления p (по умолчанию 1): ").strip()
|
||||||
|
p = int(p_input) if p_input else 1
|
||||||
|
if p <= 0 or p >= k:
|
||||||
|
print("p должно быть от 1 до k-1. Используем p=1.")
|
||||||
|
p = 1
|
||||||
|
if k - p < 2:
|
||||||
|
print(f"Ошибка: при k={k} и p={p} ядро будет содержать {k-p} факторов, что меньше 2. Используйте p <= {k-2}.")
|
||||||
|
continue
|
||||||
|
design.plan = fractional_factorial(k, p)
|
||||||
|
print(f"Дробный факторный план 2^{k-p} сгенерирован. Опытов: {len(design.plan)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка: {e}")
|
||||||
|
|
||||||
|
elif plan_type == "4":
|
||||||
|
print("План Плакетта-Бермана")
|
||||||
|
try:
|
||||||
|
design.plan = plackett_burman(k)
|
||||||
|
print(f"План Плакетта-Бермана сгенерирован. Опытов: {len(design.plan)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка: {e}")
|
||||||
|
|
||||||
|
elif plan_type == "5":
|
||||||
|
print("План Бокса-Бенкена")
|
||||||
|
try:
|
||||||
|
if k < 3:
|
||||||
|
print("План Бокса-Бенкена требует минимум 3 фактора.")
|
||||||
|
continue
|
||||||
|
design.plan = box_behnken(k)
|
||||||
|
print(f"План Бокса-Бенкена сгенерирован. Опытов: {len(design.plan)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка: {e}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Неверный тип плана. План не сгенерирован.")
|
||||||
|
|
||||||
|
elif choice == "3":
|
||||||
|
if not design.plan:
|
||||||
|
print("План не сгенерирован.")
|
||||||
|
continue
|
||||||
|
active_indices = design._active_indices()
|
||||||
|
headers = ["Опыт"] + [mixture.ings[idx+1].name for idx in active_indices]
|
||||||
|
rows = []
|
||||||
|
for i, row in enumerate(design.plan, start=1):
|
||||||
|
rows.append([str(i)] + [str(v) for v in row])
|
||||||
|
handle_table_output(headers, rows, title="Матрица плана")
|
||||||
|
|
||||||
|
elif choice == "4":
|
||||||
|
if not design.plan or not design.steps:
|
||||||
|
print("План или шаги не заданы.")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
exp_mixtures = design.get_all_mixtures(mixture)
|
||||||
|
headers = ["Опыт"] + [f"{info.name} ({info.unit})" for info in mixture.ings]
|
||||||
|
rows = []
|
||||||
|
for i, exp_mix in enumerate(exp_mixtures, start=1):
|
||||||
|
abs_ings = exp_mix.get_absolute_ingredients()
|
||||||
|
rows.append([str(i)] + [f"{ing.value:.2f}" for ing in abs_ings])
|
||||||
|
handle_table_output(headers, rows, title="Экспериментальные смеси (абсолютные значения)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка: {e}")
|
||||||
|
|
||||||
|
elif choice == "5":
|
||||||
|
save_project_partial(design=design)
|
||||||
|
print("Дизайн сохранён в проект.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Неверный пункт.")
|
||||||
|
|
||||||
|
return design
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
numpy
|
||||||
|
scipy
|
||||||
|
pyperclip
|
||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
# response.py
|
||||||
|
"""
|
||||||
|
Ввод и управление откликами эксперимента.
|
||||||
|
"""
|
||||||
|
from core.fs import save_project_partial
|
||||||
|
from core.table_io import handle_table_output
|
||||||
|
|
||||||
|
def response_menu(design, responses):
|
||||||
|
if not design.plan:
|
||||||
|
print("План эксперимента пуст. Сначала создайте или загрузите дизайн.")
|
||||||
|
input("Нажмите Enter для продолжения...")
|
||||||
|
return responses
|
||||||
|
|
||||||
|
num_experiments = len(design.plan)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("ВВОД ЗНАЧЕНИЙ ОТКЛИКА")
|
||||||
|
print(f"Количество опытов: {num_experiments}")
|
||||||
|
print("1. Показать все отклики")
|
||||||
|
print("2. Добавить новый отклик")
|
||||||
|
print("3. Редактировать отклик")
|
||||||
|
print("4. Переименовать отклик")
|
||||||
|
print("5. Удалить отклик")
|
||||||
|
print("6. Сохранить отклики в проект")
|
||||||
|
print("(Enter - выход в главное меню)")
|
||||||
|
print("=" * 50)
|
||||||
|
choice = input("Выберите пункт: ").strip()
|
||||||
|
|
||||||
|
if choice == "":
|
||||||
|
break
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
if not responses:
|
||||||
|
print("Отклики не введены.")
|
||||||
|
continue
|
||||||
|
headers = ["Опыт"] + [resp["name"] for resp in responses]
|
||||||
|
rows = []
|
||||||
|
for i in range(num_experiments):
|
||||||
|
row = [str(i + 1)]
|
||||||
|
for resp in responses:
|
||||||
|
values = resp["values"]
|
||||||
|
if i < len(values):
|
||||||
|
row.append(f"{values[i]:.3f}")
|
||||||
|
else:
|
||||||
|
row.append("—")
|
||||||
|
rows.append(row)
|
||||||
|
handle_table_output(headers, rows, title="Таблица откликов")
|
||||||
|
|
||||||
|
elif choice == "2":
|
||||||
|
name = input("Введите название нового отклика: ").strip()
|
||||||
|
if not name:
|
||||||
|
print("Название не может быть пустым.")
|
||||||
|
continue
|
||||||
|
if any(r["name"].lower() == name.lower() for r in responses):
|
||||||
|
print("Отклик с таким названием уже существует.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"Введите {num_experiments} значений для отклика '{name}':")
|
||||||
|
values = []
|
||||||
|
for i in range(num_experiments):
|
||||||
|
while True:
|
||||||
|
val = input(f" Опыт {i+1}: ").strip()
|
||||||
|
try:
|
||||||
|
values.append(float(val))
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
print("Введите число.")
|
||||||
|
responses.append({"name": name, "values": values})
|
||||||
|
print(f"Отклик '{name}' добавлен.")
|
||||||
|
|
||||||
|
elif choice == "3":
|
||||||
|
if not responses:
|
||||||
|
print("Нет откликов для редактирования.")
|
||||||
|
continue
|
||||||
|
print("Список откликов:")
|
||||||
|
for idx, resp in enumerate(responses, start=1):
|
||||||
|
print(f"{idx}. {resp['name']} (значений: {len(resp['values'])})")
|
||||||
|
try:
|
||||||
|
idx = int(input("Выберите номер отклика для редактирования: ")) - 1
|
||||||
|
if idx < 0 or idx >= len(responses):
|
||||||
|
print("Неверный номер.")
|
||||||
|
continue
|
||||||
|
resp = responses[idx]
|
||||||
|
print(f"Редактирование отклика '{resp['name']}'. Введите новые значения.")
|
||||||
|
new_values = []
|
||||||
|
for i in range(num_experiments):
|
||||||
|
while True:
|
||||||
|
val = input(f" Опыт {i+1} (было {resp['values'][i] if i < len(resp['values']) else '—'}): ").strip()
|
||||||
|
if val == "":
|
||||||
|
if i < len(resp['values']):
|
||||||
|
new_values.append(resp['values'][i])
|
||||||
|
else:
|
||||||
|
print("Значение обязательно.")
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
new_values.append(float(val))
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
print("Введите число.")
|
||||||
|
resp["values"] = new_values
|
||||||
|
print(f"Отклик '{resp['name']}' обновлён.")
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка ввода.")
|
||||||
|
|
||||||
|
elif choice == "4":
|
||||||
|
if not responses:
|
||||||
|
print("Нет откликов для переименования.")
|
||||||
|
continue
|
||||||
|
print("Список откликов:")
|
||||||
|
for idx, resp in enumerate(responses, start=1):
|
||||||
|
print(f"{idx}. {resp['name']}")
|
||||||
|
try:
|
||||||
|
idx = int(input("Выберите номер отклика для переименования: ")) - 1
|
||||||
|
if idx < 0 or idx >= len(responses):
|
||||||
|
print("Неверный номер.")
|
||||||
|
continue
|
||||||
|
new_name = input(f"Введите новое название для '{responses[idx]['name']}': ").strip()
|
||||||
|
if not new_name:
|
||||||
|
print("Название не может быть пустым.")
|
||||||
|
continue
|
||||||
|
if any(r["name"].lower() == new_name.lower() for r in responses if r != responses[idx]):
|
||||||
|
print("Отклик с таким названием уже существует.")
|
||||||
|
continue
|
||||||
|
old_name = responses[idx]["name"]
|
||||||
|
responses[idx]["name"] = new_name
|
||||||
|
print(f"Отклик '{old_name}' переименован в '{new_name}'.")
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка ввода.")
|
||||||
|
|
||||||
|
elif choice == "5":
|
||||||
|
if not responses:
|
||||||
|
print("Нет откликов для удаления.")
|
||||||
|
continue
|
||||||
|
print("Список откликов:")
|
||||||
|
for idx, resp in enumerate(responses, start=1):
|
||||||
|
print(f"{idx}. {resp['name']} (значений: {len(resp['values'])})")
|
||||||
|
try:
|
||||||
|
idx = int(input("Выберите номер отклика для удаления: ")) - 1
|
||||||
|
if idx < 0 or idx >= len(responses):
|
||||||
|
print("Неверный номер.")
|
||||||
|
continue
|
||||||
|
deleted = responses.pop(idx)
|
||||||
|
print(f"Отклик '{deleted['name']}' удалён.")
|
||||||
|
except ValueError:
|
||||||
|
print("Ошибка ввода.")
|
||||||
|
|
||||||
|
elif choice == "6":
|
||||||
|
save_project_partial(responses=responses)
|
||||||
|
print("Отклики сохранены в проект.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Неверный пункт.")
|
||||||
|
|
||||||
|
return responses
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# table.py
|
||||||
|
"""
|
||||||
|
Модуль для вывода таблиц в консоль с выравниванием.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def print_table(headers, rows):
|
||||||
|
"""
|
||||||
|
Печатает таблицу с выравниванием.
|
||||||
|
headers: список заголовков (строки)
|
||||||
|
rows: список строк, каждая строка — список значений (строки)
|
||||||
|
"""
|
||||||
|
widths = [len(str(h)) for h in headers]
|
||||||
|
for row in rows:
|
||||||
|
for i, cell in enumerate(row):
|
||||||
|
widths[i] = max(widths[i], len(str(cell)))
|
||||||
|
|
||||||
|
sep = "+" + "+".join("-" * (w + 2) for w in widths) + "+"
|
||||||
|
|
||||||
|
def print_row(cells):
|
||||||
|
print("| " + " | ".join(str(c).ljust(widths[i]) for i, c in enumerate(cells)) + " |")
|
||||||
|
|
||||||
|
print(sep)
|
||||||
|
print_row(headers)
|
||||||
|
print(sep)
|
||||||
|
for row in rows:
|
||||||
|
print_row(row)
|
||||||
|
print(sep)
|
||||||
Reference in New Issue
Block a user