互联燃料电池混合动力汽车(FCHEV)在信号交叉口下的生态驾驶控制,我盯了挺久。这类工作几乎都是同一个套路:利用V2I通信拿到红绿灯相位和倒计时信息,在满足通行时间、车速、动力系统约束的前提下,规划出一条省氢的驾驶轨迹,同时把燃料电池和动力电池之间的功率分配一起算出来。这两年SCI一区上用双层凸优化做这件事的文章肉眼可见地多了起来,原因也很实在:双层结构把“车怎么开”和“能量怎么分”两个时间尺度完全不同的子问题拆开了,又因为每个子问题都是凸的,能快速得到全局最优解。这篇文章把我复现和二次开发这个方向时的建模思路、优化套路、Matlab代码组织方式和踩过的坑整理出来,适合正在做能量管理、生态驾驶、车路协同控制方向的研究生和工程师参考。
1. 这个课题到底在解决什么问题
1.1 四个关键词拆开看
先把这个标题拆成四个部分,每部分对应一类技术点,避免一上来就糊成一个“大而全”的黑盒。
“互联”指的是车联网环境,具体到这个场景就是车与基础设施(V2I)通信。车辆在接近交叉口时能拿到信号灯的SPaT信息,即当前相位、剩余时间、下一周期时长,这些信息是生态驾驶优化的前提条件。没有这个信息,车辆就只能靠摄像头识别红绿灯,识别到红灯再减速,往往已经浪费了大量动能。
“燃料电池混合动力汽车”是研究对象。这车不是单一燃料电池驱动,而是燃料电池加动力电池组成的混合动力系统。燃料电池负责基础功率,电池负责峰值功率和再生制动回收,两者之间有一个功率分配问题。这个分配问题就是下层优化要解决的。
“信号交叉口”是场景。城市路况里交叉口是能耗重灾区,频繁停车起步、怠速等待、急加速冲过绿灯,都会显著拉高氢耗和电耗。信号灯约束让速度规划不再是简单的两点间最优控制问题,而是带有时序窗口约束的优化问题。
“生态驾驶”是目标层。Eco-driving不是一个具体算法,而是一整套驾驶策略:在知道信号灯时序的前提下,让车辆在绿灯窗口内不停车通过,或者红灯无法避免时提前减速滑行,尽量减少制动损耗和怠速时间。它的本质是“用信息换能量”。
1.2 为什么这个组合能发到一区
从学术角度看,这个题目能发一区,不是因为它用了多复杂的数学工具,而是它踩准了三个问题的交叉点。
第一是乘员安全和能耗的矛盾。规划车速不能牺牲舒适性和安全性,加速度必须平滑,停车线不能闯。这个约束组合让问题具有一定难度。
第二是时间尺度上的耦合。速度轨迹的变化是秒级到十秒级,动力电池SOC的变化是分钟级,燃料电池输出功率的动态响应又是毫秒到秒级。直接在同一个优化框架里同时处理这些不同尺度的变量,会让问题非凸或者规模爆炸。双层结构恰好把不同尺度分隔开:上层管秒级的车速,下层管分钟级的能量分配。
第三是可证明的最优性。很多生态驾驶文章用启发式、动态规划或者强化学习来做,结果能不能全局最优很难说。凸优化则不同,只要建模正确,解出来的就是全局最优,这对学术论文来说是非常强的卖点。审稿人看到“凸优化保证全局最优”这句话,天然就会对结果多一分信任。
2. 建模:从车辆动力学到信号灯约束
2.1 车辆纵向动力学和燃料电池混动系统用什么模型
建模是整个工作的地基,模型选不好,后面所有优化都是空中楼阁。我做这个项目时,采用的是工程上最常用的纵向动力学离散模型。
把一段从当前位置到交叉口的距离分成N个时间步,每步时间间隔固定为dt,通常取0.1秒到0.5秒。状态变量是位置s、速度v、加速度a,离散动力学写成:
s_{k+1} = s_k + v_k * dt + 0.5 * a_k * dt^2 v_{k+1} = v_k + a_k * dt
这两个等式在优化里是标准线性约束,直接写进约束矩阵即可。速度有上下限,加速度也有上下限,加速度上下限实际上隐含了舒适性约束和路面附着约束。
需求功率由纵向动力学推算:
F_res = m * g * f_r + 0.5 * rho * C_d * A * v^2 P_req = (m * a + F_res) * v / eta_drive
m是整车质量,f_r是滚动阻力系数,C_d是风阻系数,A是迎风面积,eta_drive是传动效率。这里的F_res是速度的二次函数,P_req里就出现了v的三次项,后面优化时要注意处理凸性问题。
燃料电池混合动力系统的模型不能太复杂也不能太简单。我用的是PEMFC加锂电池的构型,功率平衡关系写成:
P_req = P_fc + P_bat
P_fc是燃料电池输出功率,P_bat是电池功率,电池放电时为正,充电时为负。电池SOC的动态用简化模型:
SOC_{k+1} = SOC_k - P_bat[k] * dt / (3600 * Q_bat)
这个模型假设电池端电压近似恒定,忽略了内阻损耗的细节,但对功率分配优化来说已经够用。燃料电池的瞬时氢耗用二次函数拟合:
m_H2 = c0 + c1 * P_fc + c2 * P_fc^2
这里的关键是拟合系数必须保证二次项系数为正,即氢耗函数是凸的。高保真的燃料电池效率MAP表往往是凹的或非凸的,直接进优化框架会破坏凸性,所以二次拟合成凸函数是必要妥协。
2.2 信号交叉口的红绿灯信息怎么进优化框架
信号灯约束是这个问题的灵魂,也是建模时最容易出bug的地方。核心约束是:车辆在红灯期间不允许越过停车线。如果用t_pass表示车辆通过交叉口停车线的时刻,那么t_pass必须落在绿灯窗口集合里。
绿灯窗口由SPaT信息决定。假设信号周期固定,已知绿灯起始时间和时长,可以得到一系列窗口区间: [t_g1_start, t_g1_end], [t_g2_start, t_g2_end], ...
如果直接用这个约束:t_pass ∈ 任意一个绿灯窗口,这本质上是一个“或”逻辑,不是凸约束。标准凸优化框架处理不了。
我的处理方式有三种,按适用场景选。第一种是枚举法,对每个候选绿灯窗口单独求解一次优化,得到若干可行速度轨迹,从中选氢耗最小的。这个方法最简单,也最稳妥,缺点是计算量随候选窗口数量线性增加。
第二种是引入二进制变量,把“或”约束转成混合整数二次规划(MIQP)。每一段绿灯窗口对应一个二进制变量表示是否在该窗口通过,约束变成:
t_pass >= t_g_start - M * (1 - z_i) t_pass <= t_g_end + M * (1 - z_i) sum(z_i) = 1
这里M是大数。MIQP的求解速度比纯QP慢,而且Gurobi处理MIQP的耗时在小规模问题上还能接受,但一旦时间步数N变大,商业求解器也会吃力。
第三种是滚动时域策略,在每个控制周期只考虑最近的一个或两个绿灯窗口,不一次性规划整个通行过程。这个策略接近实车控制器,后续做实时部署时我倾向于这个方案。
2.3 目标函数和约束怎么写得“凸”
凸性是这个方法的上限决定因素。模型写得好不好,直接体现在目标函数和约束能否被写成凸形式。我踩过的数次坑,基本都是这里出了问题。
上层速度规划的目标函数,最直观的写法是最小化总通行时间和能耗的加权和:
J_upper = alpha * T_total + beta * 能耗近似项
这里T_total是总通行时间,能耗近似项我最开始尝试直接用P_req对时间的累加。但P_req里含有v的三次方项,v是决策变量,v^3在变量非负时虽然是凸的,但和加速度a相乘后变得非常复杂,联合求解时往往不是凸的。
解决方式是把能耗项拆开处理。风阻项0.5 * rho * C_d * A * v^3在v非负时其实是凸的,但为了保持整体的凸二次规划形式,我经常将v^3用一个分段线性函数近似,或者把v^3替换成v * v^2并引入辅助变量做松弛。
另一个更省事的方式是,在目标函数里不显式写那个复杂的瞬时功率项,而是用v的二次项和a的二次项来近似能耗。结果就是:
J_upper = alpha * T_total + beta * sum(v.^2) + gamma * sum(a.^2)
其中sum(v.^2)鼓励平稳行驶,避免过高车速导致风阻快速增加;sum(a.^2)是舒适性惩罚,同时也能减少不必要的加减速。这种写法的好处是整个目标函数是一个关于v和a的凸二次型,约束也全是线性的,标准二次规划求解器几秒钟内就能解完。
下层能量管理的目标函数相对简单:
J_lower = sum(m_H2(P_fc[k])) + lambda * (SOC_N - SOC_ref)^2
S_COM终端SOC约束用惩罚项代替硬约束,lambda取一个较大值,既能保证SOC最终回到参考值附近,又不会把优化问题变得过紧。约束方面是功率平衡、燃料电池功率上下限、爬坡功率限制和SOC上下限,全部线性。
3. 双层凸优化算法:上层管速度,下层管能量
3.1 为什么必须拆成两层
有人可能会问,能不能把速度规划和能量管理合并成一个大优化问题直接求解?理论上可以,实际操作起来很难。
原因在于,速度规划需要的决策变量是位置、速度、加速度,时间尺度以秒计;而能量管理需要的是燃料电池功率序列、电池功率序列、SOC轨迹,时间尺度长得多。两个子问题的变量数量加起来会非常可观,联合求解时目标函数的耦合项容易导致问题的Hessian矩阵不再正定,凸性荡然无存。
拆成两层之后的优势非常明显:上层问题的变量规模只有3N个左右,下层只有2N个左右,两个问题都是标准QP,求解稳定高效。而且从工程角度看,“先规划轨迹,再分配能量”本身就是车辆控制器常用的分层架构,上层对应整车控制器,下层对应能量管理控制器,物理含义清晰。
代价是两层之间的模型精度会有一定的近似损失。上层不知道下层的氢耗函数有多精细,下层认为上层给出的速度就是最终的执行轨迹,两者之间的误差需要通过迭代反馈来弥补。
3.2 上层速度规划问题怎么建
上层问题的输入是当前位置、当前速度、信号灯SPaT信息、道路限速。输出是一整条离散速度轨迹和时间序列。
正式开始建模之前,先判断一个问题:车辆能在当前绿灯窗口内通过吗?如果从当前位置以最大允许速度行驶,仍无法在绿灯结束前到达停车线,那当前绿灯窗口不可行,直接锁定下一个绿灯窗口作为目标;如果即使以最小速度巡航也能在绿灯结束前到达,那可以选择一个较平缓的速度轨迹。
实际场景中更常见的是中间情况:踩油门可以冲过去,松油门可以用下一个绿灯窗口通过。这时候优化就要权衡早到还是晚到。比如说,当前绿灯还有15秒结束,距离停车线200米,最大速度15米每秒,最小速度5米每秒。以15米每秒可以在13.3秒通过,以5米每秒需要40秒,只能等下个绿灯。上层的QP会在这两种可行方案之间,根据alpha和beta的权重自动选择综合代价更小的策略。
上层QP的决策变量序列为s、v、a,每个都是长度为N的向量。约束包括运动学等式约束、速度区间约束、加速度上下界约束、停车线位置约束和信号灯窗口约束。目标函数按前面说的凸二次型构建。
我习惯在目标函数里额外加一项小正则项,比如1e-4倍的v对参考速度差值的平方,目的是让优化问题数值上更稳定,避免出现解的唯一性问题导致求解器反复试探。
3.3 下层能量管理问题怎么建
下层问题的输入是上层算出的速度轨迹,通过车辆动力学模型算出每一时刻的需求功率P_req序列。输出是燃料电池功率P_fc和电池功率P_bat的序列。
下层问题本质上是一个带约束的功率分配问题。目标是让整趟行程的氢耗最小,同时维持SOC在安全区间。约束包括功率平衡等式P_fc + P_bat = P_req,燃料电池功率上下限,电池功率上下限,燃料电池的爬坡约束,以及SOC的动态方程和上下限。
这里有一个细节容易被忽略:燃料电池的爬坡约束。燃料电池输出功率不能突变,否则会影响电堆寿命。这个约束是线性的,形式如下:
-delta_H2 <= P_fc[k+1] - P_fc[k] <= delta_H2
delta_H2的典型值在1到5千瓦每秒之间,取决于燃料电池的型号。加上这个约束之后,下层QP的解会更接近实际,而不是出现燃料电池功率在相邻时刻之间大幅跳动的理想化结果。
SOC的初始值我想强调一下。很多教程把SOC初值设成0.8,终端不加约束,跑出来各种奇怪的解。我一般都设SOC_0 = 0.5,再在目标函数里加终端SOC惩罚项,让优化器在氢耗和SOC保持之间取折中。这样得到的功率分配结果更符合实际运行逻辑,因为电池不能只用来压榨氢耗而不考虑自身电量维持。
3.4 两层之间的迭代与信息传递
双层问题写完之后,如何把两个子问题串起来是算法设计的核心。第一种方法是简单的顺序求解,即先解上层得到速度轨迹,再解下层得到功率分配,一次性结束。这个方法快,但精度差,因为上层用的能耗模型是近似模型,和下层精细模型之间的偏差没有反馈。
第二种方法是不动点迭代。流程如下:
- 给一个初始速度轨迹猜测,通常按匀速巡航算。
- 根据当前速度轨迹解下层问题,得到氢耗序列和总氢耗。
- 将下层氢耗对速度的敏感性(即KKT乘子或数值梯度)反馈给上层。
- 上层在目标函数中增加一个拉格朗日修正项,重新求解速度轨迹。
- 重复步骤2到5,直到两次迭代之间的总氢耗变化小于阈值。
我在实际代码中遇到过迭代来回震荡的情况,后来加了阻尼更新:
v_new = (1 - rho) * v_old + rho * v_solved
rho取0.3到0.6之间,显著改善了收敛性。实测下来,这个简单的不动点迭代通常能在5到15次迭代内收敛,每次迭代求解两个QP的时间加起来不到1秒,整体计算时间可以接受。
如果追求更严格的数学性质,可以把下层的KKT条件显式写入上层的约束,得到一个带互补约束的数学规划问题(MPEC),但这类问题本身非凸,求解难度更大,不推荐在工程实现中碰。
4. Matlab代码实现:从建模到仿真复现
4.1 求解器和优化工具箱怎么选
Matlab环境下的优化求解器选择,直接影响开发效率和求解速度。我首选的是YALMIP加Gurobi组合。YALMIP负责把优化问题从代数形式转换成求解器能接受的格式,Gurobi负责实际的QP求解,速度比Matlab自带的quadprog快很多,学术版license也不难申请。
如果只装了Matlab基础包没有Gurobi,用CVX加SDPT3也跑得动,但速度明显慢,尤其是双层迭代需要解几十次QP的时候。CVX的优势是建模语法简洁,对新手友好;劣势是每次求解前会做一轮预解析,循环里反复调用时开销很大。
我个人推荐对比一下两种建模方式的适用场景,下面这张表是我自己的选型经验。
| 对比项 | YALMIP + Gurobi | CVX + SDPT3 |
|---|---|---|
| 建模语法 | 稍微繁琐,但线性约束直观 | 简洁,接近数学表达 |
| 求解速度 | 快(QP级别) | 慢(跑较大规模QP吃力) |
| 循环内重复求解 | 适合,可复用变量定义 | 不适合,预解析开销大 |
| 处理MIQP | 支持 | 支持但不推荐 |
| 安装配置复杂度 | 需额外装Gurobi | 只需Matlab和CVX |
我的建议是:如果目标是把论文里的算法验证跑通,CVX够了;如果目标是做参数扫描、双层迭代或者后续扩展对比实验,趁早换成YALMIP加Gurobi。
4.2 代码模块怎么划分
写Matlab代码最忌讳的就是把所有逻辑塞到一个大脚本里。我这个项目的组织方式是把每个功能拆成独立函数,主脚本只负责调用和汇总结果。
基本模块如下:
- main.m:主入口,设定参数、调用求解、输出结果。
- init_parameters.m:定义车辆参数、道路参数、信号灯参数和优化权重。
- generate_signal_timing.m:根据信号周期生成绿灯窗口序列。
- solve_upper_speed.m:接收SPaT信息和车辆状态,返回速度轨迹。
- solve_lower_energy.m:接收速度轨迹,返回燃料电池功率、电池功率和SOC轨迹。
- plot_results.m:把速度和能量结果画成图。
这样拆分的好处是可以单独调试某个模块,比如只调上层速度规划,不跑能量管理;或者固定速度轨迹,只测下层的功率分配逻辑。互不干扰。
4.3 关键代码结构示例
下面给出一段上层速度规划的YALMIP建模示意代码,说明核心的变量定义和约束写法。这段代码不能直接运行,只是一个结构模板,写清楚sdpvar怎么定义、约束怎么加、求解目标怎么设。
% 参数: N 时间步数, dt 时间间隔, v_min/v_max 速度界限 % a_min/a_max 加速度界限, s_stop 停车线位置 x = sdpvar(1, N); % 位置 v = sdpvar(1, N); % 速度 a = sdpvar(1, N); % 加速度 Constraints = []; % 初值 Constraints = [Constraints, x(1) == 0, v(1) == v0, a(1) == 0]; % 运动学递推 for k = 1:N-1 Constraints = [Constraints, x(k+1) == x(k) + v(k)*dt + 0.5*a(k)*dt^2]; Constraints = [Constraints, v(k+1) == v(k) + a(k)*dt]; end % 边界约束 Constraints = [Constraints, v_min <= v <= v_max]; Constraints = [Constraints, a_min <= a <= a_max]; Constraints = [Constraints, x <= s_stop]; % 信号灯窗口, 以枚举法为例, 只选某个目标窗口 Constraints = [Constraints, x(N) == s_stop]; % 停车线到达约束 % 到达时间由位置递推隐含, 若要限制到达时刻在窗口内, 可加时间变量 t_pass = N * dt; % 简化示例, 实际需要显式时间变量 Objective = sum(N - (1:N)) * dt + 0.5 * sum(a.^2); ops = sdpsettings('solver', 'gurobi', 'verbose', 0); optimize(Constraints, Objective, ops); v_opt = value(v);实际中如果要显式约束到达时刻在绿灯窗口内,我会增加一个时间变量t,然后加约束t = N*dt以及窗口上下界。上面代码做了简化。
下层能量管理的代码结构类似,变量变为P_fc和P_bat,核心约束是功率平衡和SOC递推。SOC递推实际上是一组线性等式,可以直接写进约束,不需要额外调用求解器。
4.4 仿真参数和验证场景怎么设置
参数的合理性决定仿真结果的说服力。我给一组常见参数,是我在项目里用过而且验证过物理合理的:
| 参数 | 数值 |
|---|---|
| 整车质量m | 1500 kg |
| 滚阻系数f_r | 0.015 |
| 风阻系数C_d | 0.3 |
| 迎风面积A | 2.2 m^2 |
| 传动效率eta_drive | 0.9 |
| 燃料电池最大功率 | 50 kW |
| 电池容量 | 6 Ah |
| SOC上下限 | 0.3 / 0.9 |
| 限速 | 15 m/s |
| 信号周期 | 60 s |
| 绿灯时长 | 30 s |
| 交叉口距离 | 300 m |
验证场景我建议至少设三组。第一组是“绿灯可直接通过”,验证优化器给出匀速通过轨迹。第二组是“红灯无法避免”,验证车辆提前减速滑行并在停车线前等待。第三组是“绿灯还剩半程”,验证优化器是否选择加速通过还是放弃减速等下一周期,这个场景最能体现权重alpha和beta的博弈。
仿真完成后,对比以下指标:总通行时间、总氢耗、SOC终值、最大减速度。理想情况下,生态驾驶策略相比不做速度优化的基准策略(比如看到红灯才刹车),氢耗能降低10%到25%,具体数字取决于场景。
5. 我踩过的坑和调试经验
5.1 凸性被破坏的几种情况
凸性问题是这个项目最容易翻车的地方,而且翻车方式五花八门。我遇到的最常见的情况是在目标函数里加入了v和a的乘积项,比如模拟瞬时功率时写P = m * a * v,这个项在变量同时包含加速度和速度时很难保持凸性。CVX或者YALMIP会直接报错或者给出非预期的结果。
排查这种问题,我会写一个最小的测试脚本,固定其他变量,单独检查目标函数矩阵的特征值。如果目标函数的二次型矩阵不是半正定的,就说明目标函数本身不凸。
另一个常见的坑是把信号灯窗口的“或”约束直接当成线性约束写进问题,导致可行域变成非凸集合。优化器给出的解可能是“穿过”红灯的物理不可行轨迹。这个问题的排查方法是画图看速度轨迹是否在红灯时段越过停车线,如果越过了,几乎可以断定是约束建模问题。
5.2 双层迭代不收敛怎么处理
双层迭代不收敛的表现主要有两种:一是SOC终值不满足要求,二是速度轨迹在两次迭代之间来回跳动,总氢耗不下降。
SOC问题通常出在下层目标函数里的终端惩罚项权重太小。解决方法是把lambda从100逐步往上调,调到500甚至1000,直到SOC终值落在参考值附近。还有一种更直接的方式,是给SOC设置一个终端硬约束,但硬约束会让可行域变小,有时候会直接导致问题无解,所以要谨慎。
速度轨迹来回跳的问题,我前面提到过,用阻尼更新解决。实际操作中还要注意信息传递的量纲。上层反馈给下层的是速度轨迹,下层反馈给上层的是氢耗对功率的敏感系数。如果两边的量级差太多,修正项会淹没主目标函数。我习惯把敏感系数除以一个标准化因子,让修正项的量级和主目标项保持一致。
5.3 求解太慢怎么办
求解慢是双层迭代最影响体验的问题。我的经验是先减少时间步数,dt从0.1秒放宽到0.2秒,N减半,求解时间几乎以平方级别下降,但速度轨迹的精度损失很小,因为城市生态驾驶本来就不需要极高的时间分辨率。
第二个有效手段是避免在迭代循环里反复调用sdpvar定义变量。变量定义放在循环外,循环里只更新目标函数和约束的具体数值,利用YALMIP的assign和value机制复用已有模型,速度能提升好几倍。
第三个手段是给求解器设置合适的容许误差。QP问题不需要把最优解精度逼到1e-9,设置OutputFlag关闭日志输出,再把求解器容差从默认的1e-8放宽到1e-4,速度提升非常明显,而氢耗差异在0.1%以内。
5.4 从仿真到高保真验证还有多远
Matlab里面跑通的双层凸优化只是第一步,离实际部署还差好几个层级。我常用的验证路径是:先用低阶模型验证算法逻辑,再用高保真模型验证控制效果,最后才考虑硬件在环。
高保真模型里必须加入低阶模型忽略的项,比如坡度、道路曲率、传动系统动态、燃料电池响应延迟。模型精度提升后,经常出现的情况是:低阶模型里SOC轨迹很平滑,高保真模型里因为附加阻力项,SOC出现明显波动,下层能量管理可能出现频繁切换充放电的状态。这个问题的本质是低阶模型的目标函数里没有加入对电池功率波动的惩罚,在高保真验证时加上一个小的波动惩罚项,可以显著改善控制效果。
通信延迟也是仿真到实车的一大差距。实车场景下,V2I信号有几十毫秒到几百毫秒的延迟,SPaT信息不是完美的。处理方式是在上层速度规划时对绿灯窗口做保守处理,把窗口的结束时间提前0.5秒,留出安全余量。
我个人做这个方向最大的感受是:“双层凸优化”听起来高端,实际上大部分时候就是“两个QP来回迭代”加上“把非凸项想方设法写凸”。只要把车辆模型、信号灯约束、下层能量管理写清楚,从Matlab到仿真验证的链路其实很快。最后提醒一句:不要在信号灯窗口的“或”约束上硬上二进制变量,除非问题规模很小。先枚举候选周期,大概率够用。如果后续想做实时部署,可以把上层换成查表或神经网络,下层保留QP,这又是一个能写文章的扩展方向。