SymPy Control 模块完全指南:用纯 Python 符号化建模与求解 LTI 控制系统
【免费下载链接】sympyA computer algebra system written in pure Python项目地址: https://gitcode.com/GitHub_Trending/sy/sympy
SymPy 的sympy.physics.control是一个纯 Python 实现的符号化控制系统建模与分析模块,专注于线性时不变(LTI,Linear Time-Invariant)系统:既支持 Laplace 域(s 域)的连续时间模型,也支持 z 域的离散时间模型;既能处理单输入单输出(SISO)系统,也能处理多输入多输出(MIMO)系统。读完本文,你将掌握如何用TransferFunction、StateSpace、Series/Parallel/Feedback等核心类完成从建模仿真、稳定性分析到频域绘图(Bode、Nyquist、Nichols 等)的完整控制工程工作流,并获得精确的符号解而非数值近似。本文档以 doc/src/modules/physics/control/index.rst 及其子页面为骨架,结合 lti.py、control_plots.py、routh_table.py 源码展开。
模块定位:符号化的控制系统工具箱
根据 control.rst 的说明,该模块当前能够处理LTI 系统,并提供了两类互补的建模范式:
- 传递函数(Transfer Function):系统输入到输出的频域表示。连续时间系统用
TransferFunction在 Laplace 域(变量s)表示,离散时间系统用DiscreteTransferFunction在 z 域(变量z)表示。 - 状态空间(State Space):
StateSpace与DiscreteStateSpace分别建模连续与离散状态空间系统,支持可控性(controllability)、可观性(observability)分析,以及与传递函数表示之间的互相转换,同时完整支持 MIMO 状态空间系统。
模块的核心卖点是符号计算的精确性:与依赖数值迭代逼近的数值控制包不同,这里得到的解是高度精确的符号表达式,且形式紧凑、可直接用于后续分析。全部核心代码集中在 sympy/physics/control/lti.py(约 6800 行),绘图功能在 sympy/physics/control/control_plots.py,劳斯判据表在 sympy/physics/control/routh_table.py。
sympy.physics.control在包的__init__.py中统一导出了所有公共 API(见 sympy/physics/control/init.py),因此可直接通过如下方式导入:
from sympy.physics.control import ( TransferFunction, DiscreteTransferFunction, PIDController, Series, Parallel, Feedback, TransferFunctionMatrix, MIMOSeries, MIMOParallel, MIMOFeedback, StateSpace, DiscreteStateSpace, gbt, bilinear, forward_diff, backward_diff, phase_margin, gain_margin, RouthHurwitz, pole_zero_plot, bode_plot, step_response_plot, impulse_response_plot, ramp_response_plot, nyquist_plot, nichols_plot, )传递函数模型:从有理多项式到频域对象
TransferFunction:连续时间传递函数
TransferFunction(num, den, var)用三个参数构造:num与den分别是分子与分母多项式(可以是多项式、常数甚至其他符号表达式),var是 Laplace 变换的复变量,必须是Symbol类型(源码在 lti.py 中强制校验)。分母为零会抛出ValueError:
>>> from sympy.abc import s, p, a >>> from sympy.physics.control import TransferFunction >>> tf1 = TransferFunction(s + a, s**2 + s + 1, s) >>> tf1.num a + s >>> tf1.den s**2 + s + 1 >>> tf1.var svar不强制为s,任意Symbol都可作为变换变量,例如TransferFunction(a*p**3 - a*p**2 + s*p, p + a**2, p)。分子分母也可以是浮点数、整数等常量:TransferFunction(1/2, 4, s)。
DiscreteTransferFunction:离散时间传递函数
DiscreteTransferFunction(num, den, var, sampling_time=1)是 z 域对应物,第四个参数sampling_time表示两个相邻采样时刻的时间间隔,默认为 1,可传数值或符号(如k)。注意两点约束(源码见 lti.py):
sampling_time == 0会被拒绝——想建连续模型请用TransferFunction类;sampling_time是必要概念:离散模型的互连运算会通过_check_time_compatibility校验所有系统要么同为连续、要么同为离散且采样时间一致。
>>> from sympy.abc import z, a, k >>> from sympy.physics.control import DiscreteTransferFunction >>> dtf = DiscreteTransferFunction(z**2 + a*z, z**2 + z + 1, z, 0.1) >>> dtf.sampling_time 0.1便捷工厂与构造辅助方法
create_transfer_function(num, den, var, sampling_time=0)是一个分派函数:sampling_time == 0返回TransferFunction,否则返回DiscreteTransferFunction(见 lti.py)。
TransferFunctionBase还提供了三个高效的类方法构造器(lti.py),非常实用:
# 1) 从有理表达式构造(自动识别变量,多变量冲突时需手动指定 var) tf1 = TransferFunction.from_rational_expression((s + 5)/(3*s**2 + 2*s + 1)) # 2) 从系数列表构造(系数按降幂排列,长度可不同) tf2 = TransferFunction.from_coeff_lists([1, 0, 2], [3, 2, 2, 1], s) # 3) 从零点、极点、增益构造(ZPK 形式,支持复数零极点) tf3 = TransferFunction.from_zpk([1, 2, 3], [6, 5, 4], 7, s)离散版本同样提供这些类方法,并额外提供了四个从连续模型做离散化的类方法(from_gbt、from_bilinear、from_forward_diff、from_backward_diff),将在后文"连续到离散的转换"一节详解。
传递函数的核心分析与代数能力
从 lti.py 的TransferFunctionBase源码看,每个传递函数对象都内置了完整的分析与代数接口:
- 结构属性:
num、den、var、is_proper(分子次数 ≤ 分母次数)、is_strictly_proper、is_biproper(次数相等)。 - 动态分析:
poles()/zeros():求解极点与零点,对高阶多项式会退化为用rootof表示的精确根;dc_gain():频率趋于 0 时的增益,纯积分器系统返回oo;连续系统取limit(num/den, var, 0),离散系统取limit(..., var, 1);eval_frequency(other):在复平面任意点求系统响应,如tf.eval_frequency(I*omega)得到频响;to_expr():转换回普通 SymPyExpr,便于与其余符号计算无缝衔接;expand()/to_standard_form(cancel_poles_zeros=False):展开或规整为标准有理式;is_stable(cancel_poles_zeros=False):基于 Hurwitz/Schur 条件判断渐近稳定性(不覆盖临界稳定情形)。
- 代数运算符重载:
+、-、*、/、**、一元-。其中加减乘会产生未求值的Parallel/Series对象,通过.doit()或.rewrite(TransferFunction)求值成单个传递函数;幂运算要求整数指数,负指数会翻转分子分母;tf1 / tf2会被解析为Series(tf1, tf2 的倒数)。例如:
>>> tf9 = TransferFunction(s + 1, s**2 + s + 1, s) >>> tf10 = TransferFunction(s - p, s + 3, s) >>> tf9 + tf10 # 未求值的并联 Parallel(...) >>> (tf9 + tf10).doit() # 求值为单个传递函数 TransferFunction(...)系统互联:Series、Parallel 与 Feedback
这是模块的核心工程能力:把多个子系统按拓扑连接成整体。
串并联
Series(*systems)表示串联(乘法性质),默认evaluate=False,可传evaluate=True直接求值;要求所有子系统使用同一个复变量,否则抛ValueError(校验逻辑见 lti.py 的_check_args)。Parallel(*systems)表示并联(加法性质),同样要求同变量。
两者都支持传入 SISO 状态空间对象(StateSpace),此时.doit()会返回等价的状态空间模型;也都有is_proper、is_strictly_proper、is_biproper属性(作用于求值后的结果)。
Feedback:闭环反馈
Feedback(sys1, sys2=None, sign=-1)表示两个 SISO 系统之间的闭环反馈互联(lti.py):
sys1:前向通路(被控对象 plant);sys2:反馈通路(常为反馈控制器),缺省时自动取单位传递函数 1(单位反馈);sign:反馈符号,-1为负反馈(默认),1为正反馈。
>>> from sympy.physics.control import Feedback, TransferFunction >>> plant = TransferFunction(3*s**2 + 7*s - 3, s**2 - 4*s + 2, s) >>> controller = TransferFunction(5*s - 10, s + 7, s) >>> F1 = Feedback(plant, controller) >>> F1.sys1 # 前向通路 >>> F1.sys2 # 反馈通路 >>> F1.doit() # 求值得到闭环传递函数 TransferFunction(...)Feedback还提供:
sign属性返回反馈符号,num/den属性给出闭环分子与分母结构;sensitivity属性返回闭环系统的灵敏度函数1/(1 - sign·sys1·sys2)(注意:不返回互补灵敏度函数);doit(cancel=False, expand=False)两个关键字分别控制是否约分公共因子、是否展开结果;- 当参数中含状态空间对象时,
doit()返回等价StateSpace,rewrite(TransferFunction)可转回传递函数。
PID 控制器:一行构造工业标准控制器
PIDController(kp, ki, kd, tf, var)是TransferFunction的子类(lti.py),在 Laplace 域直接表示 PID 控制器的传递函数:
kp、ki、kd:比例、积分、微分增益,缺省时分别自动取符号kp、ki、kd;tf:微分滤波器时间常数(用于滤除噪声),缺省为 0;var:复频率变量,缺省为s。
>>> from sympy import symbols >>> from sympy.physics.control import PIDController >>> kp, ki, kd = symbols('kp ki kd') >>> p1 = PIDController(kp, ki, kd) >>> p1.doit() # 转换为普通传递函数 TransferFunction(kd*s**2 + ki + kp*s, s, s) >>> p1.kp, p1.ki, p1.kd, p1.tf, p1.var其传递函数结构为(kp·tf·s² + kp·s + ki·tf·s + ki + kd·s²) / (tf·s² + s),当tf = 0时退化为经典 PID 形式(kd·s² + kp·s + ki)/s。doit()将其转化为普通TransferFunction,方便直接接入Series/Feedback进行闭环设计。
MIMO 系统:传递函数矩阵与互联
模块通过TransferFunctionMatrix及其配套互联类完整支持多输入多输出系统。
TransferFunctionMatrix
TransferFunctionMatrix(arg)是 MIMO 传递函数模型的基类(lti.py),arg是严格嵌套列表,元素为TransferFunction、SISOSeries或Parallel对象。行数 = 输出数(num_outputs),列数 = 输入数(num_inputs),shape返回(num_outputs, num_inputs):
>>> from sympy import Matrix, pprint >>> from sympy.abc import s >>> from sympy.physics.control import TransferFunctionMatrix >>> M = Matrix([[s, 1/s], [1/(s+1), s]]) >>> tfm = TransferFunctionMatrix.from_Matrix(M, s) # 从普通矩阵一行代码转换 >>> pprint(tfm, use_unicode=False) [ s 1] [ - -] [ 1 s] [ ] [ 1 s] [----- -] [s + 1 1]{t}常用操作包括:
from_Matrix(matrix, var, sampling_time=0):从普通 SymPy 矩阵高效构造(sampling_time非 0 时生成离散版本);- 索引切片:
tfm[i, j]取单个传递函数,tfm[:, 0]、tfm[0, :]取整列/整行并返回新的矩阵; transpose():转置以交换输入输出层;elem_poles()/elem_zeros():逐元素求零极点(注意:文档明确提示 MIMO 系统真正的零极点不是单个元素零极点的简单汇总);eval_frequency():在每个元素上求频响,返回普通矩阵;subs():支持单个与多个符号替换,且不修改原对象;.doit():把矩阵内部的Series/Parallel元素化简为单个TransferFunction;- 运算符:
+/-/*产生MIMOParallel/MIMOSeries未求值对象,矩阵乘法要求维度匹配(前者输入数等于后者输出数)。
MIMO 互联类
MIMOSeries(*systems, evaluate=False):MIMO 串联。注意一个反直觉约定(源码 lti.py 明确说明):MIMOSeries(A, B)等价于B*A,总是逆序;相邻系统必须满足"前一系统输出数 = 后一系统输入数"。MIMOParallel(*systems, evaluate=False):MIMO 并联,要求所有成员shape相同。MIMOFeedback(sys1, sys2, sign=-1):MIMO 闭环反馈,要求sys1与sys2的乘积为方阵且系统可逆(doit()基于灵敏度矩阵(I - sign·sys1·sys2)⁻¹·sys1求值,lti.py)。
三者均支持 MIMOStateSpace参数,doit()返回等价的状态空间实现。
状态空间模型:建模、分析与转换
StateSpace 与 DiscreteStateSpace
StateSpace(A, B, C, D)表示连续时间状态空间模型,满足:
x'(t) = A·x(t) + B·u(t) y(t) = C·x(t) + D·u(t)DiscreteStateSpace(A, B, C, D, sampling_time=1)对应离散形式x[k+1] = A·x[k] + B·u[k]、y[k] = C·x[k] + D·u[k](默认采样时间 1,同样拒绝 0)。四个矩阵均可省略——A缺省为1×1零矩阵,B/C/D自动补零到兼容维度(见 lti.py)。构造时源码会对矩阵维度做严格校验(A 必须方阵、A 行数等于 B 行数、C 行数等于 D 行数、A 列数等于 C 列数、B 列数等于 D 列数),输入非sympy矩阵会抛TypeError:
>>> from sympy import Matrix >>> from sympy.physics.control import StateSpace >>> A = Matrix([[1, 2], [1, 0]]) >>> B = Matrix([1, 1]) >>> C = Matrix([[0, 1]]) >>> D = Matrix([0]) >>> ss = StateSpace(A, B, C, D) # 只给 A、B 也可,其余自动补零 >>> ss.num_states # 2 >>> ss.shape # (1, 1),即 (num_outputs, num_inputs)状态空间分析能力
StateSpaceBase提供了完备的可控性/可观性分析工具箱(lti.py):
- 矩阵访问:
A/B/C/D(别名state_matrix、input_matrix、output_matrix、feedforward_matrix)、num_states、num_inputs、num_outputs、shape; - 可控性:
controllability_matrix()、controllable_subspace()、uncontrollable_subspace()、is_controllable()(可控矩阵秩 == 状态数); - 可观性:
observability_matrix()、observable_subspace()、unobservable_subspace()、is_observable(); - 标准形分解:
to_controllable_form()、to_observable_form()(基于相似变换把 A/B、A/C 化为块三角形式分离可控/不可控、可观/不可观子系统)、apply_similarity_transform(T); - 稳定性条件:
get_asymptotic_stability_conditions(fast=False)返回 A 矩阵特征多项式 Hurwitz(连续)/Schur(离散)不等式列表,配合reduce_inequalities可解出参数稳定域,例如:
>>> k = symbols('k') >>> A = Matrix([[0, 1, 0], [0, 0, 1], [k - 1, -2*k, -1]]) >>> ss = StateSpace(A, Matrix([1, 0, 0]), Matrix([[0, 1, 0]]), Matrix([0])) >>> reduce_inequalities(ss.get_asymptotic_stability_conditions()) (1/3 < k) & (k < 1)- 连续系统求解:
StateSpace.dsolve(initial_conditions=None, input_vector=None, var=Symbol('t'))基于 ODE 系统求解器linodesolve得到输出y(t)的闭式符号解。
与传递函数的互转
- 状态空间 → 传递函数:
ss.rewrite(TransferFunction)通过公式G(s) = C·(sI − A)⁻¹·B + D计算(lti.py),离散版本对应G(z)。 - 传递函数 → 状态空间:
tf.rewrite(StateSpace)返回可控标准形实现(_StateSpace_matrices_equivalent,要求系统 proper)。文档特别说明:该转换不唯一,同一个传递函数可有多个状态空间实现。
稳定性判据:Hurwitz/Schur 条件与劳斯-赫尔维茨表
模块提供三种互补的稳定性分析途径:
is_stable()/get_asymptotic_stability_conditions()(前文已述):前者返回布尔判断,后者返回可参与符号不等式求解的条件列表。连续系统用Poly.den.hurwitz_conditions()(极点位于左半平面),离散系统用schur_conditions()(极点位于单位圆内)。fast=True时改用EXRAW域快速生成适合lambdify的大表达式。RouthHurwitz类(routh_table.py):继承自MutableDenseMatrix,由特征多项式直接构造劳斯表,并自动处理两种特殊情形:- 首列零情形(First Column Zero Case):构造扩展劳斯表,相关信息记录在
zero_col_infos属性(元组列表,含行号与连续零的个数信息); - 整行零情形(Full Row Zero Case):用辅助多项式(auxiliary polynomial)导数的系数替换该行,触发时
zero_row_case置为True,辅助多项式本体可通过auxiliary_polynomials属性获取(其根关于原点对称,提示系统可能存在纯虚根或正负实部根)。
稳定性判读规则:统计首列符号变化次数,每次变号对应一个正实部根:
- 首列零情形(First Column Zero Case):构造扩展劳斯表,相关信息记录在
>>> from sympy import symbols >>> from sympy.physics.control import RouthHurwitz >>> b1, b2, b3, b4 = symbols('b_1 b_2 b_3 b_4') >>> s = symbols("s") >>> p = b1*s**3 + b2*s**2 + b3*s + b4 >>> RouthHurwitz(p, s)[:, 0] # 首列即稳定性判读依据 Matrix([[b_1], [b_2], [(-b_1*b_4 + b_2*b_3)/b_2], [b_4]])- 增益裕度与相位裕度:
phase_margin(system)与gain_margin(system)(lti.py)基于 Bode 图定义计算连续系统的稳定裕度。两者仅接受 SISO LTI 系统;含时延项(表达式中出现exp)抛NotImplementedError;存在除变换变量外的多余自由符号抛ValueError。
连续到离散的转换:广义双线性变换家族
模块实现了经典的控制系统离散化方法,核心是通用的gbt(generalised bilinear transformation),通过代入s(z) = (z − 1)/(T·(α·z + 1 − α))把连续传递函数H(s)离散化为H(z),其中T为采样周期、α为方法参数(lti.py)。三种特例(源码直接以不同 α 调用gbt):
| 函数 | α | 代入公式 | 典型用途 |
|---|---|---|---|
bilinear(tf, T) | 1/2 | s = (2/T)·(z−1)/(z+1) | 双线性变换(Tustin),保稳定性 |
forward_diff(tf, T) | 0 | s = (z−1)/T | 前向差分(Euler) |
backward_diff(tf, T) | 1 | s = (z−1)/(T·z) | 后向差分(Euler) |
这些函数返回"降幂"系数列表[a, b], [c, d](对应H(z) = (az+b)/(cz+d))。更推荐直接使用DiscreteTransferFunction的类方法,一步得到离散传递函数对象:
>>> from sympy.abc import s, z, L, R, T >>> from sympy.physics.control import TransferFunction, DiscreteTransferFunction >>> tf = TransferFunction(1, s*L + R, s) # RL 一阶电路 >>> dttf = DiscreteTransferFunction.from_bilinear(tf, T, z) >>> dttf.num # T*z/(2*(L + R*T/2)) + T/(2*(L + R*T/2)) >>> dttf.sampling_time # T类似地还有from_gbt(cont_tf, T, alpha, z)、from_forward_diff、from_backward_diff。
控制系统绘图:Bode、零极点、时域响应与 Nyquist/Nichols
sympy/physics/control/control_plots.py 提供了控制工程中常用的全套绘图函数。依赖说明(文档 control_plots.rst 明确):
- 画图需要外部依赖Matplotlib(源码通过
import_module('matplotlib', ...)惰性导入); - 只取数值数据(
*_numerical_data系列函数)需要NumPy。
每个绘图函数都有对应的*_numerical_data版本,便于用户用其他后端自行绘图或进一步分析。所有函数仅支持SISO LTI 系统(_check_system统一校验:非 SISO 抛NotImplementedError,含时延项不支持,存在多余自由符号抛ValueError,见 control_plots.py)。
零极点图(Pole-Zero Plot)
pole_zero_plot(system, pole_color='blue', pole_markersize=10, zero_color='orange', zero_markersize=7, grid=True, show_axes=False, show=True):在复平面用x标记极点、圆形标记零点。对应的pole_zero_numerical_data返回(zeros, poles)数值列表。
时域响应
step_response_plot(system, ...):单位阶跃响应,底层通过部分分式分解 +_fast_inverse_laplace求得符号解析解再求值(control_plots.py);impulse_response_plot(system, ...):单位冲激响应;ramp_response_plot(system, slope=1, ...):斜坡响应,slope默认 1 且不能为负。
三者共用参数:prec=8(坐标精度)、lower_limit=0、upper_limit=10(时间范围)、color='b'、grid=True、show_axes=False、show=True(show=False时返回 matplotlib 对象)。*_numerical_data默认自适应采样,传adaptive=False与n可获得均匀采样。
频域绘图
bode_plot(system, initial_exp=-5, final_exp=5, ...):一次性绘制幅频与相频两张子图。频率轴为 10 的指数范围(默认10⁻⁵ ~ 10⁵),freq_unit可选'rad/sec'(默认)或'Hz',phase_unit可选'rad'(默认)或'deg',phase_unwrap=True时自动做相位解缠绕。也可分别调用bode_magnitude_plot与bode_phase_plot;nyquist_plot(system, initial_omega=0.01, final_omega=100, ...):奈奎斯特图(同时绘制频率响应曲线及其镜像,基于plot_parametric);nichols_plot(system, initial_omega=0.01, final_omega=100, ...):尼科尔斯图(相位-增益图,横轴为相位 [deg],纵轴为幅值 [dB])。
结语与源码导航
sympy.physics.control为控制工程师和教学场景提供了一条从"写出传递函数/状态空间"到"得到精确符号结论"的完整链路,其符号解不依赖数值近似,可直接用于参数设计(如求解稳定域不等式)与后续数学推导。若需深入了解实现细节,建议按如下路径阅读仓库源码:
- 模型与互联核心:sympy/physics/control/lti.py(传递函数、PID、Series/Parallel/Feedback、MIMO 类、状态空间、离散化、稳定裕度);
- 绘图函数:sympy/physics/control/control_plots.py;
- 劳斯表:sympy/physics/control/routh_table.py;
- 公共导出入口:sympy/physics/control/init.py;
- 官方文档目录:doc/src/modules/physics/control/index.rst(子页面含 control.rst、lti.rst、control_plots.rst、routh_table.rst);
- 测试用例:sympy/physics/control/tests/test_lti.py、test_control_plots.py、test_routh_table.py,可作为 API 用法的权威示例。
【免费下载链接】sympyA computer algebra system written in pure Python项目地址: https://gitcode.com/GitHub_Trending/sy/sympy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考