1. 为什么一个"找最低油耗点"的需求会变成一个独立程序
1.1 OOL计算程序到底在解一个什么问题
第一次接到这个需求是在一个混合动力能量管理预研项目上。当时标定工程师甩给我一张发动机万有特性图,图上密密麻麻全是油耗率等值线,同时抛过来一个问题:"程序里能不能把最优工作线算出来?发动机照着这条线跑,整车的油才算得到位。"
我一开始的想法很天真:最优工作曲线,不就是每个转速下面找一个油耗率最低的扭矩点嘛,连线就完事了。真动手写MATLAB m编程才发现,这个"最优"在不同场景下定义完全不同,连数据都还没理清楚,更别提交给整车控制器用了。
OOL(Optimal Operating Line,最优工作曲线)本质上回答的是一个问题:发动机在满足某个功率需求的前提下,转速和扭矩应该取什么组合,让燃油消耗率(或综合代价)最低。说穿了,这是一个带约束的二维寻优问题——约束是功率,目标是油耗,自变量是转速和扭矩。
这个程序解不解决实际问题?解决。纯燃油车只有一条固定档位关系,能落地的工况点有限;但混动、增程、以及带有独立驱动电机的系统,发动机转速和扭矩在许多工况下是解耦的——电机可以帮忙"垫"扭矩或者"扛"转速,发动机完全可以在任意工况组合下运行。这时候如果只靠标定工程师在图上手动描点,既慢又容易漏掉边界工况,程序化计算就是刚需。
而且"最优"这两个字在不同开发阶段含义也不一样。做整车能量管理标定的时候,最关心的是稳态最低油耗率曲线;做排放预控的时候,可能要在油耗和排放之间做加权;做NVH避让的时候,还得把振动噪声区域剔除掉再找最优。
1.2 为什么这个场景适合用MATLAB m编程
顺便说说为什么我坚持用MATLAB m编程而不是别的方案。早些年同事用Excel拉数据透视表也没少干,但Excel在处理二维插值和批量寻优时确实吃力,数据一多公式就卡;后来也试过用Python重写,numpy、scipy确实能干,但是项目团队当时整套标定与仿真链路都建在MATLAB上,数据导出导入的格式、后处理工具的衔接全都依赖m文件,用MATLAB写这个OOL程序,把结果直接喂给仿真模型就是无缝对接。加上MATLAB里griddata、contourf、fminbnd这些函数几乎是为这种问题量身定做的,所以最后选型毫无悬念:m语言,矩阵化,函数封装。
严格来说,MATLAB m编程在这里的价值分成三层:一是数据层——把不规则的台架散点整理成规则网格;二是算法层——把物理约束翻译成寻优逻辑,让"最优"可以被计算机精确定义;三是工程层——输出结果能直接给标定、仿真、控制策略复用的格式。任何一个层次偷懒,最后都可能在实车标定阶段付出更大代价。
这篇文章就是把我当时从拿到台架数据、到输出那条被标定工程师认可的OOL曲线、再到把程序封装进能量管理仿真工具的完整过程复盘一遍。重点讲清楚三件事:数据怎么处理才不会算出荒唐结果、寻优逻辑怎么写才符合工程实际、以及代码落地的时候有哪些坑容易被年轻人踩进去。
2. 台架数据到手之后,先别急着写寻优代码
2.1 原始试验数据的字段和组织方式
发动机台架试验通常输出两类数据:一类是稳态工况点的点测值,一类是万有特性扫描的网格数据。格式上来讲,最常见的字段是转速n(rpm)、扭矩T(Nm)、燃油消耗量B(kg/h)或燃油消耗率BSFC(g/kWh),有的台架还给进气温度、冷却液温度、NOx排放等附加通道。OOL计算其实只需要三个物理量:转速、扭矩、BSFC。
这里有一个细节值得专门说:如果原始数据给的是油耗量B,而不是油耗率BSFC,一定要先把B换算成BSFC。换算公式是:
BSFC = B / P × 1000
其中P是发动机输出功率(kW),P = T × n / 9550。很多新手直接拿B画等高线,画出来其实是一条"小时油耗量"曲线,跟油耗率的物理意义完全不同。小时油耗量高的点不一定油耗率高,大扭矩高转速的工况小时油耗量肯定大,但这不代表发动机不经济。这个换算错误我在评审别人的程序时看到过不止一次。
数据结构上我比较推荐用四列表格组织试验数据:
| 字段 | 单位 | 说明 |
|---|---|---|
| n | rpm | 发动机转速 |
| T | Nm | 发动机扭矩 |
| B | kg/h | 燃油消耗量 |
| BSFC | g/kWh | 由B换算得到 |
2.2 数据清洗:坏点剔除是最后一道防线
台架数据看起来漂亮,但里面永远藏着坏点。常见的坏点来源有:试验工况没完全稳定就开始采集、传感器接线松动导致扭矩值跳变、发动机失火或爆震时的瞬时油耗异常等。如果坏点不剔除,插值出来的BSFC曲面会出现莫名其妙的"坑",而寻优算法最喜欢找坑——最后算出来的OOL会突然跳到一个物理上不现实的工作点。
我的做法分三步。第一步,先用散点图把n-T-BSFC画一遍,肉眼扫一遍可疑点,比如BSFC动辄上千的点直接标出来人工复核。第二步,用转速和扭矩作为输入、BSFC作为输出,做一次简单离群检测,比如残差超过3个标准差的点标记出来。第三步,也是最容易忽略的——检查边界数据是否覆盖完整的常用工作区。很多台架试验为了省时间,只扫了部分负荷线,如果用这样的数据求最优工作线,外推区间会产生误差。
注意:数据清洗要保留完整的操作记录。谁在什么时候剔除了哪个点、依据是什么,这些信息在评审和后期复算时非常重要,别等到被追问的时候才翻聊天记录。
2.3 插值成标准网格:griddata和interp2怎么选
原始试验点通常是不规则的,比如等油门开度扫出来的点落在斜线上,直接做寻优没法高效检索。惯例做法是把散点插值到规则的n-T网格上,形成二维BSFC矩阵。
这里要说一下griddata和interp2的区别。griddata接收散点坐标,直接在任意网格点上插值,语法是:
[NG, TG] = meshgrid(n_grid, T_grid); BSFC_GRID = griddata(n_raw, T_raw, BSFC_raw, NG, TG, 'linear');而interp2要求输入本身是规整的网格矩阵,适用于原始数据已经比较规整、只需要微调网格密度的情况。大多数台架原始数据都是不规整的,所以我的选择基本固定:首选griddata做散点网格化,后续如果需要细化再配合interp2重采样。
关于插值方法,我的原则是:平面数据只用'linear',不要为了曲线好看用'spline'或'cubic'。原因后续第5章会细说,先在前面给个结论——样条插值很容易在数据稀疏区域拉出波形,而波形会在寻优时被当成"最优解"选中,非常误事。
3. 寻优逻辑设计:两种"最优工作线"必须分清
3.1 方式一:最低油耗率曲线(MFC)
最低油耗率曲线指的是:对每一个离散转速,在允许的扭矩范围内找到BSFC最低的那个扭矩点,把这些点连起来。它的物理含义是"这台发动机理论上最经济的转速-扭矩组合轨迹",多用于发动机本体能力的评估和标定参考。
实现非常直接:
n_opt = zeros(size(n_grid)); T_opt = zeros(size(n_grid)); BSFC_opt = zeros(size(n_grid)); for i = 1:length(n_grid) bsfc_col = BSFC_GRID(:, i); % 当前转速列 T_col = TG(:, i); valid = ~isnan(bsfc_col); local_idx = find(valid); [BSFC_opt(i), j] = min(bsfc_col(valid)); T_opt(i) = T_col(local_idx(j)); n_opt(i) = n_grid(i); end这里有个容易写错的小细节:min函数在valid索引后的子数组上返回的序号j,是相对于子数组的,不是相对于原列的。必须用local_idx(j)把它换算回原数组的索引,再取对应的扭矩值。我第一次写的时候就踩了这个坑,导致最优扭矩串位,画出来的曲线七扭八歪。
3.2 方式二:给定功率需求下的最优工作线(等功率寻优)
MFC曲线解决的是"发动机最省油的点在哪",但整车控制实际问的更准确的问题是:"现在整车需求功率是25kW,转速和扭矩应该怎么分配最省油?"这就是等功率约束下的寻优:在功率曲线上寻找BSFC的最小值点。
为什么要区分?因为MFC曲线上的点,各自对应不同的功率,整车需求功率往往是连续变化的。强行按MFC的功率去匹配,常常无法满足实际功率需求。真实控制器里用到的OOL,通常更像第二条曲线——一条"功率-转速-扭矩"的映射关系,给一个功率值,输出最优转速和最优扭矩。
在程序里实现等功率寻优,我在网格数据上用的是掩码法:
P_GRID = TG .* NG / 9550; P_req = 25; % kW tol = 0.5; % kW mask = abs(P_GRID - P_req) < tol; bsfc_candidate = BSFC_GRID(mask); idx = find(mask); [~, j] = min(bsfc_candidate); n_opt = NG(idx(j)); T_opt = TG(idx(j));掩码法的优点是直观、不易出错,缺点是功率容差tol的选取直接影响结果精度。tol太大,候选点跨了好几条功率线,求出来的"最优"可能严重偏离实际功率需求;tol太小,候选点太少,网格分辨率不够时可能漏掉真正的最优点。
更严谨的做法是借助等高线函数contourc,直接提取目标功率在n-T平面上的等功率线坐标,然后在这条线上做一维插值和搜索:
C = contourc(NG(:,1), TG(1,:)', P_GRID, [P_req P_req]); % 解析C,得到等功率线的n和T坐标列 % 再在这条线上对BSFC插值并求最小值这个思路精度更高,但解析contourc输出结构稍微绕一点,适合封装成独立函数后使用。实车标定用的OOL表,我基本都用等功率寻优来生成,因为它直接对应控制器的需求输入。
3.3 带禁区区域的扩展:NVH和排放约束
工程上"最优"从来不是纯油耗最优。某个转速-扭矩点油耗很低,但如果正好落在NVH敏感区,整车噪声值超标,那这个点再省油也不能用。处理方式是把禁区点先罩住,不让寻优逻辑选中。
实现上我习惯做一个权重矩阵weight,与BSFC同尺寸,在禁区内设为Inf,其他区域为1。寻优时对BSFC加上权重惩罚:
BSFC_PENALTY = BSFC_GRID .* weight;这样min函数天然会自动跳过Inf区域。如果一个转速下所有扭矩点都在禁区内,就把该转速标记为不可用,OOL曲线在那个转速区间断开,程序返回NaN,上层控制逻辑再处理降功率或换挡策略。
4. m代码落地:从散乱脚本到可用函数的封装过程
4.1 主程序结构设计
最开始我写OOL计算时是一个大脚本,从读数据到画图全在一个文件里。后来发现每次改转速范围、换发动机型号,都要复制粘贴一大段,非常痛苦。后来按要求把它拆成几层:
第一层是数据读取层,负责加载试验数据、清洗、插值网格化,输出NG、TG、BSFC_GRID三个矩阵。第二层是寻优层,输入网格和约束信息,输出最优工作点数组(n_opt、T_opt、BSFC_opt、P_opt)。第三层是可视化层,负责绘制万有特性图并叠加OOL。
拆分之后最大的好处是,不同发动机只改数据读取层,不同优化目标只改寻优层,界面的显示逻辑完全不用动。这个分层思想在整个项目后期帮了大忙——有一次要换成另一款发动机的数据,我只花了半天就把整个流程重新跑通了。
4.2 函数接口设计建议
我建议把寻优部分封装成一个纯函数,对外接口设计得越窄越好:
function [n_opt, T_opt, BSFC_opt] = calcOOL(NG, TG, BSFC_GRID, P_req, mode) % mode - 'mfc' 最低油耗率曲线 % - 'power' 等功率最优工作线里面先判断mode,再分别走两条计算通路。这个函数不关心数据怎么来的,符合最小接口原则,也便于后续放进整车能耗仿真工具里跑批。命名上,函数名用动作开头,加注释说明物理量单位,过半年回头再看还能一眼看懂。
4.3 绘图:把万有特性和工作线叠在一张图上
标定工程师拿到OOL,要能直接跟万有特性图对照。所以可视化这一步不能用简单的plot,建议用contourf先画油耗率云图,再把OOL以粗红色线叠上去:
figure; contourf(NG, TG, BSFC_GRID, 20, 'LineColor', 'none'); hold on; plot(n_opt, T_opt, 'r-o', 'LineWidth', 2); xlabel('转速 (rpm)'); ylabel('扭矩 (N.m)'); colorbar;如果画等功率寻优的OOL,还可以在图上叠加若干条等功率线,用虚线表示,这样能直观看到每一个需求功率点与最优工作点的关系。我习惯再用scatter把原始试验点叠上去,方便排查数据覆盖性。
4.4 一个完整的最小可运行示例
为了演示方便,我用一个模拟BSFC曲面跑通整个流程。模拟数据不代表真实发动机,但流程是真实的。
n_line = 1000:200:4000; % 转速 T_line = 20:20:180; % 扭矩 [NG, TG] = meshgrid(n_line, T_line); P_GRID = TG .* NG / 9550; % 模拟BSFC:一个碗形区,中高负荷最低,两端升高 BSFC_GRID = 230 + 0.002*(TG-100).^2 + (P_GRID-40).^2/40 + (NG-2500).^2/1e4; [n_opt, T_opt, BSFC_opt] = calcOOL(NG, TG, BSFC_GRID, [], 'mfc');跑完会得到一条从低转速到高转速逐渐爬升的扭矩曲线,这很符合道理——高转速时摩擦功大,需要更高负荷才能进入经济区。
提醒一句:模拟的BSFC公式是我随手写的物理趋势函数,真实程序里永远不该出现这种式子,真实数据必须来自台架或仿真标定,否则算出来的OOL没有任何工程意义。
5. 真实项目里踩过的坑:外推、网格与性能
5.1 外推陷阱:NaN导致的假最优
我最早一版程序有个严重问题:某些转速下网格边界之外被griddata填了NaN,我没有过滤就直接求min,而MATLAB的min函数默认忽略NaN,结果min返回了一个空集,程序直接报错。后来改成显式过滤isnan才稳定下来。
比报错更隐蔽的问题是外推出的假数据。griddata默认不进行外推,所以BSFC_GRID在数据覆盖范围外是NaN,这是好事。但有同事图方便用了带外推的插值方式,把边界外的BSFC补成了很大的数,寻优却把边界点当成最优选中了——因为外推点在边界外,物理上根本没有真实试验支撑,这是绝对不能用的。所有涉及边界外的情况,一律返回NaN,让上层逻辑显式处理。
5.2 试验点稀稀拉拉时,线性插值也会失真
线性插值不是说不会失真,而是失真方式可控。试验点太稀疏的时候,插值出来的BSFC曲面会在某些区域形成斜长条状的"谷",寻优会沿着谷走,导致OOL出现不合理的锯齿。
我的经验是:网格分辨率要和试验点密度匹配。如果试验点平均间隔在200rpm、20Nm,那么输出网格的间隔不要小于试验点平均间隔的一半,也就是100rpm、10Nm左右。太细的网格只会放大插值噪声,不会带来真实的精度提升。
5.3 循环还能再提速吗:向量化与预分配
如果只是计算一条OOL,循环完全无所谓,几千个转速点也就是毫秒级。但这个程序如果放进优化框架里做批量计算——比如和驾驶循环仿真嵌套,一次要算几万个工况——就需要考虑性能。
两个最基本的优化:一是预分配数组,不要在循环里不断append;二是把最内层循环向量化。比如计算整张BSFC_GRID的isnan掩码,一次完成,而不是在每个转速列里反复判断。再进一步,可以用逻辑索引压缩候选点,但一般用不上,前两步足够。
% 不推荐:每次循环动态增长数组 for i = 1:N T_opt = [T_opt, new_value]; % 数组反复扩容,慢 end % 推荐:预分配再填值 T_opt = zeros(N, 1); for i = 1:N T_opt(i) = new_value; end5.4 多解与平缓谷:最优曲线居然不稳定
这是最有意思的一个坑。有些转速下BSFC随扭矩变化非常平缓,最低油耗率附近横跨20-30Nm,BSFC几乎不变。这时候数值上求出来的最小点,对网格密度、插值方案极其敏感:上一步算出来最优扭矩是110Nm,稍微改一下插值方法就跳到125Nm。这个不稳定性会让标定工程师非常困惑。
我的处理方式是引入"平台中心化":当最优扭矩附近的BSFC波动小于某个阈值(比如2 g/kWh)时,取整个平缓区间扭矩的平均值作为最优扭矩,而不是取单个最小点。这样算出来的OOL更稳定,也更符合人在图上手工选点的直觉。
具体做法是:
bsfc_col = BSFC_GRID(:, i); valid = ~isnan(bsfc_col); T_valid = T_col(valid); bsfc_valid = bsfc_col(valid); [bsfc_min, ~] = min(bsfc_valid); flat_mask = abs(bsfc_valid - bsfc_min) < 2; % 2 g/kWh平台阈值 T_opt(i) = mean(T_valid(flat_mask));这个"平台区间取均值"的思路,后来被我用到所有版本的OOL计算里,实测曲线平滑很多,也更能经受评审的追问。
6. 从一条曲线到一个决策工具:OOL的延伸应用
6.1 混动能量管理:查表代替实时寻优
OOL做出来不是给人看看的,最终要进控制器。在实车嵌入式环境里,实时做二维寻优通常不现实,更常见的是离线算好一组不同功率需求下的最优(n_opt, T_opt),做成lookup table。整车控制系统根据当前功率需求直接查表,得到目标转速和目标扭矩,再通过扭矩协调和转速调节去逼近这个工作点。
这时候OOL程序的输出格式就很重要了。我建议除了输出曲线数组,还要同时输出一张"功率-最优转速"插值表,以及"功率-最优扭矩"插值表。控制器查表时只需要一次一维插值,计算量极小。
6.2 与动态规划类策略的关系
做混动能量管理研究的同事常问我:OOL和动态规划(DP)算出来的最优工作点有什么区别?我理解是这样的——OOL是稳态最优,前提是发动机已经在某个工作点上稳定运行;DP是全程最优,考虑电池SOC变化、起停惩罚、模式切换代价等时间维度约束。所以OOL常常作为DP的一个启发性初值或者降维工具,先固定发动机沿OOL运行,再优化电池功率分配。两者不是替代关系,是不同层级的决策。
6.3 扩展方向:效率面、电驱OOL与热管理耦合
OOL的思路完全可以迁移到电驱系统。电机的效率map同样存在最优工作线的问题,只不过目标函数从油耗率换成综合效率(考虑电机、控制器、电池的损耗)。我后来在另一个项目里把同一套MATLAB代码改改目标函数,直接算出了电机的最优效率工作线,流程几乎不变,只换了输入数据的语义。这说明程序架构的价值在于"输入网格、输出最优线"这个通用框架,而不在于某一条具体的物理曲线。
更复杂的扩展是考虑发动机暖机过程——水温低时油耗率地图其实会整体抬升,最优工作线也会移动。如果项目周期允许,可以在数据准备层加一个"热状态修正"接口,按水温对BSFC map做平移或缩放,再走同一套寻优逻辑。这个方向目前我还在做初步验证,但至少架构上不需要推翻重写。
我最后想说的是,OOL计算程序真正让人头疼的往往不是那个寻优循环,而是从试验数据到可靠网格的那一段基本功。数据处理干净了,寻优代码半小时就能写出来;数据没处理干净,后面优化算法写得再花哨,出来的曲线也经不起标定工程师一句"这个点不对吧"。
如果你也在写类似的程序,建议先把"功率-转速-扭矩-油耗"四个量的物理含义在脑子里过一遍,再动手写代码。等功率约束下的优化目标到底是什么、最低油耗率点和等功率最优点的区别是什么,这两件事想清楚,你的OOL程序就已经赢了一大半。剩下的坑,填一填就过去了。