简介:面向电力系统与电动汽车研究场景的MATLAB程序包,聚焦蒙特卡洛法在充电负荷预测中的应用,适合电气工程方向学生、电网规划人员及算法初学者。程序基于出行时间、行驶距离、充电模式等随机因素,通过大量抽样模拟充电行为,可输出平均负荷、标准差及概率分布,支撑电网规划与调度决策。压缩包共3个文件,内含两个M源文件与一份DOCX模型说明,整体大小约2.07MB,代码文件分别承担主流程与负荷计算模块,说明文档则梳理了出行特性建模、随机抽样逻辑与结果可视化方法。目前已有2946人学习。通过完整程序可快速复现蒙特卡洛仿真流程,理解车辆保有量、行程分布、充电时段、充电速率和电池容量等参数对负荷曲线的影响,适合作为课程设计、科研预研或工程评估的参考。
1. 为什么电动汽车充电负荷要用蒙特卡洛法
做电网规划或小区配变扩容的时候,最头疼的不是总用电量,而是晚高峰那一段充电负荷到底会冲多高。电动汽车的起始充电时刻、日行驶里程、电池剩余电量都是随机量,简单取平均值算出来的负荷曲线,往往把峰值低估 20% 以上,这对变压器容量选择是危险的。蒙特卡洛法的思路很直接:把每个随机变量按概率分布抽样,组合成一台车的充电过程,模拟几百上千台车,叠加出一天 24 小时的充电功率曲线。这个过程不是求一个确定解,而是把不确定性转化为多条可能曲线的分布,最后取期望、分位数、包络线。对 5 年以上的人来说,真正要关注的不只是跑通程序,而是抽样分布选得对不对、模拟次数够不够、以及结果怎么验证。
2. 建模与抽样:电动汽车充电负荷的随机变量和概率分布
蒙特卡洛法的精度上限由概率模型决定,而不是由随机数质量决定。先要把一台电动汽车从“接入电网”到“离开电网”的过程抽象成可计算的参数集合。
2.1 负荷计算框架:从单台车到车队聚合
单台车的充电过程可以描述为:接入时刻、起始 SOC、充电功率、充电时长。接入时刻一般取到家时刻,起始 SOC 由日行驶里程反推,充电时长由 SOC 目标和充电功率计算。整体计算分三层:
- 单台车:输入随机变量,输出一条充电功率曲线(通常只在 10 分钟或 15 分钟粒度上有值)。
- 多台车:同一时刻叠加所有在充车辆的功率。
- 多次蒙特卡洛:重复整个车队模拟,得到每个时刻的功率分布。
% 生成一天1440分钟的时间轴,10分钟粒度 time_axis = 0:10:1430; daily_curve = zeros(1, length(time_axis)); for i = 1:num_cars [start_idx, duration, power] = simulate_one_charge(); for t = 0:duration-1 idx = mod(start_idx + t, length(time_axis)) + 1; daily_curve(idx) = daily_curve(idx) + power; end end核心是mod处理跨零点的充电场景:如果一辆车 23:50 开始充电,充到 00:30,需要折返到第二天凌晨的时段。很多初版程序在这里直接用线性索引,导致跨零点场景全部丢失,晚高峰之后的负荷段被系统性低估。上面代码的粒度是 10 分钟,对应电网规划里常用的负荷采集间隔;如果要跑 96 点曲线(15 分钟间隔),改time_axis = 0:15:1425即可。
2.2 随机变量与概率分布选择
常用的分布如下,数据来自统计年鉴、GPS 轨迹或调研问卷。注意不同地区差别很大,建议用自己的调研数据替换。
| 随机变量 | 常用分布 | 典型参数 | 备注 |
|---|---|---|---|
| 起始充电时刻 | 分段正态分布 | 均值 18:30,方差 2.5h | 晚高峰前有少量充电,需混合分布 |
| 日行驶里程 | 对数正态分布 | mu=3.2, sigma=0.9 | 单位 km,截断在 300km 内 |
| 起始 SOC | 由里程反推 | SOC = 1 - 里程/续航 | 或直接用截断正态 |
| 充电功率 | 离散分布 | 7kW 占 70%,3.5kW 占 30% | 家用慢充为主 |
simulate_one_charge函数里,我一般用makedist和random搭配:先用pd = makedist('Lognormal','mu',3.2,'sigma',0.9)定义分布对象,再用random(pd)抽取样本。这里有个细节:MATLAB 2018b 之前makedist对截断分布支持不完整,需要自己加while循环过滤,建议在循环外加rng固定种子,否则调试时每次结果都不一样。
起始充电时刻的处理是整段程序里最容易出错的地方。不能只用一个正态分布从凌晨拉到深夜,因为真实数据在 7:00-9:00 和 18:00-20:00 各有一个峰值。常见做法是双峰混合分布:上午峰用均值 8:30、方差 1.5h,晚上峰用均值 19:00、方差 2h,按 0.3/0.7 的概率选择峰值。用rand < 0.3判断落在哪个峰,再分别抽样,比直接拟合单峰分布要稳得多。
2.3 充电模式与功率等级
充电模式决定功率和时间尺度的映射关系。慢充(3.5-7kW)持续时间长,快充(30-120kW)单次只有 20 到 60 分钟。蒙特卡洛程序如果只模拟家用慢充,适合小区负荷评估;如果涉及公共快充站,需要额外引入到达时刻的泊松过程和排队模型。这里给一个简化的双模式实现:
function [start_idx, duration, power] = simulate_one_charge(car) % 选择充电功率等级:70%概率慢充,30%概率快充 if rand() < 0.7 power = 7; efficiency = 0.9; charge_cap = car.battery * (car.target_soc - car.soc0); duration = ceil(charge_cap / (power * efficiency) * 6); % 换算成10分钟块 else power = 50; efficiency = 0.95; charge_cap = car.battery * (0.8 - car.soc0); % 快充通常充到80% duration = ceil(charge_cap / (power * efficiency) * 6); end start_idx = get_start_index(car.arrive_minute); endduration的换算值得注意:charge_cap单位是 kWh,功率单位是 kW,相除得到小时数,乘以 6 得到 10 分钟块的个数。ceil向上取整保证充电需求被完全满足,但也意味着实际充电量会略高于目标,如果后面对峰谷有精确要求,可以改成按段累计电量并在最后一段截断。快充的target_soc设为 0.8,是因为多数快充协议在 80% 后降功率,蒙特卡洛里把这种非线性强行简化成 0.95 的固定效率,误差在可接受范围。
3. MATLAB 实现蒙特卡洛充电负荷:数据准备、主循环与并行化
建模完成后进入实现层面。这里给出一个可运行的最小框架,覆盖数据准备、单次模拟、聚合和结果导出四个环节。
3.1 数据准备:日行驶里程、起始充电时刻与车辆参数表
先建一个车辆结构体数组,每台车包含电池容量、续航、充电功率和接入时刻。对于大规模模拟,用结构体数组比 table 快,因为避免了大表的列访问开销。
% 车辆参数配置 num_cars = 500; % 模拟车辆数 range_ev = 300; % 续航里程km battery_cap = 60; % 电池容量kWh start_hour_peak = [8.5, 19.0]; % 上午峰、晚峰 start_sigma = [1.5, 2.0]; % 对应标准差 daily_mile_mu = 3.2; daily_mile_sigma = 0.9; cars = struct('battery', cell(num_cars, 1), ... 'soc0', cell(num_cars, 1), ... 'arrive_minute', cell(num_cars, 1));然后做抽样。里程抽样后要按续航截断:超过续航的值直接剔除重抽,否则会出现 SOC 为负。截断逻辑写在循环里,用while检查,最多重试 10 次,超过则取边界值——这是为了避免极端随机数让程序陷入死循环。
for i = 1:num_cars mile = 0; retry = 0; while mile <= 0 || mile >= range_ev mile = lognrnd(daily_mile_mu, daily_mile_sigma); retry = retry + 1; if retry > 10 mile = range_ev * 0.95; break; end end cars(i).soc0 = max(0.1, 1 - mile / range_ev); % 选取充电起始时刻 if rand() < 0.3 cars(i).arrive_minute = round(normrnd(8.5, 1.5) * 60); else cars(i).arrive_minute = round(normrnd(19.0, 2.0) * 60); end % 时间边界处理 cars(i).arrive_minute = mod(cars(i).arrive_minute, 1440); end这里用了lognrnd和normrnd,而不是random(makedist(...)),原因是直接用分布函数更快,且不需要重复构造分布对象。时间边界用mod归一到 0-1439 分钟,保证凌晨到达的车辆能被正确映射到当天。需要说明:mod只解决“24:10”这种溢出,不解决正态分布抽样出负值的问题,因此对arrive_minute小于 0 的情况,直接在后面补了一个常量偏移,实操中也可以直接丢弃该样本重新抽。
3.2 主循环:单次蒙特卡洛模拟的函数封装
把单次车队模拟封装成独立函数simulate_one_day(cars, power_mode),方便后面做收敛性分析时反复调用。内部先创建长度为 144 的日负荷数组(10 分钟粒度),然后逐车计算接入时段并叠加。
function daily_curve = simulate_one_day(cars, time_step_min) n_steps = 1440 / time_step_min; daily_curve = zeros(1, n_steps); n = length(cars); for i = 1:n car = cars(i); [p, dur] = get_charge_profile(car); start_idx = floor(car.arrive_minute / time_step_min) + 1; for t = 0:(dur - 1) idx = mod(start_idx + t - 1, n_steps) + 1; daily_curve(idx) = daily_curve(idx) + p; end end endget_charge_profile内部根据 SOC 和电池容量计算充电时长。这层封装的意义在于:后面做有序充电策略时,不需要改主循环,只替换get_charge_profile即可。所有仿真结果的可复现性靠两层保证:外层在simulate_one_day调用前设置rng(seed),内层不用任何全局随机状态。如果某个新版本 MATLAB 的随机数生成算法变了,固定种子会让结果不变,但不同版本之间可能不完全一致,这是正常的。
3.3 聚合与输出:负荷曲线、峰值和分位数
蒙特卡洛跑 K 次之后,会得到一个K x 144的矩阵all_curves。对这个矩阵做三个方向的统计:逐时刻均值得到期望负荷曲线、逐时刻 95% 分位数得到包络线、逐行取 max 得到峰值分布。
mean_curve = mean(all_curves, 1); p95_curve = prctile(all_curves, 95, 1); peak_dist = max(all_curves, [], 2); figure; plot(time_axis, mean_curve, 'b-', 'LineWidth', 1.5); hold on; plot(time_axis, p95_curve, 'r--', 'LineWidth', 1); fill([time_axis fliplr(time_axis)], ... [mean_curve fliplr(p95_curve)], 'r', 'FaceAlpha', 0.15); legend('期望曲线', '95%分位数', 'Location', 'best'); xlabel('时刻'); ylabel('充电功率/kW');用fill绘制期望线与分位数之间的置信区间带,这个带子的宽度直观反映负荷不确定性。峰值分布peak_dist通常不服从正态分布,而是右偏的,因此不要用均值加减标准差描述,直接用prctile(peak_dist, 95)作为规划容量参考值。这段代码里time_axis需要和all_curves的列数一致,如果改了时间步长,记得同步调整。
3.4 并行化:用 parfor 替代 for 加速蒙特卡洛
单次模拟处理 500 台车耗时约 0.2 秒(MATLAB 2021b 之后 JIT 加速效果明显),跑 1000 次需要 200 秒,这时候可以引入并行计算。
% 先开并行池,parfor 自动分配任务 if isempty(gcp('nocreate')) parpool('local', 4); end K = 1000; all_curves = zeros(K, 144); parfor k = 1:K all_curves(k, :) = simulate_one_day(cars, 10); endparfor有几个限制需要记住:循环体内不能有依赖上一次迭代结果的变量;cars变量必须是只读的;随机数的使用要用RandStream在每个 worker 上独立设置,否则多个 worker 可能生成相同序列。实际做法是在循环体第一行加上rng(k, 'twister'),虽然会损失一点速度,但保证了可复现性。并行池的大小不是越大越好,gcp会占用内存,模拟数据规模不大时,4 个 worker 的速度提升通常在 2.5 到 3 倍,再增加 worker 效果递减。还要注意:如果矩阵all_curves太大,parfor的传输开销会抵消计算收益,这个例子里 K=1000、144 列,只有不到 1MB,完全没有问题。
4. 收敛性判断、随机数流与经典参数调优
蒙特卡洛不是跑得越多越好。模拟次数 N 的确定要看输出指标的稳定性,而不是看曲线是否“平滑”。峰值总是比均值更难收敛,所以要分开判断。
4.1 确定模拟次数 N:用变异系数做收敛判据
用一个简单有效的方法:每增加 100 次模拟,计算一次当前峰值的均值peak_mean(i),当前后两组的相对变化率小于 1% 时,认为收敛。
% 增量式蒙特卡洛,检查峰值收敛 batch_size = 100; max_iter = 5000; peak_means = []; for b = 1:max_iter/batch_size batch_curves = zeros(batch_size, 144); for j = 1:batch_size batch_curves(j, :) = simulate_one_day(cars, 10); end all_curves((b-1)*batch_size+1:b*batch_size, :) = batch_curves; peak_batch = max(batch_curves, [], 2); peak_means(b) = mean([peak_batch; peak_means_hist]); if b > 3 rel_change = abs(peak_means(b) - peak_means(b-1)) / peak_means(b-1); fprintf('迭代%d次,峰值均值%.2f kW,相对变化%.4f%%\n', ... b*batch_size, peak_means(b), rel_change*100); if rel_change < 0.01 fprintf('收敛于%d次模拟\n', b*batch_size); break; end end end这段代码里的peak_means_hist需要在外层维护一个累计峰值列表,或者直接把peak_batch累加到总峰值数组里。更简单的方式是计算前 5% 峰值分位数的变化,因为规划时更关注极端情况。实操中,500 台车、典型分布参数下,1000 到 2000 次模拟基本能满足 1% 的收敛要求;如果车辆数增加到 5000 台,单次模拟本身已足够平滑,500 次就能收敛。判断收敛的关键不是看曲线重合,而是看峰值分布的方差是否小到可接受。
4.2 随机数流与可复现性设置
MATLAB 的随机数体系从 2016 版开始基本稳定,rng('default')每次新开 MATLAB 都会重置到梅森旋转算法的初始状态,保证脚本重复运行得到相同结果。但要注意rand、randn和lognrnd用的是同一个全局流,如果在simulate_one_day之外对随机数做了一次测试调用,后面所有抽样都会偏移。正确做法是:
- 脚本开头设置
rng(2024)固定主种子。 - 涉及并行时,每个 worker 用
rng(k, 'twister')设置与迭代索引相关的子种子。 - 调试时禁用并行,用串行模式跑一遍确认结果。
另外,如果蒙特卡洛包里后续要加“充电起始时刻的关联性”——比如某小区的车都集中在 18:00 到 19:00 到家,单纯用独立分布抽样会低估同时率——那就需要引入 Copula 函数做相关性抽样。MATLAB 的copularnd可以生成带相关性的多维样本,这是从“随机独立”走向“随机相关”的关键一步,也是这个标题下最容易扩展的方向。
4.3 分布参数的敏感性与常见误用
以下三个错误在现网程序里最常出现:
第一个是起始 SOC 直接用正态分布抽样,没有用里程映射。两者会得到近似的均值,但峰值负荷差异明显,因为 SOC 低时充电功率可能受电池充电曲线限制,直接用 SOC 分布会漏掉这种边界约束。第二个是充电时长向上取整后没有检查 SOC 上限,导致单台车充电量超过电池容量,这在车队规模大时会让总负荷偏高。第三个是把所有车都假设为到家立即充电,忽略了部分车辆在单位充电的场景。要处理单位充电,只需在get_charge_profile里加一个判断:如果接入时刻在 8:00-17:00 之间,充电功率取公司慢充桩的 3.5kW,时长不变。
matlab版本兼容性方面,脚本里用到的lognrnd、prctile、parfor在 2016b 到 2023b 之间都没有破坏性变更;gcp('nocreate')这个写法从 2013b 就有了。真正容易出问题的是fill画图在hold on后的叠加顺序,以及在不同版本的legend对线型的显示差异。如果公司用的还是 2018a 或 2021b,不需要担心代码跑不了。
4.4 排查仿真异常的三板斧
跑出来的负荷曲线如果是直线或全是零,先查三处:一是arrive_minute是否全部集中在同一时刻,用histogram(cars_arrive_minute, 24)看分布形状;二是daily_curve是否有叠加到错误索引,打印idx的最小最大值;三是充电时长duration是否为零,soc0接近 1 时确实会出现零时长,这种车应该跳过而不是报错。一个快速定位方法是在simulate_one_day里临时加一行assert(all(daily_curve >= 0)),配合try-catch打印出错车辆的编号。很多时候问题出在ceil和mod的组合上:ceil让多充了部分电量,mod让跨天时段被重复叠加,两者叠加会产生微小的总电量偏差,但这个偏差在规划层面可以接受。
5. 结果验证与进阶:信息熵、实测对比与有序充电延伸
模拟结果不能只管自己合理,还要能经得起推敲。最后一章给出三个具体的验证与扩展手段。
5.1 用信息熵评估随机样本质量
判断抽样是否“足够随机”,除了看分布直方图,还可以计算样本的信息熵。把 1440 分钟的负荷曲线按小时聚合,得到 24 维离散概率分布,然后计算其熵。如果熵值明显低于理论最大熵,说明样本集中在少数几个时段,可能是抽样函数写错了,比如起始时刻被错误地裁剪到固定值。
daily_hourly = reshape(mean_curve, 60, 24); p = sum(daily_hourly, 1) / sum(mean_curve); p(p == 0) = []; entropy_val = -sum(p .* log2(p)); fprintf('24小时负荷分布信息熵: %.3f bit\n', entropy_val);一天的负荷曲线如果完全均匀,信息熵是 log2(24) 约 4.585 bit;典型电动汽车充电场景下,熵值在 3.5 到 4.2 之间,太小说明过度集中,太大则说明负荷不够差异化。这个指标也可以用来比较蒙特卡洛抽样与实测数据的分布一致性:对实测负荷做同样的熵计算,两者差值在 0.3 以内说明模型结构合理。
5.2 用 95% 分位数包络线对接配变容量校验
规划人员真正关心的不是均值曲线,而是“最坏情况下的峰值”。从all_curves中提取逐时刻的 95% 分位数和最大值包络,然后与配变额定容量做对比:
transformer_cap = 630; % kVA utilization_95 = max(p95_curve) / transformer_cap; fprintf('95%%分位数峰值负载率: %.1f%%\n', utilization_95 * 100);如果负载率超过 80%,建议把充电策略切换到有序充电模式。这时候只需要调整get_charge_profile里的起始时间,把晚峰时段的一部分车辆延迟到 22:00 后启动充电,即可压低峰值。蒙特卡洛框架天然适合评估这种策略效果:对每台车加一个“延迟时间”随机变量,运行对比仿真即可量化削峰率。matlab的优化工具箱里fmincon可以用来优化延迟策略参数,但实际工程中先用蒙特卡洛做网格搜索就足够了。
5.3 从负荷曲线到有序充电策略的扩展
有序充电的核心是在get_charge_profile中增加一个“可延迟”字段。假设每台车都支持定时充电,默认延迟delay_min分钟启动,则蒙特卡洛的改动只有一行:start_idx = floor(car.arrive_minute / time_step_min) + 1 + floor(delay_min / time_step_min);。跑不同的延迟分布场景,就能得到有序充电的负荷曲线区间带。注意这里的充电总电量不变,只是时间平移,因此平均负荷不变,峰值会下降。离散延迟时间的分布一般选泊松分布或均匀分布,不建议用正态分布,因为现实中用户设定定时充电的行为更接近均匀特征。
更进一步,可以引入实时电价:MATLAB 里直接计算分时电价下的充电费用期望值,这会让蒙特卡洛的输出从一条曲线变成一组“负荷-费用”的联合分布,对充电运营商更有价值。至此,整个程序已经从“算负荷”演进成了“决策支持工具”。
本文还有配套的精品资源,点击获取