☰
电动汽车多目标削峰填谷优化调度:从建模到MATLAB实现
2026/10/21 9:40:36 网站建设 项目流程

我一直觉得,电动汽车有序充电是负荷侧资源里最值得琢磨的一块。电动汽车很特殊,它同时具备两个身份:从电网看,它是一堆移动负荷,无序接入会让本来就紧巴巴的晚高峰雪上加霜;从用户和系统看,它又是一块移动储能,完全可以在负荷低谷时把电存进来,在高峰时段通过V2G反向送出去。所以围绕电动汽车的多目标削峰填谷优化调度,核心从来不是把代码写得花哨,而是想清楚目标函数和约束背后,电网侧要什么、用户侧要什么、电池又受得了什么。

这篇文章我就结合一套可以扩展复用的MATLAB实现,把从问题建模到代码落地的完整链路拆开讲,包括目标函数怎么构造、约束条件怎么列、求解器怎么选、结果怎么调。无论你是刚接触有序充电,还是已经在做微网调度,都应该能从里面找到可以直接拿来用的那部分。

说明:文中所有案例均为虚构的模拟场景,负荷曲线和车辆参数为演示数据,不代表任何实际电网或小区。

1. 电动汽车调度这件事,为什么要扣住削峰填谷不放

先说一个背景现象。普通家庭用电有一个很明显的日内节奏:凌晨到清晨是负荷低谷,白天爬升,傍晚到晚上迎来尖峰。电动汽车如果无组织地接入,大多数车主是下班回家就插枪,这就把充电负荷直接叠加到了本就最高的晚高峰段上。日复一日,变压器过载风险、线路损耗、调峰压力都会放大。

而电动汽车参与削峰填谷的逻辑恰好相反:把充电行为尽可能引导到负荷低谷时段,把部分电量在高峰时段放出来补贴负荷,从而让电网的总负荷曲线变得更平坦。这个思路在技术圈已经喊了很多年,但真正落到代码层面,很多人第一步就卡住了——到底用什么数学量来刻画“填谷”?目标函数和约束条件谁来优先?“充放电优化”到底是优化钱还是优化负荷曲线?

1.1 电网负荷峰谷矛盾与电动汽车的双重身份

电网运行讲究供需实时平衡。负荷低了,机组低负荷运行,效率下降;负荷高了,需要启动更多调峰资源,成本上升。峰谷差越大,系统对调峰容量的需求就越高,所以从规划到运行,大家都希望负荷曲线尽量平稳。

电动汽车介入之后,事情变得有意思了。一台普通家用车,电池容量按50kWh算,7kW慢充接上后一晚上可以充进接近50度电;如果充电桩支持双向充放电,这辆车在高峰时段甚至能以7kW向电网反向送电。几十台车聚合起来,就是一个小型储能电站。这种双重身份决定了电动汽车调度的核心价值:既要避免无序充电变成电网负担,也要把充电资源变成可调节的灵活性资源。

不过千万别把削峰填谷理解成“只是让总负荷曲线低一点”。它更准确的数学描述是:让总负荷曲线各个时刻的离散程度变小,晚高峰的尖峰被削掉,凌晨低谷被填起来。目标函数选得好不好,直接决定你算出来的调度表是“看起来不错”还是“真能落地”。

1.2 无序充电的代价:先算一笔瞬时的账

我拿一个模拟园区场景做个粗略计算。假设园区的基础负荷在晚高峰大约为5000kW,变压器容量5500kW,看上去还有500kW裕量。这时候来50辆电动汽车,每辆以7kW同时开始充,新增负荷是350kW,变压器瞬时接近满载;如果运气不好,正好叠加在基础负荷尖峰上,就会触发负载告警。

同样350kW的充电负荷,如果推迟到凌晨1点到5点之间充电,情况完全不一样。凌晨基础负荷可能只有2500kW,即使加上350kW充电负荷,离变压器容量也富裕很多。而且凌晨电价更低,用户成本也会下降,对电网和车主是双赢。

从这两笔账能看到,削峰填谷的本质是“时间平移”:充电功率本身总量没变,变的是它出现在哪个时段。优化算法要做的就是决定每一辆车在每一个15分钟时段里,到底是充电、放电还是闲着。

1.3 多目标优化里的三个“目标”需要同时谈

为什么标题里要强调“多目标”?因为这个问题天然包含几个互相拉扯的方向。

第一个目标是系统侧的削峰填谷目标,也就是让总负荷曲线最平坦。第二个目标是用户侧的经济目标,包括购电电费最低、放电收益最大。第三个目标同样不能忽视,是电池寿命目标,频繁和深度放电会加速电池老化,如果完全不考虑这个代价,算法会让车在高峰时段疯狂放电,表面好看,实际不可持续。

这三个目标不是完全对立的。峰谷电价差比较大的地区,“削峰填谷”和“省钱”往往方向一致;但放电带来的电池损耗会让经济性变差,所以必须通过权重来平衡。后文会给出一个完整的数学表达。

2. 数学模型先于代码:目标函数与约束条件的取舍逻辑

动手写代码之前,我强烈建议先建模。很多人一上来就打开MATLAB写变量,写到一半才发现约束缺了或者目标函数量纲不对,回头改一遍相当于重构。

2.1 时间离散化与决策变量定义

调度周期一般取24小时,步长取15分钟或者1小时。15分钟是行业内比较常用的粒度,既覆盖了日内峰谷变化,又不会让问题规模爆炸。一天24小时分成96个时段,记作Ntime=96,每个时段长度Δt=0.25小时。

电动汽车数量记为Nev。决策变量有三个矩阵:

  • P_ch:Ntime×Nev,表示每辆车每个时段的充电功率,单位kW;
  • P_dis:Ntime×Nev,表示每辆车每个时段的放电功率,单位kW;
  • State:Ntime×Nev,0/1变量,1表示该时段处于充电状态,0表示不充电。

可能有人会问,为什么非要加一个0/1的State变量?因为物理上不允许同一辆车在同一时刻既充电又放电。如果没有State变量,优化器可能会钻空子:同时让P_ch和P_dis都取正值,功率虽然大,但实际能量凭空多出来一块,结果没有任何物理意义。

2.2 削峰填谷目标:用负荷均方差量化“平坦程度”

总负荷由两部分组成:基础负荷L_base(t)加上电动汽车净充电功率:

L_total(t) = L_base(t) + Σ_i P_ch(i,t) - Σ_i P_dis(i,t)

要衡量负荷曲线的平坦程度,最常用的是均方差:

f1 = (1 / Ntime) · Σ_t ( L_total(t) - mean(L_total) )²

这个值越小,说明负荷曲线围绕平均值的波动越小。均方差的好处是它对所有时段的偏离都敏感,不像“峰谷差”只关心最高和最低两个点。单纯用峰谷差作为目标,算法可能把峰削下来,却在其他时段弄出新的小尖峰,整体曲线依然乱糟糟。

2.3 充放电成本与电池损耗目标

用户侧的经济目标可以写成:

f2 = Σ_t Δt · ( p_buy(t) · Σ_i P_ch(i,t) - p_sell(t) · Σ_i P_dis(i,t) ) + Σ_i Σ_t γ · P_dis(i,t) · Δt

前半部分是电费支出减放电收益:p_buy(t)是t时段的购电价,p_sell(t)是放电上网或馈反馈给本地负荷的结算价。后半部分是我个人强烈建议加上的电池退化惩罚项,γ是单位放电电量折算出的电池损耗成本,单位是元/kWh。实测下来,γ给每kWh设0.3~0.6元,可以明显抑制算法过度放电的冲动,可以让结果更接近真实运营。

2.4 目标函数汇总与权重归一化

多目标合并最常见的方式是加权和:

F = α · f1 / f1_base + β · f2 / f2_base

这里f1_base和f2_base是不做优化时原始场景的值,除以它俩是为了归一化。原因很简单:f1的量纲是kW²的均值,数值可能是几百万;f2的量纲是元,数值可能是几千。如果直接加权,不管α和β怎么取,f1都会把f2淹没。归一化之后,两个目标都变成“相对无优化场景的倍数”,权重α和β才有实际意义。

如果α=1、β=0,算法只削峰填谷,不管成本和电池;如果α=0、β=1,算法只顾省钱,可能所有车都在凌晨集中充,虽然电费低了,但负荷曲线上又可能出现一个新的“充电峰”。所以实际工程里通常取中间值,并且会做权重敏感性扫描。

下面这张表列出了三种常见的削峰填谷目标表达,可以按场景选:

目标形式表达式优点缺点
负荷均方差最小f = E[(L(t)-L_mean)²]曲线整体平滑,目标连续可导对个别尖峰惩罚不够直接
峰谷差最小f = max(L)-min(L)直观,直接压尖峰只关注两个点,容易产生次生尖峰
最大负荷最小f = max(L)直接缓解变压器过载会让负荷尽可能平均,可能影响经济性

2.5 约束条件:不把约束列全,结果一定“跑飞”

再好的目标函数,缺了约束也是空中楼阁。我梳理一下这套框架里必须包含的几类约束:

第一类是功率上下限和充放电互斥约束:

0 ≤ P_ch(i,t) ≤ State(i,t) · Pmax_i 0 ≤ P_dis(i,t) ≤ (1 - State(i,t)) · Pmax_i

Pmax_i是第i辆车的最大充放电功率,工程上可以取充电桩的额定功率。

第二类是电池SOC递推约束:

SOC(i,t+1) = SOC(i,t) + (η_ch · P_ch(i,t) - P_dis(i,t)/η_dis) · Δt / Cap_i

η_ch和η_dis分别是充电和放电效率,Cap_i是电池容量。这个约束把功率决策和电池能量状态联系起来,是整套模型的“灵魂”。Yalmip里写这一条时,要注意时间下标对齐,别把t和t+1写错。

第三类是SOC上下限和离网目标约束:

SOC_min ≤ SOC(i,t) ≤ SOC_max SOC(i, t_depart_i) ≥ SOC_target

SOC_min一般取0.2,SOC_max取0.95,这是为了给电池留安全裕度。SOC_target是用户期望离开时的电量,通常取0.8~0.9。

第四类是车辆在网时间约束。车不在停车位上的时段,P_ch和P_dis都必须为0。这需要通过一个0/1的Availability矩阵来实现,Availability(i,t)=1表示第i辆车在t时段连接充电桩。很多初学者会漏掉这一条,导致算法让一辆“早上已经开走了的车”继续充电,结果图好看,实际不能执行。

第五类是配电容量约束,通常写成:

L_base(t) + Σ_i P_ch(i,t) - Σ_i P_dis(i,t) ≤ P_trans

P_trans是变压器或馈线的安全上限。削峰填谷做到最后,不能把变压器给顶过载,这是底线。

3. MATLAB代码的框架设计:从数据生成到求解的一条龙

模型理清楚了,代码骨架其实就很自然了。我习惯把代码拆成主脚本加三个函数:数据生成、优化建模、结果可视化。这样好处是后面换数据、换求解器都不用动主流程。

3.1 参数预设:数据集中放开头,方便批量修改

所有外部参数都集中放在主脚本开头,不要散落在代码各处。包括时段数、电动汽车数量、每辆车电池容量、最大充放电功率、充放电效率、SOC上下限、目标SOC、电价曲线等。

这些参数一旦定义好,后面函数全部通过结构体传参。我建议用struct封装,比如param.dt、param.Nev、param.p_buy。别用一堆散装全局变量,否则调参的时候改一个漏一个,很容易出问题。

3.2 典型日负荷曲线生成:手工构造还是读取数据

做算法验证时,不需要真实负荷数据,也可以先用手工构造的典型曲线把代码流程跑通。用正弦函数叠加一个晚高峰形状,再叠加一点随机波动,就能模拟出峰谷形态。下面是一段示意代码:

%% 基础参数 Ntime = 96; % 一天96个时段 dt = 0.25; % 每个时段15分钟,单位小时 Nev = 50; % 电动汽车数量 Cap = 50 * ones(Nev, 1); % 电池容量,单位kWh Pmax = 7 * ones(Nev, 1); % 最大充放电功率,单位kW eta_ch = 0.95; % 充电效率 eta_dis = 0.95; % 放电效率 SOC_min = 0.2; % 电池SOC下限 SOC_max = 0.95; % 电池SOC上限 SOC_target = 0.9; % 用户离网目标SOC %% 构造典型日基础负荷曲线 t_h = (0:Ntime-1)' * dt; % 每个时段对应的小时刻 % 基础负荷:夜间低谷约2800kW,晚高峰约5000kW Load_base = 2800 + 700 * exp(-((t_h - 5).^2) / 4) ... + 1500 * exp(-((t_h - 19).^2) / 3) ... + 120 * randn(Ntime, 1);

这段代码的意思很简单:凌晨5点左右叠加一个小幅早起负荷峰,晚上7点附近叠加一个明显的晚高峰,同时加一点随机波动模拟真实噪声。randn那条在验证时可以关掉,不然每次跑出来的结果都不一样。

3.3 用Yalmip建模还是手写fmincon,我为什么推荐前者

很多老代码还在手写fmincon做非线性约束优化,但我建议新项目直接用Yalmip建模,尤其是当问题包含0/1整数变量的时候。

手写fmincon的问题在于:SOC递推约束是一个跨时段耦合约束,你要把所有变量摊平成一维向量,然后把96×Nev条等式约束挨个装配到Aeq矩阵里,写起来非常痛苦。而且一旦出现充放电互斥约束这种整数变量,fmincon默认算法是不能直接处理的,需要改求解器,要绕很多弯。

Yalmip的好处是建模语言接近数学表达,约束直接写在constraints里,求解器选择也可以灵活切换。下面这个表格是我实际对比下来的感受:

建模方式可读性约束装配复杂度整数变量支持求解速度
手写fmincon低高,容易下标错乱弱,需混合算法一般
Yalmip建模高低,约束直接拼接支持良好取决于后端
手写整型规划矩阵很低极高支持,但难维护较快

所以后面核心代码我都用Yalmip语法来写。如果你暂时没有Yalmip,也没关系,代码逻辑同样可以套用到其他建模语言或直接手写矩阵。

4. 充放电策略的核心实现与注释风格拆解

“详实注释”这几个字是很多人找代码的重点。我见过的注释大概分两种:一种是解释“这句代码是干什么”的流水账,另一种是解释“为什么这么写、物理含义是什么、约束从哪里来”的工程注释。后者才真正有价值。

4.1 决策变量定义与主目标函数代码

先看这一段核心的Yalmip建模代码:

%% 决策变量 P_ch = sdpvar(Ntime, Nev, 'full'); % 充电功率,每个时段每个车辆一个变量 P_dis = sdpvar(Ntime, Nev, 'full'); % 放电功率,单位kW State = binvar(Ntime, Nev, 'full'); % 0/1状态变量,1=充电,0=不充电 %% 目标函数1:削峰填谷(总负荷均方差最小) % 每个时段电动汽车净功率 = 所有车充电功率之和 - 所有车放电功率之和 Load_ev = sum(P_ch - P_dis, 2); % 96x1向量 Load_total = Load_base + Load_ev; % 加基础负荷 f1 = sum((Load_total - mean(Load_total)).^2) / Ntime; %% 目标函数2:用户成本与电池损耗 % 第一项:购电费用;第二项:放电收益;第三项:电池退化惩罚 Cost_buy = dt * sum(price_buy .* sum(P_ch, 2)); Income_sell = dt * sum(price_sell .* sum(P_dis, 2)); Cost_degrad = dt * gamma_battery * sum(sum(P_dis)); f2 = Cost_buy - Income_sell + Cost_degrad; %% 总目标 F = alpha * f1 / f1_base + beta * f2 / f2_base;

关于注释,我有几个建议:第一,每个变量后面都要标注物理单位和含义,比如“单位kW”这种;第二,每个目标函数块至少要说明“为什么用这个表达式”,比如均方差对应的是整体平滑而非单一峰值;第三,关键公式要能和第二节的数学模型对号入座。

4.2 约束条件装配:最容易出错的地方

约束装配我会按五类分别写注释,每类之间空一行,方便回头检查:

Constraints = []; %% 约束1:充放电功率上下限与互斥 % 同一个时刻只能充或放,State就是做这个事情的 Constraints = [Constraints, 0 <= P_ch <= Pmax_vec .* State]; Constraints = [Constraints, 0 <= P_dis <= Pmax_vec .* (1 - State)]; %% 约束2:SOC递推 % 注意t=1时使用了初始SOC,t>=2时使用上一时段SOC SOC = sdpvar(Ntime, Nev, 'full'); for t = 1:Ntime for i = 1:Nev if t == 1 Constraints = [Constraints, SOC(t, i) == SOC_init(i)]; else eta_energy = dt / Cap(i); Constraints = [Constraints, SOC(t, i) == ... SOC(t-1, i) + eta_ch * P_ch(t, i) * eta_energy ... - P_dis(t, i) * eta_energy / eta_dis]; end end end %% 约束3:SOC上下限与离网目标 % 离网时刻由depart_idx(i)记录,该时刻SOC必须达到目标值 Constraints = [Constraints, SOC_min <= SOC <= SOC_max]; for i = 1:Nev Constraints = [Constraints, SOC(depart_idx(i), i) >= SOC_target]; end %% 约束4:车辆在网时间窗口 % Availability(i,t)=1表示该车该时段可调度,0则禁用充放电 Constraints = [Constraints, P_ch <= Availability_mat .* Pmax_vec]; Constraints = [Constraints, P_dis <= Availability_mat .* Pmax_vec]; %% 约束5:变压器容量安全 % 所有负荷之和不能超过变压器上限 Constraints = [Constraints, Load_total <= P_trans];

这里要特别提醒一下Availability窗口和SOC递推的配合。SOC递推是全天连续的,哪怕车不在停车位上,SOC状态也会保留;但和它相乘的P_ch和P_dis只在Availability=1时才能非零。所以车的SOC不会因为“人离开了”就凭空变化,只是无法充放电,符合实际。

4.3 求解与结果还原:算出来的矩阵怎么变成调度表

求解代码很短:

%% 求解 ops = sdpsettings('solver', 'your-solver', 'verbose', 2); result = optimize(Constraints, F, ops); %% 结果还原 P_ch_opt = value(P_ch); % 最优充电功率矩阵 P_dis_opt = value(P_dis); % 最优放电功率矩阵 SOC_opt = value(SOC); % SOC变化矩阵 Load_ev_opt = sum(P_ch_opt - P_dis_opt, 2); Load_total_opt = Load_base + Load_ev_opt;

得到结果后我建议立刻做三件事:第一,画总负荷曲线,看峰值是不是被削下来、谷值是不是被填上去;第二,画一辆典型车的SOC曲线和功率曲线,确认它在离家期间没有功率输出、离网前SOC达到了目标;第三,打印总成本、放电电量、峰谷差这几个指标,作为后续调参的基线。

5. 跑通之后必须回答的四个问题:调参、鲁棒性和验证

代码能跑起来只是第一步。实际调试过程中,90%的时间都花在“结果不合理”上。我把自己经常被问到的问题整理成四个方向,下面逐一讲。

5.1 权重系数怎么定:削峰效果与电池寿命的拉锯

权重α和β的取值没有绝对标准,但可以通过一个简单方法确定:先跑α=1、β=0的纯削峰场景,得到一个最大削峰潜力;再跑α=0、β=1的纯经济场景,得到一个最低成本基线。然后在这两个极端中间的几个权重点各算一次,看目标值变化。

我常用的一个小套路是:把γ(电池退化成本)和β绑在一起调。如果β增大之后,算法开始疯狂放电、电池吞吐量暴增,就说明γ设低了,需要拉高γ压制过度放电。这个“拉锯调参法”比瞎调权重直观得多。

5.2 常见五个坑及排查思路

我把自己踩过、也帮别人排查过的典型问题整理成一张表:

现象可能原因排查方法
结果出现同时充电和放电State变量缺失或约束写错检查互斥约束;检查State是否定义成binvar
某辆车SOC离网前不达标SOC_target约束下标错位打印depart_idx,确认离网时段是否正确
负荷曲线削出一个“新充电峰”权重设定不合理,全部车挤在同一时段充增加时间窗口限制或给谷时段充电功率上限
凌晨负荷被填得过高目标函数只压峰不压谷改用均方差目标而不是只限制最大负荷
求解器报无解或不可行约束互相矛盾检查Availability矩阵,确认车辆在家时段与SOC目标是否可能同时满足

无解这个坑最容易发生在“车辆离家很早,但目标SOC很高”的组合上。比如一辆车凌晨6点离家,要求SOC目标0.95,而可用充电时段只有凌晨1点到6点共5小时,用7kW充不了那么多,模型自然无解。解决方法是把SOC_target改成动态约束:根据额定功率和离家时间反推一个可达到的目标值。

5.3 如何用历史数据做闭环验证

演示场景跑通后,如果要给真实项目用,一定得做闭环验证。我通常的做法是:选两周历史负荷数据,第一周用模型逐日优化,第二周把优化结果叠加到真实负荷上,对比削峰前后的峰谷差、变压器负载率、用户充电成本。

关键是要加入误差意识。预测的基础负荷和真实负荷总有偏差,优化结果是基于预测的,实际执行时可能达不到预期削峰效果。这也是为什么下一节要讲滚动优化——静态的一天一刀切方案,在真实场景里远没有滚动调度可靠。

6. 从静态调度走向实时滚动优化

很多论文和演示代码做的都是“已知全天负荷,一次算出全天调度计划”。这作为方法论验证没问题,但放到真实运行里是不够的,因为负荷预测不可能完全准确,车也不会完全按照预设时间进出场。

6.1 为什么静态方案不够用

假设你凌晨0点算出了全天96个时段的调度表,然后原样执行到晚上。结果下午6点基础负荷突然比预测高了5%,一个小区的空调同时开启,变压器接近上限,这时候静态方案没有任何修正手段,调度表里压根没有给“突发的晚高峰”预留调节空间。

所以工程上更常见的是模型预测控制思路:每个调度周期只执行最近一个时段的决策,然后滚动重算。

6.2 滚动时域调度的实现思路

实现上并不复杂。把24小时拆成滚动窗口,每个窗口取未来4到8小时,建立同样的优化模型,但将当前实际SOC作为初始状态。求解后只执行第一个时段的充放电指令,到了下一个时段,重新读取最新SOC和最新的负荷预测,再优化剩余窗口。

这么做的好处是,每次优化都吸收了最新的预测误差和车辆变化信息,决策始终跟着真实状态走。代价是计算频率变高,但对50辆车、96时段的规模来说,现代求解器完全能秒级完成,实时性没有问题。

滚动调度的代码框架和静态版本几乎一样,唯一需要多写的是:一个循环外壳、当前时段SOC快照传入、以及“只执行第一步”的取数逻辑。这套改动不算大,但它能让算法从“仿真玩具”变成“能现场跑的调度器”。

6.3 多目标Pareto前沿:一个轻量级扫描方法

如果不想只用单一权重,还想看看不同目标之间的权衡关系,最简单的做法就是权重扫描。让β从0到1每隔0.05取一个值,每个权重点跑一次优化,把对应的f1和f2记录下来,画成二维散点。这个曲线就是近似的Pareto前沿:曲线上的每一点,都代表一个无法在不损害另一个目标的前提下进一步改善当前目标的解。

实际选点的时候,我会看两个指标:一是曲线拐点位置,拐点附近的解性价比最高;二是在Pareto前沿上找到“削峰效果下降速度突然变快”的位置,把工作点定在它左边一点,这样不会为了最后一点削峰容量而大幅牺牲经济性。

6.4 我的一点调试习惯

最后分享一个我自己的调试习惯:拿到任何一套新的调度代码,先不急着看求解部分,而是先打印所有约束和变量的维度,确认每个矩阵的维度是否匹配。维度不对、下标错位这类问题占了调试时间的一半以上。第二件事是把目标函数拆成两个子目标分别输出数值,看看归一化到底有没有生效,不要只盯着总目标F。

做完这两步再看求解结果,你会发现自己排查问题的速度能快一倍。这套电动汽车多目标削峰填谷优化调度的思路,从模型到代码到滚动扩展基本都在上面了,剩下的就是拿着你自己的负荷曲线和车辆参数,把模型里的默认数据替换掉,然后一遍遍跑、一遍遍调,很快就能找到属于自己场景的那组最优解。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询