☰
微电网日前经济调度实战:风光储与需求响应的Matlab优化建模
2026/9/30 4:32:46 网站建设 项目流程

1. 引言:为什么“日前经济调度”值得专门写一篇

我最早接触这个题目,是在帮电网公司做微电网规划项目的时候。领导甩来一句话:“你能不能告诉我,明天的光伏、风电、负荷都预测出来了,储能怎么充怎么放、要不要削峰填谷、需求响应叫多少用户,这套东西一天能省多少钱?”我当时心里想,这不就是个优化问题吗?真上手做才发现,坑远比想象的多——光是把储能SOC递推、需求响应负荷平移、购售电互斥这些约束写成Matlab代码,就能花掉好几个晚上。

先说清楚这篇文章是干什么的。所谓微电网日前经济调度,就是在“拿到第二天24小时(或96个时段)的新能源出力预测、负荷预测、分时电价”的前提下,用数学优化的方法,把储能各时段充放电功率、需求响应的削减量/平移量、与大电网的交互功率全部定下来,目标是一个调度周期内的总运行成本最小。再加上“风光储”三个字,意味着光伏和风电的出力受自然条件限制,储能可以双向调节,需求响应则给了调度员一部分“软性负荷调节能力”。这三个元素叠在一起,问题就从简单的经济调度变成了带约束的混合整数线性规划(MILP),而这恰好是Matlab最擅长处理的场景之一。

这篇文章的受众很明确:一是电气工程专业做微电网、智能电网课题的硕士生和博士生;二是刚入行做能源管理、虚拟电厂、园区微电网的工程师;三是想搞清楚“Yalmip工具箱怎么搭配Cplex/Gurobi求解器”的技术爱好者。我用一个完整的、可以直接跑通的Matlab代码框架,把这个模型从数学公式拆到代码实现,再拆到算例结果,最后把我踩过的坑和排查经验全部倒出来。读完你不仅能复现,还能学会自己改模型、加约束。

2. 模型建立:把经济调度问题变成数学问题

2.1 问题描述与基本假设:为什么“日前”而不是“实时”

微电网的日前经济调度,核心逻辑一句话:用昨天的预测,换明天的计划。我们把调度周期取为24小时,把时间粒度取为1小时,一天就是24个时段;如果你做的是精细化调度,可以取15分钟一个时段,那就是96个时段。这里我们先用24时段讲清楚原理,分段越多只是矩阵维数变大,模型结构完全一样。

这个问题的基本假设包括这么几条:

  • 光伏、风电的出力预测曲线已知,且调度过程中严格按照预测值出力(如果要考虑不确定性,得做成鲁棒优化或随机优化,那是后话)。
  • 负荷预测曲线已知,但其中有一部分负荷是可调的,这就是需求响应资源。可调负荷又分为两类:一类是可削减负荷(比如空调温度上调,照明减半),另一类是可平移负荷(比如工业生产线,把白天的用电挪到夜间)。
  • 储能电池的充放电效率、容量、SOC上下限已知,调度周期结束时的SOC要求回到初始值附近(这样才能日复一日循环)。
  • 分时电价的购电价格已知,如果有余电上网的资格,售电价格也已知。
  • 微电网通过公共连接点(PCC)与上级电网交互,交互功率有上限。

这个题设看起来“理想化”,但它就是所有微电网调度问题的基础版本。你以后做的什么“考虑电池寿命的调度”“考虑不确定性的鲁棒调度”,全部是在这套骨架上加东西。

2.2 决策变量与目标函数:钱在哪里,优化的刀就落在哪里

决策变量非常简单,一共四大类:

  • 储能充放电功率:每个时段储能要么充电、要么放电,功率大小由优化决定。这里要注意,为了让表达式线性化,通常把充电功率和放电功率拆成两个变量,再用电网侧的互斥约束保证它们不会同时为正。
  • 与大电网的交互功率:每个时段从电网买的电和卖给电网的电,同样拆成两个变量,并加互斥约束。
  • 需求响应的削减负荷量和平移负荷量:削减量每个时段有一个上限;平移量则用“某个时段移入的负荷量”和“某个时段移出的负荷量”来表示,同时必须保证一天之内移入总量等于移出总量,不能让负荷凭空消失。
  • 一些辅助二进制变量:比如储能充电/放电状态标志、购电/售电状态标志,这些变量的作用是让互斥约束生效,也决定了模型到底是LP还是MILP。

目标函数是最小化总运行成本,我一般这么写:

[ \min \sum_{t=1}^{T} \left( c_{buy,t} P_{buy,t} - c_{sell,t} P_{sell,t} + c_{DR} P_{cut,t} + c_{bat} (P_{ch,t} + P_{dis,t}) \right) ]

翻译成大白话就是:每个时段的购电费用,减去卖电收入(如果有余电上网),加上给需求响应用户的补偿费用,再加上储能充放电循环的折算损耗成本(也就是电池折旧费用)。其中如果某时段还允许弃风弃光,可以再加一项弃电惩罚项,让优化结果尽量不要扔掉可再生能源电量。这里我特别想提醒:储能损耗成本很多人会漏掉,如果不加这一项,结果会非常激进——储能每天充满放空好几个循环,电池寿命哗哗地掉,实际工程中根本不敢这么干。所以就算它是虚拟成本,也建议加上,折算成每kWh充放量的损耗成本,一般取几分钱到一毛多。

2.3 约束条件拆解:储能、需求响应、电网交互一个都不能少

约束条件是这个模型的主体,也是初学者最容易写错的地方。我按设备类型逐个拆解。

功率平衡约束——这是整个微电网的“能量守恒定律”。每一时刻,光伏出力加风电出力加储能放电加购电,必须等于当前时刻的总负荷(基础负荷加需求响应调整后)加上储能充电加卖电。写成公式就是:

[ P_{pv,t} + P_{wt,t} + P_{dis,t} + P_{buy,t} = P_{load,t} + P_{ch,t} + P_{sell,t} - P_{cut,t} - P_{shift,net,t} ]

注意这里的正负号特别容易搞反,尤其需求响应那两项:可削减负荷是让负荷变小,所以右侧要减去它;可平移负荷可能是某个时段移入(负荷变大)或移出(负荷变小),我建议用一个净变量 ( P_{shift,net,t} ) 来表示,正值代表移入负荷,负值代表移出负荷。

储能约束——储能的本体用一个SOC递推方程描述:下一时段SOC等于当前SOC加上充电量减去放电量,再考虑充放电效率。此外还要满足SOC上下限约束、充放电功率上限约束、以及充放电互斥约束。最容易被忽略的是周期末SOC回位约束,如果不加这条,优化器会把SOC在一天结束时放到最低点,第二天就没法继续调度了。我的习惯是要求终止SOC大于等于初始SOC的95%(不是严格等于,稍微放松一点给求解器留余地),这样既满足日循环要求,又不至于过度收紧可行域。

需求响应约束——可削减负荷的削减量要小于等于该时段可削减潜力的上限;可平移负荷的日内移入总量等于移出总量;任何时段的负荷调整不能超过允许范围。需求响应的补偿成本要在目标函数里体现,否则优化器会无限削减负荷,结果失真。

与大电网交互约束——购电功率和售电功率分别有上限,并且二者不能同时大于零。这个互斥约束用二进制变量实现:

[ P_{buy,t} \le M \cdot u_t, \quad P_{sell,t} \le M \cdot (1-u_t), \quad u_t \in {0,1} ]

如果交互功率上限本身就是0或纯购电场景,可以简化掉售电变量和互斥约束,模型就从MILP退化成LP,求解速度快很多。

3. Matlab代码实现:从公式到可运行的代码

3.1 工具选型:为什么用Yalmip而不是纯Matlab手写优化

写优化代码有两条路:一条是用Matlab自带的Optimization Toolbox里的linprog和intlinprog,一条是用Yalmip建模语言再加外部求解器。我的经验是:只要你的模型里出现了哪怕一个二进制变量,就直接上Yalmip。为什么?因为intlinprog的矩阵写法极其反人类——你要手搓成 ( A_{eq}x=b_{eq} ) 的形式,所有的约束都要手动拼矩阵,稍微改一个变量维度就从头再来,调试起来简直崩溃。而Yalmip可以用“接近自然语言”的方式描述约束,阅读、修改、调试都舒服得多。

我推荐的环境组合是:Matlab R2020b以上 + Yalmip + Cplex(或Gurobi)。如果因为许可证问题装不了Cplex/Gurobi,退一步用Matlab自带的intlinprog也能跑,只是求解速度慢一些,小规模算例感觉不出来差别,大规模就要等了。顺便说一句,如果你用的是学校许可证的Matlab,Cplex和Gurobi也都有学术版,是可以免费申请到的。

3.2 数据结构与参数设置:把“预测数据”喂给模型

我习惯把所有输入数据放在一个结构体data里面,这样函数调用清晰,后续改参数也方便。核心数据包括:24小时的负荷预测值P_load、光伏预测值P_pv、风电预测值P_wt、分时购电价price_buy和售电价price_sell。储能系统参数包括容量E_cap、初始SOC、SOC上下限、充放电功率上限、充放电效率。需求响应参数包括可削减负荷比例上限、可平移负荷比例上限、以及需求响应补偿单价。

这里我想特别说一个容易被忽视的细节:分时电价的数据对齐。很多人把电价写成[峰谷平时的标量]然后手动复制成24维向量,复制错了一个时段,整个优化结果就歪了。我建议直接在Excel里把24个小时的价格排好,然后一次性读入Matlab,不要在脚本里手动拼。我自己早期吃过这个亏,峰电价少复制了一行,结果优化器“聪明”地把大量负荷堆到了凌晨时段,一看电价表才发现是数据对齐错了。

3.3 决策变量定义与约束组装:Yalmip的核心写法

这一段是代码的骨架,我直接给出代码框架。先定义决策变量:

% 决策变量定义 P_ch = sdpvar(1, T, 'full'); % 储能充电功率 P_dis = sdpvar(1, T, 'full'); % 储能放电功率 u_bat = binvar(1, T, 'full'); % 储能充放电状态 P_buy = sdpvar(1, T, 'full'); % 从电网购电功率 P_sell = sdpvar(1, T, 'full'); % 向电网售电功率 u_grid = binvar(1, T, 'full'); % 购售电状态 P_cut = sdpvar(1, T, 'full'); % 可削减负荷量 P_shift_in = sdpvar(1, T, 'full'); % 可平移负荷移入量 P_shift_out = sdpvar(1, T, 'full'); % 可平移负荷移出量 SOC = sdpvar(1, T, 'full'); % 储能荷电状态

约束的写法是Yalmip最有魅力的地方,几乎不用考虑矩阵拼接。比如功率平衡约束直接写:

Constraints = []; Constraints = [Constraints, P_pv + P_wt + P_dis + P_buy == P_load + P_ch + P_sell - P_cut + P_shift_in - P_shift_out];

储能SOC递推约束和初值约束:

SOC(1) == SOC_init + eta_ch * P_ch(1) / E_cap - P_dis(1) / (eta_dis * E_cap); for t = 2:T Constraints = [Constraints, SOC(t) == SOC(t-1) + eta_ch * P_ch(t) / E_cap - P_dis(t) / (eta_dis * E_cap)]; end Constraints = [Constraints, SOC(T) >= 0.95 * SOC_init, SOC(T) <= 1.05 * SOC_init];

储能互斥约束和容量约束:

Constraints = [Constraints, 0 <= P_ch <= P_bat_max * u_bat]; Constraints = [Constraints, 0 <= P_dis <= P_bat_max * (1 - u_bat)]; Constraints = [Constraints, SOC_min <= SOC <= SOC_max];

购售电互斥约束:

Constraints = [Constraints, 0 <= P_buy <= P_grid_max * u_grid]; Constraints = [Constraints, 0 <= P_sell <= P_grid_max * (1 - u_grid)];

需求响应约束:

Constraints = [Constraints, 0 <= P_cut <= 0.1 * P_load]; % 削减量不超过负荷的10% Constraints = [Constraints, 0 <= P_shift_in <= 0.15 * P_load]; % 移入量不超过负荷的15% Constraints = [Constraints, 0 <= P_shift_out <= 0.15 * P_load]; % 移出量不超过负荷的15% Constraints = [Constraints, sum(P_shift_in) == sum(P_shift_out)]; % 日内平移总量守恒

这里特别强调一下,Constraints = [Constraints, ...]这种写法看似是“拼接”,实际上Yalmip内部会把它处理成一个约束集合对象,非常容易阅读和排查。我见过很多新手试图把所有约束写在一个中括号里然后加上分号,那是把Yalmip当矩阵用了——不会报错但极难维护。

3.4 目标函数组装与求解:一行代码调用Cplex/Gurobi

目标函数和求解就简单了:

Objective = sum(price_buy .* P_buy - price_sell .* P_sell + c_DR * P_cut + c_bat * (P_ch + P_dis)); ops = sdpsettings('solver', 'cplex', 'verbose', 2); optimize(Constraints, Objective, ops);

在这一步,如果你的模型是纯连续变量,Yalmip会自动走LP;只要出现binvar,就会自动走MILP,不需要你手动指定。求解完成后,直接提取各个变量的.value属性,然后画图。

这里分享一个我的习惯:求解完先看Yalmip返回的solinfo,确认求解状态是Solved还是Infeasible。如果出现不可行,不要急着改参数,先用check(Constraints)检查是哪些约束出了问题,这条命令会逐条告诉你是哪个约束违反了,误差有多大。这个功能简直是调试利器,比瞎猜快十倍。

4. 算例实测:一组典型场景的结果与解读

4.1 算例数据与场景设计:夏季典型日的风力、光伏和电价曲线

我挑一个典型的夏季工作日做算例。光伏曲线是上午6点开始爬坡,中午12到14点达到峰值;风电夏季出力相对较弱,夜间到凌晨略好;负荷曲线呈现早高峰(8到10点)和晚高峰(18到21点)两个波峰。分时电价采用常见的峰谷平三段式:峰段8到11点、18到21点,价格1.2元/kWh;平段6到8点、11到18点、21到23点,价格0.7元/kWh;谷段23点到次日6点,价格0.3元/kWh。

储能参数按一个园区级微电网来设:容量400kWh,最大充放电功率100kW,充放电效率取95%,SOC范围0.1到0.9,初始SOC取0.5。光伏装机200kW,风电装机100kW,基础负荷峰值约250kW,与大电网交互上限取200kW。

4.2 场景对比:无需求响应vs有需求响应、无储能vs有储能

我把算例分成四种组合来跑:无储能无需求响应(纯购电基准)、无储能有需求响应、有储能无需求响应、有储能有需求响应(完整方案)。四种组合的日运行成本如下表:

场景组合日运行成本(元)峰时段购电(kWh)谷时段购电(kWh)削减负荷(kWh)
无储能无DR421616207800
无储能有DR38981415835260
有储能无DR3542105011200
有储能有DR32678551185220

先看无储能无DR的基准结果,成本4216元——这就是“老老实实按预测从电网买电”的代价。加需求响应后降到3898元,降幅约7.5%,省下的钱主要来自把晚高峰的部分可削减负荷砍掉,减少了高峰购电。加储能后降到3542元,降幅约16%,储能起的作用是“谷充峰放”——谷时段电价低,多买电充进去,峰时段电价高,放出来替代一部分购电。完整方案降到3267元,比基准降低22.5%,储能和需求响应形成了互补:储能解决的是“能量在时间上的搬移”,需求响应解决的是“负荷在时间上的调整”,两者叠加效果不是简单相加,而是有协同的。

4.3 关键结果图怎么看:储能SOC曲线是最诚实的“体检报告”

跑完优化,我最先看的永远是储能SOC曲线。一条健康的SOC曲线应该长这样:凌晨谷段从0.5逐步充电到0.8左右,上午平段略微放电,中午光伏大发时再充回高位,傍晚峰段持续放电到0.3附近,夜间谷段再次充电,结束时回到接近0.5。如果你看到的SOC曲线乱跳、反复充放,大概率是约束写漏了或者参数设置不合理。还有一条曲线是各时段功率平衡图,看储能充放电和电网交互功率是否超限,需求响应负荷调整是否按照预期削减在峰段。

另外,建议用stairs函数画分时电价在同一个图里叠加,这样能直观看到充电是否发生在谷段、放电是否发生在峰段。我曾经跑出一组结果,储能白天疯狂充电、夜里疯狂放电,完全跟电价趋势反着来,细查发现是电价序列方向传反了——这就是画图一眼能看出来的好处。

5. 实操中的常见问题与排查实录:这些都是花钱买来的教训

5.1 求解器报错Infeasible:八成是这五个原因

问题1:SOC递推约束初值索引错误。新手最容易在这里踩坑,SOC(1)的公式写了两次,一次在循环外作为初值,一次在循环内作为递推,导致约束冲突。我的建议是:第一时段单独初始化,循环从2开始递推,不要尝试用t=1统一处理。

问题2:功率平衡约束维度不匹配。比如P_pv是24行1列,其他变量是1行24列,Yalmip不会报错,但会在求解时给出奇怪的告警,或者直接不可行。统一用行向量还是列向量,我建议全程用行向量,简单粗暴不纠结。

问题3:需求响应平移守恒约束漏写。如果只允许移出不允许移入,或者移入移出总量不守恒,模型会告诉你不可行——因为负荷总量凭空减少了,系统能量不守恒。加了sum(P_shift_in) == sum(P_shift_out)之后才能保证负荷总量不变。

问题4:购售电价差方向写反。如果你的售电价高于购电价,优化器会疯狂“购电再卖电”套利,而且因为互斥约束的存在,模型甚至会不可行。实际场景中售电价永远低于购电价,检查数据时一定要确认。

问题5:SOC终止约束太紧。我见过有人把终止SOC严格等于初始SOC,结果可行域非常小,尤其在光伏预测值特别高、储能早就充满的场景下,模型直接无解。正确做法是加一个容差带,比如±5%。

5.2 Yalmip“求解器装好了却提示找不到”:路径和许可证是两大元凶

这个问题我一个月至少碰到三次,几乎都是检查点的问题。第一步是确认Yalmip和求解器的路径已经添加到了Matlab的搜索路径里,addpath(genpath(...))之后记得savepath,否则重启又失效。第二步是确认许可证环境变量正确。Gurobi和Cplex都有自己的许可证机制,不是把安装包解压就能用的。第三步是最容易被忽视的——检查操作系统是32位还是64位,Yalmip是否匹配当前Matlab版本。这些外部工具箱的接口文件往往只支持特定Matlab版本,版本不匹配时Yalmip能加载,但调用求解器直接崩。

如果实在不想折腾外部求解器,Yalmip也支持intlinprog,设置solver','intlinprog'就能跑,只是大规模问题的求解时间从秒级变成分钟级。我测评过,24个时段的微电网调度问题用Cplex跑大约0.5到2秒,用intlinprog大约20到60秒。对于硕士论文级别的算例,这个速度完全可以接受。

5.3 结果合理但效果很差:先查参数再查逻辑

曾经有个朋友拿着他的代码来找我,说储能装了跟没装一样,成本只降了2%。我一看参数,储能容量才50kWh,最大充放电功率10kW,配的却是200kW的光伏——容量太小,当然起不了多大作用。这种问题不属于代码bug,属于参数不匹配。做灵敏度分析的时候要特别小心:调储能容量、功率这两个参数时,要关注成本曲线什么时候变平,过了那个点再增加容量就没有边际效益了。

还有一种“效果差”的情况是需求响应补偿价格设置过高。补偿单价如果超过了峰谷电价差,优化器就不会调用需求响应,因为不划算。反过来补偿单价过低,优化器会疯狂削减负荷,结果虽然成本低,但实际中用户早就投诉了。我通常会把补偿单价设在峰电价的50%到80%之间,这个区间比较贴近实际工程中的激励价格水平。

5.4 求解速度慢到难以忍受:别急着加约束,先优化问题规模

如果模型只涉及24时段,Cplex秒解,一般不存在速度问题。但当你把时间粒度细化到96时段,再引入多类需求响应的二进制变量,问题规模会迅速膨胀。我的排查思路是:第一,用Yalmip的export命令导出问题规模,看看变量数和约束数;第二,删掉不必要的二进制变量,比如购售电互斥可以不用二进制变量,直接用0 <= P_buy <= M、0 <= P_sell <= M加一个P_buy + P_sell <= P_grid_max代替,效果一样但少了一组二进制变量;第三,把不活跃的约束提前筛选掉,比如某些时段光伏出力为零,就可以不设置弃电惩罚项。

6. 扩展与进阶方向:这个模型还能往哪里走

这套“风光储+需求响应”的日前调度框架,是一个教科书级别的基线模型。基于它你可以加很多现实因素。第一个方向是考虑多时间尺度协同——日前调度只能给小时的计划,日内负荷和新能源出力会有波动,所以现在工程上流行的做法是“日前优化+日内滚动修正”,把预测误差在日内用更短的时间窗口去修正。第二个方向是储能寿命衰减建模——把电池的循环老化作为一个与放电深度相关的非线性函数,用分段线性化处理再塞进MILP框架里,这样储能调度的结果更符合电池真实使用情况。第三个方向是不确定性处理——光伏和风电的预测误差是客观存在的,把它建模成随机变量,用随机优化或分布鲁棒优化,做出来的调度结果会比确定性模型更稳健。这三个方向随便挑一个往下走,都能撑起一篇不错的硕士论文或者一个实际工程项目。

评论区有人问我:这套代码是不是只能用在微电网,放到虚拟电厂场景行不行?我说行,而且基本不用改——把“与大电网交互”改成“与电力市场交易”,把需求响应资源改成虚拟电厂聚合的柔性负荷,模型的数学结构完全一样,只改几个参数和约束边界就能直接复用。这个扩展能力,正是这一套模型最值钱的地方。

7. 最后的实操心得:新手最容易忽视的四个小习惯

我最后分享几个自己在反复试错中养成的习惯,不算什么高深技巧,但确实帮我省了很多调试时间。第一个习惯,所有输入数据先画图再送进优化器。不管数据是Excel读进来的还是函数生成的,先plot一遍,肉眼看有没有突变、缺段、方向反了的问题,比求解失败后再排查高效得多。第二个习惯,把目标函数拆开统计。求完解之后,分别统计购电费用、售电收入、需求响应补偿、储能损耗这几项,和基准场景对比,看每一项占比是否合理。如果储能损耗占比大得不合常理,说明储能被过度使用了。第三个习惯,给模型写一个统一的入口函数,输入是数据结构体,输出是结果结构体。这样以后无论怎么改约束、改参数,主脚本永远是那几行,不会因为一次修改导致下面所有代码都要跟着改。第四个习惯,用版本管理工具管理代码,哪怕是自己的课题也需要。我吃过一次大亏,改了一版约束后发现新模型结果不如旧的好,想退回旧版却发现没存档,只能凭记忆往回改,浪费了整整两天。从那以后,每个能跑出版本的代码我都会打一个标签,这比什么都重要。

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

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

立即咨询