PHEV能量管理:凸优化、ADMM与MPC结合的实时求解方案

发布时间:2026/10/5 3:59:40
PHEV能量管理:凸优化、ADMM与MPC结合的实时求解方案 前一阵子我在做插电式混合动力车辆的能量管理仿真最让我头疼的一环不是建模本身的复杂度而是“理论上最优”和“工程上能实时跑”之间的那道鸿沟。最开始我用动态规划做基准结果精度确实漂亮但心里清楚这套东西上不了车——MPC的滚动时域要求每个控制周期都快速出解而动态规划在中等预测时域下就已经慢到让人绝望。后来我把整个最优控制问题重新写成凸优化形式用Matlab的CVX框架求解再用交替方向乘子法ADMM把它嵌进模型预测控制MPC的滚动优化里计算效率和稳定性才真正落在可用的区间内。这篇文章是把这一整套思路完整记录下来从PHEV能量管理问题的数学本质到如何把非凸模型凸化、如何设计ADMM分解、如何在CVX里实现、再到仿真结果怎么分析、参数怎么调。内容面向正在做整车能量管理策略的研究生和工程师也适合对凸优化、MPC感兴趣但缺少一个具体落地场景的同学。全文基于我实际跑通的一套Matlab代码结构所有坑和取舍都会讲清楚。1. 为什么PHEV能量管理最终走向了凸优化MPC这条路1.1 先看清问题的数学本质插电式混合动力车辆的能量管理本质上是一个带约束的离散时间最优控制问题。给定一段工况对应的需求功率序列 (P_{req}(1), P_{req}(2), \dots, P_{req}(N))每个时刻需要决定三件事发动机输出功率 (P_{eng}(k))、电机输出功率 (P_{mot}(k))、电池充放电功率 (P_{bat}(k))使得整个工况段内的总燃油消耗或折算后的等效燃油消耗最小。写成数学形式大概是这样[ \min \sum_{k1}^{N} \left[ \dot{m}{fuel}\big(P{eng}(k)\big) \lambda \cdot P_{bat}(k) \right] ]约束条件包括[ P_{eng}(k) P_{mot}(k) P_{req}(k) ] [ SOC(k1) SOC(k) - \frac{P_{bat}(k)}{Q_{bat}} \cdot \Delta t ] [ SOC_{\min} \le SOC(k) \le SOC_{\max} ] [ 0 \le P_{eng}(k) \le P_{eng,\max} ] [ P_{mot,\min} \le P_{mot}(k) \le P_{mot,\max} ]这个形式看起来简单但里面藏着三个主要的非凸来源。第一个是发动机油耗函数 (\dot{m}{fuel}(P{eng}))。它来自发动机台架实验的BSFC数据做功在功率维度上通常不是一条直线甚至在小功率区段有明显的弯曲。如果直接把它当作非线性函数丢给求解器很容易导致求解器在局部最优里出不来。第二个是电池SOC动态方程。如果严格考虑电池内阻和开路电压随SOC的变化那么 (P_{bat}) 对SOC的影响系数本身又是SOC的函数形成乘积耦合凸性被破坏。很多论文在仿真里可以用查表来处理这个问题但一旦进入优化模型这个非线性项必须想办法处理掉。第三个是离散决策变量。发动机是否起停、变速箱挡位选择、模式切换纯电/混动/充电这些天然是整数变量。整数变量会让问题变成混合整数规划求解难度一下子上升一个档次。1.2 传统方法的瓶颈在哪里动态规划DP是解决上述最优控制问题的经典方法。把SOC离散成网格从工况终点向起点反向递推理论上能给出全局最优解。我一开始也是这样做的原因很简单DP的结果是所有算法对比的“金标准”没有它你后面无论用什么方法都说不清自己偏离最优解多远。但DP的致命问题是维数灾难。即使只有一个状态变量SOC把网格分成200份预测时域取300秒就有6万个待遍历节点每个节点还要枚举可能的控制量。我实测在普通笔记本上一个300秒工况的DP要跑几十秒。放到MPC框架里假设每个控制周期都要解一次优化问题预测时域哪怕只取10步一次DP也要几百毫秒这在实时控制里完全不可接受。基于规则的能量管理策略最常见的是电量消耗-电量维持也就是CD-CS策略倒是速度飞快但规则的阈值是人工标定出来的严重依赖工况特征。市区蠕行和高速巡航的最优切换点完全不同一套规则很难同时适配多种工况。实际跑下来CD-CS比DP最优解通常高8%~15%的等效油耗而且无法主动利用未来的工况信息。非线性MPC理论上更灵活但NLP求解器在非凸问题上无法保证全局最优求解时间的波动也很大。最坏情况下一步优化耗时可能超过控制周期这在工程上意味着算法不可用。1.3 我的路线判断凸化加滚动ADMM解决规模问题我的判断很直接保留MPC的滚动框架但把每个控制周期内的子问题改写成凸优化问题。凸优化保证全局最优求解时间可控而且Matlab下有CVX这个成熟的建模工具可以做快速原型验证。但直接在整个预测时域上调用CVX求解一个大QP当预测时域拉长到30秒以上时计算时间又会上升。这时候ADMM的价值就体现出来了它把一个大问题拆成若干个可以并行或顺序求解的小子问题每个小子问题规模小、结构简单甚至可以手动写出闭式解配合MPC滚动过程中天然的热启动条件整体计算效率远高于直接解大QP。2. 从MPC框架到凸优化建模问题是如何被重新表述的2.1 滚动优化框架与变量定义MPC的核心思想是“边预测边滚动”。在控制周期 (k)控制器读取当前SOC和接下来 (N) 步的需求功率序列求解一个有限时域最优控制问题得到未来 (N) 步的最优控制序列但只把第一步执行下去然后到 (k1) 时刻重新读取状态重复求解。我这里预测时域取 (N30)步长 (\Delta t1\text{s})也就是说每个控制周期往前看30秒。这个时域长度对于PHEV能量管理来说比较合理——太短看不到坡道和拥堵的提前量太长则求解负担大且预测误差累积明显。变量上我把每个时刻的控制量定义为发动机功率 (P_{eng}(k)) 和电机功率 (P_{mot}(k))电池功率通过功率平衡关系由两者决定。状态量除了SOC之外车速序列假设由工况模型提前给出不参与优化。2.2 发动机油耗曲线的凸化处理发动机油耗曲线 (\dot{m}{fuel}(P{eng})) 的处理是整个凸化的关键。工程上最常用的手段是分段线性化PWL。把发动机功率可行区间 ([0, P_{eng,\max}]) 划分成 (M) 段一般取15到25段每一段用一条直线近似原始油耗曲线。如果原始曲线在功率维度上是下凸的多数汽油机在中高负荷区满足这个条件那么分段线性化后的函数仍然是凸函数。这个凸函数可以不用显式引入整数变量而是用一组线性不等式来描述[ \dot{m}{fuel}(k) \ge a_i \cdot P{eng}(k) b_i, \quad i1,2,\dots,M ]这样每个时刻的油耗被这些下界约束“托”起来而目标函数里最小化 (\sum \dot{m}{fuel}(k)) 会自动把每个时刻的 (\dot{m}{fuel}) 压到最紧的那个线段上。这一招比引入二进制变量做精确分段线性规划要简单两个数量级而且CVX对线性约束的求解效率极高。2.3 电池SOC动态方程的线性化电池模型我一开始用了等效电路模型但很快就发现在优化框架里严格的内阻-OCV随SOC变化的非线性耦合会让问题变成非凸CVX直接报DCP错误。工程上的妥协方案是在SOC窗口内假设开路电压和电池效率近似恒定。这样SOC动态方程退化为一个线性方程[ SOC(k1) SOC(k) - \alpha \cdot P_{bat}(k) ]其中 (\alpha \frac{\Delta t}{Q_{bat}})(Q_{bat}) 是电池容量单位换算成SOC百分比。如果后续想把电池内阻损耗也纳进去可以给 (P_{bat}) 加一个凸的二次惩罚项比如 (R \cdot P_{bat}(k)^2)这仍然是凸的CVX可以直接处理。但我在第一版实现里没有加这一项原因是它会增加求解时间而且能量管理层面的鲁棒性足以覆盖这个简化误差。这里我要重点提醒一句SOC动态的线性化会带来模型误差但不要小看MPC滚动本身对模型误差的修正能力。因为每个控制周期都会用真实SOC重新初始化误差不会无限累积。2.4 目标函数中λ的物理标定目标函数里 (\lambda) 是电能消耗的价格系数它决定控制器对“用电”和“用油”的偏好。这个系数不是随便拍的。我采用的是等效燃油消耗最小化的思想1千瓦时电能如果事后靠发动机发电来补回需要消耗多少燃油考虑充电路径上的效率电网充电效率、电池充放电效率、发动机平均效率把电能折算成等效燃油。在此基础上再加一个SOC维持的惩罚权重让控制器在电量充足时不至于把电榨干在电量偏低时主动趋于混动模式。实际标定时我会先以 (\lambda) 为变量做一组离线仿真看终点SOC的均值是否落在目标值附近。如果终点SOC偏高说明电能被过度珍惜可以适当降低 (\lambda)反之则调高。这个闭环标定过程通常迭代两三轮就能找到合适的值。2.5 整理成标准凸QP经过上述凸化处理后每个MPC控制周期的优化问题变成了一个带线性等式、不等式约束的二次规划问题忽略内阻损耗时甚至是LP[ \min_{x} \frac{1}{2} x^T H x c^T x ] [ \text{s.t.} \quad A_{eq} x b_{eq}, \quad A_{ineq} x \le b_{ineq} ]决策变量 (x) 里包含 (P_{eng}(1..N))、(P_{mot}(1..N))、(SOC(1..N1)) 和辅助的油耗变量 (\dot{m}_{fuel}(1..N))。这个形式可以直接用CVX描述也可以用YALMIP但我在项目里选择了CVX因为它对凸性检查更严格能及早发现建模错误。3. ADMM如何拆解这个大优化问题原理与分治逻辑3.1 ADMM的基本思想交替方向乘子法ADMM解决的是如下形式的问题[ \min f(x) g(z) ] [ \text{s.t.} \quad x - z 0 ]它把目标函数拆成 (f) 和 (g) 两块用一条等式约束把它们联系起来。迭代过程分三步[ x^{k1} \arg\min_x \left( f(x) \frac{\rho}{2} | x - z^k u^k |^2 \right) ] [ z^{k1} \arg\min_z \left( g(z) \frac{\rho}{2} | x^{k1} - z u^k |^2 \right) ] [ u^{k1} u^k x^{k1} - z^{k1} ]这里的 (\rho) 是步长惩罚系数(u) 是缩放对偶变量。直观理解就是在更新 (x) 时不光要最小化自己的目标还要尽量靠近上一轮的 (z)更新 (z) 时同理最后通过对偶变量 (u) 记住“两边的分歧”在下一轮迭代中迫使两者靠拢。ADMM的工程友好性在于当 (f) 和 (g) 各自“容易求解”时比如一个是可分块的小问题另一个是带带状结构的小问题整体收敛速度在实际中往往比理论最坏情况好得多。对精度要求不高时迭代20到50轮就够用了。3.2 能源管理问题里的自然分解结构把第2节整理出的凸QP应用ADMM关键在于找到自然的分解结构。观察问题结构我发现 (P_{bat}(k)) 是一个天然的解耦点每个时间步的功率平衡约束 (P_{eng}(k) P_{mot}(k) P_{req}(k)) 是局部的而SOC动态 (SOC(k1) SOC(k) - \alpha P_{bat}(k)) 才是时间上耦合的全局约束。基于此我把 (P_{bat}(k)) 的“全局版本”记作 (z_k)并施加一致性约束[ P_{bat}(k) - z_k 0, \quad k 1, \dots, N ]这样原问题就被拆成了两类子问题(x) 子问题每个时间步独立求解局部功率分配(z) 子问题给定局部功率决策和对偶乘子求解整条SOC轨迹的最优修正。3.3 局部子问题每个时间步的功率分配(x) 子问题在每个时刻 (k) 是独立的[ \min \dot{m}{fuel}(P{eng}(k)) \frac{\rho}{2} \left( P_{bat}(k) - z_k u_k \right)^2 ] [ \text{s.t.} \quad P_{eng}(k) P_{mot}(k) P_{req}(k) ] [ 0 \le P_{eng}(k) \le P_{eng,\max}, \quad P_{mot,\min} \le P_{mot}(k) \le P_{mot,\max} ]这个子问题里只有两个标量变量而且如果能用功率平衡把 (P_{mot}) 替换掉它就变成关于 (P_{eng}) 的一元凸优化问题。由于油耗函数是分段线性的这个一元问题可以用三分法或者枚举分段点配合闭式解快速求解完全没有必要调用CVX。我在代码里一开始偷懒用CVX写这个局部子问题结果发现每个控制周期要调用几十次CVX建模开销比求解本身还大。后来改成手写投影求解速度提升很明显。这一点我会在后面实操章节详细展开。3.4 全局子问题SOC轨迹的协调修正(z) 子问题负责协调整条SOC轨迹[ \min \sum_{k1}^{N} \left[ \lambda z_k \frac{\rho}{2} \left( P_{bat}(k) - z_k u_k \right)^2 \right] ] [ \text{s.t.} \quad SOC(k1) SOC(k) - \alpha z_k ] [ SOC(1) SOC_{\text{init}}, \quad SOC_{\min} \le SOC(k) \le SOC_{\max} ]这是一个带线性动态约束的小规模二次规划变量规模约为 (N30) 量级。这个问题的系数矩阵呈带状结构可以用标准QP求解器一次求解也可以手写基于Thomas算法的快速求解器。我用CVX来做这个子问题形成原型后续再优化成定制求解器。这个分解的巧妙之处在于原本耦合的SOC动态约束只出现在 (z) 子问题中而 (x) 子问题完全解耦所以 (N) 越大ADMM相对于直接解大QP的并行优势越明显。3.5 热启动MPC滚动带来的天然优势MPC的滚动特性给ADMM带来了一个“白送的福利”相邻两个控制周期的优化问题差异很小上一周期迭代收敛时得到的 (z) 序列和对偶乘子 (u)可以直接作为本周期ADMM的初始值。实际运行中第一次冷启动可能需要三四十轮迭代但进入滚动稳态后通常5到10轮迭代就能满足收敛条件。这比每次都用CVX从零求解整个预测时域问题快一个数量级。这是ADMM在这个场景下真正发挥价值的核心原因。4. CVX实现细节建模、求解器配置与避坑清单4.1 CVX建模核心语法用CVX描述第2节的凸QP代码骨架大概是这样N 30; % 预测时域 Q_bat 36000; % 电池容量单位换算后 alpha 1 / Q_bat; % SOC动态系数 SOC_min 0.2; SOC_max 0.9; P_eng_max 80e3; P_mot_max 60e3; lambda_bat 1200; % 电能价格系数需标定 cvx_begin quiet variable P_eng(N) variable P_mot(N) variable SOC(N1) variable m_fuel(N) minimize( sum(m_fuel) lambda_bat * sum(P_mot) ) subject to P_eng P_mot P_req; % 功率平衡 0 P_eng P_eng_max; -P_mot_max P_mot P_mot_max; % 分段线性油耗下界凸PWL for k 1:N for i 1:M m_fuel(k) a(i) * P_eng(k) b(i); end end % SOC动态 SOC(1) SOC_init; for k 1:N SOC(k1) SOC(k) - alpha * P_mot(k); end SOC_min SOC SOC_max; cvx_end这里最关键的一行是m_fuel(k) a(i) * P_eng(k) b(i)。这套写法的本质是用一组线性不等式去逼近凸油耗曲线而不是去显式建模一个分段线性函数。如果你把油耗曲线直接写成一个查表函数塞进CVXCVX会立刻报错因为它无法识别查表操作的凸性。4.2 求解器选择别被默认的SDPT3坑了CVX的默认求解器是SDPT3它对小规模SDP问题表现不错但对纯LP或QP问题并不是最优选择。我的经验是问题类型推荐求解器备注LP油耗PWL线性SOCSeDuMi数值稳定速度快QP含内阻损耗二次项MOSEK如有许可证商用求解器对QP支持最好原型验证不在乎时间SDPT3默认通用性强在Matlab里用cvx_solver sedumi切换求解器。我第一次跑30步预测时域时没切换求解器SDPT3单次求解要600毫秒换成SeDuMi直接降到150毫秒差了一个量级。4.3 ADMM子问题要不要用CVX这个问题我纠结了很久最终结论是原型阶段全部用CVX没问题但要上性能时局部子问题必须手写。因为局部子问题每个时间步只有两个变量调用CVX的建模开销可能占据总耗时的80%以上。CVX是建模层工具它会把你的约束逐条解析成矩阵这个过程本身有成本。而手写局部子问题的闭式解本质上是把单个时间步的优化变成一个一元凸函数的极小值搜索代码量不大但速度是CVX版本的几十倍。全局子问题由于涉及SOC动态保留CVX写法在原型阶段更清晰。后续优化方向是写一个基于带状矩阵的定制QP求解器但这不是必须的因为30步规模的QP在SeDuMi下求解也就几十毫秒滚动热启动后时间更短。4.4 常见错误和处理手段我整理一下这一路踩过的具体报错和对应解法。第一类报错是“Disciplined convex programming error”。这几乎都是建模时写了CVX无法识别凸性的表达式。最常见的是用查表函数、用严格不等式、或者引入了两个变量的乘积。解决办法是回到凸化的三个基本操作线性化、变量替换、松散约束。第二类问题不是报错而是cvx_status返回Failed。排查方向包括许可证是否配置正确、约束是否矛盾、数值尺度是否差距过大。CVX的许可证问题特别坑——学术版需要注册邮箱获取许可文件如果cvx_setup后没执行cvx_licenseCVX可以运行但只能解决极小问题大问题直接失败。我身边已经不止一个人卡在这。第三类是数值尺度问题。目标函数里同时存在数值量级为1e4的油耗项和量级为1e−2的SOC偏差项时求解器很容易对小的量级“不敏感”。处理办法是对目标函数系数做归一化或者把SOC约束的单位改成百分比外的更大尺度。我在代码里把油耗项换成等效燃油消耗率除以1000问题立刻收敛变稳。第四类是计算速度问题。如果预测时域 (N) 不到10步直接CVX求解整个MPC子问题毫无压力根本不需要上ADMM。ADMM的价值在 (N \ge 30) 或者硬件资源受限时才真正凸显。不要为了炫技而引入ADMM。5. 仿真验证与结果分析怎么证明这个方法真的有效5.1 仿真参数与工况设定我搭建的仿真平台是Matlab R2023b运行在Windows 10下CPU是i7-12700。整车参数用了一组典型的并联PHEV结构参数发动机最大功率80 kW电机峰值功率60 kW电池容量10 kWhSOC窗口0.2 ~ 0.9预测时域30步步长1 s目标终点SOC0.3工况选择分三组WLTC全工况、UDDS市区工况、以及一段人工组合工况市区城郊高速组合工况用来验证算法对不同道路工况的适应能力。5.2 对比基线的建立没有基线的能量管理仿真毫无意义。我同时实现了三套方法做对比动态规划DP离线全局最优作为理论上限规则策略CD-CS作为工程下限直接CVX求解MPC每次滚动都完整调用CVX求解作为“ADMMCVX”的对照组。5.3 关键指标与一次典型结果评估从三个维度展开等效油耗、求解耗时、SOC轨迹质量。一次WLTC工况的典型结果如下数据来自我的仿真平台具体数值依参数而异但趋势有参考性方法等效油耗 (L/100km)平均单步求解时间备注动态规划 DP6.2离线不计时全局最优基准规则策略 CD-CS7.11 ms阈值人工给定直接CVX求解MPC6.4约150 ms每次滚动完整建模求解ADMMCVX的MPC6.4约25 ms热启动平均8次迭代这个结果说明几件事。第一MPC框架配合凸优化等效油耗能逼近DP最优解差距在3%左右比起规则策略有明显改善。第二直接CVX和ADMMCVX的油耗几乎一样说明ADMM在给定迭代次数下已经收敛到了足够好的解没有以牺牲最优性为代价。第三ADMM把单步求解时间从150毫秒压到25毫秒P99也在60毫秒左右虽然离真正的嵌入式实时还有距离但在快速原型和硬件在环平台上已经可以接受。5.4 SOC轨迹质量分析只看油耗不够还要看SOC轨迹是否合理。我在仿真中特别注意两个现象。第一ADMM收敛不完全时SOC轨迹会出现高频抖动。具体表现是SOC曲线在目标终点附近反复小幅振荡这不是物理现象而是对偶变量 (u) 还没稳定的结果。解决办法是加大迭代次数上限或者适当调大 (\rho)。第二SOC轨迹在预测时域末端是否贴合目标终点。如果 (\lambda) 标定得当终点SOC会落在0.3附近如果 (\lambda) 过小控制器会倾向于把电池电量用完终点SOC跌到0.2以下的窗口边界这时候需要回调 (\lambda)。5.5 ρ的选取经验ADMM的 (\rho) 是调参中最敏感的一个。我的经验法则是(\rho) 的量级应该和目标函数里局部项的曲率量级相匹配。在 (\rho) 过小时ADMM对一致性约束的惩罚不足局部子问题基本只顾各自目标收敛极慢。在 (\rho) 过大时局部子问题几乎被一致性约束支配迭代过程像“乌龟爬”同样慢。实际调试时我会把目标函数系数归一化后从 (\rho 1) 开始试观察迭代曲线如果原始残差下降太慢就减小 (\rho)如果对偶残差振荡严重就增大 (\rho)。另一个技巧是预测时域 (N) 越大(\rho) 要适当调大。因为SOC动态的耦合距离变长一个控制周期内的局部决策误差会影响后续很多步一致性约束需要更强的权重来快速纠正。6. 实操经验总结调参、稳定性和进一步优化的方向6.1 不要把ADMM当黑盒ADMM最容易被误解的一点是“一定能收敛到全局最优”。它确实能但这个结论依赖凸性和适当的 (\rho)。一旦你的模型里混入了我前面提到的查表油耗、非凸电池模型这类违反凸性的操作ADMM可能收敛到奇怪的结果而不报错。每次修改模型后我都建议先跑一次直接CVX求解的对照组确认最优解没问题再对比ADMM的结果。如果两者差异超过2%先检查ADMM的收敛判据再考虑模型凸性是否被破坏。6.2 我的推荐代码架构在实际工程代码里我建议把问题拆成几个独立函数build_local_problem构造局部子问题、solve_global_problem构造全局子问题、run_admm主循环、prepare_mpc_case滚动时域初始化。这样每一层都可以单独测试。不要把所有逻辑都堆在一个脚本里否则后面改模型或换工况时会非常痛苦。Matlab的函数句柄和结构体在这里很够用。ADMM主循环保持简短所有迭代细节封装在子函数里方便做单元测试。6.3 关于模型精度和算法鲁棒性的取舍凸化过程必然带来模型误差这一点无法回避。我的处理原则是能量管理策略层接受简化模型把精确模型放在仿真验证层。也就是说在Matlab里搭一套非线性整车模型包含详细的BSFC map和电池内阻模型作为被控对象优化算法在简化凸模型上求解算出的控制量再喂给非线性整车模型看闭环表现。这样既利用了凸优化的快速求解又能在仿真中验证简化模型的误差是否在可接受范围内。实测下来这套简化模型在WLTC下的油耗误差在2%左右对能量管理策略评估来说完全可以接受。6.4 后续可以扩展的方向这套“凸优化ADMMMPC”的框架可扩展性不错。我认为值得尝试的方向有三个。第一是随机MPC把需求功率的预测误差建模成随机扰动在目标函数里加入对预测不确定性的惩罚项。ADMM的可分解性让随机场景下的分布式求解变得自然。第二是多车编队能量管理车队内每台车的局部问题天然独立ADMM可以充当分布式协调器通过全局一致性约束让整个车队的电量消耗和SOC轨迹达成协同。第三是结合强化学习做端到端调参ADMM里的 (\rho)、迭代次数阈值、MPC里的预测时域都可以作为强化学习动作离线训练后在线调整减少人工标定工作量。我在实际跑这套算法时最深的体会是ADMM不是一个“装上去就能用”的求解器它是一个需要理解问题结构后主动设计的算法框架。同一个问题分解方式不同性能天差地别。如果你正卡在PHEV能量管理或类似MPC问题上先把问题凸化搞清楚再谈ADMM顺序一定不要反。先给一个简单的直接CVX版本跑通再逐步替换成ADMM版本每一步都有对照组兜底这样调试起来踏实得多。