拿到"基于风光储能和需求响应的微电网日前经济调度"这种题目,很多人第一反应是赶紧找个Matlab代码跑起来,结果折腾一周发现,问题根本不在算法,而在建模本身——储能SOC怎么算、需求响应怎样进入目标函数、功率平衡等式怎么闭合,每一处都是坑。我过去带过不少做毕设和项目中期的同学,也帮人排查过很多"运行报错、结果荒谬"的案例,今天把这套东西从头到尾捋一遍:从问题定义、数学建模,到Yalmip求解和结果体检,最后附上实操中一定会遇到的坑。
这篇内容适用于三类人:正在做微电网经济调度方向毕业设计的学生、刚接手园区微电网项目的工程师、以及想系统搞懂"日前调度到底在优化什么"的入门研究者。全文不绕弯,代码思路直接给,坑也直接点名。
1. 为什么是"日前"调度:问题边界与成本构成的底层逻辑
1.1 微电网调度这个词,到底在求什么
微电网的本质,是把分布式风电、光伏、储能、本地负荷(以及可选的需求响应资源)打包成一个可控单元,既能并网运行,也能在必要时候孤岛运行。而"日前经济调度"这四个字,核心动作是"提前一天排计划":根据第二天的负荷预测、风电光伏预测、分时电价,把每台设备在每一个时段发多少电、充多少电、从电网买多少电,全部预先定下来。
为什么一定要提前做?因为调度问题不是单时段的。储能电池有一个跨时段的荷电状态(SOC)约束,今天充的电明天才能放;机组有爬坡约束,出力不能瞬间跳跃。这些时间耦合关系只有在"多时段联合优化"的框架下才能处理。换句话说,你不能每小时单独算一个平衡,必须把24个小时放在一起,让优化器自己决定"哪一小时充电、哪一小时放电"最合算。
用通俗的话来讲:如果一个家庭主妇打算控制一周的买菜预算,她不会每天早上才想今天吃什么,而是提前计划好哪天买肉、哪天买菜,冰箱(储能)就是她的蓄水池。日前调度干的就是这件事,只不过把冰箱换成了电池,把菜价换成了分时电价。
决策变量包括:每个时段的风电实际出力、光伏实际出力、储能充电功率、储能放电功率、向电网购电/售电功率、需求响应削减量,如果微电网里还有柴油机或者燃气轮机,还要加入机组启停状态的0/1变量。约束条件则是功率平衡、各类设备出力上下限、储能SOC动态、购售电上限、需求响应限度等。
1.2 把成本拆开看:哪些钱是调度能省下来的
经济调度的目标函数,本质上就是一句话:让系统在满足所有约束的前提下,总运行成本最小化。具体到风光储+需求响应的微电网,目标函数通常包含这几块:
- 向电网购电的成本,按分时电价结算;
- 向电网售电的收益(如果有余电上网),注意这个是负成本;
- 需求响应补偿费用,也就是让用户削减负荷需要付的钱;
- 如果含柴油机/燃气轮机,还有燃料成本与启停成本;
- 可选的风光弃电惩罚项。
很多人第一次建模容易漏掉最后一项。你不给风电和光伏设定"弃电惩罚"或者"最小消纳比例",优化器就会为了省事把免费的风光出力悄悄丢弃,只为了不破坏功率平衡。这在数学上是可行的,但在工程上完全错误,所以要么在目标函数里加一项很小的弃风弃光惩罚系数,要么专门加约束保证可再生能源消纳比例不低于某个值。
举个成本结构的例子:某地分时电价,峰时1.2元/kWh,平时0.7元/kWh,谷时0.35元/kWh。储能充电时如果算上10%的损耗,从电网买1 kWh谷电实际充进电池约0.9 kWh,等效成本约0.39元/kWh,峰时放出来卖或者自用,相当于每转移1 kWh省下大约0.75元。这一来一回,就是储能套利的利润空间。而需求响应削减1 kWh的成本则取决于你和用户签的补偿单价。调度器做的事情,本质上就是把"储能转移电量"和"需求响应削减负荷"这两种资源,按照边际成本从低到高排列组合,同时配合风电光伏的最大化利用,形成一套成本最低的24小时计划。
2. 风光储出力建模:不确定性怎么"塞进"优化模型
2.1 风电光伏的日前预测出力处理
风、光出力是间歇性电源,在日前调度这个时间尺度上,你拿到的通常是一组预测曲线:比如气象系统预测明天24小时的辐照度和风速,再通过功率曲线折算成预测出力。处理方式有确定性预测、场景法和鲁棒优化三种,我的建议是:初版模型一律先用确定性预测,跑通了再考虑不确定性。
确定性预测的建模思路非常简单:把预测值当成"最大可用出力",优化器在约束范围内可以决定实际用多少,差值部分就是弃风弃光量。写成公式就是0 ≤ P_wind(t) ≤ P_wind_fc(t),0 ≤ P_pv(t) ≤ P_pv_fc(t)。这个写法的好处是,模型天然具备灵活性——如果某个时段电价极低、消纳不了,系统可以选择少发;如果电价高,系统会尽量多发电。
需要注意单位问题。功率单位是MW,能量单位是MWh,如果步长是1小时,功率乘以1就等于能量;如果步长是15分钟,那功率乘以0.25才是该时段能量。这个问题看似基础,实际是储能模型错误的头号来源,后面第5节还会展开说。
Matlab里用Yalmip声明风电和光伏出力变量时很简单:
P_wind_fc = xlsread('data.xlsx', '风电预测'); % 24x1,风电日前预测曲线 P_pv_fc = xlsread('data.xlsx', '光伏预测'); % 24x1,光伏日前预测曲线 P_wind = sdpvar(24, 1, 'full'); % 风电实际调度出力 P_pv = sdpvar(24, 1, 'full'); % 光伏实际调度出力 C = []; C = [C, 0 <= P_wind <= P_wind_fc]; C = [C, 0 <= P_pv <= P_pv_fc];2.2 储能电池模型的三个关键约束组
储能是整个微电网调度的灵魂,也是出错率最高的部分。我见过大量代码把储能简化成"充放电功率有上下限",SOC只写一个范围,结果解出来完全不满足能量守恒。完整的储能模型必须包含三组约束,缺一不可。
第一组是功率边界:
0 ≤ P_ch(t) ≤ P_ch_max × u_ch(t) 0 ≤ P_dis(t) ≤ P_dis_max × u_dis(t) u_ch(t) + u_dis(t) ≤ 1
其中u_ch和u_dis是0/1变量,表示同一时段不能既充电又放电。很多人问能不能不引入二进制变量,直接用连续变量?也可以,但必须在目标函数里加一个很小的"同时充放电惩罚项",或者用P_ch + P_dis ≤ P_max这个近似约束。否则某些目标函数结构下,优化器会出现既充又放的荒谬解。
第二组是SOC动态方程,这是储能模型的命门:
SOC(t+1) = SOC(t) + η_ch × P_ch(t) × dt - P_dis(t) / η_dis × dt
其中η_ch是充电效率(比如0.95),η_dis是放电效率(比如0.95),dt是步长,单位小时。注意充电时是"乘效率",放电时是"除效率",因为电池充进去1 kWh,实际存下的是0.95 kWh;放出来1 kWh,实际消耗的是1/0.95 kWh。这个细节很多人写反,结果储能的能量凭空多出来一块。
第三组是SOC边界与首末约束:
SOC_min ≤ SOC(t) ≤ SOC_max SOC(1) = SOC_0 SOC(T+1) ≥ SOC_0 (或者干脆要求相等)
首末约束的意义在于:不能让调度方案把电池在一天结束时放空。如果允许SOC_end随便掉到0,优化器会为了多卖电把电池榨干,第二天的运行就没法继续了。工程上一般要求SOC_end ≥ SOC_0,或者至少维持在一个合理水平。
2.3 需求响应资源的建模方式
"需求响应"这个词在不同文献里有完全不同的含义,建模前必须先搞清楚你是哪一种。业界通常分成两类:
- 价格型需求响应(Price-based DR):用户根据电价自动调整用电行为,模型里体现为负荷-价格弹性关系,比如负荷 = 基准负荷 × (当前电价/参考电价)^弹性系数。
- 激励型需求响应(Incentive-based DR):调度中心直接和用户签订协议,在特定时段要求削减负荷,用户获得补偿。
在微电网日前经济调度里,最常见的是激励型DR,因为它和优化的衔接最自然。最简单的模型是:
P_load_dr(t) = P_load_base(t) - P_dr(t) 0 ≤ P_dr(t) ≤ P_dr_max(t)
也就是每个时段最多削减多少负荷,削减后系统按削减后的负荷做功率平衡。写进Matlab时:
P_load_base = xlsread('data.xlsx', '基础负荷'); % 24x1 P_dr_max = 0.1 * P_load_base; % 假设每时段最多削减10% P_dr = sdpvar(24, 1, 'full'); C = [C, 0 <= P_dr <= P_dr_max]; C = [C, sum(P_dr) <= sum(P_load_base) * 0.05]; % 全天总削减量不超过5%但如果你真的想让模型有说服力,光有这个还不够,需求响应还有成本与舒适度约束的问题——这正是下一节要重点展开的。
3. 需求响应的经济学本质:补偿成本与用户约束的取舍
3.1 三个必须写进模型的DR参数
很多初学者把需求响应当做一个"免费削负荷"的资源,这是大忌。DR的本质是花钱买用户的用电权,它是和储能并列的一种灵活性资源,有成本、有时间限制、有用户体验边界。一个可靠的DR模型至少要包含三个参数:
第一,单时段最大削减量,防止优化器把某个时段负荷全砍掉。比如某时段基础负荷200 kW,允许削30%,那P_dr_max就是60 kW,再高用户的生产生活就受不了了。
第二,全天削减总量或削减时长限制。比如协议规定一天内最多削减3个小时,或者全天削减电量不超过当日用电量的10%。这类约束保证了用户的舒适度和生产连续性,必须在模型里写死。
第三,削减补偿单价。这是最关键的——DR不是免费的,补偿单价直接决定调度器"用不用、用多少"DR资源。
3.2 分档补偿的线性化技巧
实际项目里,补偿单价很少是固定值。用户通常愿意接受阶梯式补偿:削减前30 kW,每kWh补偿0.5元;削减30~60 kW的部分,每kWh补偿0.8元。这种分段线性成本在目标函数里怎么处理?
可以用分段线性化的写法。假设第一档上限为50 kWh(注意这里是每时段50 kW功率),单价c1=0.5,第二档也是50 kWh,单价c2=0.8。引入两个非负变量p1和p2,满足:
0 ≤ p1 ≤ 50 0 ≤ p2 ≤ 50 P_dr = p1 + p2
目标函数中DR成本写为c1 × p1 + c2 × p2。由于单价是递增的,优化器自然优先使用便宜的第一档,不需要额外加二进制变量就能正确表达阶梯成本。Yalmip代码如下:
p1 = sdpvar(24, 1, 'full'); p2 = sdpvar(24, 1, 'full'); C = [C, 0 <= p1 <= 50, 0 <= p2 <= 50]; C = [C, P_dr == p1 + p2]; DR_cost = 0.5 * sum(p1) + 0.8 * sum(p2);3.3 DR和储能到底谁优先:边际成本排序
有人会问:DR削减负荷和储能放电压峰,都是"削峰"手段,调度器怎么决定用谁?答案是看边际成本谁更低。
假设储能从谷时充电到峰时放电的综合成本(电费损耗+电池循环折损)约为0.5元/kWh,而DR补偿单价是0.6元/kWh,那调度器优先储能;如果DR补偿是0.3元/kWh,调度器会优先削减负荷,因为更便宜。真实系统中DR资源和储能资源还会互相配合——峰时先调用便宜的DR削减一部分负荷,再用储能放电压一部分峰,两个一起把购电功率降下来。
从结果曲线上看,做了DR的调度方案通常会呈现两个特征:一是原负荷曲线的峰值被明显削掉一块,二是系统向电网购电的峰值得到了抑制,峰谷差收窄。这就是DR的"经济价值"所在:相当于用可控的用户侧调节能力替代了部分储能容量甚至电网扩容需求。
4. Matlab代码实现:Yalmip建模与求解器链路
4.1 为什么选Yalmip + 商用求解器,而不是手写粒子群
Matlab做优化调度,工具选型很关键。目前主流方案是Yalmip建模,后端接Gurobi、Cplex这类商用求解器,或者用Matlab自带的intlinprog。也见到很多人用粒子群(PSO)、遗传算法(GA)硬解调度问题,我的态度是:除非你的模型里有严重的非线性环节(比如设备效率随负载率变化、非凸运行区),否则不要优先用启发式算法。
原因有三点。第一,日前经济调度模型本质上是线性规划(LP)或混合整数线性规划(MILP),这类问题商用求解器可以拿到全局最优解,并且求解速度极快,几十个变量、几百个约束的问题通常几秒钟就出结果。第二,启发式算法每次运行结果都可能不同,无法证明解的全局最优性,这在论文审稿或者工程验收时是硬伤。第三,Yalmip的声明式建模风格非常友好,数学约束和代码几乎一一对应,改模型比手写求解器快好几个量级。
版本兼容性上,Matlab R2019b以上配Yalmip基本没什么坑。求解器如果学校有Gurobi授权就直接用,没有的话先用intlinprog验证正确性,再找Cplex学术版或者开源的CBC/GLPK都行。
4.2 决策变量、目标函数、约束组装的代码骨架
这里给出一个完整的模型骨架,覆盖风光储+需求响应+并网购售电,步长1小时、24个时段:
%% 1. 数据准备 % 假设已有以下列向量(24x1):P_load_base, P_wind_fc, P_pv_fc, Price_buy, Price_sell T = 24; dt = 1; %% 2. 决策变量 P_grid_buy = sdpvar(T, 1, 'full'); % 从电网购电功率 P_grid_sell = sdpvar(T, 1, 'full'); % 向电网售电功率 P_wind = sdpvar(T, 1, 'full'); % 风电实际出力 P_pv = sdpvar(T, 1, 'full'); % 光伏实际出力 P_ch = sdpvar(T, 1, 'full'); % 储能充电功率 P_dis = sdpvar(T, 1, 'full'); % 储能放电功率 SOC = sdpvar(T+1, 1, 'full');% 储能荷电状态,T+1个点 u_ch = binvar(T, 1); % 充电状态0/1 u_dis = binvar(T, 1); % 放电状态0/1 P_dr = sdpvar(T, 1, 'full'); % 需求响应削减量 z_buy = binvar(T, 1); % 购电状态0/1 z_sell = binvar(T, 1); % 售电状态0/1 %% 3. 目标函数:购电成本 - 售电收益 + DR补偿成本 Cost = Price_buy' * P_grid_buy - Price_sell' * P_grid_sell + 0.6 * sum(P_dr); % 如果有多档DR成本,按3.2节做分段线性化 %% 4. 约束条件 C = []; % 4.1 功率平衡:发电+购电 = 需求(削减后)+充电;放电看作发电 C = [C, P_wind + P_pv + P_dis + P_grid_buy == P_load_base - P_dr + P_ch + P_grid_sell]; % 4.2 风电光伏 C = [C, 0 <= P_wind <= P_wind_fc]; C = [C, 0 <= P_pv <= P_pv_fc]; % 4.3 储能功率边界 C = [C, 0 <= P_ch <= 500 * u_ch]; C = [C, 0 <= P_dis <= 500 * u_dis]; C = [C, u_ch + u_dis <= 1]; % 4.4 储能SOC动态 eta = 0.95; C = [C, SOC(2:T+1) == SOC(1:T) + eta * P_ch * dt - P_dis / eta * dt]; C = [C, 100 <= SOC <= 900]; % kWh C = [C, SOC(1) == 300]; C = [C, SOC(T+1) >= 300]; % 次日初始SOC不低于当天初始 % 4.5 购售电互斥和上限 M = 2000; % 足够大的数 C = [C, 0 <= P_grid_buy <= M * z_buy]; C = [C, 0 <= P_grid_sell <= M * z_sell]; C = [C, z_buy + z_sell <= 1]; % 4.6 需求响应 C = [C, 0 <= P_dr <= 0.3 * P_load_base]; % 每时段最多削减30% C = [C, sum(P_dr) <= 0.1 * sum(P_load_base)]; % 一天总量不超过10% %% 5. 求解 ops = sdpsettings('solver', 'cplex', 'verbose', 1); result = optimize(C, Cost, ops);这段代码里有两个细节必须注意。第一个是购售电互斥:如果同时允许买电和卖电为正,优化器可能在一个时段既买又卖,产生无意义的套利。解决办法是用两个二进制变量z_buy和z_sell,加互斥约束。第二个是功率平衡等式里的方向符号:我把充电负荷放在了等式右侧(需求侧),放电放在左侧(供给侧),这个约定必须一致,否则SOC能量守恒和功率平衡就对不上。
4.3 从代码到图:最优调度的可视化输出
求解完成后,把结果取出来:
P_wind_opt = value(P_wind); P_pv_opt = value(P_pv); P_dis_opt = value(P_dis); P_ch_opt = value(P_ch); P_dr_opt = value(P_dr); P_grid_opt = value(P_grid_buy) - value(P_grid_sell); SOC_opt = value(SOC);画图建议至少三张:
- 功率平衡堆叠图:横轴是1~24小时,纵轴是功率,风电、光伏、储能放电、购电功率堆叠起来,应该与"基础负荷-需求响应削减量+储能充电"这条线完全对齐。这张图能直观看出功率平衡等式有没有写错。
- SOC曲线:理想情况下,SOC在谷时段上升、峰时段下降,整体呈锯齿波。如果SOC曲线出现莫名其妙的突跳,说明SOC动态方程的单位或者效率方向出了问题。
- 电价与储能动作对照图:把分时电价和P_ch/P_dis画在同一个图上,正常情况下储能充电一定发生在低电价时段,放电一定发生在高电价时段,否则调度结果必然有误。
可视化这一步看着简单,却是排查模型错误最有力的工具。我第一次做微电网调度时,SOC曲线怎么都对不上,最后发现就是dt忘乘了——那种感觉,查了一下午代码,结果问题在"单位制"上。
5. 实测中必定会踩的坑:从约束冲突到求解不收敛
5.1 储能的时钟单位之谜
这个坑我每次带人都会强调,因为它实在太隐蔽了。SOC动态方程里有一个dt,步长不同dt就不同。每小时一个点的调度,dt=1;15分钟一个点(一天96个时段),dt=0.25。很多代码从网上下载下来,上一版是96时段写的,下一版改成24时段,SOC方程里忘了把dt从0.25改回1,结果SOC每小时凭空多出来/少掉一大块能量。
怎么自查?做一个手动推算:储能额定容量900 kWh,初始SOC=300 kWh,一小时充电功率100 kW,效率0.95。如果步长1小时,充完电SOC应该是300+0.95×100=395 kWh。用这个简单算例去核对代码输出,一眼就能看出模型对不对。
5.2 功率平衡约束写成不等式导致的无意义解
功率平衡等式是微电网调度模型的中枢,必须写严格等号。有人为了"让模型更灵活",把它写成≥或者≤,结果解出来要么发电比负荷多一大截(多余的售电被目标函数里的负收益利用了),要么负荷得不到满足。虽然目标函数会自动避免一些荒谬情况,但不保证所有情况下都正常,尤其是存在多个二进制变量、目标函数项互相嵌套时,等号约束一旦放松,调度计划就失真了。
还有一点,功率平衡等式必须包含需求响应削减后的负荷。很多人先写平衡等式再单独加DR约束,最后发现等式里的负荷还是原始负荷,DR削减掉的那部分功率凭空消失了,整个系统功率凭空多出一块,却还能"平衡"——因为约束没接上。
5.3 无界解与不收敛:一套诊断流程
遇到"Problem is unbounded"或者"infeasible"报错,别慌,按顺序排查:
- 无界解:通常意味着某些变量没有边界。检查所有决策变量是否都有上下限约束,尤其是P_grid_buy和P_grid_sell。如果漏了购电上限,购电功率可以无限大,目标函数会不断变负,必然无界。
- 不可行:优先怀疑约束之间互相矛盾。最常见的组合是"SOC初始值很低但要求结束时必须满电",加上"放电功率下界太高"。诊断方法是做模块化测试:先把DR、储能、购售电互斥一个个去掉,看哪组约束加上去之后模型从可行变不可行,再用二分法缩小冲突范围。
- MILP求解卡死:如果二进制变量太多(比如有几十台机组启停),求解时间指数级上升。可以用Yalmip的
sdpsettings('solver', 'cplex', 'cplex.mip.tolerances.mipgap', 0.01)设置1%的MIP间隙,显著加速收敛。或者在工程允许范围内把储能0/1变量放松成连续变量,用互补约束近似,速度会快很多。
记得有一次帮人调代码,模型一直报infeasible,我让他把每个约束单独打印出来检查,最后发现是SOC初始值300和SOC下限500矛盾——变量声明没问题,是参数写错了。这种低级错误用模块化测试最多十分钟就能定位。
6. 结果怎么才算对:一份调度方案的体检清单
模型跑通之后,别急着写报告,先给结果做个体检。表格里这六项检查通过,方案基本就站得住脚:
| 检查项 | 判断标准 | 常见失败原因 |
|---|---|---|
| 功率平衡 | 每个时段发电侧(风光+放电+购电)与需求侧(负荷-削减+充电+售电)严格相等 | 等式方向写反、DR量没接入 |
| SOC首末一致 | SOC(24)不低于SOC(0) | 缺少首末约束,或者SOC初始值设得过高 |
| 储能不能同时充放 | 任意时段P_ch与P_dis不同时为正 | 漏了u_ch+u_dis≤1约束 |
| 购售电互斥 | 任意时段不同时为正 | 漏了z_buy+z_sell≤1约束 |
| DR削减量合规 | 单时段削减量和全天总削减量都在设定范围内 | 约束上下限写错或忘写总量约束 |
| 消纳率达标 | 弃风弃光率低于设定值或惩罚项起作用 | 目标函数缺少弃电惩罚项 |
体检之外,最好再做一次灵敏度分析。固定其他参数,把储能容量从100 kWh逐步升到1000 kWh,观察总成本怎么变化;或者把DR补偿单价从0.3元/kWh调到1.0元/kWh,看DR使用量如何响应。这类分析是论文和项目报告里最有说服力的部分,因为它回答了"为什么这个配置最优"以及"在什么情况下方案需要调整"。
灵敏度分析还能暴露模型的"边界行为"。比如当DR补偿单价极低时,优化器可能把全天大部分负荷都削减掉,这说明模型缺少对DR总量的硬性约束,需要回头检查用户舒适度条件。反过来,如果储能容量继续增大但成本不降了,说明储能已经足够覆盖峰谷差,再加容量边际效益递减——这个拐点就是经济最优储能容量。
做这个项目最大的体会是:不要急着写代码,先把公式手推一遍。我见过太多人拿着现成的Matlab代码跑通就以为完事了,结果问一句"为什么这个时段储能充电而另一个时段不充",完全答不上来。真正理解风光储和需求响应在日前调度里的角色,靠的是把每个约束的物理意义抠清楚,而不是跑通一段demo。你如果能把SOC动态方程、功率平衡等式、DR补偿成本这三样东西的原理讲明白,再回去看任何一份现成代码,都会发现思路清晰得多。
如果后续想往深了走,可以考虑把确定性预测换成随机场景优化,或者把单一微电网扩展成多微电网协同调度,但前提都是先把这篇讲的确定性框架吃透。先把正确性做出来,再谈进阶,这条路最稳。