MPC 模型预测控制全解析:从底层数学推导到 Python 闭环仿真落地
1. 状态预测的数学推导:从“现在”推导“未来”
在计算机中处理的是离散时间系统。假设通过机理建模(或系统辨识),我们得到了车辆动力系统的线性化离散状态空间方程:
x(k+1)=Ax(k)+Bu(k)x(k+1) = Ax(k) + Bu(k)x(k+1)=Ax(k)+Bu(k)
- x(k)x(k)x(k) 是系统的状态向量(例如:当前的电池 SOC、车速)。
- u(k)u(k)u(k) 是控制输入向量(例如:发动机输出功率、电机扭矩)。
MPC 强调“看得很远”。我们需要预测未来 NpN_pNp(预测时域,Prediction Horizon)个步长的状态,同时假设控制指令在未来 NcN_cNc(控制时域,Control Horizon)个步长内变化,之后保持不变(Nc≤NpN_c \le N_pNc≤Np)。
通过不断迭代上面的方程,我们可以写出未来每一步的预测:
x(k+1∣k)=Ax(k)+Bu(k∣k)x(k+1|k) = Ax(k) + Bu(k|k)x(k+1∣k)=Ax(k)+Bu(k∣k)
x(k+2∣k)=A2x(k)+ABu(k∣k)+Bu(k+1∣k)x(k+2|k) = A^2x(k) + ABu(k|k) + Bu(k+1|k)x(k+2∣k)=A2x(k)+ABu(k∣k)+Bu(k+1∣k)
⋮\vdots⋮
x(k+Np∣k)=ANpx(k)+ANp−1Bu(k∣k)+⋯+ANp−NcBu(k+Nc−1∣k)x(k+N_p|k) = A^{N_p}x(k) + A^{N_p-1}Bu(k|k) + \dots + A^{N_p-N_c}Bu(k+N_c-1|k)x(k+Np∣k)=ANpx(k)+ANp−1Bu(k∣k)+⋯+ANp−NcBu(k+Nc−1∣k)
为了方便计算机求解,我们必须将其向量化(矩阵化)。定义未来状态矩阵 XXX 和未来控制矩阵 UUU:
X=[x(k+1∣k)x(k+2∣k)⋮x(k+Np∣k)],U=[u(k∣k)u(k+1∣k)⋮u(k+Nc−1∣k)]X = \begin{bmatrix} x(k+1|k) \\ x(k+2|k) \\ \vdots \\ x(k+N_p|k) \end{bmatrix}, \quad U = \begin{bmatrix} u(k|k) \\ u(k+1|k) \\ \vdots \\ u(k+N_c-1|k) \end{bmatrix}X=
x(k+1∣k)x(k+2∣k)⋮x(k+Np∣k)
,U=
u(k∣k)u(k+1∣k)⋮u(k+Nc−1∣k)
整合后,预测方程可以写成优雅的紧凑形式:
X=Fx(k)+ΦUX = F x(k) + \Phi UX=Fx(k)+ΦU
其中,FFF 和 Φ\PhiΦ 是由系统矩阵 AAA 和 BBB 组合而成的常数矩阵。这一步是写仿真代码的基础,你需要在代码里用循环生成这两个大矩阵。
2. 目标函数(Cost Function)的二次型转化
在能量管理策略中,我们希望燃油消耗最小,同时电池 SOC 偏离参考值的程度最小,且控制动作不要太剧烈(保护执行机构)。我们通常定义一个二次型目标函数 (Quadratic Cost Function):
J=∑i=1NpxT(k+i∣k)Qx(k+i∣k)+∑j=0Nc−1uT(k+j∣k)Ru(k+j∣k)J = \sum_{i=1}^{N_p} x^T(k+i|k) Q x(k+i|k) + \sum_{j=0}^{N_c-1} u^T(k+j|k) R u(k+j|k)J=i=1∑NpxT(k+i∣k)Qx(k+i∣k)+j=0∑Nc−1uT(k+j∣k)Ru(k+j∣k)
- QQQ 是状态权重矩阵(惩罚 SOC 偏离)。
- RRR 是控制权重矩阵(惩罚扭矩输出过大或变化过快)。
利用前面推导的 XXX 和 UUU 矩阵,目标函数也可以写成矩阵形式:
J=XTQˉX+UTRˉUJ = X^T \bar{Q} X + U^T \bar{R} UJ=XTQˉX+UTRˉU
(Qˉ\bar{Q}Qˉ 和 Rˉ\bar{R}Rˉ 是扩展后的对角矩阵)。
接下来是最关键的代换:把预测方程 X=Fx(k)+ΦUX = F x(k) + \Phi UX=Fx(k)+ΦU 代入目标函数中。因为 x(k)x(k)x(k) 是当前测量的已知量,未知的只有未来的控制序列 UUU。展开并去掉与 UUU 无关的常数项后,我们得到一个只关于 UUU 的标准二次规划(QP)形式:
J(U)=12UTHU+fTUJ(U) = \frac{1}{2} U^T H U + f^T UJ(U)=21UTHU+fTU
这里:
- H=2(ΦTQˉΦ+Rˉ)H = 2(\Phi^T \bar{Q} \Phi + \bar{R})H=2(ΦTQˉΦ+Rˉ) (Hessian 矩阵)
- f=2ΦTQˉFx(k)f = 2 \Phi^T \bar{Q} F x(k)f=2ΦTQˉFx(k)
3. 约束条件的矩阵化表示
车辆系统充满了硬性物理限制。比如,电机的最大扭矩有限,电池 SOC 不能过充过放。
- 控制约束(如电机扭矩极限):Umin≤U≤UmaxU_{\min} \le U \le U_{\max}Umin≤U≤Umax
- 状态约束(如 SOC 范围):Xmin≤X≤XmaxX_{\min} \le X \le X_{\max}Xmin≤X≤Xmax
同样的,把 X=Fx(k)+ΦUX = F x(k) + \Phi UX=Fx(k)+ΦU 代入状态约束,所有的约束最终都可以转化为关于求解变量 UUU 的线性不等式:
AineqU≤bineqA_{ineq} U \le b_{ineq}AineqU≤bineq
4. 终局:求解与滚动执行
到这一步,你已经把一个复杂的工程控制问题,变成了一个标准的二次规划 (QP) 问题:
Minimize: J(U)=12UTHU+fTUJ(U) = \frac{1}{2} U^T H U + f^T UJ(U)=21UTHU+fTU
Subject to: AineqU≤bineqA_{ineq} U \le b_{ineq}AineqU≤bineq
这就是为什么在部署时,你需要用到 OSQP、CasADi 或者 qpOASES 这样的底层求解器。你只需要在每个控制周期:
- 读取当前传感器数据 x(k)x(k)x(k)。
- 更新向量 fff 和边界条件 bineqb_{ineq}bineq。
- 把 H,f,Aineq,bineqH, f, A_{ineq}, b_{ineq}H,f,Aineq,bineq 扔给求解器,算出最优序列 U∗=[u∗(0),u∗(1),… ]TU^* = [u^*(0), u^*(1), \dots]^TU∗=[u∗(0),u∗(1),…]T。
- 只提取第一个值 u∗(0)u^*(0)u∗(0) 作为当前车辆的控制指令(比如发给电机的扭矩命令)。
- 等待下一个采样周期,重复步骤 1-4。
如果将 MPC 与你熟悉的机器学习算法相比,强化学习(如 PPO)是在线下花费巨量时间把“经验”(策略网络)训练好,线上直接推理(很快);而 MPC 则是线上实时求解这个复杂的 QP 矩阵,因此对硬件算力要求更高。这也是为什么实际工程中,算法的计算效率极其重要。
为了让你直观感受到那些复杂的矩阵是怎么在实际中运作的,我们构建一个极简的**车辆定速巡航(Cruise Control)**场景。
假设我们要让车速保持在 20 m/s,为了纯粹演示原理,我们只看未来 2 步(预测时域 Np=2N_p = 2Np=2)。
1. 建立系统模型(车辆物理规律)
假设车辆的动力学极其简单,当前车速加上你的加速度指令,就是下一秒的车速。
- 状态量 x(k)x(k)x(k):当前车速
- 控制量 u(k)u(k)u(k):加速度指令(油门/刹车)
- 状态方程:x(k+1)=1⋅x(k)+0.5⋅u(k)x(k+1) = 1 \cdot x(k) + 0.5 \cdot u(k)x(k+1)=1⋅x(k)+0.5⋅u(k)
(在这里,系统矩阵 A=1A=1A=1,控制矩阵 B=0.5B=0.5B=0.5)
2. 定义目标与约束(我们想要什么,且受什么限制)
- 目标函数:我们希望车速尽可能贴近目标 20 m/s,同时为了乘坐舒适,加速度不能突变(省油/平稳)。
J=(x−20)2+0.1⋅u2J = (x - 20)^2 + 0.1 \cdot u^2J=(x−20)2+0.1⋅u2
(这里跟目标速度的偏差权重 Q=1Q=1Q=1,对加速度的惩罚权重 R=0.1R=0.1R=0.1) - 物理约束:车辆的加速/减速能力是有限的。
−5≤u(k)≤5-5 \le u(k) \le 5−5≤u(k)≤5
3. MPC 的“大脑”开始运转:预测未来 2 步
假设传感器测得现在(第 0 秒)的车速是 15 m/s,即 x(0)=15x(0) = 15x(0)=15。
MPC 算法在代码里会这样往后推导:
- 预测第 1 秒: x(1)=15+0.5⋅u(0)x(1) = 15 + 0.5 \cdot u(0)x(1)=15+0.5⋅u(0)
- 预测第 2 秒: x(2)=x(1)+0.5⋅u(1)=15+0.5⋅u(0)+0.5⋅u(1)x(2) = x(1) + 0.5 \cdot u(1) = 15 + 0.5 \cdot u(0) + 0.5 \cdot u(1)x(2)=x(1)+0.5⋅u(1)=15+0.5⋅u(0)+0.5⋅u(1)
4. 转化为二次规划问题(QP求解)
把上面预测的 x(1)x(1)x(1) 和 x(2)x(2)x(2) 代入我们的目标函数 JJJ 中(包含了未来两步的误差和控制惩罚):
Jtotal=[(x(1)−20)2+0.1⋅u(0)2]+[(x(2)−20)2+0.1⋅u(1)2]J_{total} = [(x(1) - 20)^2 + 0.1 \cdot u(0)^2] + [(x(2) - 20)^2 + 0.1 \cdot u(1)^2]Jtotal=[(x(1)−20)2+0.1⋅u(0)2]+[(x(2)−20)2+0.1⋅u(1)2]
你仔细看这个 JtotalJ_{total}Jtotal,因为 x(1)x(1)x(1) 和 x(2)x(2)x(2) 都可以用 u(0)u(0)u(0) 和 u(1)u(1)u(1) 表示,所以这个长长的式子最终变成了只有一个未知数集合 [u(0),u(1)][u(0), u(1)][u(0),u(1)] 的一元二次方程组。
求解器(比如 OSQP)接手工作:它要在满足约束(−5≤u≤5-5 \le u \le 5−5≤u≤5)的前提下,找到一组 [u(0),u(1)][u(0), u(1)][u(0),u(1)],让 JtotalJ_{total}Jtotal 最小。
5. 滚动执行(反馈校正的魅力)
假设求解器瞬间算出了最优解:
- u∗(0)=4u^*(0) = 4u∗(0)=4 m/s²
- u∗(1)=2u^*(1) = 2u∗(1)=2 m/s²
重点来了: MPC 非常“短视且务实”。它只执行第一个指令 u(0)=4u(0) = 4u(0)=4。
此时踩下油门,车辆在现实中跑了 1 秒。
1 秒后,按照理论,车速应该是 15+0.5×4=1715 + 0.5 \times 4 = 1715+0.5×4=17 m/s。
但是,由于遇到了上坡路(外部干扰,模型没预测到),传感器实际测得的车速是 16 m/s。
没关系!MPC 会将新的 x(0)=16x(0) = 16x(0)=16 作为初始状态,抛弃之前的 u(1)u(1)u(1),重新预测接下来的 2 步,再算一次最优的油门指令。
这就是 MPC 的本质:一个不断根据现实情况,重新规划短期未来的“导航仪”。 在你熟悉的 HEV 能量管理策略中,只是状态从单一的“车速”变成了“SOC + 车速 + 发动机转速”,约束条件变得更复杂而已,底层的循环逻辑与这个极简例子完全一致。
下面是这个定速巡航极简模型的 Python 代码实现。为了让你看到序列的变化,我把预测时域 NpN_pNp 从 2 步扩展到了 5 步。
Python 伪代码:极简 MPC 实现
import cvxpy as cp
import numpy as np
# ================= 1. 定义系统与 MPC 参数 =================
A = 1.0 # 状态矩阵
B = 0.5 # 控制矩阵
N = 5 # 预测时域 (Prediction Horizon)
x_target = 20.0 # 目标车速 (m/s)
Q = 1.0 # 状态误差惩罚权重
R = 0.1 # 控制增量惩罚权重
# ================= 2. 声明优化变量 =================
# x 是未来 N+1 个状态 (包含当前初始状态)
x = cp.Variable(N + 1)
# u 是未来 N 个控制指令
u = cp.Variable(N)
# ================= 3. 构建目标函数与约束 =================
cost = 0
constraints = []
# 假设传感器读取到当前车速为 15 m/s (这是每次循环都要更新的初始状态)
x_current = 15.0
constraints += [x[0] == x_current]
# 核心:循环构建未来 N 步的数学关系
for k in range(N):
# a. 累加目标函数 (Cost): 追求速度贴近目标,且控制动作越小越好
cost += Q * cp.square(x[k] - x_target) + R * cp.square(u[k])
# b. 添加系统动力学约束: 建立前后时刻的纽带 x(k+1) = A*x(k) + B*u(k)
constraints += [x[k+1] == A * x[k] + B * u[k]]
# c. 添加物理边界约束: 加速度限制在 [-5, 5] 之间
constraints += [u[k] >= -5, u[k] <= 5]
# 终端误差惩罚 (可选,让最后一步的车速尽量收敛)
cost += Q * cp.square(x[N] - x_target)
# ================= 4. 求解 QP 问题 =================
# 将目标函数和约束打包,交给底层求解器 (如 OSQP)
problem = cp.Problem(cp.Minimize(cost), constraints)
problem.solve()
# ================= 5. 滚动执行 (Receding Horizon) =================
print(f"求解器计算出的未来 {N} 步加速度序列: {np.round(u.value, 2)}")
print(f"🌟 实际发给车辆执行的只有第一步指令: u(0) = {u.value[0]:.2f} m/s²")
代码逻辑拆解
这段代码完美映射了之前解释的数学逻辑:
cp.Variable:这就是在告诉计算机,你去帮我找 u(0)u(0)u(0) 到 u(4)u(4)u(4) 这 5 个未知数。for k in range(N)循环:这是 MPC 区别于传统控制的最优雅之处。你不需要手算未来的展开式,循环体内部的constraints += [x[k+1] == A * x[k] + B * u[k]]就直接把未来 5 步的预测模型“硬编码”成了优化问题的强制约束。problem.solve():这是一个黑盒过程。底层调用的通常是 C++ 写的求解器,它会通过内点法或有效集法,瞬间找出使cost最小的u序列。- 提取
u.value[0]:这就是 MPC 的“滚动”精髓。虽然算出了未来 5 步的策略,但我们只取第一个值去执行。
进阶:如何将其映射到你的研究中?
当你把这套框架平移到混合动力汽车 (HEV) 的能量管理策略时,代码的骨架完全不用动,只需要做以下“升级”:
- 状态扩维:
x不再只是一个标量车速,而是一个向量$x = [\text{SOC}, \text{车速}]^T$。 - 控制扩维:
u变成向量$u = [\text{发动机扭矩}, \text{电机扭矩}]^T$。 - 非线性挑战:车辆的发动机万有特性图(BSFC map)和电池内阻模型是高度非线性的。这时简单的 Ax+BuAx + BuAx+Bu 就不够用了。你要么在每个采样点对非线性模型进行泰勒展开线性化(这就成了 LTV-MPC 或 线性时变 MPC),要么直接使用非线性求解器(如 CasADi 结合 IPOPT 求解器)来做 NMPC。
你可以尝试把这段代码复制到一个 Python 脚本里运行一下,修改一下 x_current(当前车速)或者 Q 和 R 的权重比例,观察输出的加速度指令有什么变化。这能极大地培养你对 MPC 参数整定的直觉。
在混合动力汽车(HEV)的实际能量管理中,你面对的不再是简单的 x(k+1)=1⋅x(k)+0.5⋅u(k)x(k+1) = 1 \cdot x(k) + 0.5 \cdot u(k)x(k+1)=1⋅x(k)+0.5⋅u(k)。发动机的燃油消耗率(BSFC map)、电池的开路电压和内阻随 SOC 的变化、以及空气阻力,全都是高度非线性的。
标准的二次规划(QP)求解器只认识线性约束。为了让 MPC 能处理 HEV 的非线性系统,业界最主流的做法是线性时变模型预测控制(LTV-MPC)。它的核心思想是:非线性太复杂,那我们就把当前这一小段“切平”当成线性的。
这主要分为“线性化”和“离散化”两步。
1. 线性化:泰勒展开 (Taylor Expansion)
假设我们建立的 HEV 连续时间非线性动力学方程为:
x˙(t)=f(x(t),u(t))\dot{x}(t) = f(x(t), u(t))x˙(t)=f(x(t),u(t))
其中 xxx 是包含 SOC、车速等的状态向量,uuu 是扭矩等控制向量。
在每一个控制周期开始时,车辆都有一个当前的“工作点”(Operating Point),比如当前测量的状态 x0x_0x0 和上一时刻的控制量 u0u_0u0。
我们在 (x0,u0)(x_0, u_0)(x0,u0) 处对非线性函数 fff 进行一阶泰勒展开(忽略二阶以上的高阶项):
x˙(t)≈f(x0,u0)+∂f∂x∣(x0,u0)(x−x0)+∂f∂u∣(x0,u0)(u−u0)\dot{x}(t) \approx f(x_0, u_0) + \frac{\partial f}{\partial x}\bigg|_{(x_0,u_0)} (x - x_0) + \frac{\partial f}{\partial u}\bigg|_{(x_0,u_0)} (u - u_0)x˙(t)≈f(x0,u0)+∂x∂f
(x0,u0)(x−x0)+∂u∂f
(x0,u0)(u−u0)
这里出现了两个关键的雅可比矩阵 (Jacobian Matrices):
- 状态雅可比:Ac=∂f∂xA_c = \frac{\partial f}{\partial x}Ac=∂x∂f
- 控制雅可比:Bc=∂f∂uB_c = \frac{\partial f}{\partial u}Bc=∂u∂f
为了书写方便,我们定义状态和控制的增量:
Δx=x−x0\Delta x = x - x_0Δx=x−x0
Δu=u−u0\Delta u = u - u_0Δu=u−u0
于是,原本复杂的非线性系统,在当前工作点附近,被近似成了一个优美的连续线性增量方程:
Δx˙=AcΔx+BcΔu\Delta \dot{x} = A_c \Delta x + B_c \Delta uΔx˙=AcΔx+BcΔu
2. 离散化:从连续走向离散 (Discretization)
计算机是无法直接计算微分 Δx˙\Delta \dot{x}Δx˙ 的,我们需要把它转化为离散的步长(采样时间 TsT_sTs)。最常用且最容易在代码里实现的是前向欧拉法 (Forward Euler)。
微积分的基础告诉我们,导数可以近似为差分:
Δx˙≈Δx(k+1)−Δx(k)Ts\Delta \dot{x} \approx \frac{\Delta x(k+1) - \Delta x(k)}{T_s}Δx˙≈TsΔx(k+1)−Δx(k)
把这个代入刚才的线性方程中:
Δx(k+1)−Δx(k)Ts=AcΔx(k)+BcΔu(k)\frac{\Delta x(k+1) - \Delta x(k)}{T_s} = A_c \Delta x(k) + B_c \Delta u(k)TsΔx(k+1)−Δx(k)=AcΔx(k)+BcΔu(k)
稍微移项整理一下,把 Δx(k+1)\Delta x(k+1)Δx(k+1) 留在左边:
Δx(k+1)=Δx(k)+TsAcΔx(k)+TsBcΔu(k)\Delta x(k+1) = \Delta x(k) + T_s A_c \Delta x(k) + T_s B_c \Delta u(k)Δx(k+1)=Δx(k)+TsAcΔx(k)+TsBcΔu(k)
Δx(k+1)=(I+TsAc)Δx(k)+(TsBc)Δu(k)\Delta x(k+1) = (I + T_s A_c) \Delta x(k) + (T_s B_c) \Delta u(k)Δx(k+1)=(I+TsAc)Δx(k)+(TsBc)Δu(k)
(其中 III 是单位矩阵)
魔法时刻到了! 我们定义两个新的离散矩阵:
- Ak=I+TsAcA_k = I + T_s A_cAk=I+TsAc
- Bk=TsBcB_k = T_s B_cBk=TsBc
最终的离散化方程变成了:
Δx(k+1)=AkΔx(k)+BkΔu(k)\Delta x(k+1) = A_k \Delta x(k) + B_k \Delta u(k)Δx(k+1)=AkΔx(k)+BkΔu(k)
你看,这个公式是不是和我们最开始讲的 x(k+1)=Ax(k)+Bu(k)x(k+1) = Ax(k) + Bu(k)x(k+1)=Ax(k)+Bu(k) 结构完全一样了?现在,你可以放心地把 AkA_kAk 和 BkB_kBk 喂给上一节提到的二次规划(QP)求解器了。
3. LTV-MPC 在底层的真实运作流程
由于车辆一直在开,工作点 (x0,u0)(x_0, u_0)(x0,u0) 每时每刻都在变。因此,在 LTV-MPC 的代码循环中,你每走一步,都要重新计算一次矩阵。它的真实流程是这样的:
- 读取状态:传感器读取当前 SOC 和车速 x(k)x(k)x(k)。
- 求雅可比:在当前状态下,对非线性的发动机/电池模型求导,得到连续矩阵 Ac,BcA_c, B_cAc,Bc。
- 离散化:乘上采样时间 TsT_sTs,加上单位矩阵,得到离散矩阵 Ak,BkA_k, B_kAk,Bk。
- 构建 QP:利用 Ak,BkA_k, B_kAk,Bk 预测未来 NpN_pNp 步,把目标函数和约束打包成大矩阵 HHH 和 fff。
- 求解最优增量:求解器算出未来一系列的最优控制增量 ΔU∗\Delta U^*ΔU∗。
- 输出控制:提取第一个值 Δu∗(0)\Delta u^*(0)Δu∗(0),实际发给执行机构的指令是 u=u0+Δu∗(0)u = u_0 + \Delta u^*(0)u=u0+Δu∗(0)。
- 进入下一时刻,循环上述步骤。
4. 算法视角的对比与启发
理解了这一套繁琐的“求导 -> 线性化 -> 离散化 -> 求解矩阵”流程,你再回看其他优化与控制算法,会有更深刻的认识:
- 为何用强化学习 (PPO)? LTV-MPC 需要精确的物理模型,且每一步都在实时算矩阵,算力开销极大。而 PPO 通过让智能体在线下与环境进行数百万次的交互,把这些非线性的映射关系直接“压缩”进了神经网络的权重里。线上执行时,只需一次前向传播(Forward Pass),这就是所谓“用线下训练时间换取线上推理速度”。
- 为何引入启发式算法 (如蜣螂优化 DBO)? 如果你不喜欢泰勒展开带来的线性化误差,想直接面对非线性的成本函数求最优解,QP 求解器就无能为力了。这时,DBO 这类群体智能算法的优势就体现出来了:它们不需要模型可导,也不需要算雅可比矩阵,而是通过“撒点”和规则迭代,强行在非线性空间中搜索出一个最优扭矩分配方案。
在能量管理中,我们最核心的诉求就是:在满足驾驶员需求功率的前提下,合理分配发动机和电机的功率,同时控制电池的 SOC(荷电状态)。
这是一个非常典型的非线性系统。我们一步步推导它的 AkA_kAk 和 BkB_kBk 矩阵。
1. 建立非线性物理模型 f(x,u)f(x, u)f(x,u)
假设一辆并联式 HEV,驾驶员当前的需求功率是 PreqP_{req}Preq(在当前预测步长内视为已知的外部干扰)。
- 状态量 xxx:电池当前的 SOCSOCSOC。
- 控制量 uuu:发动机输出功率 PeP_ePe。
此时,留给电机去承担的功率就是:Pm=Preq−PeP_m = P_{req} - P_ePm=Preq−Pe。(忽略传动效率简化说明)。
根据电池的等效内阻模型,电池的充放电电流 III 与电机功率 PmP_mPm 存在一个高度非线性的关系(包含平方根):
I=Voc(x)−Voc(x)2−4Rin(x)Pm2Rin(x)I = \frac{V_{oc}(x) - \sqrt{V_{oc}(x)^2 - 4 R_{in}(x) P_m}}{2 R_{in}(x)}I=2Rin(x)Voc(x)−Voc(x)2−4Rin(x)Pm
(其中 Voc(x)V_{oc}(x)Voc(x) 是开路电压,Rin(x)R_{in}(x)Rin(x) 是等效内阻,它们都是 SOCSOCSOC 即 xxx 的非线性函数)。
电池 SOC 的变化率(即连续状态方程)定义为电流除以电池最大容量 QmaxQ_{max}Qmax:
x˙=f(x,u)=−Voc(x)−Voc(x)2−4Rin(x)(Preq−u)2Rin(x)Qmax\dot{x} = f(x, u) = -\frac{V_{oc}(x) - \sqrt{V_{oc}(x)^2 - 4 R_{in}(x) (P_{req} - u)}}{2 R_{in}(x) Q_{max}}x˙=f(x,u)=−2Rin(x)QmaxVoc(x)−Voc(x)2−4Rin(x)(Preq−u)
你看,这个公式里既有根号,分母上还有关于状态变量 xxx 的函数。传统的线性算法面对这个方程根本无从下手。
2. 线性化:求解雅可比矩阵 (Jacobian)
在当前时刻 kkk,传感器测得真实状态 x0=0.6x_0 = 0.6x0=0.6(SOC为60%),上一时刻发动机功率为 u0=20kWu_0 = 20kWu0=20kW。我们将在这个“工作点 (x0,u0)(x_0, u_0)(x0,u0)” 对系统进行泰勒展开。
因为我们目前只有 1 个状态和 1 个控制量,所以这里的雅可比“矩阵”退化成了标量导数。
① 求控制雅可比 BcB_cBc(对 uuu 求偏导):
系统状态对发动机功率 uuu 的偏导数相对容易计算,使用复合函数求导法则:
Bc=∂f∂u∣(x0,u0)=1QmaxVoc(x0)2−4Rin(x0)(Preq−u0)B_c = \frac{\partial f}{\partial u}\bigg|_{(x_0, u_0)} = \frac{1}{Q_{max} \sqrt{V_{oc}(x_0)^2 - 4 R_{in}(x_0) (P_{req} - u_0)}}Bc=∂u∂f
(x0,u0)=QmaxVoc(x0)2−4Rin(x0)(Preq−u0)1
工程意义:它代表了在当前状态下,你每多踩一分油门(发动机多出 1kW 功率),电池 SOC 变化率会受到多大影响。
② 求状态雅可比 AcA_cAc(对 xxx 求偏导):
Ac=∂f∂x∣(x0,u0)A_c = \frac{\partial f}{\partial x}\bigg|_{(x_0, u_0)}Ac=∂x∂f
(x0,u0)
由于 Voc(x)V_{oc}(x)Voc(x) 和 Rin(x)R_{in}(x)Rin(x) 在实际工程中通常不是平滑的数学公式,而是保存在控制单元里的一维查表(Look-up Table)。如果你试图去硬算它的解析导数,会非常痛苦。
💡 编程实战技巧(数值微积分):
在写 Python 或 C++ 代码时,我们通常使用数值差分来近似这个雅可比矩阵,这与数值分析中的差商概念一致:
Ac≈f(x0+Δx,u0)−f(x0,u0)ΔxA_c \approx \frac{f(x_0 + \Delta x, u_0) - f(x_0, u_0)}{\Delta x}Ac≈Δxf(x0+Δx,u0)−f(x0,u0)
(比如取 Δx=0.001\Delta x = 0.001Δx=0.001。计算机算一次这个差分只需要几微秒,比推导几大页解析式安全得多)。
算完这两步,我们就得到了常数 AcA_cAc 和 BcB_cBc。我们的非线性方程在 (x0,u0)(x_0, u_0)(x0,u0) 附近就被拍扁成了线性方程:
Δx˙=AcΔx+BcΔu\Delta \dot{x} = A_c \Delta x + B_c \Delta uΔx˙=AcΔx+BcΔu
3. 离散化:步入计算机的世界
假设控制器的采样时间 Ts=0.1T_s = 0.1Ts=0.1 秒。应用前向欧拉法:
- 离散状态矩阵:Ak=1+Ts⋅AcA_k = 1 + T_s \cdot A_cAk=1+Ts⋅Ac
- 离散控制矩阵:Bk=Ts⋅BcB_k = T_s \cdot B_cBk=Ts⋅Bc
至此,原本那个挂着根号、查表满天飞的非线性电池模型,在当前这 0.10.10.1 秒的预测周期内,被完美地化简成了:
Δx(k+1)=AkΔx(k)+BkΔu(k)\Delta x(k+1) = A_k \Delta x(k) + B_k \Delta u(k)Δx(k+1)=AkΔx(k)+BkΔu(k)
4. 扩展到多维度
刚才的例子是 1D 的。但在真实的 HEV 控制中,你的状态空间往往更复杂。比如状态变量 X=[SOC,车速,发动机转速]TX = [SOC, \text{车速}, \text{发动机转速}]^TX=[SOC,车速,发动机转速]T,控制变量 U=[发动机扭矩,发电机扭矩,机械制动力]TU = [\text{发动机扭矩}, \text{发电机扭矩}, \text{机械制动力}]^TU=[发动机扭矩,发电机扭矩,机械制动力]T。
这个时候,AcA_cAc 就会变成一个 3×33 \times 33×3 的矩阵,BcB_cBc 会变成一个 3×33 \times 33×3 的矩阵。
Ac=[∂f1∂x1∂f1∂x2∂f1∂x3∂f2∂x1∂f2∂x2∂f2∂x3∂f3∂x1∂f3∂x2∂f3∂x3]A_c = \begin{bmatrix} \frac{\partial f_1}{\partial x_1} & \frac{\partial f_1}{\partial x_2} & \frac{\partial f_1}{\partial x_3} \\ \frac{\partial f_2}{\partial x_1} & \frac{\partial f_2}{\partial x_2} & \frac{\partial f_2}{\partial x_3} \\ \frac{\partial f_3}{\partial x_1} & \frac{\partial f_3}{\partial x_2} & \frac{\partial f_3}{\partial x_3} \end{bmatrix}Ac= ∂x1∂f1∂x1∂f2∂x1∂f3∂x2∂f1∂x2∂f2∂x2∂f3∂x3∂f1∂x3∂f2∂x3∂f3
在进行 Ak=I+TsAcA_k = I + T_s A_cAk=I+TsAc 计算时,III 就变成了 3×33 \times 33×3 的单位矩阵。后续在使用求解器时,我们会经常计算这些矩阵的范数(Matrix Norms)来确保求解过程中的数值稳定性,避免矩阵奇异导致求解失败。
没问题,这部分确实非常抽象。面对一大堆物理方程和根号,很容易迷失在数学符号里。
咱们先把上一条回复里那些吓人的物理符号(比如开路电压、内阻等)全部扔掉!我们换一种纯计算机编程的思维来理解这件事。
1. 核心矛盾:求解器是个“笨蛋”
在计算机里,所谓“复杂的非线性物理模型”,其实就是一个 黑盒函数(就像代码里写的一个 function)。
你给这个黑盒输入两个值:
- 当前的电池剩余电量(SOC),假设用 xxx 表示。
- 发动机发出的功率,假设用 uuu 表示。
它会吐出一个值:电池电量下一秒会掉多少(即 SOC 的变化率)。
因为电池的化学特性很复杂,这个函数的图像在三维空间里是一座起伏不平的“山丘”(非线性)。
但是,最底层的优化求解器只认识平整的“斜坡”(也就是线性方程 y=ax+buy = ax + buy=ax+bu)。如果你把一座“山丘”直接扔给它,它会报错宕机。
怎么骗过求解器呢?我们需要用**“切线法”(在数学上叫泰勒展开)**,把山丘的当前位置削平。
2. 傻瓜式推导过程(带入具体数字)
我们来模拟代码在第 0 秒运行时的动作。
传感器告诉你:现在的 SOC 是 0.6(也就是 60%电量),此时发动机的功率是 20 kW。
你把这两个值丢进黑盒算一下,算出电池现在的变化率是 -0.05(意思是当前状态下,每秒掉 5% 的电)。
接下来,代码要做三步:
第一步:求雅可比矩阵(其实就是求斜率 / 敏感度)
计算机怎么求斜率?很简单,就是微调一下输入,看看输出变了多少(这其实对应着数值分析里的差分思想)。
① 求状态的斜率 (AcA_cAc):
我们把 SOC 微微增加 0.001,变成 0.601。发动机功率保持 20 kW 不变。
把 (0.601, 20) 丢进黑盒,算出来变化率变成了 -0.048。
那 SOC 的斜率就是:
Ac=−0.048−(−0.05)0.001=2A_c = \frac{-0.048 - (-0.05)}{0.001} = 2Ac=0.001−0.048−(−0.05)=2
(物理意义:在当前状态下,SOC 每多 1,电量流失的速度就加快 2。)
② 求控制的斜率 (BcB_cBc):
同理,SOC 保持 0.6 不变。我们把发动机功率微微增加 0.01,变成 20.01 kW。
把 (0.6, 20.01) 丢进黑盒,算出来变化率变成了 -0.045。
那发动机功率的斜率就是:
Bc=−0.045−(−0.05)0.01=0.5B_c = \frac{-0.045 - (-0.05)}{0.01} = 0.5Bc=0.01−0.045−(−0.05)=0.5
(物理意义:在当前状态下,发动机每多发 1 的力,电量流失的速度就减缓 0.5。)
第二步:组装成线性方程 (连续状态)
现在,在这个瞬间(SOC为 0.6,功率为 20 的点),原本复杂的一大串物理黑盒,被我们用刚才算出的两个斜率(2 和 0.5)拍扁成了一个非常简单的一元一次方程!
我们用增量(也就是变化量 Δ\DeltaΔ)来表示:
电量的变化率 = 2 × (SOC的变化量) + 0.5 × (功率的变化量)
写成你熟悉的数学符号:
Δx˙=2Δx+0.5Δu\Delta \dot{x} = 2 \Delta x + 0.5 \Delta uΔx˙=2Δx+0.5Δu
这就是所谓的连续模型线性化。那些头疼的根号全没了。
第三步:离散化(切碎时间)
代码是一个循环,不能连续计算。假设我们设定的控制间隔是 0.1 秒(采样时间 Ts=0.1T_s = 0.1Ts=0.1)。
在短短的 0.1 秒内,微积分变成了简单的加减乘除:
未来的 SOC 增量 = 现在的 SOC 增量 + (电量变化率 × 0.1秒)
把第二步算出的等式代进来:
Δx(k+1)=Δx(k)+(2Δx(k)+0.5Δu(k))⋅0.1\Delta x(k+1) = \Delta x(k) + (2 \Delta x(k) + 0.5 \Delta u(k)) \cdot 0.1Δx(k+1)=Δx(k)+(2Δx(k)+0.5Δu(k))⋅0.1
我们把括号拆开,把 Δx(k)\Delta x(k)Δx(k) 合并在一起:
Δx(k+1)=(1+2⋅0.1)Δx(k)+(0.5⋅0.1)Δu(k)\Delta x(k+1) = (1 + 2 \cdot 0.1) \Delta x(k) + (0.5 \cdot 0.1) \Delta u(k)Δx(k+1)=(1+2⋅0.1)Δx(k)+(0.5⋅0.1)Δu(k)
Δx(k+1)=1.2Δx(k)+0.05Δu(k)\Delta x(k+1) = 1.2 \Delta x(k) + 0.05 \Delta u(k)Δx(k+1)=1.2Δx(k)+0.05Δu(k)
3. 大功告成
你看最终这个公式:
Δx(k+1)=1.2Δx(k)+0.05Δu(k)\Delta x(k+1) = 1.2 \Delta x(k) + 0.05 \Delta u(k)Δx(k+1)=1.2Δx(k)+0.05Δu(k)
在这个公式里:
- 1.2 就是所谓的离散状态矩阵 AkA_kAk
- 0.05 就是所谓的离散控制矩阵 BkB_kBk
它是一个完美的 Ax+BuAx + BuAx+Bu 的线性结构!这时候,你就可以把 1.2 和 0.05 这两个数字,直接喂给求解器,求解器就能在一瞬间算出下一步的最优解。
由于非线性模型像一座“山丘”,每个地方的坡度不一样,所以车辆每开 0.1 秒,到了下一个状态,代码就会把上面这三步重新再算一遍,求出新的 1.2 和 0.05。这就叫 LTV-MPC(线性时变模型预测控制)。
在上一节中,我们已经把复杂的非线性黑盒,拍扁成了这样一个优美的线性差分方程(当前时刻的数字):
Δx(k+1)=1.2Δx(k)+0.05Δu(k)\Delta x(k+1) = 1.2 \Delta x(k) + 0.05 \Delta u(k)Δx(k+1)=1.2Δx(k)+0.05Δu(k)
为了演示矩阵是怎么拼出来的,我们设定预测未来 2 步(Np=2N_p = 2Np=2)。
此时,求解器唯一需要寻找的“未知数”,就是未来两步的控制增量指令,我们把它打包成一个列向量 UUU:
U=[Δu(0)Δu(1)]U = \begin{bmatrix} \Delta u(0) \\ \Delta u(1) \end{bmatrix}U=[Δu(0)Δu(1)]
求解器只接受一种格式:左边是常数矩阵乘以未知数 UUU,右边是常数向量,即 AineqU≤bineqA_{ineq} U \le b_{ineq}AineqU≤bineq。
我们分两步来把物理限制塞进这个格式里。
第一步:控制量限制(发动机功率增量不能太大)
出于保护执行机构的物理限制,发动机的功率不能在一瞬间突变。假设我们规定:每 0.1 秒的预测步长内,发动机功率的变化量 Δu\Delta uΔu 不能超过 5 kW。
也就是说,对于未来两步,我们有以下物理限制:
- −5≤Δu(0)≤5-5 \le \Delta u(0) \le 5−5≤Δu(0)≤5
- −5≤Δu(1)≤5-5 \le \Delta u(1) \le 5−5≤Δu(1)≤5
但是,求解器只认识 ≤\le≤(小于等于号),不认识 ≥\ge≥。所以我们要把左边的大于等于号,两边同乘负号,全部统一成 ≤\le≤ 的形式:
- Δu(0)≤5\Delta u(0) \le 5Δu(0)≤5
- −Δu(0)≤5-\Delta u(0) \le 5−Δu(0)≤5
- Δu(1)≤5\Delta u(1) \le 5Δu(1)≤5
- −Δu(1)≤5-\Delta u(1) \le 5−Δu(1)≤5
我们把这 4 个不等式提取系数,写成矩阵形式:
[10−10010−1][Δu(0)Δu(1)]≤[5555]\begin{bmatrix} 1 & 0 \\ -1 & 0 \\ 0 & 1 \\ 0 & -1 \end{bmatrix} \begin{bmatrix} \Delta u(0) \\ \Delta u(1) \end{bmatrix} \le \begin{bmatrix} 5 \\ 5 \\ 5 \\ 5 \end{bmatrix}
1−100001−1
[Δu(0)Δu(1)]≤
5555
你看,这不就构成了 AcontrolU≤bcontrolA_{control} U \le b_{control}AcontrolU≤bcontrol 吗?第一部分搞定!
第二步:状态量限制(电池 SOC 不能掉得太狠)
这里是 MPC 最神奇、也是最核心的地方:把对未来的限制,转化为对现在的要求。
假设为了保护电池,我们要求未来的 SOC 状态变量(增量)不能掉得太厉害,假设物理底线是:
- 未来第一步:−Δx(1)≤0.3-\Delta x(1) \le 0.3−Δx(1)≤0.3
- 未来第二步:−Δx(2)≤0.3-\Delta x(2) \le 0.3−Δx(2)≤0.3
注意:这里出现了 Δx(1)\Delta x(1)Δx(1) 和 Δx(2)\Delta x(2)Δx(2),但我们的未知数向量 UUU 里只有 Δu\Delta uΔu! 求解器不认识 Δx\Delta xΔx,怎么办?
答案是:用模型把 Δx\Delta xΔx 替换掉!
根据我们在上一节算出的模型 Δx(k+1)=1.2Δx(k)+0.05Δu(k)\Delta x(k+1) = 1.2 \Delta x(k) + 0.05 \Delta u(k)Δx(k+1)=1.2Δx(k)+0.05Δu(k),我们往未来推演两步:
- 推演第 1 步:
Δx(1)=1.2Δx(0)+0.05Δu(0)\Delta x(1) = 1.2 \Delta x(0) + 0.05 \Delta u(0)Δx(1)=1.2Δx(0)+0.05Δu(0) - 推演第 2 步:(把第一步的结果代入)
Δx(2)=1.2Δx(1)+0.05Δu(1)=1.2(1.2Δx(0)+0.05Δu(0))+0.05Δu(1)\Delta x(2) = 1.2 \Delta x(1) + 0.05 \Delta u(1) = 1.2(1.2 \Delta x(0) + 0.05 \Delta u(0)) + 0.05 \Delta u(1)Δx(2)=1.2Δx(1)+0.05Δu(1)=1.2(1.2Δx(0)+0.05Δu(0))+0.05Δu(1)
展开后得到:
Δx(2)=1.44Δx(0)+0.06Δu(0)+0.05Δu(1)\Delta x(2) = 1.44 \Delta x(0) + 0.06 \Delta u(0) + 0.05 \Delta u(1)Δx(2)=1.44Δx(0)+0.06Δu(0)+0.05Δu(1)
现在,我们把推演出的 Δx(1)\Delta x(1)Δx(1) 和 Δx(2)\Delta x(2)Δx(2),代回到刚才的物理底线中:
- −(1.2Δx(0)+0.05Δu(0))≤0.3-(1.2 \Delta x(0) + 0.05 \Delta u(0)) \le 0.3−(1.2Δx(0)+0.05Δu(0))≤0.3
- −(1.44Δx(0)+0.06Δu(0)+0.05Δu(1))≤0.3-(1.44 \Delta x(0) + 0.06 \Delta u(0) + 0.05 \Delta u(1)) \le 0.3−(1.44Δx(0)+0.06Δu(0)+0.05Δu(1))≤0.3
接下来,把含有未知数 Δu\Delta uΔu 的项留在左边,把所有已知数(包括当前测量到的初始状态 Δx(0)\Delta x(0)Δx(0))全部移到右边:
- −0.05Δu(0)≤0.3+1.2Δx(0)-0.05 \Delta u(0) \le 0.3 + 1.2 \Delta x(0)−0.05Δu(0)≤0.3+1.2Δx(0)
- −0.06Δu(0)−0.05Δu(1)≤0.3+1.44Δx(0)-0.06 \Delta u(0) - 0.05 \Delta u(1) \le 0.3 + 1.44 \Delta x(0)−0.06Δu(0)−0.05Δu(1)≤0.3+1.44Δx(0)
提取系数,再次写成矩阵形式:
[−0.050−0.06−0.05][Δu(0)Δu(1)]≤[0.3+1.2Δx(0)0.3+1.44Δx(0)]\begin{bmatrix} -0.05 & 0 \\ -0.06 & -0.05 \end{bmatrix} \begin{bmatrix} \Delta u(0) \\ \Delta u(1) \end{bmatrix} \le \begin{bmatrix} 0.3 + 1.2 \Delta x(0) \\ 0.3 + 1.44 \Delta x(0) \end{bmatrix}[−0.05−0.060−0.05][Δu(0)Δu(1)]≤[0.3+1.2Δx(0)0.3+1.44Δx(0)]
这就是 AstateU≤bstateA_{state} U \le b_{state}AstateU≤bstate。第二部分也搞定了!
第三步:大满贯合体
最后,我们只需要把“控制量限制”的矩阵和“状态量限制”的矩阵上下拼接到一起,就得到了求解器梦寐以求的最终形态:
[10−10010−1−0.050−0.06−0.05][Δu(0)Δu(1)]≤[55550.3+1.2Δx(0)0.3+1.44Δx(0)]\begin{bmatrix} 1 & 0 \\ -1 & 0 \\ 0 & 1 \\ 0 & -1 \\ -0.05 & 0 \\ -0.06 & -0.05 \end{bmatrix} \begin{bmatrix} \Delta u(0) \\ \Delta u(1) \end{bmatrix} \le \begin{bmatrix} 5 \\ 5 \\ 5 \\ 5 \\ 0.3 + 1.2 \Delta x(0) \\ 0.3 + 1.44 \Delta x(0) \end{bmatrix} 1−100−0.05−0.06001−10−0.05 [Δu(0)Δu(1)]≤ 55550.3+1.2Δx(0)0.3+1.44Δx(0)
这个庞大的矩阵就是 AineqU≤bineqA_{ineq} U \le b_{ineq}AineqU≤bineq。
在控制代码的每个循环周期里:
- Δx(0)\Delta x(0)Δx(0) 是此时此刻传感器的真实读数,代码会把它更新到右边向量 bineqb_{ineq}bineq 的对应位置。
- 然后,把最左边这个 6×26 \times 26×2 的常数矩阵 AineqA_{ineq}Aineq,和右边这个 6×16 \times 16×1 的常数向量 bineqb_{ineq}bineq 丢给求解器。
- 求解器瞬间算出一组满足这 6 个不等式的最优解 Δu(0)\Delta u(0)Δu(0) 和 Δu(1)\Delta u(1)Δu(1)。
- 提取 Δu(0)\Delta u(0)Δu(0) 下发给发动机。
现在你明白为什么在学术论文和教科书里,MPC 的矩阵推导总是占据好几页纸了吧?因为在真实的 HEV 中,你的状态可能有 3 个,预测时域 NpN_pNp 可能有 20 步甚至 50 步,那这个 AineqA_{ineq}Aineq 矩阵将会膨胀成一个几百行、几十列的超级大矩阵。
幸运的是,虽然数学推导很繁琐,但在工程实践中,如果你使用 cvxpy 或 CasADi 这样的高级库,你只需要用几行 for 循环写出物理限制,底层会自动帮你完成这些复杂的矩阵拼接。
不过,理解了这个底层拼装逻辑,对你排查一种极其常见的报错至关重要:求解器提示“Infeasible”(无可行解)。
在跑仿真甚至实车测试时,如果控制台突然弹出一大排红色的 “Solver Error: Infeasible Problem”,这绝对是所有控制工程师的噩梦。
因为一旦求解器报 “Infeasible”(无可行解),意味着它找不到任何一组控制指令能够同时满足你设定的所有条件。此时,代码如果没有后备机制(Fallback),程序就会直接崩溃,车辆就会瞬间失去控制指令(俗称“撒把”)。
为了解决这个问题,我们需要搞清楚它为什么会发生,以及工程上最经典的应对策略。
1. 为什么会产生 “Infeasible”?
你可以把我们在上一节推导出的矩阵不等式 AineqU≤bineqA_{ineq} U \le b_{ineq}AineqU≤bineq 想象成一个“包围圈”。每一个不等式都是一堵墙,所有的墙围出来的那个空间,叫做可行域 (Feasible Region)。求解器的工作就是在包围圈里找一个最优解。
但在混合动力汽车(HEV)的极端工况下,这个包围圈可能会完全消失(可行域变为空集)。
典型场景:互相矛盾的约束
假设在某一个瞬间:
- 驾驶员猛踩油门:需求极大的功率 PreqP_{req}Preq,要求发动机和电机必须全功率输出。
- 电池 SOC 极低:刚好跌到了你的硬性保护底线 0.3。电池管理系统(BMS)死死限制:Δx≥0\Delta x \ge 0Δx≥0(电量绝对不能再下降了,甚至需要充电)。
- 发动机能力有限:发动机的最大扭矩不足以既满足驾驶员的加速需求,又给电池充电。
此时,求解器看着这些约束矩阵:
- 约束 A 喊着:“电机必须出力!”(必然消耗 SOC)
- 约束 B 喊着:“SOC 绝对不能掉!”
- 约束 C 喊着:“发动机已经到极限了,帮不了忙!”
求解器算了一圈,发现没有任何一种扭矩分配方式能同时满足 A、B、C。于是双手一摊,抛出 “Infeasible”。
2. 终极救场方案:软约束与松弛变量 (Soft Constraints & Slack Variables)
在工程中,绝对不能让求解器崩溃。面对互相矛盾的约束,我们的原则是:“两害相权取其轻”。
换句话说,如果必须要打破规则,那我们就允许求解器“稍微”打破一下状态量的物理底线,但要对这种违规行为施加极其严厉的惩罚。这就是 MPC 工业部署中最核心的技巧:引入松弛变量 (Slack Variable) ϵ\epsilonϵ。
我们把原本冰冷无情的硬约束 (Hard Constraint) 改造一下:
- 原本的硬约束:SOC(k)≥0.3SOC(k) \ge 0.3SOC(k)≥0.3
- 改造后的软约束:SOC(k)≥0.3−ϵSOC(k) \ge 0.3 - \epsilonSOC(k)≥0.3−ϵ
这里的 ϵ\epsilonϵ 是一个新的优化变量(且 ϵ≥0\epsilon \ge 0ϵ≥0)。
这段数学语言翻译成白话就是:“SOC 最好不要低于 0.3,但如果实在没办法,允许你低于 0.3 一点点(低于的量就是 ϵ\epsilonϵ)。”
那怎么保证求解器不会滥用这个特权呢?我们在目标函数 (Cost Function) 里给它狠狠记上一笔!
原本的目标函数是:
J=(跟踪误差)2+R⋅(控制增量)2J = (\text{跟踪误差})^2 + R \cdot (\text{控制增量})^2J=(跟踪误差)2+R⋅(控制增量)2
现在,我们把松弛变量也加进去,并赋予一个天文数字级别的权重 ρ\rhoρ(比如 ρ=106\rho = 10^6ρ=106):
J=(跟踪误差)2+R⋅(控制增量)2+ρ⋅ϵ2J = (\text{跟踪误差})^2 + R \cdot (\text{控制增量})^2 + \rho \cdot \epsilon^2J=(跟踪误差)2+R⋅(控制增量)2+ρ⋅ϵ2
这套机制的精妙之处在于:
- 平时(可行域充足时):因为 ρ\rhoρ 极大,求解器为了让 JJJ 最小,会拼死把 ϵ\epsilonϵ 压榨到 0。此时软约束等效于硬约束,绝对不触碰 SOC 底线。
- 极端工况(面临 Infeasible 时):即使 ϵ\epsilonϵ 只取 0.01(即 SOC 掉到 0.29),在庞大的 ρ\rhoρ 放大下,JJJ 也会变得极大。但没关系,代价再大,也比无解强。求解器宁愿吃下这个天价罚单,也会硬着头皮算出一个控制动作 UUU 交给你,从而保证了车辆控制程序的连续运算,避免了崩溃。
注:通常我们只对状态量(如 SOC、车速)使用软约束。对于控制量(如电机最大物理扭矩),它是物理定律决定的,给再多罚款也超不过,必须保留为硬约束。
3. 底层排查与矩阵诊断
如果你的代码中已经加入了松弛变量,却依然频繁报 “Infeasible”,这就说明大概率不是物理工况的问题,而是你的矩阵在数值计算上崩盘了。
在连续非线性模型离散化(如泰勒展开)并反复累乘预测时域 NpN_pNp 步的过程中,矩阵的数值范围很容易发生爆炸或消失。
这时,你需要跳出纯粹的物理逻辑,从数值计算的角度对你拼装出的 HHH 矩阵和 AineqA_{ineq}Aineq 矩阵进行诊断:
- 矩阵条件数 (Condition Number):在把矩阵喂给 OSQP 或 CasADi 等求解器之前,计算一下 Hessian 矩阵 HHH 的条件数(cond(H)\text{cond}(H)cond(H))。如果条件数极大,说明矩阵病态 (Ill-conditioned)。求解器在进行矩阵求逆或迭代时,微小的截断误差会被无限放大,导致找不到解。
- 数据归一化 (Normalization):HEV 里的单位差距极其悬殊。比如发动机功率 uuu 是几万瓦(10410^4104 量级),而 SOC 状态 xxx 是 0 到 1 之间的小数(10−110^{-1}10−1 量级)。在组装矩阵时,不同维度的数据混在一起,容易导致矩阵的某些行极其庞大,某些行极小。在送入优化器前,必须将状态矩阵和控制矩阵通过左乘对角缩放矩阵,强行约束到 [−1,1][-1, 1][−1,1] 的范数区间内。
结合你目前研究的控制算法,无论是 MPC 中的矩阵求解,还是在 PPO 中对状态空间的设计,处理好约束冲突和数值量级都是确保模型在 Ubuntu 虚拟环境和 GPU 上能够稳定收敛的关键前提。
第一部分:极简连续模型的 Python 仿真实战
为了不被复杂的非线性系统干扰,我们先用一个连续时间的线性车辆动力学模型。假设我们要控制一辆车的加速度,使其达到并维持目标车速。
连续物理模型:
根据牛顿第二定律,忽略风阻的简化连续模型为:
v˙(t)=1mF(t)−μg\dot{v}(t) = \frac{1}{m} F(t) - \mu gv˙(t)=m1F(t)−μg
其中 v˙(t)\dot{v}(t)v˙(t) 是加速度(车速的变化率),F(t)F(t)F(t) 是驱动力(控制量),mmm 是车重,μg\mu gμg 是滚动阻力。
下面的 Python 代码完整演示了:如何把连续公式转化为离散矩阵,并塞进 MPC 求解器中进行仿真。
import numpy as np
import cvxpy as cp
import matplotlib.pyplot as plt
# ================= 1. 物理参数与仿真设置 =================
m = 1000.0 # 车重 (kg)
mu = 0.015 # 滚动阻力系数
g = 9.81 # 重力加速度
T_s = 0.1 # 采样时间 (秒)
N_p = 10 # 预测时域 (步长)
sim_steps = 100 # 总仿真步数 (10秒)
v_target = 20.0 # 目标车速 (m/s)
v_current = 0.0 # 初始车速
# ================= 2. 连续模型 -> 离散矩阵推导 =================
A_k = 1.0
B_k = T_s / m
d_k = - T_s * mu * g
# ================= 3. MPC 权重设置 =================
Q = 10.0 # 惩罚车速误差
R = 0.001 # 惩罚过大的驱动力
# 记录仿真数据用于画图
history_v = []
history_F = []
# ================= 4. 闭环仿真大循环 =================
print("🚀 开始 MPC 仿真...")
for step in range(sim_steps):
v_pred = cp.Variable(N_p + 1)
F_cmd = cp.Variable(N_p)
cost = 0
constraints = [v_pred[0] == v_current]
for k in range(N_p):
cost += Q * cp.square(v_pred[k] - v_target) + R * cp.square(F_cmd[k])
constraints += [v_pred[k+1] == A_k * v_pred[k] + B_k * F_cmd[k] + d_k]
constraints += [F_cmd[k] >= -3000, F_cmd[k] <= 5000]
problem = cp.Problem(cp.Minimize(cost), constraints)
problem.solve(solver=cp.OSQP)
if problem.status != cp.OPTIMAL:
print(f"步数 {step}: 求解器失败 ({problem.status})")
break
F_opt = F_cmd.value[0]
v_current = A_k * v_current + B_k * F_opt + d_k
history_v.append(v_current)
history_F.append(F_opt)
# 每 20 步在控制台打印一次进度
if step % 20 == 0:
print(f"进度: {step}/{sim_steps} | 当前车速: {v_current:.2f} m/s | 输出驱动力: {F_opt:.2f} N")
print("✅ 仿真结束!正在生成图表...")
# ================= 5. 可视化输出 =================
# 为了避免在 Ubuntu 等环境下缺少中文字体导致方块乱码,这里使用英文标签
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
# 绘制车速曲线
ax1.plot(history_v, label='Actual Speed (m/s)', color='blue', linewidth=2)
ax1.axhline(y=v_target, color='red', linestyle='--', label='Target Speed (20 m/s)')
ax1.set_title('MPC Velocity Control')
ax1.set_ylabel('Velocity (m/s)')
ax1.legend()
ax1.grid(True)
# 绘制控制力矩曲线
ax2.plot(history_F, label='Control Force (N)', color='green', linewidth=2)
ax2.axhline(y=5000, color='black', linestyle=':', label='Max Force Limit')
ax2.set_title('MPC Control Action (Force)')
ax2.set_xlabel('Simulation Steps (0.1s/step)')
ax2.set_ylabel('Force (N)')
ax2.legend()
ax2.grid(True)
plt.tight_layout()
plt.show() # 这句是让弹窗显示出来的关键!
你可以直接在有 cvxpy 环境的 Ubuntu 里运行这段代码,它清晰地展示了状态在 v_pred (优化器大脑里的虚拟未来) 和 v_current (真实物理世界) 之间的交替传递。
第二部分:真实 HEV 离散状态空间方程硬核推导
现在,我们将难度拉满,推导并联混合动力汽车(Parallel HEV)能量管理策略中最核心的方程。这正是你在接下来的研究中需要写进论文公式推导部分的干货。
1. 明确变量定义
- 状态变量 X(t)X(t)X(t):电池荷电状态 SOC(t)SOC(t)SOC(t)。
- 控制变量 U(t)U(t)U(t):发动机输出功率 Pe(t)P_e(t)Pe(t)。
- 外部扰动 D(t)D(t)D(t):驾驶员需求功率 Preq(t)P_{req}(t)Preq(t)(假设已知且当前预测步内不变)。
- 中间变量:电池输出功率 Pb(t)=Preq(t)−Pe(t)P_b(t) = P_{req}(t) - P_e(t)Pb(t)=Preq(t)−Pe(t)。
2. 写出连续的非线性物理方程 f(X,U)f(X, U)f(X,U)
电池的 SOC 变化率是由电流积分决定的。基于电池的等效内阻模型,连续非线性状态方程为:
SOC˙(t)=f(SOC,Pe)=−Voc(SOC)−Voc(SOC)2−4Rint(SOC)⋅(Preq−Pe)2Rint(SOC)⋅Qmax\dot{SOC}(t) = f(SOC, P_e) = -\frac{V_{oc}(SOC) - \sqrt{V_{oc}(SOC)^2 - 4 R_{int}(SOC) \cdot (P_{req} - P_e)}}{2 R_{int}(SOC) \cdot Q_{max}}SOC˙(t)=f(SOC,Pe)=−2Rint(SOC)⋅QmaxVoc(SOC)−Voc(SOC)2−4Rint(SOC)⋅(Preq−Pe)
(注:VocV_{oc}Voc 是开路电压,RintR_{int}Rint 是内阻,QmaxQ_{max}Qmax 是电池总容量。VocV_{oc}Voc 和 RintR_{int}Rint 都是关于 SOCSOCSOC 的非线性查表函数。)
3. 求解连续状态的雅可比矩阵 (连续线性化)
为了把它变成 ΔSOC˙=AcΔSOC+BcΔPe\Delta \dot{SOC} = A_c \Delta SOC + B_c \Delta P_eΔSOC˙=AcΔSOC+BcΔPe 的形式,我们在当前工作点 (SOC0,Pe0)(SOC_0, P_{e0})(SOC0,Pe0) 处对偏导数进行解析推导。
① 求控制矩阵 BcB_cBc(对 PeP_ePe 求偏导):
使用复合函数链式求导法则。令根号内部部分为 Δroot=Voc2−4Rint(Preq−Pe)\Delta_{root} = V_{oc}^2 - 4 R_{int} (P_{req} - P_e)Δroot=Voc2−4Rint(Preq−Pe)。
对 PeP_ePe 求偏导时,VocV_{oc}Voc 和 RintR_{int}Rint 都是常数(因为它们只与 SOC 相关):
Bc=∂f∂Pe=−12RintQmax⋅(−12Δroot⋅4Rint)B_c = \frac{\partial f}{\partial P_e} = -\frac{1}{2 R_{int} Q_{max}} \cdot \left( -\frac{1}{2\sqrt{\Delta_{root}}} \cdot 4 R_{int} \right)Bc=∂Pe∂f=−2RintQmax1⋅(−2Δroot1⋅4Rint)
化简后得到:
Bc=1QmaxVoc(SOC0)2−4Rint(SOC0)⋅(Preq−Pe0)B_c = \frac{1}{Q_{max} \sqrt{V_{oc}(SOC_0)^2 - 4 R_{int}(SOC_0) \cdot (P_{req} - P_{e0})}}Bc=QmaxVoc(SOC0)2−4Rint(SOC0)⋅(Preq−Pe0)1
② 求状态矩阵 AcA_cAc(对 SOCSOCSOC 求偏导):
这是一个令人极其痛苦的解析推导,因为商的求导公式 (uv)′=u′v−uv′v2\left(\frac{u}{v}\right)' = \frac{u'v - uv'}{v^2}(vu)′=v2u′v−uv′ 会产生极其冗长的项,并且包含对查表函数 Voc′(SOC)V_{oc}'(SOC)Voc′(SOC) 和 Rint′(SOC)R_{int}'(SOC)Rint′(SOC) 的求导。
这也是为什么在之前的回复中,我强烈建议在代码里直接使用数值差分近似计算 AcA_cAc:
Ac≈f(SOC0+δ,Pe0)−f(SOC0,Pe0)δA_c \approx \frac{f(SOC_0 + \delta, P_{e0}) - f(SOC_0, P_{e0})}{\delta}Ac≈δf(SOC0+δ,Pe0)−f(SOC0,Pe0)
(设定 δ=10−4\delta = 10^{-4}δ=10−4,计算机一瞬间就能算出极度精确的 AcA_cAc 常数值,完全规避了解析推导的错误风险)。
4. 走向离散状态空间 (最终输出形式)
拿到当前工作点的连续常数 AcA_cAc 和 BcB_cBc 后,结合控制周期(采样时间)TsT_sTs。
利用前向欧拉法,将连续微分转化为离散差分方程:
SOC(k+1)=SOC(k)+Ts⋅SOC˙(k)SOC(k+1) = SOC(k) + T_s \cdot \dot{SOC}(k)SOC(k+1)=SOC(k)+Ts⋅SOC˙(k)
将线性化增量模型 ΔSOC˙≈AcΔSOC+BcΔPe\Delta \dot{SOC} \approx A_c \Delta SOC + B_c \Delta P_eΔSOC˙≈AcΔSOC+BcΔPe 代入,最终推导出的HEV离散状态空间增量模型为:
ΔSOC(k+1)=(1+TsAc)ΔSOC(k)+(TsBc)ΔPe(k)\Delta SOC(k+1) = (1 + T_s A_c) \Delta SOC(k) + (T_s B_c) \Delta P_e(k)ΔSOC(k+1)=(1+TsAc)ΔSOC(k)+(TsBc)ΔPe(k)
即:
ΔSOC(k+1)=AkΔSOC(k)+BkΔPe(k)\Delta SOC(k+1) = A_k \Delta SOC(k) + B_k \Delta P_e(k)ΔSOC(k+1)=AkΔSOC(k)+BkΔPe(k)
其中:
- Ak=1+TsAcA_k = 1 + T_s A_cAk=1+TsAc
- Bk=TsBcB_k = T_s B_cBk=TsBc
这就完全对应了第一部分 Python 代码中 v_pred[k+1] == A_k * v_pred[k] + B_k * F_cmd[k] 的标准矩阵形式。
在写大论文时,你可以将上述方程组排版进“控制导向模型建模 (Control-oriented Modeling)”章节;而在写代码时,直接在每个控制周期 step 内调用这个数值计算逻辑,就能让你的 LTV-MPC 在复杂的 HEV 系统中稳健跑起来。
结合之前MPC离散动力学约束这段代码,我逐行、逐语法、逐逻辑超细拆解,结合你 VSCode 调试、变量查看的需求,全程大白话。
目标代码
# 循环遍历每个时刻 k
for k in range(N):
# 系统状态动力学约束
constraints += [x[k+1] == A @ x[k] + B @ u[k]]
一、先讲数学公式(控制原理)
离散线性系统状态方程:
xk+1=Axk+Buk\boldsymbol{x}_{k+1} = \boldsymbol{A}\boldsymbol{x}_k + \boldsymbol{B}\boldsymbol{u}_kxk+1=Axk+Buk
- xk\boldsymbol{x}_kxk:第 kkk 步系统状态(位置、速度等)
- uk\boldsymbol{u}_kuk:第 kkk 步控制输入(油门、力矩等)
- A\boldsymbol{A}A:状态转移矩阵,描述系统自然变化
- B\boldsymbol{B}B:控制输入矩阵,描述控制量对状态的影响
- 含义:下一时刻状态 = 当前状态演化 + 控制量作用
二、逐段代码详解
1. for k in range(N):
N:预测时域长度(MPC 要预测未来 N 步)k:时间步索引,从 0,1,2...N−10,1,2...N-10,1,2...N−1 逐个遍历- 作用:
给每一个时刻,都绑定一套动力学规则,
保证全程物理规律不失效。
2. constraints
- 本质:Python 列表
- 存储 MPC 优化问题里所有约束:
动力学、上下限、初始状态等 - 优化器(cvxpy/jump)会强制满足列表里所有式子。
3. += 语法
列表A += 列表B
作用:把右边列表里的内容,合并追加到左边列表。
4. 外层中括号 [ ... ]
[ x[k+1] == A @ x[k] + B @ u[k] ]
- 把一条约束表达式,包装成只有一个元素的列表
- 必须加的原因:
constraints是列表,+=只能拼接 列表+列表
不加[]会直接报错。
5. 核心表达式
x[k+1] == A @ x[k] + B @ u[k]
x[k]:当前时刻状态变量u[k]:当前时刻控制变量@:矩阵乘法(Python 规范,控制代码必用)- ❌ 不能用
*:*是逐元素相乘
- ❌ 不能用
A @ x[k]:系统无控制下的状态变化B @ u[k]:控制输入带来的状态变化==:优化约束相等
不是普通判断相等,是告诉优化器:这一项必须严格成立
三、结合你的疑惑:为什么是约束?
普通赋值:
x[k+1] = A@x[k] + B@u[k]
是直接计算赋值,写死数值;
而优化里:
x[k+1] == A@x[k] + B@u[k]
x、ux、ux、u 都是优化变量(未知数)
不是固定数,是让优化器在求解时自动满足这个等式。
四、结合你 VSCode 调试痛点
你打断点想查看:
- 每一轮
k是多少 x[k]、u[k]维度/数值A@x[k]计算结果
关键提醒
- 这行是约束定义,不是运算语句
👉 调试时看不到 x[k] 具体数值
因为此时还没求解,变量全是「符号变量」 - 只有执行
prob.solve()之后
变量才会算出具体数值,才能打印/断点查看
五、极简总结(背诵版)
- 循环:给未来每一步都加动力学规则
constraints:约束容器(列表)[]:为了满足 Python 列表拼接语法@:标准矩阵乘法==:优化等式约束,强制系统符合物理方程- 没求解前,变量都是符号,断点看不到具体值
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)