做电力方向的项目,尤其是涉及新能源接入和配电网分析的,几乎绕不开一个问题:电动汽车大规模接入后,充电负荷到底长什么样?这个问题看着简单,但真正落地你会发现,每辆车的充电时间、充电时长、剩余电量都不是定死的,而是充满随机性的。我一般直接上 Matlab 写蒙特卡洛算法,把电动汽车充电负荷整个“模拟”出来。这套思路我在好几个项目里用过,既能用来评估小区配电容量,也能给有序充电策略提供量化支撑。如果你正在做相关课题、毕业设计,或者想给充电桩布局找个数据依据,这篇内容应该能给你一套可以直接照抄的框架。
1. 整体设计思路:蒙特卡洛在充电负荷模拟中的定位
1.1 蒙特卡洛方法解决的核心问题
蒙特卡洛算法本质上就是“用大量随机抽样去逼近一个真实过程”。说直白点,现实中你根本没法精确知道明天晚上小区里每辆车几点到家、还剩多少电、会不会立刻充。那你就不猜单个结果,而是把每种可能都按概率让计算机抽一遍,抽几千几万次之后,统计结果就自然收敛到一个可信的规律上。
这就像你掷硬币,单次结果无法预测,但掷一万次,正面比例一定趋近50%。充电负荷也一样——单个车主几点充电不可预测,但一整片车主的群体行为,在足够多样本下会呈现稳定的时间分布特征。蒙特卡洛干的就是这件事:生成一大批“虚拟车主”,让它们各自带着随机的到达时间、随机的起始电量和随机的充电时长去接入电网,最后把所有车的负载叠加起来,就是我们要的负荷曲线。
它和传统解析方法的区别在于:传统方法是先假设“所有车统一在18点、统一剩50%电开始充”,得到一条理想曲线,但实际配电网规划和有序充电策略恰恰需要知道“最坏情况下负荷能冲多高”“95%置信区间有多大”。这些信息只有蒙特卡洛这种概率性模拟能给出来。
1.2 充电行为的概率化建模框架
做蒙特卡洛模拟,第一件事不是急着写代码,而是把一辆车的充电过程拆成几个独立的随机环节。我的习惯是拆成三个:
第一个是“哪天什么时候插上充电枪”,这个变量直接决定了负荷会叠加到一天中的哪个时间段。第二个是“插上时电池还剩多少电”,它决定了这辆车要充多久。第三个是“以多大功率充电”,慢充桩、快充桩、交流桩、直流桩差异很大。
三个变量组合起来,就能确定某一辆车的完整负荷形态。举个例子,一辆车18:30到家、剩余电量40%、接在7kW慢充桩上,那它的充电动作就是从18:30开始、持续约5个小时、每时段增加7kW负荷。把这一套动作重复到几千辆车上,每辆车都有自己的“个性化负荷块”,在时间轴上叠加在一起,自然就形成了一天中从傍晚开始爬升、到深夜逐步回落的充电负荷曲线。
这个“拆环节、定分布、算叠加”的框架,是整个仿真项目的骨架。后面所有代码和参数,都是在往这个骨架里填内容。
1.3 模型边界条件的确定
模型正式搭之前,要把边界条件定清楚,否则算出来的结果没法解释。我最常用的一组默认值是这样的:
| 参数 | 默认取值 | 说明 |
|---|---|---|
| 车辆规模 N | 1000 辆 | 代表一个中型小区或一片集中充电区域 |
| 电池容量 | 60 kWh | 主流纯电车型的中位水平 |
| 单位里程能耗 | 0.18 kWh/km | 偏保守,覆盖冬季衰减 |
| 慢充功率 | 7 kW | 家用交流桩主流规格 |
| 充电效率 | 0.9 | 交流转直流过程有损耗 |
| 时间分辨率 | 15 分钟 | 一天96个时段,计算量适中 |
车辆规模和时间分辨率是两个最关键的边界。车辆太少,负荷曲线会剧烈抖动,看不出规律;时间分辨率太粗,比如按1小时算,晚高峰的负荷尖峰就会被抹平,对配电网分析来说误差太大。15分钟是兼顾计算量和精度的折中值。
2. 关键参数与概率分布:建模前的必做功课
2.1 到达时间用什么分布最合理
充电到达时间,也就是车主插枪的时间,是整条负荷曲线最关键的影响变量。国内居民区充电场景下,绝大多数车主是下班回家后充电,所以到达时间通常集中在17点到21点之间。我用正态分布来建模,中心值取18点,标准差取1.5小时。
arrive_hour = normrnd(18, 1.5, N, 1); arrive_hour = max(0, min(24, arrive_hour)); % 截断到0~24小时这里有几个容易忽略的坑。第一,正态分布会产生少量很早或很晚的极端值,比如凌晨3点到达,这在“回家即充电”场景里不合常理,所以要截断。第二,同样用正态分布,不同场景的参数差异很大——如果是商业区白天充电,中心值要挪到上午9点;如果是电动出租车,则会出现午后和凌晨两个峰。千万不要一套参数打天下。
更精细的做法是分段正态分布,把17点到20点设置高概率密度,其余时段线性递减。这个在学术论文里很常见,工程上如果手上没有实测数据,用单段正态分布也足够支撑配电容量评估了。
2.2 行驶里程与起始SOC的推导
有了到达时间,下一个问题是“车插上时还剩多少电”。直接去给SOC找分布会很别扭,因为SOC和每天的行驶里程是绑定关系。更合理的做法是:先生成日行驶里程,再用里程反推SOC。
日行驶里程我常用对数正态分布来模拟,参数取均值约30到40公里。理由很简单:大部分车主一天就跑二三十公里,偶尔长途跑到一两百公里,这个分布是明显右偏的,对数正态比普通正态拟合得更好。
生成里程并推导SOC的核心逻辑是这样的:
mileage = lognrnd(3.2, 0.6, N, 1); % 日均里程约30km,右偏分布 SOC = 1 - (mileage * 0.18) / Cap; SOC = max(SOC, 0.1); % SOC下限保护,防止过放计算这背后的物理逻辑就是:SOC等于电池“还剩多少”,等于1减去“今天耗了多少电占电池容量的比例”。0.18是每公里能耗,乘以里程就是今天消耗的电量。我设SOC下限0.1,一方面是因为电池管理系统通常会限制深度放电,另一方面也是为了防止出现“充满不了但计算上还要倒扣电”的奇怪结果。
2.3 充电功率与电池容量的配置建议
充电功率不能写成一个固定值。小区里既有7kW慢充桩,也可能有几台60kW快充桩,不同功率对应不同的应用场景和负荷形态。我习惯把功率按概率进行分层:
| 充电类型 | 功率 | 占比 | 适用场景 |
|---|---|---|---|
| 慢充/交流 | 3 kW | 20% | 老旧小区、小功率家充 |
| 常规/家用交流 | 7 kW | 70% | 新建小区标配 |
| 快充/直流 | 50 kW | 10% | 公共快充站、超级充电桩 |
代码里可以根据占比做随机抽样,然后再把对应功率乘进去。要注意的是,快充虽然占比低,但对负荷尖峰影响极大——一辆50kW快充车的瞬时负荷相当于7辆车同时慢充。如果模拟区域公共快充桩占比较高,总负荷曲线会在某个时段突然拔高,这是完全正常的现象,配电网恰恰最关心这种“瞬时冲击”。
电池容量我也建议做一个简单分布,而不是所有车都是60kWh。可以按70%的乘用车60kWh、20%的SUV 75kWh、10%的紧凑型车45kWh来分层。这样模拟出来的结果更贴近真实车辆构成。
3. Matlab仿真实现:从随机车辆到负荷曲线
3.1 初始化、随机数发生与车辆群体生成
工程仿真里最容易被忽略、但最影响结果可复现性的,就是随机数发生器的初始化。同一套模型,如果随机数种子不同,跑出来峰值可能差几个千瓦;如果不设置种子,每次运行结果都变,那就没法追踪问题、没法对比方案。
我自己的习惯是固定rng(2024)这种明确编号的种子,保证任何时间重跑代码都能得到一模一样的结果。需要做多次蒙特卡洛模拟时,再把种子随轮次变化。
生成车辆群体的完整初始化代码是这样的:
clear; clc; close all; rng(2024); % 固定随机数种子,确保结果可复现 N = 1000; % 车辆数 Cap = 60; % 电池容量 kWh efficiency = 0.9; % 充电效率 P_charge = 7; % 慢充功率 kW dt = 0.25; % 时间分辨率,15分钟 T = 24 / dt; % 一天的总时段数,96个 arrive_hour = normrnd(18, 1.5, N, 1); arrive_hour = max(0, min(24, arrive_hour)); mileage = lognrnd(3.2, 0.6, N, 1); SOC = 1 - (mileage * 0.18) / Cap; SOC = max(SOC, 0.1); charge_time = (1 - SOC) .* Cap ./ (efficiency * P_charge);注意这里用了.^、./这种点运算,因为所有变量都是N维列向量,必须逐元素计算。这一段跑完,charge_time里存的就是每一辆车“需要充几个小时”。有些车SOC在90%以上,充电时间只有不到1小时;有些车跑得远、SOC只剩30%,就要充近7小时——这都是合理且符合直觉的结果。
3.2 蒙特卡洛主循环:负荷叠加逻辑
有了每辆车的到达时间和充电时长,接下来就是把它们“放到”96个时段的时间轴上。这部分的物理含义是:每辆车从到达时刻所在时段开始,在之后的若干个时段里,每个时段都贡献一个固定的充电功率。
load_profile = zeros(1, 96); for i = 1:N % 到达时刻对应时段下标 start_idx = floor(arrive_hour(i) * 4) + 1; % 充电持续时段数,向上取整保证不满一段也算一次 duration_idx = ceil(charge_time(i) * 4); for k = 0:duration_idx - 1 idx = start_idx + k; if idx <= 96 load_profile(idx) = load_profile(idx) + P_charge; end end end这一小段是整个仿真的“心脏”。它做的事情其实很朴素:每辆车在到达时段之后,连续若干时段都在耗电,就把这些时段的负荷加上去。if idx <= 96是边界处理,防止充电延续到第二天凌晨时数组越界。
这段代码有几处细节值得展开说。start_idx可能出现0吗?不会,因为到达时间最小是0点,floor(0)+1=1。出现97甚至更高吗?有可能,如果某人23:50到达,换算下来start_idx是96,没问题;如果极端情况生成一个接近24的值,floor(23.98*4)+1=96也安全。duration_idx为什么用ceil?因为充电2.1小时,跨了9个15分钟时段,最后一个不满时段确实也在消耗功率,向上取整更接近物理实际。
3.3 结果后处理:平均负荷、峰值与置信区间
跑完一轮,得到的load_profile只是一次随机实验的结果。真正体现蒙特卡洛价值的是“多轮重复实验后的统计规律”。我一般会跑200到500轮,每轮都重新生成1000辆车,然后对每一时段求均值、求分位数。
M = 200; % 模拟轮数 profiles = zeros(M, 96); for sim = 1:M rng(sim * 1000 + 7); % 每轮不同种子 % 重新生成所有车辆参数(核心代码同上文) arrive_hour = normrnd(18, 1.5, N, 1); arrive_hour = max(0, min(24, arrive_hour)); mileage = lognrnd(3.2, 0.6, N, 1); SOC = 1 - (mileage * 0.18) / Cap; SOC = max(SOC, 0.1); charge_time = (1 - SOC) .* Cap ./ (efficiency * P_charge); load_profile = zeros(1, 96); for i = 1:N start_idx = floor(arrive_hour(i) * 4) + 1; duration_idx = ceil(charge_time(i) * 4); for k = 0:duration_idx - 1 idx = start_idx + k; if idx <= 96 load_profile(idx) = load_profile(idx) + P_charge; end end end profiles(sim, :) = load_profile; end mean_load = mean(profiles, 1); upper95 = prctile(profiles, 95, 1); lower5 = prctile(profiles, 5, 1);这段代码跑完后,mean_load就是每个时段“平均期望负荷”,upper95和lower5给出90%置信区间。画图时,最好用stairs或plot配合fill把置信区间带画出来,阶梯状曲线比光滑曲线更符合“15分钟一个采样点”的真实离散性。
峰值负荷看max(mean_load),极端场景看max(upper95)。这两个数可以直接拿去做变压器容量校核——前者是日常运行参考,后者是安全边界。
4. 踩坑记录与调试经验
4.1 随机数种子与结果稳定性
我在项目初期犯过的最大错误,是忘记固定随机数种子。每次重跑代码结果都不一样,峰值有时是580kW,有时是620kW,根本没法判断是参数改动导致的差异,还是随机波动导致的差异。后来养成了固定种子的习惯,代码顶部一个rng就把这件事解决掉了。
多轮模拟里,每轮的种子也必须有规律,比如rng(sim * 1000 + 7)这种。这样第5轮出问题时,能快速定位到那一轮的随机样本,重新调试。有人会用rng('shuffle'),这在生产环境里不建议,运行结果完全不可复现,出现问题很难排查。
4.2 负荷曲线波动过大怎么办
模拟完发现单次负荷曲线锯齿感极强、峰值忽高忽低,不要急着调分布参数,先从三个方向检查:
| 可能原因 | 表现 | 解决办法 |
|---|---|---|
| 车辆规模太小 | 曲线剧烈抖动,局部峰值异常 | 增加N,比如从500加到2000 |
| 模拟轮数不足 | 多次均值曲线仍有明显差异 | 增加M,达到500轮 |
| 分布尾部过重 | 偶尔出现极端长充电时长 | 对里程和到达时间做截断处理 |
关于车辆规模,有个经验数值:N小于200时,晚高峰负荷几乎就是几辆车叠加的结果,曲线完全没规律;N达到500以上,曲线形态才会趋于稳定;N到1000以上,单次模拟的均值曲线已经很有参考价值了。
4.3 性能瓶颈与向量化改造
for循环版本的问题在于,当N是5000、M是500时,内层有5000次车辆级操作、500轮重复,整体运算量会明显上升,跑起来很磨人。虽然仿真本身不需要实时,但调参数时每改一个数字就要干等几分钟,体验很差。我后来把“车辆内部逐时段累加”的循环砍掉了,改用“事件计数+差分”的方式。
原理其实不难:负荷曲线本质上就是“每个时段有多少辆车在充电”。如果知道每个时段有多少辆车开始充电、有多少辆车结束充电,那当前时段的充电车辆数就是上一时段车辆数加上新来的、减去结束的。用MATLAB的accumarray和cumsum能一次性完成:
start_idx = floor(arrive_hour * 4) + 1; duration_idx = ceil(charge_time * 4); start_idx = start_idx(start_idx <= 96); % 忽略0点后到达 duration_idx = duration_idx(1:numel(start_idx)); % 开始事件:start_idx 时刻多一辆车开始充电 start_event = accumarray(start_idx, 1, [97 1]); % 结束事件:超过96的按第97个虚拟时段处理,代表次日凌晨结束后再减 end_idx = min(start_idx + duration_idx, 97); end_event = accumarray(end_idx, 1, [97 1]); % 差分累加得到每个时段的充电车辆数 charging_count = cumsum(start_event - end_event); load_profile = charging_count(1:96)' * P_charge;这段代码最关键的一行是end_idx的处理。一辆车在23:00开始充电、充4小时,结束时间在次日凌晨3点,但当天96个时段只能看到它0:00时还在充电,所以结束事件放到第97个虚拟位置,代表“今天内它始终不结束”,这样当天最后几个时段的车辆计数才不会出错。这个优化跑1万辆车也基本是瞬间完成,调参效率大幅提升,建议沿用到正式项目中。
5. 从仿真到应用:模型怎么用才能物有所值
5.1 模型验证的现实途径
模型跑通只是起点,关键是结果可不可信。我拿到一条模拟负荷曲线后,第一件事是和真实场景对照。如果手头有某小区充电桩的历史负荷数据,直接对比曲线形态和峰值出现的时间段;如果没有实测数据,就对配电变压器的额定容量做一个粗略校核,比如一台630kVA变压器,晚高峰充电负荷加上基础生活负荷,是否已经逼近甚至超过容量极限。
实测数据更好的地方在于可以反向校准参数。比如发现模拟曲线峰值比实测提前1小时,那可能是到达时间分布的均值设早了,把normrnd的中心值从18点调到19点再跑,直到趋势吻合。这个过程看似繁琐,却是整个仿真项目最有价值的部分——参数贴合本地场景之后,模型才真正具备预测能力。
5.2 模拟结果能支撑哪些决策
这套模型算出来的结果,最常见的应用有四个方向。
第一个是配电容量评估:用max(upper95)和基础负荷叠加,判断现有变压器是否需要增容。第二个是有序充电策略对比:在模型里加入“延时充电”规则,例如所有车辆到达后强制延后2小时再启动,对比峰值下降幅度。第三个是充电桩规划:在不同渗透率下反复运行,看哪个区域的配电网压力最大。第四个是动态电价响应分析:如果分时电价引导用户凌晨充电,重新调整到达时间分布和启动策略,量化削峰填谷的效果。
我实际做过的最有效的一个应用,是在模拟曲线基础上加了一个简单的“延时启动”逻辑:假设30%的用户响应号召、推迟2小时充电,峰值负荷直接下降了约18%。这种量化结论,比拍脑袋说“大家尽量错峰充电”有力得多。
5.3 一些个人的实操体会
最后说点掏心窝的话。这套仿真的代码部分其实只占两成工作量,剩下八成都在参数标定和结果验证上。很多初学者一上来就开始敲代码,把分布、功率填进去跑出曲线就以为完成了,但拿到的曲线值不值得信、对应的场景是什么,往往说不出所以然。
我个人的建议是:先想清楚模型要给谁用、解决什么决策问题。评估变压器容量,重点盯峰值和置信区间上界;制定有序充电策略,重点看负荷曲线的形态变化和削峰比例;做科研课题,则要系统讨论不同分布的敏感性。目标不同,模型配置完全不同。
还有一个很重要的习惯:所有参数必须集中放在代码最前面的参数区,加注释说明取值依据。这样项目放三个月再回来,或者交给别人接手,不至于对着代码猜每个数字是什么含义、哪里来的依据。这个细节看起来不起眼,在实际项目维护中能省下大量沟通成本。