""" ================================================================================ 自动控制原理AI+数智平台 (Automatic Control Theory AI+ Platform) ================================================================================ 版权声明 (Copyright Notice): 本项目由"西北工业大学2025年校级本科生建设项目"资助 Funded by Northwestern Polytechnical University 2025 Undergraduate Construction Project 项目信息 (Project Information): 课程名称:《自动控制理论》 Course: Automatic Control Theory 负责人:魏鹏飞 Supervisor: Wei Pengfei 联系方式:pengfeiwei@nwpu.edu.cn Email: pengfeiwei@nwpu.edu.cn 机构:西北工业大学 Institution: Northwestern Polytechnical University (NWPU) 功能简介 (Features): - 时域分析:阶跃响应、脉冲响应、性能指标计算 - 频域分析:Bode图、Nyquist图、稳定裕度分析 - 根轨迹分析:动态轨迹绘制、增益调节、极点跟踪 - AI智能问答:基于DeepSeek/Gemini的专业教学助手 最后更新 (Last Updated): 2025-10-16 版本 (Version): 1.0.0 ================================================================================ """ import gradio as gr import numpy as np import control as ct import matplotlib.pyplot as plt import re # ==================== API 配置 ==================== # 将 API 相关配置集中在此处,方便修改 # # DeepSeek API 配置说明: # 1. API_KEY: 您的 DeepSeek API 密钥 # - 从 https://platform.deepseek.com/api_keys 获取 # - 或使用环境变量: API_KEY = os.environ.get("DEEPSEEK_API_KEY", "") # # 2. API_BASE_URL: API 服务的基础 URL # - 官方地址: https://api.deepseek.com/v1 # - DeepSeek API 兼容 OpenAI 格式 # # 3. API_MODEL: 使用的 DeepSeek 模型名称 # - deepseek-chat (推荐,性能强大) # - deepseek-coder (代码专用) # - 其他可用模型请参考官方文档 API_KEY = "sk-2292af2428d7419897ca1fb6e99ba6bc" # 请在此处填入您的 DeepSeek API 密钥 API_BASE_URL = "https://api.deepseek.com/v1" # API 基础 URL API_MODEL = "deepseek-chat" # 使用的模型名称 API_TYPE = "deepseek" # API 类型: "deepseek" 或 "gemini" # ================================================== import json import os # --- [FIXED] 辅助函数:将系数数组转换为LaTeX数学公式字符串 --- def coeffs_to_latex(coeffs, var='s'): """ 一个更健壮的函数,用于将系数数组转换为美观的LaTeX字符串。 """ coeffs = np.atleast_1d(coeffs) # 如果只有一个系数(常数),直接返回 if coeffs.size == 1: return f"{coeffs[0]:g}" latex_str = "" degree = len(coeffs) - 1 for i, coeff in enumerate(coeffs): # 跳过系数为0的项 if np.isclose(coeff, 0): continue # 确定符号 sign = "" if i > 0 and latex_str: if coeff > 0: sign = " + " else: sign = " - " elif coeff < 0: sign = "-" # 处理绝对值 coeff_abs = abs(coeff) # 处理系数的显示 coeff_str = "" # 仅当系数不为1或为常数项时显示系数 if not np.isclose(coeff_abs, 1) or degree - i == 0: coeff_str = f"{coeff_abs:g}" # 处理变量和幂 power = degree - i power_str = "" if power > 0: power_str = var if power > 1: power_str += f"^{{{power}}}" latex_str += f"{sign}{coeff_str}{power_str}" return latex_str if latex_str else "0" # --- 功能函数1:显示传递函数 --- def display_transfer_function(num_str, den_str): try: num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str) den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str) num_coeffs = np.array([float(n) for n in num_str_cleaned.split(',') if n]) den_coeffs = np.array([float(d) for d in den_str_cleaned.split(',') if d]) if num_coeffs.size == 0 or den_coeffs.size == 0: return "分子或分母不能为空" num_latex = coeffs_to_latex(num_coeffs) den_latex = coeffs_to_latex(den_coeffs) tf_latex = f"$$ G(s) = \\frac{{{num_latex}}}{{{den_latex}}} $$" return tf_latex except Exception as e: return f"输入格式错误: {e}" # --- 功能函数2:执行时域分析和绘图 --- def time_domain_analysis(num_str, den_str): try: num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str) den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str) num = np.array([float(n) for n in num_str_cleaned.split(',') if n]) den = np.array([float(d) for d in den_str_cleaned.split(',') if d]) if num.size == 0 or den.size == 0: return None, "错误:分子或分母系数不能为空。" system = ct.TransferFunction(num, den) t = np.linspace(0, 15, 1000) T_step, yout_step = ct.step_response(system, T=t) T_impulse, yout_impulse = ct.impulse_response(system, T=t) try: info = ct.step_info(system) if isinstance(info, dict): metrics_text = ( f"Rise Time: {info.get('RiseTime', float('nan')):.2f} s\n" f"Peak Time: {info.get('PeakTime', float('inf')):.2f} s\n" f"Peak: {info.get('Peak', float('inf')):.2f}\n" f"Overshoot: {info.get('Overshoot', float('nan')):.1f} %\n" f"Settling Time: {info.get('SettlingTime', float('nan')):.2f} s\n" f"Steady State Value: {info.get('SteadyStateValue', float('nan')):.2f}" ) else: metrics_text = "系统性能指标计算失败。" except RuntimeError: metrics_text = "系统可能不稳定,无法计算阶跃响应指标。" except Exception: metrics_text = "无法计算所有性能指标。" fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) ax1.plot(T_step, yout_step); ax1.set_title("Unit Step Response"); ax1.set_xlabel("Time (s)"); ax1.set_ylabel("Amplitude"); ax1.grid(True) ax2.plot(T_impulse, yout_impulse); ax2.set_title("Unit Impulse Response"); ax2.set_xlabel("Time (s)"); ax2.set_ylabel("Amplitude"); ax2.grid(True) plt.tight_layout() return fig, metrics_text except Exception as e: return None, f"错误: {e}\n请检查您的输入。" # --- 功能函数3:执行频域分析和绘图 --- def frequency_domain_analysis(num_str, den_str, k): try: num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str) den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str) num_coeffs = np.array([float(n) for n in num_str_cleaned.split(',') if n]) den_coeffs = np.array([float(d) for d in den_str_cleaned.split(',') if d]) if num_coeffs.size == 0 or den_coeffs.size == 0: return None, "错误:分子或分母系数不能为空。", "", "" system_with_gain = ct.TransferFunction(k * num_coeffs, den_coeffs) num_latex = coeffs_to_latex(num_coeffs) den_latex = coeffs_to_latex(den_coeffs) tf_latex_with_gain = f"$$ G_{{open}}(s) = K \\times \\frac{{{num_latex}}}{{{den_latex}}} \\quad \\text{{where }} K = {k:.2f} $$" fig = plt.figure(figsize=(12, 6)); gs = fig.add_gridspec(2, 2) ax_mag = fig.add_subplot(gs[0, 0]); ax_phase = fig.add_subplot(gs[1, 0], sharex=ax_mag) omega_range = np.logspace(-2, 3, 1000) mag, phase, omega = ct.frequency_response(system_with_gain, omega_range) ax_mag.semilogx(omega, 20 * np.log10(mag)); ax_mag.grid(True, which='both'); ax_mag.set_ylabel("Magnitude (dB)"); ax_mag.set_title("Bode Plot") ax_phase.semilogx(omega, np.rad2deg(phase)); ax_phase.grid(True, which='both'); ax_phase.set_ylabel("Phase (deg)"); ax_phase.set_xlabel("Frequency (rad/s)"); ax_phase.axhline(y=-180, color='r', linestyle='--', linewidth=0.8) ax_nyquist = fig.add_subplot(gs[:, 1]) ct.nyquist_plot(system_with_gain, ax=ax_nyquist); ax_nyquist.set_title("Nyquist Plot"); ax_nyquist.grid(True) plt.tight_layout() try: gm, pm, _, _ = ct.margin(system_with_gain) gm_db = 20 * np.log10(gm) if gm > 0 and np.isfinite(gm) else float('inf') is_stable = gm_db > 0 and pm > 0 stability_text = f"**Evaluation**: **{'System Stable' if is_stable else 'System Unstable'}**" metrics_text = "" if np.isinf(gm_db): metrics_text += f"Gain Margin (GM): inf dB\n(Note: Phase never crosses -180° line)\n" else: metrics_text += f"Gain Margin (GM): {gm_db:.2f} dB\n" if np.isinf(pm): metrics_text += f"Phase Margin (PM): not defined" else: metrics_text += f"Phase Margin (PM): {pm:.2f} deg" except Exception as e: metrics_text = f"Unable to compute stability margins."; stability_text = "**Evaluation**: **Cannot determine**" return fig, metrics_text, tf_latex_with_gain, stability_text except Exception as e: return None, f"错误: {e}", "", "" # --- 功能函数4:执行根轨迹分析 --- def root_locus_analysis(num_str, den_str, log_k): try: k = 10**log_k num_str_cleaned = re.sub(r'[^0-9,\-.]', '', num_str) den_str_cleaned = re.sub(r'[^0-9,\-.]', '', den_str) num_coeffs = np.array([float(n) for n in num_str_cleaned.split(',') if n]) den_coeffs = np.array([float(d) for d in den_str_cleaned.split(',') if d]) if num_coeffs.size == 0 or den_coeffs.size == 0: return None, "错误:分子或分母系数不能为空。", k open_loop_system = ct.TransferFunction(num_coeffs, den_coeffs) fig, ax = plt.subplots(figsize=(8, 6)) closed_loop_system = ct.feedback(k * open_loop_system, 1) current_poles = ct.poles(closed_loop_system) ol_poles = open_loop_system.poles(); ol_zeros = open_loop_system.zeros() points_of_interest = np.concatenate(([0j], ol_poles, ol_zeros, current_poles)) min_real = np.min(np.real(points_of_interest)); max_real = np.max(np.real(points_of_interest)) min_imag = np.min(np.imag(points_of_interest)); max_imag = np.max(np.imag(points_of_interest)) center_real = (max_real + min_real) / 2; span_real = max(abs(max_real - min_real), 2) * 1.5 center_imag = (max_imag + min_imag) / 2; span_imag = max(abs(max_imag - min_imag), 2) * 1.5 max_span = max(span_real, span_imag) rlist, klist = ct.root_locus(open_loop_system, plot=False, grid=False) for i in range(rlist.shape[1]): ax.plot(np.real(rlist[:, i]), np.imag(rlist[:, i]), 'b-') ax.set_xlim(center_real - max_span / 2, center_real + max_span / 2); ax.set_ylim(center_imag - max_span / 2, center_imag + max_span / 2) ax.plot(np.real(current_poles), np.imag(current_poles), 'rx', markersize=10, markeredgewidth=2, label=f'Poles at K={k:.2f}') ax.set_xlabel("Real Axis"); ax.set_ylabel("Imaginary Axis"); ax.set_title("Root Locus"); ax.grid(True); ax.legend(loc='upper right'); ax.set_aspect('equal', adjustable='box') poles_text = "Closed-Loop Poles:\n" for p in current_poles: poles_text += f"{p.real:.3f} {'+' if p.imag >= 0 else '-'} {abs(p.imag):.3f}j\n" return fig, poles_text, k except Exception as e: return None, f"错误: {e}", 10**log_k # --- [新增] 功能函数5: AI 智能问答 (支持 DeepSeek 和 Gemini) --- # 注意: API 配置已移至文件开头的配置区域,方便统一管理和修改 # 如果您在本地运行并设置了环境变量,可以在配置区域使用: # API_KEY = os.environ.get("DEEPSEEK_API_KEY", "") # 异步函数以处理流式响应 async def chat_with_ai(message, history): """ 与 AI 模型进行流式对话。支持 DeepSeek 和 Gemini API。 """ # 系统指令,设定AI的角色和回答风格 system_prompt = "你是一位精通自动控制原理的专家教授。请用清晰、准确、专业的中文来回答有关自动控制课程内容的问题。在适当的时候,可以使用公式和示例来辅助解释。" # 检查 API_KEY 是否配置 if not API_KEY or API_KEY.strip() == "": history.append([message, "❌ 错误:API_KEY 未配置。请在文件开头配置 API_KEY。"]) yield history return # 初始化机器人回复 bot_response = "" history.append([message, "正在思考..."]) yield history # 立即显示用户消息 try: import aiohttp if API_TYPE == "deepseek": # DeepSeek API (OpenAI 兼容格式) api_url = f"{API_BASE_URL}/chat/completions" # 构造消息历史 messages = [{"role": "system", "content": system_prompt}] for user_msg, bot_msg in history[:-1]: # 排除最后一条(刚添加的) if user_msg: messages.append({"role": "user", "content": user_msg}) if bot_msg: messages.append({"role": "assistant", "content": bot_msg}) messages.append({"role": "user", "content": message}) payload = { "model": API_MODEL, "messages": messages, "stream": True, "temperature": 0.7, "max_tokens": 2048 } headers = { "Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}" } async with aiohttp.ClientSession() as session: async with session.post(api_url, json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=60)) as response: if response.status == 200: # 处理流式响应 async for line in response.content: line = line.decode('utf-8').strip() if not line or line == "data: [DONE]": continue if line.startswith("data: "): line = line[6:] try: data = json.loads(line) if "choices" in data and len(data["choices"]) > 0: delta = data["choices"][0].get("delta", {}) content = delta.get("content", "") if content: bot_response += content history[-1][1] = bot_response yield history except json.JSONDecodeError: pass if not bot_response: history[-1][1] = "⚠️ API 返回了空响应,请稍后重试。" yield history else: error_text = await response.text() history[-1][1] = f"❌ API请求出错 (状态码: {response.status}):\n{error_text}" yield history else: # Gemini API api_url = f"{API_BASE_URL}/models/{API_MODEL}:streamGenerateContent?key={API_KEY}" # 构造 Gemini 格式的消息历史 api_history = [] for user_msg, bot_msg in history[:-1]: if user_msg: api_history.append({"role": "user", "parts": [{"text": user_msg}]}) if bot_msg: api_history.append({"role": "model", "parts": [{"text": bot_msg}]}) payload = { "contents": api_history + [{"role": "user", "parts": [{"text": message}]}], "systemInstruction": {"parts": [{"text": system_prompt}]}, "generationConfig": { "temperature": 0.7, "topK": 1, "topP": 1, "maxOutputTokens": 2048, } } async with aiohttp.ClientSession() as session: async with session.post(api_url, json=payload, headers={'Content-Type': 'application/json'}, timeout=aiohttp.ClientTimeout(total=60)) as response: if response.status == 200: has_content = False async for chunk in response.content.iter_any(): chunk_text = chunk.decode('utf-8') for line in chunk_text.split('\n'): if not line.strip(): continue if line.startswith('data: '): line = line[6:] try: data = json.loads(line) if "candidates" in data and len(data["candidates"]) > 0: candidate = data["candidates"][0] if "content" in candidate and "parts" in candidate["content"]: text_part = candidate["content"]["parts"][0].get("text", "") if text_part: has_content = True bot_response += text_part history[-1][1] = bot_response yield history except json.JSONDecodeError: pass if not has_content: history[-1][1] = "⚠️ API 返回了空响应,请稍后重试。" yield history else: error_text = await response.text() history[-1][1] = f"❌ API请求出错 (状态码: {response.status}):\n{error_text}" yield history except aiohttp.ClientError as e: history[-1][1] = f"❌ 网络连接错误: {e}\n请检查网络连接或 API_BASE_URL 配置。" yield history except Exception as e: history[-1][1] = f"❌ 发生错误: {type(e).__name__}: {e}" yield history # --- Gradio 界面定义 --- # 自定义 CSS 样式 - 全面现代化升级 custom_css = """ /* ==================== 全局样式 ==================== */ * { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .gradio-container { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important; background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%) !important; } /* ==================== Emoji 显示修复 ==================== */ /* 确保所有 emoji 正常显示,不被背景色覆盖 */ * { font-feature-settings: "liga" 1, "calt" 1, "kern" 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* 针对 Gradio Markdown 组件的 emoji 修复 */ .gr-markdown code { background: transparent !important; padding: 0 !important; font-family: inherit !important; } /* 确保 HTML 内容中的 emoji 不受影响 */ .gr-html * { background-clip: border-box !important; -webkit-background-clip: border-box !important; -webkit-text-fill-color: initial !important; } /* ==================== 标题区域 ==================== */ .main-title { text-align: center; font-size: 3em !important; font-weight: 900 !important; margin-bottom: 0.3em; letter-spacing: -1px; animation: titleGlow 3s ease-in-out infinite; color: #333; } /* 标题渐变效果(不影响emoji) */ h1.main-title { background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } @keyframes titleGlow { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.2); } } .subtitle { text-align: center; color: #555; font-size: 1.2em; font-weight: 500; margin-bottom: 1.5em; letter-spacing: 0.5px; } /* 确保 subtitle 中的 emoji 正常显示 */ p.subtitle { color: #555 !important; } /* ==================== 项目信息横幅 ==================== */ .project-info-banner { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 20px; border-radius: 12px; margin-bottom: 25px; box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3); animation: bannerSlide 0.6s ease-out; } @keyframes bannerSlide { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } /* ==================== 标签页样式 ==================== */ .tab-nav { background: white; border-radius: 12px; padding: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); margin-bottom: 20px; } .tab-nav button { font-weight: 600 !important; font-size: 1.1em !important; padding: 12px 24px !important; border-radius: 8px !important; border: none !important; transition: all 0.3s ease !important; } .tab-nav button:hover { background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%) !important; transform: translateY(-2px) !important; } .tab-nav button.selected { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: white !important; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important; } /* ==================== 卡片和分组样式 ==================== */ .gr-group { background: white !important; border-radius: 16px !important; padding: 24px !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08) !important; border: 1px solid rgba(102, 126, 234, 0.1) !important; margin-bottom: 20px !important; transition: transform 0.3s ease, box-shadow 0.3s ease !important; } .gr-group:hover { transform: translateY(-4px) !important; box-shadow: 0 12px 48px rgba(102, 126, 234, 0.15) !important; } /* 卡片标题 */ .card-title { font-size: 1.4em; font-weight: 700; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 15px; border-bottom: 3px solid; border-image: linear-gradient(90deg, #667eea, #764ba2) 1; padding-bottom: 10px; display: flex; align-items: center; gap: 8px; } /* 修复 emoji 显示问题 - emoji 不应用渐变色 */ .card-title::before { content: ''; display: inline-block; } /* 确保 HTML 内的 emoji 正常显示 */ div.card-title { background: transparent !important; -webkit-background-clip: initial !important; -webkit-text-fill-color: initial !important; background-clip: initial !important; color: #333 !important; } /* 只对文字部分应用渐变(如果需要) */ div.card-title span { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } /* ==================== 输入框样式 ==================== */ .gr-textbox input, .gr-textbox textarea { border: 2px solid #e0e7ff !important; border-radius: 10px !important; padding: 12px 16px !important; font-size: 1em !important; transition: all 0.3s ease !important; background: #fafbff !important; } .gr-textbox input:focus, .gr-textbox textarea:focus { border-color: #667eea !important; box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1) !important; background: white !important; transform: translateY(-2px); } .gr-textbox label { font-weight: 600 !important; color: #4a5568 !important; margin-bottom: 8px !important; } /* ==================== 按钮样式 ==================== */ button { border-radius: 10px !important; font-weight: 600 !important; padding: 12px 24px !important; font-size: 1em !important; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; border: none !important; cursor: pointer !important; } .primary-btn, button[variant="primary"] { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: white !important; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important; } .primary-btn:hover, button[variant="primary"]:hover { transform: translateY(-3px) scale(1.02) !important; box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5) !important; } .primary-btn:active, button[variant="primary"]:active { transform: translateY(-1px) scale(0.98) !important; } button[variant="secondary"] { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%) !important; color: white !important; box-shadow: 0 4px 15px rgba(240, 147, 251, 0.4) !important; } button[variant="secondary"]:hover { transform: translateY(-3px) scale(1.02) !important; box-shadow: 0 8px 25px rgba(240, 147, 251, 0.5) !important; } /* ==================== 滑块样式 ==================== */ .gr-slider { padding: 20px 10px !important; } .gr-slider input[type="range"] { height: 8px !important; border-radius: 4px !important; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%) !important; } .gr-slider input[type="range"]::-webkit-slider-thumb { width: 20px !important; height: 20px !important; background: white !important; border: 3px solid #667eea !important; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important; cursor: pointer !important; } .gr-slider input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2) !important; box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6) !important; } /* ==================== 图表容器 ==================== */ .plot-container { border-radius: 16px !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08) !important; overflow: hidden !important; background: white !important; padding: 10px !important; border: 1px solid rgba(102, 126, 234, 0.1) !important; } .gr-plot { border-radius: 12px !important; } /* ==================== 知识卡片 ==================== */ .knowledge-card { background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%); border-radius: 16px; padding: 24px; margin-top: 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); max-height: 550px; overflow-y: auto; border: 1px solid rgba(102, 126, 234, 0.15); animation: cardFadeIn 0.6s ease-out; } @keyframes cardFadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .knowledge-card::-webkit-scrollbar { width: 10px; } .knowledge-card::-webkit-scrollbar-track { background: #f1f3f9; border-radius: 10px; } .knowledge-card::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; border: 2px solid #f1f3f9; } .knowledge-card::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, #5568d3 0%, #65408b 100%); } .knowledge-card h3 { color: #667eea; font-size: 1.5em; font-weight: 700; margin-top: 0; margin-bottom: 20px; padding-bottom: 12px; border-bottom: 3px solid; border-image: linear-gradient(90deg, #667eea, #764ba2) 1; } /* 知识卡片标题中的 emoji 保持彩色 */ .knowledge-card h3::first-letter { color: inherit; } .knowledge-card h4 { color: #4a5568; font-size: 1.2em; font-weight: 600; margin-top: 20px; margin-bottom: 12px; } .knowledge-card details { margin: 15px 0; border-radius: 8px; overflow: hidden; } .knowledge-card details summary { cursor: pointer; padding: 12px 16px; font-weight: 700; border-radius: 8px; transition: all 0.3s ease; user-select: none; color: #333; font-size: 1.05em; } .knowledge-card details summary:hover { transform: translateX(5px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); filter: brightness(0.98); } .knowledge-card details[open] summary { margin-bottom: 10px; border-radius: 8px 8px 0 0; } .knowledge-card table { border-collapse: collapse; width: 100%; margin: 15px 0; font-size: 0.95em; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); border-radius: 8px; overflow: hidden; } .knowledge-card table th { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px; font-weight: 700; } .knowledge-card table td { padding: 10px 12px; border: 1px solid #e0e7ff; } .knowledge-card table tr:hover { background: #f8f9ff; transition: background 0.2s ease; } /* ==================== 聊天机器人样式 ==================== */ .chatbot { border-radius: 16px !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08) !important; border: 1px solid rgba(102, 126, 234, 0.1) !important; } /* 用户消息气泡 - 紫色渐变 + 白色文字 */ .message.user { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: white !important; border-radius: 18px 18px 4px 18px !important; padding: 12px 18px !important; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3) !important; } /* 确保用户消息内的所有文字都是白色 */ .message.user * { color: white !important; } /* 机器人消息气泡 - 白色背景 + 深色文字 */ .message.bot { background: white !important; border: 1px solid #e0e7ff !important; border-radius: 18px 18px 18px 4px !important; padding: 12px 18px !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important; color: #2d3748 !important; } /* 确保机器人消息内的文字清晰可见 */ .message.bot * { color: #2d3748 !important; } .message.bot p { color: #2d3748 !important; line-height: 1.6 !important; margin: 8px 0 !important; } .message.bot strong { color: #1a202c !important; font-weight: 700 !important; } .message.bot code { background: #f7fafc !important; color: #667eea !important; padding: 2px 6px !important; border-radius: 4px !important; font-family: 'Consolas', 'Monaco', monospace !important; } .message.bot pre { background: #f7fafc !important; border: 1px solid #e2e8f0 !important; border-radius: 6px !important; padding: 12px !important; overflow-x: auto !important; } .message.bot pre code { background: transparent !important; padding: 0 !important; } /* ==================== 性能指标文本框 ==================== */ .gr-textbox.output-metrics { font-family: 'Consolas', 'Monaco', monospace !important; background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%) !important; border: 2px solid #e0e7ff !important; border-radius: 12px !important; padding: 16px !important; font-size: 0.95em !important; line-height: 1.8 !important; } /* ==================== 数字显示框 ==================== */ .gr-number, .gain-display { font-size: 1.3em !important; font-weight: 700 !important; color: #667eea !important; text-align: center !important; background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%) !important; border: 2px solid #667eea !important; border-radius: 12px !important; padding: 16px !important; } .gain-display input { text-align: center !important; font-size: 1.5em !important; font-weight: 800 !important; color: #667eea !important; } /* ==================== 输出显示框 ==================== */ .output-display { background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%) !important; padding: 20px !important; border-radius: 12px !important; border: 2px solid #e0e7ff !important; min-height: 80px !important; } .stability-result { padding: 15px !important; border-radius: 10px !important; text-align: center !important; font-size: 1.2em !important; font-weight: 700 !important; margin-top: 10px !important; } /* ==================== 现代聊天机器人样式 ==================== */ .modern-chatbot { background: white !important; border-radius: 16px !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08) !important; } /* ==================== Markdown 样式增强 ==================== */ .gr-markdown { line-height: 1.8 !important; } .gr-markdown h1, .gr-markdown h2, .gr-markdown h3 { font-weight: 700 !important; margin-top: 1.5em !important; margin-bottom: 0.8em !important; } .gr-markdown code { background: #f0f4ff !important; padding: 2px 8px !important; border-radius: 4px !important; font-family: 'Consolas', 'Monaco', monospace !important; color: #667eea !important; border: 1px solid #e0e7ff !important; } .gr-markdown pre { background: #f8f9ff !important; border: 2px solid #e0e7ff !important; border-radius: 8px !important; padding: 16px !important; } /* ==================== 加载动画 ==================== */ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } } .loading { animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; } /* ==================== 响应式设计 ==================== */ @media (max-width: 768px) { .main-title { font-size: 2em !important; } .subtitle { font-size: 1em !important; } .knowledge-card { padding: 16px; max-height: 400px; } } /* ==================== 暗色主题支持 ==================== */ @media (prefers-color-scheme: dark) { .gradio-container { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%) !important; } .gr-group { background: #0f3460 !important; border-color: rgba(102, 126, 234, 0.3) !important; } .knowledge-card { background: linear-gradient(135deg, #0f3460 0%, #1a1a2e 100%); border-color: rgba(102, 126, 234, 0.3); } .knowledge-card h3, .knowledge-card h4 { color: #a0aec0; } } /* ==================== 特殊效果 ==================== */ .shimmer { background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.3) 50%, rgba(255,255,255,0) 100%); animation: shimmer 2s infinite; } @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } """ with gr.Blocks(title="自动控制原理学习网站 - AI+数智平台", css=custom_css) as demo: # 主标题 - 带动画效果 gr.HTML("
✨ 交互式控制系统分析与设计工具 | 时域·频域·根轨迹·AI问答 ✨
") # 项目信息横幅 - 优化对比度和可读性 gr.HTML(""" """) with gr.Tabs() as tabs: with gr.TabItem("⏱️ 时域分析 (Time Domain)", id=0): gr.HTML("""对于一个典型的二阶系统,其标准传递函数形式为:
G(s) = ωn² / (s² + 2ζωns + ωn²)
响应从终值的10%上升到90%所需的时间
📐 近似公式:tr ≈ 1.8 / ωn
响应达到第一个峰值所需的时间
📐 公式:tp = π / (ωn√(1-ζ²)) = π / ωd
其中 ωd = ωn√(1-ζ²) 是阻尼振荡频率
响应超过稳态值的最大百分比
📐 公式:σ% = e(-πζ/√(1-ζ²)) × 100%
仅与阻尼比 ζ 有关
💡 常见值:
• ζ = 0.5 时,σ% ≈ 16%
• ζ = 0.707 时,σ% ≈ 4.3%
响应达到并保持在稳态值 ±2%(或±5%)范围内所需的时间
📐 公式:
• 2%误差带:ts ≈ 4 / (ζωn)
• 5%误差带:ts ≈ 3 / (ζωn)
主要由 ζωn 决定(系统时间常数)
不同输入下的稳态误差:
• 单位阶跃输入:ess = 1/(1+Kp)
• 单位斜坡输入:ess = 1/Kv
• 单位抛物线输入:ess = 1/Ka
其中 Kp, Kv, Ka 分别为位置、速度、加速度误差常数
标准传递函数:
G(s) = K / (τs + 1)
参数说明:
阶跃响应:
y(t) = K(1 - e-t/τ)
💡 关键时间点:
特点:一阶系统无超调,响应单调上升
系统类型分类:
根据开环传递函数原点处的极点数(积分环节数)v 分类:
| 类型 | Kp | Kv | Ka |
|---|---|---|---|
| 0 型 | 有限值 | 0 | 0 |
| I 型 | ∞ | 有限值 | 0 |
| II 型 | ∞ | ∞ | 有限值 |
稳态误差计算:
⚡ 工程结论:
系统型别越高,跟踪能力越强,但稳定性可能降低。实际系统常用 I 型或 II 型。
主导极点概念:
距虚轴最近的极点对系统动态响应起主导作用,其他极点影响较小。
判断条件:
零点的影响:
🔺 左半平面零点(最小相位系统):
🔻 右半平面零点(非最小相位系统):
频域分析通过研究系统对不同频率正弦信号的响应特性来评估系统性能。
定义: 在相角为-180°时,系统增益可以增加的最大倍数(或dB数),而不会使系统变得不稳定。
GMdB = -20 log10 |G(jωpc)|
ωpc (相角交越频率):系统相角等于-180°时的频率
| GM > 0 dB | ✅ 系统稳定 |
| GM = 0 dB | ⚠️ 临界稳定 |
| GM < 0 dB | ❌ 系统不稳定 |
💡 工程要求: 通常要求 GM ≥ 6 dB (约2倍),提供对增益变化的鲁棒性
定义: 在增益为1(0dB)时,系统相角与-180°之间的差值。
PM = 180° + ∠G(jωgc)
ωgc (增益交越频率):系统幅值等于1(0dB)时的频率
| PM > 0° | ✅ 系统稳定 |
| PM = 0° | ⚠️ 临界稳定 |
| PM < 0° | ❌ 系统不稳定 |
💡 工程要求: 通常要求 PM ≥ 30° ~ 60°
• PM ≈ 45° ~ 60° : 良好的阻尼特性
• PM 越大,系统超调量越小
对于二阶系统:ζ ≈ PM/100 (PM以度为单位)
| PM | ζ | 超调量 |
| 30° | ≈ 0.3 | ≈ 37% |
| 45° | ≈ 0.45 | ≈ 20% |
| 60° | ≈ 0.6 | ≈ 10% |
组成:幅频特性图 + 相频特性图
💡 优点:
定义:开环频率特性 G(jω)H(jω) 在复平面上的轨迹图
🎯 奈奎斯特稳定判据:
闭环系统稳定的充要条件:当 ω 从 -∞ 变化到 +∞ 时,Nyquist曲线逆时针包围(-1, j0)点的圈数 N 等于开环系统右半平面极点数 P。
Z = P - N
其中 Z 为闭环系统右半平面极点数
闭环频率响应的幅值下降到-3dB时的频率。BW 越大,系统响应越快。
闭环频率响应的最大幅值。Mr 越小,系统阻尼越好。通常要求 Mr < 1.3 ~ 1.5。
• 幅频:20logK (dB) - 水平直线
• 相频:0° - 水平直线
• 幅频:-20dB/dec 斜率的直线
• 相频:-90° - 水平直线
• 幅频:+20dB/dec 斜率的直线
• 相频:+90° - 水平直线
• 转折频率:ω = 1/T
• 幅频:低频0dB,高频-20dB/dec
• 相频:从0°下降到-90°
• 转折频率:ω = 1/T
• 幅频:低频0dB,高频+20dB/dec
• 相频:从0°上升到+90°
• 转折频率:ω = ωn
• 幅频:低频0dB,高频-40dB/dec
• 相频:从0°下降到-180°
• 谐振峰值与阻尼比 ζ 相关
定义:传递函数的所有零点和极点都在左半s平面的系统。
✅ 特点:
定义:传递函数在右半s平面有零点或极点的系统。
⚠️ 特点:
特点:幅频特性为常数(|G(jω)| = 1),只改变相频特性。
应用:相位校正、延时补偿
定义: 根轨迹是当开环系统增益 K 从 0 变化到 ∞ 时,闭环系统特征方程的根(极点)在 s 平面上描绘出的轨迹。
主要作用:
对于单位负反馈系统,闭环传递函数为:
T(s) = KG(s) / [1 + KG(s)H(s)]
特征方程:
1 + K G(s)H(s) = 0 或 K G(s)H(s) = -1
| 符号 | 含义 |
|---|---|
| G(s) | 前向通道传递函数 |
| H(s) | 反馈通道传递函数(单位反馈时 H(s)=1) |
| K | 可变增益参数 |
| 特征方程的根 | 闭环极点 |
设开环传递函数为:
G(s)H(s) = K(s-z1)(s-z2)···(s-zm) / [(s-p1)(s-p2)···(s-pn)]
根轨迹上的点 s0 必须满足:
1️⃣ 幅值条件(充要条件)
|K G(s0)H(s0)| = 1
💡 物理意义: 确定增益 K 的值,使得 s0 成为闭环极点。
2️⃣ 相角条件(充要条件)
∠G(s0)H(s0) = (2k+1)180°
其中 k = 0, ±1, ±2, ±3, ...
💡 物理意义: 判断 s 平面上某点是否在根轨迹上。
角度计算公式:
∠G(s0)H(s0) = Σ∠(s0-zi) - Σ∠(s0-pj)
1️⃣ 起点和终点
2️⃣ 根轨迹分支数
3️⃣ 实轴上的根轨迹
实轴上某区段,若其右侧的实数开环零点和极点总数为奇数,则该区段在根轨迹上。
4️⃣ 渐近线
当 n > m 时,有 (n-m) 条分支沿渐近线趋向无穷远:
渐近线角度: φa = (2k+1)180° / (n-m)
k = 0, 1, 2, ..., (n-m-1)
渐近线交点(重心): σa = (Σpj - Σzi) / (n-m)
5️⃣ 分离点/会合点
| 区域 | 条件 | 稳定性 |
|---|---|---|
| 左半平面 | Re(s) < 0 | ✅ 稳定 |
| 虚轴 | Re(s) = 0 | ⚠️ 临界稳定 |
| 右半平面 | Re(s) > 0 | ❌ 不稳定 |
稳定性判断准则:
从原点出发的射线代表恒定阻尼比 ζ 的轨迹:
θ = arccos(ζ)
| 阻尼比 ζ | 角度 θ | 系统响应特性 |
|---|---|---|
| 0.5 | 60° | 欠阻尼,有较大超调 |
| 0.707 | 45° | 最佳阻尼,超调适中 |
💡 工程应用:
通过根轨迹与阻尼比等值线的交点,可以选择满足动态性能要求的增益 K 值。
| 规则 | 内容 |
|---|---|
| 起点 | K=0,位于开环极点 |
| 终点 | K→∞,位于开环零点或无穷远 |
| 分支数 | 等于 max(n, m) |
| 实轴段 | 右侧零极点总数为奇数的区段 |
| 渐近线数 | n - m 条 |
| 渐近线角度 | φa = (2k+1)·180°/(n-m) |
| 渐近线交点 | σa = (Σp - Σz)/(n-m) |
| 分离/会合点 | dK/ds = 0 的实根 |
| 与虚轴交点 | 用劳斯判据或令 s=jω 求解 |
| 出射角/入射角 | 复数极点/零点处的根轨迹切线角度 |
📝 绘制步骤:
定义:当参数 K 从 0 变化到 -∞ 时,闭环极点的轨迹。
相角条件:
∠G(s0)H(s0) = 2k·180° (k = 0, ±1, ±2, ...)
⚠️ 与常规根轨迹的区别:
应用场景:
稳定性分析:
✅ 稳定条件:所有闭环极点都在左半 s 平面
动态性能分析:
| 极点位置 | 系统响应特性 |
|---|---|
| 实轴负半轴 | 单调响应,无振荡 |
| 左半平面共轭复数 | 衰减振荡,有超调 |
| 虚轴上 | 等幅振荡,临界稳定 |
| 右半平面 | 发散,不稳定 |
增益 K 的选择:
💡 设计原则:
性能折中:
增大 K 可以提高系统精度和快速性,但可能降低稳定性和增大超调。 需要在根轨迹上选择合适的工作点,平衡各项性能指标。
1️⃣ 系统分析:
2️⃣ 控制器设计:
3️⃣ 极点配置:
通过添加合适的零极点,使闭环极点位于期望位置:
4️⃣ 鲁棒性分析:
根轨迹可以直观显示参数不确定性对系统稳定性的影响, 帮助评估系统的鲁棒性能。