1. 为什么要给电动车续航做蒙特卡洛模拟:单点估算解决不了“万一”
要用一句话说清楚这件事的话:电动车续航从来不是一个固定值,而我们每个人真正关心的其实是它的分布。
我刚接触电动车辆数据分析时,有一台测试车标称续航400公里。夏天在城市里来回跑,实际能开到390左右;到了冬天跑高速,开着暖风,同一辆车经常只有260出头。这种差异不是个案,而是物理规律叠加使用习惯后的必然结果。但有一个问题几乎没人认真回答:如果我只想保证“这台车跑一趟300公里的路,中途不充电”,我的置信度到底是多少?是直接出发还是先充电更稳当?
用确定性公式去算,无非是代入一个平均速度、一个温度、一个负载,得到一条续航曲线。可是现实中平均速度不会固定在某个点,温度也不是恒定的,空调什么时候开到最大、行李装多重、路上有没有坡,这一切都有波动。单点估算最大的问题就是它把不可能同时出现的极端值混在一起,或者反过来,把所有不利因素都摊均匀了,结果既骗自己也算不出风险。
蒙特卡洛方法恰好是给这类问题准备的。核心思路很简单:把每个不确定参数当成随机变量,按它们各自可能的范围采样,然后完整跑一遍车辆能耗模型,记录结果。重复几千到几万次之后,你会得到一组“续航结果样本”,而不是一个数字。对这组样本做百分位统计,就可以回答“100次出行里,有多少次会出现电量告警”或者“充电时间最长的一次用了多久”。
我选 Matlab 来做这件事,一是因为它的数组运算和矩阵操作很适合蒙特卡洛这种重复计算场景,二是符号计算与绘图工具就在一起,模拟完直接出分布图,不需要再倒一次数据。2024a 之后的版本跑这种 1 万次循环的模型完全没有压力,代码写清楚了,后续改参数、加输入维度都方便。你完全可以把它看作一套“概率化续航评估框架”,而不仅仅是一个计算脚本。
这套模型核心要处理的对象是两个:行驶里程对应的能量消耗,以及充电时实际补充进去的功率。前者决定“我还能跑多远”,后者决定“我要充多久”。这两者的分布合在一起,才能回答完整的问题。接下来我把它们的建模方法、仿真代码、结果解读和常见坑逐一展开。下面的章节我尽量写成可以直接照抄的研究笔记,而不是教科书式的理论推导。
2. 行驶里程的物理基础:先把确定性公式搭对,再注入随机参数
2.1 行驶阻力与百公里电耗:我常用的一组基准参数
续航仿真的第一步,是把车辆行驶过程中的能量消耗用物理公式描述出来。如果这一步偏差太大,后面再怎么蒙特卡洛都是白搭。我习惯把行驶工况简化为恒速段叠加,因为对于长距离续航评估来说,频繁加减速的瞬态过程对总能耗的影响可以通过一个效率系数吸收掉。
车辆匀速行驶时,需要克服的阻力主要有三项:
- 滚动阻力:F_roll = m × g × Crr,m 是整车质量,g 取 9.8,Crr 是滚动阻力系数,一般轿车在 0.010~0.015。
- 空气阻力:F_drag = 0.5 × ρ × Cd × A × v²,ρ 是空气密度,Cd 是风阻系数,A 是迎风面积,v 必须换算成米/秒。
- 坡度阻力:F_grade = m × g × sin(θ),θ 是道路坡度。平路可以忽略,山区路线建议加上。
驱动电机提供给车轮的功率就是这些阻力之和乘以车速。再加上传动效率 η,整车从电池侧消耗的功率大约是:
P_electric = (F_rolling + F_drag) / η × v + 辅助负载
如果要从这个功率换算到“百公里电耗”,更直观的写法是:
E_per_100km = (P_electric × 100) / v_avg
其中 P_electric 单位是 kW,v_avg 单位是 km/h。这样算出来的是 kWh/100km,是我们日常看表显电耗最熟悉的单位。
我常用的一组车辆参数是:整备质量 2100 kg(含乘员和行李大约到这个水平),风阻系数 0.27,迎风面积 2.3 m²,滚动阻力系数 0.012,驱动系效率 0.88。代入上面公式,按平均速度 90 km/h 计算,电机牵引能耗大约是 14.5 kWh/100km。加上空调、音响、灯光这类辅助负载之后,整车电耗通常在 16~20 kWh/100km 之间。注意这个值本身不是固定的,它随速度升高而上升,尤其是空气阻力与速度的平方成正比,跑高速比市区挪动要敏感得多。
2.2 温度衰减、辅助负载与可用电量:最容易忽略的随机源
确定性公式只是骨架。实际仿真里,每个输入都应当按“现实中它可能取什么范围”来随机化。我最看重三个随机源。
第一个是环境温度。锂电池在低温下内阻增大、可用容量下降,零下 20℃ 时可用容量通常只有常温的七到八成;反过来,超过 30℃ 后热管理又会让电量折损一点。我一般用一个分段插值函数来近似温度对电池容量的影响。
第二个是辅助负载。这里最容易被低估。常温下,空调压缩机功率在 1.5~3 kW,冬季 PTC 加热器烧起来可以到 4~7 kW,再加上座椅加热、除雾,电耗不是小数目。按 100 km/h 跑一个小时,能耗多出 2~4 kWh,对于一块 60 kWh 的电池来说就是几个百分点的 SOC。蒙特卡洛里如果不把辅助负载设为随机变量,结果往往过于乐观。
第三个是电池可用容量的动态变化。电池额定容量是 60 kWh,但日常使用中不可能真正把它用到 0,保护策略一般允许放电到 5%~10% SOC;同时电池老化(SOH 下降)也要考虑。所以仿真中我从来不写“用满 60 kWh”,而是把可用能量写成:
usableCap = batteryCap × SOH × tempFactor
SOH 我通常设置成 0.9 甚至 0.85 来模拟中后期车辆的真实状态。这个值放到蒙特卡洛里再乘以一个随机波动系数,就能覆盖不同批次电池之间的差异。
2.3 把驾驶风格变成分布:平均速度与路线长度的建模思路
驾驶风格的随机化没有标准答案,我的做法是按路线场景组织。假设要评估“从城市边缘到异地的高速路程”,我就将路程采样为一个连续均匀分布或三角分布,比如 120~280 km。平均速度也按路段类型采样:城市拥堵 20~35 km/h,郊区 40~60 km/h,高速 90~120 km/h。
对于长距离出行,我更推荐把路线切段建模。比如第一阶段城区、第二阶段高速、第三阶段山区,每段的距离占比和平均速度分别从不同分布里采样,最后把各段能耗累加。这样做的好处是避免了把高速和市区混在一个平均速度里,导致空气阻力项被严重低估。蒙特卡洛本来就是用来处理复杂组合的,没必要为了省事把所有东西压缩成一个标量。
随机变量之间还有一个容易忽略的问题:相关系数。比如低温天气里开暖风几乎是必然的,那么温度低和辅助负载大这两个参数就不应该独立采样,否则会出现“很冷但空调没开”的伪样本。我一般通过条件抽样来处理,至少把辅助负载设为温度的函数,而不是简单粗暴地让二者完全独立。这个细节后面单独展开。
3. 充电功率建模:恒定功率和正态分布都是危险的简化
3.1 真实充电过程几乎不存在“恒定功率”
如果你做过充电桩实测,会发现充电功率是一条随时间变化的曲线,而不是一个固定数字。交流慢充(7 kW 左右)相对平稳,但直流快充的功率曲线非常明显:刚插上时可能瞬间冲到 120 kW,随着 SOC 上升,电池内阻和保护策略会让功率逐步下调,到 80% 以后可能跌到 40 kW 甚至更低。
很多初学蒙特卡洛的人会把充电功率设为常数,比如“桩是 60 kW,那就按 60 kW 算充电时间”。这在慢充场景下误差不大,但是在快充场景下会把充电时间低估 20%~40%。假设一辆车从 10% 充到 80%,平均功率如果只有峰值功率的 60%,那么按峰值算出来的时间自然严重偏乐观。
正确的做法是把充电桩额定功率和车辆接收功率分开看。整车请求功率通常受充电桩上限、电池温度、SOC 阶段三方约束。蒙特卡洛里我习惯分两步:根据 SOC 起始区间决定采用哪一段充电曲线,再根据该区段的平均充电功率计算补充电量所需时间。这样既保留物理真实性,又不会让模型复杂到难以维护。
3.2 为什么不能直接对充电功率用 randn 或者正态分布
这是我在代码评审里反复强调的问题。有些人图省事,直接P = mu + sigma * randn(...)来生成充电功率样本。这样出现的第一个问题是:正态分布样本可能取负值,而充电功率在物理上不可能小于 0。第二,正态分布没有上界,可能生成 300 kW 的充电功率,市面上几乎没有乘用车能长时间吃下这个功率。样本一旦生成,后续的充电时间计算会跟着产生荒谬的数值。
正确做法是使用截断分布。Matlab 自带函数可以直接构造:
pdNorm = makedist('Normal', 'mu', 60, 'sigma', 12); pdTrunc = truncate(pdNorm, 20, 90); % 物理上下限 P_sample = random(pdTrunc, 100000, 1);truncate 会把落在范围外的概率重新归一化到区间内,既保留了正态分布的形状,又不会产生负值或者超常规的大功率。另一个更贴近工程实际的方案是直接用经验分布。你手里如果有某款车型的实测充电数据,用histogram看形状之后,选用对数正态分布或威布尔分布拟合,再通过fitdist生成样本。这样得到的充电功率分布比任何拍脑袋的正态分布都靠谱。
3.3 我用的充电模型:按 SOC 区间映射平均功率
为了兼顾精度与计算速度,我在蒙特卡洛主循环里没有逐秒仿真充电过程,而是把充电曲线离散化成几个 SOC 区间,每个区间对应一个平均接收功率,做成查找表。比如一个 120 kW 的直流快充桩,典型近似可以是:
| SOC 区间 | 平均充电功率(kW) |
|---|---|
| 5%~30% | 100 |
| 30%~60% | 85 |
| 60%~80% | 60 |
| 80%~95% | 35 |
这里数值只是示例,不同电池的热管理能力差别很大。真正的数据应该从实车充电测试里拿,或者至少使用厂家公布的充电曲线做插值。主循环中只要知道当前 SOC 落在哪个区间,就取出对应功率,和充电桩额定功率取最小值,再除以补电所需能量,就得到该段的充电时间。
如果你希望模型更细,还可以加入电池温度对充电功率的影响:冬天冷电池低温下充电功率会被限制,这是另一种随机源。我会把冬天充电功率的上限单独打折,并在truncate的范围设置中体现出来。
4. Matlab 仿真实现:一套可以直接跑通的蒙特卡洛代码
4.1 随机种子与输入参数表:先锁住结果再说
蒙特卡洛可比拼的就是能复现。如果每次运行得到不同结果,你就没法调试对错。所以代码第一行我会写:
rng(42); % 任何固定整数都可以,拿到哪个都一样固定随机种子之后,整个项目对同一个输入参数集合永远给出同一组结果。这在调整模型结构时特别重要——你知道结果的差异来自模型改动,而不是运气。
接着把车辆固定参数当作结构体集中管理,不要散落到循环里到处魔改数字。这样后期更换车型、对标不同电池版本,只需要改一个地方:
veh = struct(); veh.mass = 2100; % kg veh.cd = 0.27; veh.area = 2.3; % m^2 veh.crr = 0.012; veh.rho = 1.2; % kg/m^3 veh.eff = 0.88; veh.batteryCap = 60; % kWh veh.soh = 0.9;4.2 主循环拆解:一次模拟到底在算什么
我建议先用普通的 for 循环把逻辑写明白,再去优化性能,因为蒙特卡洛的核心是可读性。下面这段代码基本是我最早版本的原型,之后只是在细节上做加法:
numSim = 10000; finalSOC = zeros(numSim, 1); chargeTimeMin = zeros(numSim, 1); distanceKm = zeros(numSim, 1); tempFactorFunc = @(t) interp1([-20 0 10 20 30], ... [0.72 0.86 0.94 1.0 0.96], t, 'linear', 'extrap'); for i = 1:numSim % --- 随机输入 --- distanceKm(i) = 120 + 180 * rand; % 单次出行距离 120~300 km plausibleSpeed = [25 55 80 95 110]; % 典型路段车速 avgSpeed = plausibleSpeed(randi(5)); % 从典型路段中随机抽一个 temp = -5 + 15 * rand; % 冬季场景 -5~10°C auxLoad = 2 + 3 * rand; % 辅助负载 2~5 kW % --- 能耗模型 --- F_roll = veh.mass * 9.8 * veh.crr; F_drag = 0.5 * veh.rho * veh.cd * veh.area * (avgSpeed / 3.6)^2; drivePowerKW = (F_roll + F_drag) * (avgSpeed / 3.6) / 1000 / veh.eff; totalPowerKW = drivePowerKW + auxLoad; consumedKWh = totalPowerKW * distanceKm(i) / avgSpeed; % 总耗电量 % --- 电池可用容量 & 到达SOC --- usableCap = veh.batteryCap * veh.soh * tempFactorFunc(temp); socArrivePct = 100 * (usableCap * 0.95 - consumedKWh) / usableCap; % --- 充电过程:补电并记录时间,否则记录剩余电量 --- if socArrivePct < 5 socArrivePct = 5; % 电池保护兜底 end startSOC = socArrivePct / 100; targetSOC = 0.8; if startSOC >= targetSOC chargeTimeMin(i) = 0; else neededKWh = (targetSOC - startSOC) * usableCap; if startSOC < 0.3 avgChgPow = 100; % 快充高功率段 elseif startSOC < 0.6 avgChgPow = 85; else avgChgPow = 60; end chargeTimeMin(i) = neededKWh / avgChgPow * 60; end finalSOC(i) = socArrivePct; end这个循环的逻辑一句话总结:随机生成一次出行条件,算一遍能耗,看剩余 SOC,再按 SOC 区间取充电功率算充电时长。跑完 1 万次之后,finalSOC 和 chargeTimeMin 就是两个随机样本集合,后续所有统计都围绕它们展开。
注意这里的 avgSpeed 是随机离散值,不是连续分布的。你也可以改成连续均匀分布,但用离散值的好处是能明确代表不同场景,不容易出现“平均速度 87.3 km/h”这种十分别扭的样本。如果想更平滑,可以在每段速度内再加正态扰动。
4.3 性能优化:一万次循环只是起步,五万次怎么办
上面的循环在普通台式机上跑 1 万次,速度很快,不需要优化。但如果你要跑 5 万次甚至 20 万次,有几个优化手段是立竿见影的。
第一个是parfor。把 for 换成 parfor,同时确保每个迭代之间没有依赖关系,代码基本不需要改。我第一次用的时候犯过一个小错:循环里给distanceKm(i)写值这种方式在 parfor 中是完全允许的,但要确保变量不是成长数组。提前预分配好输出数组就不会报错。
第二个是使用“分层采样”替代纯随机采样。蒙特卡洛的随机样本有时会出现局部密集、局部稀疏,尤其整体概率分布尾部样本不足时,会导致尾部百分位估计不准。可以用lhsnorm或者自己写拉丁超立方采样工具,先把参数分布均匀切层再抽样,保证尾部也被覆盖到。
第三个是减少无谓的重复计算。比如 tempFactorFunc 是对每个温度样本做插值,如果百万次循环,这个插值开销也不小。可以先把插值表预计算成密集表格,循环里直接查最近点。性能提升肉眼可见。
5. 结果解读:把分布图看懂,你才算真正完成仿真
5.1 从直方图到百分位:平均值没有给到你的信息
仿真跑完,先画直方图:
figure; histogram(finalSOC, 30); xlabel('到达SOC (%)'); ylabel('频数');如果直方图是右偏的,说明多数情况下电量充足,但少数恶劣条件下 SOC 被压得很低。这时候你会特别想知道最差的 5% 是什么水平。
Matlab 一行就能拿到分位数:
Q = quantile(finalSOC, [0.05 0.25 0.5 0.75 0.95]);假设输出是 [18, 32, 45, 58, 71],含义是:在最差的 5% 场景里,到达电量只剩 18%;一半的场景能达到 45%;最好的 5% 场景能有 71% 剩余。显然,如果你要保证“不充电也能完成出行”,看平均值 45% 是没有意义的,你需要看的是 5% 分位数 18%,或者更严格地看 1% 分位数。
5.2 充电时间的分布:覆盖一个区间才能做充电规划
充电时间同样不要只看一个数。将 chargeTimeMin 画出来之后,大概率是长尾分布——多数情况一小时出头,偶尔因为低温快充功率被限制,充到 80% 要两三个小时。
我通常会同时算两个指标:中位充电时间和 90% 分位充电时间。中位数代表“一般体验”,90% 分位代表“我该按最坏情况预留的时间”。如果 90% 分位是 2 小时,那在沿途充电站规划时就要按 2 小时而不是 1 小时来排行程。
再进一步,可以按充电桩类型拆分。比如将慢充桩(7 kW)和快充桩(平均 80 kW)分别跑一遍,对比充电时间分布的重叠程度。这个对比结果可以直接支撑充电站建设评估:一个快充站到底要配几个枪,才能把排队时间的 90% 分位压到用户可接受的范围。
5.3 这套结果的实际用途:从车型评估到路线预判
蒙特卡洛的最终价值不是输出一堆图形,而是进入决策流程。我现在做车型评估时,会把不同车辆的参数分别跑同一套随机输入,看它们的“5% 分位续航”差多少。这比拿标称续航对比更有说服力,因为标称续航是理想条件,而 5% 分位续航反映了天气、路况与负载的联合压力。
做长途出行规划时,我会直接把仿真结果转化为“电量告警概率”:统计 finalSOC 低于 20% 的次数占总模拟次数的比例。如果这个比例是 8%,就意味着跑这条路一百次大概有八次会陷入低电量的被动局面。再结合沿途充电站间隔距离,就能判断是否需要在中途安排一次充电。
6. 我踩过的坑,以及让模型更可信的几个实操建议
6.1 最经典的翻车:随机参数没有物理边界
我有一次图省事,直接用randn生成环境温度,结果是仿真样本里出现了 40℃ 和 -30℃ 同时大量存在的情况,最后绘制的直方图两边长尾特别夸张,整条温度曲线跟实际季节完全对不上。从那以后,凡是物理量有明确边界的,我都用均匀分布、截断正态分布,或者直接用实测经验数据。仿真里允许出现不合理的样本,错的不一定是模型,但一定影响结论置信度,必须从源头防住。
6.2 参数相关性:低温与暖风是一个组合,不是两个独立事件
前面提过低温与暖风的相关性。我第一版模型把温度设为随机变量,辅助负载也独立采样,结果跑出来一组“零下 10℃但辅助负载只有 1 kW”的样本。这在实际生活里几乎不会发生。解决办法也不复杂,先把温度采样,然后让辅助负载分布的条件期望跟着温度走,比如:
temp = -10 + 20 * rand; if temp < 0 auxLoad = 4 + 2 * rand; % 天冷,暖风必然开大 else auxLoad = 1.5 + 1.5 * rand; % 天热或春秋,空调温和 end蒙特卡洛不是要求你把所有因素都独立随机化,合理的做法是让因素之间保持真实世界的耦合。这一步做不做,对尾部结果影响特别大。
6.3 校准永远是最重要的一步:用真实行程回流模型
模型再漂亮,没有实测数据校准就是自嗨。我建议至少找 10~15 条真实行程记录,包括出发 SOC、实际到达 SOC、环境温度、辅助负载大概水平。把这些输入喂回仿真模型,生成对应到达 SOC,再与真实到达 SOC 对比。如果偏差都在 5% 以内,模型基本可信;如果系统性偏低或偏高,就要回头检查 η、滚阻系数和辅助负载分布是否合理。
另外,温度系数不能只靠一个插值表拍脑袋,最好用同一款车在夏天和冬天的数据分别标定。我自己的经验是,温度影响的斜率比想象中陡,尤其 0℃ 附近变化很快。插值表里面的节点值得花一点时间用实测值替换。
6.4 扩展方向:加入排队、电池老化和动态电价
这套蒙特卡洛框架的扩展性很好。我后来在充电时间模块里加入了充电桩排队模型,把随机到达的车辆数和每个桩的占用时长做成二次蒙特卡洛,于是充电站吞吐量评估也能做。电池老化方面,给 SOH 加一个随使用里程衰减的过程,就能预测几年后同一段出行路线的 5% 分位续航会掉到什么水平。动态电价则是在充电时间计算之后再叠加一个电费成本分布,用来算不同充电策略的费用期望。
如果你要把这些全部塞进同一个模型,建议保持模块化:能耗计算、充电曲线、排队过程各自独立成函数,主循环只负责采样和调用。这样模型复杂度增加时,不至于把调试变成灾难。
从我自己的经验来看,蒙特卡洛 + Matlab 的组合在电动车辆续航这类多随机变量问题上,确实是性价比很高的做法。核心成本不在代码,而在输入分布的建模和校准。只要这两步做扎实,结果能给你的决策带来相当大的确定性——这里的确定性指的是对不确定性的量化,而这才是蒙特卡洛真正该有的价值。