更新项目格式及部分ui细节

This commit is contained in:
2025-10-20 00:23:45 +08:00
parent 071e4da378
commit a22bb42bd4
9 changed files with 1654 additions and 2309 deletions
+208
View File
@@ -0,0 +1,208 @@
import numpy as np
import control as ct
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import re
# --- 辅助函数:将系数数组转换为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):
if np.isclose(coeff, 0):
continue
sign = ""
if i > 0 and latex_str:
sign = " + " if coeff > 0 else " - "
elif coeff < 0:
sign = "-"
coeff_abs = abs(coeff)
coeff_str = f"{coeff_abs:g}" if not np.isclose(coeff_abs, 1) or degree - i == 0 else ""
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"
def _parse_coeffs(s_val):
"""从字符串解析系数,返回Numpy数组"""
cleaned_str = re.sub(r'[^0-9,\-.]', '', s_val)
if not cleaned_str:
return np.array([])
return np.array([float(n) for n in cleaned_str.split(',') if n])
# --- 功能函数1:显示传递函数 ---
def display_transfer_function(num_str, den_str):
try:
num_coeffs = _parse_coeffs(num_str)
den_coeffs = _parse_coeffs(den_str)
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)
return f"$$ G(s) = \\frac{{{num_latex}}}{{{den_latex}}} $$"
except Exception as e:
return f"输入格式错误: {e}"
# --- 功能函数2:执行时域分析和绘图 ---
def time_domain_analysis(num_str, den_str):
try:
num = _parse_coeffs(num_str)
den = _parse_coeffs(den_str)
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)
metrics_text = "无法计算性能指标。"
try:
info = ct.step_info(system)
if isinstance(info, dict):
metrics_text = (
f"上升时间: {info.get('RiseTime', float('nan')):.2f} s\n"
f"峰值时间: {info.get('PeakTime', float('inf')):.2f} s\n"
f"峰值: {info.get('Peak', float('inf')):.2f}\n"
f"超调量: {info.get('Overshoot', float('nan')):.1f} %\n"
f"调节时间: {info.get('SettlingTime', float('nan')):.2f} s\n"
f"稳态值: {info.get('SteadyStateValue', float('nan')):.2f}"
)
except (RuntimeError, 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_coeffs = _parse_coeffs(num_str)
den_coeffs = _parse_coeffs(den_str)
if num_coeffs.size == 0 or den_coeffs.size == 0:
return None, "错误:系数不能为空。", "", ""
system_with_gain = ct.TransferFunction(k * num_coeffs, den_coeffs)
tf_latex_with_gain = f"$$ G_{{open}}(s) = K \\times \\frac{{{coeffs_to_latex(num_coeffs)}}}{{{coeffs_to_latex(den_coeffs)}}} \\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()
metrics_text, stability_text = "无法计算稳定裕度。", "**评估**: <font color='orange'>**无法判断**</font>"
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"**评估**: <font color='{'green' if is_stable else 'red'}'>**{'系统稳定' if is_stable else '系统不稳定'}**</font>"
metrics_text = ""
if np.isinf(gm_db): metrics_text += f"增益裕度 (GM): inf dB\n(注: 相位未穿过-180°线)\n"
else: metrics_text += f"增益裕度 (GM): {gm_db:.2f} dB\n"
if np.isinf(pm): metrics_text += f"相位裕度 (PM): 未定义"
else: metrics_text += f"相位裕度 (PM): {pm:.2f} deg"
except Exception:
pass
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_coeffs = _parse_coeffs(num_str)
den_coeffs = _parse_coeffs(den_str)
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)
# 绘制根轨迹
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-', linewidth=1.5, alpha=0.7)
# 只标记当前增益下的闭环极点
ax.plot(np.real(current_poles), np.imag(current_poles), 'rx',
markersize=12, markeredgewidth=2.5, label=f'Poles at K={k:.2f}', zorder=5)
# 动态计算坐标轴范围 - 只基于当前闭环极点位置
pole_real = np.real(current_poles)
pole_imag = np.imag(current_poles)
# 找到极点的最大绝对值(实部和虚部)
max_real = np.max(np.abs(pole_real))
max_imag = np.max(np.abs(pole_imag))
# 取实部和虚部的最大值作为基准
max_val = max(max_real, max_imag)
# 确保最小显示范围
if max_val < 1.0:
max_val = 1.0
# 添加 30% 的边距,使图像更美观
margin = 1.3
limit = max_val * margin
# 设置对称的坐标轴范围(以原点为中心)
ax.set_xlim(-limit, limit)
ax.set_ylim(-limit, limit)
# 绘制虚轴(稳定性边界)和实轴
ax.axvline(x=0, color='k', linestyle='--', linewidth=0.8, alpha=0.5)
ax.axhline(y=0, color='k', linestyle='-', linewidth=0.5, alpha=0.3)
ax.set_xlabel("Real Axis")
ax.set_ylabel("Imaginary Axis")
ax.set_title("Root Locus")
ax.grid(True, alpha=0.3)
ax.legend(loc='best', fontsize=9)
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
+147 -2287
View File
File diff suppressed because it is too large Load Diff
+499
View File
@@ -0,0 +1,499 @@
"""
知识卡片内容 - 包含时域、频域、根轨迹分析的公式和方法
使用纯HTML格式,无需LaTeX渲染库
"""
# 时域分析知识卡片
TIME_DOMAIN_KNOWLEDGE = """
<div class='knowledge-card'>
<h3 style='color: #667eea; margin-top: 0; display: flex; align-items: center; gap: 10px;'>
<span style='font-size: 1.3em;'>📚</span> 时域分析常用公式
</h3>
<details open>
<summary style='background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
📐 二阶系统标准形式
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<p style='margin: 10px 0;'><strong>标准传递函数:</strong></p>
<div style='text-align: center; background: white; padding: 20px; border-radius: 8px; margin: 10px 0; box-shadow: 0 2px 6px rgba(0,0,0,0.05);'>
<div style='font-size: 1.3em; display: inline-block;'>
<i>G</i>(<i>s</i>) =
<div style='display: inline-block; vertical-align: middle; margin: 0 8px;'>
<div style='text-align: center; border-bottom: 2px solid #333; padding: 0 10px 5px;'>ω<sub>n</sub><sup>2</sup></div>
<div style='text-align: center; padding: 5px 10px 0;'><i>s</i><sup>2</sup> + 2ζω<sub>n</sub><i>s</i> + ω<sub>n</sub><sup>2</sup></div>
</div>
</div>
</div>
<h4 style='color: #667eea; margin-top: 20px; font-size: 1.1em;'>📊 关键参数</h4>
<table style='width: 100%; border-collapse: collapse; margin: 15px 0; font-size: 0.95em; box-shadow: 0 2px 10px rgba(0,0,0,0.08); border-radius: 8px; overflow: hidden;'>
<tr style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;'>
<th style='padding: 12px; text-align: left; font-weight: 700;'>参数</th>
<th style='padding: 12px; text-align: left; font-weight: 700;'>含义</th>
<th style='padding: 12px; text-align: left; font-weight: 700;'>影响</th>
</tr>
<tr style='background: white;'>
<td style='padding: 10px; border: 1px solid #e0e7ff;'><strong>ζ (zeta)</strong></td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'>阻尼比</td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'>控制超调量和振荡</td>
</tr>
<tr style='background: #f8f9ff;'>
<td style='padding: 10px; border: 1px solid #e0e7ff;'><strong>ω<sub>n</sub></strong></td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'>无阻尼自然频率 (rad/s)</td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'>决定响应速度</td>
</tr>
</table>
<h4 style='color: #764ba2; margin-top: 20px; font-size: 1.1em;'>🎯 阻尼比分类</h4>
<ul style='line-height: 2; margin-left: 20px;'>
<li><strong style='color: #f5576c;'>ζ &lt; 0:</strong> 不稳定系统</li>
<li><strong style='color: #ffa726;'>ζ = 0:</strong> 无阻尼振荡</li>
<li><strong style='color: #4caf50;'>0 &lt; ζ &lt; 1:</strong> 欠阻尼(有振荡)⭐ 最常见</li>
<li><strong style='color: #2196f3;'>ζ = 1:</strong> 临界阻尼(无超调)</li>
<li><strong style='color: #9c27b0;'>ζ &gt; 1:</strong> 过阻尼(响应慢)</li>
</ul>
</div>
</details>
<details open>
<summary style='background: linear-gradient(135deg, #fff0f8 0%, #fce4ec 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
📊 时域性能指标(欠阻尼系统)
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<div style='background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); padding: 15px; border-radius: 10px; margin: 12px 0; border-left: 4px solid #667eea;'>
<strong style='color: #667eea; font-size: 1.1em;'>1️⃣ 上升时间 (Rise Time, t<sub>r</sub>)</strong>
<p style='margin: 8px 0;'>响应从 10% 上升到 90% 终值所需时间</p>
<div style='text-align: center; background: white; padding: 15px; border-radius: 6px; margin: 8px 0;'>
<div style='font-size: 1.2em; display: inline-block;'>
<i>t</i><sub>r</sub> ≈
<div style='display: inline-block; vertical-align: middle; margin: 0 5px;'>
<div style='text-align: center; border-bottom: 2px solid #333; padding: 0 8px 3px;'>1.8</div>
<div style='text-align: center; padding: 3px 8px 0;'>ω<sub>n</sub></div>
</div>
</div>
</div>
</div>
<div style='background: linear-gradient(135deg, #fff4f0 0%, #ffe0b2 100%); padding: 15px; border-radius: 10px; margin: 12px 0; border-left: 4px solid #ff9800;'>
<strong style='color: #f57c00; font-size: 1.1em;'>2️⃣ 峰值时间 (Peak Time, t<sub>p</sub>)</strong>
<p style='margin: 8px 0;'>响应达到第一个峰值的时间</p>
<div style='text-align: center; background: white; padding: 15px; border-radius: 6px; margin: 8px 0;'>
<div style='font-size: 1.2em; display: inline-block;'>
<i>t</i><sub>p</sub> =
<div style='display: inline-block; vertical-align: middle; margin: 0 5px;'>
<div style='text-align: center; border-bottom: 2px solid #333; padding: 0 8px 3px;'>π</div>
<div style='text-align: center; padding: 3px 8px 0;'>ω<sub>n</sub>√(1−ζ²)</div>
</div>
=
<div style='display: inline-block; vertical-align: middle; margin: 0 5px;'>
<div style='text-align: center; border-bottom: 2px solid #333; padding: 0 8px 3px;'>π</div>
<div style='text-align: center; padding: 3px 8px 0;'>ω<sub>d</sub></div>
</div>
</div>
</div>
<p style='margin: 8px 0; color: #666; font-size: 0.95em;'>其中 ω<sub>d</sub> = ω<sub>n</sub>√(1−ζ²) 为阻尼振荡频率</p>
</div>
<div style='background: linear-gradient(135deg, #f0fff4 0%, #c8e6c9 100%); padding: 15px; border-radius: 10px; margin: 12px 0; border-left: 4px solid #4caf50;'>
<strong style='color: #388e3c; font-size: 1.1em;'>3️⃣ 超调量 (Overshoot, σ%)</strong>
<p style='margin: 8px 0;'>响应超过稳态值的最大百分比</p>
<div style='text-align: center; background: white; padding: 15px; border-radius: 6px; margin: 8px 0; font-size: 1.2em;'>
σ% = <i>e</i><sup>−πζ/√(1−ζ²)</sup> × 100%
</div>
<p style='margin: 8px 0; color: #388e3c; font-weight: 600;'>💡 仅与 ζ 有关!</p>
<table style='width: 100%; margin: 10px 0; font-size: 0.9em;'>
<tr style='background: #e8f5e9;'>
<td style='padding: 6px; border: 1px solid #c8e6c9;'><strong>ζ = 0.5</strong></td>
<td style='padding: 6px; border: 1px solid #c8e6c9;'>σ% ≈ 16%</td>
</tr>
<tr style='background: white;'>
<td style='padding: 6px; border: 1px solid #c8e6c9;'><strong>ζ = 0.707</strong></td>
<td style='padding: 6px; border: 1px solid #c8e6c9;'>σ% ≈ 4.3% ⭐ 最佳</td>
</tr>
</table>
</div>
<div style='background: linear-gradient(135deg, #fff0f8 0%, #f3e5f5 100%); padding: 15px; border-radius: 10px; margin: 12px 0; border-left: 4px solid #9c27b0;'>
<strong style='color: #7b1fa2; font-size: 1.1em;'>4️⃣ 调节时间 (Settling Time, t<sub>s</sub>)</strong>
<p style='margin: 8px 0;'>响应达到并保持在稳态值 ±2%(或±5%)范围内的时间</p>
<div style='text-align: center; background: white; padding: 15px; border-radius: 6px; margin: 8px 0;'>
<div style='font-size: 1.2em;'>
<i>t</i><sub>s</sub> ≈
<div style='display: inline-block; vertical-align: middle; margin: 0 5px;'>
<div style='text-align: center; border-bottom: 2px solid #333; padding: 0 8px 3px;'>4</div>
<div style='text-align: center; padding: 3px 8px 0;'>ζω<sub>n</sub></div>
</div>
&nbsp; (2% 误差带)
</div>
<div style='font-size: 1.2em; margin-top: 10px;'>
<i>t</i><sub>s</sub> ≈
<div style='display: inline-block; vertical-align: middle; margin: 0 5px;'>
<div style='text-align: center; border-bottom: 2px solid #333; padding: 0 8px 3px;'>3</div>
<div style='text-align: center; padding: 3px 8px 0;'>ζω<sub>n</sub></div>
</div>
&nbsp; (5% 误差带)
</div>
</div>
</div>
</div>
</details>
<details>
<summary style='background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
🎯 一阶系统特性
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<p><strong>标准传递函数:</strong></p>
<div style='text-align: center; background: white; padding: 20px; border-radius: 8px; margin: 10px 0;'>
<div style='font-size: 1.3em; display: inline-block;'>
<i>G</i>(<i>s</i>) =
<div style='display: inline-block; vertical-align: middle; margin: 0 8px;'>
<div style='text-align: center; border-bottom: 2px solid #333; padding: 0 10px 5px;'><i>K</i></div>
<div style='text-align: center; padding: 5px 10px 0;'>τ<i>s</i> + 1</div>
</div>
</div>
</div>
<p><strong>阶跃响应:</strong></p>
<div style='text-align: center; background: white; padding: 20px; border-radius: 8px; margin: 10px 0; font-size: 1.2em;'>
<i>y</i>(<i>t</i>) = <i>K</i>(1 <i>e</i><sup><i>t</i>/τ</sup>)
</div>
<div style='background: #e3f2fd; padding: 12px; border-radius: 8px; margin: 10px 0;'>
<p><strong>💡 关键时间点:</strong></p>
<ul style='line-height: 1.8;'>
<li><strong><i>t</i> = τ:</strong> 达到稳态值的 63.2%</li>
<li><strong><i>t</i> = 3τ:</strong> 达到稳态值的 95%</li>
<li><strong><i>t</i> = 4τ:</strong> 达到稳态值的 98.2%</li>
<li><strong><i>t</i> = 5τ:</strong> 达到稳态值的 99.3%</li>
</ul>
</div>
</div>
</details>
</div>
"""
# 频域分析知识卡片
FREQUENCY_DOMAIN_KNOWLEDGE = """
<div class='knowledge-card'>
<h3 style='color: #667eea; margin-top: 0; display: flex; align-items: center; gap: 10px;'>
<span style='font-size: 1.3em;'>📚</span> 频域分析常用方法
</h3>
<details open>
<summary style='background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
🎯 增益裕度 (Gain Margin, GM)
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<p><strong>定义:</strong>在相角为 -180° 时,系统增益可增加的最大倍数</p>
<div style='text-align: center; background: white; padding: 20px; border-radius: 8px; margin: 10px 0;'>
<div style='font-size: 1.3em;'>
GM<sub>dB</sub> = 20log<sub>10</sub>|<i>G</i>(<i>j</i>ω<sub>pc</sub>)|
</div>
</div>
<p style='margin: 8px 0;'><strong>ω<sub>pc</sub></strong> - 相角交越频率(相角 = -180°)</p>
<h4 style='color: #667eea; margin-top: 15px;'>判断准则</h4>
<table style='width: 100%; border-collapse: collapse; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08); border-radius: 8px; overflow: hidden;'>
<tr style='background: #e8f5e9;'>
<td style='padding: 10px; border: 1px solid #c8e6c9; font-weight: 600;'>GM &gt; 0 dB</td>
<td style='padding: 10px; border: 1px solid #c8e6c9;'>✅ 系统稳定</td>
</tr>
<tr style='background: #fff3e0;'>
<td style='padding: 10px; border: 1px solid #ffe0b2; font-weight: 600;'>GM = 0 dB</td>
<td style='padding: 10px; border: 1px solid #ffe0b2;'>⚠️ 临界稳定</td>
</tr>
<tr style='background: #ffebee;'>
<td style='padding: 10px; border: 1px solid #ffcdd2; font-weight: 600;'>GM &lt; 0 dB</td>
<td style='padding: 10px; border: 1px solid #ffcdd2;'>❌ 系统不稳定</td>
</tr>
</table>
<div style='background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); padding: 12px; border-radius: 8px; margin: 10px 0;'>
<p><strong>💡 工程要求:</strong>通常要求 <strong>GM ≥ 6 dB</strong>(约2倍增益余量)</p>
</div>
</div>
</details>
<details open>
<summary style='background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
🎯 相角裕度 (Phase Margin, PM)
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<p><strong>定义:</strong>在增益为 1 (0dB) 时,系统相角与 -180° 的差值</p>
<div style='text-align: center; background: white; padding: 20px; border-radius: 8px; margin: 10px 0; font-size: 1.3em;'>
PM = 180° + ∠<i>G</i>(<i>j</i>ω<sub>gc</sub>)
</div>
<p style='margin: 8px 0;'><strong>ω<sub>gc</sub></strong> - 增益交越频率(幅值 = 1 或 0dB</p>
<h4 style='color: #764ba2; margin-top: 15px;'>判断准则</h4>
<table style='width: 100%; border-collapse: collapse; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08); border-radius: 8px; overflow: hidden;'>
<tr style='background: #e8f5e9;'>
<td style='padding: 10px; border: 1px solid #c8e6c9; font-weight: 600;'>PM &gt; 0°</td>
<td style='padding: 10px; border: 1px solid #c8e6c9;'>✅ 系统稳定</td>
</tr>
<tr style='background: #fff3e0;'>
<td style='padding: 10px; border: 1px solid #ffe0b2; font-weight: 600;'>PM = 0°</td>
<td style='padding: 10px; border: 1px solid #ffe0b2;'>⚠️ 临界稳定</td>
</tr>
<tr style='background: #ffebee;'>
<td style='padding: 10px; border: 1px solid #ffcdd2; font-weight: 600;'>PM &lt; 0°</td>
<td style='padding: 10px; border: 1px solid #ffcdd2;'>❌ 系统不稳定</td>
</tr>
</table>
<div style='background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); padding: 12px; border-radius: 8px; margin: 10px 0;'>
<p><strong>💡 工程要求:</strong>通常要求 <strong>PM ∈ [30°, 60°]</strong></p>
<p style='margin: 8px 0;'>• PM ≈ 45° ~ 60°: 良好阻尼特性</p>
<p style='margin: 8px 0;'>• PM 越大,超调量越小</p>
</div>
<h4 style='color: #f57c00; margin-top: 15px;'>与时域性能的关系</h4>
<div style='text-align: center; background: white; padding: 15px; border-radius: 8px; margin: 10px 0; font-size: 1.2em;'>
ζ ≈ <div style='display: inline-block; vertical-align: middle; margin: 0 5px;'>
<div style='text-align: center; border-bottom: 2px solid #333; padding: 0 8px 3px;'>PM</div>
<div style='text-align: center; padding: 3px 8px 0;'>100</div>
</div> &nbsp; (PM 以度为单位)
</div>
<table style='width: 100%; margin: 10px 0; font-size: 0.9em; box-shadow: 0 2px 6px rgba(0,0,0,0.06); border-radius: 6px; overflow: hidden;'>
<tr style='background: #f5f5f5; font-weight: 600;'>
<td style='padding: 8px; border: 1px solid #ddd;'>PM</td>
<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;'>30°</td><td style='padding: 8px; border: 1px solid #ddd;'>≈ 0.3</td><td style='padding: 8px; border: 1px solid #ddd;'>≈ 37%</td></tr>
<tr><td style='padding: 8px; border: 1px solid #ddd;'>45°</td><td style='padding: 8px; border: 1px solid #ddd;'>≈ 0.45</td><td style='padding: 8px; border: 1px solid #ddd;'>≈ 20%</td></tr>
<tr><td style='padding: 8px; border: 1px solid #ddd;'>60°</td><td style='padding: 8px; border: 1px solid #ddd;'>≈ 0.6</td><td style='padding: 8px; border: 1px solid #ddd;'>≈ 10% ⭐</td></tr>
</table>
</div>
</details>
<details>
<summary style='background: linear-gradient(135deg, #fff0f8 0%, #fce4ec 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
📊 Bode 图与 Nyquist 图
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<h4 style='color: #667eea;'>Bode 图(伯德图)</h4>
<p><strong>组成:</strong>幅频特性图 + 相频特性图</p>
<ul style='line-height: 1.8;'>
<li><strong>横轴:</strong>频率 ω (对数刻度)</li>
<li><strong>纵轴(幅频):</strong>20log|<i>G</i>(<i>j</i>ω)| (dB)</li>
<li><strong>纵轴(相频):</strong>∠<i>G</i>(<i>j</i>ω) (度)</li>
</ul>
<div style='background: #e3f2fd; padding: 12px; border-radius: 8px; margin: 10px 0;'>
<p><strong>💡 优点:</strong></p>
<ul style='line-height: 1.6;'>
<li>便于绘制(渐近线近似)</li>
<li>直观读取稳定裕度</li>
<li>串联系统可图形叠加</li>
</ul>
</div>
<h4 style='color: #764ba2; margin-top: 20px;'>Nyquist 图(奈奎斯特图)</h4>
<p><strong>定义:</strong>开环频率特性在复平面上的轨迹</p>
<div style='background: #fff3e0; padding: 12px; border-radius: 8px; margin: 10px 0;'>
<p><strong>🎯 奈奎斯特稳定判据:</strong></p>
<p style='text-align: center; font-weight: 600; margin: 10px 0; font-size: 1.2em;'><i>Z</i> = <i>P</i> <i>N</i></p>
<p style='font-size: 0.95em;'>• <i>Z</i>: 闭环右半平面极点数<br>
• <i>P</i>: 开环右半平面极点数<br>
• <i>N</i>: 曲线逆时针包围 (1, <i>j</i>0) 的圈数</p>
<p style='margin-top: 10px; color: #f57c00; font-weight: 600;'>稳定条件:<i>Z</i> = 0</p>
</div>
</div>
</details>
<details>
<summary style='background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
🔧 典型环节频率特性
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<div style='margin-bottom: 15px; padding: 12px; background: white; border-radius: 8px; border-left: 4px solid #667eea;'>
<h4 style='margin-top: 0; color: #667eea;'>1️⃣ 比例环节 <i>K</i></h4>
<p>• 幅频:20log<i>K</i> (dB) - 水平线<br>• 相频:0° - 水平线</p>
</div>
<div style='margin-bottom: 15px; padding: 12px; background: white; border-radius: 8px; border-left: 4px solid #f57c00;'>
<h4 style='margin-top: 0; color: #f57c00;'>2️⃣ 积分环节 1/<i>s</i></h4>
<p>• 幅频:-20dB/dec 斜率<br>• 相频:-90° - 水平线</p>
</div>
<div style='margin-bottom: 15px; padding: 12px; background: white; border-radius: 8px; border-left: 4px solid #4caf50;'>
<h4 style='margin-top: 0; color: #4caf50;'>3️⃣ 惯性环节 1/(<i>Ts</i>+1)</h4>
<p>• 转折频率:ω = 1/<i>T</i><br>• 幅频:低频 0dB,高频 -20dB/dec<br>• 相频:0° → -90°</p>
</div>
</div>
</details>
</div>
"""
# 根轨迹分析知识卡片
ROOT_LOCUS_KNOWLEDGE = """
<div class='knowledge-card'>
<h3 style='color: #667eea; margin-top: 0; display: flex; align-items: center; gap: 10px;'>
<span style='font-size: 1.3em;'>📚</span> 根轨迹分析要点
</h3>
<details open>
<summary style='background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
🎯 根轨迹基本概念
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<p><strong>定义:</strong>当开环增益 <i>K</i> 从 0 → ∞ 变化时,闭环特征方程根(极点)在 <i>s</i> 平面上的运动轨迹</p>
<h4 style='color: #667eea; margin-top: 15px;'>特征方程</h4>
<div style='text-align: center; background: white; padding: 15px; border-radius: 8px; margin: 10px 0; font-size: 1.3em;'>
1 + <i>KG</i>(<i>s</i>)<i>H</i>(<i>s</i>) = 0
</div>
<h4 style='color: #764ba2; margin-top: 15px;'>主要作用</h4>
<ul style='line-height: 1.8;'>
<li>✅ 直观显示增益对极点位置的影响</li>
<li>✅ 判断系统稳定性</li>
<li>✅ 选择合适的增益值</li>
<li>✅ 设计控制器参数</li>
</ul>
</div>
</details>
<details open>
<summary style='background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
📐 根轨迹绘制基本条件
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<div style='background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); padding: 15px; border-radius: 10px; margin: 12px 0; border-left: 4px solid #4caf50;'>
<h4 style='margin-top: 0; color: #388e3c;'>1️⃣ 幅值条件(充要条件)</h4>
<div style='text-align: center; background: white; padding: 15px; border-radius: 6px; margin: 8px 0; font-size: 1.2em;'>
|<i>KG</i>(<i>s</i><sub>0</sub>)<i>H</i>(<i>s</i><sub>0</sub>)| = 1
</div>
<p style='margin: 8px 0;'><strong>💡 用途:</strong>确定增益 <i>K</i> 的值</p>
</div>
<div style='background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); padding: 15px; border-radius: 10px; margin: 12px 0; border-left: 4px solid #ff9800;'>
<h4 style='margin-top: 0; color: #f57c00;'>2️⃣ 相角条件(充要条件)</h4>
<div style='text-align: center; background: white; padding: 15px; border-radius: 6px; margin: 8px 0; font-size: 1.2em;'>
∠<i>G</i>(<i>s</i><sub>0</sub>)<i>H</i>(<i>s</i><sub>0</sub>) = (2<i>k</i>+1) × 180°
</div>
<p style='margin: 8px 0; font-size: 0.95em;'>其中 <i>k</i> = 0, ±1, ±2, ...</p>
<p style='margin: 8px 0;'><strong>💡 用途:</strong>判断 <i>s</i> 平面某点是否在根轨迹上</p>
</div>
</div>
</details>
<details open>
<summary style='background: linear-gradient(135deg, #fff0f8 0%, #fce4ec 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
🌟 根轨迹基本性质
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<table style='width: 100%; border-collapse: collapse; margin: 15px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.08); border-radius: 8px; overflow: hidden;'>
<tr style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;'>
<th style='padding: 12px; text-align: left; font-weight: 700;'>规则</th>
<th style='padding: 12px; text-align: left; font-weight: 700;'>内容</th>
</tr>
<tr style='background: white;'>
<td style='padding: 10px; border: 1px solid #e0e7ff; font-weight: 600;'>起点</td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'><i>K</i>=0,位于<strong>开环极点</strong></td>
</tr>
<tr style='background: #f8f9ff;'>
<td style='padding: 10px; border: 1px solid #e0e7ff; font-weight: 600;'>终点</td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'><i>K</i>→∞,位于<strong>开环零点</strong>或无穷远</td>
</tr>
<tr style='background: white;'>
<td style='padding: 10px; border: 1px solid #e0e7ff; font-weight: 600;'>分支数</td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'>max(<i>n</i>, <i>m</i>)<i>n</i>=极点数,<i>m</i>=零点数</td>
</tr>
<tr style='background: #f8f9ff;'>
<td style='padding: 10px; border: 1px solid #e0e7ff; font-weight: 600;'>实轴段</td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'>右侧零极点总数为<strong>奇数</strong>的区段</td>
</tr>
<tr style='background: white;'>
<td style='padding: 10px; border: 1px solid #e0e7ff; font-weight: 600;'>渐近线数</td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'><strong><i>n</i> <i>m</i></strong> 条</td>
</tr>
<tr style='background: #f8f9ff;'>
<td style='padding: 10px; border: 1px solid #e0e7ff; font-weight: 600;'>渐近线角度</td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'>φ<sub>a</sub> = (2<i>k</i>+1)×180°/(<i>n</i><i>m</i>)</td>
</tr>
<tr style='background: white;'>
<td style='padding: 10px; border: 1px solid #e0e7ff; font-weight: 600;'>渐近线交点</td>
<td style='padding: 10px; border: 1px solid #e0e7ff;'>σ<sub>a</sub> = (Σ<i>p</i> Σ<i>z</i>)/(<i>n</i><i>m</i>)</td>
</tr>
</table>
</div>
</details>
<details>
<summary style='background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
🛡️ <i>s</i> 平面稳定性区域
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<table style='width: 100%; border-collapse: collapse; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08); border-radius: 8px; overflow: hidden;'>
<tr style='background: #f5f5f5; font-weight: 600;'>
<th style='padding: 10px; border: 1px solid #ddd;'>区域</th>
<th style='padding: 10px; border: 1px solid #ddd;'>条件</th>
<th style='padding: 10px; border: 1px solid #ddd;'>稳定性</th>
</tr>
<tr>
<td style='padding: 10px; border: 1px solid #ddd;'>左半平面</td>
<td style='padding: 10px; border: 1px solid #ddd;'>Re(<i>s</i>) &lt; 0</td>
<td style='padding: 10px; border: 1px solid #ddd; background: #c8e6c9;'><strong>✅ 稳定</strong></td>
</tr>
<tr>
<td style='padding: 10px; border: 1px solid #ddd;'>虚轴</td>
<td style='padding: 10px; border: 1px solid #ddd;'>Re(<i>s</i>) = 0</td>
<td style='padding: 10px; border: 1px solid #ddd; background: #fff9c4;'><strong>⚠️ 临界稳定</strong></td>
</tr>
<tr>
<td style='padding: 10px; border: 1px solid #ddd;'>右半平面</td>
<td style='padding: 10px; border: 1px solid #ddd;'>Re(<i>s</i>) &gt; 0</td>
<td style='padding: 10px; border: 1px solid #ddd; background: #ffcdd2;'><strong>❌ 不稳定</strong></td>
</tr>
</table>
<div style='background: #e3f2fd; padding: 12px; border-radius: 8px; margin: 10px 0;'>
<p><strong>💡 虚轴(<i>x</i>=0</strong>即为<strong>稳定性边界</strong></p>
<p style='margin: 8px 0;'>根轨迹穿越虚轴点对应<strong>临界增益 <i>K</i><sub>c</sub></strong></p>
</div>
</div>
</details>
<details>
<summary style='background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); padding: 12px 16px; border-radius: 10px; cursor: pointer; font-weight: 700; color: #333; font-size: 1.05em; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
📏 阻尼比等值线
</summary>
<div style='padding: 15px; background: #fafbff; margin-top: 5px; border-radius: 10px; border: 1px solid #e0e7ff;'>
<p>从原点出发的射线代表恒定阻尼比 ζ 的轨迹:</p>
<div style='text-align: center; background: white; padding: 15px; border-radius: 8px; margin: 10px 0; font-size: 1.2em;'>
θ = arccos(ζ)
</div>
<table style='width: 100%; margin: 10px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.06); border-radius: 6px; overflow: hidden;'>
<tr style='background: #f5f5f5; font-weight: 600;'>
<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>
<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>
<td style='padding: 8px; border: 1px solid #ddd;'><strong>0.707</strong></td>
<td style='padding: 8px; border: 1px solid #ddd;'><strong>45°</strong></td>
<td style='padding: 8px; border: 1px solid #ddd;'><strong>⭐ 最佳阻尼</strong></td>
</tr>
</table>
<div style='background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); padding: 12px; border-radius: 8px; margin: 10px 0;'>
<p><strong>💡 工程应用:</strong>通过根轨迹与阻尼比等值线的交点,选择满足性能要求的增益 <i>K</i></p>
</div>
</div>
</details>
</div>
"""
+337
View File
@@ -0,0 +1,337 @@
/* ==================== 全局样式 ==================== */
* {
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 显示修复 ==================== */
* {
font-feature-settings: "liga" 1, "calt" 1, "kern" 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.gr-markdown code {
background: transparent !important; padding: 0 !important; font-family: inherit !important;
}
.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;
}
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;
}
p.subtitle { color: #555 !important; }
/* ==================== 项目信息横幅 ==================== */
.project-info-banner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px 30px;
border-radius: 20px;
margin-bottom: 30px;
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.35);
animation: bannerSlide 0.6s ease-out;
position: relative;
overflow: hidden;
}
.project-info-banner::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: shimmer 8s infinite linear;
}
@keyframes shimmer {
0% { transform: translate(-100%, -100%) rotate(0deg); }
100% { transform: translate(100%, 100%) rotate(360deg); }
}
@keyframes bannerSlide {
from { transform: translateY(-30px); 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: 20px !important;
padding: 28px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.06), 0 2px 8px rgba(0, 0, 0, 0.04) !important;
border: 1px solid rgba(102, 126, 234, 0.08) !important;
margin-bottom: 20px !important;
transition: transform 0.3s ease, box-shadow 0.3s ease !important;
backdrop-filter: blur(10px);
}
.gr-group:hover {
transform: translateY(-6px) !important;
box-shadow: 0 12px 48px rgba(102, 126, 234, 0.12), 0 4px 16px rgba(102, 126, 234, 0.08) !important;
}
.card-title {
font-size: 1.5em;
font-weight: 700;
color: #333 !important;
margin-bottom: 18px;
border-bottom: 3px solid;
border-image: linear-gradient(90deg, #667eea, #764ba2) 1;
padding-bottom: 12px;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: -0.5px;
}
/* ==================== 输入框样式 ==================== */
.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: 12px !important;
font-weight: 600 !important;
padding: 14px 28px !important;
font-size: 1.05em !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
border: none !important;
cursor: pointer !important;
letter-spacing: 0.3px;
}
.primary-btn, button[variant="primary"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
position: relative;
overflow: hidden;
}
.primary-btn::before, button[variant="primary"]::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.primary-btn:hover::before, button[variant="primary"]:hover::before {
width: 300px;
height: 300px;
}
.primary-btn:hover, button[variant="primary"]:hover {
transform: translateY(-4px) scale(1.03) !important;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.5) !important;
}
button[variant="secondary"] {
background-color: white !important;
color: #667eea !important;
border: 2px solid #667eea !important;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15) !important;
}
button[variant="secondary"]:hover {
background: linear-gradient(135deg, #f0f4ff 0%, #faf5ff 100%) !important;
transform: translateY(-3px) !important;
box-shadow: 0 6px 18px rgba(102, 126, 234, 0.25) !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;
}
/* ==================== 聊天机器人样式 ==================== */
.modern-chatbot {
background: white !important;
border-radius: 20px !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: 20px 20px 4px 20px !important;
padding: 14px 18px !important;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.25) !important;
animation: messageSlideIn 0.3s ease-out;
}
.message.user * {
color: white !important;
}
.message.bot {
background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%) !important;
border: 1px solid #e0e7ff !important;
border-radius: 20px 20px 20px 4px !important;
color: #2d3748 !important;
padding: 14px 18px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06) !important;
animation: messageSlideIn 0.3s ease-out;
}
.message.bot * { color: #2d3748 !important; }
.message.bot code {
background: #f0f4ff !important;
color: #667eea !important;
padding: 3px 8px !important;
border-radius: 6px !important;
border: 1px solid #e0e7ff !important;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace !important;
}
@keyframes messageSlideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ==================== 知识卡片样式 ==================== */
.knowledge-card {
background: white !important;
border-radius: 16px !important;
padding: 24px !important;
box-shadow: 0 6px 24px rgba(102, 126, 234, 0.1) !important;
border: 1px solid rgba(102, 126, 234, 0.12) !important;
margin: 15px 0 !important;
}
.knowledge-card h3 {
margin-top: 0 !important;
padding-bottom: 15px !important;
border-bottom: 2px solid;
border-image: linear-gradient(90deg, #667eea, #764ba2) 1;
}
.knowledge-card h4 {
margin-top: 18px !important;
margin-bottom: 12px !important;
font-size: 1.1em !important;
font-weight: 700 !important;
}
.knowledge-card details {
margin: 12px 0 !important;
transition: all 0.3s ease !important;
}
.knowledge-card details[open] {
margin-bottom: 18px !important;
}
.knowledge-card details summary {
cursor: pointer !important;
user-select: none !important;
outline: none !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative !important;
}
.knowledge-card details summary:hover {
transform: translateX(5px) !important;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15) !important;
}
.knowledge-card details summary::marker {
font-size: 1.2em !important;
}
.knowledge-card details[open] summary {
margin-bottom: 8px !important;
}
.knowledge-card table {
border-collapse: collapse !important;
width: 100% !important;
margin: 15px 0 !important;
font-size: 0.95em !important;
}
.knowledge-card table td,
.knowledge-card table th {
padding: 12px !important;
text-align: left !important;
}
.knowledge-card ul {
margin: 10px 0 !important;
padding-left: 25px !important;
}
.knowledge-card li {
margin: 6px 0 !important;
line-height: 1.6 !important;
}
.knowledge-card p {
margin: 10px 0 !important;
line-height: 1.7 !important;
}
.knowledge-card strong {
color: #667eea !important;
font-weight: 700 !important;
}
/* 滚动条美化 */
.knowledge-card::-webkit-scrollbar,
div[style*="overflow-y: auto"]::-webkit-scrollbar {
width: 8px !important;
}
.knowledge-card::-webkit-scrollbar-track,
div[style*="overflow-y: auto"]::-webkit-scrollbar-track {
background: #f1f1f1 !important;
border-radius: 4px !important;
}
.knowledge-card::-webkit-scrollbar-thumb,
div[style*="overflow-y: auto"]::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border-radius: 4px !important;
}
.knowledge-card::-webkit-scrollbar-thumb:hover,
div[style*="overflow-y: auto"]::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #5568d3 0%, #6a3f8a 100%) !important;
}
/* ... 其他CSS规则可以从原文件复制过来 ... */
+83
View File
@@ -0,0 +1,83 @@
import json
import aiohttp
import config
async def chat_with_ai(message: str, history: list):
"""
与 AI 模型进行流式对话。
"""
system_prompt = """你是一位精通自动控制原理的专家教授。请用清晰、准确、专业的中文来回答有关自动控制课程内容的问题。
重要规则:
1. 当需要表达数学公式时,必须使用 LaTeX 格式
2. 行内公式使用 $公式$ 或 \\(公式\\)
3. 独立公式使用 $$公式$$ 或 \\[公式\\]
4. 例如:传递函数可以写成 $G(s) = \\frac{K}{s(s+1)}$
5. 二阶系统标准形式:$$G(s) = \\frac{\\omega_n^2}{s^2 + 2\\zeta\\omega_n s + \\omega_n^2}$$
请在适当的时候使用公式和示例来辅助解释。"""
if not config.API_KEY or config.API_KEY.strip() == "":
history.append({"role": "assistant", "content": "❌ 错误:API_KEY 未配置。请在 config.py 文件中配置 API_KEY。"})
yield history
return
history.append({"role": "user", "content": message})
# 添加临时的 "正在思考" 消息
history.append({"role": "assistant", "content": "正在思考..."})
yield history
bot_response = ""
try:
if config.API_TYPE == "deepseek":
api_url = f"{config.API_BASE_URL}/chat/completions"
messages_for_api = [{"role": "system", "content": system_prompt}] + history[:-1]
payload = {
"model": config.API_MODEL,
"messages": messages_for_api,
"stream": True, "temperature": 0.7, "max_tokens": 2048
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {config.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]["content"] = bot_response
yield history
except json.JSONDecodeError:
pass
if not bot_response:
history[-1]["content"] = "⚠️ API 返回了空响应,请稍后重试。"
yield history
else:
error_text = await response.text()
history[-1]["content"] = f"❌ API请求出错 (状态码: {response.status}):\n{error_text}"
yield history
else:
history[-1]["content"] = f"❌ 不支持的 API 类型: {config.API_TYPE}"
yield history
except aiohttp.ClientError as e:
history[-1]["content"] = f"❌ 网络连接错误: {e}"
yield history
except Exception as e:
history[-1]["content"] = f"❌ 发生错误: {type(e).__name__}: {e}"
yield history
+19
View File
@@ -0,0 +1,19 @@
# ==================== API 配置 ====================
# 将 API 相关配置集中在此处,方便修改
# DeepSeek API 配置说明:
# 1. API_KEY: 您的 DeepSeek API 密钥
# - 从 https://platform.deepseek.com/api_keys 获取
# 2. API_BASE_URL: API 服务的基础 URL
# - 官方地址: https://api.deepseek.com/v1
# 3. API_MODEL: 使用的 DeepSeek 模型名称
# - deepseek-chat (推荐) 或 deepseek-coder (代码专用)
API_KEY = "sk-2292af2428d7419897ca1fb6e99ba6bc" # 请在此处填入您的 DeepSeek API 密钥
API_BASE_URL = "https://api.deepseek.com/v1"
API_MODEL = "deepseek-chat"
API_TYPE = "deepseek" # 当前支持 "deepseek"
# ==================== Gradio 应用启动配置 ====================
SERVER_NAME = "0.0.0.0" # 监听所有网络接口
SERVER_PORT = 7860 # 指定一个端口
SHARE = False # 是否创建Gradio的公开分享链接
+2 -2
View File
@@ -1,4 +1,4 @@
{
"total_users": 1,
"last_saved_at": 1760886050.8138287
"total_users": 15,
"last_saved_at": 1760890996.5595384
}
+231
View File
@@ -0,0 +1,231 @@
import gradio as gr
from assets.knowledge_cards_html import (
TIME_DOMAIN_KNOWLEDGE,
FREQUENCY_DOMAIN_KNOWLEDGE,
ROOT_LOCUS_KNOWLEDGE
)
def create_header():
"""创建页面顶部的标题、横幅和在线计数器"""
gr.HTML("<h1 class='main-title'> 自动控制理论AI+数智平台</h1>")
gr.HTML("<p class='subtitle'>✨ 交互式控制系统分析与设计工具 | 时域·频域·根轨迹·AI问答 ✨</p>")
online_counter = gr.HTML(elem_id="online-counter")
gr.HTML("""
<div class='project-info-banner'>
<div style='display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 25px;'>
<div style='display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.1); padding: 12px 18px; border-radius: 12px; backdrop-filter: blur(10px);'>
<span style='font-size: 1.8em; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));'>📚</span>
<div>
<div style='font-size: 0.8em; color: rgba(255,255,255,0.85); font-weight: 500; letter-spacing: 0.5px;'>课程 Course</div>
<div style='font-weight: 700; font-size: 1.15em; color: white; margin-top: 2px;'>自动控制理论</div>
</div>
</div>
<div style='display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.1); padding: 12px 18px; border-radius: 12px; backdrop-filter: blur(10px);'>
<span style='font-size: 1.8em; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));'>👨‍🏫</span>
<div>
<div style='font-size: 0.8em; color: rgba(255,255,255,0.85); font-weight: 500; letter-spacing: 0.5px;'>负责人 Supervisor</div>
<div style='font-weight: 700; font-size: 1.15em; color: white; margin-top: 2px;'>魏鹏飞</div>
</div>
</div>
<div style='display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.1); padding: 12px 18px; border-radius: 12px; backdrop-filter: blur(10px);'>
<span style='font-size: 1.8em; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));'>📧</span>
<div>
<div style='font-size: 0.8em; color: rgba(255,255,255,0.85); font-weight: 500; letter-spacing: 0.5px;'>联系方式 Contact</div>
<a href='mailto:pengfeiwei@nwpu.edu.cn'
style='font-weight: 700; font-size: 1.15em; color: white; text-decoration: none;
border-bottom: 2px solid rgba(255,255,255,0.5); padding-bottom: 2px; margin-top: 2px; display: inline-block;
transition: all 0.3s ease;'
onmouseover='this.style.borderColor="white"; this.style.transform="translateY(-1px)";'
onmouseout='this.style.borderColor="rgba(255,255,255,0.5)"; this.style.transform="translateY(0)";'>
pengfeiwei@nwpu.edu.cn
</a>
</div>
</div>
</div>
<div style='text-align: center; margin-top: 20px; padding-top: 18px; border-top: 2px solid rgba(255,255,255,0.2);'>
<div style='display: inline-flex; align-items: center; gap: 8px; background: rgba(255,255,255,0.08); padding: 10px 20px; border-radius: 25px; backdrop-filter: blur(5px);'>
<span style='font-size: 1.3em; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));'>🎓</span>
<span style='font-weight: 600; font-size: 1.05em; color: white;'>西北工业大学</span>
<span style='color: white; opacity: 0.9; font-size: 1.05em;'>Northwestern Polytechnical University</span>
</div>
<div style='margin-top: 10px;'>
<span style='color: white; opacity: 0.9; font-size: 0.95em; font-weight: 500;'>2025年校级本科生建设项目资助</span>
</div>
</div>
</div>
""")
return online_counter
def create_time_domain_tab():
"""创建时域分析选项卡的UI组件"""
ui_dict = {}
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>🔧 系统模型</div>")
ui_dict["tf_display"] = gr.Markdown(label="当前传递函数", elem_classes="output-display")
with gr.Row():
ui_dict["confirm_button"] = gr.Button("✓ 显示传递函数", variant="secondary", scale=1)
ui_dict["analyze_button"] = gr.Button("🚀 开始分析", variant="primary", scale=1, elem_classes="primary-btn")
with gr.Group():
gr.HTML("<div class='card-title'>📈 动态性能指标</div>")
ui_dict["output_metrics"] = gr.Textbox(
label="Performance Metrics", lines=8, interactive=False, elem_classes="output-metrics"
)
with gr.Column(scale=2):
ui_dict["output_plot"] = gr.Plot(label="时域响应曲线", elem_classes="plot-container")
# 知识卡片
gr.HTML(f"""
<div id="time-domain-knowledge" style="max-height: 600px; overflow-y: auto; padding-right: 8px;">
{TIME_DOMAIN_KNOWLEDGE}
</div>
<script>
(function() {{
// 等待DOM加载完成
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', renderMath);
}} else {{
renderMath();
}}
function renderMath() {{
// 延迟执行以确保MathJax已加载
setTimeout(function() {{
if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {{
MathJax.typesetPromise([document.getElementById('time-domain-knowledge')])
.catch((err) => console.log('MathJax渲染错误:', err));
}} else {{
console.log('MathJax未加载,将在500ms后重试');
setTimeout(renderMath, 500);
}}
}}, 300);
}}
}})();
</script>
""")
return ui_dict
def create_frequency_domain_tab():
"""创建频域分析选项卡的UI组件"""
ui_dict = {}
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>🎚️ 调整系统增益</div>")
ui_dict["log_k_slider"] = gr.Slider(minimum=-4, maximum=4, value=1, step=0.01, label="对数增益 log₁₀(K)", info="💡 拖动滑块查看实时变化")
ui_dict["k_number_display"] = gr.Number(value=10.0, label="当前增益 K", interactive=False, elem_classes="gain-display")
with gr.Group():
gr.HTML("<div class='card-title'>🔧 当前系统模型</div>")
ui_dict["tf_display"] = gr.Markdown(label="含增益K的开环传递函数", elem_classes="output-display")
with gr.Group():
gr.HTML("<div class='card-title'>📊 稳定裕度分析</div>")
ui_dict["metrics_display"] = gr.Textbox(label="Stability Margins", lines=4, interactive=False, elem_classes="output-metrics")
ui_dict["stability_display"] = gr.Markdown(elem_classes="stability-result")
with gr.Column(scale=2):
ui_dict["plot_output"] = gr.Plot(label="频域响应图", elem_classes="plot-container")
# 知识卡片
gr.HTML(f"""
<div id="freq-domain-knowledge" style="max-height: 600px; overflow-y: auto; padding-right: 8px;">
{FREQUENCY_DOMAIN_KNOWLEDGE}
</div>
<script>
(function() {{
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', renderMath);
}} else {{
renderMath();
}}
function renderMath() {{
setTimeout(function() {{
if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {{
MathJax.typesetPromise([document.getElementById('freq-domain-knowledge')])
.catch((err) => console.log('MathJax渲染错误:', err));
}} else {{
console.log('MathJax未加载,将在500ms后重试');
setTimeout(renderMath, 500);
}}
}}, 300);
}}
}})();
</script>
""")
return ui_dict
def create_root_locus_tab():
"""创建根轨迹分析选项卡的UI组件"""
ui_dict = {}
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.HTML("<div class='card-title'>🎚️ 调整系统增益</div>")
ui_dict["log_k_slider"] = gr.Slider(minimum=-4, maximum=4, value=1, step=0.01, label="对数增益 log₁₀(K)", info="💡 拖动滑块观察极点移动")
ui_dict["k_number_display"] = gr.Number(value=10.0, label="当前增益 K", interactive=False, elem_classes="gain-display")
with gr.Group():
gr.HTML("<div class='card-title'>📍 闭环极点位置</div>")
ui_dict["poles_display"] = gr.Textbox(label="Closed-Loop Pole Locations", lines=6, interactive=False, elem_classes="output-metrics")
with gr.Column(scale=2):
ui_dict["plot_output"] = gr.Plot(label="根轨迹图")
gr.HTML(f"""
<div id="root-locus-knowledge" style="max-height: 600px; overflow-y: auto; padding-right: 8px;">
{ROOT_LOCUS_KNOWLEDGE}
</div>
<script>
(function() {{
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', renderMath);
}} else {{
renderMath();
}}
function renderMath() {{
setTimeout(function() {{
if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {{
MathJax.typesetPromise([document.getElementById('root-locus-knowledge')])
.catch((err) => console.log('MathJax渲染错误:', err));
}} else {{
console.log('MathJax未加载,将在500ms后重试');
setTimeout(renderMath, 500);
}}
}}, 300);
}}
}})();
</script>
""")
return ui_dict
def create_chatbot_tab():
"""创建AI问答选项卡的UI组件"""
ui_dict = {}
ui_dict["chatbot"] = gr.Chatbot(
label="🎓 自控原理AI助教",
type="messages",
avatar_images=("https://img.icons8.com/fluency/96/user-male-circle.png", "https://img.icons8.com/fluency/96/chatbot.png"),
height=650,
latex_delimiters=[
{"left": "$$", "right": "$$", "display": True},
{"left": "$", "right": "$", "display": False},
{"left": "\\[", "right": "\\]", "display": True},
{"left": "\\(", "right": "\\)", "display": False}
],
elem_classes="modern-chatbot",
show_copy_button=True
)
with gr.Row():
ui_dict["chat_input"] = gr.Textbox(label="", placeholder="💬 输入您的问题...", scale=4, lines=2, max_lines=4)
with gr.Column(scale=1, min_width=120):
ui_dict["send_button"] = gr.Button("📤 发送", variant="primary", size="lg")
ui_dict["clear_button"] = gr.Button("🗑️ 清空", variant="secondary", size="lg")
gr.Examples(
examples=["什么是传递函数?", "如何判断系统稳定性?", "解释Bode图的物理意义", "PID控制器各参数的作用"],
inputs=ui_dict["chat_input"],
label="💡 试试这些问题:"
)
return ui_dict
+108
View File
@@ -0,0 +1,108 @@
import json
import os
import time
from threading import Lock
import atexit
# --- 配置 ---
TIMEOUT_SECONDS = 300 # 5分钟无活动视为离线
STATS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data_usage")
STATS_FILE = os.path.join(STATS_DIR, "usage_stats.json")
SAVE_INTERVAL_SECONDS = int(os.environ.get("USAGE_SAVE_INTERVAL", "60"))
# --- 全局变量 ---
active_users = {} # {session_id: last_active_timestamp}
users_lock = Lock()
stats_lock = Lock()
total_users = 0
seen_sessions = set()
_LAST_SAVE_TS = 0.0
# --- 内部函数 ---
def _ensure_stats_dir():
try:
os.makedirs(STATS_DIR, exist_ok=True)
except OSError:
pass
def _load_usage_stats():
global total_users, _LAST_SAVE_TS
try:
if os.path.exists(STATS_FILE):
with open(STATS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
total_users = int(data.get("total_users", 0))
_LAST_SAVE_TS = float(data.get("last_saved_at", time.time()))
except (IOError, json.JSONDecodeError):
total_users = 0
_LAST_SAVE_TS = time.time()
def _save_usage_stats():
global _LAST_SAVE_TS
try:
with stats_lock:
tmp_path = STATS_FILE + ".tmp"
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump({"total_users": total_users, "last_saved_at": time.time()}, f, ensure_ascii=False, indent=2)
os.replace(tmp_path, STATS_FILE)
_LAST_SAVE_TS = time.time()
except (IOError, OSError):
pass
def _maybe_save_usage_stats():
if time.time() - _LAST_SAVE_TS >= SAVE_INTERVAL_SECONDS:
_save_usage_stats()
# --- 外部接口 ---
def update_user_activity(session_id: str):
"""更新用户活跃时间并在首次出现时累计总人数。"""
current_time = time.time()
with users_lock:
active_users[session_id] = current_time
with stats_lock:
global total_users
if session_id not in seen_sessions:
seen_sessions.add(session_id)
total_users += 1
def get_active_users_count():
"""获取当前活跃用户数量"""
current_time = time.time()
with users_lock:
expired_users = [uid for uid, last_time in active_users.items() if current_time - last_time > TIMEOUT_SECONDS]
for uid in expired_users:
del active_users[uid]
return len(active_users)
def get_online_status_html():
"""生成在线人数与总人数的显示HTML,并按需触发持久化。"""
count = get_active_users_count()
with stats_lock:
total = total_users
_maybe_save_usage_stats()
return f"""
<div style='display: inline-flex; align-items: center; gap: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 8px 16px; border-radius: 20px;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);'>
<span style='font-size: 1.2em;'>🌐</span>
<span style='color: white; font-weight: 600; font-size: 1em;'>在线人数:</span>
<span style='color: #fff; font-weight: 700; font-size: 1.2em;
background: rgba(255,255,255,0.2); padding: 2px 12px;
border-radius: 12px; min-width: 30px; text-align: center;'>{count}</span>
<span style='opacity: 0.4; color: #fff;'>&nbsp;|&nbsp;</span>
<span style='font-size: 1.2em;'>👥</span>
<span style='color: white; font-weight: 600; font-size: 1em;'>总人数:</span>
<span style='color: #fff; font-weight: 700; font-size: 1.2em;
background: rgba(255,255,255,0.2); padding: 2px 12px;
border-radius: 12px; min-width: 30px; text-align: center;'>{total}</span>
</div>
"""
# --- 初始化 ---
_ensure_stats_dir()
_load_usage_stats()
atexit.register(_save_usage_stats) # 确保程序退出时保存数据