Files
AutoControlCourse/app.py
T
2025-10-16 14:34:16 +08:00

2176 lines
107 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.
"""
================================================================================
自动控制原理AI+数智平台 (Automatic Control Theory AI+ Platform)
================================================================================
版权声明 (Copyright Notice):
本项目由"西北工业大学2025年校级本科生建设项目"资助
Funded by Northwestern Polytechnical University 2025 Undergraduate
Construction Project
项目信息 (Project Information):
课程名称:《自动控制理论》
Course: Automatic Control Theory
负责人:魏鹏飞
Supervisor: Wei Pengfei
联系方式:pengfeiwei@nwpu.edu.cn
Email: pengfeiwei@nwpu.edu.cn
机构:西北工业大学
Institution: Northwestern Polytechnical University (NWPU)
功能简介 (Features):
- 时域分析:阶跃响应、脉冲响应、性能指标计算
- 频域分析:Bode图、Nyquist图、稳定裕度分析
- 根轨迹分析:动态轨迹绘制、增益调节、极点跟踪
- AI智能问答:基于DeepSeek/Gemini的专业教学助手
最后更新 (Last Updated): 2025-10-16
版本 (Version): 1.0.0
================================================================================
"""
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 = """
/* ==================== 全局样式 ==================== */
* {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.gradio-container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important;
background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%) !important;
}
/* ==================== Emoji 显示修复 ==================== */
/* 确保所有 emoji 正常显示,不被背景色覆盖 */
* {
font-feature-settings: "liga" 1, "calt" 1, "kern" 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 针对 Gradio Markdown 组件的 emoji 修复 */
.gr-markdown code {
background: transparent !important;
padding: 0 !important;
font-family: inherit !important;
}
/* 确保 HTML 内容中的 emoji 不受影响 */
.gr-html * {
background-clip: border-box !important;
-webkit-background-clip: border-box !important;
-webkit-text-fill-color: initial !important;
}
/* ==================== 标题区域 ==================== */
.main-title {
text-align: center;
font-size: 3em !important;
font-weight: 900 !important;
margin-bottom: 0.3em;
letter-spacing: -1px;
animation: titleGlow 3s ease-in-out infinite;
color: #333;
}
/* 标题渐变效果(不影响emoji) */
h1.main-title {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
@keyframes titleGlow {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.2); }
}
.subtitle {
text-align: center;
color: #555;
font-size: 1.2em;
font-weight: 500;
margin-bottom: 1.5em;
letter-spacing: 0.5px;
}
/* 确保 subtitle 中的 emoji 正常显示 */
p.subtitle {
color: #555 !important;
}
/* ==================== 项目信息横幅 ==================== */
.project-info-banner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
border-radius: 12px;
margin-bottom: 25px;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
animation: bannerSlide 0.6s ease-out;
}
@keyframes bannerSlide {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* ==================== 标签页样式 ==================== */
.tab-nav {
background: white;
border-radius: 12px;
padding: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
margin-bottom: 20px;
}
.tab-nav button {
font-weight: 600 !important;
font-size: 1.1em !important;
padding: 12px 24px !important;
border-radius: 8px !important;
border: none !important;
transition: all 0.3s ease !important;
}
.tab-nav button:hover {
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%) !important;
transform: translateY(-2px) !important;
}
.tab-nav button.selected {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important;
}
/* ==================== 卡片和分组样式 ==================== */
.gr-group {
background: white !important;
border-radius: 16px !important;
padding: 24px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08) !important;
border: 1px solid rgba(102, 126, 234, 0.1) !important;
margin-bottom: 20px !important;
transition: transform 0.3s ease, box-shadow 0.3s ease !important;
}
.gr-group:hover {
transform: translateY(-4px) !important;
box-shadow: 0 12px 48px rgba(102, 126, 234, 0.15) !important;
}
/* 卡片标题 */
.card-title {
font-size: 1.4em;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 15px;
border-bottom: 3px solid;
border-image: linear-gradient(90deg, #667eea, #764ba2) 1;
padding-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
/* 修复 emoji 显示问题 - emoji 不应用渐变色 */
.card-title::before {
content: '';
display: inline-block;
}
/* 确保 HTML 内的 emoji 正常显示 */
div.card-title {
background: transparent !important;
-webkit-background-clip: initial !important;
-webkit-text-fill-color: initial !important;
background-clip: initial !important;
color: #333 !important;
}
/* 只对文字部分应用渐变(如果需要) */
div.card-title span {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ==================== 输入框样式 ==================== */
.gr-textbox input, .gr-textbox textarea {
border: 2px solid #e0e7ff !important;
border-radius: 10px !important;
padding: 12px 16px !important;
font-size: 1em !important;
transition: all 0.3s ease !important;
background: #fafbff !important;
}
.gr-textbox input:focus, .gr-textbox textarea:focus {
border-color: #667eea !important;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1) !important;
background: white !important;
transform: translateY(-2px);
}
.gr-textbox label {
font-weight: 600 !important;
color: #4a5568 !important;
margin-bottom: 8px !important;
}
/* ==================== 按钮样式 ==================== */
button {
border-radius: 10px !important;
font-weight: 600 !important;
padding: 12px 24px !important;
font-size: 1em !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
border: none !important;
cursor: pointer !important;
}
.primary-btn, button[variant="primary"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important;
}
.primary-btn:hover, button[variant="primary"]:hover {
transform: translateY(-3px) scale(1.02) !important;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5) !important;
}
.primary-btn:active, button[variant="primary"]:active {
transform: translateY(-1px) scale(0.98) !important;
}
button[variant="secondary"] {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%) !important;
color: white !important;
box-shadow: 0 4px 15px rgba(240, 147, 251, 0.4) !important;
}
button[variant="secondary"]:hover {
transform: translateY(-3px) scale(1.02) !important;
box-shadow: 0 8px 25px rgba(240, 147, 251, 0.5) !important;
}
/* ==================== 滑块样式 ==================== */
.gr-slider {
padding: 20px 10px !important;
}
.gr-slider input[type="range"] {
height: 8px !important;
border-radius: 4px !important;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%) !important;
}
.gr-slider input[type="range"]::-webkit-slider-thumb {
width: 20px !important;
height: 20px !important;
background: white !important;
border: 3px solid #667eea !important;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important;
cursor: pointer !important;
}
.gr-slider input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2) !important;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6) !important;
}
/* ==================== 图表容器 ==================== */
.plot-container {
border-radius: 16px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08) !important;
overflow: hidden !important;
background: white !important;
padding: 10px !important;
border: 1px solid rgba(102, 126, 234, 0.1) !important;
}
.gr-plot {
border-radius: 12px !important;
}
/* ==================== 知识卡片 ==================== */
.knowledge-card {
background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%);
border-radius: 16px;
padding: 24px;
margin-top: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
max-height: 550px;
overflow-y: auto;
border: 1px solid rgba(102, 126, 234, 0.15);
animation: cardFadeIn 0.6s ease-out;
}
@keyframes cardFadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.knowledge-card::-webkit-scrollbar {
width: 10px;
}
.knowledge-card::-webkit-scrollbar-track {
background: #f1f3f9;
border-radius: 10px;
}
.knowledge-card::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
border: 2px solid #f1f3f9;
}
.knowledge-card::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #5568d3 0%, #65408b 100%);
}
.knowledge-card h3 {
color: #667eea;
font-size: 1.5em;
font-weight: 700;
margin-top: 0;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 3px solid;
border-image: linear-gradient(90deg, #667eea, #764ba2) 1;
}
/* 知识卡片标题中的 emoji 保持彩色 */
.knowledge-card h3::first-letter {
color: inherit;
}
.knowledge-card h4 {
color: #4a5568;
font-size: 1.2em;
font-weight: 600;
margin-top: 20px;
margin-bottom: 12px;
}
.knowledge-card details {
margin: 15px 0;
border-radius: 8px;
overflow: hidden;
}
.knowledge-card details summary {
cursor: pointer;
padding: 12px 16px;
font-weight: 700;
border-radius: 8px;
transition: all 0.3s ease;
user-select: none;
color: #333;
font-size: 1.05em;
}
.knowledge-card details summary:hover {
transform: translateX(5px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
filter: brightness(0.98);
}
.knowledge-card details[open] summary {
margin-bottom: 10px;
border-radius: 8px 8px 0 0;
}
.knowledge-card table {
border-collapse: collapse;
width: 100%;
margin: 15px 0;
font-size: 0.95em;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border-radius: 8px;
overflow: hidden;
}
.knowledge-card table th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px;
font-weight: 700;
}
.knowledge-card table td {
padding: 10px 12px;
border: 1px solid #e0e7ff;
}
.knowledge-card table tr:hover {
background: #f8f9ff;
transition: background 0.2s ease;
}
/* ==================== 聊天机器人样式 ==================== */
.chatbot {
border-radius: 16px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08) !important;
border: 1px solid rgba(102, 126, 234, 0.1) !important;
}
/* 用户消息气泡 - 紫色渐变 + 白色文字 */
.message.user {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
border-radius: 18px 18px 4px 18px !important;
padding: 12px 18px !important;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3) !important;
}
/* 确保用户消息内的所有文字都是白色 */
.message.user * {
color: white !important;
}
/* 机器人消息气泡 - 白色背景 + 深色文字 */
.message.bot {
background: white !important;
border: 1px solid #e0e7ff !important;
border-radius: 18px 18px 18px 4px !important;
padding: 12px 18px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
color: #2d3748 !important;
}
/* 确保机器人消息内的文字清晰可见 */
.message.bot * {
color: #2d3748 !important;
}
.message.bot p {
color: #2d3748 !important;
line-height: 1.6 !important;
margin: 8px 0 !important;
}
.message.bot strong {
color: #1a202c !important;
font-weight: 700 !important;
}
.message.bot code {
background: #f7fafc !important;
color: #667eea !important;
padding: 2px 6px !important;
border-radius: 4px !important;
font-family: 'Consolas', 'Monaco', monospace !important;
}
.message.bot pre {
background: #f7fafc !important;
border: 1px solid #e2e8f0 !important;
border-radius: 6px !important;
padding: 12px !important;
overflow-x: auto !important;
}
.message.bot pre code {
background: transparent !important;
padding: 0 !important;
}
/* ==================== 性能指标文本框 ==================== */
.gr-textbox.output-metrics {
font-family: 'Consolas', 'Monaco', monospace !important;
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%) !important;
border: 2px solid #e0e7ff !important;
border-radius: 12px !important;
padding: 16px !important;
font-size: 0.95em !important;
line-height: 1.8 !important;
}
/* ==================== 数字显示框 ==================== */
.gr-number, .gain-display {
font-size: 1.3em !important;
font-weight: 700 !important;
color: #667eea !important;
text-align: center !important;
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%) !important;
border: 2px solid #667eea !important;
border-radius: 12px !important;
padding: 16px !important;
}
.gain-display input {
text-align: center !important;
font-size: 1.5em !important;
font-weight: 800 !important;
color: #667eea !important;
}
/* ==================== 输出显示框 ==================== */
.output-display {
background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%) !important;
padding: 20px !important;
border-radius: 12px !important;
border: 2px solid #e0e7ff !important;
min-height: 80px !important;
}
.stability-result {
padding: 15px !important;
border-radius: 10px !important;
text-align: center !important;
font-size: 1.2em !important;
font-weight: 700 !important;
margin-top: 10px !important;
}
/* ==================== 现代聊天机器人样式 ==================== */
.modern-chatbot {
background: white !important;
border-radius: 16px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08) !important;
}
/* ==================== Markdown 样式增强 ==================== */
.gr-markdown {
line-height: 1.8 !important;
}
.gr-markdown h1, .gr-markdown h2, .gr-markdown h3 {
font-weight: 700 !important;
margin-top: 1.5em !important;
margin-bottom: 0.8em !important;
}
.gr-markdown code {
background: #f0f4ff !important;
padding: 2px 8px !important;
border-radius: 4px !important;
font-family: 'Consolas', 'Monaco', monospace !important;
color: #667eea !important;
border: 1px solid #e0e7ff !important;
}
.gr-markdown pre {
background: #f8f9ff !important;
border: 2px solid #e0e7ff !important;
border-radius: 8px !important;
padding: 16px !important;
}
/* ==================== 加载动画 ==================== */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.loading {
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.main-title {
font-size: 2em !important;
}
.subtitle {
font-size: 1em !important;
}
.knowledge-card {
padding: 16px;
max-height: 400px;
}
}
/* ==================== 暗色主题支持 ==================== */
@media (prefers-color-scheme: dark) {
.gradio-container {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%) !important;
}
.gr-group {
background: #0f3460 !important;
border-color: rgba(102, 126, 234, 0.3) !important;
}
.knowledge-card {
background: linear-gradient(135deg, #0f3460 0%, #1a1a2e 100%);
border-color: rgba(102, 126, 234, 0.3);
}
.knowledge-card h3, .knowledge-card h4 {
color: #a0aec0;
}
}
/* ==================== 特殊效果 ==================== */
.shimmer {
background: linear-gradient(90deg,
rgba(255,255,255,0) 0%,
rgba(255,255,255,0.3) 50%,
rgba(255,255,255,0) 100%);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
"""
with gr.Blocks(title="自动控制原理学习网站 - AI+数智平台", css=custom_css) as demo:
# 主标题 - 带动画效果
gr.HTML("<h1 class='main-title'> 自动控制原理AI+数智平台</h1>")
gr.HTML("<p class='subtitle'>✨ 交互式控制系统分析与设计工具 | 时域·频域·根轨迹·AI问答 ✨</p>")
# 项目信息横幅 - 优化对比度和可读性
gr.HTML("""
<div class='project-info-banner'>
<div style='display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 20px; color: white;'>
<div style='display: flex; align-items: center; gap: 8px;'>
<span style='font-size: 1.5em;'>📚</span>
<div>
<div style='font-size: 0.85em; opacity: 0.95; color: rgba(255,255,255,0.9);'>课程 Course</div>
<div style='font-weight: 700; font-size: 1.1em; color: white;'>自动控制理论</div>
</div>
</div>
<div style='height: 40px; width: 2px; background: rgba(255,255,255,0.4);'></div>
<div style='display: flex; align-items: center; gap: 8px;'>
<span style='font-size: 1.5em;'>👨‍🏫</span>
<div>
<div style='font-size: 0.85em; opacity: 0.95; color: rgba(255,255,255,0.9);'>负责人 Supervisor</div>
<div style='font-weight: 700; font-size: 1.1em; color: white;'>魏鹏飞</div>
</div>
</div>
<div style='height: 40px; width: 2px; background: rgba(255,255,255,0.4);'></div>
<div style='display: flex; align-items: center; gap: 8px;'>
<span style='font-size: 1.5em;'>📧</span>
<div>
<div style='font-size: 0.85em; opacity: 0.95; color: rgba(255,255,255,0.9);'>联系方式 Contact</div>
<a href='mailto:pengfeiwei@nwpu.edu.cn'
style='font-weight: 700; font-size: 1.1em; color: white; text-decoration: none;
border-bottom: 2px solid rgba(255,255,255,0.6); padding-bottom: 2px;
transition: all 0.3s ease;'
onmouseover='this.style.borderColor="white"; this.style.paddingBottom="4px"; this.style.textShadow="0 0 10px rgba(255,255,255,0.8)";'
onmouseout='this.style.borderColor="rgba(255,255,255,0.6)"; this.style.paddingBottom="2px"; this.style.textShadow="none";'>
pengfeiwei@nwpu.edu.cn
</a>
</div>
</div>
</div>
<div style='text-align: center; margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.3); color: white;'>
<span style='font-size: 1.2em; margin-right: 8px;'>🎓</span>
<span style='font-weight: 600; font-size: 1.05em; color: white;'>西北工业大学 Northwestern Polytechnical University</span>
<span style='margin: 0 12px; opacity: 0.7; color: white;'>|</span>
<span style='opacity: 0.95; color: white;'>2025年校级本科生建设项目资助</span>
</div>
</div>
""")
with gr.Tabs() as tabs:
with gr.TabItem("⏱️ 时域分析 (Time Domain)", id=0):
gr.HTML("""
<div style='background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
padding: 12px 20px; border-radius: 10px; margin-bottom: 15px;
border-left: 4px solid #667eea;'>
<span style='font-size: 1.1em; font-weight: 600; color: #4a5568;'>
💡 <strong>快速开始:</strong>输入传递函数的分子和分母系数(逗号分隔),点击"开始分析"查看系统响应特性
</span>
</div>
""")
with gr.Row():
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.Row():
confirm_button = gr.Button(
"✓ 显示传递函数",
variant="secondary",
scale=1,
size="lg"
)
analyze_button = gr.Button(
"🚀 开始分析",
variant="primary",
scale=1,
elem_classes="primary-btn",
size="lg"
)
with gr.Group():
gr.HTML("<div class='card-title'>🔧 系统模型</div>")
tf_display = gr.Markdown(label="当前传递函数", elem_classes="output-display")
with gr.Group():
gr.HTML("<div class='card-title'>📈 动态性能指标</div>")
output_metrics = gr.Textbox(
label="Performance Metrics",
lines=8,
interactive=False,
elem_classes="output-metrics"
)
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>
<details open>
<summary style='background: #e3f2fd; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold;'>
📐 二阶系统标准形式
</summary>
<div style='padding: 10px; background: #fafafa; margin-top: 5px; border-radius: 5px;'>
<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>
</div>
</details>
<details open>
<summary style='background: #fff0f8; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold;'>
📊 时域性能指标(欠阻尼系统,0 &lt; ζ &lt; 1
</summary>
<div style='padding: 10px; background: #fafafa; margin-top: 5px; border-radius: 5px;'>
<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>
</details>
<details>
<summary style='background: #e8f5e9; 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></p>
<p style='text-align: center; font-size: 1.1em;'>G(s) = K / (τs + 1)</p>
<p><strong>参数说明:</strong></p>
<ul style='line-height: 1.8;'>
<li><strong>K</strong> - 系统增益(稳态增益)</li>
<li><strong>τ (tau)</strong> - 时间常数,单位:秒</li>
</ul>
<p><strong>阶跃响应:</strong></p>
<p style='text-align: center;'>y(t) = K(1 - e<sup>-t/τ</sup>)</p>
<div style='background: #e3f2fd; padding: 12px; border-radius: 6px; margin: 10px 0;'>
<p><strong>💡 关键时间点:</strong></p>
<ul>
<li>t = τ 时,响应达到稳态值的 63.2%</li>
<li>t = 3τ 时,响应达到稳态值的 95%</li>
<li>t = 4τ 时,响应达到稳态值的 98.2%</li>
<li>t = 5τ 时,响应达到稳态值的 99.3%</li>
</ul>
</div>
<p><strong>特点:</strong>一阶系统无超调,响应单调上升</p>
</div>
</details>
<details>
<summary style='background: #fff3e0; 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></p>
<p>根据开环传递函数原点处的极点数(积分环节数)v 分类:</p>
<table style='width: 100%; border-collapse: collapse; margin: 15px 0;'>
<tr style='background: #667eea; color: white;'>
<th style='padding: 10px; border: 1px solid #ddd;'>类型</th>
<th style='padding: 10px; border: 1px solid #ddd;'>K<sub>p</sub></th>
<th style='padding: 10px; border: 1px solid #ddd;'>K<sub>v</sub></th>
<th style='padding: 10px; border: 1px solid #ddd;'>K<sub>a</sub></th>
</tr>
<tr>
<td style='padding: 8px; border: 1px solid #ddd;'>0 型</td>
<td style='padding: 8px; border: 1px solid #ddd;'>有限值</td>
<td style='padding: 8px; border: 1px solid #ddd;'>0</td>
<td style='padding: 8px; border: 1px solid #ddd;'>0</td>
</tr>
<tr style='background: #f5f5f5;'>
<td style='padding: 8px; border: 1px solid #ddd;'>I 型</td>
<td style='padding: 8px; border: 1px solid #ddd;'>∞</td>
<td style='padding: 8px; border: 1px solid #ddd;'>有限值</td>
<td style='padding: 8px; border: 1px solid #ddd;'>0</td>
</tr>
<tr>
<td style='padding: 8px; border: 1px solid #ddd;'>II 型</td>
<td style='padding: 8px; border: 1px solid #ddd;'>∞</td>
<td style='padding: 8px; border: 1px solid #ddd;'>∞</td>
<td style='padding: 8px; border: 1px solid #ddd;'>有限值</td>
</tr>
</table>
<p><strong>稳态误差计算:</strong></p>
<ul style='line-height: 1.8;'>
<li>阶跃输入 r(t) = Re<sub>ss</sub> = R/(1+K<sub>p</sub>)</li>
<li>斜坡输入 r(t) = Rte<sub>ss</sub> = R/K<sub>v</sub></li>
<li>抛物线输入 r(t) = Rt²/2e<sub>ss</sub> = R/K<sub>a</sub></li>
</ul>
<div style='background: #fff3e0; padding: 12px; border-radius: 6px; margin: 10px 0;'>
<p><strong>⚡ 工程结论:</strong></p>
<p>系统型别越高,跟踪能力越强,但稳定性可能降低。实际系统常用 I 型或 II 型。</p>
</div>
</div>
</details>
<details>
<summary style='background: #f3e5f5; 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></p>
<p>距虚轴最近的极点对系统动态响应起主导作用,其他极点影响较小。</p>
<p><strong>判断条件:</strong></p>
<ul style='line-height: 1.8;'>
<li>主导极点与其他极点的实部相差 <strong>5倍以上</strong></li>
<li>主导极点与零点距离较远(相差3倍以上)</li>
</ul>
<p><strong>零点的影响:</strong></p>
<div style='background: #ffebee; padding: 12px; border-radius: 6px; margin: 10px 0;'>
<p><strong>🔺 左半平面零点(最小相位系统):</strong></p>
<ul>
<li>增加超调量</li>
<li>减小上升时间</li>
<li>零点越靠近极点,影响越大</li>
</ul>
</div>
<div style='background: #e8f5e9; padding: 12px; border-radius: 6px; margin: 10px 0;'>
<p><strong>🔻 右半平面零点(非最小相位系统):</strong></p>
<ul>
<li>引起反向响应(初始反向运动)</li>
<li>增加调节时间</li>
<li>降低系统性能</li>
</ul>
</div>
</div>
</details>
</div>
""")
with gr.TabItem("📊 频域分析 (Frequency Domain)", id=1):
gr.HTML("""
<div style='background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
padding: 12px 20px; border-radius: 10px; margin-bottom: 15px;
border-left: 4px solid #f5576c;'>
<span style='font-size: 1.1em; font-weight: 600; color: #4a5568;'>
💡 <strong>使用技巧:</strong>拖动增益滑块查看系统稳定性变化,观察Bode图和Nyquist图的实时更新
</span>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>🎚️ 调整系统增益</div>")
log_k_slider_freq = gr.Slider(
minimum=-4,
maximum=4,
value=1,
step=0.01,
label="对数增益 log₁₀(K)",
info="💡 滑块范围:10⁻⁴ ~ 10⁴"
)
k_number_display_freq = gr.Number(
value=10.0,
label="当前增益 K (Gain)",
interactive=False,
elem_classes="gain-display"
)
with gr.Group():
gr.HTML("<div class='card-title'>🔧 当前系统模型</div>")
freq_tf_display = gr.Markdown(label="含增益K的传递函数", elem_classes="output-display")
with gr.Group():
gr.HTML("<div class='card-title'>📊 稳定裕度分析</div>")
freq_metrics_display = gr.Textbox(
label="Stability Margins",
lines=4,
interactive=False,
elem_classes="output-metrics"
)
freq_stability_display = gr.Markdown(elem_classes="stability-result")
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;'>📊 Bode图与Nyquist图</summary>
<div style='padding: 10px;'>
<h4>Bode图(伯德图)</h4>
<p><strong>组成:</strong>幅频特性图 + 相频特性图</p>
<ul style='line-height: 1.8;'>
<li><strong>横轴:</strong>频率 ω (对数刻度)</li>
<li><strong>纵轴(幅频):</strong>幅值 20log|G(jω)| (dB)</li>
<li><strong>纵轴(相频):</strong>相角 ∠G(jω) (度)</li>
</ul>
<div style='background: #e3f2fd; padding: 12px; border-radius: 6px; margin: 10px 0;'>
<p><strong>💡 优点:</strong></p>
<ul>
<li>便于绘制(用渐近线近似)</li>
<li>直观读取稳定裕度</li>
<li>便于串联系统分析(图形叠加)</li>
</ul>
</div>
<h4 style='margin-top: 20px;'>Nyquist图(奈奎斯特图)</h4>
<p><strong>定义:</strong>开环频率特性 G(jω)H(jω) 在复平面上的轨迹图</p>
<ul style='line-height: 1.8;'>
<li><strong>横轴:</strong>实部 Re[G(jω)]</li>
<li><strong>纵轴:</strong>虚部 Im[G(jω)]</li>
</ul>
<div style='background: #fff3e0; padding: 12px; border-radius: 6px; margin: 10px 0;'>
<p><strong>🎯 奈奎斯特稳定判据:</strong></p>
<p>闭环系统稳定的充要条件:当 ω 从 -∞ 变化到 +∞ 时,Nyquist曲线逆时针包围(-1, j0)点的圈数 N 等于开环系统右半平面极点数 P。</p>
<p style='text-align: center; font-weight: bold;'>Z = P - N</p>
<p style='color: #666;'>其中 Z 为闭环系统右半平面极点数</p>
</div>
<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>
<details>
<summary style='cursor: pointer; font-weight: bold; color: #333; font-size: 1.1em; padding: 10px; background: #e8f5e9; border-radius: 5px; margin: 10px 0;'>🔧 典型环节的频率特性</summary>
<div style='padding: 10px;'>
<h4>1️⃣ 比例环节 K</h4>
<p>• 幅频:20logK (dB) - 水平直线<br>
• 相频:0° - 水平直线</p>
<h4>2️⃣ 积分环节 1/s</h4>
<p>• 幅频:-20dB/dec 斜率的直线<br>
• 相频:-90° - 水平直线</p>
<h4>3️⃣ 微分环节 s</h4>
<p>• 幅频:+20dB/dec 斜率的直线<br>
• 相频:+90° - 水平直线</p>
<h4>4️⃣ 惯性环节 1/(Ts+1)</h4>
<p>• 转折频率:ω = 1/T<br>
• 幅频:低频0dB,高频-20dB/dec<br>
• 相频:从0°下降到-90°</p>
<h4>5️⃣ 一阶微分环节 Ts+1</h4>
<p>• 转折频率:ω = 1/T<br>
• 幅频:低频0dB,高频+20dB/dec<br>
• 相频:从0°上升到+90°</p>
<h4>6️⃣ 二阶振荡环节 ω<sub>n</sub>²/(s²+2ζω<sub>n</sub>s+ω<sub>n</sub>²)</h4>
<p>• 转折频率:ω = ω<sub>n</sub><br>
• 幅频:低频0dB,高频-40dB/dec<br>
• 相频:从0°下降到-180°<br>
• 谐振峰值与阻尼比 ζ 相关</p>
</div>
</details>
<details>
<summary style='cursor: pointer; font-weight: bold; color: #333; font-size: 1.1em; padding: 10px; background: #fce4ec; border-radius: 5px; margin: 10px 0;'>⚡ 最小相位系统与全通系统</summary>
<div style='padding: 10px;'>
<h4>最小相位系统</h4>
<p><strong>定义:</strong>传递函数的所有零点和极点都在左半s平面的系统。</p>
<div style='background: #e8f5e9; padding: 12px; border-radius: 6px; margin: 10px 0;'>
<p><strong>✅ 特点:</strong></p>
<ul>
<li>幅频特性与相频特性一一对应</li>
<li>可由幅频特性唯一确定相频特性</li>
<li>相角滞后最小(同样幅频特性下)</li>
</ul>
</div>
<h4 style='margin-top: 20px;'>非最小相位系统</h4>
<p><strong>定义:</strong>传递函数在右半s平面有零点或极点的系统。</p>
<div style='background: #ffebee; padding: 12px; border-radius: 6px; margin: 10px 0;'>
<p><strong>⚠️ 特点:</strong></p>
<ul>
<li>相角滞后较大</li>
<li>幅频特性相同,相频特性不同</li>
<li>稳定性和动态性能较差</li>
</ul>
</div>
<h4 style='margin-top: 20px;'>全通系统</h4>
<p><strong>特点:</strong>幅频特性为常数(|G(jω)| = 1),只改变相频特性。</p>
<p><strong>应用:</strong>相位校正、延时补偿</p>
</div>
</details>
</div>
""")
with gr.TabItem("🎯 根轨迹 (Root Locus)", id=2):
gr.HTML("""
<div style='background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
padding: 12px 20px; border-radius: 10px; margin-bottom: 15px;
border-left: 4px solid #4caf50;'>
<span style='font-size: 1.1em; font-weight: 600; color: #4a5568;'>
💡 <strong>分析要点:</strong>观察极点在s平面的运动轨迹,左半平面的极点表示系统稳定
</span>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>🎚️ 调整系统增益</div>")
log_k_slider_rl = gr.Slider(
minimum=-4,
maximum=4,
value=1,
step=0.01,
label="对数增益 log₁₀(K)",
info="💡 拖动滑块观察极点移动"
)
k_number_display = gr.Number(
value=10.0,
label="当前增益 K (Gain)",
interactive=False,
elem_classes="gain-display"
)
with gr.Group():
gr.HTML("<div class='card-title'>📍 闭环极点位置</div>")
rl_poles_display = gr.Textbox(
label="Closed-Loop Pole Locations",
lines=6,
interactive=False,
elem_classes="output-metrics"
)
with gr.Column(scale=2):
rl_plot_output = gr.Plot(label="Root Locus Plot")
rl_formula_text = """
<div class="knowledge-card">
<h3 style='color: #667eea; margin-top: 0;'>📚 根轨迹知识要点 (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>
<details>
<summary style="background: #e3f2fd; 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;">
<table style='width: 100%; border-collapse: collapse; margin: 15px 0;'>
<tr style='background: #667eea; color: white;'>
<th style='padding: 10px; border: 1px solid #ddd;'>规则</th>
<th style='padding: 10px; border: 1px solid #ddd;'>内容</th>
</tr>
<tr>
<td style='padding: 8px; border: 1px solid #ddd; font-weight: bold;'>起点</td>
<td style='padding: 8px; border: 1px solid #ddd;'>K=0,位于开环极点</td>
</tr>
<tr style='background: #f5f5f5;'>
<td style='padding: 8px; border: 1px solid #ddd; font-weight: bold;'>终点</td>
<td style='padding: 8px; border: 1px solid #ddd;'>K→∞,位于开环零点或无穷远</td>
</tr>
<tr>
<td style='padding: 8px; border: 1px solid #ddd; font-weight: bold;'>分支数</td>
<td style='padding: 8px; border: 1px solid #ddd;'>等于 max(n, m)</td>
</tr>
<tr style='background: #f5f5f5;'>
<td style='padding: 8px; border: 1px solid #ddd; font-weight: bold;'>实轴段</td>
<td style='padding: 8px; border: 1px solid #ddd;'>右侧零极点总数为奇数的区段</td>
</tr>
<tr>
<td style='padding: 8px; border: 1px solid #ddd; font-weight: bold;'>渐近线数</td>
<td style='padding: 8px; border: 1px solid #ddd;'>n - m 条</td>
</tr>
<tr style='background: #f5f5f5;'>
<td style='padding: 8px; border: 1px solid #ddd; font-weight: bold;'>渐近线角度</td>
<td style='padding: 8px; border: 1px solid #ddd;'>φ<sub>a</sub> = (2k+1)·180°/(n-m)</td>
</tr>
<tr>
<td style='padding: 8px; border: 1px solid #ddd; font-weight: bold;'>渐近线交点</td>
<td style='padding: 8px; border: 1px solid #ddd;'>σ<sub>a</sub> = (Σp - Σz)/(n-m)</td>
</tr>
<tr style='background: #f5f5f5;'>
<td style='padding: 8px; border: 1px solid #ddd; font-weight: bold;'>分离/会合点</td>
<td style='padding: 8px; border: 1px solid #ddd;'>dK/ds = 0 的实根</td>
</tr>
<tr>
<td style='padding: 8px; border: 1px solid #ddd; font-weight: bold;'>与虚轴交点</td>
<td style='padding: 8px; border: 1px solid #ddd;'>用劳斯判据或令 s=jω 求解</td>
</tr>
<tr style='background: #f5f5f5;'>
<td style='padding: 8px; border: 1px solid #ddd; font-weight: bold;'>出射角/入射角</td>
<td style='padding: 8px; border: 1px solid #ddd;'>复数极点/零点处的根轨迹切线角度</td>
</tr>
</table>
<div style="background: #e3f2fd; padding: 12px; border-radius: 6px; margin: 10px 0;">
<p><strong>📝 绘制步骤:</strong></p>
<ol style='line-height: 1.8;'>
<li>标出开环零极点</li>
<li>确定实轴上的根轨迹</li>
<li>计算渐近线(角度和交点)</li>
<li>求分离点/会合点</li>
<li>计算与虚轴交点</li>
<li>求复数极点的出射角</li>
<li>绘制完整根轨迹</li>
</ol>
</div>
</div>
</details>
<details>
<summary style="background: #fff3e0; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; margin-top: 10px;">
🔍 零度根轨迹(180°根轨迹)
</summary>
<div style="padding: 10px; background: #fafafa; margin-top: 5px; border-radius: 5px;">
<p><strong>定义:</strong>当参数 K 从 0 变化到 -∞ 时,闭环极点的轨迹。</p>
<p><strong>相角条件:</strong></p>
<p style="text-align: center; background: white; padding: 10px; border-radius: 5px;">
∠G(s<sub>0</sub>)H(s<sub>0</sub>) = 2k·180° (k = 0, ±1, ±2, ...)
</p>
<div style="background: #fff3e0; padding: 12px; border-radius: 6px; margin: 10px 0;">
<p><strong>⚠️ 与常规根轨迹的区别:</strong></p>
<ul style='line-height: 1.8;'>
<li><strong>实轴段:</strong>右侧零极点总数为<strong>偶数</strong>的区段</li>
<li><strong>渐近线角度:</strong>φ<sub>a</sub> = 2k·180°/(n-m)</li>
<li><strong>图形表示:</strong>通常用虚线表示</li>
</ul>
</div>
<p><strong>应用场景:</strong></p>
<ul style='line-height: 1.8;'>
<li>负反馈系统中的正增益变化</li>
<li>正反馈系统分析</li>
<li>参数补偿器设计</li>
</ul>
</div>
</details>
<details>
<summary style="background: #f3e5f5; 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><strong>稳定性分析:</strong></p>
<div style="background: #e8f5e9; padding: 12px; border-radius: 6px; margin: 10px 0;">
<p><strong>✅ 稳定条件:</strong>所有闭环极点都在左半 s 平面</p>
<ul>
<li>根轨迹完全在左半平面 → 系统对所有 K>0 都稳定</li>
<li>根轨迹穿越虚轴 → 存在临界增益 K<sub>c</sub></li>
<li>根轨迹在右半平面 → 某些 K 值下系统不稳定</li>
</ul>
</div>
<p><strong>动态性能分析:</strong></p>
<table style='width: 100%; border-collapse: collapse; margin: 10px 0;'>
<tr style='background: #667eea; color: white;'>
<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;'>单调响应,无振荡</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>
<tr>
<td style='padding: 8px; border: 1px solid #ddd;'>虚轴上</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>
<p><strong>增益 K 的选择:</strong></p>
<div style="background: #e3f2fd; padding: 12px; border-radius: 6px; margin: 10px 0;">
<p><strong>💡 设计原则:</strong></p>
<ul style='line-height: 1.8;'>
<li><strong>稳定性要求:</strong>K < K<sub>c</sub>(临界增益)</li>
<li><strong>快速性要求:</strong>选择使主导极点实部较大的 K</li>
<li><strong>平稳性要求:</strong>选择使主导极点阻尼比 ζ ∈ [0.4, 0.8] 的 K</li>
<li><strong>精度要求:</strong>K 越大,稳态误差越小</li>
</ul>
</div>
<p><strong>性能折中:</strong></p>
<p style='color: #666; line-height: 1.8;'>
增大 K 可以提高系统精度和快速性,但可能降低稳定性和增大超调。
需要在根轨迹上选择合适的工作点,平衡各项性能指标。
</p>
</div>
</details>
<details>
<summary style="background: #e8eaf6; 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><strong>1️⃣ 系统分析:</strong></p>
<ul style='line-height: 1.8;'>
<li>判断系统稳定性及稳定范围</li>
<li>确定临界增益 K<sub>c</sub></li>
<li>分析参数变化对系统性能的影响</li>
<li>确定主导极点位置</li>
</ul>
<p><strong>2️⃣ 控制器设计:</strong></p>
<ul style='line-height: 1.8;'>
<li><strong>相位超前补偿:</strong>在根轨迹左侧增加零点,拉动根轨迹向左</li>
<li><strong>相位滞后补偿:</strong>在原点附近增加零极点对,提高低频增益</li>
<li><strong>PID控制器:</strong>通过零极点配置实现性能要求</li>
</ul>
<p><strong>3️⃣ 极点配置:</strong></p>
<div style="background: #fff3e0; padding: 12px; border-radius: 6px; margin: 10px 0;">
<p>通过添加合适的零极点,使闭环极点位于期望位置:</p>
<ul>
<li>满足阻尼比要求:ζ = cosθ(θ为极点角度)</li>
<li>满足调节时间要求:σ = 4/(ζω<sub>n</sub>)</li>
<li>满足超调量要求:σ% = e<sup>-πζ/√(1-ζ²)</sup></li>
</ul>
</div>
<p><strong>4️⃣ 鲁棒性分析:</strong></p>
<p style='line-height: 1.8;'>
根轨迹可以直观显示参数不确定性对系统稳定性的影响,
帮助评估系统的鲁棒性能。
</p>
</div>
</details>
</div>
"""
gr.HTML(rl_formula_text)
with gr.TabItem("🤖 智能问答 (Q&A)", id=3):
gr.HTML("""
<div style='background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%);
padding: 12px 20px; border-radius: 10px; margin-bottom: 15px;
border-left: 4px solid #764ba2;'>
<span style='font-size: 1.1em; font-weight: 600; color: #4a5568;'>
💡 <strong>AI助手:</strong>基于DeepSeek大模型,可以回答控制理论相关问题,支持LaTeX公式渲染
</span>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
chatbot = gr.Chatbot(
label="🎓 自控原理AI助教",
bubble_full_width=False,
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"
)
with gr.Row():
chat_input = gr.Textbox(
label="",
placeholder="💬 输入您的问题,例如:什么是PID控制器?如何分析系统稳定性?",
scale=4,
lines=2,
max_lines=4
)
with gr.Column(scale=1, min_width=120):
send_button = gr.Button(
"📤 发送",
variant="primary",
size="lg",
elem_classes="primary-btn"
)
clear_button = gr.Button(
"🗑️ 清空",
variant="secondary",
size="lg"
)
# 示例问题
gr.HTML("""
<div style='margin-top: 15px; padding: 15px; background: white; border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05); border: 1px solid #e0e7ff;'>
<div style='font-weight: 700; color: #667eea; margin-bottom: 10px; font-size: 1.1em;'>
💡 试试这些问题:
</div>
<div style='display: flex; flex-wrap: wrap; gap: 8px;'>
<span style='background: #f0f4ff; padding: 6px 14px; border-radius: 20px;
font-size: 0.9em; color: #4a5568; border: 1px solid #e0e7ff;'>
什么是传递函数?
</span>
<span style='background: #f0f4ff; padding: 6px 14px; border-radius: 20px;
font-size: 0.9em; color: #4a5568; border: 1px solid #e0e7ff;'>
如何判断系统稳定性?
</span>
<span style='background: #f0f4ff; padding: 6px 14px; border-radius: 20px;
font-size: 0.9em; color: #4a5568; border: 1px solid #e0e7ff;'>
解释Bode图的物理意义
</span>
<span style='background: #f0f4ff; padding: 6px 14px; border-radius: 20px;
font-size: 0.9em; color: #4a5568; border: 1px solid #e0e7ff;'>
PID控制器各参数的作用
</span>
</div>
</div>
""")
# --- 事件绑定部分 ---
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
)
# 清空聊天记录
clear_button.click(
lambda: [],
outputs=chatbot
)
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(
server_name="0.0.0.0", # 监听所有网络接口
server_port=7860, # 指定一个端口
share=False # 关闭Gradio的临时分享
)