当你驾驶汽车时,你会不断调整方向盘使车保持在车道中央;空调根据温度反馈自动调节制冷功率;火箭通过精确控制推力保持轨道。这些看似不同的系统有一个共同的数学基础——控制理论。本章我们将探索如何用微分方程描述和设计控制系统,从经典的
PID
控制器到现代状态空间方法,看看数学如何帮助我们驯服复杂的动态系统。
控制理论的基本概念
开环与闭环控制
开环控制 :控制信号不依赖于系统输出。 -
例子:微波炉设定时间后加热 - 优点:简单 - 缺点:无法处理干扰
闭环控制(反馈控制) :控制信号根据系统输出进行调整。
- 例子:空调根据室温调节 - 优点:能抵抗干扰,自动修正误差 -
缺点:可能不稳定
控制系统的基本框图
1 2 3 4 5 6 +-----+ e(t) +------------+ u(t) +---------+ y(t) r(t) -->| + |--(-)-------->| Controller |---------->| Plant |-------+--> +-----+ +------------+ +---------+ | ^ | | Feedback | +-----------------------------------------------------------+
:参考输入(期望值)
:系统输出
:误差
:控制输入
Plant:被控对象
微分方程描述
大多数物理系统可以用常微分方程描述。例如,一个质量-弹簧-阻尼系统:
$$
m + c + kx = u(t) $$
其中 是外加力(控制输入), 是位移(输出)。
传递函数与拉普拉斯变换
拉普拉斯变换回顾
Extra close brace or missing open brace \mathcal{L}\{f(t)} = F(s) = \int_0^\infty f(t)e^{-st}dt
关键性质 : - Extra close brace or missing open brace \mathcal{L}\{\dot{f}} = sF(s) - f(0) -
Extra close brace or missing open brace \mathcal{L}\{\ddot{f}} = s^2F(s) - sf(0) - \dot{f}(0) 这使得微分方程变成代数方程!
传递函数
对于线性时不变系统,传递函数定义为(零初始条件):
$$
G(s) = $$
例子 :质量-弹簧-阻尼系统
$$
ms^2X(s) + csX(s) + kX(s) = U(s)
G(s) = = $$
一阶系统
传递函数: $$
G(s) = $$ - :直流增益 - :时间常数
阶跃响应 : $$
y(t) = K(1 - e^{-t/}) $$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import numpy as npimport matplotlib.pyplot as pltfrom scipy.integrate import odeintfrom scipy.signal import lti, stepK = 2.0 tau = 1.0 num = [K] den = [tau, 1 ] system = lti(num, den) t, y_tf = step(system) def first_order_system (y, t, K, tau, u ): dydt = (K*u - y) / tau return dydt t_ode = np.linspace(0 , 8 , 200 ) y_ode = odeint(first_order_system, 0 , t_ode, args=(K, tau, 1 )) y_analytical = K * (1 - np.exp(-t_ode/tau)) fig, axes = plt.subplots(1 , 2 , figsize=(14 , 5 )) ax1 = axes[0 ] ax1.plot(t, y_tf, 'b-' , linewidth=2 , label='Transfer Function' ) ax1.plot(t_ode, y_ode, 'r--' , linewidth=2 , label='ODE Solution' ) ax1.plot(t_ode, y_analytical, 'g:' , linewidth=2 , label='Analytical' ) ax1.axhline(y=K, color='k' , linestyle='--' , alpha=0.5 , label=f'Final value = {K} ' ) ax1.axhline(y=K*0.632 , color='orange' , linestyle=':' , alpha=0.5 ) ax1.axvline(x=tau, color='orange' , linestyle=':' , alpha=0.5 ) ax1.plot(tau, K*0.632 , 'ro' , markersize=10 ) ax1.text(tau+0.1 , K*0.632 , f'τ = {tau} ' , fontsize=10 ) ax1.set_xlabel('Time' , fontsize=12 ) ax1.set_ylabel('Output y(t)' , fontsize=12 ) ax1.set_title('First-Order System Step Response' , fontsize=14 , fontweight='bold' ) ax1.legend(fontsize=10 ) ax1.grid(True , alpha=0.3 ) ax2 = axes[1 ] tau_values = [0.5 , 1.0 , 2.0 , 4.0 ] colors = plt.cm.viridis(np.linspace(0 , 0.8 , len (tau_values))) for tau_val, color in zip (tau_values, colors): y = K * (1 - np.exp(-t_ode/tau_val)) ax2.plot(t_ode, y, color=color, linewidth=2 , label=f'τ = {tau_val} ' ) ax2.axhline(y=K, color='k' , linestyle='--' , alpha=0.5 ) ax2.set_xlabel('Time' , fontsize=12 ) ax2.set_ylabel('Output y(t)' , fontsize=12 ) ax2.set_title('Effect of Time Constant' , fontsize=14 , fontweight='bold' ) ax2.legend(fontsize=11 ) ax2.grid(True , alpha=0.3 ) plt.tight_layout() plt.savefig('first_order_system.png' , dpi=150 , bbox_inches='tight' ) plt.show()
二阶系统
标准形式:
传递函数: $$
G(s) = $$
参数意义 : - :自然频率 - :阻尼比 - :直流增益
极点 : $$
s = -_n _n $$
阻尼比的影响 : - :无阻尼(持续振荡) - :欠阻尼(衰减振荡) - :临界阻尼 - :过阻尼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 from scipy.signal import lti, stepfig, axes = plt.subplots(1 , 2 , figsize=(14 , 5 )) omega_n = 1.0 K = 1.0 t = np.linspace(0 , 15 , 500 ) ax1 = axes[0 ] zeta_values = [0.1 , 0.3 , 0.5 , 0.7 , 1.0 , 2.0 ] colors = plt.cm.coolwarm(np.linspace(0 , 1 , len (zeta_values))) for zeta, color in zip (zeta_values, colors): num = [K * omega_{n*}*2 ] den = [1 , 2 *zeta*omega_n, omega_{n*}*2 ] system = lti(num, den) t_out, y = step(system, T=t) ax1.plot(t_out, y, color=color, linewidth=2 , label=f'ζ = {zeta} ' ) ax1.axhline(y=K, color='k' , linestyle='--' , alpha=0.5 ) ax1.set_xlabel('Time' , fontsize=12 ) ax1.set_ylabel('Output y(t)' , fontsize=12 ) ax1.set_title('Second-Order System: Effect of Damping Ratio' , fontsize=14 , fontweight='bold' ) ax1.legend(fontsize=10 ) ax1.grid(True , alpha=0.3 ) ax2 = axes[1 ] theta = np.linspace(0 , np.pi, 100 ) ax2.plot(np.cos(theta), np.sin(theta), 'k--' , alpha=0.3 ) ax2.plot(np.cos(theta), -np.sin(theta), 'k--' , alpha=0.3 ) for zeta, color in zip (zeta_values, colors): if zeta < 1 : real_part = -zeta * omega_n imag_part = omega_n * np.sqrt(1 - zeta**2 ) ax2.plot(real_part, imag_part, 'o' , color=color, markersize=10 ) ax2.plot(real_part, -imag_part, 'o' , color=color, markersize=10 ) elif zeta == 1 : ax2.plot(-omega_n, 0 , 'o' , color=color, markersize=10 ) else : s1 = -zeta*omega_n + omega_{n*}np.sqrt(zeta**2 - 1 ) s2 = -zeta*omega_n - omega_{n*}np.sqrt(zeta**2 - 1 ) ax2.plot(s1, 0 , 'o' , color=color, markersize=10 ) ax2.plot(s2, 0 , 'o' , color=color, markersize=10 ) ax2.axhline(y=0 , color='k' , linewidth=0.5 ) ax2.axvline(x=0 , color='k' , linewidth=0.5 ) ax2.fill_between([-3 , 0 ], [-3 , -3 ], [3 , 3 ], alpha=0.1 , color='green' , label='Stable region' ) ax2.set_xlabel('Real Part' , fontsize=12 ) ax2.set_ylabel('Imaginary Part' , fontsize=12 ) ax2.set_title('Pole Locations in s-plane' , fontsize=14 , fontweight='bold' ) ax2.set_xlim(-3 , 1 ) ax2.set_ylim(-2 , 2 ) ax2.grid(True , alpha=0.3 ) ax2.set_aspect('equal' ) plt.tight_layout() plt.savefig('second_order_system.png' , dpi=150 , bbox_inches='tight' ) plt.show()
PID 控制器
基本原理
PID 控制器是工业中最常用的控制器,由三个部分组成:
$$
u(t) = K_p e(t) + K_i _0^t e()d+ K_d $$ -
P(比例) :与误差成正比,快速响应但有稳态误差 -
I(积分) :消除稳态误差,但可能导致振荡 -
D(微分) :预测误差趋势,增加稳定性,但对噪声敏感
传递函数形式
$$
G_c(s) = K_p + + K_d s = $$
各部分的作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 def pid_simulation (): """PID 控制器仿真""" def plant (y, t, u ): x, x_dot = y m, c, k = 1.0 , 0.5 , 1.0 x_ddot = (u - c*x_dot - k*x) / m return [x_dot, x_ddot] class PIDController : def __init__ (self, Kp, Ki, Kd ): self.Kp = Kp self.Ki = Ki self.Kd = Kd self.integral = 0 self.prev_error = 0 def compute (self, error, dt ): self.integral += error * dt derivative = (error - self.prev_error) / dt if dt > 0 else 0 self.prev_error = error u = self.Kp * error + self.Ki * self.integral + self.Kd * derivative return u def reset (self ): self.integral = 0 self.prev_error = 0 dt = 0.01 t_final = 20 t = np.arange(0 , t_final, dt) setpoint = 1.0 configs = [ ('P only' , 2.0 , 0 , 0 ), ('PI' , 2.0 , 1.0 , 0 ), ('PD' , 2.0 , 0 , 1.0 ), ('PID' , 2.0 , 1.0 , 1.0 ), ] fig, axes = plt.subplots(2 , 2 , figsize=(14 , 10 )) for ax, (name, Kp, Ki, Kd) in zip (axes.flat, configs): pid = PIDController(Kp, Ki, Kd) y = [0 , 0 ] y_history = [y[0 ]] u_history = [] for i in range (1 , len (t)): error = setpoint - y[0 ] u = pid.compute(error, dt) u_history.append(u) from scipy.integrate import odeint y_new = odeint(plant, y, [0 , dt], args=(u,))[-1 ] y = list (y_new) y_history.append(y[0 ]) ax.plot(t, y_history, 'b-' , linewidth=2 , label='Output' ) ax.axhline(y=setpoint, color='r' , linestyle='--' , linewidth=2 , label='Setpoint' ) ax.set_xlabel('Time' , fontsize=12 ) ax.set_ylabel('Position' , fontsize=12 ) ax.set_title(f'{name} : Kp={Kp} , Ki={Ki} , Kd={Kd} ' , fontsize=12 , fontweight='bold' ) ax.legend(fontsize=10 ) ax.grid(True , alpha=0.3 ) ax.set_ylim(-0.5 , 2 ) plt.tight_layout() plt.savefig('pid_control.png' , dpi=150 , bbox_inches='tight' ) plt.show() pid_simulation()
PID 参数整定
Ziegler-Nichols 方法 :
设 ,逐渐增大 直到系统恰好持续振荡
记录此时的 (临界增益)和振荡周期 3. 根据下表设定 PID 参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def ziegler_nichols_tuning (): """Ziegler-Nichols 整定演示""" def plant_dynamics (state, t, u, m=1.0 , c=0.2 , k=1.0 ): x, v = state return [v, (u - c*v - k*x)/m] def find_critical_gain (): Kp_test = np.linspace(0.1 , 10 , 100 ) oscillation_detected = [] for Kp in Kp_test: dt = 0.01 t = np.arange(0 , 50 , dt) y = [0.1 , 0 ] y_history = [] for _ in range (len (t)): error = 0 - y[0 ] u = Kp * error y_new = odeint(plant_dynamics, y, [0 , dt], args=(u,))[-1 ] y = list (y_new) y_history.append(y[0 ]) y_history = np.array(y_history) if len (y_history) > 100 : late_signal = y_history[-500 :] amplitude_variation = np.max (late_signal) - np.min (late_signal) if amplitude_variation > 0.01 : oscillation_detected.append((Kp, amplitude_variation)) return oscillation_detected fig, axes = plt.subplots(2 , 2 , figsize=(14 , 10 )) Kp_values = [1.0 , 3.0 , 5.0 , 8.0 ] dt = 0.01 t = np.arange(0 , 30 , dt) for ax, Kp in zip (axes.flat, Kp_values): y = [0.5 , 0 ] y_history = [] for _ in range (len (t)): error = 0 - y[0 ] u = Kp * error y_new = odeint(plant_dynamics, y, [0 , dt], args=(u,))[-1 ] y = list (y_new) y_history.append(y[0 ]) ax.plot(t, y_history, 'b-' , linewidth=2 ) ax.axhline(y=0 , color='r' , linestyle='--' , alpha=0.5 ) ax.set_xlabel('Time' , fontsize=12 ) ax.set_ylabel('Position' , fontsize=12 ) ax.set_title(f'Kp = {Kp} ' , fontsize=12 , fontweight='bold' ) ax.grid(True , alpha=0.3 ) plt.suptitle('Finding Critical Gain (Ziegler-Nichols)' , fontsize=14 , fontweight='bold' ) plt.tight_layout() plt.savefig('ziegler_nichols.png' , dpi=150 , bbox_inches='tight' ) plt.show() ziegler_nichols_tuning()
状态空间方法
状态空间表示
任何线性时不变系统可以写成:
其中: - :状态向量 -
:输入向量 - :输出向量 - :系统矩阵 - :输入矩阵 - :输出矩阵 - :前馈矩阵
从微分方程到状态空间
例子 : 定义状态 :
$$
y =
$$
稳定性分析
系统稳定当且仅当
的所有特征值有负实部。
Lyapunov 稳定性 :如果存在正定矩阵 使得 (
正定),则系统稳定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 import numpy as npfrom scipy.linalg import solve_continuous_lyapunovdef state_space_analysis (): """状态空间分析""" m, c, k = 1.0 , 0.5 , 2.0 A = np.array([[0 , 1 ], [-k/m, -c/m]]) B = np.array([[0 ], [1 /m]]) C = np.array([[1 , 0 ]]) D = np.array([[0 ]]) eigenvalues = np.linalg.eigvals(A) print ("系统矩阵 A:" ) print (A) print (f"\n 特征值: {eigenvalues} " ) print (f"系统稳定: {all (ev.real < 0 for ev in eigenvalues)} " ) Q = np.eye(2 ) P = solve_continuous_lyapunov(A.T, -Q) print (f"\nLyapunov 矩阵 P:" ) print (P) print (f"P 正定: {all (np.linalg.eigvals(P) > 0 )} " ) def state_space_ode (x, t, A, B, u ): return A @ x + B.flatten() * u t = np.linspace(0 , 15 , 500 ) x0 = [1 , 0 ] fig, axes = plt.subplots(2 , 2 , figsize=(14 , 10 )) ax1 = axes[0 , 0 ] sol = odeint(state_space_ode, x0, t, args=(A, B, 0 )) ax1.plot(t, sol[:, 0 ], 'b-' , linewidth=2 , label='Position x' ) ax1.plot(t, sol[:, 1 ], 'r-' , linewidth=2 , label='Velocity v' ) ax1.set_xlabel('Time' , fontsize=12 ) ax1.set_ylabel('State' , fontsize=12 ) ax1.set_title('Free Response (u = 0)' , fontsize=12 , fontweight='bold' ) ax1.legend(fontsize=11 ) ax1.grid(True , alpha=0.3 ) ax2 = axes[0 , 1 ] for x0_val in [(1 , 0 ), (0 , 1 ), (-1 , 0 ), (0 , -1 ), (0.5 , 0.5 )]: sol = odeint(state_space_ode, x0_val, t, args=(A, B, 0 )) ax2.plot(sol[:, 0 ], sol[:, 1 ], linewidth=1.5 , alpha=0.7 ) ax2.plot(x0_val[0 ], x0_val[1 ], 'ko' , markersize=6 ) ax2.plot(0 , 0 , 'r*' , markersize=15 , label='Equilibrium' ) ax2.set_xlabel('Position x' , fontsize=12 ) ax2.set_ylabel('Velocity v' , fontsize=12 ) ax2.set_title('Phase Portrait' , fontsize=12 , fontweight='bold' ) ax2.legend(fontsize=11 ) ax2.grid(True , alpha=0.3 ) ax2.set_aspect('equal' ) ax3 = axes[1 , 0 ] x = np.array([0.0 , 0.0 ]) x_history = [x.copy()] dt = t[1 ] - t[0 ] for _ in range (len (t)-1 ): u = 1.0 x_dot = A @ x + B.flatten() * u x = x + x_dot * dt x_history.append(x.copy()) x_history = np.array(x_history) ax3.plot(t, x_history[:, 0 ], 'b-' , linewidth=2 ) ax3.axhline(y=1 /k, color='r' , linestyle='--' , alpha=0.5 , label=f'Steady state = {1 /k:.2 f} ' ) ax3.set_xlabel('Time' , fontsize=12 ) ax3.set_ylabel('Position' , fontsize=12 ) ax3.set_title('Step Response' , fontsize=12 , fontweight='bold' ) ax3.legend(fontsize=11 ) ax3.grid(True , alpha=0.3 ) ax4 = axes[1 , 1 ] ax4.plot(eigenvalues.real, eigenvalues.imag, 'rx' , markersize=15 , mew=3 ) ax4.axhline(y=0 , color='k' , linewidth=0.5 ) ax4.axvline(x=0 , color='k' , linewidth=0.5 ) ax4.fill_between([-2 , 0 ], [-2 , -2 ], [2 , 2 ], alpha=0.1 , color='green' , label='Stable region' ) ax4.set_xlabel('Real Part' , fontsize=12 ) ax4.set_ylabel('Imaginary Part' , fontsize=12 ) ax4.set_title('Pole Locations' , fontsize=12 , fontweight='bold' ) ax4.legend(fontsize=11 ) ax4.grid(True , alpha=0.3 ) ax4.set_xlim(-2 , 1 ) ax4.set_ylim(-2 , 2 ) plt.tight_layout() plt.savefig('state_space.png' , dpi=150 , bbox_inches='tight' ) plt.show() state_space_analysis()
可控性与可观性
可控性
定义 :如果可以通过适当的输入
将系统从任意初始状态转移到任意目标状态,则系统是可控的 。
可控性矩阵 :
可控性条件 :系统可控当且仅当 。
可观性
定义 :如果可以从输出 和输入
确定初始状态,则系统是可观的 。
可观性矩阵 :
可观性条件 :系统可观当且仅当 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 def controllability_observability (): """可控性和可观性分析""" A1 = np.array([[0 , 1 ], [-2 , -3 ]]) B1 = np.array([[0 ], [1 ]]) C1 = np.array([[1 , 0 ]]) A2 = np.array([[1 , 0 ], [0 , 2 ]]) B2 = np.array([[1 ], [0 ]]) C2 = np.array([[1 , 1 ]]) A3 = np.array([[1 , 1 ], [0 , 2 ]]) B3 = np.array([[1 ], [1 ]]) C3 = np.array([[1 , 0 ]]) def analyze_system (A, B, C, name ): n = A.shape[0 ] controllability = B Ai_B = B for i in range (1 , n): Ai_B = A @ Ai_B controllability = np.hstack([controllability, Ai_B]) ctrl_rank = np.linalg.matrix_rank(controllability) observability = C C_Ai = C for i in range (1 , n): C_Ai = C_Ai @ A observability = np.vstack([observability, C_Ai]) obs_rank = np.linalg.matrix_rank(observability) print (f"\n{name} :" ) print (f" A = {A.tolist()} " ) print (f" B = {B.T.tolist()} " ) print (f" C = {C.tolist()} " ) print (f" 可控性矩阵秩: {ctrl_rank} /{n} -> {'可控' if ctrl_rank == n else '不可控' } " ) print (f" 可观性矩阵秩: {obs_rank} /{n} -> {'可观' if obs_rank == n else '不可观' } " ) return ctrl_rank == n, obs_rank == n systems = [ (A1, B1, C1, "系统 1(完全可控可观)" ), (A2, B2, C2, "系统 2(不可控)" ), (A3, B3, C3, "系统 3(不可观)" ), ] for A, B, C, name in systems: analyze_system(A, B, C, name) controllability_observability()
状态反馈与极点配置
状态反馈
如果所有状态可测量,可以使用状态反馈:
闭环系统:
极点配置定理
定理 :如果 可控,则存在反馈增益
使得
的特征值可以被任意指定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 from scipy.signal import place_polesdef pole_placement_demo (): """极点配置演示""" A = np.array([[0 , 1 ], [2 , 0 ]]) B = np.array([[0 ], [1 ]]) C = np.array([[1 , 0 ]]) orig_poles = np.linalg.eigvals(A) print (f"原始极点: {orig_poles} " ) print (f"系统不稳定(有正实部极点)" ) desired_poles = np.array([-2 , -3 ]) result = place_poles(A, B, desired_poles) K = result.gain_matrix print (f"\n 期望极点: {desired_poles} " ) print (f"反馈增益 K = {K} " ) A_cl = A - B @ K achieved_poles = np.linalg.eigvals(A_cl) print (f"实现的极点: {achieved_poles} " ) fig, axes = plt.subplots(1 , 2 , figsize=(14 , 5 )) t = np.linspace(0 , 5 , 500 ) x0 = [1 , 0 ] def open_loop (x, t ): return A @ x sol_open = odeint(open_loop, x0, t) def closed_loop (x, t ): return (A - B @ K) @ x sol_closed = odeint(closed_loop, x0, t) ax1 = axes[0 ] ax1.plot(t, sol_open[:, 0 ], 'r-' , linewidth=2 , label='Open-loop (unstable)' ) ax1.plot(t, sol_closed[:, 0 ], 'b-' , linewidth=2 , label='Closed-loop (stabilized)' ) ax1.axhline(y=0 , color='k' , linestyle='--' , alpha=0.3 ) ax1.set_xlabel('Time' , fontsize=12 ) ax1.set_ylabel('Position x' , fontsize=12 ) ax1.set_title('Stabilization via State Feedback' , fontsize=14 , fontweight='bold' ) ax1.legend(fontsize=11 ) ax1.grid(True , alpha=0.3 ) ax1.set_ylim(-5 , 15 ) ax2 = axes[1 ] ax2.plot(orig_poles.real, orig_poles.imag, 'rx' , markersize=15 , mew=3 , label='Open-loop poles' ) ax2.plot(achieved_poles.real, achieved_poles.imag, 'bo' , markersize=12 , label='Closed-loop poles' ) ax2.axhline(y=0 , color='k' , linewidth=0.5 ) ax2.axvline(x=0 , color='k' , linewidth=0.5 ) ax2.fill_between([-4 , 0 ], [-3 , -3 ], [3 , 3 ], alpha=0.1 , color='green' , label='Stable region' ) ax2.set_xlabel('Real Part' , fontsize=12 ) ax2.set_ylabel('Imaginary Part' , fontsize=12 ) ax2.set_title('Pole Locations' , fontsize=14 , fontweight='bold' ) ax2.legend(fontsize=10 ) ax2.grid(True , alpha=0.3 ) ax2.set_xlim(-4 , 3 ) ax2.set_ylim(-3 , 3 ) plt.tight_layout() plt.savefig('pole_placement.png' , dpi=150 , bbox_inches='tight' ) plt.show() pole_placement_demo()
LQR 最优控制
问题设定
线性二次调节器( LQR)寻找使以下代价函数最小的控制律:
$$
J = _0( T Q + ^T R ) dt $$ - :状态权重矩阵(半正定) - :控制权重矩阵(正定)
最优解
最优控制律是状态反馈:
其中 , 是代数 Riccati 方程的解: $$
A^T P + PA - PBR{-1}B T P + Q = 0 $$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 from scipy.linalg import solve_continuous_aredef lqr_demo (): """LQR 控制器演示""" M = 1.0 m = 0.1 l = 0.5 g = 9.81 A = np.array([ [0 , 1 , 0 , 0 ], [0 , 0 , -m*g/M, 0 ], [0 , 0 , 0 , 1 ], [0 , 0 , (M+m)*g/(M*l), 0 ] ]) B = np.array([[0 ], [1 /M], [0 ], [-1 /(M*l)]]) open_poles = np.linalg.eigvals(A) print (f"开环极点: {open_poles} " ) print (f"系统不稳定: {any (p.real > 0 for p in open_poles)} " ) Q = np.diag([10 , 1 , 100 , 10 ]) R = np.array([[1 ]]) P = solve_continuous_are(A, B, Q, R) K = np.linalg.inv(R) @ B.T @ P print (f"\nLQR 增益 K = {K.flatten()} " ) A_cl = A - B @ K closed_poles = np.linalg.eigvals(A_cl) print (f"闭环极点: {closed_poles} " ) def inverted_pendulum (x, t, K, A, B ): u = -K @ x return (A @ x + B @ u.T).flatten() t = np.linspace(0 , 10 , 1000 ) x0 = [0 , 0 , 0.2 , 0 ] sol = odeint(inverted_pendulum, x0, t, args=(K, A, B)) u_history = [-K @ sol[i] for i in range (len (t))] fig, axes = plt.subplots(2 , 2 , figsize=(14 , 10 )) ax1 = axes[0 , 0 ] ax1.plot(t, sol[:, 0 ], 'b-' , linewidth=2 ) ax1.set_xlabel('Time (s)' , fontsize=12 ) ax1.set_ylabel('Cart Position (m)' , fontsize=12 ) ax1.set_title('Cart Position' , fontsize=12 , fontweight='bold' ) ax1.grid(True , alpha=0.3 ) ax2 = axes[0 , 1 ] ax2.plot(t, sol[:, 2 ]*180 /np.pi, 'r-' , linewidth=2 ) ax2.set_xlabel('Time (s)' , fontsize=12 ) ax2.set_ylabel('Pendulum Angle (deg)' , fontsize=12 ) ax2.set_title('Pendulum Angle' , fontsize=12 , fontweight='bold' ) ax2.grid(True , alpha=0.3 ) ax3 = axes[1 , 0 ] ax3.plot(t, [u[0 ][0 ] for u in u_history], 'g-' , linewidth=2 ) ax3.set_xlabel('Time (s)' , fontsize=12 ) ax3.set_ylabel('Control Force (N)' , fontsize=12 ) ax3.set_title('Control Input' , fontsize=12 , fontweight='bold' ) ax3.grid(True , alpha=0.3 ) ax4 = axes[1 , 1 ] ax4.plot(open_poles.real, open_poles.imag, 'rx' , markersize=15 , mew=3 , label='Open-loop' ) ax4.plot(closed_poles.real, closed_poles.imag, 'bo' , markersize=12 , label='Closed-loop (LQR)' ) ax4.axhline(y=0 , color='k' , linewidth=0.5 ) ax4.axvline(x=0 , color='k' , linewidth=0.5 ) ax4.fill_between([-10 , 0 ], [-10 , -10 ], [10 , 10 ], alpha=0.1 , color='green' ) ax4.set_xlabel('Real Part' , fontsize=12 ) ax4.set_ylabel('Imaginary Part' , fontsize=12 ) ax4.set_title('Pole Locations' , fontsize=12 , fontweight='bold' ) ax4.legend(fontsize=10 ) ax4.grid(True , alpha=0.3 ) ax4.set_xlim(-10 , 5 ) ax4.set_ylim(-10 , 10 ) plt.suptitle('LQR Control of Inverted Pendulum' , fontsize=14 , fontweight='bold' ) plt.tight_layout() plt.savefig('lqr_control.png' , dpi=150 , bbox_inches='tight' ) plt.show() lqr_demo()
观测器设计
为什么需要观测器?
在实际系统中,并非所有状态都可直接测量。观测器 (或状态估计器)从可测量的输入和输出重构状态。
Luenberger 观测器
其中 是估计状态,
是观测器增益。
估计误差动态 :
通过选择 ,可以任意配置误差收敛速度。
分离原理
定理 :对于线性系统,可以独立设计状态反馈控制器和观测器。闭环极点是控制器极点和观测器极点的并集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 def observer_demo (): """观测器设计演示""" A = np.array([[0 , 1 ], [-2 , -3 ]]) B = np.array([[0 ], [1 ]]) C = np.array([[1 , 0 ]]) desired_ctrl_poles = np.array([-2 , -3 ]) result_ctrl = place_poles(A, B, desired_ctrl_poles) K = result_ctrl.gain_matrix desired_obs_poles = np.array([-10 , -12 ]) result_obs = place_poles(A.T, C.T, desired_obs_poles) L = result_obs.gain_matrix.T print (f"控制器增益 K = {K} " ) print (f"观测器增益 L = {L.flatten()} " ) def full_system (state, t, A, B, C, K, L, r ): x = state[:2 ] x_hat = state[2 :] y = C @ x y_hat = C @ x_hat u = -K @ x_hat + r x_dot = A @ x + B @ u.flatten() x_hat_dot = A @ x_hat + B @ u.flatten() + L @ (y - y_hat) return np.concatenate([x_dot, x_hat_dot]) t = np.linspace(0 , 5 , 500 ) x0 = [1 , 0 ] x_hat0 = [0 , 0 ] state0 = x0 + x_hat0 sol = odeint(full_system, state0, t, args=(A, B, C, K, L, 0 )) fig, axes = plt.subplots(2 , 2 , figsize=(14 , 10 )) ax1 = axes[0 , 0 ] ax1.plot(t, sol[:, 0 ], 'b-' , linewidth=2 , label='True x ₁' ) ax1.plot(t, sol[:, 2 ], 'r--' , linewidth=2 , label='Estimated x ̂₁' ) ax1.set_xlabel('Time' , fontsize=12 ) ax1.set_ylabel('State 1' , fontsize=12 ) ax1.set_title('State 1: True vs Estimated' , fontsize=12 , fontweight='bold' ) ax1.legend(fontsize=11 ) ax1.grid(True , alpha=0.3 ) ax2 = axes[0 , 1 ] ax2.plot(t, sol[:, 1 ], 'b-' , linewidth=2 , label='True x ₂' ) ax2.plot(t, sol[:, 3 ], 'r--' , linewidth=2 , label='Estimated x ̂₂' ) ax2.set_xlabel('Time' , fontsize=12 ) ax2.set_ylabel('State 2' , fontsize=12 ) ax2.set_title('State 2: True vs Estimated' , fontsize=12 , fontweight='bold' ) ax2.legend(fontsize=11 ) ax2.grid(True , alpha=0.3 ) ax3 = axes[1 , 0 ] e1 = sol[:, 0 ] - sol[:, 2 ] e2 = sol[:, 1 ] - sol[:, 3 ] ax3.plot(t, e1, 'b-' , linewidth=2 , label='Error in x ₁' ) ax3.plot(t, e2, 'r-' , linewidth=2 , label='Error in x ₂' ) ax3.set_xlabel('Time' , fontsize=12 ) ax3.set_ylabel('Estimation Error' , fontsize=12 ) ax3.set_title('Estimation Error (converges to 0)' , fontsize=12 , fontweight='bold' ) ax3.legend(fontsize=11 ) ax3.grid(True , alpha=0.3 ) ax4 = axes[1 , 1 ] ctrl_poles = np.linalg.eigvals(A - B @ K) obs_poles = np.linalg.eigvals(A - L @ C) ax4.plot(ctrl_poles.real, ctrl_poles.imag, 'bo' , markersize=12 , label='Controller poles' ) ax4.plot(obs_poles.real, obs_poles.imag, 'rs' , markersize=12 , label='Observer poles' ) ax4.axhline(y=0 , color='k' , linewidth=0.5 ) ax4.axvline(x=0 , color='k' , linewidth=0.5 ) ax4.set_xlabel('Real Part' , fontsize=12 ) ax4.set_ylabel('Imaginary Part' , fontsize=12 ) ax4.set_title('Closed-Loop Poles' , fontsize=12 , fontweight='bold' ) ax4.legend(fontsize=11 ) ax4.grid(True , alpha=0.3 ) ax4.set_xlim(-15 , 2 ) ax4.set_ylim(-5 , 5 ) plt.suptitle('Observer-Based Control (Separation Principle)' , fontsize=14 , fontweight='bold' ) plt.tight_layout() plt.savefig('observer.png' , dpi=150 , bbox_inches='tight' ) plt.show() observer_demo()
频域分析
Bode 图
Bode 图显示系统的频率响应: - 幅值图 : ( dB) vs ( log) -
相位图 : (度) vs ( log)
稳定裕度
增益裕度 :相位为 时,还可以增加多少增益才使系统不稳定。
相位裕度 :增益为 0 dB 时,离 还有多少相位余量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 from scipy import signaldef frequency_analysis (): """频域分析演示""" num = [1 ] den = [1 , 2 , 1 ] sys = signal.TransferFunction(num, den) w, mag, phase = signal.bode(sys, n=500 ) fig, axes = plt.subplots(2 , 2 , figsize=(14 , 10 )) ax1 = axes[0 , 0 ] ax1.semilogx(w, mag, 'b-' , linewidth=2 ) ax1.axhline(y=-3 , color='r' , linestyle='--' , alpha=0.5 , label='-3 dB' ) ax1.set_xlabel('Frequency (rad/s)' , fontsize=12 ) ax1.set_ylabel('Magnitude (dB)' , fontsize=12 ) ax1.set_title('Bode Plot: Magnitude' , fontsize=12 , fontweight='bold' ) ax1.legend(fontsize=11 ) ax1.grid(True , alpha=0.3 ) ax2 = axes[0 , 1 ] ax2.semilogx(w, phase, 'r-' , linewidth=2 ) ax2.set_xlabel('Frequency (rad/s)' , fontsize=12 ) ax2.set_ylabel('Phase (degrees)' , fontsize=12 ) ax2.set_title('Bode Plot: Phase' , fontsize=12 , fontweight='bold' ) ax2.grid(True , alpha=0.3 ) Kp = 10 num_ol = [Kp] den_ol = [1 , 2 , 1 , 0 ] sys_ol = signal.TransferFunction(num_ol, den_ol) w_ol, mag_ol, phase_ol = signal.bode(sys_ol, n=500 ) ax3 = axes[1 , 0 ] ax3.semilogx(w_ol, mag_ol, 'b-' , linewidth=2 ) ax3.axhline(y=0 , color='k' , linestyle='--' , alpha=0.5 , label='0 dB' ) ax3.set_xlabel('Frequency (rad/s)' , fontsize=12 ) ax3.set_ylabel('Magnitude (dB)' , fontsize=12 ) ax3.set_title('Open-Loop Bode: Magnitude' , fontsize=12 , fontweight='bold' ) ax3.legend(fontsize=11 ) ax3.grid(True , alpha=0.3 ) ax4 = axes[1 , 1 ] ax4.semilogx(w_ol, phase_ol, 'r-' , linewidth=2 ) ax4.axhline(y=-180 , color='k' , linestyle='--' , alpha=0.5 , label='-180 °' ) ax4.set_xlabel('Frequency (rad/s)' , fontsize=12 ) ax4.set_ylabel('Phase (degrees)' , fontsize=12 ) ax4.set_title('Open-Loop Bode: Phase' , fontsize=12 , fontweight='bold' ) ax4.legend(fontsize=11 ) ax4.grid(True , alpha=0.3 ) plt.tight_layout() plt.savefig('frequency_analysis.png' , dpi=150 , bbox_inches='tight' ) plt.show() frequency_analysis()
应用实例:四旋翼飞行器
简化模型
考虑四旋翼在一个轴上的姿态控制:
$$
I = $$
其中 是俯仰角, 是力矩。
PID 控制设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 def quadrotor_attitude_control (): """四旋翼姿态控制""" I = 0.01 def quadrotor (state, t, tau ): theta, omega = state theta_ddot = tau / I return [omega, theta_ddot] class AttitudeController : def __init__ (self, Kp, Ki, Kd ): self.Kp = Kp self.Ki = Ki self.Kd = Kd self.integral = 0 self.prev_error = 0 def compute (self, theta_des, theta, omega, dt ): error = theta_des - theta self.integral += error * dt derivative = -omega tau = self.Kp * error + self.Ki * self.integral + self.Kd * derivative return tau dt = 0.001 t_final = 5 t = np.arange(0 , t_final, dt) theta_des = np.zeros_like(t) theta_des[t > 1 ] = 0.5 theta_des[t > 3 ] = 0 controllers = [ ('Under-tuned' , 0.1 , 0 , 0.01 ), ('Well-tuned' , 0.5 , 0.1 , 0.05 ), ('Aggressive' , 2.0 , 0.5 , 0.1 ), ] fig, axes = plt.subplots(2 , 2 , figsize=(14 , 10 )) ax1 = axes[0 , 0 ] ax2 = axes[0 , 1 ] ax3 = axes[1 , 0 ] colors = ['blue' , 'green' , 'red' ] for (name, Kp, Ki, Kd), color in zip (controllers, colors): controller = AttitudeController(Kp, Ki, Kd) state = [0 , 0 ] theta_history = [0 ] tau_history = [] for i in range (1 , len (t)): tau = controller.compute(theta_des[i], state[0 ], state[1 ], dt) tau = np.clip(tau, -0.1 , 0.1 ) tau_history.append(tau) state_new = odeint(quadrotor, state, [0 , dt], args=(tau,))[-1 ] state = list (state_new) theta_history.append(state[0 ]) ax1.plot(t, np.array(theta_history)*180 /np.pi, color=color, linewidth=2 , label=name) ax2.plot(t[1 :], tau_history, color=color, linewidth=1.5 , label=name, alpha=0.8 ) ax1.plot(t, theta_des*180 /np.pi, 'k--' , linewidth=2 , label='Desired' ) ax1.set_xlabel('Time (s)' , fontsize=12 ) ax1.set_ylabel('Pitch Angle (deg)' , fontsize=12 ) ax1.set_title('Attitude Response' , fontsize=12 , fontweight='bold' ) ax1.legend(fontsize=10 ) ax1.grid(True , alpha=0.3 ) ax2.set_xlabel('Time (s)' , fontsize=12 ) ax2.set_ylabel('Control Torque (Nm)' , fontsize=12 ) ax2.set_title('Control Input' , fontsize=12 , fontweight='bold' ) ax2.legend(fontsize=10 ) ax2.grid(True , alpha=0.3 ) ax3 = axes[1 , 0 ] controller = AttitudeController(0.5 , 0.1 , 0.05 ) np.random.seed(42 ) state = [0 , 0 ] theta_history = [0 ] for i in range (1 , len (t)): theta_measured = state[0 ] + 0.01 * np.random.randn() omega_measured = state[1 ] + 0.1 * np.random.randn() disturbance = 0.005 * np.sin(2 *np.pi*0.5 *t[i]) tau = controller.compute(theta_des[i], theta_measured, omega_measured, dt) tau = np.clip(tau, -0.1 , 0.1 ) state_new = odeint(quadrotor, state, [0 , dt], args=(tau + disturbance,))[-1 ] state = list (state_new) theta_history.append(state[0 ]) ax3.plot(t, np.array(theta_history)*180 /np.pi, 'b-' , linewidth=1.5 , label='With noise & disturbance' ) ax3.plot(t, theta_des*180 /np.pi, 'k--' , linewidth=2 , label='Desired' ) ax3.set_xlabel('Time (s)' , fontsize=12 ) ax3.set_ylabel('Pitch Angle (deg)' , fontsize=12 ) ax3.set_title('Robustness Test' , fontsize=12 , fontweight='bold' ) ax3.legend(fontsize=10 ) ax3.grid(True , alpha=0.3 ) ax4 = axes[1 , 1 ] ax4.set_xlim(-2 , 2 ) ax4.set_ylim(-1.5 , 1.5 ) from matplotlib.patches import Rectangle, FancyArrowPatch body = Rectangle((-0.5 , -0.1 ), 1.0 , 0.2 , fill=True , color='gray' , alpha=0.7 ) ax4.add_patch(body) prop_positions = [(-0.8 , 0 ), (0.8 , 0 )] for x, y in prop_positions: ax4.plot([x-0.3 , x+0.3 ], [y+0.15 , y+0.15 ], 'b-' , linewidth=3 ) ax4.arrow(x, y+0.2 , 0 , 0.3 , head_width=0.1 , head_length=0.05 , fc='red' , ec='red' ) ax4.annotate('' , xy=(0.5 , 0.8 ), xytext=(0 , 0.3 ), arrowprops=dict (arrowstyle='->' , color='green' , lw=2 )) ax4.text(0.6 , 0.7 , 'θ (pitch)' , fontsize=12 , color='green' ) ax4.set_aspect('equal' ) ax4.axis('off' ) ax4.set_title('Quadrotor Attitude Control' , fontsize=12 , fontweight='bold' ) plt.tight_layout() plt.savefig('quadrotor_control.png' , dpi=150 , bbox_inches='tight' ) plt.show() quadrotor_attitude_control()
总结
本章我们学习了控制理论的核心内容:
传递函数 :将微分方程转化为代数问题
PID 控制 :工业中最常用的控制器
状态空间方法 :现代控制理论的基础
可控性与可观性 :系统能否被控制和观测
极点配置 :任意指定闭环极点
LQR 最优控制 :最小化代价函数的最优控制
观测器设计 :从输出重构状态
频域分析 :从频率响应角度理解系统
控制理论将微分方程从描述工具提升为设计工具——我们不仅能分析系统,还能主动设计系统的行为。从飞机自动驾驶到手机防抖,控制理论的应用无处不在。
练习题
基础题
求一阶系统 的阶跃响应,并画出曲线。
对于二阶系统 :
计算自然频率
和阻尼比 -
判断系统是欠阻尼、临界阻尼还是过阻尼
计算阶跃响应的超调量
设计一个 PID
控制器使以下系统的阶跃响应满足:超调量<10%,调节时间<2 秒:
$$
G(s) = 将 以 下 微 分 方 程 转 化 为 状 态 空 间 形 式 : + 6 + 11 + 6y = u判 断 以 下 系 统 的 可 控 性 和 可 观 性 :
A =
, B =
, C =
$$
进阶题
串级控制 :设计一个串级 PID
控制器控制温度系统:
内环:加热器功率控制
外环:温度控制 比较单回路和串级控制的性能。
系统辨识 :给定阶跃响应数据,估计一阶系统的参数 和 。
证明对于 LQR 问题,如果 可控且
可观,则闭环系统渐近稳定。
离散化 :将连续时间系统 离散化为 。讨论采样周期的选择。
设计一个 Luenberger 观测器,使估计误差的收敛速度是控制器极点的 3
倍。
编程题
实现一个通用的 PID 控制器类,包括:
积分饱和( anti-windup)
微分滤波
输出限幅
编写程序自动进行 Ziegler-Nichols 参数整定。
实现一个完整的倒立摆仿真器,包括:
非线性动力学
LQR 控制器
状态观测器
可视化动画
使用强化学习(如 PPO)训练一个神经网络控制器,与 LQR
比较性能。
实现一个数字滤波器设计工具,支持 Butterworth 、 Chebyshev
等类型。
思考题
PID 控制器如此简单,为什么仍然是工业中最常用的控制器?
讨论模型不确定性对控制器性能的影响。如何设计鲁棒控制器?
比较极点配置和 LQR 的优缺点。什么情况下用哪种方法?
为什么观测器极点通常设计得比控制器极点快?
讨论机器学习方法(如深度强化学习)与传统控制理论的关系和各自适用场景。
参考资料
Ogata, K. (2010). Modern Control Engineering . Prentice
Hall.
Franklin, G. F., Powell, J. D., & Emami-Naeini, A. (2015).
Feedback Control of Dynamic Systems . Pearson.
Å str ö m, K. J., & Murray, R. M. (2008). Feedback
Systems: An Introduction for Scientists and Engineers . Princeton
University Press.
Dorf, R. C., & Bishop, R. H. (2017). Modern Control
Systems . Pearson.
Skogestad, S., & Postlethwaite, I. (2005). Multivariable
Feedback Control: Analysis and Design . Wiley.
Lewis, F. L., Vrabie, D., & Syrmos, V. L. (2012). Optimal
Control . Wiley.
Khalil, H. K.
(2002). Nonlinear Systems . Prentice Hall.
本文是《常微分方程的世界》系列的第 16 章。