Files
AutoControlCourse/ui_components.py
T
2026-04-07 19:23:12 +08:00

566 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import gradio as gr
from assets.knowledge_cards_html import (
TIME_DOMAIN_KNOWLEDGE,
FREQUENCY_DOMAIN_KNOWLEDGE,
ROOT_LOCUS_KNOWLEDGE,
ENGINE_CONTROL_KNOWLEDGE,
MOTOR_CONTROL_KNOWLEDGE,
GPR_KNOWLEDGE,
NN_KNOWLEDGE,
EMS_KNOWLEDGE,
)
def create_header():
"""创建页面顶部的标题、横幅和在线计数器"""
gr.HTML("<h1 class='main-title'> 自动控制理论AI+数智平台</h1>")
gr.HTML("<p class='subtitle'>✨ 交互式控制系统分析与设计工具 | 时域·频域·根轨迹·AI问答 ✨</p>")
online_counter = gr.HTML(elem_id="online-counter")
gr.HTML("""
<div class='project-info-banner'>
<div style='display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 25px;'>
<div style='display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.1); padding: 12px 18px; border-radius: 12px; backdrop-filter: blur(10px);'>
<span style='font-size: 1.8em; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));'>📚</span>
<div>
<div style='font-size: 0.8em; color: rgba(255,255,255,0.85); font-weight: 500; letter-spacing: 0.5px;'>课程 Course</div>
<div style='font-weight: 700; font-size: 1.15em; color: white; margin-top: 2px;'>自动控制理论</div>
</div>
</div>
<div style='display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.1); padding: 12px 18px; border-radius: 12px; backdrop-filter: blur(10px);'>
<span style='font-size: 1.8em; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));'>👨‍🏫</span>
<div>
<div style='font-size: 0.8em; color: rgba(255,255,255,0.85); font-weight: 500; letter-spacing: 0.5px;'>负责人 Supervisor</div>
<div style='font-weight: 700; font-size: 1.15em; color: white; margin-top: 2px;'>魏鹏飞</div>
</div>
</div>
<div style='display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.1); padding: 12px 18px; border-radius: 12px; backdrop-filter: blur(10px);'>
<span style='font-size: 1.8em; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));'>📧</span>
<div>
<div style='font-size: 0.8em; color: rgba(255,255,255,0.85); font-weight: 500; letter-spacing: 0.5px;'>联系方式 Contact</div>
<a href='mailto:pengfeiwei@nwpu.edu.cn'
style='font-weight: 700; font-size: 1.15em; color: white; text-decoration: none;
border-bottom: 2px solid rgba(255,255,255,0.5); padding-bottom: 2px; margin-top: 2px; display: inline-block;
transition: all 0.3s ease;'
onmouseover='this.style.borderColor="white"; this.style.transform="translateY(-1px)";'
onmouseout='this.style.borderColor="rgba(255,255,255,0.5)"; this.style.transform="translateY(0)";'>
pengfeiwei@nwpu.edu.cn
</a>
</div>
</div>
</div>
<div style='text-align: center; margin-top: 20px; padding-top: 18px; border-top: 2px solid rgba(255,255,255,0.2);'>
<div style='display: inline-flex; align-items: center; gap: 8px; background: rgba(255,255,255,0.08); padding: 10px 20px; border-radius: 25px; backdrop-filter: blur(5px);'>
<span style='font-size: 1.3em; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));'>🎓</span>
<span style='font-weight: 600; font-size: 1.05em; color: white;'>西北工业大学</span>
<span style='color: white; opacity: 0.9; font-size: 1.05em;'>Northwestern Polytechnical University</span>
</div>
<div style='margin-top: 10px;'>
<span style='color: white; opacity: 0.9; font-size: 0.95em; font-weight: 500;'>2025年校级本科生建设项目资助</span>
</div>
</div>
</div>
""")
return online_counter
def create_time_domain_tab():
"""创建时域分析选项卡的UI组件"""
ui_dict = {}
with gr.Row():
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():
gr.HTML("<div class='card-title'>🔧 系统模型</div>")
ui_dict["tf_display"] = gr.Markdown(label="当前传递函数", elem_classes="output-display")
with gr.Row():
ui_dict["confirm_button"] = gr.Button("✓ 显示传递函数", variant="secondary", scale=1)
ui_dict["analyze_button"] = gr.Button("🚀 开始分析", variant="primary", scale=1, elem_classes="primary-btn")
with gr.Group():
gr.HTML("<div class='card-title'>📈 动态性能指标</div>")
ui_dict["output_metrics"] = gr.Textbox(
label="Performance Metrics", lines=8, interactive=False, elem_classes="output-metrics"
)
with gr.Column(scale=2):
ui_dict["output_plot"] = gr.Plot(label="时域响应曲线", elem_classes="plot-container")
# 知识卡片
gr.HTML(f"""
<div id="time-domain-knowledge" style="max-height: 600px; overflow-y: auto; padding-right: 8px;">
{TIME_DOMAIN_KNOWLEDGE}
</div>
<script>
(function() {{
// 等待DOM加载完成
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', renderMath);
}} else {{
renderMath();
}}
function renderMath() {{
// 延迟执行以确保MathJax已加载
setTimeout(function() {{
if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {{
MathJax.typesetPromise([document.getElementById('time-domain-knowledge')])
.catch((err) => console.log('MathJax渲染错误:', err));
}} else {{
console.log('MathJax未加载,将在500ms后重试');
setTimeout(renderMath, 500);
}}
}}, 300);
}}
}})();
</script>
""")
return ui_dict
def create_frequency_domain_tab():
"""创建频域分析选项卡的UI组件"""
ui_dict = {}
with gr.Row():
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():
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["k_number_display"] = gr.Number(value=10.0, label="当前增益 K", interactive=False, elem_classes="gain-display")
with gr.Group():
gr.HTML("<div class='card-title'>🔧 当前系统模型</div>")
ui_dict["tf_display"] = gr.Markdown(label="含增益K的开环传递函数", elem_classes="output-display")
with gr.Group():
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["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):
ui_dict["plot_output"] = gr.Plot(label="频域响应图", elem_classes="plot-container")
# 知识卡片
gr.HTML(f"""
<div id="freq-domain-knowledge" style="max-height: 600px; overflow-y: auto; padding-right: 8px;">
{FREQUENCY_DOMAIN_KNOWLEDGE}
</div>
<script>
(function() {{
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', renderMath);
}} else {{
renderMath();
}}
function renderMath() {{
setTimeout(function() {{
if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {{
MathJax.typesetPromise([document.getElementById('freq-domain-knowledge')])
.catch((err) => console.log('MathJax渲染错误:', err));
}} else {{
console.log('MathJax未加载,将在500ms后重试');
setTimeout(renderMath, 500);
}}
}}, 300);
}}
}})();
</script>
""")
return ui_dict
def create_root_locus_tab():
"""创建根轨迹分析选项卡的UI组件"""
ui_dict = {}
with gr.Row():
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():
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["k_number_display"] = gr.Number(value=10.0, label="当前增益 K", interactive=False, elem_classes="gain-display")
with gr.Group():
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")
with gr.Row():
ui_dict["analyze_button"] = gr.Button("🚀 开始分析", variant="primary", scale=1, elem_classes="primary-btn")
with gr.Column(scale=2):
ui_dict["plot_output"] = gr.Plot(label="根轨迹图")
gr.HTML(f"""
<div id="root-locus-knowledge" style="max-height: 600px; overflow-y: auto; padding-right: 8px;">
{ROOT_LOCUS_KNOWLEDGE}
</div>
<script>
(function() {{
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', renderMath);
}} else {{
renderMath();
}}
function renderMath() {{
setTimeout(function() {{
if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {{
MathJax.typesetPromise([document.getElementById('root-locus-knowledge')])
.catch((err) => console.log('MathJax渲染错误:', err));
}} else {{
console.log('MathJax未加载,将在500ms后重试');
setTimeout(renderMath, 500);
}}
}}, 300);
}}
}})();
</script>
""")
return ui_dict
def create_case_demo_tab():
"""创建算例演示选项卡 — 四阶段交互设计(蒸馏→发动机→电机→能量管理)"""
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.Column(scale=1):
with gr.Group():
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(
choices=["起飞-巡航-降落", "高机动阶跃", "经济巡航"],
value="起飞-巡航-降落", label="工况模板")
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,
label="仿真步长 (s)")
ui_dict["hybrid_run_button"] = gr.Button("🚀 运行混动系统仿真", variant="primary",
size="lg", elem_classes="primary-btn")
with gr.Group():
gr.HTML("<div class='card-title'>📝 结果摘要</div>")
ui_dict["hybrid_summary"] = gr.Markdown()
with gr.Column(scale=2):
ui_dict["hybrid_plot"] = gr.Plot(label="混动系统响应图")
ui_dict["hybrid_table"] = gr.Dataframe(
headers=["时间(s)", "目标转速", "实际转速", "发动机功率(kW)",
"电池功率(kW)", "SOC(%)", "EMS模式"],
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
def create_chatbot_tab():
"""创建AI问答选项卡的UI组件"""
ui_dict = {}
ui_dict["chatbot"] = gr.Chatbot(
label="🎓 自控原理AI助教",
type="messages",
avatar_images=("https://img.icons8.com/fluency/96/user-male-circle.png", "https://img.icons8.com/fluency/96/chatbot.png"),
height=650,
latex_delimiters=[
{"left": "$$", "right": "$$", "display": True},
{"left": "$", "right": "$", "display": False},
{"left": "\\[", "right": "\\]", "display": True},
{"left": "\\(", "right": "\\)", "display": False}
],
elem_classes="modern-chatbot",
show_copy_button=True
)
with gr.Row():
ui_dict["chat_input"] = gr.Textbox(label="", placeholder="💬 输入您的问题...", scale=4, lines=2, max_lines=4)
with gr.Column(scale=1, min_width=120):
ui_dict["send_button"] = gr.Button("📤 发送", variant="primary", size="lg")
ui_dict["clear_button"] = gr.Button("🗑️ 清空", variant="secondary", size="lg")
gr.Examples(
examples=["什么是传递函数?", "如何判断系统稳定性?", "解释Bode图的物理意义", "PID控制器各参数的作用"],
inputs=ui_dict["chat_input"],
label="💡 试试这些问题:"
)
return ui_dict