feature/case-demo-model-minimal #1

Merged
Hongru merged 3 commits from feature/case-demo-model-minimal into master 2026-04-07 19:39:13 +08:00
16 changed files with 4338 additions and 838 deletions
Showing only changes of commit cf9a1a2654 - Show all commits
+31
View File
@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
Binary file not shown.
Binary file not shown.
+275
View File
@@ -0,0 +1,275 @@
"""
GPR → NN 知识蒸馏脚本
将训练好的高斯过程回归 (GPR) 代理模型的知识蒸馏到轻量级神经网络 (MLP) 中。
支持两种模式:
1. 直接从 CSV 原始数据训练 NN (快速模式, 无需 GPR 依赖)
2. 从 GPR 教师模型蒸馏 (完整模式, 需 botorch/gpytorch)
用法:
python distill_gpr_to_nn.py # 从Model目录运行
python Model/src/distill_gpr_to_nn.py # 从项目根目录运行
也可在代码中调用:
from distill_gpr_to_nn import distill, distill_from_csv
result = distill_from_csv(csv_path, nn_save_path, ...)
result = distill(gpr_csv_path, gpr_pth_path, nn_save_path, ...)
"""
import os
import sys
import csv as csv_mod
import numpy as np
import torch
import torch.nn as nn
# 确保 src 目录在 path 中
_this_dir = os.path.dirname(os.path.abspath(__file__))
if _this_dir not in sys.path:
sys.path.insert(0, _this_dir)
from lightweight_model import EngineNNProxy
# ============================================================
# 工具函数: 手写 StandardScaler (避免 sklearn import 导致的
# numpy/scipy 递归问题)
# ============================================================
class SimpleScaler:
"""轻量 Z-Score 归一化, 兼容 EngineNNProxy.set_normalization_params"""
def __init__(self):
self.mean_ = None
self.scale_ = None
def fit(self, X):
X = np.asarray(X, dtype=np.float64)
self.mean_ = X.mean(axis=0)
self.scale_ = X.std(axis=0)
self.scale_[self.scale_ < 1e-12] = 1.0
return self
def transform(self, X):
return (np.asarray(X, dtype=np.float64) - self.mean_) / self.scale_
def fit_transform(self, X):
self.fit(X)
return self.transform(X)
def inverse_transform(self, X):
return np.asarray(X, dtype=np.float64) * self.scale_ + self.mean_
# ============================================================
# 模式一: 直接从 CSV 训练 (快速, 无需 sklearn/botorch)
# ============================================================
def distill_from_csv(csv_path, nn_save_path,
epochs=3000, lr=1e-3, batch_size=256,
hidden_size=64, verbose=True, progress_callback=None):
"""
直接从 CSV 原始发动机数据训练 NN 代理模型。
Returns
-------
dict with keys: loss_history, nn_model, X_train, Y_train, Y_nn,
scaler_X, scaler_Y, rel_error_fuel, rel_error_power
"""
# 1. 读取 CSV
if verbose:
print(f"-> Loading CSV: {csv_path}")
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv_mod.reader(f)
header = next(reader)
rows = [r for r in reader]
col_idx = {name: i for i, name in enumerate(header)}
data = np.array([[float(x) for x in r] for r in rows], dtype=np.float64)
X_cols = ['Altitude_m', 'Mach', 'RPM']
Y_cols = ['WF_kg_h', 'Power_kW']
X_raw = data[:, [col_idx[c] for c in X_cols]]
Y_raw = data[:, [col_idx[c] for c in Y_cols]]
# 过滤零/非物理值
valid = (Y_raw[:, 0] > 0.5) & (Y_raw[:, 1] > 0.5) & (X_raw[:, 2] > 500)
X_phys = X_raw[valid].astype(np.float32)
Y_phys = Y_raw[valid].astype(np.float32)
if verbose:
print(f"-> Valid samples: {len(X_phys)} / {len(data)}")
# 2. 归一化
scaler_X = SimpleScaler()
scaler_X.fit(X_phys)
Y_log = np.log1p(Y_phys.astype(np.float64))
scaler_Y = SimpleScaler()
scaler_Y.fit(Y_log)
return _train_nn(X_phys, Y_phys, scaler_X, scaler_Y, nn_save_path,
epochs, lr, batch_size, hidden_size, verbose,
progress_callback=progress_callback)
# ============================================================
# 模式二: 从 GPR 教师模型蒸馏 (需 botorch/gpytorch/sklearn)
# ============================================================
def _generate_teacher_data(gpr_csv_path, gpr_pth_path,
n_altitude=25, n_mach=6, n_rpm=30):
"""
加载 GPR 教师模型,在输入空间的密集网格上生成标注数据。
"""
from engine_gpr_class import EngineGPRModel
gpr = EngineGPRModel(csv_path=gpr_csv_path)
success = gpr.load_model(pth_path=gpr_pth_path)
if not success:
raise RuntimeError(f"Failed to load GPR model from {gpr_pth_path}")
df = gpr.df
H_range = np.linspace(df['Altitude_m'].min(), df['Altitude_m'].max(), n_altitude)
Ma_range = np.linspace(df['Mach'].min(), df['Mach'].max(), n_mach)
RPM_range = np.linspace(max(df['RPM'].min(), 1000), df['RPM'].max(), n_rpm)
H, Ma, RPM = np.meshgrid(H_range, Ma_range, RPM_range, indexing='ij')
X_grid = np.column_stack([H.ravel(), Ma.ravel(), RPM.ravel()])
print(f"-> Querying GPR teacher on {len(X_grid)} grid points ...")
Y_pred, _ = gpr.predict(X_grid) # [WF_kg_h, Power_kW]
valid = (Y_pred[:, 0] > 0.5) & (Y_pred[:, 1] > 0.5)
X_valid = X_grid[valid].astype(np.float32)
Y_valid = Y_pred[valid].astype(np.float32)
print(f"-> Valid samples: {len(X_valid)} / {len(X_grid)}")
scaler_X = SimpleScaler()
scaler_X.fit(X_valid)
Y_log = np.log1p(Y_valid.astype(np.float64))
scaler_Y = SimpleScaler()
scaler_Y.fit(Y_log)
return X_valid, Y_valid, scaler_X, scaler_Y
def distill(gpr_csv_path, gpr_pth_path, nn_save_path,
n_altitude=25, n_mach=6, n_rpm=30,
epochs=3000, lr=1e-3, batch_size=512,
hidden_size=64, verbose=True):
"""
从 GPR 教师模型蒸馏到 NN。需安装 botorch / gpytorch。
"""
X_phys, Y_phys, scaler_X, scaler_Y = _generate_teacher_data(
gpr_csv_path, gpr_pth_path, n_altitude, n_mach, n_rpm
)
return _train_nn(X_phys, Y_phys, scaler_X, scaler_Y, nn_save_path,
epochs, lr, batch_size, hidden_size, verbose)
# ============================================================
# 通用 NN 训练核心
# ============================================================
def _train_nn(X_phys, Y_phys, scaler_X, scaler_Y, nn_save_path,
epochs=3000, lr=1e-3, batch_size=256,
hidden_size=64, verbose=True, progress_callback=None):
"""训练 NN 并保存模型。"""
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if verbose:
print(f"-> Training device: {device}")
# 创建模型并移至设备
nn_model = EngineNNProxy(hidden_size=hidden_size)
nn_model.set_normalization_params(scaler_X, scaler_Y)
nn_model = nn_model.to(device)
# 准备归一化数据
X_norm = scaler_X.transform(X_phys).astype(np.float32)
Y_log = np.log1p(Y_phys.astype(np.float64))
Y_norm = scaler_Y.transform(Y_log).astype(np.float32)
X_t = torch.tensor(X_norm, dtype=torch.float32)
Y_t = torch.tensor(Y_norm, dtype=torch.float32)
dataset = torch.utils.data.TensorDataset(X_t, Y_t)
loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)
# 训练
optimizer = torch.optim.Adam(nn_model.net.parameters(), lr=lr)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=epochs, eta_min=lr * 0.01)
loss_fn = nn.MSELoss()
loss_history = []
nn_model.train()
n_params = sum(p.numel() for p in nn_model.parameters())
if verbose:
print(f"-> Training NN ({hidden_size}x{hidden_size}, {n_params} params) "
f"for {epochs} epochs on {len(X_phys)} samples ...")
for epoch in range(epochs):
epoch_loss = 0.0
n_batches = 0
for xb, yb in loader:
xb, yb = xb.to(device), yb.to(device)
pred = nn_model.net(xb)
loss = loss_fn(pred, yb)
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch_loss += loss.item()
n_batches += 1
scheduler.step()
avg_loss = epoch_loss / max(n_batches, 1)
loss_history.append(avg_loss)
if verbose and (epoch + 1) % 500 == 0:
print(f" Epoch {epoch+1:>5d}/{epochs} Loss: {avg_loss:.6f}")
if progress_callback is not None and (epoch + 1) % 50 == 0:
progress_callback(epoch + 1, epochs, avg_loss)
nn_model.eval()
# 验证精度(在 CPU 上做推理,避免显存占用)
nn_model_cpu = nn_model.cpu()
with torch.no_grad():
X_phys_t = torch.tensor(X_phys, dtype=torch.float32)
Y_nn = nn_model_cpu(X_phys_t).numpy()
rel_err_fuel = np.mean(
np.abs(Y_nn[:, 0] - Y_phys[:, 0]) / np.maximum(Y_phys[:, 0], 1e-6)) * 100
rel_err_power = np.mean(
np.abs(Y_nn[:, 1] - Y_phys[:, 1]) / np.maximum(Y_phys[:, 1], 1e-6)) * 100
if verbose:
print(f"-> Distillation complete!")
print(f" Fuel Flow MAPE: {rel_err_fuel:.2f}%")
print(f" Power MAPE: {rel_err_power:.2f}%")
# 保存(始终保存 CPU 版,推理时无需 GPU 环境)
if nn_save_path:
os.makedirs(os.path.dirname(os.path.abspath(nn_save_path)), exist_ok=True)
torch.save(nn_model_cpu.state_dict(), nn_save_path)
if verbose:
print(f"-> Saved NN model to {nn_save_path}")
return {
'loss_history': loss_history,
'nn_model': nn_model_cpu,
'X_train': X_phys,
'Y_train': Y_phys,
'Y_nn': Y_nn,
'scaler_X': scaler_X,
'scaler_Y': scaler_Y,
'rel_error_fuel': rel_err_fuel,
'rel_error_power': rel_err_power,
}
if __name__ == "__main__":
model_root = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
csv_path = os.path.join(model_root, "data", "Cleaned_Engine_Data_Full.csv")
nn_path = os.path.join(model_root, "data", "engine_nn_proxy.pth")
# 使用 CSV 直接训练模式 (无需 sklearn/botorch)
result = distill_from_csv(csv_path, nn_path, epochs=3000)
print(f"\nFinal training loss: {result['loss_history'][-1]:.6f}")
+184 -241
View File
@@ -1,100 +1,72 @@
import sys import sys
import os import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import numpy as np import numpy as np
import torch
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from src.engine_gpr_class import EngineGPRModel from tqdm import tqdm
from src.increPID import IncrementalPIDController from src.lightweight_model import EngineNNProxy
from src.mpc_controller import TurboShaftMPCController
class TurboshaftDynamicSim: class TurboshaftDynamicSim:
""" """
涡轴发动机动态仿真类 涡轴发动机动态仿真类
包含一阶燃油执行机构和转子动力学模型
包含功能:
1. 基于 NN Proxy (轻量化神经网络) 的稳态代理模型
2. 一阶燃油执行机构动态 (First-order Actuator Dynamics)
3. 转子动力学积分 (Rotor Dynamics)
4. MPC (模型预测控制) 用于功率跟随
""" """
def __init__(self, gpr_csv_path="data/Cleaned_Engine_Data_Full.csv", gpr_pth_path="data/engine_gpr_model.pth", def __init__(self, nn_pth_path="data/engine_nn_proxy.pth",
tau_fuel=0.15, K_inertia=100.0, tau_fuel=0.15, K_inertia=100.0,
kp=4.652, ki=7.078, kd=0.222, min_fuel=10.0, max_fuel=600.0, mpc_horizon=15, mpc_dt=0.02, min_fuel=10.0, max_fuel=400.0,
verbose=False): mpc_overshoot_limit=0.05):
""" print("-> 正在加载 NN 稳态代理模型...")
初始化仿真环境和代理模型 self.engine_model = EngineNNProxy()
:param gpr_csv_path: GPR模型使用的数据集路径
:param gpr_pth_path: 预训练的模型参数路径
:param tau_fuel: 燃油执行机构时间常数 (s)
:param K_inertia: 转子惯性增益 (RPM / (kg/h))
:param kp: PID比例系数 (用于功率跟随控制,归一化域)
:param ki: PID积分系数 (归一化域)
:param kd: PID微分系数 (归一化域)
:param min_fuel: 燃油流量下限 (kg/h)
:param max_fuel: 燃油流量上限 (kg/h)
:param verbose: 是否打印详细信息
"""
import os
self.verbose = verbose
if verbose: if not os.path.exists(nn_pth_path):
print("-> 正在加载 GPR 稳态代理模型...") raise FileNotFoundError(f"Missing NN model file: {nn_pth_path}. Please run scripts/distill_gpr_to_nn.py first.")
self.engine_model = EngineGPRModel(gpr_csv_path)
# 检查模型文件是否存在,不存在则训练 self.engine_model.load_state_dict(torch.load(nn_pth_path, map_location='cpu'))
if not os.path.exists(gpr_pth_path): self.engine_model.eval()
if verbose: print("-> NN Model Loaded (CPU Mode for Sim Loop)")
print(f"-> 模型文件 {gpr_pth_path} 不存在,正在训练模型...")
self.engine_model.train(save_path=gpr_pth_path)
else:
success = self.engine_model.load_model(gpr_pth_path)
if not success:
if verbose:
print(f"-> 模型加载失败,正在重新训练...")
self.engine_model.train(save_path=gpr_pth_path)
# 动态参数
self.tau_fuel = tau_fuel self.tau_fuel = tau_fuel
self.K_inertia = K_inertia self.K_inertia = K_inertia
# 归一化基准 (用于PID计算)
self.max_fuel = max_fuel self.max_fuel = max_fuel
self.max_power_ref = 300.0 # 300kw为功率归一化基准,根据实际数据调整 self.max_power_ref = 300.0
# 控制器初始化 (使用归一化参数,通过 scaling 自动处理) self.mpc = TurboShaftMPCController(
self.pid = IncrementalPIDController( tau_fuel=tau_fuel, K_inertia=K_inertia, dt=mpc_dt, horizon=mpc_horizon,
kp=kp, ki=ki, kd=kd, dt=0.01, min_fuel=min_fuel, max_fuel=max_fuel, overshoot_limit=mpc_overshoot_limit
output_min=min_fuel, output_max=max_fuel,
input_scale=self.max_power_ref, output_scale=self.max_fuel
) )
# 环境与状态变量
self.H_env = 0.0 self.H_env = 0.0
self.Ma_env = 0.0 self.Ma_env = 0.0
self.N_current = 0.0 self.N_current = 0.0
self.Wf_act_current = 0.0 self.Wf_act_current = 0.0
self.power_generated = 0.0 self.power_generated = 0.0
# 控制指令
self.Wf_cmd = 0.0 self.Wf_cmd = 0.0
def _solve_steady_rpm(self, target_val, target_type='power'): def _solve_steady_rpm(self, target_val, target_type='power'):
""" """数值反解给定功率/燃油下的稳态转速"""
内部方法:反解稳态转速 (RPM)
:param target_val: 目标值 (Power[kW] 或 Fuel[kg/h])
:param target_type: 'power''fuel'
:return: 对应的稳态转速
"""
from scipy.optimize import brentq from scipy.optimize import brentq
# 搜索范围 [RPM_min, RPM_max],根据经验或数据范围设定
low_bound, high_bound = 0.0, 60000.0 low_bound, high_bound = 0.0, 60000.0
def objective(n): def objective(n):
current_input = np.array([[self.H_env, self.Ma_env, n]]) # 构造输入: [H, Ma, N]
pred_mean, _ = self.engine_model.predict(current_input) current_input = torch.tensor([[self.H_env, self.Ma_env, n]], dtype=torch.float32)
with torch.no_grad():
pred_mean = self.engine_model(current_input).numpy()
# index 0: Fuel Flow (kg/h), index 1: Power (kW) # 输出: [0]=Fuel, [1]=Power
val = pred_mean[0, 1] if target_type == 'power' else pred_mean[0, 0] val = pred_mean[0, 1] if target_type == 'power' else pred_mean[0, 0]
return val - target_val return val - target_val
try: try:
# 简单的边界检查,防止报错
f_low = objective(low_bound) f_low = objective(low_bound)
f_high = objective(high_bound) f_high = objective(high_bound)
if f_low * f_high > 0: if f_low * f_high > 0:
@@ -108,118 +80,104 @@ class TurboshaftDynamicSim:
return (low_bound + high_bound) / 2.0 return (low_bound + high_bound) / 2.0
def set_steady_state_by_power(self, H_env, Ma_env, Power_target): def set_steady_state_by_power(self, H_env, Ma_env, Power_target):
""" """设定初始稳态工况点"""
通过目标功率初始化稳态
:param Power_target: 目标轴功率 (kW)
:return: 对应的稳态转速 (RPM)
"""
self.H_env = H_env self.H_env = H_env
self.Ma_env = Ma_env self.Ma_env = Ma_env
# 反解转速
self.N_current = self._solve_steady_rpm(Power_target, target_type='power') self.N_current = self._solve_steady_rpm(Power_target, target_type='power')
# 计算该转速下的稳态燃油 # 计算该稳态下的燃油消耗
current_input = np.array([[self.H_env, self.Ma_env, self.N_current]]) current_input = torch.tensor([[self.H_env, self.Ma_env, self.N_current]], dtype=torch.float32)
pred_mean, _ = self.engine_model.predict(current_input) with torch.no_grad():
pred_mean = self.engine_model(current_input).numpy()
self.power_generated = pred_mean[0, 1] self.power_generated = pred_mean[0, 1]
self.Wf_act_current = pred_mean[0, 0] self.Wf_act_current = pred_mean[0, 0]
self.Wf_cmd = self.Wf_act_current self.Wf_cmd = self.Wf_act_current
print(f"-> 稳态(Power)已配置: H={H_env}, Ma={Ma_env}, Target_P={Power_target:.1f} kW => N={self.N_current:.1f} RPM, Wf={self.Wf_cmd:.2f} kg/h") if self.verbose else None print(f"-> 稳态(Power)已配置: H={H_env}, Ma={Ma_env}, Target_P={Power_target:.1f} kW => N={self.N_current:.1f} RPM, Wf={self.Wf_cmd:.2f} kg/h")
self.mpc.reset(initial_output=self.Wf_cmd, initial_N=self.N_current)
# 重置PID控制器至当前稳态输出 (自动处理归一化)
self.pid.reset(initial_output=self.Wf_cmd)
return self.N_current return self.N_current
def set_steady_state_by_fuel(self, H_env, Ma_env, Wf_target): def compute_control_law(self, dt, target_power, precalc_params=None):
""" """调用 MPC 更新控制指令"""
通过目标燃油流量初始化稳态 self.Wf_cmd = self.mpc.compute(
:param Wf_target: 目标燃油流量 (kg/h) current_N=self.N_current,
:return: 对应的稳态转速 (RPM) current_Wfact=self.Wf_act_current,
""" target_power=target_power,
self.H_env = H_env precalc_params=precalc_params
self.Ma_env = Ma_env )
# 反解转速
self.N_current = self._solve_steady_rpm(Wf_target, target_type='fuel')
# 确认该状态下的功率
current_input = np.array([[self.H_env, self.Ma_env, self.N_current]])
pred_mean, _ = self.engine_model.predict(current_input)
self.power_generated = pred_mean[0, 1]
self.Wf_act_current = pred_mean[0, 0] # 理论上应该非常接近 Wf_target
self.Wf_cmd = self.Wf_act_current
print(f"-> 稳态(Fuel)已配置: H={H_env}, Ma={Ma_env}, Target_Wf={Wf_target:.1f} kg/h => N={self.N_current:.1f} RPM, Power={self.power_generated:.1f} kW") if self.verbose else None
# 重置PID控制器至当前稳态输出 (自动处理归一化)
self.pid.reset(initial_output=self.Wf_cmd)
return self.N_current
def set_fuel_command(self, Wf_cmd):
"""
更改燃油流量指令
:param Wf_cmd: 目标燃油流量指令
"""
self.Wf_cmd = Wf_cmd
def set_flight_condition(self, H_env=None, Ma_env=None):
"""
在运行过程中更改当前的飞行条件 (高度和马赫数)
:param H_env: 新的飞行高度 (m)。如果为 None,则保持不变。
:param Ma_env: 新的飞行马赫数。如果为 None,则保持不变。
"""
if H_env is not None:
self.H_env = H_env
if Ma_env is not None:
self.Ma_env = Ma_env
def compute_control_law(self, dt, target_power):
"""
计算控制律 (PID控制: Power -> Wf)
:param dt: 控制周期 (s)
:param target_power: 期望功率 (kW)
:return: 计算出的燃油指令
"""
self.pid.dt = dt
# 1. 计算控制增量 (PID内部会自动处理归一化)
self.Wf_cmd = self.pid.compute(setpoint=target_power, measurement=self.power_generated)
return self.Wf_cmd return self.Wf_cmd
def step(self, dt, target_power=None): def step(self, dt, target_power=None):
""" """
执行单步动态仿真 执行单步动态仿真 (High-Performance Optimized)
:param dt: 积分步长 (s)
:param target_power: 目标轴功率 (kW),若不为 None 则执行一次PID控制
:return: (当前转速, 实际供油量, 当前需要的平衡供油量, 当前功率)
"""
# 0. 闭环控制计算
if target_power is not None:
self.compute_control_law(dt, target_power)
# 1. 燃油执行机构动态 (一阶惯性) 加速策略:
- 聚合 GPR 预测请求: 将 MPC 所需的梯度计算点与当前物理状态点合并为一个 Batch (Size=2)
- 减少 GPU I/O 次数: 从每步 3 次减少为 1 次
"""
# --- 0. 统一 GPU 批次预测 (Batch Prediction) ---
# 构造输入: [Row 0: 当前状态点, Row 1: 用于梯度计算的微扰点]
delta_N = 5.0
# 判断模型类型: NN 模型使用 forwardGPR 模型使用 predict
if hasattr(self.engine_model, 'predict') and not hasattr(self.engine_model, 'forward'):
# GPR 模型
inputs = np.array([
[self.H_env, self.Ma_env, self.N_current],
[self.H_env, self.Ma_env, self.N_current + delta_N]
])
pred_mean, _ = self.engine_model.predict(inputs)
else:
# NN 模型
import torch
inputs = torch.tensor([
[self.H_env, self.Ma_env, self.N_current],
[self.H_env, self.Ma_env, self.N_current + delta_N]
], dtype=torch.float32)
with torch.no_grad():
pred_mean = self.engine_model(inputs).numpy()
# 核心加速点:一次 GPU 调用获取所有信息
# pred_mean 形如 [[Wf0, Pow0], [Wf1, Pow1]]
# 提取结果
Wf_req_current = pred_mean[0, 0]
Power_current = pred_mean[0, 1]
Wf_req_pert = pred_mean[1, 0]
Power_pert = pred_mean[1, 1]
# --- 1. 闭环控制计算 ---
if target_power is not None:
# 在 Python 端快速计算梯度,避免在 MPC 内部再次调用模型
k_wf = (Wf_req_pert - Wf_req_current) / delta_N
k_p = (Power_pert - Power_current) / delta_N
# 使用预计算好的参数,MPC 内部将不再调用 engine_model.predict
params = (Wf_req_current, Power_current, k_wf, k_p)
self.compute_control_law(dt, target_power, precalc_params=params)
# --- 2. 燃油执行机构动态 (一阶惯性) ---
dWf_act_dt = (self.Wf_cmd - self.Wf_act_current) / self.tau_fuel dWf_act_dt = (self.Wf_cmd - self.Wf_act_current) / self.tau_fuel
Wf_act_next = self.Wf_act_current + dWf_act_dt * dt Wf_act_next = self.Wf_act_current + dWf_act_dt * dt
# 2. 调用GPR代理模型算当前转速下的阻力矩(需求燃油) # --- 3. 调用NN代理模型算当前气动热力参数 ---
current_input = np.array([[self.H_env, self.Ma_env, self.N_current]]) current_input = torch.tensor([[self.H_env, self.Ma_env, self.N_current]], dtype=torch.float32)
pred_mean, _ = self.engine_model.predict(current_input) with torch.no_grad():
pred_mean = self.engine_model(current_input).numpy()
# GPR 输出: [0]: Fuel Flow, [1]: Shaft Power # [0]: Fuel Flow (kg/h), [1]: Power (kW)
Wf_req_current = pred_mean[0, 0] #维持当前转速所需的稳态燃油 Wf_req_current = pred_mean[0, 0]
Power_current = pred_mean[0, 1] Power_current = pred_mean[0, 1]
self.power_generated = Power_current # 更新当前功率状态 self.power_generated = Power_current
# 3. 转子动力学积分 (燃料差额 -> 转速加速度) # --- 4. 转子动力学积分 (供油盈余 -> 加速) ---
dN_dt = self.K_inertia * (self.Wf_act_current - Wf_req_current) dN_dt = self.K_inertia * (self.Wf_act_current - Wf_req_current)
N_next = self.N_current + dN_dt * dt N_next = self.N_current + dN_dt * dt
# 4. 状态更新 # 更新状态
self.Wf_act_current = Wf_act_next self.Wf_act_current = Wf_act_next
self.N_current = N_next self.N_current = N_next
@@ -228,7 +186,8 @@ class TurboshaftDynamicSim:
if __name__ == "__main__": if __name__ == "__main__":
# ========================================== # ==========================================
# 【测试示例】利用类运行功率闭环控制仿真 # 涡轴发动机动态响应测试脚本
# 模拟复杂剖面: 包含阶跃、正弦、斜坡指令及变高度/马赫数干扰
# ========================================== # ==========================================
import matplotlib import matplotlib
matplotlib.use('Agg') matplotlib.use('Agg')
@@ -238,121 +197,105 @@ if __name__ == "__main__":
plt.rcParams['font.serif'] = ['DejaVu Serif', 'Times New Roman'] plt.rcParams['font.serif'] = ['DejaVu Serif', 'Times New Roman']
plt.rcParams['axes.unicode_minus'] = True plt.rcParams['axes.unicode_minus'] = True
# 初始化仿真 (优化后的PID参数) sim = TurboshaftDynamicSim(mpc_dt=0.02)
# kp: 比例系数 - 增大以加快响应速度
# ki: 积分系数 - 适中以消除稳态误差
# kd: 微分系数 - 设为0避免控制振荡
sim = TurboshaftDynamicSim()
# 1. 设置初始稳态 (通过目标功率设定) # 初始状态
P_initial_target = 100.0 # kW P_initial_target = 100.0
sim.set_steady_state_by_power(H_env=0.0, Ma_env=0.0, Power_target=P_initial_target) sim.set_steady_state_by_power(H_env=0.0, Ma_env=0.0, Power_target=P_initial_target)
# 仿真参数
dt = 0.02 dt = 0.02
t_end = 30.0 # 增加仿真时间以展示更多指令变化 t_end = 60.0
time_array = np.arange(0, t_end, dt) time_array = np.arange(0, t_end, dt)
# 数据记录 N_log, Wf_act_log, Wf_cmd_log, Power_log, Power_target_log = [], [], [], [], []
N_log = [] H_env_log, Ma_env_log = [], []
Wf_act_log = []
Wf_cmd_log = []
Power_log = []
Power_target_log = []
# 定义多段功率指令 (时间[s], 目标功率[kW]) print("-> 开始极限工况仿真测试 (大动态指令 + 连续外界干扰)...")
power_profile = [
(0.0, 100.0), # 初始稳态
(3.0, 200.0), # 阶跃上升
(8.0, 150.0), # 阶跃下降
(13.0, 250.0), # 阶跃上升至高功率
(18.0, 100.0), # 快速下降
(23.0, 180.0), # 再次上升
]
# 斜坡指令测试:从18kW开始以一定速率上升 for t in tqdm(time_array, desc="Simulating"):
ramp_start_time = 25.0 # --- 1. 生成复杂功率指令 (Setpoint) ---
ramp_rate = 10.0 # kW/s if t < 10.0:
# 阶跃测试
target_p = 100.0 if t < 3 else 220.0 if t < 7 else 80.0
elif t < 25.0:
# 正弦跟踪 (0.2Hz)
target_p = 140.0 + 50.0 * np.sin(2 * np.pi * 0.2 * (t - 10.0))
elif t < 40.0:
# 锯齿波测试
cycle = (t - 25.0) % 5.0
target_p = 80.0 + (80.0 / 5.0) * cycle
else:
# 极限大范围跳变
target_p = 170.0 if t < 48.0 else 40.0
# 设定飞行条件改变的时间 # --- 2. 生成环境扰动 (Disturbance) ---
flight_change_time = 28.0 if t < 15.0:
h_env, ma_env = 0.0, 0.0
elif t < 30.0:
# 爬升阶段: 0->3000m
h_env = 0.0 + (3000.0 / 15.0) * (t - 15.0)
ma_env = 0.0 + (0.2 / 15.0) * (t - 15.0)
elif t < 45.0:
h_env, ma_env = 3000.0, 0.2
else:
# 突发机动
h_env, ma_env = 500.0, 0.4
def get_target_power(t, profile, ramp_start, ramp_rate, default_power): sim.H_env = h_env
"""根据时间获取当前目标功率""" sim.Ma_env = ma_env
for i, (time, _) in enumerate(profile):
if t < time:
return profile[i-1][1] if i > 0 else default_power
# 如果在斜坡区间
if t >= ramp_start:
last_static_power = profile[-1][1]
ramp_power = last_static_power + ramp_rate * (t - ramp_start)
return min(ramp_power, 300.0) # 限制最大300kW
return profile[-1][1]
print("-> 开始仿真步进 (闭环控制)...") # 执行单步仿真
print("-> 功率指令配置文件:") N_cur, Wf_act_cur, Wf_req, Power_cur = sim.step(dt, target_power=target_p)
for time, power in power_profile:
print(f" t={time:.1f}s -> {power:.0f} kW")
print(f" t={ramp_start_time:.1f}s -> 斜坡上升 (速率 {ramp_rate} kW/s)")
print()
for t in time_array:
# 确定当前的目标功率
current_target_P = get_target_power(t, power_profile, ramp_start_time, ramp_rate, P_initial_target)
# 触发飞行条件变化 (确保仅触发一次以免重复打印)
if flight_change_time <= t < flight_change_time + dt:
print(f"\n[!] 时间 t={t:.2f}s, 模拟飞行条件跃变...")
sim.set_flight_condition(H_env=3000.0, Ma_env=0.2)
# 执行闭环仿真步进
N_cur, Wf_act_cur, Wf_req, Power_cur = sim.step(dt, target_power=current_target_P)
# 记录数据
N_log.append(N_cur) N_log.append(N_cur)
Wf_act_log.append(Wf_act_cur) Wf_act_log.append(Wf_act_cur)
Wf_cmd_log.append(sim.Wf_cmd) Wf_cmd_log.append(sim.Wf_cmd)
Power_log.append(Power_cur) Power_log.append(Power_cur)
Power_target_log.append(current_target_P) Power_target_log.append(target_p)
H_env_log.append(h_env)
Ma_env_log.append(ma_env)
# 绘图 # ==========================================
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10)) # 绘图逻辑
fig.suptitle(f'Turboshaft Engine Dynamic Simulation\nPID Parameters: Kp={sim.pid.kp}, Ki={sim.pid.ki}, Kd={sim.pid.kd}', # ==========================================
fontsize=12, fontweight='bold') fig, axes = plt.subplots(4, 1, figsize=(14, 12), gridspec_kw={'height_ratios': [2.5, 2, 2, 1.5]})
fig.suptitle('Turboshaft Engine: MPC Extreme Stress Test\nSine Tracking + Continuous Disturbances',
fontsize=14, fontweight='bold')
# Plot 1: Power Tracking # Plot 1: Power Tracking (核心表现)
ax1.plot(time_array, Power_target_log, 'k--', linewidth=2, label='Target Power') axes[0].plot(time_array, Power_target_log, 'k--', linewidth=2, label='Target Power (Command)')
ax1.plot(time_array, Power_log, 'g-', linewidth=2, label='Actual Power') axes[0].plot(time_array, Power_log, 'g-', linewidth=2, label='Actual Power (MPC)')
ax1.set_ylabel('Shaft Power [kW]') axes[0].set_ylabel('Shaft Power [kW]', fontweight='bold')
ax1.set_title('Closed-Loop Control: Power Tracking', fontweight='bold') axes[0].set_title('Performance: Complex Trajectory Tracking', fontweight='bold')
ax1.grid(True, linestyle=':', alpha=0.7) axes[0].grid(True, linestyle=':', alpha=0.7)
ax1.legend() axes[0].legend(loc='upper right')
# Plot 2: Rotor Speed # Plot 2: Rotor Speed
ax2.plot(time_array, N_log, 'b-', linewidth=2, label='Engine Speed (N)') axes[1].plot(time_array, N_log, 'b-', linewidth=2, label='Engine Speed (N)')
ax2.set_ylabel('Rotor Speed [RPM]') axes[1].set_ylabel('Rotor Speed [RPM]', fontweight='bold')
ax2.set_title('Engine Response: Rotor Speed', fontweight='bold') axes[1].set_title('State: Rotor Speed Response', fontweight='bold')
ax2.grid(True, linestyle=':', alpha=0.7) axes[1].grid(True, linestyle=':', alpha=0.7)
ax2.legend() axes[1].legend(loc='upper right')
# Plot 3: Fuel Flow (Control Input) # Plot 3: Fuel Flow (Control Input)
ax3.plot(time_array, Wf_cmd_log, 'k--', linewidth=1.5, label='Fuel Command') axes[2].plot(time_array, Wf_cmd_log, 'r--', linewidth=1.5, label='Fuel Command (MPC Output)')
ax3.plot(time_array, Wf_act_log, 'r-', linewidth=2, label='Actual Fuel') axes[2].plot(time_array, Wf_act_log, 'm-', linewidth=2, label='Actual Fuel Actuator')
ax3.set_xlabel('Time [s]') axes[2].set_ylabel('Fuel Flow [kg/h]', fontweight='bold')
ax3.set_ylabel('Fuel Flow [kg/h]') axes[2].set_title('Control Effort: Actuator Dynamics', fontweight='bold')
ax3.set_title('Control Effort: Fuel Flow', fontweight='bold') axes[2].grid(True, linestyle=':', alpha=0.7)
ax3.grid(True, linestyle=':', alpha=0.7) axes[2].legend(loc='upper right')
ax3.legend()
# Plot 4: Environmental Disturbances
ax4_1 = axes[3]
ax4_2 = ax4_1.twinx()
ax4_1.plot(time_array, H_env_log, 'c-', linewidth=2, label='Altitude (m)')
ax4_2.plot(time_array, Ma_env_log, 'y-', linewidth=2, label='Mach Number')
ax4_1.set_xlabel('Time [s]', fontweight='bold')
ax4_1.set_ylabel('Altitude [m]', color='c', fontweight='bold')
ax4_2.set_ylabel('Mach', color='y', fontweight='bold')
axes[3].set_title('Disturbances: Flight Conditions', fontweight='bold')
axes[3].grid(True, linestyle=':', alpha=0.7)
plt.tight_layout(rect=[0, 0, 1, 0.96]) plt.tight_layout(rect=[0, 0, 1, 0.96])
plt.savefig('figures/engine_dynamic_sim_plot.png') plt.savefig('figures/engine_mpc_stress_test.png', dpi=200)
print('Plot saved to figures/engine_dynamic_sim_plot.png') print('-> 仿真完成!极限制图已保存至: figures/engine_mpc_stress_test.png')
# 保存数据到 .dat 文件
dat_file = 'data/pid_tuning_data.dat'
with open(dat_file, 'w') as f:
f.write('# Time[s]\tTarget_Power[kW]\tActual_Power[kW]\tRotor_Speed[RPM]\tFuel_Command[kg/h]\tActual_Fuel[kg/h]\n')
for i in range(len(time_array)):
f.write(f'{time_array[i]:.4f}\t{Power_target_log[i]:.2f}\t{Power_log[i]:.2f}\t{N_log[i]:.2f}\t{Wf_cmd_log[i]:.2f}\t{Wf_act_log[i]:.2f}\n')
print(f'Data saved to {dat_file}')
# plt.show()
+20 -6
View File
@@ -34,10 +34,13 @@ class EngineGPRModel:
self.scaler_X = StandardScaler() self.scaler_X = StandardScaler()
self.scaler_Y = StandardScaler() self.scaler_Y = StandardScaler()
self.model = None self.model = None
# 自动检测并使用 GPU
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {self.device}")
def _prepare_data(self): def _prepare_data(self):
"""数据预处理:读取、筛选、Log变换、标准化""" """数据预处理:读取、筛选、Log变换、标准化"""
self.df = pd.read_csv(self.csv_path) self.df = pd.read_csv(self.csv_path, encoding='utf-8')
X_df = self.df[['Altitude_m', 'Mach', 'RPM']] X_df = self.df[['Altitude_m', 'Mach', 'RPM']]
# 剔除无效特征 # 剔除无效特征
@@ -56,7 +59,8 @@ class EngineGPRModel:
X_scaled = self.scaler_X.fit_transform(X_numpy) X_scaled = self.scaler_X.fit_transform(X_numpy)
Y_scaled = self.scaler_Y.fit_transform(Y_numpy) Y_scaled = self.scaler_Y.fit_transform(Y_numpy)
return torch.tensor(X_scaled, dtype=torch.double), torch.tensor(Y_scaled, dtype=torch.double) # 转换为 Tensor 并移动到 GPU (如果可用)
return torch.tensor(X_scaled, dtype=torch.double).to(self.device), torch.tensor(Y_scaled, dtype=torch.double).to(self.device)
def _init_model(self, train_X, train_Y): def _init_model(self, train_X, train_Y):
"""内部方法:统一初始化模型结构(包含 Mean 和 Prior 设置)""" """内部方法:统一初始化模型结构(包含 Mean 和 Prior 设置)"""
@@ -83,6 +87,7 @@ class EngineGPRModel:
# 使用统一初始化方法 # 使用统一初始化方法
self.model = self._init_model(train_X, train_Y) self.model = self._init_model(train_X, train_Y)
self.model.to(self.device)
# 仅在训练开始前设定初始值,引导优化方向 # 仅在训练开始前设定初始值,引导优化方向
if hasattr(self.model.covar_module, 'base_kernel'): if hasattr(self.model.covar_module, 'base_kernel'):
@@ -109,10 +114,12 @@ class EngineGPRModel:
train_X, train_Y = self._prepare_data() train_X, train_Y = self._prepare_data()
# 必须使用完全相同的结构初始化,否则 load_state_dict 会报错 # 必须使用完全相同的结构初始化,否则 load_state_dict 会报错
self.model = self._init_model(train_X, train_Y) self.model = self._init_model(train_X, train_Y)
self.model.to(self.device)
# 使用 strict=False 忽略 Prior 缓冲区的差异(例如 _transformed_loc 等内部参数) # 使用 strict=False 忽略 Prior 缓冲区的差异(例如 _transformed_loc 等内部参数)
# 这些参数通常不影响模型预测,只影响后续继续训练时的约束 # 这些参数通常不影响模型预测,只影响后续继续训练时的约束
self.model.load_state_dict(torch.load(pth_path), strict=True) state_dict = torch.load(pth_path, map_location=self.device)
self.model.load_state_dict(state_dict, strict=True)
self.model.eval() self.model.eval()
print("-> Model loaded successfully.") print("-> Model loaded successfully.")
return True return True
@@ -125,12 +132,19 @@ class EngineGPRModel:
if self.model is None: raise ValueError("Model not initialized.") if self.model is None: raise ValueError("Model not initialized.")
self.model.eval() self.model.eval()
test_X_scaled = torch.tensor(self.scaler_X.transform(test_X_real), dtype=torch.double)
# Determine device from model parameters
try:
device = next(self.model.parameters()).device
except StopIteration:
device = torch.device('cpu')
test_X_scaled = torch.tensor(self.scaler_X.transform(test_X_real), dtype=torch.double, device=device)
with torch.no_grad(): with torch.no_grad():
posterior = self.model.posterior(test_X_scaled) posterior = self.model.posterior(test_X_scaled)
mu_scaled = posterior.mean.numpy() mu_scaled = posterior.mean.detach().cpu().numpy()
var_scaled = posterior.variance.numpy() var_scaled = posterior.variance.detach().cpu().numpy()
# 反归一化 # 反归一化
mu_log_real = self.scaler_Y.inverse_transform(mu_scaled) mu_log_real = self.scaler_Y.inverse_transform(mu_scaled)
+70
View File
@@ -0,0 +1,70 @@
import torch
import torch.nn as nn
import numpy as np
class EngineNNProxy(nn.Module):
"""
轻量级神经网络代理模型,用于替代笨重的 GPR 模型。
结构: 简单的 MLP (多层感知机)
输入: [Altitude, Mach, RPM] (未归一化)
输出: [FuelFlow, Power] (未归一化)
"""
def __init__(self, hidden_size=64):
super(EngineNNProxy, self).__init__()
# 定义网络结构
# 对应 distill_gpr_to_nn.py 中的索引访问:
# 0: Linear
# 1: ReLU / Tanh
# 2: Linear
# 3: ReLU / Tanh
# 4: Linear (Output)
self.net = nn.Sequential(
nn.Linear(3, hidden_size), # 0
nn.Tanh(), # 1: Tanh 通常比 ReLU 更适合平滑的物理函数拟合
nn.Linear(hidden_size, hidden_size), # 2
nn.Tanh(), # 3
nn.Linear(hidden_size, 2) # 4: 输出 2 个物理量 (Fuel, Power)
)
# 归一化参数 (注册为 buffer 以便随模型保存)
# 初始化为默认值,防止未调用 set_normalization_params 时报错
self.register_buffer('x_mean', torch.zeros(3))
self.register_buffer('x_scale', torch.ones(3))
self.register_buffer('y_mean', torch.zeros(2))
self.register_buffer('y_scale', torch.ones(2))
def set_normalization_params(self, scaler_X, scaler_Y):
"""
从 sklearn StandardScaler 中提取参数
scaler_X: 用于输入的归一化器
scaler_Y: 用于输出 (Log1p Space) 的归一化器
"""
if scaler_X is not None:
self.x_mean.copy_(torch.tensor(scaler_X.mean_, dtype=torch.float32))
self.x_scale.copy_(torch.tensor(scaler_X.scale_, dtype=torch.float32))
if scaler_Y is not None:
self.y_mean.copy_(torch.tensor(scaler_Y.mean_, dtype=torch.float32))
self.y_scale.copy_(torch.tensor(scaler_Y.scale_, dtype=torch.float32))
def forward(self, x):
"""
前向传播: 物理输入 -> 物理输出
包含: 归一化 -> NN推理 -> 反归一化 -> expm1
"""
# 1. 输入归一化 (Z-Score)
# 确保输入 x 与 buffer 在同一设备
x = x.to(self.x_mean.device)
x_norm = (x - self.x_mean) / self.x_scale
# 2. 神经网络推理 (预测 Log Normalized Z-Score)
y_norm_pred = self.net(x_norm)
# 3. 输出反归一化 (Z-Score Inverse)
y_log1p_pred = y_norm_pred * self.y_scale + self.y_mean
# 4. 指数还原 (Inverse Log1p)
y_phys_pred = torch.expm1(y_log1p_pred)
return y_phys_pred
+180 -368
View File
@@ -9,18 +9,16 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
模块提供了永磁同步电机 (PMSM) 的离散时间仿真实现,涵盖了闭环转速控制、 模块提供了永磁同步电机 (PMSM) 的离散时间仿真实现,涵盖了闭环转速控制、
物理级的电磁转矩估算及端电压/损耗模型的动力学计算。 物理级的电磁转矩估算及端电压/损耗模型的动力学计算。
系统符号约定: 系统符号约定:
- 轴系转矩 (T_motor): 正值表示吸收轴系功率 (发电机/负载响应),负值表示向轴系输出功率 (驱动动力)。 - 轴系转矩 (T_motor): 正值表示吸收轴系功率 (发电机/负载响应),负值表示向轴系输出功率 (驱动动力)。
- 直流母线功率 (P_bus): 正值表示从母线汲取有功功率,负值表示向母线回馈有功功率。 - 直流母线功率 (P_bus): 正值表示从母线汲取有功功率,负值表示向母线回馈有功功率。
""" """
from typing import Any, Dict from typing import Any, Dict, Optional
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import numpy as np import numpy as np
from src.increPID import IncrementalPIDController from tqdm import tqdm
from src.mpc_controller import MotorMPCController
class MotorSim: class MotorSim:
@@ -40,12 +38,7 @@ class MotorSim:
eta_mot: float = 0.95, eta_mot: float = 0.95,
eta_gen: float = 0.93, eta_gen: float = 0.93,
B_visc: float = 3e-4, B_visc: float = 3e-4,
k_p_w: float = 13.440362,
k_i_w: float = 42.997816,
k_d_w: float = 0.484660,
k_d_w_error: float = 0.05,
p_bus_slew_rate_kw: float = 2000.0, p_bus_slew_rate_kw: float = 2000.0,
t_cmd_slew_rate_nm_s: float = 0.0,
tau_i: float = 0.004, tau_i: float = 0.004,
k_mod: float = 1 / np.sqrt(3), k_mod: float = 1 / np.sqrt(3),
P_const_loss: float = 250.0, P_const_loss: float = 250.0,
@@ -53,9 +46,11 @@ class MotorSim:
k_inv: float = 0.015, k_inv: float = 0.015,
tau_n_ref: float = 0.35, tau_n_ref: float = 0.35,
tau_t_cmd: float = 0.12, tau_t_cmd: float = 0.12,
speed_priority_band_rpm: float = 180.0,
tau_v_bus: float = 0.08, tau_v_bus: float = 0.08,
i_s_max: float | None = None, i_s_max: Optional[float] = None,
mpc_W_speed: float = 0.0,
mpc_W_dcost: float = 0.0,
mpc_overshoot_limit: float = 0.05,
): ):
""" """
初始化电机动力学仿真器参数。 初始化电机动力学仿真器参数。
@@ -72,12 +67,7 @@ class MotorSim:
:param eta_mot: 稳态驱动效率参考值 :param eta_mot: 稳态驱动效率参考值
:param eta_gen: 稳态发电效率参考值 :param eta_gen: 稳态发电效率参考值
:param B_visc: 轴系黏性摩擦阻尼系数 (N·m·s) :param B_visc: 轴系黏性摩擦阻尼系数 (N·m·s)
:param k_p_w: 速度控制环比例增益
:param k_i_w: 速度控制环积分增益
:param k_d_w: 速度控制环微分控制增益(闭环阻尼)
:param k_d_w_error: 目标速度微分前馈增益
:param p_bus_slew_rate_kw: 允许的最大母线请求功率变化率 (kW/s) :param p_bus_slew_rate_kw: 允许的最大母线请求功率变化率 (kW/s)
:param t_cmd_slew_rate_nm_s: 允许的最大转矩指令变化率 (Nm/s)
:param tau_i: 闭环电流/电磁转矩等效一阶延迟时间常数 (s) :param tau_i: 闭环电流/电磁转矩等效一阶延迟时间常数 (s)
:param k_mod: DC-AC 变换器电压利用系数 :param k_mod: DC-AC 变换器电压利用系数
:param P_const_loss: 独立于工况的系统常量损耗 (W) :param P_const_loss: 独立于工况的系统常量损耗 (W)
@@ -85,9 +75,11 @@ class MotorSim:
:param k_inv: DC-AC 逆变环节损耗系数 :param k_inv: DC-AC 逆变环节损耗系数
:param tau_n_ref: 目标转速给定指令一阶低通滤波时间常数 (s) :param tau_n_ref: 目标转速给定指令一阶低通滤波时间常数 (s)
:param tau_t_cmd: 转矩输出指令一阶低通滤波时间常数 (s) :param tau_t_cmd: 转矩输出指令一阶低通滤波时间常数 (s)
:param speed_priority_band_rpm: 目标跟随允许容差基准带 (RPM)
:param tau_v_bus: DC侧动态响应一阶滤波时间常数 (s) :param tau_v_bus: DC侧动态响应一阶滤波时间常数 (s)
:param i_s_max: 定子相电流约束阈值有效值 (A) :param i_s_max: 定子相电流约束阈值有效值 (A)
:param mpc_W_speed: MPC转速控制权重
:param mpc_W_dcost: MPC控制增量权重
:param mpc_overshoot_limit: MPC超调量硬约束 (0.05 = 5%)
""" """
self._validate_parameters( self._validate_parameters(
n_p, n_p,
@@ -116,12 +108,7 @@ class MotorSim:
self.eta_mot = eta_mot self.eta_mot = eta_mot
self.eta_gen = eta_gen self.eta_gen = eta_gen
self.B_visc = B_visc self.B_visc = B_visc
self.k_p_w = k_p_w
self.k_i_w = k_i_w
self.k_d_w = k_d_w
self.k_d_w_error = k_d_w_error
self.p_bus_slew_rate_kw = p_bus_slew_rate_kw self.p_bus_slew_rate_kw = p_bus_slew_rate_kw
self.t_cmd_slew_rate_nm_s = max(0.0, float(t_cmd_slew_rate_nm_s))
self.tau_i = tau_i self.tau_i = tau_i
self.k_mod = k_mod self.k_mod = k_mod
self.P_const_loss = P_const_loss self.P_const_loss = P_const_loss
@@ -129,9 +116,11 @@ class MotorSim:
self.k_inv = k_inv self.k_inv = k_inv
self.tau_n_ref = max(1e-4, float(tau_n_ref)) self.tau_n_ref = max(1e-4, float(tau_n_ref))
self.tau_t_cmd = max(1e-4, float(tau_t_cmd)) self.tau_t_cmd = max(1e-4, float(tau_t_cmd))
self.speed_priority_band = max(5.0, float(speed_priority_band_rpm)) * 2.0 * np.pi / 60.0
self.tau_v_bus = max(1e-4, float(tau_v_bus)) self.tau_v_bus = max(1e-4, float(tau_v_bus))
# 超调量约束参数 (用于转矩变化率限制)
self._overshoot_limit = mpc_overshoot_limit
self.tau_rate = self.P_rate / self.w_rate self.tau_rate = self.P_rate / self.w_rate
if i_s_max is None: if i_s_max is None:
base_current = self.tau_rate / (1.5 * self.n_p * max(abs(self.psi_f), 1e-6)) base_current = self.tau_rate / (1.5 * self.n_p * max(abs(self.psi_f), 1e-6))
@@ -160,28 +149,20 @@ class MotorSim:
self.u_an, self.u_bn, self.u_cn = 0.0, 0.0, 0.0 self.u_an, self.u_bn, self.u_cn = 0.0, 0.0, 0.0
self.duty_a, self.duty_b, self.duty_c = 0.5, 0.5, 0.5 self.duty_a, self.duty_b, self.duty_c = 0.5, 0.5, 0.5
self._sim_t = 0.0 self._sim_t = 0.0
self._w_error_int = 0.0
self._w_error_prev = 0.0
self._dw_error_f = 0.0
self._p_bus_req_prev = 0.0
self._w_set_f = 0.0 self._w_set_f = 0.0
self._t_ref_f = 0.0 self._t_ref_f = 0.0
self._v_bus_f = float(self.u_dc) self._v_bus_f = float(self.u_dc)
self._t_load_f = 0.0 self._t_load_f = 0.0
self._t_ext_f = 0.0 self._t_ext_f = 0.0
# 系统机械约束约束配置 # 系统机械约束配置
self._dw_max = 800.0 self._dw_max = 800.0
# 增量式PID控制器 (用于转速环功率控制) # MPC控制器 (用于转速环功率控制)
# output_scale: 额定功率300kWinput_scale: 额定转速575.95 rad/s self.mpc = MotorMPCController(
self.speed_pid = IncrementalPIDController( J=J, B_visc=B_visc, dt=0.02, horizon=10,
kp=k_p_w, ki=k_i_w, kd=k_d_w, W_speed=mpc_W_speed, W_dcost=mpc_W_dcost,
dt=0.02, # 将在step中动态更新 overshoot_limit=mpc_overshoot_limit
output_min=-self.P_rate/1000.0 * 1.2, # 允许放电
output_max=self.P_rate/1000.0 * 1.2, # 允许充电
input_scale=self.w_rate,
output_scale=self.P_rate/1000.0
) )
self._log = { self._log = {
@@ -208,7 +189,7 @@ class MotorSim:
} }
# 滤波系数缓存 (用于优化: 避免每步重复计算 exp(-dt/tau)) # 滤波系数缓存 (用于优化: 避免每步重复计算 exp(-dt/tau))
self._last_dt: float | None = None self._last_dt: Optional[float] = None
self._a_v: float = 0.0 # 母线电压滤波系数 self._a_v: float = 0.0 # 母线电压滤波系数
self._a_n: float = 0.0 # 转速参考滤波系数 self._a_n: float = 0.0 # 转速参考滤波系数
self._a_t_load: float = 0.0 # 负载转矩滤波系数 self._a_t_load: float = 0.0 # 负载转矩滤波系数
@@ -220,8 +201,6 @@ class MotorSim:
self._EPSILON: float = 1e-6 # 数值 epsilon,用于避免除零 self._EPSILON: float = 1e-6 # 数值 epsilon,用于避免除零
self._TAU_LOAD_FILTER: float = 0.05 # 负载转矩滤波时间常数 (s) self._TAU_LOAD_FILTER: float = 0.05 # 负载转矩滤波时间常数 (s)
self._TORQUE_OVERRATE: float = 1.35 # 转矩过载系数 self._TORQUE_OVERRATE: float = 1.35 # 转矩过载系数
self._W_ERROR_DEADBAND: float = 0.05 # 转速误差死区 (rad/s)
self._DW_ERROR_FILTER: float = 0.15 # 微分误差滤波系数
# 日志记录配置:可选的最大日志长度限制 (None 表示无限制) # 日志记录配置:可选的最大日志长度限制 (None 表示无限制)
self._log_maxlen: int | None = None # 可设置为如 100000 来限制内存使用 self._log_maxlen: int | None = None # 可设置为如 100000 来限制内存使用
@@ -380,10 +359,11 @@ class MotorSim:
w_abs = abs(w_M) w_abs = abs(w_M)
w_e = self.n_p * w_abs w_e = self.n_p * w_abs
if w_e < self._EPSILON: if w_e < 1e-6:
return float(max(0.0, abs(self._torque_from_currents(0.0, self.i_s_max)))) return float(max(0.0, abs(self._torque_from_currents(0.0, self.i_s_max))))
# 优化: 减少网格点数从40到12使用更智能的搜索策略 # 优化: 减少网格点数从40到12在保证精度的同时提高效率
# 使用更智能的搜索策略:从估计的MTPA点开始搜索
i_d_estimate = max(-self.i_s_max, min(0.0, -0.5 * (self.psi_f / (self.L_q - self.L_d)) if self.L_q != self.L_d else 0.0)) i_d_estimate = max(-self.i_s_max, min(0.0, -0.5 * (self.psi_f / (self.L_q - self.L_d)) if self.L_q != self.L_d else 0.0))
i_d_grid = np.array([ i_d_grid = np.array([
i_d_estimate - 0.3 * self.i_s_max, i_d_estimate - 0.3 * self.i_s_max,
@@ -407,20 +387,14 @@ class MotorSim:
tau_candidate = abs(self._torque_from_currents(i_d, i_q)) tau_candidate = abs(self._torque_from_currents(i_d, i_q))
if tau_candidate > tau_best: if tau_candidate > tau_best:
tau_best = tau_candidate tau_best = tau_candidate
if i_d < -0.01 * self.i_s_max: if i_d < -0.01 * self.i_s_max: # 继续搜索更负的i_d
continue continue
tau_p_max = self.P_rate / max(w_abs, self._W_MIN_RAD) tau_p_max = self.P_rate / max(w_abs, self._W_MIN_RAD)
return float(max(0.0, min(tau_best, tau_p_max, self._TORQUE_OVERRATE * self.tau_rate))) return float(max(0.0, min(tau_best, tau_p_max, self._TORQUE_OVERRATE * self.tau_rate)))
def _apply_power_slew(self, p_bus_req_kw: float, dt: float) -> float: def _apply_power_slew(self, p_bus_req_kw: float) -> float:
"""对母线请求功率施加斜率限制。""" """对母线请求功率施加斜率限制(当前实现为 passthrough"""
if self.p_bus_slew_rate_kw <= 0:
self._p_bus_req_prev = float(p_bus_req_kw)
return float(p_bus_req_kw)
delta = self.p_bus_slew_rate_kw * dt
self._p_bus_req_prev = float(p_bus_req_kw)
return float(p_bus_req_kw) return float(p_bus_req_kw)
def _power_to_torque_ref(self, p_bus_w: float, w_M: float) -> float: def _power_to_torque_ref(self, p_bus_w: float, w_M: float) -> float:
@@ -434,13 +408,13 @@ class MotorSim:
if abs(p_bus_w) < 1e-9: if abs(p_bus_w) < 1e-9:
return 0.0 return 0.0
w_eff = max(abs(w_M), 50.0) w_eff = max(abs(w_M), self._W_MIN_RAD)
if p_bus_w >= 0: if p_bus_w >= 0:
# 取电模式:从母线取有功,经过内部热量/电磁损耗后,输出转动推力(负转矩) # 取电模式:从母线取有功,经过内部热量/电磁损耗后,输出转动推力(负转矩)
return -(p_bus_w * self.eta_mot) / w_eff return -(p_bus_w * self.eta_mot) / w_eff
else: else:
# 发电模式:电机受到负负载强推,需要输出极大的正机械阻力(查表吸收电能) # 发电模式:电机受到负负载强推,需要输出极大的正机械阻力(查表吸收电能)
return -p_bus_w / (w_eff * max(self.eta_gen, 1e-6)) return -p_bus_w / (w_eff * max(self.eta_gen, self._EPSILON))
def _current_ref_from_torque(self, t_motor_ref: float, w_M: float, v_bus: float) -> tuple[float, float]: def _current_ref_from_torque(self, t_motor_ref: float, w_M: float, v_bus: float) -> tuple[float, float]:
""" """
@@ -468,12 +442,14 @@ class MotorSim:
iq_sign = 1.0 if i_q_ref >= 0 else -1.0 iq_sign = 1.0 if i_q_ref >= 0 else -1.0
iq_abs_target = abs(i_q_ref) iq_abs_target = abs(i_q_ref)
# 优化: 减少网格点数从80到20 # 优化: 减少网格点数从80到20,使用更高效的搜索
for i_d in np.linspace(-self.i_s_max, 0.0, 20): i_d_candidates = np.linspace(-self.i_s_max, 0.0, 20)
for i_d in i_d_candidates:
iq_max = self._max_iq_given_id(i_d, w_e, v_bus) iq_max = self._max_iq_given_id(i_d, w_e, v_bus)
if iq_max >= iq_abs_target: if iq_max >= iq_abs_target:
return float(i_d), float(iq_sign * iq_abs_target) return float(i_d), float(iq_sign * iq_abs_target)
# 如果没找到,使用最大弱磁点
i_d_fw = -self.i_s_max i_d_fw = -self.i_s_max
i_q_fw = iq_sign * self._max_iq_given_id(i_d_fw, w_e, v_bus) i_q_fw = iq_sign * self._max_iq_given_id(i_d_fw, w_e, v_bus)
return float(i_d_fw), float(i_q_fw) return float(i_d_fw), float(i_q_fw)
@@ -560,10 +536,10 @@ class MotorSim:
""" """
if dt <= 0: if dt <= 0:
raise ValueError('dt must be positive') raise ValueError('dt must be positive')
if v_bus <= self._EPSILON: if v_bus <= 1e-6:
raise ValueError('v_bus must be positive') raise ValueError('v_bus must be positive')
# 更新滤波系数缓存 # 更新滤波系数缓存 (仅当 dt 变化时重新计算)
self._update_filter_coefficients(dt) self._update_filter_coefficients(dt)
self._v_bus_f += (float(v_bus) - self._v_bus_f) * self._a_v self._v_bus_f += (float(v_bus) - self._v_bus_f) * self._a_v
@@ -576,51 +552,29 @@ class MotorSim:
self._w_set_f += (w_set_cmd - self._w_set_f) * self._a_n self._w_set_f += (w_set_cmd - self._w_set_f) * self._a_n
w_set = self._w_set_f w_set = self._w_set_f
w_error = w_set - self.w_M
if abs(w_error) < self._W_ERROR_DEADBAND:
w_error = 0.0
dw_error = (w_error - self._w_error_prev) / max(dt, self._EPSILON)
self._w_error_prev = w_error
self._dw_error_f = self._dw_error_f * (1 - self._DW_ERROR_FILTER) + dw_error * self._DW_ERROR_FILTER
self._w_error_int += w_error * dt
p_lim_kw = (t_lim * max(self.w_M, self._W_MIN_RAD)) / 1000.0 / max(self.eta_mot, self._EPSILON) p_lim_kw = (t_lim * max(self.w_M, self._W_MIN_RAD)) / 1000.0 / max(self.eta_mot, self._EPSILON)
int_limit = 1.2 * p_lim_kw / max(self.k_i_w, self._EPSILON) if self.k_i_w > 0 else 0.0
self._w_error_int = np.clip(self._w_error_int, -int_limit, int_limit)
self._t_load_f += (float(t_load) - self._t_load_f) * self._a_t_load self._t_load_f += (float(t_load) - self._t_load_f) * self._a_t_load
self._t_ext_f += (float(t_ext) - self._t_ext_f) * self._a_t_load self._t_ext_f += (float(t_ext) - self._t_ext_f) * self._a_t_load
# 阻力前馈折算为补偿所需电功率(kW)
t_ff = self._t_load_f + self._t_ext_f + self.B_visc * self.w_M
p_mech_ff_kw = (t_ff * max(self.w_M, 1e-3)) / 1000.0
if p_mech_ff_kw > 0:
p_elec_ff_kw = p_mech_ff_kw / max(self.eta_mot, self._EPSILON)
else:
p_elec_ff_kw = p_mech_ff_kw * self.eta_gen
# 阻力前馈折算为补偿所需电功率(kW)
t_ff = self._t_load_f + self._t_ext_f + self.B_visc * self.w_M
p_mech_ff_kw = (t_ff * max(self.w_M, 1e-3)) / 1000.0
if p_mech_ff_kw > 0:
p_elec_ff_kw = p_mech_ff_kw / max(self.eta_mot, 1e-6)
else:
p_elec_ff_kw = p_mech_ff_kw * self.eta_gen
# ------------------------------------------------------------- # -------------------------------------------------------------
# A) 需求生成层: 以转速环计算下一拍应向电池索取的 P_bus_req_kw # A) 需求生成层: 以转速环 MPC 计算下一拍应向电池索取的 P_bus_req_kw
# ------------------------------------------------------------- # -------------------------------------------------------------
# 使用增量式PID计算控制量 # 使用 MPC 控制器计算转矩指令,然后转换为功率请求
self.speed_pid.dt = dt t_motor_cmd = self.mpc.compute(
p_pid = self.speed_pid.compute(setpoint=w_set, measurement=self.w_M) current_w=self.w_M,
target_w=w_set,
t_load=self._t_load_f,
t_ext=self._t_ext_f,
t_lim_upper=t_lim,
t_lim_lower=-t_lim
)
# 添加微分前馈和阻力前馈 # 将 MPC 转矩指令转换为功率请求 (kW)
p_d_ff = self.k_d_w_error * self._dw_error_f # 正转矩(发电) -> 吸收功率,负转矩(驱动) -> 输出功率
p_cmd_raw = -t_motor_cmd * max(self.w_M, self._W_MIN_RAD) / 1000.0
p_cmd_raw = p_pid + p_d_ff + p_elec_ff_kw
p_bus_req_kw = float(np.clip(p_cmd_raw, -p_lim_kw, p_lim_kw)) p_bus_req_kw = float(np.clip(p_cmd_raw, -p_lim_kw, p_lim_kw))
p_bus_req_kw = self._apply_power_slew(p_bus_req_kw, dt) p_bus_req_kw = self._apply_power_slew(p_bus_req_kw)
self.p_bus_req_kw = p_bus_req_kw self.p_bus_req_kw = p_bus_req_kw
@@ -630,6 +584,47 @@ class MotorSim:
t_motor_ref = self._power_to_torque_ref(p_bus_actual_kw * 1000.0, self.w_M) t_motor_ref = self._power_to_torque_ref(p_bus_actual_kw * 1000.0, self.w_M)
t_motor_ref = float(np.clip(t_motor_ref, -t_lim, t_lim)) t_motor_ref = float(np.clip(t_motor_ref, -t_lim, t_lim))
# -------------------------------------------------------------
# 超调量硬约束: 基于转矩变化率限制
# 当接近目标转速时,限制转矩变化以防止超调
# -------------------------------------------------------------
w_target = w_set
w_max_allowed = w_target * (1 + self._overshoot_limit) # 105%
w_min_allowed = w_target * (1 - self._overshoot_limit) # 95%
# 记录上一时刻的转矩参考
t_motor_ref_prev = getattr(self, '_t_motor_ref_prev', 0.0)
if w_target > self._W_MIN_RAD:
# 升速时: 如果当前转速超过允许最大值,强制减速
if w_set > self.w_M and self.w_M > w_max_allowed:
t_motor_ref = -t_lim # 最大制动
# 降速时: 如果当前转速低于允许最小值,强制加速
elif w_set < self.w_M and self.w_M < w_min_allowed:
t_motor_ref = t_lim # 最大驱动
# 接近目标时 (95%-105% 区间): 限制转矩变化率
elif self.w_M > w_target * 0.90:
# 计算允许的最大转矩变化
# 使转速不超过 w_max_allowed 的最大加速度
max_allowed_dw = (w_max_allowed - self.w_M) / dt
max_allowed_T = -self.J * max_allowed_dw - t_load - t_ext - self.B_visc * self.w_M
# 限制正转矩 (驱动) 不超过计算值
if t_motor_ref < 0: # 驱动扭矩
t_motor_ref = max(t_motor_ref, max_allowed_T)
# 使转速不低于 w_min_allowed 的最小加速度
min_allowed_dw = (w_min_allowed - self.w_M) / dt
min_allowed_T = -self.J * min_allowed_dw - t_load - t_ext - self.B_visc * self.w_M
# 限制负转矩 (制动) 不超过计算值
if t_motor_ref > 0: # 制动扭矩
t_motor_ref = min(t_motor_ref, min_allowed_T)
self._t_motor_ref_prev = t_motor_ref
self._t_ref_f += (t_motor_ref - self._t_ref_f) * self._a_t self._t_ref_f += (t_motor_ref - self._t_ref_f) * self._a_t
t_motor_ref_filtered = float(self._t_ref_f) t_motor_ref_filtered = float(self._t_ref_f)
@@ -760,12 +755,8 @@ class MotorSim:
self.u_an, self.u_bn, self.u_cn = 0.0, 0.0, 0.0 self.u_an, self.u_bn, self.u_cn = 0.0, 0.0, 0.0
self.duty_a, self.duty_b, self.duty_c = 0.5, 0.5, 0.5 self.duty_a, self.duty_b, self.duty_c = 0.5, 0.5, 0.5
self._sim_t = 0.0 self._sim_t = 0.0
# 增量式PID控制器重置 # MPC控制器重置
self.speed_pid.reset(initial_output=0.0) self.mpc.reset(initial_w=self.w_M)
self._w_error_int = 0.0
self._w_error_prev = 0.0
self._dw_error_f = 0.0
self._p_bus_req_prev = 0.0
self._w_set_f = 0.0 self._w_set_f = 0.0
self._t_ref_f = 0.0 self._t_ref_f = 0.0
self._v_bus_f = float(self.u_dc) self._v_bus_f = float(self.u_dc)
@@ -774,231 +765,6 @@ class MotorSim:
self._t_ext_f = 0.0 self._t_ext_f = 0.0
def run_motor_test() -> Dict[str, Any]:
"""基于接口的独立电机测试 - 复杂测试用例"""
import matplotlib.pyplot as plt
print('=' * 60)
print('Motor Dynamic Test - Complex Profile')
print('=' * 60)
# 使用优化后的PID参数
motor = MotorSim(
P_rate=300e3,
w_rate=575.95,
J=0.8,
k_p_w=13.440362,
k_i_w=42.997816,
k_d_w=0.484660,
P_const_loss=300.0,
k_fe=4e-4,
)
dt = 0.02
t_end = 100.0
time_array = np.arange(0.0, t_end, dt)
# 存储关键诊断数据
torque_balance = []
power_balance = []
p_actual_kw = 0.0
for t in time_array:
# 复杂测试工况 - 多段转速变化 + 多种扰动
# 目标转速曲线
if t < 3.0:
n_setpoint = 0.0
elif t < 8.0:
n_setpoint = 2000.0
elif t < 15.0:
n_setpoint = 2800.0
elif t < 22.0:
n_setpoint = 2200.0
elif t < 32.0:
n_setpoint = 3500.0
elif t < 42.0:
n_setpoint = 3800.0
elif t < 52.0:
n_setpoint = 2800.0
elif t < 62.0:
n_setpoint = 3000.0
elif t < 72.0:
n_setpoint = 4000.0
elif t < 82.0:
n_setpoint = 4200.0
elif t < 92.0:
n_setpoint = 3200.0
else:
n_setpoint = 3000.0
# 外部转矩扰动
if t < 5.0:
t_ext = 25.0
elif t < 12.0:
t_ext = 10.0
elif t < 18.0:
t_ext = -30.0
elif t < 25.0:
t_ext = 15.0
elif t < 35.0:
t_ext = -50.0
elif t < 45.0:
t_ext = 8.0
elif t < 55.0:
t_ext = -20.0
elif t < 65.0:
t_ext = 25.0
elif t < 75.0:
t_ext = -60.0
elif t < 85.0:
t_ext = 20.0
else:
t_ext = 0.0
# 负载转矩 - 变化因子
if t < 10.0:
t_load_factor = 1.0
elif t < 22.0:
t_load_factor = 1.5
elif t < 32.0:
t_load_factor = 0.8
elif t < 42.0:
t_load_factor = 1.8
elif t < 52.0:
t_load_factor = 1.0
elif t < 62.0:
t_load_factor = 1.4
elif t < 72.0:
t_load_factor = 0.9
elif t < 82.0:
t_load_factor = 1.6
else:
t_load_factor = 1.1
base_t_load = 16.0 + 0.020 * motor.w_M + 1.2e-5 * motor.w_M**2
t_load = base_t_load * t_load_factor
# 母线电压波动
v_bus = 520.0 + 15.0 * np.sin(2 * np.pi * t / 8.0) + 5.0 * np.sin(2 * np.pi * t / 3.0)
state = motor.step(dt, n_setpoint, p_actual_kw, v_bus, t_load, t_ext)
p_actual_kw = state['p_bus_req_kw']
torque_balance.append(state['torque_balance_error'])
power_balance.append(state['power_balance_error'])
state = motor.get_state()
p_bus_kw = np.array(motor._log['p_bus_kw'])
p_req_kw = np.array(motor._log['p_bus_req_kw'])
p_shaft_kw = np.array(motor._log['p_shaft_kw'])
p_loss_kw = np.array(motor._log['p_loss_kw'])
n_log = np.array(motor._log['n_rpm'])
t_motor_log = np.array(motor._log['t_motor'])
t_load_log = np.array(motor._log['t_load'])
t_ext_log = np.array(motor._log['t_ext'])
v_bus_log = np.array(motor._log['v_bus'])
v_motor_log = np.array(motor._log['v_motor'])
i_bus_log = np.array(motor._log['i_bus_a'])
# 计算性能指标
n_setpoints = []
for t in time_array:
if t < 3.0:
n_setpoints.append(0.0)
elif t < 8.0:
n_setpoints.append(2000.0)
elif t < 15.0:
n_setpoints.append(2800.0)
elif t < 22.0:
n_setpoints.append(2200.0)
elif t < 32.0:
n_setpoints.append(3500.0)
elif t < 42.0:
n_setpoints.append(3800.0)
elif t < 52.0:
n_setpoints.append(2800.0)
elif t < 62.0:
n_setpoints.append(3000.0)
elif t < 72.0:
n_setpoints.append(4000.0)
elif t < 82.0:
n_setpoints.append(4200.0)
elif t < 92.0:
n_setpoints.append(3200.0)
else:
n_setpoints.append(3000.0)
n_setpoints = np.array(n_setpoints)
error = n_setpoints - n_log
ise = np.sum(error**2) * dt
iae = np.sum(np.abs(error)) * dt
print(f"Final: speed={state['n_rpm']:.0f} RPM ({state['w_rad_s']:.1f} rad/s)")
print(f" t_motor={state['t_motor']:.2f} Nm, p_bus={state['p_bus_kw']:.2f} kW, v_motor={state['v_motor']:.1f} V")
print(f"Performance - ISE: {ise:.2f}, IAE: {iae:.2f}")
# 诊断结果
torque_balance = np.array(torque_balance)
power_balance = np.array(power_balance)
print(f"Diagnostic - Torque balance error (mean): {np.mean(np.abs(torque_balance)):.4f} Nm")
print(f"Diagnostic - Power balance error (mean): {np.mean(np.abs(power_balance)):.4f} W")
fig, axes = plt.subplots(5, 1, figsize=(14, 14), sharex=True)
axes[0].plot(time_array, n_setpoints, 'r--', lw=1.0, alpha=0.7, label='Setpoint')
axes[0].plot(time_array, n_log, 'b-', lw=1.6, label='Actual')
axes[0].set_ylabel('Speed [RPM]')
axes[0].set_title('Motor Speed - Complex Profile Test (100s)')
axes[0].grid(True, linestyle=':')
axes[0].legend()
axes[1].plot(time_array, t_motor_log, 'r-', lw=1.6, label='Motor Torque')
axes[1].plot(time_array, t_load_log, 'k--', lw=1.0, label='Load Torque')
axes[1].plot(time_array, t_ext_log, color='tab:purple', lw=1.0, label='External Torque')
axes[1].set_ylabel('Torque [Nm]')
axes[1].set_title('Torque Balance')
axes[1].axhline(0.0, color='k', lw=0.6)
axes[1].grid(True, linestyle=':')
axes[1].legend()
axes[2].plot(time_array, p_req_kw, color='tab:gray', lw=1.2, label='Requested Bus Power')
axes[2].plot(time_array, p_bus_kw, color='m', lw=1.4, label='Actual Bus Power')
axes[2].plot(time_array, p_shaft_kw, 'g-', lw=1.2, label='Shaft Power')
axes[2].plot(time_array, p_loss_kw, color='tab:orange', lw=1.0, label='Loss Power')
axes[2].set_ylabel('Power [kW]')
axes[2].set_title('Bus / Shaft / Loss Power')
axes[2].axhline(0.0, color='k', lw=0.6)
axes[2].grid(True, linestyle=':')
axes[2].legend()
axes[3].plot(time_array, v_bus_log, color='tab:cyan', lw=1.4, label='Bus Voltage')
axes[3].plot(time_array, v_motor_log, color='tab:red', lw=1.2, label='Motor Potential')
axes[3].set_ylabel('Voltage [V]')
axes[3].set_title('Bus Voltage and Motor Equivalent Potential')
axes[3].grid(True, linestyle=':')
axes[3].legend()
axes[4].plot(time_array, i_bus_log, color='tab:brown', lw=1.4, label='Bus Current')
axes[4].set_ylabel('Current [A]')
axes[4].set_xlabel('Time [s]')
axes[4].set_title('Bus Current')
axes[4].grid(True, linestyle=':')
axes[4].legend()
plt.tight_layout()
plt.savefig("figures/test_complex_motor_output.png")
return {
'final_state': state,
'peak_speed_rpm': float(np.max(n_log)),
'peak_bus_current_a': float(np.max(i_bus_log)),
'torque_balance_error_mean': float(np.mean(np.abs(torque_balance))),
'power_balance_error_mean': float(np.mean(np.abs(power_balance))),
'ise': ise,
'iae': iae,
}
def run_motor_battery_coupled_test() -> Dict[str, Any]: def run_motor_battery_coupled_test() -> Dict[str, Any]:
"""执行混合动力电机与电池协同闭环抗扰动验证例程 - 复杂测试用例""" """执行混合动力电机与电池协同闭环抗扰动验证例程 - 复杂测试用例"""
import matplotlib import matplotlib
@@ -1019,12 +785,11 @@ def run_motor_battery_coupled_test() -> Dict[str, Any]:
P_rate=300e3, P_rate=300e3,
w_rate=575.95, w_rate=575.95,
J=0.8, J=0.8,
k_p_w=13.440362,
k_i_w=42.997816,
k_d_w=0.484660,
P_const_loss=300.0, P_const_loss=300.0,
k_fe=4e-4, k_fe=4e-4,
u_dc=battery.V_t, u_dc=battery.V_t,
mpc_W_speed=100,
mpc_W_dcost=12,
) )
dt = 0.02 dt = 0.02
@@ -1041,36 +806,60 @@ def run_motor_battery_coupled_test() -> Dict[str, Any]:
t_ext_log = [] t_ext_log = []
t_load_log = [] t_load_log = []
# 螺旋桨负载模型参数
k_drag = 1.5e-6 # 气动阻力系数 (与转速平方成正比)
J_prop = 0.15 # 螺旋桨等效转动惯量 (kg·m²)
T_min = 10.0 # 最小阻力矩 (Nm)
p_actual_kw = 0.0 p_actual_kw = 0.0
for t in time_array: n_prev = 0.0 # 上一时刻转速 (RPM)
w_prev = 0.0 # 上一时刻角速度 (rad/s)
for t in tqdm(time_array, desc="Coupled Test"):
# 复杂测试工况 - 多段转速变化 + 多种扰动 # 复杂测试工况 - 多段转速变化 + 多种扰动
p_ext_elec = 0.0 # 外部电功率扰动 (kW) p_ext_elec = 0.0 # 外部电功率扰动 (kW)
# 目标转速曲线 - 复杂多段变化 # 目标转速曲线 - 包含阶跃、斜坡、正弦波等复杂信号
if t < 3.0: if t < 3.0:
n_setpoint = 0.0 n_setpoint = 0.0
elif t < 8.0: elif t < 8.0:
n_setpoint = 2000.0 # 斜坡上升:从0到2000 RPM
n_setpoint = 2000.0 * (t - 3.0) / 5.0
elif t < 15.0: elif t < 15.0:
# 阶跃
n_setpoint = 2800.0 n_setpoint = 2800.0
elif t < 22.0: elif t < 22.0:
n_setpoint = 2200.0 # 正弦波动:2800 ± 600 RPM,周期10s
n_setpoint = 2800.0 + 600.0 * np.sin(2 * np.pi * (t - 15.0) / 10.0)
elif t < 32.0: elif t < 32.0:
n_setpoint = 3500.0 # 斜坡上升 + 正弦波动
base = 2200.0 + 1300.0 * (t - 22.0) / 10.0 # 斜坡上升
n_setpoint = base + 300.0 * np.sin(2 * np.pi * (t - 22.0) / 8.0)
elif t < 42.0: elif t < 42.0:
# 阶跃到3800
n_setpoint = 3800.0 n_setpoint = 3800.0
elif t < 52.0: elif t < 52.0:
n_setpoint = 2800.0 # 斜坡下降 + 正弦波动
base = 3800.0 - 1000.0 * (t - 42.0) / 10.0 # 斜坡下降
n_setpoint = base + 200.0 * np.sin(2 * np.pi * (t - 42.0) / 6.0)
elif t < 62.0: elif t < 62.0:
n_setpoint = 3000.0 # 梯形波:先上升再保持
elif t < 72.0: if t < 55.0:
n_setpoint = 4000.0 n_setpoint = 2800.0 + 400.0 * (t - 52.0) / 3.0
elif t < 82.0:
n_setpoint = 4200.0
elif t < 92.0:
n_setpoint = 3200.0
else: else:
n_setpoint = 3000.0 n_setpoint = 3200.0
elif t < 72.0:
# 正弦波动:3000 ± 1000 RPM
n_setpoint = 3000.0 + 1000.0 * np.sin(2 * np.pi * (t - 62.0) / 10.0)
elif t < 82.0:
# 斜坡上升
n_setpoint = 3000.0 + 600.0 * (t - 72.0) / 10.0
elif t < 92.0:
# 衰减正弦波
amplitude = 400.0 * np.exp(-(t - 82.0) / 5.0)
n_setpoint = 3200.0 + amplitude * np.sin(2 * np.pi * (t - 82.0) / 8.0)
else:
# 斜坡下降到3000
n_setpoint = 3200.0 - 200.0 * (t - 92.0) / 8.0
# 外部转矩扰动 - 模拟发动机并联/涡轴输出变化 # 外部转矩扰动 - 模拟发动机并联/涡轴输出变化
if t < 5.0: if t < 5.0:
@@ -1096,38 +885,61 @@ def run_motor_battery_coupled_test() -> Dict[str, Any]:
else: else:
t_ext = 0.0 t_ext = 0.0
# 负载转矩 - 模拟风速/气压变化 # 螺旋桨负载模型:气动阻力矩 + 惯性负载
if t < 10.0: # 气动阻力: T = k_drag * n² (与转速平方成正比)
t_load = 20.0 # 惯性负载: T = J_prop * alpha (与加速度成正比,模拟螺旋桨惯性)
elif t < 22.0: n_current = n_setpoint # 使用目标转速计算负载
t_load = 80.0 w_current = n_current * 2 * np.pi / 60.0 # rad/s
elif t < 32.0:
t_load = 40.0
elif t < 42.0:
t_load = 120.0
elif t < 52.0:
t_load = 50.0
elif t < 62.0:
t_load = 90.0
elif t < 72.0:
t_load = 60.0
elif t < 82.0:
t_load = 130.0
else:
t_load = 70.0
# 外部电功率扰动 # 计算角加速度 (rad/s²)
if 35.0 < t < 40.0: alpha = (w_current - w_prev) / dt if dt > 0 else 0.0
# 气动阻力矩 (与转速平方成正比)
t_aero = k_drag * n_current ** 2 + T_min
# 惯性负载 (与加速度成正比)
t_inertia = J_prop * alpha
t_load = t_aero + t_inertia
# 更新上一时刻转速
w_prev = w_current
# 外部电功率扰动 (发动机/燃气轮机发电功率注入,正值表示充电,负值表示负载)
if 5.0 < t < 10.0:
p_ext_elec = 40.0 # 涡轴发电机注入 40kW
elif 10.0 < t < 15.0:
p_ext_elec = 50.0 # 涡轴发电机注入 50kW
elif 20.0 < t < 23.0:
p_ext_elec = 60.0 # 涡轴发电机注入 60kW
elif 25.0 < t < 30.0:
p_ext_elec = 70.0 # 涡轴发电机注入 70kW
elif 32.0 < t < 35.0:
p_ext_elec = 55.0 # 涡轴发电机注入 55kW
elif 35.0 < t < 40.0:
p_ext_elec = 80.0 # 涡轴发电机注入 80kW p_ext_elec = 80.0 # 涡轴发电机注入 80kW
elif 45.0 < t < 48.0:
p_ext_elec = 90.0 # 涡轴发电机注入 90kW (短时高功率充电)
elif 50.0 < t < 53.0:
p_ext_elec = 45.0 # 涡轴发电机注入 45kW
elif 55.0 < t < 60.0: elif 55.0 < t < 60.0:
p_ext_elec = -100.0 # 其他设备用电 100kW p_ext_elec = -100.0 # 其他设备用电 100kW (负载)
elif 62.0 < t < 65.0:
p_ext_elec = 50.0 # 涡轴发电机注入 50kW
elif 65.0 < t < 70.0:
p_ext_elec = 65.0 # 涡轴发电机注入 65kW
elif 72.0 < t < 75.0:
p_ext_elec = 55.0 # 涡轴发电机注入 55kW
elif 75.0 < t < 80.0: elif 75.0 < t < 80.0:
p_ext_elec = 60.0 # 涡轴发电机注入 60kW p_ext_elec = 60.0 # 涡轴发电机注入 60kW
elif 82.0 < t < 85.0:
p_ext_elec = 70.0 # 涡轴发电机注入 70kW
elif 88.0 < t < 95.0:
p_ext_elec = 75.0 # 涡轴发电机注入 75kW
# 母线电压波动 # 母线电压波动
v_bus = battery.V_t + 15.0 * np.sin(2 * np.pi * t / 8.0) + 5.0 * np.sin(2 * np.pi * t / 3.0) v_bus = battery.V_t + 15.0 * np.sin(2 * np.pi * t / 8.0) + 5.0 * np.sin(2 * np.pi * t / 3.0)
# 延迟一拍的实际电能参与当前动态步,进而PID在内环打出对下一拍的请求 # 延迟一拍的实际电能参与当前动态步,进而MPC在内环打出对下一拍的请求
state = motor.step(dt, n_setpoint, p_actual_kw, v_bus, t_load, t_ext) state = motor.step(dt, n_setpoint, p_actual_kw, v_bus, t_load, t_ext)
p_req_kw = state['p_bus_req_kw'] p_req_kw = state['p_bus_req_kw']
@@ -1174,7 +986,7 @@ def run_motor_battery_coupled_test() -> Dict[str, Any]:
axes[0].legend() axes[0].legend()
# 2. 转矩响应 # 2. 转矩响应
axes[1].plot(time_array, t_motor_log, 'r-', lw=1.6, label='Motor Torque (PID Output)') axes[1].plot(time_array, t_motor_log, 'r-', lw=1.6, label='Motor Torque (MPC Output)')
axes[1].plot(time_array, np.array(t_load_log), 'k--', lw=1.2, label='Propeller Load Torque') axes[1].plot(time_array, np.array(t_load_log), 'k--', lw=1.2, label='Propeller Load Torque')
axes[1].plot(time_array, np.array(t_ext_log), color='tab:purple', lw=1.2, label='External Torque') axes[1].plot(time_array, np.array(t_ext_log), color='tab:purple', lw=1.2, label='External Torque')
axes[1].axhline(0.0, color='k', lw=0.6) axes[1].axhline(0.0, color='k', lw=0.6)
@@ -1210,7 +1022,7 @@ def run_motor_battery_coupled_test() -> Dict[str, Any]:
axes[4].legend() axes[4].legend()
plt.tight_layout() plt.tight_layout()
plt.savefig("figures/test_complex_output.png") plt.savefig("figures/test_complex_output.png", dpi=300, bbox_inches='tight')
print("Plot saved to figures/test_complex_output.png") print("Plot saved to figures/test_complex_output.png")
return { return {
+298
View File
@@ -0,0 +1,298 @@
import numpy as np
# ==============================================================================
# Pure-Python projected gradient descent for box-constrained optimization
# (替代 scipy.optimize.minimize SLSQP,避免 Fortran ABI 兼容性问题)
# ==============================================================================
def _minimize_box(objective, x0, bounds, lr=1.0, max_iter=80, ftol=1e-6):
"""
Projected gradient descent with Armijo backtracking line search.
For small-to-medium MPC horizons (H ≤ 30) this is fast enough.
"""
n = len(x0)
lb = np.array([b[0] for b in bounds], dtype=np.float64)
ub = np.array([b[1] for b in bounds], dtype=np.float64)
x = np.clip(np.array(x0, dtype=np.float64), lb, ub)
eps = 1e-5 # finite-difference step
f_prev = objective(x)
for _it in range(max_iter):
# Approximate gradient via central differences
grad = np.empty(n, dtype=np.float64)
for i in range(n):
x_p = x.copy(); x_p[i] += eps
x_m = x.copy(); x_m[i] -= eps
grad[i] = (objective(x_p) - objective(x_m)) / (2 * eps)
# Backtracking line search (Armijo condition)
step = lr
for _ in range(12):
x_new = np.clip(x - step * grad, lb, ub)
f_new = objective(x_new)
if f_new < f_prev - 1e-4 * step * np.dot(grad, x - x_new):
break
step *= 0.5
else:
x_new = np.clip(x - step * grad, lb, ub)
f_new = objective(x_new)
if abs(f_prev - f_new) < ftol:
x = x_new
break
x = x_new
f_prev = f_new
class _Result:
pass
res = _Result()
res.x = x
res.fun = f_prev
res.success = True
return res
# ==============================================================================
# 1. 涡轴发动机 MPC 控制器 (基于 Scipy SLSQP 高速求解)
# ==============================================================================
class TurboShaftMPCController:
def __init__(self, tau_fuel, K_inertia, dt=0.02, horizon=15,
min_fuel=10.0, max_fuel=600.0, overshoot_limit=0.05):
"""
初始化高速 MPC 控制器 (基于 Scipy SLSQP)
替代 GEKKO 以消除文件 I/O 开销,提升单步推理速度 50x 以上。
:param overshoot_limit: 超调量硬约束 (0.05 = 5%)
"""
self.dt = dt
self.H = horizon
self.tau_fuel = tau_fuel
self.K_inertia = K_inertia
self.min_fuel = min_fuel
self.max_fuel = max_fuel
# 权重参数 (与 GEKKO 版本保持一致)
self.W_power = 200.0 # Power Setpoint Weight
self.W_dcost = 1.5 # Delta Control Weight (DCOST)
# 超调量约束
self.overshoot_limit = overshoot_limit # 5% 硬约束
# 超调惩罚权重 (使用很大的值使约束"硬"化)
self.W_overshoot_penalty = 1e6
# 状态缓存
self.last_u = 100.0
self.last_N = 0.0
def reset(self, initial_output, initial_N):
self.last_u = initial_output
self.last_N = initial_N
def compute(self, current_N, current_Wfact, target_power, engine_model=None, H_env=0, Ma_env=0, precalc_params=None, **kwargs):
"""
计算 MPC 控制律
:param precalc_params: (必须) 元组 (Wf_req_0, Power_0, k_wf, k_p)。
FastMPC 必须配合 Batch Prediction 使用。
"""
if precalc_params is not None:
Wf_req_0, Power_0, k_wf, k_p = precalc_params
else:
raise ValueError("FastMPC 必须配合 Batch Prediction 使用 (提供 precalc_params)")
N0 = current_N
Wf_act0 = current_Wfact
N_ref = N0 # 用于线性化的参考点
# --- 构建线性预测模型 ---
# State x = [N, Wf_act]
# x_{k+1} = A x_k + B u_k + d
# N_{k+1} = N_k + dt * K * (Wf_act_k - (Wf_req_0 + k_wf*(N_k - N0)))
# = (1 - dt*K*k_wf) N_k + (dt*K) Wf_act_k + dt*K*(k_wf*N0 - Wf_req_0)
# Wf_act_{k+1} = Wf_act_k + dt * (u_k - Wf_act_k) / tau
# = (1 - dt/tau) Wf_act_k + (dt/tau) u_k
dt = self.dt
K = self.K_inertia
tau = self.tau_fuel
A = np.array([
[1 - dt * K * k_wf, dt * K],
[0, 1 - dt / tau]
])
B = np.array([0, dt / tau])
d = np.array([dt * K * (k_wf * N_ref - Wf_req_0), 0])
# Power Output: P = P0 + k_p * (N - N0)
# = k_p * N + (P0 - k_p * N0)
C_p = k_p
D_p = Power_0 - k_p * N_ref
# --- 超调量硬约束 ---
# 升功率时: P_k <= target_power * (1 + overshoot_limit)
# 降功率时: P_k >= target_power * (1 - overshoot_limit)
P_max = target_power * (1 + self.overshoot_limit)
P_min = target_power * (1 - self.overshoot_limit)
# 判断是升功率还是降功率
is_ramping_up = target_power > Power_0
# --- 优化目标函数 ---
# Variables: U = [u_0, ..., u_{H-1}]
# x_0 is fixed.
# Cost = sum_{k=1 to H} W_power * (P_k - P_target)^2 + sum_{k=0 to H-1} W_dcost * (u_k - u_{k-1})^2
# + W_overshoot_penalty * (违反约束的惩罚)
def objective(U):
cost = 0.0
x_k = np.array([N0, Wf_act0])
u_prev = self.last_u # Use last commanded u for the first delta
for k in range(self.H):
u_k = U[k]
# Dynamics Step
x_k = A @ x_k + B * u_k + d
# Output
P_k = C_p * x_k[0] + D_p
# Cost Accumulation
cost += self.W_power * (P_k - target_power) ** 2
cost += self.W_dcost * (u_k - u_prev) ** 2
# 超调惩罚: 如果违反约束,添加巨大惩罚
if is_ramping_up:
if P_k > P_max:
cost += self.W_overshoot_penalty * (P_k - P_max) ** 2
else:
if P_k < P_min:
cost += self.W_overshoot_penalty * (P_min - P_k) ** 2
u_prev = u_k
return cost
# --- 求解 ---
# 初始猜测: 保持上一次的输入
U0 = np.full(self.H, float(self.last_u), dtype=np.float64)
# 约束: lb <= u <= ub
bounds = [(self.min_fuel, self.max_fuel) for _ in range(self.H)]
# 使用纯Python投影梯度下降求解带约束优化 (替代 SLSQP)
res = _minimize_box(objective, U0, bounds, lr=5.0, max_iter=60, ftol=1e-3)
# 更新状态
u_opt = res.x[0]
self.last_u = u_opt
return u_opt
# ==============================================================================
# 2. 驱动电机 MPC 控制器 (基于 Scipy SLSQP 高速求解)
# ==============================================================================
class MotorMPCController:
def __init__(self, J, B_visc, dt=0.02, horizon=10, W_speed=100.0, W_dcost=1.0, overshoot_limit=0.05):
"""
初始化高速电机 MPC 控制器 (基于 Scipy SLSQP)
替代 GEKKO 以消除文件 I/O 开销,提升单步推理速度 50x 以上。
:param overshoot_limit: 超调量硬约束 (0.05 = 5%)
"""
self.dt = dt
self.H = horizon
self.J = J
self.B_visc = B_visc
# 权重参数 (可调)
self.W_speed = W_speed # 转速跟踪权重
self.W_dcost = W_dcost # 控制变化惩罚
# 超调量约束
self.overshoot_limit = overshoot_limit # 5% 硬约束
# 超调惩罚权重 (使用很大的值使约束"硬"化)
self.W_overshoot_penalty = 1e6
# 状态缓存
self.last_T_cmd = 0.0
def reset(self, initial_w=0.0):
"""重新初始化状态"""
self.last_T_cmd = 0.0
def compute(self, current_w, target_w, t_load, t_ext, t_lim_upper, t_lim_lower):
"""
执行单步 MPC 优化计算
:param current_w: 当前实际转速 (rad/s)
:param target_w: 目标转速 (rad/s)
:param t_load: 当前气动负载转矩 (Nm)
:param t_ext: 当前外部轴系转矩 (Nm)
:param t_lim_upper: 当前母线电压下,电机能发出的【正向转矩上限】(发电极限)
:param t_lim_lower: 当前母线电压下,电机能发出的【负向转矩下限】(驱动极限)
:return: 最优转矩指令 (Nm)
"""
w0 = current_w
dt = self.dt
J = self.J
B = self.B_visc
# --- 离散线性模型 ---
# w_{k+1} = w_k + dt * (-(T_cmd + T_ext + T_load + B*w_k) / J)
# = (1 - dt*B/J) * w_k - dt/J * T_cmd - dt/J * (T_ext + T_load)
a = 1 - dt * B / J
b = -dt / J
d = -dt / J * (t_ext + t_load)
# --- 超调量硬约束 ---
# 升速时: w_k <= target_w * (1 + overshoot_limit)
# 降速时: w_k >= target_w * (1 - overshoot_limit)
w_max = target_w * (1 + self.overshoot_limit)
w_min = target_w * (1 - self.overshoot_limit)
# 判断是升速还是降速
is_ramping_up = target_w > w0
# --- 优化目标函数 ---
# Cost = sum_{k=1 to H} W_speed * (w_k - target_w)^2 + sum_{k=0 to H-1} W_dcost * (T_k - T_{k-1})^2
# + W_overshoot_penalty * (违反约束的惩罚)
def objective(U):
cost = 0.0
w_k = w0
T_prev = self.last_T_cmd
for k in range(self.H):
T_k = U[k]
w_k = a * w_k + b * T_k + d # 动力学迭代
cost += self.W_speed * (w_k - target_w) ** 2
cost += self.W_dcost * (T_k - T_prev) ** 2
# 超调惩罚: 如果违反约束,添加巨大惩罚
if is_ramping_up:
if w_k > w_max:
cost += self.W_overshoot_penalty * (w_k - w_max) ** 2
else:
if w_k < w_min:
cost += self.W_overshoot_penalty * (w_min - w_k) ** 2
T_prev = T_k
return cost
# 初始猜测
U0 = np.full(self.H, float(self.last_T_cmd), dtype=np.float64)
# 约束: 转矩边界
bounds = [(t_lim_lower, t_lim_upper) for _ in range(self.H)]
# 求解 (使用纯Python投影梯度下降,替代 SLSQP)
res = _minimize_box(objective, U0, bounds, lr=50.0, max_iter=120, ftol=1e-5)
T_opt = res.x[0]
self.last_T_cmd = T_opt
return T_opt
+9 -19
View File
@@ -1,6 +1,6 @@
import os
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from tqdm import tqdm
from engine_dynamic_sim import TurboshaftDynamicSim from engine_dynamic_sim import TurboshaftDynamicSim
from motor_sim import MotorSim from motor_sim import MotorSim
@@ -10,25 +10,15 @@ class SeriesHybridSystem:
""" """
串联式混合动力系统总成 串联式混合动力系统总成
""" """
def __init__(self): def __init__(self, mpc_overshoot_limit: float = 0.05):
# ===== 新增:按Model目录定位GPR数据与权重,避免从主项目调用时路径错误 ===== self.genset = TurboshaftDynamicSim(mpc_overshoot_limit=mpc_overshoot_limit)
model_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
gpr_csv_path = os.path.join(model_root, "data", "Cleaned_Engine_Data_Full.csv")
gpr_pth_path = os.path.join(model_root, "data", "engine_gpr_model.pth")
self.genset = TurboshaftDynamicSim(
gpr_csv_path=gpr_csv_path,
gpr_pth_path=gpr_pth_path,
kp=0.5,
ki=0.5,
kd=0.0
)
self.drive_motor = MotorSim( self.drive_motor = MotorSim(
P_rate=300e3, P_rate=300e3,
w_rate=575.95, w_rate=575.95,
k_p_w=5.0, # 增大比例增益
k_i_w=2.0, # 增大积分增益
k_d_w=0.5, # 适度微分
J=1.0, J=1.0,
mpc_W_speed=40,
mpc_W_dcost=5,
mpc_overshoot_limit=mpc_overshoot_limit, # 超调量硬约束 (5%)
) )
self.battery = BatterySim(capacity_kwh=50.0, initial_soc=0.6) self.battery = BatterySim(capacity_kwh=50.0, initial_soc=0.6)
@@ -172,7 +162,7 @@ if __name__ == "__main__":
'p_batt_actual_kw', 'wf_kg_h']} 'p_batt_actual_kw', 'wf_kg_h']}
print("-> 开始全系统闭环步进仿真 (总时长 3 分钟)...") print("-> 开始全系统闭环步进仿真 (总时长 3 分钟)...")
for t in time_array: for t in tqdm(time_array, desc='Simulating', unit='step'):
# 3分钟测试剖面 # 3分钟测试剖面
if t < 15.0: if t < 15.0:
target_rpm, load_torque = 1500.0, 50.0 # 地面滑行 target_rpm, load_torque = 1500.0, 50.0 # 地面滑行
@@ -204,7 +194,7 @@ if __name__ == "__main__":
# 2. 功率分配流向 # 2. 功率分配流向
axes[1].plot(time_array, log['p_drive_req_kw'], 'k--', lw=1.5, label='Drive Motor Request') axes[1].plot(time_array, log['p_drive_req_kw'], 'k--', lw=1.5, label='Drive Motor Request')
axes[1].plot(time_array, log['p_engine_out_kw'], 'r-', lw=1.5, label='Engine Output (APU)') axes[1].plot(time_array, log['p_engine_out_kw'], 'r-', lw=1.5, label='Engine Output (Turbo Shaft)')
axes[1].plot(time_array, log['p_batt_actual_kw'], 'g-', lw=1.5, label='Battery Output') axes[1].plot(time_array, log['p_batt_actual_kw'], 'g-', lw=1.5, label='Battery Output')
axes[1].set_ylabel('Power [kW]') axes[1].set_ylabel('Power [kW]')
axes[1].set_title('System Power Flow (Energy Management)') axes[1].set_title('System Power Flow (Energy Management)')
@@ -249,7 +239,7 @@ if __name__ == "__main__":
os.makedirs(data_dir) os.makedirs(data_dir)
dat_path = os.path.join(data_dir, 'series_hybrid_data.dat') dat_path = os.path.join(data_dir, 'series_hybrid_data.dat')
with open(dat_path, 'w') as f: with open(dat_path, 'w', encoding='utf-8') as f:
f.write('# Time(s)\tTarget_RPM\tActual_RPM\tError_RPM\tSOC\tEngine_Power\tDrive_Req\tBatt_Power\n') f.write('# Time(s)\tTarget_RPM\tActual_RPM\tError_RPM\tSOC\tEngine_Power\tDrive_Req\tBatt_Power\n')
for i in range(len(time_array)): for i in range(len(time_array)):
err = log['target_prop_rpm'][i] - log['prop_speed_rpm'][i] err = log['target_prop_rpm'][i] - log['prop_speed_rpm'][i]
+255 -69
View File
@@ -1,3 +1,4 @@
import os
import gradio as gr import gradio as gr
import time import time
from functools import partial from functools import partial
@@ -10,8 +11,8 @@ from analysis_functions import (
frequency_domain_analysis, frequency_domain_analysis,
root_locus_analysis root_locus_analysis
) )
# ===== 新增:算例演示模块函数导入 ===== # ===== 算例演示模块函数导入(四阶段设计)=====
from case_demo_functions import run_case_demo from case_demo_functions import run_distillation_demo, run_gpr_training, run_engine_design, run_motor_design, run_hybrid_demo
from chatbot import chat_with_ai from chatbot import chat_with_ai
from user_stats import get_online_status_html, update_user_activity from user_stats import get_online_status_html, update_user_activity
from ui_components import ( from ui_components import (
@@ -23,8 +24,98 @@ from ui_components import (
create_chatbot_tab create_chatbot_tab
) )
# ===== 系统资源监控 =====
def get_system_monitor_html():
"""获取 CPU / 内存 / GPU 使用率的 HTML 小组件"""
try:
import psutil
cpu_pct = psutil.cpu_percent(interval=0)
mem = psutil.virtual_memory()
mem_pct = mem.percent
mem_used_gb = mem.used / (1024 ** 3)
mem_total_gb = mem.total / (1024 ** 3)
except ImportError:
return "<div style='text-align:center;color:#999;font-size:0.8em;'>psutil 未安装,无法监控系统资源</div>"
# GPU 信息 — 优先用 nvidia-smi(不依赖 PyTorch CUDA 版本),再用 torch.cuda 兜底
gpu_html = ""
try:
import subprocess as _sp
_r = _sp.run(
['nvidia-smi', '--query-gpu=name,memory.used,memory.total,utilization.gpu',
'--format=csv,noheader,nounits'],
capture_output=True, text=True, timeout=3
)
if _r.returncode == 0 and _r.stdout.strip():
_parts = [p.strip() for p in _r.stdout.strip().split('\n')[0].split(',')]
_gpu_mem_used = float(_parts[1]) / 1024 # MiB → GiB
_gpu_mem_total = float(_parts[2]) / 1024
_gpu_util = float(_parts[3])
_gc = '#ff6b6b' if _gpu_util > 80 else '#ffd93d' if _gpu_util > 50 else '#6bcb77'
gpu_html = (
f"<div style='display:inline-flex;align-items:center;gap:6px;'>"
f"<span>🎮 GPU</span>"
f"<div style='width:90px;height:8px;background:#444;border-radius:4px;overflow:hidden;'>"
f"<div style='width:{min(_gpu_util, 100):.0f}%;height:100%;background:{_gc};'></div>"
f"</div>"
f"<span>{_gpu_util:.0f}% &nbsp; {_gpu_mem_used:.1f}/{_gpu_mem_total:.0f} GB</span>"
f"</div>"
)
else:
raise RuntimeError("nvidia-smi no output")
except Exception:
try:
import torch as _torch
if _torch.cuda.is_available():
_mem_alloc = _torch.cuda.memory_allocated(0) / (1024 ** 3)
_mem_total = _torch.cuda.get_device_properties(0).total_memory / (1024 ** 3)
_util = _mem_alloc / max(_mem_total, 0.01) * 100
_gc = '#ff6b6b' if _util > 80 else '#ffd93d' if _util > 50 else '#6bcb77'
gpu_html = (
f"<div style='display:inline-flex;align-items:center;gap:6px;'>"
f"<span>🎮 GPU</span>"
f"<div style='width:90px;height:8px;background:#444;border-radius:4px;overflow:hidden;'>"
f"<div style='width:{min(_util, 100):.0f}%;height:100%;background:{_gc};'></div>"
f"</div>"
f"<span>{_mem_alloc:.1f}/{_mem_total:.0f} GB</span>"
f"</div>"
)
else:
gpu_html = "<div style='display:inline-flex;align-items:center;gap:4px;'><span>🎮 GPU N/A</span></div>"
except Exception:
gpu_html = "<div style='display:inline-flex;align-items:center;gap:4px;'><span>🎮 GPU N/A</span></div>"
cpu_color = '#ff6b6b' if cpu_pct > 80 else '#ffd93d' if cpu_pct > 50 else '#6bcb77'
mem_color = '#ff6b6b' if mem_pct > 80 else '#ffd93d' if mem_pct > 50 else '#6bcb77'
html = (
f"<div style='display:flex;justify-content:center;gap:20px;flex-wrap:wrap;"
f"font-size:0.82em;color:#ddd;padding:4px 10px;'>"
# CPU
f"<div style='display:inline-flex;align-items:center;gap:6px;'>"
f"<span>🖥️ CPU</span>"
f"<div style='width:90px;height:8px;background:#444;border-radius:4px;overflow:hidden;'>"
f"<div style='width:{min(cpu_pct, 100):.0f}%;height:100%;background:{cpu_color};'></div>"
f"</div>"
f"<span>{cpu_pct:.0f}%</span>"
f"</div>"
# Memory
f"<div style='display:inline-flex;align-items:center;gap:6px;'>"
f"<span>💾 RAM</span>"
f"<div style='width:90px;height:8px;background:#444;border-radius:4px;overflow:hidden;'>"
f"<div style='width:{min(mem_pct, 100):.0f}%;height:100%;background:{mem_color};'></div>"
f"</div>"
f"<span>{mem_used_gb:.1f}/{mem_total_gb:.0f} GB ({mem_pct:.0f}%)</span>"
f"</div>"
# GPU
f"{gpu_html}"
f"</div>"
)
return html
# 加载外部CSS文件 # 加载外部CSS文件
with open("assets/styles.css", "r", encoding="utf-8") as f: with open(os.path.join(os.path.dirname(__file__), "assets", "styles.css"), "r", encoding="utf-8") as f:
custom_css = f.read() custom_css = f.read()
# --- 主应用界面 --- # --- 主应用界面 ---
@@ -36,23 +127,8 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus
# 创建头部信息和在线计数器 # 创建头部信息和在线计数器
online_counter = create_header() online_counter = create_header()
# 创建共享的输入组件 # 系统资源监控(始终可见)
with gr.Row(): system_monitor = gr.HTML(value=get_system_monitor_html, elem_id="system-monitor")
with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>📊 通用系统参数</div>")
num_input = gr.Textbox(
label="传递函数分子系数 (Numerator)",
value="1",
placeholder="例如: 1 或 1,2,3",
info="💡 用逗号分隔多个系数,从最高次项到常数项"
)
den_input = gr.Textbox(
label="传递函数分母系数 (Denominator)",
value="1,6,11,6",
placeholder="例如: 1,2,1",
info="💡 分母阶数通常高于或等于分子阶数"
)
# 创建功能选项卡 # 创建功能选项卡
with gr.Tabs() as tabs: with gr.Tabs() as tabs:
@@ -80,13 +156,13 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus
# --- 时域分析事件 --- # --- 时域分析事件 ---
time_domain_ui["confirm_button"].click( time_domain_ui["confirm_button"].click(
fn=display_transfer_function, fn=display_transfer_function,
inputs=[num_input, den_input], inputs=[time_domain_ui["num_input"], time_domain_ui["den_input"]],
outputs=[time_domain_ui["tf_display"]] outputs=[time_domain_ui["tf_display"]]
).then(lambda: get_online_status_html(), outputs=online_counter) ).then(lambda: get_online_status_html(), outputs=online_counter)
time_domain_ui["analyze_button"].click( time_domain_ui["analyze_button"].click(
fn=time_domain_analysis, fn=time_domain_analysis,
inputs=[num_input, den_input], inputs=[time_domain_ui["num_input"], time_domain_ui["den_input"]],
outputs=[time_domain_ui["output_plot"], time_domain_ui["output_metrics"]] outputs=[time_domain_ui["output_plot"], time_domain_ui["output_metrics"]]
).then(lambda: get_online_status_html(), outputs=online_counter) ).then(lambda: get_online_status_html(), outputs=online_counter)
@@ -96,7 +172,7 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus
fig, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k) fig, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k)
return fig, metrics, tf_latex, stability, k, get_online_status_html() return fig, metrics, tf_latex, stability, k, get_online_status_html()
freq_inputs = [num_input, den_input, freq_domain_ui["log_k_slider"]] freq_inputs = [freq_domain_ui["num_input"], freq_domain_ui["den_input"], freq_domain_ui["log_k_slider"]]
freq_outputs = [ freq_outputs = [
freq_domain_ui["plot_output"], freq_domain_ui["plot_output"],
freq_domain_ui["metrics_display"], freq_domain_ui["metrics_display"],
@@ -116,7 +192,7 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus
fig, poles, k_val = root_locus_analysis(num, den, log_k) fig, poles, k_val = root_locus_analysis(num, den, log_k)
return fig, poles, k_val, get_online_status_html() return fig, poles, k_val, get_online_status_html()
rl_inputs = [root_locus_ui["log_k_slider"], num_input, den_input] rl_inputs = [root_locus_ui["log_k_slider"], root_locus_ui["num_input"], root_locus_ui["den_input"]]
rl_outputs = [ rl_outputs = [
root_locus_ui["plot_output"], root_locus_ui["plot_output"],
root_locus_ui["poles_display"], root_locus_ui["poles_display"],
@@ -129,59 +205,170 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus
outputs=rl_outputs outputs=rl_outputs
) )
# 当输入框变化时,也更新频域和根轨迹(如果它们是当前可见的) # 频域:当传递函数输入框变化时自动更新
def update_all_on_tf_change(num, den, log_k_freq, log_k_rl): freq_domain_ui["num_input"].change(
# 更新频域 fn=update_frequency_analysis_wrapper,
k_freq = 10**log_k_freq inputs=freq_inputs, outputs=freq_outputs
fig_freq, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k_freq) )
freq_domain_ui["den_input"].change(
# 更新根轨迹 fn=update_frequency_analysis_wrapper,
fig_rl, poles, k_val_rl = root_locus_analysis(num, den, log_k_rl) inputs=freq_inputs, outputs=freq_outputs
return (
fig_freq, metrics, tf_latex, stability, k_freq,
fig_rl, poles, k_val_rl,
get_online_status_html()
) )
tf_change_inputs = [num_input, den_input, freq_domain_ui["log_k_slider"], root_locus_ui["log_k_slider"]] # 根轨迹:当传递函数输入框变化时自动更新
tf_change_outputs = freq_outputs[:-1] + rl_outputs[:-1] + [online_counter] root_locus_ui["num_input"].change(
fn=update_rl_view_wrapper,
inputs=rl_inputs, outputs=rl_outputs
)
root_locus_ui["den_input"].change(
fn=update_rl_view_wrapper,
inputs=rl_inputs, outputs=rl_outputs
)
num_input.change(fn=update_all_on_tf_change, inputs=tf_change_inputs, outputs=tf_change_outputs) # ===== 算例演示事件绑定(四阶段)=====
den_input.change(fn=update_all_on_tf_change, inputs=tf_change_inputs, outputs=tf_change_outputs)
# ===== 新增:算例演示事件包装器 ===== # --- 阶段零-AGPR 模型训练 ---
def run_case_demo_wrapper(sim_time, dt, initial_soc, initial_engine_power, profile, rpm_scale, load_scale, sid): def run_gpr_wrapper(mode, sid, progress=gr.Progress(track_tqdm=True)):
update_user_activity(sid) update_user_activity(sid)
fig, summary, table_data = run_case_demo( fig, summary = run_gpr_training(mode=mode, progress=progress)
sim_time_s=sim_time, return fig, summary, get_online_status_html()
dt=dt,
initial_soc_pct=initial_soc, case_demo_ui["gpr_run_button"].click(
initial_engine_power_kw=initial_engine_power, fn=run_gpr_wrapper,
profile_name=profile, inputs=[case_demo_ui["gpr_mode"], session_id],
rpm_scale=rpm_scale, outputs=[case_demo_ui["gpr_plot"], case_demo_ui["gpr_summary"], online_counter]
load_scale=load_scale )
# --- 阶段零-B:NN 模型训练(蒸馏)---
def run_distillation_wrapper(epochs, lr, hidden, sid, progress=gr.Progress(track_tqdm=True)):
update_user_activity(sid)
fig, summary = run_distillation_demo(epochs, lr, hidden, progress=progress)
return fig, summary, get_online_status_html()
case_demo_ui["distill_run_button"].click(
fn=run_distillation_wrapper,
inputs=[
case_demo_ui["distill_epochs"], case_demo_ui["distill_lr"],
case_demo_ui["distill_hidden"],
session_id
],
outputs=[case_demo_ui["distill_plot"], case_demo_ui["distill_summary"], online_counter]
)
# --- 阶段一:发动机控制器设计 ---
def run_engine_design_wrapper(sim_time, dt, init_power, target_power,
controller_type,
kp, ki, kd, tau_fuel, K_inertia,
mpc_horizon, mpc_W_power, mpc_W_dcost, mpc_overshoot,
sid, progress=gr.Progress(track_tqdm=True)):
update_user_activity(sid)
fig, summary = run_engine_design(
sim_time, dt, init_power, target_power,
controller_type,
kp, ki, kd, tau_fuel, K_inertia,
mpc_horizon, mpc_W_power, mpc_W_dcost, mpc_overshoot / 100.0,
progress=progress
)
return fig, summary, get_online_status_html()
case_demo_ui["eng_run_button"].click(
fn=run_engine_design_wrapper,
inputs=[
case_demo_ui["eng_sim_time"], case_demo_ui["eng_dt"],
case_demo_ui["eng_init_power"], case_demo_ui["eng_target_power"],
case_demo_ui["eng_controller_type"],
case_demo_ui["eng_kp"], case_demo_ui["eng_ki"], case_demo_ui["eng_kd"],
case_demo_ui["eng_tau_fuel"], case_demo_ui["eng_K_inertia"],
case_demo_ui["eng_mpc_horizon"], case_demo_ui["eng_mpc_W_power"],
case_demo_ui["eng_mpc_W_dcost"], case_demo_ui["eng_mpc_overshoot"],
session_id
],
outputs=[case_demo_ui["eng_plot"], case_demo_ui["eng_summary"], online_counter]
)
# --- 阶段二:电机控制器设计 ---
def run_motor_design_wrapper(sim_time, dt, target_rpm, load_torque,
controller_type,
kp, ki, kd, J,
mpc_W_speed, mpc_W_dcost, mpc_overshoot,
sid, progress=gr.Progress(track_tqdm=True)):
update_user_activity(sid)
fig, summary = run_motor_design(
sim_time, dt, target_rpm, load_torque,
controller_type,
kp, ki, kd, J,
mpc_W_speed, mpc_W_dcost, mpc_overshoot / 100.0,
progress=progress
)
return fig, summary, get_online_status_html()
case_demo_ui["mot_run_button"].click(
fn=run_motor_design_wrapper,
inputs=[
case_demo_ui["mot_sim_time"], case_demo_ui["mot_dt"],
case_demo_ui["mot_target_rpm"], case_demo_ui["mot_load_torque"],
case_demo_ui["mot_controller_type"],
case_demo_ui["mot_kp"], case_demo_ui["mot_ki"], case_demo_ui["mot_kd"],
case_demo_ui["mot_J"],
case_demo_ui["mot_mpc_W_speed"], case_demo_ui["mot_mpc_W_dcost"],
case_demo_ui["mot_mpc_overshoot"],
session_id
],
outputs=[case_demo_ui["mot_plot"], case_demo_ui["mot_summary"], online_counter]
)
# --- 阶段三:能量管理策略设计(自动引用前两阶段控制器参数)---
def run_hybrid_demo_wrapper(sim_time, dt, initial_soc, initial_engine_power,
profile,
eng_ctrl_type, eng_kp, eng_ki, eng_kd,
eng_mpc_horizon, eng_mpc_W_power, eng_mpc_W_dcost, eng_mpc_overshoot,
mot_ctrl_type, mot_kp, mot_ki, mot_kd, mot_J,
mot_mpc_W_speed, mot_mpc_W_dcost, mot_mpc_overshoot,
soc_target, soc_low, soc_high,
p_eng_min, p_eng_max, p_charge, k_soc,
power_reserve, battery_capacity, sid,
progress=gr.Progress(track_tqdm=True)):
update_user_activity(sid)
fig, summary, table_data = run_hybrid_demo(
sim_time, dt, initial_soc, initial_engine_power, profile,
eng_ctrl_type, eng_kp, eng_ki, eng_kd,
eng_mpc_horizon, eng_mpc_W_power, eng_mpc_W_dcost, eng_mpc_overshoot / 100.0,
mot_ctrl_type, mot_kp, mot_ki, mot_kd, mot_J,
mot_mpc_W_speed, mot_mpc_W_dcost, mot_mpc_overshoot / 100.0,
soc_target, soc_low, soc_high,
p_eng_min, p_eng_max, p_charge, k_soc,
power_reserve, battery_capacity,
progress=progress
) )
return fig, summary, table_data, get_online_status_html() return fig, summary, table_data, get_online_status_html()
# ===== 新增:算例演示按钮事件绑定 ===== case_demo_ui["hybrid_run_button"].click(
case_demo_ui["run_button"].click( fn=run_hybrid_demo_wrapper,
fn=run_case_demo_wrapper,
inputs=[ inputs=[
case_demo_ui["sim_time"], case_demo_ui["sim_time"], case_demo_ui["dt"],
case_demo_ui["dt"], case_demo_ui["initial_soc"], case_demo_ui["initial_engine_power"],
case_demo_ui["initial_soc"],
case_demo_ui["initial_engine_power"],
case_demo_ui["profile"], case_demo_ui["profile"],
case_demo_ui["rpm_scale"], # 发动机控制器参数
case_demo_ui["load_scale"], case_demo_ui["eng_controller_type"],
case_demo_ui["eng_kp"], case_demo_ui["eng_ki"], case_demo_ui["eng_kd"],
case_demo_ui["eng_mpc_horizon"], case_demo_ui["eng_mpc_W_power"],
case_demo_ui["eng_mpc_W_dcost"], case_demo_ui["eng_mpc_overshoot"],
# 电机控制器参数
case_demo_ui["mot_controller_type"],
case_demo_ui["mot_kp"], case_demo_ui["mot_ki"], case_demo_ui["mot_kd"],
case_demo_ui["mot_J"],
case_demo_ui["mot_mpc_W_speed"], case_demo_ui["mot_mpc_W_dcost"],
case_demo_ui["mot_mpc_overshoot"],
# 能量管理策略参数
case_demo_ui["soc_target"], case_demo_ui["soc_low"], case_demo_ui["soc_high"],
case_demo_ui["p_eng_min"], case_demo_ui["p_eng_max"],
case_demo_ui["p_charge"], case_demo_ui["k_soc"],
case_demo_ui["power_reserve"], case_demo_ui["battery_capacity"],
session_id session_id
], ],
outputs=[ outputs=[
case_demo_ui["plot"], case_demo_ui["hybrid_plot"], case_demo_ui["hybrid_summary"],
case_demo_ui["summary"], case_demo_ui["hybrid_table"], online_counter
case_demo_ui["table"],
online_counter
] ]
) )
@@ -215,14 +402,13 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus
) )
# --- 页面加载和定时器事件 --- # --- 页面加载和定时器事件 ---
def on_page_load(sid): demo.load(fn=lambda: get_online_status_html(), outputs=[online_counter])
update_user_activity(sid)
return get_online_status_html()
demo.load(fn=on_page_load, inputs=[session_id], outputs=[online_counter])
gr.Timer(10).tick(fn=get_online_status_html, outputs=online_counter) gr.Timer(10).tick(fn=get_online_status_html, outputs=online_counter)
# 系统资源监控定时刷新(每 3 秒)
gr.Timer(3).tick(fn=get_system_monitor_html, outputs=system_monitor)
if __name__ == "__main__": if __name__ == "__main__":
demo.queue().launch( demo.queue().launch(
File diff suppressed because it is too large Load Diff
+1001 -97
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,4 +1,4 @@
{ {
"total_users": 22, "total_users": 37,
"last_saved_at": 1775496284.1673155 "last_saved_at": 1775560914.9575145
} }
+5 -4
View File
@@ -1,7 +1,7 @@
# requirements.txt # requirements.txt
# ===== 新增:PyTorch CPU 轮子下载源 ===== # ===== PyTorchGPU (CUDA 12.1) 版本,如无 NVIDIA GPU 可改为 cpu =====
--extra-index-url https://download.pytorch.org/whl/cpu --extra-index-url https://download.pytorch.org/whl/cu121
gradio==4.44.1 gradio==4.44.1
gradio-client==1.3.0 gradio-client==1.3.0
@@ -14,10 +14,11 @@ matplotlib==3.9.4
aiohttp==3.13.5 aiohttp==3.13.5
pillow==10.4.0 pillow==10.4.0
# ===== 新增:混动模型(Model)运行依赖 ===== # ===== 新增:混动模型(Model)运行依赖 =====
torch==2.4.1 torch==2.4.1+cu121
botorch==0.14.0 botorch==0.14.0
gpytorch==1.14 gpytorch==1.14
pyro-ppl==1.9.1 pyro-ppl==1.9.1
pandas==2.3.3 pandas==2.3.3
scipy==1.15.3 scipy>=1.10
scikit-learn==1.7.1 scikit-learn==1.7.1
psutil>=5.9
+324 -21
View File
@@ -1,8 +1,13 @@
import gradio as gr import gradio as gr
from assets.knowledge_cards_html import ( from assets.knowledge_cards_html import (
TIME_DOMAIN_KNOWLEDGE, TIME_DOMAIN_KNOWLEDGE,
FREQUENCY_DOMAIN_KNOWLEDGE, FREQUENCY_DOMAIN_KNOWLEDGE,
ROOT_LOCUS_KNOWLEDGE ROOT_LOCUS_KNOWLEDGE,
ENGINE_CONTROL_KNOWLEDGE,
MOTOR_CONTROL_KNOWLEDGE,
GPR_KNOWLEDGE,
NN_KNOWLEDGE,
EMS_KNOWLEDGE,
) )
def create_header(): def create_header():
@@ -66,6 +71,20 @@ def create_time_domain_tab():
ui_dict = {} ui_dict = {}
with gr.Row(): with gr.Row():
with gr.Column(scale=1): with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>📊 传递函数设定</div>")
ui_dict["num_input"] = gr.Textbox(
label="分子系数 (Numerator)",
value="1",
placeholder="例如: 1 或 1,2,3",
info="💡 用逗号分隔,从最高次项到常数项"
)
ui_dict["den_input"] = gr.Textbox(
label="分母系数 (Denominator)",
value="1,6,11,6",
placeholder="例如: 1,2,1",
info="💡 分母阶数通常 ≥ 分子阶数"
)
with gr.Group(): with gr.Group():
gr.HTML("<div class='card-title'>🔧 系统模型</div>") gr.HTML("<div class='card-title'>🔧 系统模型</div>")
ui_dict["tf_display"] = gr.Markdown(label="当前传递函数", elem_classes="output-display") ui_dict["tf_display"] = gr.Markdown(label="当前传递函数", elem_classes="output-display")
@@ -116,6 +135,20 @@ def create_frequency_domain_tab():
ui_dict = {} ui_dict = {}
with gr.Row(): with gr.Row():
with gr.Column(scale=1): with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>📊 传递函数设定</div>")
ui_dict["num_input"] = gr.Textbox(
label="分子系数 (Numerator)",
value="1",
placeholder="例如: 1 或 1,2,3",
info="💡 用逗号分隔,从最高次项到常数项"
)
ui_dict["den_input"] = gr.Textbox(
label="分母系数 (Denominator)",
value="1,6,11,6",
placeholder="例如: 1,2,1",
info="💡 分母阶数通常 ≥ 分子阶数"
)
with gr.Group(): with gr.Group():
gr.HTML("<div class='card-title'>🎚️ 调整系统增益</div>") gr.HTML("<div class='card-title'>🎚️ 调整系统增益</div>")
ui_dict["log_k_slider"] = gr.Slider(minimum=-4, maximum=4, value=1, step=0.01, label="对数增益 log₁₀(K)", info="💡 拖动滑块查看实时变化") ui_dict["log_k_slider"] = gr.Slider(minimum=-4, maximum=4, value=1, step=0.01, label="对数增益 log₁₀(K)", info="💡 拖动滑块查看实时变化")
@@ -127,6 +160,8 @@ def create_frequency_domain_tab():
gr.HTML("<div class='card-title'>📊 稳定裕度分析</div>") gr.HTML("<div class='card-title'>📊 稳定裕度分析</div>")
ui_dict["metrics_display"] = gr.Textbox(label="Stability Margins", lines=4, interactive=False, elem_classes="output-metrics") ui_dict["metrics_display"] = gr.Textbox(label="Stability Margins", lines=4, interactive=False, elem_classes="output-metrics")
ui_dict["stability_display"] = gr.Markdown(elem_classes="stability-result") ui_dict["stability_display"] = gr.Markdown(elem_classes="stability-result")
with gr.Row():
ui_dict["analyze_button"] = gr.Button("🚀 开始分析", variant="primary", scale=1, elem_classes="primary-btn")
with gr.Column(scale=2): with gr.Column(scale=2):
ui_dict["plot_output"] = gr.Plot(label="频域响应图", elem_classes="plot-container") ui_dict["plot_output"] = gr.Plot(label="频域响应图", elem_classes="plot-container")
# 知识卡片 # 知识卡片
@@ -163,6 +198,20 @@ def create_root_locus_tab():
ui_dict = {} ui_dict = {}
with gr.Row(): with gr.Row():
with gr.Column(scale=1): with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>📊 传递函数设定</div>")
ui_dict["num_input"] = gr.Textbox(
label="分子系数 (Numerator)",
value="1",
placeholder="例如: 1 或 1,2,3",
info="💡 用逗号分隔,从最高次项到常数项"
)
ui_dict["den_input"] = gr.Textbox(
label="分母系数 (Denominator)",
value="1,6,11,6",
placeholder="例如: 1,2,1",
info="💡 分母阶数通常 ≥ 分子阶数"
)
with gr.Group(): with gr.Group():
gr.HTML("<div class='card-title'>🎚️ 调整系统增益</div>") gr.HTML("<div class='card-title'>🎚️ 调整系统增益</div>")
ui_dict["log_k_slider"] = gr.Slider(minimum=-4, maximum=4, value=1, step=0.01, label="对数增益 log₁₀(K)", info="💡 拖动滑块观察极点移动") ui_dict["log_k_slider"] = gr.Slider(minimum=-4, maximum=4, value=1, step=0.01, label="对数增益 log₁₀(K)", info="💡 拖动滑块观察极点移动")
@@ -170,6 +219,8 @@ def create_root_locus_tab():
with gr.Group(): with gr.Group():
gr.HTML("<div class='card-title'>📍 闭环极点位置</div>") gr.HTML("<div class='card-title'>📍 闭环极点位置</div>")
ui_dict["poles_display"] = gr.Textbox(label="Closed-Loop Pole Locations", lines=6, interactive=False, elem_classes="output-metrics") ui_dict["poles_display"] = gr.Textbox(label="Closed-Loop Pole Locations", lines=6, interactive=False, elem_classes="output-metrics")
with gr.Row():
ui_dict["analyze_button"] = gr.Button("🚀 开始分析", variant="primary", scale=1, elem_classes="primary-btn")
with gr.Column(scale=2): with gr.Column(scale=2):
ui_dict["plot_output"] = gr.Plot(label="根轨迹图") ui_dict["plot_output"] = gr.Plot(label="根轨迹图")
gr.HTML(f""" gr.HTML(f"""
@@ -201,34 +252,285 @@ def create_root_locus_tab():
return ui_dict return ui_dict
def create_case_demo_tab(): def create_case_demo_tab():
"""创建算例演示选项卡的UI组件""" """创建算例演示选项卡 — 四阶段交互设计(蒸馏→发动机→电机→能量管理)"""
ui_dict = {} ui_dict = {}
# === MathJax re-render helper (reused across tabs) ===
def _mathjax_script(div_id):
return f"""
<script>
(function() {{
function renderMath() {{
setTimeout(function() {{
if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {{
MathJax.typesetPromise([document.getElementById('{div_id}')])
.catch(function(err) {{ console.log('MathJax error:', err); }});
}} else {{ setTimeout(renderMath, 500); }}
}}, 300);
}}
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', renderMath);
}} else {{ renderMath(); }}
}})();
</script>"""
with gr.Tabs():
# ========== 阶段零:模型训练(GPR + NN 两个子标签页)==========
with gr.TabItem("🧬 模型训练", id="distill_tab"):
gr.HTML("""<div style='background:linear-gradient(135deg,#f3e5f5,#e1bee7);padding:10px 16px;
border-radius:8px;margin-bottom:10px;font-size:0.92em;color:#6a1b9a;'>
<b>阶段零</b>:模型训练包含两步——先训练/验证 GPR 高斯过程代理模型,
再将其知识蒸馏为轻量 NN(MLP)用于后续实时控制仿真。</div>""")
with gr.Tabs():
# ----- 子标签页 A: GPR 模型训练 -----
with gr.TabItem("📈 GPR 模型训练", id="gpr_sub_tab"):
with gr.Row(): with gr.Row():
with gr.Column(scale=1): with gr.Column(scale=1):
with gr.Group(): with gr.Group():
gr.HTML("<div class='card-title'>🧪 算例参数设置</div>") gr.HTML("<div class='card-title'>🔬 GPR 训练设置</div>")
ui_dict["gpr_mode"] = gr.Radio(
choices=["load", "train"], value="load",
label="运行模式",
info="load: 加载已有权重 | train: 从头训练(需 botorch")
ui_dict["gpr_run_button"] = gr.Button(
"🚀 运行 GPR 训练 / 加载", variant="primary",
elem_classes="primary-btn")
with gr.Group():
gr.HTML("<div class='card-title'>📝 GPR 结果</div>")
ui_dict["gpr_summary"] = gr.Markdown()
with gr.Column(scale=2):
ui_dict["gpr_plot"] = gr.Plot(label="GPR 模型结果")
gr.HTML(f"""
<div id="gpr-knowledge" style="max-height:600px;overflow-y:auto;padding-right:8px;">
{GPR_KNOWLEDGE}
</div>
{_mathjax_script('gpr-knowledge')}
""")
# ----- 子标签页 B: NN 模型训练 -----
with gr.TabItem("🧠 NN 模型训练", id="nn_sub_tab"):
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>🧪 NN 训练参数</div>")
ui_dict["distill_epochs"] = gr.Slider(minimum=500, maximum=8000, value=3000, step=100,
label="训练轮数 (Epochs)", info="越多越精确,但耗时更长")
ui_dict["distill_lr"] = gr.Slider(minimum=1e-4, maximum=1e-2, value=3e-3, step=1e-4,
label="学习率 (LR)", info="推荐 1e-3 ~ 5e-3")
ui_dict["distill_hidden"] = gr.Slider(minimum=16, maximum=256, value=64, step=16,
label="隐藏层宽度", info="MLP每层神经元数")
ui_dict["distill_run_button"] = gr.Button("🚀 开始 NN 训练", variant="primary",
elem_classes="primary-btn")
with gr.Group():
gr.HTML("<div class='card-title'>📝 NN 训练结果</div>")
ui_dict["distill_summary"] = gr.Markdown()
with gr.Column(scale=2):
ui_dict["distill_plot"] = gr.Plot(label="NN 训练结果 (Loss + Parity)")
gr.HTML(f"""
<div id="nn-knowledge" style="max-height:600px;overflow-y:auto;padding-right:8px;">
{NN_KNOWLEDGE}
</div>
{_mathjax_script('nn-knowledge')}
""")
# ========== 阶段一:发动机控制器设计 ==========
with gr.TabItem("🔧 发动机控制器设计", id="engine_tab"):
gr.HTML("""<div style='background:linear-gradient(135deg,#fff3e0,#ffe0b2);padding:10px 16px;
border-radius:8px;margin-bottom:10px;font-size:0.92em;color:#e65100;'>
<b>阶段一</b>:选择 PID 或 MPC 控制器,调整参数,运行阶跃响应测试,观察功率跟踪性能。</div>""")
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>🎯 控制器选择</div>")
ui_dict["eng_controller_type"] = gr.Radio(
choices=["PID", "MPC"], value="PID", label="控制器类型",
info="PID: 经典三参数 | MPC: 模型预测控制")
with gr.Group(visible=True) as eng_pid_group:
gr.HTML("<div class='card-title'>🎛️ PID 参数</div>")
ui_dict["eng_kp"] = gr.Slider(minimum=0.1, maximum=20, value=4.652, step=0.01,
label="比例增益 Kp", info="增大加快响应,过大导致振荡")
ui_dict["eng_ki"] = gr.Slider(minimum=0.0, maximum=20, value=7.078, step=0.01,
label="积分增益 Ki", info="消除稳态误差,过大导致超调")
ui_dict["eng_kd"] = gr.Slider(minimum=0.0, maximum=5, value=0.222, step=0.001,
label="微分增益 Kd", info="抑制振荡,改善动态特性")
ui_dict["eng_pid_group"] = eng_pid_group
with gr.Group(visible=False) as eng_mpc_group:
gr.HTML("<div class='card-title'>🎛️ MPC 参数</div>")
ui_dict["eng_mpc_horizon"] = gr.Slider(minimum=3, maximum=30, value=15, step=1,
label="预测时域 (Horizon)", info="MPC前看步数")
ui_dict["eng_mpc_W_power"] = gr.Slider(minimum=1, maximum=500, value=200, step=1,
label="功率跟踪权重 W_power")
ui_dict["eng_mpc_W_dcost"] = gr.Slider(minimum=0.01, maximum=20, value=1.5, step=0.01,
label="控制增量权重 W_Δcost")
ui_dict["eng_mpc_overshoot"] = gr.Slider(minimum=1, maximum=30, value=5, step=1,
label="超调限制 (%)")
ui_dict["eng_mpc_group"] = eng_mpc_group
with gr.Group():
gr.HTML("<div class='card-title'>⚙️ 发动机模型参数</div>")
ui_dict["eng_tau_fuel"] = gr.Slider(minimum=0.05, maximum=1.0, value=0.15, step=0.01,
label="燃油执行机构时间常数 τ (s)")
ui_dict["eng_K_inertia"] = gr.Slider(minimum=10, maximum=500, value=100, step=5,
label="转子惯性增益 K")
with gr.Group():
gr.HTML("<div class='card-title'>🧪 仿真设置</div>")
ui_dict["eng_sim_time"] = gr.Slider(minimum=5, maximum=60, value=30, step=1,
label="仿真时长 (s)")
ui_dict["eng_dt"] = gr.Dropdown(choices=[0.02, 0.05, 0.1], value=0.02,
label="仿真步长 (s)")
ui_dict["eng_init_power"] = gr.Slider(minimum=20, maximum=250, value=100, step=5,
label="初始功率 (kW)")
ui_dict["eng_target_power"] = gr.Slider(minimum=20, maximum=300, value=200, step=5,
label="目标功率 (kW)")
ui_dict["eng_run_button"] = gr.Button("🚀 运行发动机仿真", variant="primary",
elem_classes="primary-btn")
with gr.Group():
gr.HTML("<div class='card-title'>📝 设计结果</div>")
ui_dict["eng_summary"] = gr.Markdown()
with gr.Column(scale=2):
ui_dict["eng_plot"] = gr.Plot(label="发动机控制器阶跃响应")
gr.HTML(f"""
<div id="engine-knowledge" style="max-height:600px;overflow-y:auto;padding-right:8px;">
{ENGINE_CONTROL_KNOWLEDGE}
</div>
{_mathjax_script('engine-knowledge')}
""")
# Radio toggle PID/MPC visibility
ui_dict["eng_controller_type"].change(
fn=lambda ct: (gr.update(visible=(ct == "PID")), gr.update(visible=(ct == "MPC"))),
inputs=[ui_dict["eng_controller_type"]],
outputs=[eng_pid_group, eng_mpc_group],
)
# ========== 阶段二:电机控制器设计 ==========
with gr.TabItem("⚡ 电机控制器设计", id="motor_tab"):
gr.HTML("""<div style='background:linear-gradient(135deg,#e3f2fd,#bbdefb);padding:10px 16px;
border-radius:8px;margin-bottom:10px;font-size:0.92em;color:#1565c0;'>
<b>阶段二</b>:选择 PID 或 MPC 控制器,运行转速跟踪 + 负载扰动测试。
在仿真60%时刻自动施加50%负载扰动,检验抗扰能力。</div>""")
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>🎯 控制器选择</div>")
ui_dict["mot_controller_type"] = gr.Radio(
choices=["PID", "MPC"], value="PID", label="控制器类型",
info="PID: 经典三参数 | MPC: 模型预测控制")
with gr.Group(visible=True) as mot_pid_group:
gr.HTML("<div class='card-title'>🎛️ PID 参数</div>")
ui_dict["mot_kp"] = gr.Slider(minimum=0.1, maximum=50, value=5.0, step=0.1,
label="比例增益 Kp", info="增大加快转速响应")
ui_dict["mot_ki"] = gr.Slider(minimum=0.0, maximum=100, value=2.0, step=0.1,
label="积分增益 Ki", info="消除转速稳态偏差")
ui_dict["mot_kd"] = gr.Slider(minimum=0.0, maximum=5, value=0.5, step=0.01,
label="微分增益 Kd", info="抑制转速振荡")
ui_dict["mot_pid_group"] = mot_pid_group
with gr.Group(visible=False) as mot_mpc_group:
gr.HTML("<div class='card-title'>🎛️ MPC 参数</div>")
ui_dict["mot_mpc_W_speed"] = gr.Slider(minimum=1, maximum=500, value=200, step=1,
label="转速跟踪权重 W_speed")
ui_dict["mot_mpc_W_dcost"] = gr.Slider(minimum=0.01, maximum=20, value=0.3, step=0.01,
label="控制增量权重 W_Δcost")
ui_dict["mot_mpc_overshoot"] = gr.Slider(minimum=1, maximum=30, value=5, step=1,
label="超调限制 (%)")
ui_dict["mot_mpc_group"] = mot_mpc_group
with gr.Group():
gr.HTML("<div class='card-title'>⚙️ 电机模型参数</div>")
ui_dict["mot_J"] = gr.Slider(minimum=0.1, maximum=5.0, value=1.0, step=0.1,
label="转动惯量 J (kg·m²)", info="越大响应越慢但越平稳")
with gr.Group():
gr.HTML("<div class='card-title'>🧪 仿真设置</div>")
ui_dict["mot_sim_time"] = gr.Slider(minimum=5, maximum=60, value=30, step=1,
label="仿真时长 (s)")
ui_dict["mot_dt"] = gr.Dropdown(choices=[0.02, 0.05, 0.1], value=0.02,
label="仿真步长 (s)")
ui_dict["mot_target_rpm"] = gr.Slider(minimum=500, maximum=5000, value=2000, step=50,
label="目标转速 (RPM)")
ui_dict["mot_load_torque"] = gr.Slider(minimum=10, maximum=400, value=80, step=5,
label="负载转矩 (Nm)")
ui_dict["mot_run_button"] = gr.Button("🚀 运行电机仿真", variant="primary",
elem_classes="primary-btn")
with gr.Group():
gr.HTML("<div class='card-title'>📝 设计结果</div>")
ui_dict["mot_summary"] = gr.Markdown()
with gr.Column(scale=2):
ui_dict["mot_plot"] = gr.Plot(label="电机控制器阶跃响应")
gr.HTML(f"""
<div id="motor-knowledge" style="max-height:600px;overflow-y:auto;padding-right:8px;">
{MOTOR_CONTROL_KNOWLEDGE}
</div>
{_mathjax_script('motor-knowledge')}
""")
# Radio toggle PID/MPC visibility
ui_dict["mot_controller_type"].change(
fn=lambda ct: (gr.update(visible=(ct == "PID")), gr.update(visible=(ct == "MPC"))),
inputs=[ui_dict["mot_controller_type"]],
outputs=[mot_pid_group, mot_mpc_group],
)
# ========== 阶段三:能量管理策略设计 ==========
with gr.TabItem("🔋 能量管理策略设计", id="ems_tab"):
gr.HTML("""<div style='background:linear-gradient(135deg,#e8f5e9,#c8e6c9);padding:10px 16px;
border-radius:8px;margin-bottom:10px;font-size:0.92em;color:#2e7d32;'>
<b>阶段三</b>:设计基于规则的能量管理策略(自动引用前两阶段的控制器参数)。<br>
<b>策略原理</b>SOC &lt; 下限阈值 → 进入<b>充电模式</b>;
SOC &gt; 上限阈值 → 退出充电,进入<b>功率跟随模式</b>。
下限~上限之间为<b>滞环区间</b>,防止模式频繁切换。</div>""")
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>📊 SOC规则参数(滞环控制)</div>")
ui_dict["soc_target"] = gr.Slider(minimum=30, maximum=80, value=60, step=1,
label="SOC目标值 (%)", info="功率跟随模式下的SOC补偿基准")
ui_dict["soc_low"] = gr.Slider(minimum=15, maximum=50, value=30, step=1,
label="SOC下限阈值 (%)", info="低于此值→进入充电模式")
ui_dict["soc_high"] = gr.Slider(minimum=50, maximum=90, value=70, step=1,
label="SOC上限阈值 (%)", info="高于此值→退出充电模式")
with gr.Group():
gr.HTML("<div class='card-title'>⚡ 功率规则参数</div>")
ui_dict["p_eng_min"] = gr.Slider(minimum=10, maximum=100, value=20, step=5,
label="发动机最小功率 (kW)")
ui_dict["p_eng_max"] = gr.Slider(minimum=100, maximum=350, value=300, step=10,
label="发动机最大功率 (kW)")
ui_dict["p_charge"] = gr.Slider(minimum=50, maximum=300, value=200, step=10,
label="充电模式发动机功率 (kW)", info="进入充电模式后发动机固定输出")
ui_dict["k_soc"] = gr.Slider(minimum=0, maximum=200, value=50, step=5,
label="SOC补偿增益 (kW/ΔSOC)", info="跟随模式下对SOC偏差的修正力度")
ui_dict["power_reserve"] = gr.Slider(minimum=0, maximum=50, value=10, step=1,
label="动态功率储备 (%)", info="发动机额外预留功率百分比")
with gr.Group():
gr.HTML("<div class='card-title'>🔧 系统与仿真参数</div>")
ui_dict["battery_capacity"] = gr.Slider(minimum=10, maximum=200, value=50, step=5,
label="电池容量 (kWh)")
ui_dict["initial_soc"] = gr.Slider(minimum=10, maximum=95, value=60, step=1,
label="初始SOC (%)")
ui_dict["initial_engine_power"] = gr.Slider(minimum=20, maximum=250, value=50, step=5,
label="初始发动机功率 (kW)")
ui_dict["profile"] = gr.Dropdown( ui_dict["profile"] = gr.Dropdown(
choices=["起飞-巡航-降落", "高机动阶跃", "经济巡航"], choices=["起飞-巡航-降落", "高机动阶跃", "经济巡航"],
value="起飞-巡航-降落", value="起飞-巡航-降落", label="工况模板")
label="工况模板" ui_dict["sim_time"] = gr.Slider(minimum=20, maximum=180, value=60, step=5,
) label="仿真时长 (s)")
ui_dict["sim_time"] = gr.Slider(minimum=20, maximum=180, value=60, step=5, label="仿真时长 (s)") ui_dict["dt"] = gr.Dropdown(choices=[0.02, 0.05, 0.1], value=0.02,
ui_dict["dt"] = gr.Dropdown(choices=[0.02, 0.05, 0.1], value=0.02, label="仿真步长 (s)") label="仿真步长 (s)")
ui_dict["initial_soc"] = gr.Slider(minimum=20, maximum=90, value=60, step=1, label="初始SOC (%)") ui_dict["hybrid_run_button"] = gr.Button("🚀 运行混动系统仿真", variant="primary",
ui_dict["initial_engine_power"] = gr.Slider(minimum=20, maximum=250, value=50, step=5, label="初始发动机功率 (kW)") size="lg", elem_classes="primary-btn")
ui_dict["rpm_scale"] = gr.Slider(minimum=0.6, maximum=1.4, value=1.0, step=0.05, label="目标转速缩放系数")
ui_dict["load_scale"] = gr.Slider(minimum=0.6, maximum=1.4, value=1.0, step=0.05, label="负载转矩缩放系数")
ui_dict["run_button"] = gr.Button("🚀 运行混动算例", variant="primary", elem_classes="primary-btn")
with gr.Group(): with gr.Group():
gr.HTML("<div class='card-title'>📝 结果解读</div>") gr.HTML("<div class='card-title'>📝 结果摘要</div>")
ui_dict["summary"] = gr.Markdown() ui_dict["hybrid_summary"] = gr.Markdown()
with gr.Column(scale=2): with gr.Column(scale=2):
ui_dict["plot"] = gr.Plot(label="混动系统响应图") ui_dict["hybrid_plot"] = gr.Plot(label="混动系统响应图")
ui_dict["table"] = gr.Dataframe( ui_dict["hybrid_table"] = gr.Dataframe(
headers=["时间(s)", "目标转速", "实际转速", "发动机功率(kW)", "电池功率(kW)", "SOC(%)"], headers=["时间(s)", "目标转速", "实际转速", "发动机功率(kW)",
label="关键时刻数据", "电池功率(kW)", "SOC(%)", "EMS模式"],
interactive=False label="关键时刻数据", interactive=False
) )
gr.HTML(f"""
<div id="ems-knowledge" style="max-height:600px;overflow-y:auto;padding-right:8px;">
{EMS_KNOWLEDGE}
</div>
{_mathjax_script('ems-knowledge')}
""")
return ui_dict return ui_dict
def create_chatbot_tab(): def create_chatbot_tab():
@@ -260,3 +562,4 @@ def create_chatbot_tab():
label="💡 试试这些问题:" label="💡 试试这些问题:"
) )
return ui_dict return ui_dict