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_{t1}^{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}xb_{eq} ) 的形式所有的约束都要手动拼矩阵稍微改一个变量维度就从头再来调试起来简直崩溃。而Yalmip可以用“接近自然语言”的方式描述约束阅读、修改、调试都舒服得多。我推荐的环境组合是Matlab R2020b以上 Yalmip Cplex或Gurobi。如果因为许可证问题装不了Cplex/Gurobi退一步用Matlab自带的intlinprog也能跑只是求解速度慢一些小规模算例感觉不出来差别大规模就要等了。顺便说一句如果你用的是学校许可证的MatlabCplex和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八成是这五个原因问题1SOC递推约束初值索引错误。新手最容易在这里踩坑SOC(1)的公式写了两次一次在循环外作为初值一次在循环内作为递推导致约束冲突。我的建议是第一时段单独初始化循环从2开始递推不要尝试用t1统一处理。问题2功率平衡约束维度不匹配。比如P_pv是24行1列其他变量是1行24列Yalmip不会报错但会在求解时给出奇怪的告警或者直接不可行。统一用行向量还是列向量我建议全程用行向量简单粗暴不纠结。问题3需求响应平移守恒约束漏写。如果只允许移出不允许移入或者移入移出总量不守恒模型会告诉你不可行——因为负荷总量凭空减少了系统能量不守恒。加了sum(P_shift_in) sum(P_shift_out)之后才能保证负荷总量不变。问题4购售电价差方向写反。如果你的售电价高于购电价优化器会疯狂“购电再卖电”套利而且因为互斥约束的存在模型甚至会不可行。实际场景中售电价永远低于购电价检查数据时一定要确认。问题5SOC终止约束太紧。我见过有人把终止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一遍肉眼看有没有突变、缺段、方向反了的问题比求解失败后再排查高效得多。第二个习惯把目标函数拆开统计。求完解之后分别统计购电费用、售电收入、需求响应补偿、储能损耗这几项和基准场景对比看每一项占比是否合理。如果储能损耗占比大得不合常理说明储能被过度使用了。第三个习惯给模型写一个统一的入口函数输入是数据结构体输出是结果结构体。这样以后无论怎么改约束、改参数主脚本永远是那几行不会因为一次修改导致下面所有代码都要跟着改。第四个习惯用版本管理工具管理代码哪怕是自己的课题也需要。我吃过一次大亏改了一版约束后发现新模型结果不如旧的好想退回旧版却发现没存档只能凭记忆往回改浪费了整整两天。从那以后每个能跑出版本的代码我都会打一个标签这比什么都重要。