资讯动态

广义奈奎斯特曲线:MIMO系统稳定性分析的原理与Python实现

发布时间:2026/10/21 6:45:05 来源:尧图企业网站定制
简介一份用于多输入多输出MIMO系统广义奈奎斯特曲线绘制的MATLAB工具适合通信工程专业学生、研究人员及工程技术人员。该工具基于开环传递函数矩阵在复平面上绘制广义奈奎斯特曲线用于评估MIMO系统稳定性与频率响应特性可直接应用于无线通信课程设计、毕业设计或科研仿真。压缩包内共7个m脚本文件整体仅5KB代码精简包含符号转换、降阶处理、示例运行等模块模块分工明确便于二次开发与学习。目前已有3162人学习浏览作者经过实际测试并配套原理说明建议结合通信原理教材使用能进一步加深对稳定边界与信道容量关系的理解。通过运行示例读者可快速理解广义奈奎斯特判据的MATLAB实现方法并结合自身需求修改参数、更换信道模型进一步掌握MIMO系统稳定性分析与频域设计技巧是一份兼具理论讲解与代码实践价值的优质资源。1. 广义奈奎斯特曲线到底是什么多输入多输出系统的稳定性不能只看单回路当被控对象是一个两输入两输出的耦合系统时我第一反应还是用传统奈奎斯特判据去校核每个回路的幅值裕度和相位裕度结果闭环支路全部显示稳定连上控制器一运行就发散。后来才明白MIMO系统的稳定性必须看整个传递函数矩阵的特征轨迹也就是本文要讲的广义奈奎斯特曲线。它是一种把单变量奈奎斯特判据推广到多变量系统的图形化工具核心是画出开环传递函数矩阵的特征值随频率变化的轨迹再数一数它们围绕临界点-1j0的圈数用这个圈数和开环右半平面极点数判断闭环稳定性。适合你在做多变量控制系统设计、解耦控制或者模型降阶验证时使用不用去背复杂的行列式函数用几张图就能判断系统是否稳定还能直观看到系统在哪个频率段最接近失稳。各个环节的坑不少本文会把这些年积累的踩坑记录一并讲清楚。2. 广义奈奎斯特理论的数学基础特征轨迹和特征增益不是一回事2.1 从传递函数矩阵到特征函数一个m输入m输出的线性时不变系统开环传递函数矩阵记为L(s)G(s)K(s)其中G(s)是被控对象K(s)是控制器L(s)是m×m的有理传函矩阵。系统的闭环特征方程为det(I_mL(s))0。直接对这个行列式做奈奎斯特围线积分当然可以但m越大行列式越复杂而且物理意义不直观。广义奈奎斯特判据的做法是引入L(s)的特征函数λ_i(s)定义为满足det(λ_i(s)I_m - L(s))0的标量函数。对于每个频率点ω把L(jω)看成常数矩阵求出它的m个复特征值λ_1(jω),...,λ_m(jω)让ω从0到∞扫过去每个特征值就在复平面上画出一条曲线这些曲线就叫特征轨迹也就是广义奈奎斯特曲线。为什么特征轨迹能有判稳作用因为det(I_mL(s)) ∏_{i1}^m (1λ_i(s))。这个等式并不要求L(s)可对角化。利用Schur分解任意方阵可以酉变换为上三角矩阵对角元素就是特征值因此行列式等于对角元素乘积。把IL代入同样成立。于是闭环特征方程等价于∏(1λ_i(s))0也就是说只要某条特征轨迹λ_i(jω)穿过-1j0点就有1λ_i0对应闭环极点在虚轴上。更一般地当s沿奈奎斯特围线走一圈时每个1λ_i(s)的相角变化量对应了λ_i轨迹围绕-1点的圈数。把m条轨迹的圈数加起来再用幅角原理和开环右半平面极点联系起来就得到稳定性判据。特征轨迹的几何意义很直观它就好像是给系统矩阵的“脾性”做了一次主成分分解。在某个频率下矩阵L(jω)的作用相当于对输入向量做缩放和旋转而特征值就是这种作用的固有模式。如果某个特征值轨迹靠近-1点意味着系统在这个频率下存在一种内部模式反馈后刚好把该模式推向振荡边缘。对于耦合系统这种内部模式往往对应着多个回路同时动作的组合效果单看某个回路的幅相曲线完全看不到。要特别注意特征轨迹上的每一个点都是通过把L(jω)作为一个整体矩阵求特征值得到的它反映的是多变量系统中各通道之间的相互作用而不是某个独立回路的开环幅相特性。这也是为什么单独看每个回路的奈奎斯特图会误判的原因通道间的耦合会通过特征值叠加到临界点附近即使每个对角通道都很稳定特征轨迹也可能把-1点包进去。一个经典的现象是两输入两输出系统对角通道分别都有60°相位裕度但特征轨迹离-1点只有0.1的距离稍微调一下参数就振荡。2.2 广义奈奎斯特判据的公式与适用条件设P是开环传递函数矩阵L(s)在右半平面不含虚轴的极点数目计重数N是当ω从-∞到∞扫过时所有特征轨迹围绕临界点(-1j0)的顺时针净圈数总和。那么闭环系统稳定的充分必要条件是N等于P。这里需要注意方向约定如果把频率从0到∞作为主曲线负频率部分的轨迹是正频率轨迹的共轭镜像对实有理系统而言计算净圈数时要把正负两部分都算进去或者采用从0到∞画曲线并首尾相连的做法再按等效圈数判断。如果方向规定为逆时针为正那么判据变成N -P不同教科书可能有不同约定绘图时要在代码注释里写清楚自己用的是哪种约定否则以后自己看也会糊涂。使用这个判据有三个前提。第一L(s)在虚轴上不能有极点如果有积分环节围线要沿着虚轴向右绕一个小半圆避开原点处理方法和单变量奈奎斯特判据完全一致。第二L(s)必须是方阵因为广义奈奎斯特判据基于方阵的特征值分解如果被控对象输入输出数不同需要先把系统方阵化比如通过串联补偿器或选择等量的控制通道。第三特征轨迹的连续追踪是正确的如果因为数值排序导致曲线拼错N就算不对。这一点是后面绘制代码里最容易翻车的地方。从数学推导中可以提炼出一个实用结论特征轨迹对参数变化的敏感性比单个通道幅相特性更强。因为特征值是矩阵元素的非线性函数某个通道的增益变化会同时影响所有特征轨迹。在做多变量系统的稳定裕度分析时与其逐个回路加增益去试不如画出特征轨迹族看临界点周围被包围的情况随参数变化的趋势。这也是广义奈奎斯特曲线在工程里比单纯计算特征值更有价值的原因——它把频率维度也展开了你能看到失稳风险到底集中在哪个频段。表单变量奈奎斯特判据与广义奈奎斯特判据的对比对比项单变量奈奎斯特广义奈奎斯特对象标量传递函数G(jω)方阵传递函数矩阵L(jω)曲线数量1条m条特征轨迹临界点(-1, j0)(-1, j0)包围圈数开环右半平面极点数所有特征轨迹包围圈数总和能体现耦合吗不能能特征值耦合了所有通道绘图复杂度直接用bode或nyquist需要排序、追踪特征值3. 用Python计算并绘制MIMO系统的广义奈奎斯特曲线3.1 建立系统的传递函数矩阵常见做法是用python-control库定义传输函数再配合numpy计算矩阵特征值。这里以一个两输入两输出的模拟耦合系统为例子它的L(s)设计成有明显的通道间交叉耦合以便观察特征轨迹的包络。系统矩阵定义如下import numpy as np import matplotlib.pyplot as plt import control as ctrl # 定义一个2x2的开环传递函数矩阵 L(s) G(s)*K(s) # 这里直接把L(s)写成有理矩阵形式 s ctrl.tf(s) L11 2.0 / ((s 1) * (s 2)) L12 0.5 / (s 3) L21 0.3 / (s 4) L22 1.5 / ((s 1) * (s 5)) L ctrl.tf([[L11, L12], [L21, L22]])这个例子中每个元素都是稳定的实有理传递函数整体L(s)是2×2矩阵。把L(s)直接写成开环传递函数矩阵省去了G和K分离的步骤因为广义奈奎斯特判据只关心乘积。如果我们做的是控制器设计应该先把G和K的传函矩阵乘起来得到L再传给后续函数。需要注意python-control里tf构造2×2矩阵时每个元素必须是TransferFunction对象不能是普通常数如果是比例环节要写成ctrl.tf([k], [1])。有了L后下一步就是选择频率范围。频率范围选得是否合适直接影响曲线是否覆盖了所有临界穿越。一般建议从系统主导极点对应转折频率的1/10扫描到10倍带宽。对于上面的例子极点频率大致是1、2、3、4、5 rad/s所以我一般取ω从0.01到100 rad/s按对数均匀分布2000个点。如果系统里有积分环节低频端要到0.001甚至更低才能看到无穷远处特征轨迹的渐近方向。高频端要超过所有右半平面零点的模值这样特征轨迹才会收束到原点。3.2 数值计算特征轨迹的两种思路第一种思路是直接对每个ω求L(jω)矩阵数值再调用numpy的eigvals求复特征值。这种方法最直接适用于任何能算出频响矩阵的模型。L(jω)的计算可以利用python-control的frequency_response功能也可以手动代入复数j*omega。手动代入的代码更轻量不依赖control库的频响函数但要注意python-control的TransferFunction对象不支持直接按复数求值需要自己构造复数矩阵。第二种思路是先把L(s)化成状态空间实现然后在每个频率点上计算频率响应矩阵。状态空间方法的好处是计算效率高特别是当系统的阶数很高时频响计算可以复用代数运算。python-control的transfer_function对象有内置的frequency_response方法实际上就是利用状态空间实现的。我平时做仿真更推荐直接使用control.frequency_response因为它的接口统一既支持传函矩阵也支持状态空间模型返回的频响数组维度是(m, m, n_freq)。下面的代码展示了两种等价写法# 方法A用control自带频响计算 omega_grid np.logspace(-2, 2, 2000) # freq_response返回三个值频率、频响、幅相信息 freq_vals, F_resp, _ ctrl.frequency_response(L, omega_grid) print(F_resp.shape) # 输出(2, 2, 2000) # 方法B手动在频点上构造复数矩阵适用于非标准模型 def eval_ftf(ftf, omega): # 对于单个传函对象用complex_frequency_response手动计算更通用 # 这里用control内部函数获取复数响应 _, resp, _ ctrl.frequency_response(ftf, [omega]) return resp.reshape(-1)[0] Gjw np.array([[eval_ftf(L[i][j], 1.5) for j in range(2)] for i in range(2)])方法A适合标准模型方法B适合想自己控制每个元素求值逻辑的场景。实际使用中如果L(s)是从其他工具导入的非标准对象或者每个元素是拉普拉斯变量s的多项式比值方法B更稳妥。注意eval_ftf里的frequency_response返回的多维数组要reshape成标量这一步经常被忽略导致矩阵嵌套数组后面求特征值直接报错。3.3 绘制特征轨迹的完整代码与参数说明下面这段代码是完整可运行的它把L(s)转换为频响矩阵在指定频率点上计算特征值并对特征值排序保证曲线连续最后画出所有特征轨迹。def eigenloci(L, w): 计算L(s)的特征轨迹。 参数: L: control.TransferFunction矩阵 w: 频率点序列(rad/s从小到大) 返回: lambdas: 形状为(len(w), m)的复数数组第i行为第i个频率点的m个特征值 m len(L) # 假设方阵 lambdas np.zeros((len(w), m), dtypecomplex) # 构造频率响应矩阵 for idx, omega in enumerate(w): Gjw np.zeros((m, m), dtypecomplex) for r in range(m): for cidx in range(m): # 用Bode求频响取复数值 _, Gval, _ ctrl.frequency_response(L[r][cidx], [omega]) Gjw[r, cidx] Gval.reshape(-1)[0] # 求所有特征值排序由后续函数处理 lambdas[idx, :] np.linalg.eigvals(Gjw) # 对特征值排序保证相邻频率点的特征值对应同一条轨迹 lambdas_sorted np.zeros_like(lambdas) lambdas_sorted[0, :] lambdas[0, :] for k in range(1, len(w)): prev lambdas_sorted[k-1, :] curr lambdas[k, :] # 贪心匹配每个当前特征值匹配到前后最接近的前一个特征值 for j in range(m): dist np.abs(curr - prev) idx_match np.argmin(dist) lambdas_sorted[k, j] curr[idx_match] # 将已匹配的特征值标记为无穷大避免重复使用 curr[idx_match] np.inf # 同时把prev中已经匹配的项也标记防止后续匹配 dist[:, j] np.inf return lambdas_sorted w np.logspace(-2, 2, 2000) lambdas eigenloci(L, w) # 绘制 plt.figure(figsize(8, 6)) for i in range(lambdas.shape[1]): plt.plot(lambdas[:, i].real, lambdas[:, i].imag, labelfλ{i1}) # 标记临界点 plt.plot(-1, 0, rx, markersize12, label(-1, j0)) plt.xlabel(Re(λ)) plt.ylabel(Im(λ)) plt.title(广义奈奎斯特曲线 (特征轨迹)) plt.grid(True, alpha0.3) plt.legend() plt.axis(equal) plt.show()代码里的核心逻辑是先通过frequency_response取每个频率点上矩阵元素的复数响应组装成复数矩阵Gjw然后numpy.linalg.eigvals一次算出该频率点的全部特征值。这里有一个容易忽略的细节frequency_response返回的Gval是一个多维数组即使输入是单频率点也需要用reshape(-1)[0]取出标量。如果不做这一步Gjw会被赋值为数组后面的特征值计算会报错或得到错误维度。参数说明频率点数量2000是个经验值太少会导致曲线拐角处不光滑包围圈数可能漏算太多则计算量大但现代计算机2000点完全可以接受。ω范围0.01到100 rad/s覆盖了矩阵元素所有极点的转折频率。请根据实际系统调整如果系统有积分环节频率下限要更低否则积分部分的相位特性画不全如果系统带宽很高上限要提高。特征值排序代码用了贪心匹配它的基本思想是在相邻频率点之间把距离最近的特征值视为同一条特征轨迹的延续。这个办法简单有效但在两条轨迹距离很接近甚至交叉时会出错第5.1节会详细讲。另外如果矩阵的特征值有重根贪心匹配会很难处理这时候要用基于特征向量的追踪算法或者用轨迹的连续性假设做预测。4. 从频响数据直接计算广义奈奎斯特曲线不依赖解析模型的做法4.1 为什么频响数据路径在工程中更常见拿到一个真实的多变量对象时往往没有干净的解析传函矩阵。比如机床进给系统、电力电子变换器或者化工过程对象要么是仿真模型里已经离散化的状态空间要么是扫频仪实测出来的Bode数据。这时候如果还想画广义奈奎斯特曲线就不能直接调用第3章的解析模型代码而要先从频响数据恢复出有理传函矩阵或者在频点上直接做插值。我一般建议优先做有理式拟合因为拟合出来的L(s)还能继续用于控制器设计、模型降阶和其他频域分析。有理式拟合的典型算法是向量拟合Vector Fitting它能把频响数据拟合为基于极点和留数的有理近似。这里不需要你自己实现完整算法很多工具箱都有现成函数。拟合的关键是设定合适的极点数量。极点数量太少共振峰附近拟合不准确特征轨迹会偏离真实极点数量太多拟合会过学习把噪声也当成系统动态产生虚假的极点对。一个稳妥的做法是先用拟合工具的误差曲线判断拟合后的频响相对误差在40 dB以下然后逐渐增加极点数观察新增极点是否落在左半平面且对频响误差贡献小于1 dB。如果增加一个极点数但误差没有明显下降就退回上一个数量。向量拟合的基本步骤是给定一组频响数据H(jω_k)可以是一个矩阵的每个元素先初始化一组极点然后通过迭代最小二乘修正极点位置再求解留数。迭代收敛后把所有落在右半平面的极点直接镜像到左半平面因为物理系统的特征轨迹要求开环极点不能随意设在右半平面除非它本来就不稳定。这里要注意如果系统本身不稳定右半平面极点不应该被强制镜像否则拟合出来的L(s)会丢掉真实的右半平面极点信息P就数错了。4.2 在离散频点上直接计算特征轨迹的方法如果只是为了快速看趋势不想做有理式拟合可以在离散频点上直接计算特征值。做法是把每个频点上的实测频响矩阵比如H(jω_k)当作L(jω_k)用numpy.eigvals求特征值然后借助频率间隔做连线。这个方法省去了拟合但有一个前提频响数据要包含足够宽的频率范围特别要包含那些特征值穿越临界点附近的频段。否则相邻频点之间的距离太远连出来的曲线会跳过包围圈。离散频点方法的代码和第3.3节几乎一样只是Gjw的来源从control.frequency_response换成了实测数据矩阵。假设我们有频率数组w_data和复数频响数组H_data形状为(nfreq, m, m)那么只需要把第3.3节循环里的组装部分替换掉def eigenloci_from_data(H_data, w_data): # 输入H_data: (nfreq, m, m)复数数组代表每个频率点的L(jw)矩阵 # 输入w_data: (nfreq,)频率点 nfreq, m, _ H_data.shape lambdas np.zeros((nfreq, m), dtypecomplex) for k in range(nfreq): lambdas[k, :] np.linalg.eigvals(H_data[k, :, :]) # 后面的特征值排序和绘图复用第3.3节的排序函数 # 这里直接调用上一节的排序逻辑 lambdas_sorted np.zeros_like(lambdas) lambdas_sorted[0, :] lambdas[0, :] for k in range(1, nfreq): prev lambdas_sorted[k-1, :] curr lambdas[k, :] for j in range(m): dist np.abs(curr - prev) idx_match np.argmin(dist) lambdas_sorted[k, j] curr[idx_match] curr[idx_match] np.inf dist[:, j] np.inf return lambdas_sorted # 假设已有实测数据结构 # lambdas eigenloci_from_data(H_data, w_data)这里没有考虑特征值排序实际使用中要把第3.3节的排序函数搬过来。如果实测频响的相位噪声较大特征值轨迹可能毛毛糙糙肉眼判断包围圈还好但程序自动数圈数时会误判。此时可以先用平滑滤波处理频响数据但滤波会改变系统的幅相特性要谨慎。另一种更可靠的方法是先用有理式拟合并限制极点数量天然获得光滑的频响。表有理式拟合与离散频点直接计算的对比维度有理式拟合离散频点直接计算数据需求频响数据覆盖完整频响数据覆盖完整且频率分辨率足够输出连续有理函数L(s)离散特征点序列抗噪声能力较强极点数量可控较弱噪声会直接出现在轨迹上能否用于控制器设计能不能实现难度需要向量拟合工具简单十几行代码常见翻车点极点数量选错导致过拟合频率间隔太大导致曲线跳变4.3 一个可落地的拟合后绘图流程如果你手头只有实测扫频数据我会建议按下面这个流程走这也是我处理真实对象时最常用的一套组合拳# 伪代码表达流程从频响数据到特征轨迹 # 1. 读入频响数据shape(nfreq, m, m) H np.load(frequency_resp.npy) # 示例数据 w np.load(frequency_points.npy) # 2. 调用向量拟合工具这里以虚构接口为例 # fit_result vector_fitting(w, H, n_poles12, iterations20) # L_ss fit_result.to_state_space() # 3. 用拟合后的状态空间模型重新计算频响更光滑 # w_refined np.logspace(np.log10(w[0]), np.log10(w[-1]), 5000) # _, H_fit, _ ctrl.frequency_response(L_ss, w_refined) # 4. 调用eigenloci_from_data(H_fit, w_refined) 得到特征轨迹这套流程的关键在向量拟合的极点数量。我一般从6个极点开始逐步加到12个观察拟合误差曲线。如果误差在感兴趣的频段内低于-40 dB就停止增加。如果加到的极点已经出现右半平面镜像特征实际系统是稳定的就要检查频响数据是不是在某个频段相位跳变不连续这种情况通常是扫频数据存在泄漏或窗函数影响。5. 广义奈奎斯特曲线绘制避坑指南特征轨迹排序、连续性和包围数判定5.1 特征值排序跳变导致曲线断裂现象画出来的特征轨迹在某个频率点突然跳到另一条曲线或者曲线出现锯齿状断裂看起来完全不像光滑的随频率变化的轨迹。原因numpy.linalg.eigvals返回的特征值顺序是任意的没有按物理轨迹的连续性排列。当两个特征值在某个频率附近靠近时数值求解器可能交换它们的顺序导致曲线在交点处断裂。这是求特征值的固有现象不是代码算错了。解决最常用的办法就是第3.3节的贪心最小距离匹配把前后两个频率点的特征值配对。但在交叉点处贪心匹配会出错因为两条轨迹交叉时最小距离匹配会把前后点错误配对。改进方法是假设特征轨迹的导数连续用上一频率点的特征值和导数值预测当前频率点的位置再用预测值做匹配。我一般会先尝试贪心匹配如果发现交点处仍跳变就把频率分辨率提高让相邻频率点距离足够小这样即使交叉跳跃也会比较小。还有一种做法是用逐点追踪算法比如用前一步的特征向量作为当前特征值计算的初始值这类方法适合已经知道状态空间矩阵的场合。一个实际经验是当特征轨迹在某个区域接近重合时不要急着调排序算法先检查一下频率点是否足够密。如果两条轨迹之间的最小距离小于相邻频率点的移动步长那么无论什么排序算法都可能出错。这时加密频率点比改进算法更有效。另外把λ_i(jω)按照实部和虚部同时匹配比单独看复数欧氏距离更稳定因为复数距离在相位跳变时会产生大的虚部误差。5.2 奇异值曲线被误当成特征轨迹现象在分析多变量系统鲁棒性时有人把最大奇异值随频率变化的曲线当成广义奈奎斯特曲线还用它判断临界点距离。原因奇异值分解中的奇异值σ_i(G(jω))是复数矩阵G的奇异值等于特征值λ_i(G^H G)的算术平方根它们是实数而特征轨迹的λ_i(G)是复数。两者只在极特殊的情况下相等比如矩阵是正常矩阵满足A^H A A A^H。一般工程系统的传递函数矩阵不满足这个条件因此奇异值曲线给出的只是增益度量无法体现相位信息用它不能判断是否包围-1点。解决确认数据来源。绘制广义奈奎斯特曲线时必须调用eigvals而不是svdvals。如果看到曲线横轴是实数奇异值随频率变化通常画成幅值图那就是画错了。正确的特征轨迹是在复平面上画出来的既有实部又有虚部。有一种容易混淆的场景matlab的sigma函数画出的是奇异值Bode图看起来像一堆“花瓣”实际上和广义奈奎斯特曲线完全是两码事。判断方法很简单如果图形的横轴是频率纵轴是幅值分贝那一定不是特征轨迹。5.3 包围数判断与频率方向约定现象计算包围圈数时有人只对ω从0到∞的正频率特征轨迹数圈结果和开环右半平面极点数对不上。原因广义奈奎斯特判据要求完整的奈奎斯特围线频率从-∞到∞。实际绘制时如果只画正频率另一半曲线要靠对称性补全。对于实有理传递函数矩阵λ(-jω)conj(λ(jω))对单特征值成立因此正频率曲线关于实轴对称。但包围圈数必须同时考虑正负两条半段的贡献。比如正频率曲线逆时针绕-1点半圈负频率曲线顺时针绕半圈两者抵消净圈数为零。只看正频率会误判。解决一种做法是画出负频率对应的共轭曲线凑成完整曲线再用多边形面积法或射线法数圈。另一种做法是只在图上画正频率但数圈时把首尾连接成的闭合路径作为包围路径计算该路径绕-1点的指数。注意系统有积分环节时特征轨迹会在低频处走向无穷这时要显式地用小圆弧绕过极点和单变量奈奎斯特图一样。我建议在代码里把首末两点用直线连起来用这个闭合路径计算绕点数物理上等价于频率围线的虚轴绕行。对于从0到∞的曲线首末两点通常是实轴上的点ω0时特征值为实数ω∞时特征值趋向0或无穷连线闭合后就能计算包围数。一个我踩过好几遍的坑用复数相角累加法数圈时会碰到±2π的跳变。如果你不先做相位解卷绕直接累加相邻点的相位差一个正常绕行会被算成零圈。所以要么用现成的路径包围数算法要么把相位差限制在(-π, π)内再累加这其实也是一种解卷绕。5.4 开环右半平面极点数P的统计错误现象代码和曲线都对但闭环稳定性判断出错最终发现P统计错了。原因MIMO开环传递函数矩阵的极点不是把每个元素的分母分别解根再简单合并。L(s)的极点是它的最小公分母多项式det(sI-A_L)的根其中A_L是L(s)的任一最小实现。换句话说如果某个极点出现在L11的分母中但通过行列式运算被抵消掉了它就不算L(s)的极点。另一个常见误区是只看对角元素的分母忽略交叉通道引入的极点。解决先得到L(s)的最小实现再求所有极点的实部。在python-control中可以先调用ctrl.minreal对L(s)化简然后使用pole函数得到极点的完整列表。统计实部大于0的极点数并注意多重极点按重数计数。我用过一个简单检查方法把P算出来后把临界点改为-2j0重新画特征轨迹并数圈如果闭环特征方程对应的圈数也随之改变且差值为零一般说明P统计正确。这个方法其实是在验证幅角原理的一致性因为同一个L(s)的右半平面极点数不随临界点移动而改变。5.5 频率范围不足导致漏掉临界穿越现象特征轨迹画完了看起来所有曲线都离-1点很远可闭环系统实际是不稳定的。把频率范围拉宽后再画发现高频段有一条特征轨迹绕了一个大圈包住了-1点。原因频率范围没覆盖到高频谐振区域。多变量系统往往有高频的寄生动态比如执行器的二阶振荡、传输延迟带来的相位滞后这些动态对应的特征轨迹可能在高频段远离原点甚至绕出大弧线。如果只按主导极点的转折频率设定频率上限就会漏掉这些弧线。解决频率上限至少取系统最大非零零点模值的10倍如果有纯延迟环节上限要更高。最简单的方法是把频率上限不断增加直到特征轨迹的高频段不再显著变化。另外低频端也不能忽略积分环节会让特征轨迹在低频处沿着负实轴走向-∞如果ω从0.01开始可能看不到渐近线方向导致起始点取错。我习惯把频率下限设为0.001 rad/s上限设为1000 rad/s画出来之后再根据曲线形态裁剪。6. 让曲线真正有用交互检查临界穿越与多变量裕度估计曲线画出来只是第一步在实际设计中我更常用下面两个进阶操作。第一个是用鼠标交互式检查曲线是否真正包围-1点。matplotlib的ginput可以在图上选点但数圈还是靠眼睛。我通常写一个小函数利用路径包含检测比如射线法自动返回每条特征轨迹绕-1点的圈数让代码代替人眼。射线法对凹多边形也能正确处理比相角累加稳定得多。第二个是计算多变量稳定裕度。单变量系统用幅值裕度和相位裕度多变量系统可以用特征轨迹到-1点的距离来定义一个度量在所有频率点上求所有|λ_i(jω)1|的最小值。这个最小值越大闭环对参数摄动的鲁棒性越强。如果最小值接近0说明某个特征轨迹已经快要穿越-1点系统即将失稳。这个指标比单独看每个回路的裕度更有意义因为它考虑了耦合效应。下面这段代码演示了如何计算最小距离和用射线法数圈# 计算所有特征轨迹到-1点的最小距离 dmin np.min(np.abs(lambdas 1)) print(f最小距离到临界点: {dmin:.4f}) # 射线法计算某条轨迹绕-1点的圈数 def winding_number(points, center-10j): # 输入points: 一条特征轨迹按频率顺序排列的复数数组 # 输出围绕center的净圈数逆时针为正 n len(points) theta np.zeros(n) for k in range(n): p1 points[k] - center p2 points[(k 1) % n] - center # 计算p1到p2的角度增量用arctan2计算保证范围 ang1 np.arctan2(p1.imag, p1.real) ang2 np.arctan2(p2.imag, p2.real) delta ang2 - ang1 # 解卷绕限制到[-pi, pi] while delta np.pi: delta - 2*np.pi while delta -np.pi: delta 2*np.pi theta[k] delta return int(round(np.sum(theta) / (2*np.pi))) w_total 0 for i in range(lambdas.shape[1]): w_total winding_number(lambdas[:, i]) print(f总包围圈数 N {w_total})这段代码把每条轨迹首尾相连成一个闭合路径然后累加每个相邻点的相位增量。相位解卷绕通过while循环实现防止±2π跳变。这种方法比直接使用np.angle累加稳定得多我在很多次分析中都靠它避免了手动数圈的错误。当然如果一条特征轨迹的首尾没有恰当连接比如系统在虚轴上存在极点需要先手动处理围线绕行再调用这个函数。我自己在项目中养成的习惯是画完广义奈奎斯特曲线后第一件事不是数圈而是先看曲线与负实轴的交点频率。如果某条轨迹在某个频率穿越负实轴且模长小于1意味着该频率处接近失稳再去核对对应的输入输出通道这样能快速找出危险频段。这个方法帮我定位过几次耦合导致的弱阻尼振荡。另外我遇到特征轨迹排序不好处理的时候会先降低频率分辨率到200个点左右手工观察每条轨迹的走向确认连续后再加密频率点。这套流程走下来广义奈奎斯特曲线基本不再是什么黑匣子希望这篇笔记的思路和坑能帮到你让你画曲线时少绕几个弯。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑