Files
AutoControlCourse/app.py
T
2025-10-15 20:30:07 +08:00

1007 lines
55 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
import numpy as np
import control as ct
import matplotlib.pyplot as plt
import re
# ==================== API 配置 ====================
# 将 API 相关配置集中在此处,方便修改
#
# DeepSeek API 配置说明:
# 1. API_KEY: 您的 DeepSeek API 密钥
# - 从 https://platform.deepseek.com/api_keys 获取
# - 或使用环境变量: API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
#
# 2. API_BASE_URL: API 服务的基础 URL
# - 官方地址: https://api.deepseek.com/v1
# - DeepSeek API 兼容 OpenAI 格式
#
# 3. API_MODEL: 使用的 DeepSeek 模型名称
# - deepseek-chat (推荐,性能强大)
# - deepseek-coder (代码专用)
# - 其他可用模型请参考官方文档
API_KEY = "sk-2292af2428d7419897ca1fb6e99ba6bc" # 请在此处填入您的 DeepSeek API 密钥
API_BASE_URL = "https://api.deepseek.com/v1" # API 基础 URL
API_MODEL = "deepseek-chat" # 使用的模型名称
API_TYPE = "deepseek" # API 类型: "deepseek" 或 "gemini"
# ==================================================
import json
import os
# --- [FIXED] 辅助函数:将系数数组转换为LaTeX数学公式字符串 ---
def coeffs_to_latex(coeffs, var='s'):
"""
一个更健壮的函数,用于将系数数组转换为美观的LaTeX字符串。
"""
coeffs = np.atleast_1d(coeffs)
# 如果只有一个系数(常数),直接返回
if coeffs.size == 1:
return f"{coeffs[0]:g}"
latex_str = ""
degree = len(coeffs) - 1
for i, coeff in enumerate(coeffs):
# 跳过系数为0的项
if np.isclose(coeff, 0):
continue
# 确定符号
sign = ""
if i > 0 and latex_str:
if coeff > 0:
sign = " + "
else:
sign = " - "
elif coeff < 0:
sign = "-"
# 处理绝对值
coeff_abs = abs(coeff)
# 处理系数的显示
coeff_str = ""
# 仅当系数不为1或为常数项时显示系数
if not np.isclose(coeff_abs, 1) or degree - i == 0:
coeff_str = f"{coeff_abs:g}"
# 处理变量和幂
power = degree - i
power_str = ""
if power > 0:
power_str = var
if power > 1:
power_str += f"^{{{power}}}"
latex_str += f"{sign}{coeff_str}{power_str}"
return latex_str if latex_str else "0"
# --- 功能函数1:显示传递函数 ---
def display_transfer_function(num_str, den_str):
try:
num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str)
den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str)
num_coeffs = np.array([float(n) for n in num_str_cleaned.split(',') if n])
den_coeffs = np.array([float(d) for d in den_str_cleaned.split(',') if d])
if num_coeffs.size == 0 or den_coeffs.size == 0:
return "分子或分母不能为空"
num_latex = coeffs_to_latex(num_coeffs)
den_latex = coeffs_to_latex(den_coeffs)
tf_latex = f"$$ G(s) = \\frac{{{num_latex}}}{{{den_latex}}} $$"
return tf_latex
except Exception as e:
return f"输入格式错误: {e}"
# --- 功能函数2:执行时域分析和绘图 ---
def time_domain_analysis(num_str, den_str):
try:
num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str)
den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str)
num = np.array([float(n) for n in num_str_cleaned.split(',') if n])
den = np.array([float(d) for d in den_str_cleaned.split(',') if d])
if num.size == 0 or den.size == 0:
return None, "错误:分子或分母系数不能为空。"
system = ct.TransferFunction(num, den)
t = np.linspace(0, 15, 1000)
T_step, yout_step = ct.step_response(system, T=t)
T_impulse, yout_impulse = ct.impulse_response(system, T=t)
try:
info = ct.step_info(system)
if isinstance(info, dict):
metrics_text = (
f"Rise Time: {info.get('RiseTime', float('nan')):.2f} s\n"
f"Peak Time: {info.get('PeakTime', float('inf')):.2f} s\n"
f"Peak: {info.get('Peak', float('inf')):.2f}\n"
f"Overshoot: {info.get('Overshoot', float('nan')):.1f} %\n"
f"Settling Time: {info.get('SettlingTime', float('nan')):.2f} s\n"
f"Steady State Value: {info.get('SteadyStateValue', float('nan')):.2f}"
)
else:
metrics_text = "系统性能指标计算失败。"
except RuntimeError:
metrics_text = "系统可能不稳定,无法计算阶跃响应指标。"
except Exception:
metrics_text = "无法计算所有性能指标。"
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
ax1.plot(T_step, yout_step); ax1.set_title("Unit Step Response"); ax1.set_xlabel("Time (s)"); ax1.set_ylabel("Amplitude"); ax1.grid(True)
ax2.plot(T_impulse, yout_impulse); ax2.set_title("Unit Impulse Response"); ax2.set_xlabel("Time (s)"); ax2.set_ylabel("Amplitude"); ax2.grid(True)
plt.tight_layout()
return fig, metrics_text
except Exception as e:
return None, f"错误: {e}\n请检查您的输入。"
# --- 功能函数3:执行频域分析和绘图 ---
def frequency_domain_analysis(num_str, den_str, k):
try:
num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str)
den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str)
num_coeffs = np.array([float(n) for n in num_str_cleaned.split(',') if n])
den_coeffs = np.array([float(d) for d in den_str_cleaned.split(',') if d])
if num_coeffs.size == 0 or den_coeffs.size == 0:
return None, "错误:分子或分母系数不能为空。", "", ""
system_with_gain = ct.TransferFunction(k * num_coeffs, den_coeffs)
num_latex = coeffs_to_latex(num_coeffs)
den_latex = coeffs_to_latex(den_coeffs)
tf_latex_with_gain = f"$$ G_{{open}}(s) = K \\times \\frac{{{num_latex}}}{{{den_latex}}} \\quad \\text{{where }} K = {k:.2f} $$"
fig = plt.figure(figsize=(12, 6)); gs = fig.add_gridspec(2, 2)
ax_mag = fig.add_subplot(gs[0, 0]); ax_phase = fig.add_subplot(gs[1, 0], sharex=ax_mag)
omega_range = np.logspace(-2, 3, 1000)
mag, phase, omega = ct.frequency_response(system_with_gain, omega_range)
ax_mag.semilogx(omega, 20 * np.log10(mag)); ax_mag.grid(True, which='both'); ax_mag.set_ylabel("Magnitude (dB)"); ax_mag.set_title("Bode Plot")
ax_phase.semilogx(omega, np.rad2deg(phase)); ax_phase.grid(True, which='both'); ax_phase.set_ylabel("Phase (deg)"); ax_phase.set_xlabel("Frequency (rad/s)"); ax_phase.axhline(y=-180, color='r', linestyle='--', linewidth=0.8)
ax_nyquist = fig.add_subplot(gs[:, 1])
ct.nyquist_plot(system_with_gain, ax=ax_nyquist); ax_nyquist.set_title("Nyquist Plot"); ax_nyquist.grid(True)
plt.tight_layout()
try:
gm, pm, _, _ = ct.margin(system_with_gain)
gm_db = 20 * np.log10(gm) if gm > 0 and np.isfinite(gm) else float('inf')
is_stable = gm_db > 0 and pm > 0
stability_text = f"**Evaluation**: <font color='{'green' if is_stable else 'red'}'>**{'System Stable' if is_stable else 'System Unstable'}**</font>"
metrics_text = ""
if np.isinf(gm_db): metrics_text += f"Gain Margin (GM): inf dB\n(Note: Phase never crosses -180° line)\n"
else: metrics_text += f"Gain Margin (GM): {gm_db:.2f} dB\n"
if np.isinf(pm): metrics_text += f"Phase Margin (PM): not defined"
else: metrics_text += f"Phase Margin (PM): {pm:.2f} deg"
except Exception as e:
metrics_text = f"Unable to compute stability margins."; stability_text = "**Evaluation**: <font color='orange'>**Cannot determine**</font>"
return fig, metrics_text, tf_latex_with_gain, stability_text
except Exception as e:
return None, f"错误: {e}", "", ""
# --- 功能函数4:执行根轨迹分析 ---
def root_locus_analysis(num_str, den_str, log_k):
try:
k = 10**log_k
num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str)
den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str)
num_coeffs = np.array([float(n) for n in num_str_cleaned.split(',') if n])
den_coeffs = np.array([float(d) for d in den_str_cleaned.split(',') if d])
if num_coeffs.size == 0 or den_coeffs.size == 0:
return None, "错误:分子或分母系数不能为空。", k
open_loop_system = ct.TransferFunction(num_coeffs, den_coeffs)
fig, ax = plt.subplots(figsize=(8, 6))
closed_loop_system = ct.feedback(k * open_loop_system, 1)
current_poles = ct.poles(closed_loop_system)
ol_poles = open_loop_system.poles(); ol_zeros = open_loop_system.zeros()
points_of_interest = np.concatenate(([0j], ol_poles, ol_zeros, current_poles))
min_real = np.min(np.real(points_of_interest)); max_real = np.max(np.real(points_of_interest))
min_imag = np.min(np.imag(points_of_interest)); max_imag = np.max(np.imag(points_of_interest))
center_real = (max_real + min_real) / 2; span_real = max(abs(max_real - min_real), 2) * 1.5
center_imag = (max_imag + min_imag) / 2; span_imag = max(abs(max_imag - min_imag), 2) * 1.5
max_span = max(span_real, span_imag)
rlist, klist = ct.root_locus(open_loop_system, plot=False, grid=False)
for i in range(rlist.shape[1]): ax.plot(np.real(rlist[:, i]), np.imag(rlist[:, i]), 'b-')
ax.set_xlim(center_real - max_span / 2, center_real + max_span / 2); ax.set_ylim(center_imag - max_span / 2, center_imag + max_span / 2)
ax.plot(np.real(current_poles), np.imag(current_poles), 'rx', markersize=10, markeredgewidth=2, label=f'Poles at K={k:.2f}')
ax.set_xlabel("Real Axis"); ax.set_ylabel("Imaginary Axis"); ax.set_title("Root Locus"); ax.grid(True); ax.legend(loc='upper right'); ax.set_aspect('equal', adjustable='box')
poles_text = "Closed-Loop Poles:\n"
for p in current_poles: poles_text += f"{p.real:.3f} {'+' if p.imag >= 0 else '-'} {abs(p.imag):.3f}j\n"
return fig, poles_text, k
except Exception as e:
return None, f"错误: {e}", 10**log_k
# --- [新增] 功能函数5: AI 智能问答 (支持 DeepSeek 和 Gemini) ---
# 注意: API 配置已移至文件开头的配置区域,方便统一管理和修改
# 如果您在本地运行并设置了环境变量,可以在配置区域使用:
# API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
# 异步函数以处理流式响应
async def chat_with_ai(message, history):
"""
与 AI 模型进行流式对话。支持 DeepSeek 和 Gemini API。
"""
# 系统指令,设定AI的角色和回答风格
system_prompt = "你是一位精通自动控制原理的专家教授。请用清晰、准确、专业的中文来回答有关自动控制课程内容的问题。在适当的时候,可以使用公式和示例来辅助解释。"
# 检查 API_KEY 是否配置
if not API_KEY or API_KEY.strip() == "":
history.append([message, "❌ 错误:API_KEY 未配置。请在文件开头配置 API_KEY。"])
yield history
return
# 初始化机器人回复
bot_response = ""
history.append([message, "正在思考..."])
yield history # 立即显示用户消息
try:
import aiohttp
if API_TYPE == "deepseek":
# DeepSeek API (OpenAI 兼容格式)
api_url = f"{API_BASE_URL}/chat/completions"
# 构造消息历史
messages = [{"role": "system", "content": system_prompt}]
for user_msg, bot_msg in history[:-1]: # 排除最后一条(刚添加的)
if user_msg:
messages.append({"role": "user", "content": user_msg})
if bot_msg:
messages.append({"role": "assistant", "content": bot_msg})
messages.append({"role": "user", "content": message})
payload = {
"model": API_MODEL,
"messages": messages,
"stream": True,
"temperature": 0.7,
"max_tokens": 2048
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}"
}
async with aiohttp.ClientSession() as session:
async with session.post(api_url, json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=60)) as response:
if response.status == 200:
# 处理流式响应
async for line in response.content:
line = line.decode('utf-8').strip()
if not line or line == "data: [DONE]":
continue
if line.startswith("data: "):
line = line[6:]
try:
data = json.loads(line)
if "choices" in data and len(data["choices"]) > 0:
delta = data["choices"][0].get("delta", {})
content = delta.get("content", "")
if content:
bot_response += content
history[-1][1] = bot_response
yield history
except json.JSONDecodeError:
pass
if not bot_response:
history[-1][1] = "⚠️ API 返回了空响应,请稍后重试。"
yield history
else:
error_text = await response.text()
history[-1][1] = f"❌ API请求出错 (状态码: {response.status}):\n{error_text}"
yield history
else: # Gemini API
api_url = f"{API_BASE_URL}/models/{API_MODEL}:streamGenerateContent?key={API_KEY}"
# 构造 Gemini 格式的消息历史
api_history = []
for user_msg, bot_msg in history[:-1]:
if user_msg:
api_history.append({"role": "user", "parts": [{"text": user_msg}]})
if bot_msg:
api_history.append({"role": "model", "parts": [{"text": bot_msg}]})
payload = {
"contents": api_history + [{"role": "user", "parts": [{"text": message}]}],
"systemInstruction": {"parts": [{"text": system_prompt}]},
"generationConfig": {
"temperature": 0.7,
"topK": 1,
"topP": 1,
"maxOutputTokens": 2048,
}
}
async with aiohttp.ClientSession() as session:
async with session.post(api_url, json=payload, headers={'Content-Type': 'application/json'}, timeout=aiohttp.ClientTimeout(total=60)) as response:
if response.status == 200:
has_content = False
async for chunk in response.content.iter_any():
chunk_text = chunk.decode('utf-8')
for line in chunk_text.split('\n'):
if not line.strip():
continue
if line.startswith('data: '):
line = line[6:]
try:
data = json.loads(line)
if "candidates" in data and len(data["candidates"]) > 0:
candidate = data["candidates"][0]
if "content" in candidate and "parts" in candidate["content"]:
text_part = candidate["content"]["parts"][0].get("text", "")
if text_part:
has_content = True
bot_response += text_part
history[-1][1] = bot_response
yield history
except json.JSONDecodeError:
pass
if not has_content:
history[-1][1] = "⚠️ API 返回了空响应,请稍后重试。"
yield history
else:
error_text = await response.text()
history[-1][1] = f"❌ API请求出错 (状态码: {response.status}):\n{error_text}"
yield history
except aiohttp.ClientError as e:
history[-1][1] = f"❌ 网络连接错误: {e}\n请检查网络连接或 API_BASE_URL 配置。"
yield history
except Exception as e:
history[-1][1] = f"❌ 发生错误: {type(e).__name__}: {e}"
yield history
# --- Gradio 界面定义 ---
# 自定义 CSS 样式
custom_css = """
/* 整体界面样式优化 */
.gradio-container {
font-family: 'Segoe UI', Arial, sans-serif !important;
}
/* 标题样式 */
.main-title {
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 2.5em !important;
font-weight: bold;
margin-bottom: 0.5em;
}
.subtitle {
text-align: center;
color: #666;
font-size: 1.1em;
margin-bottom: 2em;
}
/* 可滚动的知识卡片 */
.knowledge-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 12px;
padding: 20px;
margin-top: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-height: 500px;
overflow-y: auto;
}
.knowledge-card::-webkit-scrollbar {
width: 8px;
}
.knowledge-card::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.knowledge-card::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.knowledge-card::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* 按钮样式优化 */
.primary-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border: none !important;
color: white !important;
font-weight: bold !important;
transition: transform 0.2s;
}
.primary-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important;
}
/* 标签页样式 */
.tab-nav button {
font-weight: 600 !important;
font-size: 1.05em !important;
}
/* 图表容器 */
.plot-container {
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
/* 输入框样式 */
.input-section {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
/* 卡片标题 */
.card-title {
font-size: 1.3em;
font-weight: bold;
color: #333;
margin-bottom: 10px;
border-bottom: 3px solid #667eea;
padding-bottom: 8px;
}
"""
with gr.Blocks(title="自动控制原理学习网站", css=custom_css) as demo:
gr.Markdown("<h1 class='main-title'> 自动控制原理AI+数智平台</h1>")
gr.Markdown("<p class='subtitle'>交互式控制系统分析与设计工具 | 时域·频域·根轨迹·AI问答</p>")
with gr.Tabs() as tabs:
with gr.TabItem("时域分析 (Time Domain)", id=0):
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.Markdown("<div class='card-title'>📊 输入系统参数</div>")
num_input = gr.Textbox(label="传递函数分子系数 (Numerator)", value="1", placeholder="例如: 1 或 1,2,3")
den_input = gr.Textbox(label="传递函数分母系数 (Denominator)", value="1,6,11,6", placeholder="例如: 1,2,1")
with gr.Row():
confirm_button = gr.Button("✓ 显示传递函数", variant="secondary", scale=1)
analyze_button = gr.Button("🚀 开始分析", variant="primary", scale=1, elem_classes="primary-btn")
with gr.Group():
gr.Markdown("<div class='card-title'>🔧 系统模型</div>")
tf_display = gr.Markdown(label="当前传递函数")
with gr.Group():
gr.Markdown("<div class='card-title'>📈 动态性能指标</div>")
output_metrics = gr.Textbox(label="Performance Metrics", lines=8, interactive=False)
with gr.Column(scale=2):
output_plot = gr.Plot(label="Time Response Curves", elem_classes="plot-container")
# 使用 HTML 创建可滚动的知识卡片
gr.HTML("""
<div class='knowledge-card'>
<h3 style='color: #667eea; margin-top: 0;'>📚 常用公式 (Common Formulas)</h3>
<h4 style='color: #333; border-bottom: 2px solid #e0e0e0; padding-bottom: 5px;'>二阶系统标准形式</h4>
<p>对于一个典型的二阶系统,其标准传递函数形式为:</p>
<p style='text-align: center; font-size: 1.1em;'>G(s) = ω<sub>n</sub>² / (s² + 2ζω<sub>n</sub>s + ω<sub>n</sub>²)</p>
<h4 style='color: #333; margin-top: 20px;'>参数说明</h4>
<ul style='line-height: 1.8;'>
<li><strong>ζ (zeta)</strong> - 阻尼比 (Damping Ratio)
<ul style='margin-left: 20px; color: #666;'>
<li>ζ &lt; 0: 不稳定系统</li>
<li>ζ = 0: 无阻尼振荡(临界情况)</li>
<li>0 &lt; ζ &lt; 1: 欠阻尼(有振荡)⭐ 最常见</li>
<li>ζ = 1: 临界阻尼(无振荡)</li>
<li>ζ &gt; 1: 过阻尼(无振荡,响应慢)</li>
</ul>
</li>
<li><strong>ω<sub>n</sub> (omega_n)</strong> - 无阻尼自然频率
<ul style='margin-left: 20px; color: #666;'>
<li>单位:rad/s</li>
<li>表示系统的固有振荡频率</li>
<li>值越大,系统响应越快</li>
</ul>
</li>
</ul>
<h4 style='color: #333; margin-top: 20px; border-bottom: 2px solid #e0e0e0; padding-bottom: 5px;'>时域性能指标(欠阻尼系统,0 &lt; ζ &lt; 1</h4>
<div style='background: #f0f4ff; padding: 15px; border-radius: 8px; margin: 10px 0;'>
<strong>1️⃣ 上升时间 (Rise Time, t<sub>r</sub>)</strong>
<p>响应从终值的10%上升到90%所需的时间</p>
<p>📐 近似公式:t<sub>r</sub> ≈ 1.8 / ω<sub>n</sub></p>
</div>
<div style='background: #fff4f0; padding: 15px; border-radius: 8px; margin: 10px 0;'>
<strong>2️⃣ 峰值时间 (Peak Time, t<sub>p</sub>)</strong>
<p>响应达到第一个峰值所需的时间</p>
<p>📐 公式:t<sub>p</sub> = π / (ω<sub>n</sub>√(1-ζ²)) = π / ω<sub>d</sub></p>
<p style='color: #666;'>其中 ω<sub>d</sub> = ω<sub>n</sub>√(1-ζ²) 是阻尼振荡频率</p>
</div>
<div style='background: #f0fff4; padding: 15px; border-radius: 8px; margin: 10px 0;'>
<strong>3️⃣ 超调量 (Percent Overshoot, σ%)</strong>
<p>响应超过稳态值的最大百分比</p>
<p>📐 公式:σ% = e<sup>(-πζ/√(1-ζ²))</sup> × 100%</p>
<p style='color: #666;'>仅与阻尼比 ζ 有关</p>
<p>💡 常见值:
<br>• ζ = 0.5 时,σ% ≈ 16%
<br>• ζ = 0.707 时,σ% ≈ 4.3%
</p>
</div>
<div style='background: #fff0f8; padding: 15px; border-radius: 8px; margin: 10px 0;'>
<strong>4️⃣ 调节时间 (Settling Time, t<sub>s</sub>)</strong>
<p>响应达到并保持在稳态值 ±2%(或±5%)范围内所需的时间</p>
<p>📐 公式:
<br>• 2%误差带:t<sub>s</sub> ≈ 4 / (ζω<sub>n</sub>)
<br>• 5%误差带:t<sub>s</sub> ≈ 3 / (ζω<sub>n</sub>)
</p>
<p style='color: #666;'>主要由 ζω<sub>n</sub> 决定(系统时间常数)</p>
</div>
<div style='background: #f8f0ff; padding: 15px; border-radius: 8px; margin: 10px 0;'>
<strong>5️⃣ 稳态误差 (Steady-State Error, e<sub>ss</sub>)</strong>
<p>不同输入下的稳态误差:</p>
<p>• 单位阶跃输入:e<sub>ss</sub> = 1/(1+K<sub>p</sub>)
<br>• 单位斜坡输入:e<sub>ss</sub> = 1/K<sub>v</sub>
<br>• 单位抛物线输入:e<sub>ss</sub> = 1/K<sub>a</sub></p>
<p style='color: #666; font-size: 0.9em;'>其中 K<sub>p</sub>, K<sub>v</sub>, K<sub>a</sub> 分别为位置、速度、加速度误差常数</p>
</div>
</div>
""")
with gr.TabItem("频域分析 (Frequency Domain)", id=1):
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.Markdown("<div class='card-title'>🎚️ 调整系统增益</div>")
log_k_slider_freq = gr.Slider(minimum=-1, maximum=3, value=1, step=0.01, label="对数增益 log10(K)")
k_number_display_freq = gr.Number(value=10.0, label="增益 K (Gain)", interactive=False)
with gr.Group():
gr.Markdown("<div class='card-title'>🔧 当前系统模型</div>")
freq_tf_display = gr.Markdown(label="含增益K的传递函数")
with gr.Group():
gr.Markdown("<div class='card-title'>📊 稳定裕度</div>")
freq_metrics_display = gr.Textbox(label="Stability Margins", lines=4, interactive=False)
freq_stability_display = gr.Markdown()
with gr.Column(scale=2):
freq_plot_output = gr.Plot(label="Frequency Response Plots", elem_classes="plot-container")
# 频域分析知识卡片
gr.HTML("""
<div class='knowledge-card'>
<h3 style='color: #667eea; margin-top: 0;'>📚 常用定义 (Common Definitions)</h3>
<details open>
<summary style='cursor: pointer; font-weight: bold; color: #333; font-size: 1.1em; padding: 10px; background: #f0f4ff; border-radius: 5px; margin: 10px 0;'>📈 频域分析基础</summary>
<div style='padding: 10px; line-height: 1.8;'>
<p>频域分析通过研究系统对不同频率正弦信号的响应特性来评估系统性能。</p>
<ul>
<li><strong>Bode图</strong>:幅频和相频特性</li>
<li><strong>Nyquist图</strong>:极坐标表示</li>
<li><strong>稳定裕度</strong>:系统稳定性余量</li>
</ul>
</div>
</details>
<details open>
<summary style='cursor: pointer; font-weight: bold; color: #333; font-size: 1.1em; padding: 10px; background: #fff0f8; border-radius: 5px; margin: 10px 0;'>🎯 增益裕度 (Gain Margin, GM)</summary>
<div style='padding: 10px;'>
<p><strong>定义:</strong> 在相角为-180°时,系统增益可以增加的最大倍数(或dB数),而不会使系统变得不稳定。</p>
<p style='background: #f5f5f5; padding: 10px; border-radius: 5px; text-align: center;'>
GM<sub>dB</sub> = -20 log<sub>10</sub> |G(jω<sub>pc</sub>)|
</p>
<h4>关键概念</h4>
<p><strong>ω<sub>pc</sub></strong> (相角交越频率):系统相角等于-180°时的频率</p>
<h4>判断准则</h4>
<table style='width: 100%; border-collapse: collapse; margin: 10px 0;'>
<tr style='background: #e8f5e9;'><td style='padding: 8px; border: 1px solid #ddd;'>GM > 0 dB</td><td style='padding: 8px; border: 1px solid #ddd;'>✅ 系统稳定</td></tr>
<tr style='background: #fff3e0;'><td style='padding: 8px; border: 1px solid #ddd;'>GM = 0 dB</td><td style='padding: 8px; border: 1px solid #ddd;'>⚠️ 临界稳定</td></tr>
<tr style='background: #ffebee;'><td style='padding: 8px; border: 1px solid #ddd;'>GM < 0 dB</td><td style='padding: 8px; border: 1px solid #ddd;'>❌ 系统不稳定</td></tr>
</table>
<p style='background: #e3f2fd; padding: 10px; border-radius: 5px;'>
💡 <strong>工程要求:</strong> 通常要求 GM ≥ 6 dB (约2倍),提供对增益变化的鲁棒性
</p>
</div>
</details>
<details open>
<summary style='cursor: pointer; font-weight: bold; color: #333; font-size: 1.1em; padding: 10px; background: #f0fff4; border-radius: 5px; margin: 10px 0;'>🎯 相角裕度 (Phase Margin, PM)</summary>
<div style='padding: 10px;'>
<p><strong>定义:</strong> 在增益为1(0dB)时,系统相角与-180°之间的差值。</p>
<p style='background: #f5f5f5; padding: 10px; border-radius: 5px; text-align: center;'>
PM = 180° + ∠G(jω<sub>gc</sub>)
</p>
<h4>关键概念</h4>
<p><strong>ω<sub>gc</sub></strong> (增益交越频率):系统幅值等于1(0dB)时的频率</p>
<h4>判断准则</h4>
<table style='width: 100%; border-collapse: collapse; margin: 10px 0;'>
<tr style='background: #e8f5e9;'><td style='padding: 8px; border: 1px solid #ddd;'>PM > 0°</td><td style='padding: 8px; border: 1px solid #ddd;'>✅ 系统稳定</td></tr>
<tr style='background: #fff3e0;'><td style='padding: 8px; border: 1px solid #ddd;'>PM = 0°</td><td style='padding: 8px; border: 1px solid #ddd;'>⚠️ 临界稳定</td></tr>
<tr style='background: #ffebee;'><td style='padding: 8px; border: 1px solid #ddd;'>PM < 0°</td><td style='padding: 8px; border: 1px solid #ddd;'>❌ 系统不稳定</td></tr>
</table>
<p style='background: #e3f2fd; padding: 10px; border-radius: 5px;'>
💡 <strong>工程要求:</strong> 通常要求 PM ≥ 30° ~ 60°<br>
• PM ≈ 45° ~ 60° : 良好的阻尼特性<br>
• PM 越大,系统超调量越小
</p>
<h4>与时域性能的关系</h4>
<p>对于二阶系统:ζ ≈ PM/100 (PM以度为单位)</p>
<table style='width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 0.9em;'>
<tr style='background: #f5f5f5; font-weight: bold;'><td style='padding: 6px; border: 1px solid #ddd;'>PM</td><td style='padding: 6px; border: 1px solid #ddd;'>ζ</td><td style='padding: 6px; border: 1px solid #ddd;'>超调量</td></tr>
<tr><td style='padding: 6px; border: 1px solid #ddd;'>30°</td><td style='padding: 6px; border: 1px solid #ddd;'>≈ 0.3</td><td style='padding: 6px; border: 1px solid #ddd;'>≈ 37%</td></tr>
<tr><td style='padding: 6px; border: 1px solid #ddd;'>45°</td><td style='padding: 6px; border: 1px solid #ddd;'>≈ 0.45</td><td style='padding: 6px; border: 1px solid #ddd;'>≈ 20%</td></tr>
<tr><td style='padding: 6px; border: 1px solid #ddd;'>60°</td><td style='padding: 6px; border: 1px solid #ddd;'>≈ 0.6</td><td style='padding: 6px; border: 1px solid #ddd;'>≈ 10%</td></tr>
</table>
</div>
</details>
<details>
<summary style='cursor: pointer; font-weight: bold; color: #333; font-size: 1.1em; padding: 10px; background: #f8f0ff; border-radius: 5px; margin: 10px 0;'>📊 其他重要概念</summary>
<div style='padding: 10px;'>
<h4>带宽 (Bandwidth, BW)</h4>
<p>闭环频率响应的幅值下降到-3dB时的频率。BW 越大,系统响应越快。</p>
<h4>谐振峰值 (Resonant Peak, M<sub>r</sub>)</h4>
<p>闭环频率响应的最大幅值。M<sub>r</sub> 越小,系统阻尼越好。通常要求 M<sub>r</sub> < 1.3 ~ 1.5。</p>
</div>
</details>
</div>
""")
with gr.TabItem("根轨迹 (Root Locus)", id=2):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 1. 调整系统增益"); log_k_slider_rl = gr.Slider(minimum=-1, maximum=3, value=1, step=0.01, label="对数增益 log10(K)")
k_number_display = gr.Number(value=10.0, label="增益 K (Gain)", interactive=False); gr.Markdown("### 2. 当前增益下的闭环极点")
rl_poles_display = gr.Textbox(label="Closed-Loop Pole Locations", lines=4, interactive=False)
with gr.Column(scale=2):
rl_plot_output = gr.Plot(label="Root Locus Plot")
rl_formula_text = """
<div class="knowledge-card">
<h3 class="card-title">📚 根轨迹知识要点 (Root Locus Essentials)</h3>
<details open>
<summary style="background: #fff0f8; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold;">
🎯 根轨迹法简介
</summary>
<div style="padding: 10px; background: #fafafa; margin-top: 5px; border-radius: 5px;">
<p><strong>定义:</strong> 根轨迹是当开环系统增益 <strong>K</strong> 从 0 变化到 <strong>∞</strong> 时,闭环系统特征方程的根(极点)在 s 平面上描绘出的轨迹。</p>
<p><strong>主要作用:</strong></p>
<ul>
<li>✅ 直观显示参数变化对系统极点位置的影响</li>
<li>✅ 判断系统稳定性</li>
<li>✅ 选择合适的增益值</li>
<li>✅ 设计控制器参数</li>
</ul>
</div>
</details>
<details>
<summary style="background: #f0fff4; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; margin-top: 10px;">
🔧 闭环系统与特征方程
</summary>
<div style="padding: 10px; background: #fafafa; margin-top: 5px; border-radius: 5px;">
<p>对于单位负反馈系统,闭环传递函数为:</p>
<p style="text-align: center; background: white; padding: 10px; border-radius: 5px;">
T(s) = KG(s) / [1 + KG(s)H(s)]
</p>
<p><strong>特征方程:</strong></p>
<p style="text-align: center; background: white; padding: 10px; border-radius: 5px;">
1 + K G(s)H(s) = 0 或 K G(s)H(s) = -1
</p>
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
<tr style="background: #e3f2fd;">
<th style="padding: 8px; border: 1px solid #ddd;">符号</th>
<th style="padding: 8px; border: 1px solid #ddd;">含义</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">G(s)</td>
<td style="padding: 8px; border: 1px solid #ddd;">前向通道传递函数</td>
</tr>
<tr style="background: #f5f5f5;">
<td style="padding: 8px; border: 1px solid #ddd;">H(s)</td>
<td style="padding: 8px; border: 1px solid #ddd;">反馈通道传递函数(单位反馈时 H(s)=1</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">K</td>
<td style="padding: 8px; border: 1px solid #ddd;">可变增益参数</td>
</tr>
<tr style="background: #f5f5f5;">
<td style="padding: 8px; border: 1px solid #ddd;">特征方程的根</td>
<td style="padding: 8px; border: 1px solid #ddd;">闭环极点</td>
</tr>
</table>
</div>
</details>
<details>
<summary style="background: #f8f0ff; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; margin-top: 10px;">
📐 根轨迹绘制的基本条件
</summary>
<div style="padding: 10px; background: #fafafa; margin-top: 5px; border-radius: 5px;">
<p>设开环传递函数为:</p>
<p style="text-align: center; background: white; padding: 10px; border-radius: 5px;">
G(s)H(s) = K(s-z<sub>1</sub>)(s-z<sub>2</sub>)···(s-z<sub>m</sub>) / [(s-p<sub>1</sub>)(s-p<sub>2</sub>)···(s-p<sub>n</sub>)]
</p>
<p><strong>根轨迹上的点 s<sub>0</sub> 必须满足:</strong></p>
<div style="background: #e8f5e9; padding: 10px; border-left: 4px solid #4caf50; margin: 10px 0;">
<p><strong>1️⃣ 幅值条件(充要条件)</strong></p>
<p style="text-align: center;">|K G(s<sub>0</sub>)H(s<sub>0</sub>)| = 1</p>
<p>💡 <strong>物理意义:</strong> 确定增益 K 的值,使得 s<sub>0</sub> 成为闭环极点。</p>
</div>
<div style="background: #fff3e0; padding: 10px; border-left: 4px solid #ff9800; margin: 10px 0;">
<p><strong>2️⃣ 相角条件(充要条件)</strong></p>
<p style="text-align: center;">∠G(s<sub>0</sub>)H(s<sub>0</sub>) = (2k+1)180°</p>
<p style="text-align: center; font-size: 0.9em;">其中 k = 0, ±1, ±2, ±3, ...</p>
<p>💡 <strong>物理意义:</strong> 判断 s 平面上某点是否在根轨迹上。</p>
<p><strong>角度计算公式:</strong></p>
<p style="text-align: center;">∠G(s<sub>0</sub>)H(s<sub>0</sub>) = Σ∠(s<sub>0</sub>-z<sub>i</sub>) - Σ∠(s<sub>0</sub>-p<sub>j</sub>)</p>
<ul style="font-size: 0.9em;">
<li>从所有零点到 s<sub>0</sub> 的角度之和</li>
<li>减去从所有极点到 s<sub>0</sub> 的角度之和</li>
</ul>
</div>
</div>
</details>
<details>
<summary style="background: #fff0f8; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; margin-top: 10px;">
🌟 根轨迹的基本性质
</summary>
<div style="padding: 10px; background: #fafafa; margin-top: 5px; border-radius: 5px;">
<div style="background: white; padding: 10px; margin: 10px 0; border-radius: 5px; border: 1px solid #ddd;">
<p><strong>1️⃣ 起点和终点</strong></p>
<ul>
<li>🟢 <strong>起点</strong> (K=0):开环极点 p<sub>j</sub></li>
<li>🔴 <strong>终点</strong> (K→∞):开环零点 z<sub>i</sub> 或无穷远处</li>
</ul>
</div>
<div style="background: white; padding: 10px; margin: 10px 0; border-radius: 5px; border: 1px solid #ddd;">
<p><strong>2️⃣ 根轨迹分支数</strong></p>
<ul>
<li>分支数 = max(n, m),其中 n=极点数,m=零点数</li>
<li>当 n > m 时,有 (n-m) 条分支趋向无穷远</li>
</ul>
</div>
<div style="background: white; padding: 10px; margin: 10px 0; border-radius: 5px; border: 1px solid #ddd;">
<p><strong>3️⃣ 实轴上的根轨迹</strong></p>
<p>实轴上某区段,若其右侧的实数开环零点和极点总数为<strong>奇数</strong>,则该区段在根轨迹上。</p>
</div>
<div style="background: white; padding: 10px; margin: 10px 0; border-radius: 5px; border: 1px solid #ddd;">
<p><strong>4️⃣ 渐近线</strong></p>
<p>当 n > m 时,有 (n-m) 条分支沿渐近线趋向无穷远:</p>
<p style="text-align: center; background: #f5f5f5; padding: 8px; margin: 5px 0;">
<strong>渐近线角度:</strong> φ<sub>a</sub> = (2k+1)180° / (n-m)
</p>
<p style="text-align: center; font-size: 0.9em;">k = 0, 1, 2, ..., (n-m-1)</p>
<p style="text-align: center; background: #f5f5f5; padding: 8px; margin: 5px 0;">
<strong>渐近线交点(重心):</strong> σ<sub>a</sub> = (Σp<sub>j</sub> - Σz<sub>i</sub>) / (n-m)
</p>
</div>
<div style="background: white; padding: 10px; margin: 10px 0; border-radius: 5px; border: 1px solid #ddd;">
<p><strong>5️⃣ 分离点/会合点</strong></p>
<ul>
<li>定义:多条根轨迹分支分离或会合的点</li>
<li>求解条件:dK/ds = 0</li>
</ul>
</div>
</div>
</details>
<details>
<summary style="background: #f0fff4; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; margin-top: 10px;">
🛡️ s 平面的稳定性区域
</summary>
<div style="padding: 10px; background: #fafafa; margin-top: 5px; border-radius: 5px;">
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
<tr style="background: #e3f2fd;">
<th style="padding: 8px; border: 1px solid #ddd;">区域</th>
<th style="padding: 8px; border: 1px solid #ddd;">条件</th>
<th style="padding: 8px; border: 1px solid #ddd;">稳定性</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">左半平面</td>
<td style="padding: 8px; border: 1px solid #ddd;">Re(s) < 0</td>
<td style="padding: 8px; border: 1px solid #ddd; background: #c8e6c9;">✅ 稳定</td>
</tr>
<tr style="background: #f5f5f5;">
<td style="padding: 8px; border: 1px solid #ddd;">虚轴</td>
<td style="padding: 8px; border: 1px solid #ddd;">Re(s) = 0</td>
<td style="padding: 8px; border: 1px solid #ddd; background: #fff9c4;">⚠️ 临界稳定</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">右半平面</td>
<td style="padding: 8px; border: 1px solid #ddd;">Re(s) > 0</td>
<td style="padding: 8px; border: 1px solid #ddd; background: #ffcdd2;">❌ 不稳定</td>
</tr>
</table>
<div style="background: #e3f2fd; padding: 10px; margin-top: 10px; border-radius: 5px;">
<p><strong>稳定性判断准则:</strong></p>
<ul>
<li>✅ 所有闭环极点都在左半平面 → 系统稳定</li>
<li>❌ 有极点在右半平面 → 系统不稳定</li>
<li>⚠️ 有极点在虚轴上 → 临界稳定</li>
</ul>
</div>
</div>
</details>
<details>
<summary style="background: #f8f0ff; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; margin-top: 10px;">
📏 阻尼比等值线
</summary>
<div style="padding: 10px; background: #fafafa; margin-top: 5px; border-radius: 5px;">
<p>从原点出发的射线代表恒定阻尼比 ζ 的轨迹:</p>
<p style="text-align: center; background: white; padding: 10px; border-radius: 5px;">
θ = arccos(ζ)
</p>
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
<tr style="background: #e3f2fd;">
<th style="padding: 8px; border: 1px solid #ddd;">阻尼比 ζ</th>
<th style="padding: 8px; border: 1px solid #ddd;">角度 θ</th>
<th style="padding: 8px; border: 1px solid #ddd;">系统响应特性</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">0.5</td>
<td style="padding: 8px; border: 1px solid #ddd;">60°</td>
<td style="padding: 8px; border: 1px solid #ddd;">欠阻尼,有较大超调</td>
</tr>
<tr style="background: #f5f5f5;">
<td style="padding: 8px; border: 1px solid #ddd;">0.707</td>
<td style="padding: 8px; border: 1px solid #ddd;">45°</td>
<td style="padding: 8px; border: 1px solid #ddd;">最佳阻尼,超调适中</td>
</tr>
</table>
<div style="background: #e8f5e9; padding: 10px; margin-top: 10px; border-radius: 5px;">
<p><strong>💡 工程应用:</strong></p>
<p>通过根轨迹与阻尼比等值线的交点,可以选择满足动态性能要求的增益 K 值。</p>
</div>
</div>
</details>
</div>
"""
gr.HTML(rl_formula_text)
with gr.TabItem("智能问答 (Q&A)", id=3):
chatbot = gr.Chatbot(
label="自控原理小助手",
bubble_full_width=False,
avatar_images=(None, "https://img.icons8.com/plasticine/100/bot.png"),
height=600,
latex_delimiters=[
{"left": "$$", "right": "$$", "display": True},
{"left": "$", "right": "$", "display": False},
{"left": "\\[", "right": "\\]", "display": True},
{"left": "\\(", "right": "\\)", "display": False}
]
)
chat_input = gr.Textbox(label="您的问题", placeholder="你好,请解释一下什么是PID控制器?", scale=4)
send_button = gr.Button("发送", variant="primary", scale=1)
# --- 事件绑定部分 ---
confirm_button.click(fn=display_transfer_function, inputs=[num_input, den_input], outputs=tf_display)
analyze_button.click(fn=time_domain_analysis, inputs=[num_input, den_input], outputs=[output_plot, output_metrics])
def update_frequency_analysis(num, den, log_k):
k = 10**log_k
fig, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k)
return fig, metrics, tf_latex, stability, k
log_k_slider_freq.release(fn=update_frequency_analysis, inputs=[num_input, den_input, log_k_slider_freq], outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq])
num_input.change(fn=update_frequency_analysis, inputs=[num_input, den_input, log_k_slider_freq], outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq])
den_input.change(fn=update_frequency_analysis, inputs=[num_input, den_input, log_k_slider_freq], outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq])
def update_rl_view(log_k, num, den):
fig, poles, k_val = root_locus_analysis(num, den, log_k)
return fig, poles, k_val
log_k_slider_rl.release(fn=update_rl_view, inputs=[log_k_slider_rl, num_input, den_input], outputs=[rl_plot_output, rl_poles_display, k_number_display])
num_input.change(fn=update_rl_view, inputs=[log_k_slider_rl, num_input, den_input], outputs=[rl_plot_output, rl_poles_display, k_number_display])
den_input.change(fn=update_rl_view, inputs=[log_k_slider_rl, num_input, den_input], outputs=[rl_plot_output, rl_poles_display, k_number_display])
# 聊天机器人事件处理 - 支持 DeepSeek 和 Gemini
# 按钮点击事件
send_button.click(
fn=chat_with_ai,
inputs=[chat_input, chatbot],
outputs=chatbot,
).then(
lambda: "", # 清空输入框
outputs=chat_input
)
# 输入框回车事件
chat_input.submit(
fn=chat_with_ai,
inputs=[chat_input, chatbot],
outputs=chatbot,
).then(
lambda: "", # 清空输入框
outputs=chat_input
)
def on_tab_select(evt: gr.SelectData, num, den, log_k_freq, log_k_rl):
outputs = {
freq_plot_output: gr.update(),
freq_metrics_display: gr.update(),
freq_tf_display: gr.update(),
freq_stability_display: gr.update(),
k_number_display_freq: gr.update(),
rl_plot_output: gr.update(),
rl_poles_display: gr.update(),
k_number_display: gr.update()
}
if evt.index == 1:
k_freq = 10**log_k_freq
fig, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k_freq)
outputs[freq_plot_output], outputs[freq_metrics_display], outputs[freq_tf_display], outputs[freq_stability_display], outputs[k_number_display_freq] = fig, metrics, tf_latex, stability, k_freq
elif evt.index == 2:
fig, poles, k_val = root_locus_analysis(num, den, log_k_rl)
outputs[rl_plot_output], outputs[rl_poles_display], outputs[k_number_display] = fig, poles, k_val
return outputs
tabs.select(
on_tab_select,
inputs=[num_input, den_input, log_k_slider_freq, log_k_slider_rl],
outputs=[freq_plot_output, freq_metrics_display, freq_tf_display, freq_stability_display, k_number_display_freq, rl_plot_output, rl_poles_display, k_number_display]
)
if __name__ == "__main__":
# 需要安装 aiohttp: pip install aiohttp
demo.launch(debug=True)