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_pNcNp)。

通过不断迭代上面的方程,我们可以写出未来每一步的预测:
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(kk)
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(kk)+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+Npk)=ANpx(k)+ANp1Bu(kk)++ANpNcBu(k+Nc1∣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+Npk) ,U= u(kk)u(k+1∣k)u(k+Nc1∣k)

整合后,预测方程可以写成优雅的紧凑形式:
X=Fx(k)+ΦUX = F x(k) + \Phi UX=Fx(k)+ΦU
其中,FFFΦ\PhiΦ 是由系统矩阵 AAABBB 组合而成的常数矩阵。这一步是写仿真代码的基础,你需要在代码里用循环生成这两个大矩阵。


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=1NpxT(k+ik)Qx(k+ik)+j=0Nc1uT(k+jk)Ru(k+jk)

  • QQQ 是状态权重矩阵(惩罚 SOC 偏离)。
  • RRR 是控制权重矩阵(惩罚扭矩输出过大或变化过快)。

利用前面推导的 XXXUUU 矩阵,目标函数也可以写成矩阵形式:
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≤Umax⁡U_{\min} \le U \le U_{\max}UminUUmax
  • 状态约束(如 SOC 范围):Xmin⁡≤X≤Xmax⁡X_{\min} \le X \le X_{\max}XminXXmax

同样的,把 X=Fx(k)+ΦUX = F x(k) + \Phi UX=Fx(k)+ΦU 代入状态约束,所有的约束最终都可以转化为关于求解变量 UUU 的线性不等式:
AineqU≤bineqA_{ineq} U \le b_{ineq}AineqUbineq


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}AineqUbineq

这就是为什么在部署时,你需要用到 OSQP、CasADi 或者 qpOASES 这样的底层求解器。你只需要在每个控制周期:

  1. 读取当前传感器数据 x(k)x(k)x(k)
  2. 更新向量 fff 和边界条件 bineqb_{ineq}bineq
  3. 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
  4. 只提取第一个值 u∗(0)u^*(0)u(0) 作为当前车辆的控制指令(比如发给电机的扭矩命令)。
  5. 等待下一个采样周期,重复步骤 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)=1x(k)+0.5u(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=(x20)2+0.1u2
    (这里跟目标速度的偏差权重 Q=1Q=1Q=1,对加速度的惩罚权重 R=0.1R=0.1R=0.1)
  • 物理约束:车辆的加速/减速能力是有限的。
    −5≤u(k)≤5-5 \le u(k) \le 55u(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.5u(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.5u(1)=15+0.5u(0)+0.5u(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.1u(0)2]+[(x(2)20)2+0.1u(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 55u5)的前提下,找到一组 [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²")

代码逻辑拆解

这段代码完美映射了之前解释的数学逻辑:

  1. cp.Variable:这就是在告诉计算机,你去帮我找 u(0)u(0)u(0)u(4)u(4)u(4) 这 5 个未知数。
  2. for k in range(N) 循环:这是 MPC 区别于传统控制的最优雅之处。你不需要手算未来的展开式,循环体内部的 constraints += [x[k+1] == A * x[k] + B * u[k]] 就直接把未来 5 步的预测模型“硬编码”成了优化问题的强制约束。
  3. problem.solve():这是一个黑盒过程。底层调用的通常是 C++ 写的求解器,它会通过内点法或有效集法,瞬间找出使 cost 最小的 u 序列。
  4. 提取 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(当前车速)或者 QR 的权重比例,观察输出的加速度指令有什么变化。这能极大地培养你对 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)=1x(k)+0.5u(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)+xf (x0,u0)(xx0)+uf (x0,u0)(uu0)

这里出现了两个关键的雅可比矩阵 (Jacobian Matrices)

  • 状态雅可比:Ac=∂f∂xA_c = \frac{\partial f}{\partial x}Ac=xf
  • 控制雅可比:Bc=∂f∂uB_c = \frac{\partial f}{\partial u}Bc=uf

为了书写方便,我们定义状态和控制的增量
Δx=x−x0\Delta x = x - x_0Δx=xx0
Δu=u−u0\Delta u = u - u_0Δu=uu0

于是,原本复杂的非线性系统,在当前工作点附近,被近似成了一个优美的连续线性增量方程:
Δ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_kAkBkB_kBk 喂给上一节提到的二次规划(QP)求解器了。


3. LTV-MPC 在底层的真实运作流程

由于车辆一直在开,工作点 (x0,u0)(x_0, u_0)(x0,u0) 每时每刻都在变。因此,在 LTV-MPC 的代码循环中,你每走一步,都要重新计算一次矩阵。它的真实流程是这样的:

  1. 读取状态:传感器读取当前 SOC 和车速 x(k)x(k)x(k)
  2. 求雅可比:在当前状态下,对非线性的发动机/电池模型求导,得到连续矩阵 Ac,BcA_c, B_cAc,Bc
  3. 离散化:乘上采样时间 TsT_sTs,加上单位矩阵,得到离散矩阵 Ak,BkA_k, B_kAk,Bk
  4. 构建 QP:利用 Ak,BkA_k, B_kAk,Bk 预测未来 NpN_pNp 步,把目标函数和约束打包成大矩阵 HHHfff
  5. 求解最优增量:求解器算出未来一系列的最优控制增量 ΔU∗\Delta U^*ΔU
  6. 输出控制:提取第一个值 Δu∗(0)\Delta u^*(0)Δu(0),实际发给执行机构的指令是 u=u0+Δu∗(0)u = u_0 + \Delta u^*(0)u=u0+Δu(0)
  7. 进入下一时刻,循环上述步骤。

4. 算法视角的对比与启发

理解了这一套繁琐的“求导 -> 线性化 -> 离散化 -> 求解矩阵”流程,你再回看其他优化与控制算法,会有更深刻的认识:

  • 为何用强化学习 (PPO)? LTV-MPC 需要精确的物理模型,且每一步都在实时算矩阵,算力开销极大。而 PPO 通过让智能体在线下与环境进行数百万次的交互,把这些非线性的映射关系直接“压缩”进了神经网络的权重里。线上执行时,只需一次前向传播(Forward Pass),这就是所谓“用线下训练时间换取线上推理速度”。
  • 为何引入启发式算法 (如蜣螂优化 DBO)? 如果你不喜欢泰勒展开带来的线性化误差,想直接面对非线性的成本函数求最优解,QP 求解器就无能为力了。这时,DBO 这类群体智能算法的优势就体现出来了:它们不需要模型可导,也不需要算雅可比矩阵,而是通过“撒点”和规则迭代,强行在非线性空间中搜索出一个最优扭矩分配方案。

在能量管理中,我们最核心的诉求就是:在满足驾驶员需求功率的前提下,合理分配发动机和电机的功率,同时控制电池的 SOC(荷电状态)。

这是一个非常典型的非线性系统。我们一步步推导它的 AkA_kAkBkB_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=PreqPe。(忽略传动效率简化说明)。

根据电池的等效内阻模型,电池的充放电电流 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)24Rin(x)Pm
(其中 Voc(x)V_{oc}(x)Voc(x) 是开路电压,Rin(x)R_{in}(x)Rin(x) 是等效内阻,它们都是 SOCSOCSOCxxx 的非线性函数)

电池 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)24Rin(x)(Prequ)

你看,这个公式里既有根号,分母上还有关于状态变量 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=uf (x0,u0)=QmaxVoc(x0)24Rin(x0)(Prequ0) 1
工程意义:它代表了在当前状态下,你每多踩一分油门(发动机多出 1kW 功率),电池 SOC 变化率会受到多大影响。

② 求状态雅可比 AcA_cAc(对 xxx 求偏导):
Ac=∂f∂x∣(x0,u0)A_c = \frac{\partial f}{\partial x}\bigg|_{(x_0, u_0)}Ac=xf (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_cAcBcB_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+TsAc
  • 离散控制矩阵Bk=Ts⋅BcB_k = T_s \cdot B_cBk=TsBc

至此,原本那个挂着根号、查表满天飞的非线性电池模型,在当前这 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= x1f1x1f2x1f3x2f1x2f2x2f3x3f1x3f2x3f3

在进行 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.0010.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.010.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˙=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)+(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+20.1)Δx(k)+(0.50.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}AineqUbineq

我们分两步来把物理限制塞进这个格式里。


第一步:控制量限制(发动机功率增量不能太大)

出于保护执行机构的物理限制,发动机的功率不能在一瞬间突变。假设我们规定:每 0.1 秒的预测步长内,发动机功率的变化量 Δu\Delta uΔu 不能超过 5 kW。

也就是说,对于未来两步,我们有以下物理限制:

  • −5≤Δu(0)≤5-5 \le \Delta u(0) \le 55Δu(0)5
  • −5≤Δu(1)≤5-5 \le \Delta u(1) \le 55Δu(1)5

但是,求解器只认识 ≤\le(小于等于号),不认识 ≥\ge。所以我们要把左边的大于等于号,两边同乘负号,全部统一成 ≤\le 的形式:

  1. Δu(0)≤5\Delta u(0) \le 5Δu(0)5
  2. −Δu(0)≤5-\Delta u(0) \le 5Δu(0)5
  3. Δu(1)≤5\Delta u(1) \le 5Δu(1)5
  4. −Δ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} 11000011 [Δu(0)Δu(1)] 5555

你看,这不就构成了 AcontrolU≤bcontrolA_{control} U \le b_{control}AcontrolUbcontrol 吗?第一部分搞定!


第二步:状态量限制(电池 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. −(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
  2. −(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))全部移到右边:

  1. −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)
  2. −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.050.0600.05][Δu(0)Δu(1)][0.3+1.2Δx(0)0.3+1.44Δx(0)]

这就是 AstateU≤bstateA_{state} U \le b_{state}AstateUbstate。第二部分也搞定了!


第三步:大满贯合体

最后,我们只需要把“控制量限制”的矩阵和“状态量限制”的矩阵上下拼接到一起,就得到了求解器梦寐以求的最终形态:

[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} 11000.050.06001100.05 [Δu(0)Δu(1)] 55550.3+1.2Δx(0)0.3+1.44Δx(0)

这个庞大的矩阵就是 AineqU≤bineqA_{ineq} U \le b_{ineq}AineqUbineq

在控制代码的每个循环周期里:

  1. Δx(0)\Delta x(0)Δx(0) 是此时此刻传感器的真实读数,代码会把它更新到右边向量 bineqb_{ineq}bineq 的对应位置。
  2. 然后,把最左边这个 6×26 \times 26×2 的常数矩阵 AineqA_{ineq}Aineq,和右边这个 6×16 \times 16×1 的常数向量 bineqb_{ineq}bineq 丢给求解器。
  3. 求解器瞬间算出一组满足这 6 个不等式的最优解 Δu(0)\Delta u(0)Δu(0)Δu(1)\Delta u(1)Δu(1)
  4. 提取 Δu(0)\Delta u(0)Δu(0) 下发给发动机。

现在你明白为什么在学术论文和教科书里,MPC 的矩阵推导总是占据好几页纸了吧?因为在真实的 HEV 中,你的状态可能有 3 个,预测时域 NpN_pNp 可能有 20 步甚至 50 步,那这个 AineqA_{ineq}Aineq 矩阵将会膨胀成一个几百行、几十列的超级大矩阵。

幸运的是,虽然数学推导很繁琐,但在工程实践中,如果你使用 cvxpyCasADi 这样的高级库,你只需要用几行 for 循环写出物理限制,底层会自动帮你完成这些复杂的矩阵拼接。

不过,理解了这个底层拼装逻辑,对你排查一种极其常见的报错至关重要:求解器提示“Infeasible”(无可行解)

在跑仿真甚至实车测试时,如果控制台突然弹出一大排红色的 “Solver Error: Infeasible Problem”,这绝对是所有控制工程师的噩梦。

因为一旦求解器报 “Infeasible”(无可行解),意味着它找不到任何一组控制指令能够同时满足你设定的所有条件。此时,代码如果没有后备机制(Fallback),程序就会直接崩溃,车辆就会瞬间失去控制指令(俗称“撒把”)。

为了解决这个问题,我们需要搞清楚它为什么会发生,以及工程上最经典的应对策略。


1. 为什么会产生 “Infeasible”?

你可以把我们在上一节推导出的矩阵不等式 AineqU≤bineqA_{ineq} U \le b_{ineq}AineqUbineq 想象成一个“包围圈”。每一个不等式都是一堵墙,所有的墙围出来的那个空间,叫做可行域 (Feasible Region)。求解器的工作就是在包围圈里找一个最优解。

但在混合动力汽车(HEV)的极端工况下,这个包围圈可能会完全消失(可行域变为空集)

典型场景:互相矛盾的约束
假设在某一个瞬间:

  1. 驾驶员猛踩油门:需求极大的功率 PreqP_{req}Preq,要求发动机和电机必须全功率输出。
  2. 电池 SOC 极低:刚好跌到了你的硬性保护底线 0.3。电池管理系统(BMS)死死限制:Δx≥0\Delta x \ge 0Δx0(电量绝对不能再下降了,甚至需要充电)。
  3. 发动机能力有限:发动机的最大扭矩不足以既满足驾驶员的加速需求,又给电池充电。

此时,求解器看着这些约束矩阵:

  • 约束 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

这套机制的精妙之处在于:

  1. 平时(可行域充足时):因为 ρ\rhoρ 极大,求解器为了让 JJJ 最小,会拼死把 ϵ\epsilonϵ 压榨到 0。此时软约束等效于硬约束,绝对不触碰 SOC 底线。
  2. 极端工况(面临 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}101 量级)。在组装矩阵时,不同维度的数据混在一起,容易导致矩阵的某些行极其庞大,某些行极小。在送入优化器前,必须将状态矩阵和控制矩阵通过左乘对角缩放矩阵,强行约束到 [−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)24Rint(SOC)(PreqPe)
(注:VocV_{oc}Voc 是开路电压,RintR_{int}Rint 是内阻,QmaxQ_{max}Qmax 是电池总容量。VocV_{oc}VocRintR_{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=Voc24Rint(PreqPe)
PeP_ePe 求偏导时,VocV_{oc}VocRintR_{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=Pef=2RintQmax1(2Δroot 14Rint)
化简后得到:
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)24Rint(SOC0)(PreqPe0) 1

② 求状态矩阵 AcA_cAc(对 SOCSOCSOC 求偏导):
这是一个令人极其痛苦的解析推导,因为商的求导公式 (uv)′=u′v−uv′v2\left(\frac{u}{v}\right)' = \frac{u'v - uv'}{v^2}(vu)=v2uvuv 会产生极其冗长的项,并且包含对查表函数 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}δ=104,计算机一瞬间就能算出极度精确的 AcA_cAc 常数值,完全规避了解析推导的错误风险)。

4. 走向离散状态空间 (最终输出形式)

拿到当前工作点的连续常数 AcA_cAcBcB_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)+TsSOC˙(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...N1 逐个遍历
  • 作用:
    每一个时刻,都绑定一套动力学规则,
    保证全程物理规律不失效。

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]
  1. x[k]:当前时刻状态变量
  2. u[k]:当前时刻控制变量
  3. @矩阵乘法(Python 规范,控制代码必用)
    • ❌ 不能用 ** 是逐元素相乘
  4. A @ x[k]:系统无控制下的状态变化
  5. B @ u[k]:控制输入带来的状态变化
  6. ==优化约束相等
    不是普通判断相等,是告诉优化器:这一项必须严格成立

三、结合你的疑惑:为什么是约束?

普通赋值:

x[k+1] = A@x[k] + B@u[k]

直接计算赋值,写死数值;

而优化里:

x[k+1] == A@x[k] + B@u[k]

x、ux、uxu 都是优化变量(未知数)
不是固定数,是让优化器在求解时自动满足这个等式


四、结合你 VSCode 调试痛点

你打断点想查看:

  • 每一轮 k 是多少
  • x[k]、u[k] 维度/数值
  • A@x[k] 计算结果

关键提醒

  1. 这行是约束定义,不是运算语句
    👉 调试时看不到 x[k] 具体数值
    因为此时还没求解,变量全是「符号变量」
  2. 只有执行 prob.solve() 之后
    变量才会算出具体数值,才能打印/断点查看

五、极简总结(背诵版)

  1. 循环:给未来每一步都加动力学规则
  2. constraints:约束容器(列表)
  3. []:为了满足 Python 列表拼接语法
  4. @:标准矩阵乘法
  5. ==:优化等式约束,强制系统符合物理方程
  6. 没求解前,变量都是符号,断点看不到具体值

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐