1. 为什么PHEV能量管理要用凸优化MPC:一个需要实时求解的最优控制问题
做插电式混合动力车辆(PHEV)能源管理的人,最后基本都会遇到同一个瓶颈:全局最优算法算得慢,在线策略又不够省油。我之前一直用动态规划(DP)做离线基准,结果确实漂亮,但 DP 需要完整工况,实车控制器根本背不动。后来我把模型预测控制(MPC)框架落到凸优化求解上,在 Matlab 里用 CVX 搭了一个完整的滚动时域闭环,中间还用 ADMM(交替方向乘子法)对求解器做了加速拆分。这篇文章就是那次完整过程的记录,从整车建模到凸形式转换,再到 ADMM 和 CVX 的配合方式,最后是仿真里那些踩过的坑,希望能给同样在这个方向折腾的研究生和工程师一点参考。
PHEV 的能源管理,本质上是一个带状态约束的有限时域最优控制问题。不管你用串联、并联还是混联拓扑,发动机和电机必须共同满足驾驶员的驱动功率需求 P_req,同时电池 SOC 要维持在合理区间。所谓能量管理策略,说穿了就是决定每一时刻的发动机功率 P_ice 和电机功率 P_mot 怎么分。这个分配直接关系燃油消耗、电量维持和驾驶平顺性。
传统做法大多是查表规则或者等效燃油消耗最小策略(ECMS),优点是计算量小,缺点是跟全局最优差了一截,一般差距能到 10% 到 20%。动态规划虽然能做到全局最优,但需要提前预知完整驾驶循环,而且状态网格越细,计算复杂度越高,基本只能用来离线评价其他策略的上限。MPC 的做法则是把全局问题切成有限时域,每次只求解未来 N 步内的最优控制序列,然后只执行第一步,下个采样周期滚动重来。这种思路在理论上能逼近动态规划,工程上又有实时运行的潜力,所以成了 PHEV 能量管理研究里的主流方向之一。
但 MPC 有一个绕不开的问题:每一步都需要在线求解一个优化问题,而 PHEV 动力系统的非线性主要来自发动机油耗 Map、电机效率和电池内阻。直接用非线性求解器求解,实时性很难看;强行线性化,又得处理分段函数和查表,推导和调试都很痛苦。凸优化的思路恰恰能在这时候起作用——把目标函数和约束都写成凸形式,既能保证全局最优,又能用现成的高效求解器,把单步求解时间从秒级压到百毫秒级甚至更低。
那为什么还要把 ADMM 扯进来呢?很多论文里 MPC 直接调 CVX 就能出结果,这在离线研究没问题。但如果要往快速原型或半实物仿真走,每一步都调用通用内点法求解器还是会有点紧张。ADMM 的一大优势是能把一个大问题拆成若干小问题,每个小问题结构简单,可以解析求解或只用很小的矩阵计算,这让凸 MPC 更接近实时。CVX 负责把模型和约束清晰地表达出来,保证建模正确;ADMM 负责分解和加速。两者结合起来,既省去手写求导和 KKT 条件的麻烦,又能承接 ADMM 的分解逻辑,对于做算法验证的人是很顺手的一条路。
需要注意,ADMM 不是银弹。它对惩罚参数 ρ 比较敏感,收敛判据也要仔细设计。我后面专门有一节讲这些实际调参的经验,这里先不铺开。先把问题定性和凸建模讲清楚,不然代码根本写不出来。
1.1 能量管理问题的本质:功率分配与 SOC 约束的耦合
先说最核心的物理图景。以并联 PHEV 为例,发动机和电机都能驱动车轮,发动机的输出功率 P_ice 和电机的输出功率 P_mot 之间有一个恒等关系:
P_req(t) = P_ice(t) + P_mot(t)
这里所有功率都指机械功率。如果驾驶员需求功率确定,那么控制器真正需要决定的其实只有一个自由度——另一个变量由等式约束直接消掉。这个自由度选谁都可以,一般选发动机功率,电机功率则由需求功率减去发动机功率得到。
电池 SOC 是唯一的状态量,它的动态方程可以写成离散时间形式:
SOC(k+1) = SOC(k) - P_bat(k) * T / (Q_bat * V_oc * 3600)
其中 P_bat 是电池放电功率,Q_bat 是电池容量,V_oc 是开路电压,T 是采样时间。注意当电机处于发电模式时 P_mot 是负值,对应的电池充电功率也是负值,SOC 会增加。这就在在时间维度上把前后两个时刻的状态联系起来了,也把功率分配和 SOC 约束牢牢绑定在一起——你不可能这这一步把电全放光,因为下一步还要继续开车。
所以整个优化问题的骨架是:给定未来 Np 步的功率需求序列,求解 Np 步的发动机功率和电机功率序列,使得燃油消耗和 SOC 偏离参考值的惩罚最小,同时满足发动机功率上下限、电机功率上下限以及 SOC 上下限约束。这个框架本身很简单,难点全在如何把发动机油耗、电机效率这些非线性物理特性写成凸形式。
1.2 凸优化与实时性的折衷:为什么要突出 ADMM 和 CVX
对于科研来说,直接用 CVX 解完整问题完全够用。CVX 是 Matlab 下的凸优化建模工具,你不需要手写求解器,只需要声明变量、约束、目标函数,它会自动转换成标准型交给底层求解器(默认 SDPT3,也可以换 Mosek 或 Gurobi)。这种建模方式最大的好处是快——快在开发速度和可读性上,你可以在十几分钟内改好一个模型版本,跑仿真看趋势。
但 CVX 毕竟是一套高层封装,它在每个 MPC 采样周期都做一次完整的符号解析和内部转换,这在快速原型阶段还可以忍受,离嵌入式实时控制还有距离。ADMM 的价值就在这里:它把原问题拆成多个块,第一个人只负责发动机子问题,第二个人只负责电机子问题,两个子问题之间只通过一个线性等式约束耦合。由于每个子问题的结构非常清晰,第二类甚至可以直接通过解析投影公式求解,第一类如果也没有复杂约束,同样可以用解析的方法。这样算法出来后,可以一步步手写成底层的 C 代码,脱离 CVX 也能跑。
我用 CVX 加 ADMM 的混合模式,本质上是在不确定子问题解析表达式的阶段,先借用 CVX 验证建模的正确性,同时让 ADMM 框架先把问题拆解开。等到代码验证无误后,再逐步把 CVX 的子问题替换成手写求解器。对做研究的人来说,这是一条比较平滑的路线:先用高层工具确认理论没问题,再考虑性能优化。
2. 从整车到凸模型:怎么把功率分配、SOC 动态都写成 CVX 能吃的形式
建模这一步是整篇文章最关键的,也是最容易不耐烦的地方。很多人上来就想把发动机高度非线性 Map 塞进优化器,结果 CVX 报错或者 ADMM 不收敛。我自己的经验是,建模时就需要时刻问一个问题:这个表达式是凸的吗?如果回答不了,就先用更简单的表达式近似,把框架跑通再逐步精细化。
2.1 并联 PHEV 动力总成拓扑的模型简化
并联 PHEV 的拓扑可以画成一条简单的功率流:发动机直连车轮,电机通过齿轮箱或者减速器连接到输出轴,中间有离合器决定发动机是否参与驱动。这种拓扑下,驱动需求功率被发动机和电机直接分担,结构最简单,也最适合作为 ADMM 的测试对象。
动力总成的静态模型包含三块。第一块是发动机,输出机械功率满足上下限约束:
P_ice_min ≤ P_ice ≤ P_ice_max
P_ice_min 表示怠速时的最小功率,不是 0,这对优化结果有实际影响:如果 P_ice_min 设成 0,优化器可能会让发动机很频繁地在 0 和正功率之间切换,实际中这既不可行也不省油。第二块是电机,能工作在电动和发电两个象限:
-P_mot_max ≤ P_mot ≤ P_mot_max
第三块是电池,它的端功率和电机机械功率之间隔着一个电机效率 η_mot。如果忽略动态过程,可以写成:
P_bat = P_mot / η_mot,P_mot ≥ 0(电动机模式) P_bat = P_mot * η_mot,P_mot < 0(发电机模式)
这种分段的效率模型会让问题不凸,但可以通过把 η_mot 设为常数来凸化。更进一步,可以把电机损耗写成 P_mot 的二次凸函数,从而得到单调映射。我在前期仿真中直接取 η_mot = 0.9,足够用来验证 MPC 和 ADMM 的框架,后期再替换成更精细的效率和损耗模型。
2.2 电池 SOC 动态方程与损耗项的凸处理
电池模型我用最简单的等效电路:一个恒定的开路电压 V_oc,一个内阻 R_bat。这样做的好处是 SOC 动态方程里,电池功率 P_bat 和 SOC 的关系是线性的,天然凸。如果内阻随 SOC 变化,我通常直接取工作点附近的内阻值,反正能量管理策略更看重 SOC 轨迹和能耗趋势,而不是电池里微小的非线性。
目标函数是油电权衡的核心。发动机燃油消耗率在常见工况下是一条 U 形曲线,可以用二次凸函数拟合:
f_fuel(P_ice) = a * P_ice^2 + b * P_ice + c
其中 a > 0 保证凸性。拟合方法很简单,用最小二乘扫一遍发动机油耗 Map 上的数据点就行。对于电池电量消耗,我不直接用电池功率作目标,而是把 SOC 偏离参考值的平方放进目标函数:
cost_soc = w_soc * sum_square(SOC(k+1:Np+1) - SOC_ref)
这样 MPC 会温和地让 SOC 保持在参考值附近,而不是粗暴地限制电池用完。最终的优化问题就是一个凸二次规划:二次目标、线性等式约束、盒式约束。这种问题 CVX 描述起来非常自然,ADMM 的收敛性也有保证。
这里有一个容易被忽视的点:SOC 的上下限一定要在约束里留出余量。不是直接把 SOC 限制在 0.3 到 0.8,而是把上限设为略小于物理上限,下限略大于物理下限,比如 0.35 到 0.75。否则因为预测误差和模型失配,MPC 很容易请求一个超出可行域的功率序列,导致 CVX 返回不可行。
3. ADMM 核心迭代:为什么它能比直接 CVX 更快,以及如何映射 PHEV 问题
ADMM 的理论并不难,难的是把 PHEV 问题映射进去。很多人看教材上一堆公式,不知道该怎么把发动机功率和 SOC 动态塞进 x、z、λ 的更新里。我这里用比较直白的语言把对应关系讲清楚。
3.1 从增广拉格朗日开始:x、z 分离的对偶结构
ADMM 求解的标准问题形式是:
min f(x) + g(z) s.t. A x + B z = c
求 f 和 g 都是凸函数。对于 PHEV 的 MPC 问题,我可以把 x 定义为发动机功率序列 P_ice 以及 SOC 序列,把 z 定义为电机功率序列 P_mot,约束就是每个时刻的功率平衡:
P_ice(k) + P_mot(k) = P_req(k)
写成向量形式就是 A = [I, 0?],B = I,c = P_req。但注意 SOC 动态方程中也含有 P_mot,需要通过 P_bat 与 P_mot 之间的线性或凸映射关系把 SOC 更新纳入 x 子问题。为了保持 ADMM 的拆分清晰,我会把 SOC 的线性等式放进 x-update 的约束里,z 只保留电机功率的盒状约束。
构造增广拉格朗日:
L_rho(x, z, λ) = f(x) + g(z) + λ^T (A x + B z - c) + (ρ/2) ||A x + B z - c||^2
然后在每次迭代中交替求解:
x^{k+1} = argmin_x L_rho(x, z^k, λ^k) z^{k+1} = argmin_z L_rho(x^{k+1}, z, λ^k) λ^{k+1} = λ^k + ρ (A x^{k+1} + B z^{k+1} - c)
这就是 ADMM 的三步迭代。和直接求原问题的 KKT 条件相比,它的好处是 x、z 每次迭代只需要各自解决一个规模更小的子问题。在 PHEV 问题里,发动机功率子问题只需要处理带上下限的二次目标,电机功率子问题甚至可以通过对二次函数求导截断投影得到解析解。
3.2 三步迭代在 MPC 里的具体形态
我把 MPC 每一步的优化变量都写成序列形式,这样子在滚动时域里不需要频繁重排数据结构。给定当前 SOC 和未来 Np 步的需求功率,x-update 的 CVX 片段大致是:
cvx_begin quiet variable Pi(Np) variable S(Np+1) S(1) == SOC_cur; S(2:end) == S(1:end-1) - alpha_vec .* Pm; % Pm 来自 z 的当前值 Pi >= P_ice_min; Pi <= P_ice_max; minimize( w_fuel * sum( a*Pi.^2 + b*Pi + c ) ... + w_soc * sum_square( S(2:end) - SOC_ref ) ... + lambda' * (Pi + Pm - P_req) ... + rho/2 * sum_square( Pi + Pm - P_req ) ) cvx_endz-update 则可以直接解一个简单的投影问题。因为目标函数里 g(P_mot) 是 P_mot 的二次函数,约束只有上下限,所以先求无约束最优解再投影到区间内:
P_mot_unconstrained = ... % 由 P_req、Pi、lambda、rho 和 SOC 目标项共同决定 P_mot_new = min(max(P_mot_unconstrained, -P_mot_max), P_mot_max)
最后更新 λ:
lambda = lambda + rho * (Pi + P_mot_new - P_req)
这里一个关键细节是:SOC 目标项同时出现在 x 和 z 的更新中,会影响 P_mot 的解析解,所以不能简单套用教科书里的投影公式,需要把所有含有 P_mot 的项展开求导。这也是用 CVX 跑 x-update 的原因——至少有一条可信的路径能验证最终结果。
3.3 为什么一定是凸的:非凸问题 ADMM 会怎样
ADMM 本身并不强制要求 f 和 g 都是凸函数,它也能迭代,但收敛到全局最优需要凸性保证。如果发动机油耗拟合出来的二次项是负数,或者电机损耗模型搞出一个非凸区间,ADMM 的迭代就会开始乱跳:原始残差减小但目标函数不下降,或者 λ 变得异常大然后震荡。我刚开始测试时,把发动机油耗用一次线性函数近似,虽然也是凸的,但结果偏向纯电策略,油耗优化效果不好;后来换成二次凸函数,ADMM 收敛速度反而没什么变化,但策略合理多了。
所以,真正花时间建模并不是浪费时间。模型的凸性一旦破坏,后面所有调 ρ、调权重的工作都白做。一个简单自检方法:把目标函数的一阶和二阶导数符号在可行域内扫一遍,确认二次项系数正、约束矩阵正定。如果复杂模型确实没法保证凸性,就退回分段线性或二次拟合。
4. Matlab 代码实现:CVX建模、MPC滚动时域与ADMM求解器骨架
理论说得再多,最终要落地到代码。这一节直接给骨架,代码风格偏向科研验证,重点是把 MPC 和 ADMM 的结构展示清楚。你会看到 CVX 被用在 x-update 里,这不是必须的,但非常适合做二次验证。
4.1 CVX 安装与许可证问题:一个绕不开的第一课
CVX 的安装本身很简单:从官方下载对应 Matlab 版本的压缩包,解压到纯英文路径,然后在 Matlab 里切换到该目录,运行 cvx_setup。它会自动设置路径、检测求解器并申请许可证。学术用户需要免费申请许可文件,填学校邮箱几分钟就能收到 license.dat,放到 CVX 目录下重新运行 cvx_setup 即可。
很多同学卡在启动失败或者许可证验证不过,原因往往是:放在中文路径下、Matlab 版本过新导致 CVX 版本兼容性问题、或者没有以管理员权限运行 Matlab。如果遇到 MathWorks 许可证报错,优先检查 Matlab 本身授权,CVX 的学术许可通常不会和 Matlab 冲突。另一个常见坑是 CVX 自带的 SDPT3 求解器在某些问题上数值表现一般,我建议有条件的话在 cvx_setup 后挂上 Mosek 或 Gurobi 求解器——PHEV 的二次规划问题不算大,Gurobi 内点法往往十几毫秒就能出解。
4.2 滚动时域优化主循环:从数据生成到 CVX 调用
下面是主循环骨架,简化了不少细节但结构完整:
% PHEV 凸 MPC 主循环(ADMM + CVX 混合) clear; clc; % 参数定义 T = 1; % 采样时间 [s] Np = 20; % 预测时域 T_sim = 300; % 仿真时长 [s] Q_bat = 40; % 电池容量 [Ah] V_oc = 320; % 开路电压 [V] SOC_ref = 0.6; % SOC 参考值 P_ice_min = 5; % 发动机最小功率 [kW] P_ice_max = 60; % 发动机最大功率 [kW] P_mot_max = 40; % 电机最大功率 [kW] SOC_lo = 0.3; SOC_hi = 0.8; % SOC 硬边界 x0 = 0.65; % 初始 SOC % 需求功率序列,可由 WLTC 或 NEDC 等工况换算 % 这里直接从一个 mat 文件加载,格式为列向量 load('P_req_wltc.mat'); P_req_all = P_req_wltc(1:T_sim+Np); % 权重 w_fuel = 5; w_soc = 20; % ADMM 参数 rho = 3; max_iter = 30; tol = 1e-3; % 仿真状态记录 SOC_sim = zeros(T_sim+1, 1); SOC_sim(1) = x0; for k = 1:T_sim P_req = P_req_all(k:k+Np-1); % 调用 ADMM+CVX 混合求解器 [P_ice_seq, SOC_seq] = solve_mpc_admm_cvx(... P_req, SOC_sim(k), Np, T, Q_bat, V_oc, SOC_ref, ... P_ice_min, P_ice_max, P_mot_max, ... w_fuel, w_soc, rho, max_iter, tol); % 只执行第一个控制量 P_ice_applied = P_ice_seq(1); P_mot_applied = P_req(1) - P_ice_applied; % 仿真被控对象,电机效率 0.9 eta_mot = 0.9; P_bat = P_mot_applied / eta_mot; SOC_sim(k+1) = SOC_sim(k) - P_bat * T / (Q_bat * V_oc * 3600); SOC_sim(k+1) = max(SOC_lo, min(SOC_hi, SOC_sim(k+1))); end主循环里用了 1 秒的采样时间,Np 取 20,也就是预测未来 20 秒。PHEV 能量管理的预测时域不需要太长,因为发动机和电机动态相对慢,20 到 40 秒已经足够看到 SOC 的变化趋势。太长反而会让 CVX 的问题规模变大,ADMM 迭代变慢。
4.3 ADMM 求解器函数:x-update 用 CVX、z-update 用解析投影
下面是核心函数的骨架,重点展示 ADMM 每一步怎么和 CVX 配合:
function [P_ice_seq, SOC_seq] = solve_mpc_admm_cvx(... P_req, SOC_cur, Np, T, Q_bat, V_oc, SOC_ref, ... P_ice_min, P_ice_max, P_mot_max, ... w_fuel, w_soc, rho, max_iter, tol) % 初始化 Pi = zeros(Np, 1); Pm = zeros(Np, 1); lambda = zeros(Np, 1); alpha_vec = T / (Q_bat * V_oc * 3600); % SOC 更新系数 for iter = 1:max_iter % ---- x-update:CVX 求解发动机功率和 SOC 序列 ---- cvx_begin quiet variable Pi_cvx(Np) variable S(Np+1) S(1) == SOC_cur; % SOC 动态:S(k+1) = S(k) - alpha_vec*P_bat_k % 简化处理:P_bat = Pm / eta_mot,这里把 Pm 作为常数传入 S(2:end) == S(1:end-1) - alpha_vec .* (Pm / 0.9); Pi_cvx >= P_ice_min; Pi_cvx <= P_ice_max; minimize( w_fuel * sum( 0.02*Pi_cvx.^2 + 0.8*Pi_cvx + 3 ) ... + w_soc * sum_square( S(2:end) - SOC_ref ) ... + lambda' * (Pi_cvx + Pm - P_req) ... + (rho/2) * sum_square( Pi_cvx + Pm - P_req ) ) cvx_end Pi = Pi_cvx; % ---- z-update:解析投影 ---- % 将 z 的目标展开对 Pm 求导,得到无约束最优解 % 完整推导需要代入 SOC 目标项和增广项,这里示意关键部分 Pm_unc = P_req - Pi - (1/rho) * lambda - alpha_vec .* ( ... ); Pm_new = min(max(Pm_unc, -P_mot_max), P_mot_max); % ---- dual update ---- lambda = lambda + rho * (Pi + Pm_new - P_req); % ---- 收敛判断 ---- if norm(Pi + Pm_new - P_req, inf) < tol break; end end P_ice_seq = Pi; SOC_seq = S; end这个函数在 x-update 里目标函数持有 CVX,求解器给出的 S 包含了未来 SOC 轨迹。z-update 里我省略了完整解析式,因为实际写的时候还要处理 SOC 目标项对 Pm 的梯度,但不复杂。核心思想是:每个 ADMM 迭代只需要求解一个很小的凸二次规划,以及一个截断投影,单次迭代时间在毫秒级。比如 Np=20 时,x-update 只有 40 个变量,CVX 求解时间大约 10 到 30 毫秒;30 次迭代大约 0.3 到 1 秒。对离线仿真来说完全够用,如果想实时化,后续需要把 x-update 也改成解析式。
4.4 怎么验证 MPC 闭环:分层对比,不只看油耗数字
代码跑完后,第一件事不是看油耗多少,而是做开环验证。选一个固定工况,比如 WLTC 前 300 秒,给定初始 SOC,用 CVX 一次性求解整个时域的最优功率分配(相当于离线 MPC 一步到位),记为全局参考解。然后再用 ADMM 迭代多次逼近这个解,看看最终目标函数值和功率序列是不是一致。如果差异在 1% 以内,说明 ADMM 的求解精度没问题。
第二步才是闭环验证。把 ADMM 求解器放进滚动时域,对比 SOC 轨迹和总油耗。这里我习惯画一张图,把需求功率、发动机功率、电机功率、SOC 四条曲线叠在一起。重点观察功率切换是否频繁、发动机是否在很低的效率点运行,以及 SOC 是否能稳定围绕参考值。有时候油耗数字差不多,但功率分配连续性差,说明目标函数里的平滑项或者预测时域设置有问题。
5. 实测中的坑:权重、ρ、数值稳定性,一个个说
这一节是我最想写的部分。论文里很少写这些,但实际跑仿真时十个问题里有八个出在这些地方。
5.1 权重 w_fuel 和 w_soc:并不是越大越好
目标函数里 w_fuel 和 w_soc 的比例决定了策略倾向。w_fuel 太小,MPC 会疯狂用电,SOC 一路下探;w_fuel 太大,发动机会长时间在高功率点工作,SOC 被充到上限,这也不符合插电混动“先用电,再用油”的原则。比较合理的做法是设定一个等效因子,先按电量消耗和燃油消耗的等效关系给一个初始比值,然后微调。我在 WLTC 循环上测试,w_fuel 和 w_soc 从 5 和 20 这个比例开始,SOC 能平缓维持在 0.6 附近。如果 SOC 冲过 0.7,说明 w_soc 太小,需要增加;反过来如果 SOC 长期在 0.55 以下,可能是 w_soc 太大,电池被过度保护。
另外一个技巧是:不要把 SOC_ref 设成固定值,可以按当前 SOC 做目标。比如当前 SOC 高,就让 SOC_ref 稍高一点,鼓励优先用电;当前 SOC 低,就稍微调低参考值,避免发动机为强制充电而消耗额外燃油。这有点类似 ECMS 里的等效因子自适应,但实现起来不难,只需在每一采样周期更新 SOC_ref。
5.2 ADMM 参数 ρ:收敛速度的天平
ρ 是 ADMM 里最敏感的参数。ρ 太小,原始残差收敛很慢,需要更多迭代;ρ 太大,对偶残差收敛慢,而且 λ 容易震荡。我测试的 Np=20 并联 PHEV 问题,ρ 在 1 到 10 之间都能收敛,但迭代次数差别很大。一个可复用的经验是:先用固定 ρ=2 跑一遍完整工况,画出原始残差和对偶残差的对数曲线。
如果原始残差下降很慢,说明 ρ 偏小,把它调成 5 或 8;如果对偶残差震荡,说明 ρ 偏大,调回 0.5 或 1。更高级的做法是残差平衡自适应:当原始残差大于若干倍对偶残差时增大 ρ,反之减小 ρ。但这个自适应策略在循环里跑起来容易过度耦合,我建议先固定值摸清楚问题结构,再引入自适应。
5.3 CVX 数值问题:约束冲突和求解失败
我在调试中最常见的问题是 CVX 返回“Infeasible problem”。原因往往不是模型逻辑错,而是数值尺度太糟糕。比如电池容量单位用 Ah,功率单位用 kW,SOC 是 0-1 的小数,三个量在同一等式里就出现跨数量级的系数。CVX 的内点法对尺度敏感,我处理的办法是把功率统一成 kW,SOC 保持 0-1,但等式系数乘以 1e-4 之类的小量,或者直接对变量做归一化处理。
另一个坑是 SOC 动态约束和目标函数里同时存在 SOC 变量和功率变量,如果 Np 取很大,预测末端 SOC 容易超出边界导致不可行。解决办法是缩短 Np,或者给 SOC 边界加松弛变量,在目标函数里用大的惩罚系数。松弛变量在 CVX 里写起来很简单,比如:
variable slack_nonneg(Np+1) >= 0 S(2:end) >= SOC_lo - slack_nonneg;不过松弛变量会轻微改变原问题特性,使用时要关注最终 SOC 轨迹。
5.4 发动机油耗 Map 的拟合:别忽视二次项
我见过很多人图省事,把发动机油耗当成常数或一次函数。这样做目标函数从 QP 退化成 LP,优化器会倾向于把功率全部给某一个动力源,因为线性目标的最优解一定在边界上。实际发动机油耗率是 U 形曲线,最优输出点在中段,所以二次项系数 a 必须大于 0,而且拟合时要保证整个功率范围内 f(P_ice) 非负且凸。如果你用几个测试点做了最小二乘,拟合结果出现轻微凹区间,需要手动把 a 提高或者去掉异常点重新拟合。这个系数哪怕有一点偏离,MPC 在功率分配上就会明显跑偏。
6. 往后走:从 CVX 验证到嵌入式部署的研究路径
当 CVX + ADMM 的仿真闭环跑通后,下一步通常会考虑怎么把它往实时系统推进。这一节可能对纯粹做离线优化的人用不上,但对打算发论文或者做工程落地的人,算是很实用的路线参考。
6.1 从 CVX 原型到嵌入式算法:解析算子更值得投入
CVX 在仿真里是神器,但它的符号计算和通用内点在嵌入式环境里几乎不可能跑。ADMM 的分解优势在于,每个子问题结构非常固定,可以推导出纯向量化的解析更新公式。一旦你把 x-update 也改成解析式,整个算法就变成了几个循环内的矩阵乘法和截断操作,完全可以用 C 语言裸写,甚至移植到单片机里。
我在实践中推荐分三步走。第一步,用 CVX 验证凸模型正确;第二步,在 Matlab 里手写 ADMM 的解析子问题,替换掉 CVX 调用,并对比两步结果;第三步,用 codegen 或直接手写 C 代码,把整个算法落地。第二步很多人会选择跳过,但直接在 Matlab 手写一遍会让你发现很多 CVX 掩盖掉的数值细节,比如矩阵条件数、无约束解是否越界、λ 的更新是否会让原始残差反弹等。
6.2 场景扩展:随机 MPC、多车协同和电池老化
PHEV 能量管理在论文里通常和智能交通场景结合。你可以把前车速度预测引入需求功率模型,用随机 MPC 处理预测误差;也可以在多车协同场景下,让每辆车只负责自己的发动机和电机,车辆之间通过充电桩或者电网功率共享来耦合,ADMM 天然支持这种分布式优化。电池老化也是一个非常热的方向:把 SOH 作为状态加入目标函数,让 SOC 参考值随老化下降,策略会更接近工程实际。这些扩展本质上不改变凸模型框架,只是把目标函数增加几项或者把约束矩阵扩大,ADMM 的结构优势会越来越明显。
6.3 一点个人的操作建议
最后说点实在的。我建议大家从经典工况入手,先把 WLTC 或 NEDC 的功率需求提取出来,用动态规划算一个离线最优作为基准,然后用凸 MPC 去逼近。如果 MPC 的油耗在基准线的 5% 以内,恭喜,你已经达到主流论文水准。然后你再逐步换用更复杂的车型参数、更贴合物理的电池模型,甚至加入速度预测。
调试 ADMM 的时候,不要只盯着最终油耗。把每一步迭代的原始残差、对偶残差和时间打出来,能直观看到参数调整的效果。收敛太慢和收敛到错误解是完全不同的两件事,前者大概率是 ρ 的问题,后者大概率是模型凸性的问题。
我自己在复现这个项目时,最有价值的时刻其实不是最终油耗数字好看,而是看着 ADMM 迭代次数从几百次降到二三十次,并且功率曲线变得平滑可靠。这种从“玄学调参”到“理解每一步在干什么”的转变,才是做这一类研究最让人舒服的体验。希望这篇文章能帮你少走点弯路,把时间花在真正值得深挖的地方。