更新项目格式及部分ui细节
This commit is contained in:
@@ -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
|
||||
@@ -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;'>ζ < 0:</strong> 不稳定系统</li>
|
||||
<li><strong style='color: #ffa726;'>ζ = 0:</strong> 无阻尼振荡</li>
|
||||
<li><strong style='color: #4caf50;'>0 < ζ < 1:</strong> 欠阻尼(有振荡)⭐ 最常见</li>
|
||||
<li><strong style='color: #2196f3;'>ζ = 1:</strong> 临界阻尼(无超调)</li>
|
||||
<li><strong style='color: #9c27b0;'>ζ > 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>
|
||||
(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>
|
||||
(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 > 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 < 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 > 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 < 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> (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>) < 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>) > 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>
|
||||
"""
|
||||
@@ -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
@@ -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
|
||||
@@ -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的公开分享链接
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"total_users": 1,
|
||||
"last_saved_at": 1760886050.8138287
|
||||
"total_users": 15,
|
||||
"last_saved_at": 1760890996.5595384
|
||||
}
|
||||
@@ -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
@@ -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;'> | </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) # 确保程序退出时保存数据
|
||||
Reference in New Issue
Block a user