咱们做微电网的,应该都有同感:风光出力像心情一样不稳定,负荷也是说涨就涨,如果只靠“拍脑袋”安排明天的发电计划,不是浪费就是不够用。而基于风光储能和需求响应的微电网日前经济调度,就是解决这个问题的一套标准玩法——提前24小时,把风电、光伏、储能、可调负荷放在一个优化框架里,算出第二天每一小时各设备该发多少、充多少、切多少,让总运行成本最低。
这项工作说难不难,说简单也不简单。难在建模要考虑的约束多、变量杂,不难在思路其实很清晰:目标函数负责“算钱”,约束条件负责“定规矩”,求解器负责“找答案”。我最早接触这套东西时,折腾了将近两周才把Matlab下的完整代码跑通,回头看踩过的坑基本都集中在模型细节和参数设置上。这篇文章就把整个思路、模型、代码和调参经验摊开来讲,适合正在做微电网优化调度方向的学生、刚入行的工程师,以及想快速用Matlab做个可复现Demo的研究者。
1. 整体思路:前一天的三份“约定”
1.1 这个调度问题到底在算什么?
日前经济调度的本质,是一个带约束的优化问题。你可以把它理解成:明天一共有24个时段,每个时段都有一个“能量大账本”,我们需要在满足所有设备物理限制的前提下,决定每个时段的买卖电、充放电、燃气轮机出力、需求响应削减量。目标是让这24小时的总成本最小。
优化变量包括连续变量和0-1变量。连续变量是各时段的机组出力、储能充放电功率、联络线交换功率等;0-1变量是储能充电/放电状态标志、需求响应是否动作等标志位。用数学语言说,这是一个混合整数线性规划问题,也有的模型为了精度会做成混合整数二次规划。但工程上绝大多数场景,线性化后的MILP计算快、收敛稳、结果易解释,所以我个人建议先从线性模型入手。
我见过一些初学者一上来就写非常复杂的非线性目标,比如把储能寿命衰减、电池温度都塞进模型,结果求解器半天算不出结果,代码还跑得极慢。合适的策略是:第一版做线性化标准模型,确定线条跑通后再逐步加入精细化约束。所谓“能用、好用、再优化”,是搭建这类模型的基本原则。
1.2 风光、储能、需求响应各自扮演什么角色?
这套系统里有四类主要参与者:风电、光伏、储能、需求响应负荷。它们各自承担的任务不同。
风电和光伏是“免费电源”,发一度电不花燃料钱,但它们的出力曲线由自然条件决定,基本不可控。所以模型里它们通常作为给定边界条件,也就是预测曲线直接输入。不过在弃风弃光惩罚项存在时,允许调度方案选择削减一部分风电光伏出力,这是目前国内很多微电网项目的实际需要。
储能是调节“蓄水池”,解决风光出力与负荷错配的问题。中午光伏多的时候,把多余的电存起来,晚上负荷高峰期再放出来。储能的核心约束是SOC(荷电状态)的连续性:这一小时充了多少、放了多少,直接影响下一小时的剩余电量。另外储能还有一个“二分性”约束,即同一时段不能同时充电和放电,这个约束需要用0-1变量加一个大M实现,是建模中最容易出现冗余约束的地方。
需求响应本质是“用时间换钱”。通过给用户补贴,让用户在负荷高峰期少用一部分电,把用电需求转移到风电光伏出力充足时段。需求响应不是无限可切的,一般有上限,而且切负荷会带来补偿成本,所以优化器会自己权衡:电价高时多切,电价低时少切甚至不切。
1.3 为什么非要用“日前”?
“日前”两个字很有讲究。不是说实时调度不重要,而是从时间尺度上说,日前调度承担的是“全局粗调”的职责。实时调度在分钟级尺度上做小幅度修正,但它的前提是日前方案已经把大体框架定下来了,比如储能该在哪个时段充电、燃气轮机该在哪个时段启动,这些大方向必须在日前定好。
日前的完整操作窗口,给了优化器足够的视野去“看全貌”。比如一个简单的规则调度方案,看到晚上8点负荷高就用储能放电,但它看不到凌晨2点风电大发、负荷低谷的电价便宜,所以不会在那个时段充电。而日前优化调度是“全局统筹”,24小时一起优化,哪便宜哪充电、哪紧张哪放电,一目了然。这种全局视野,是日前调度相对启发式规则最大的优势。
2. 模型设计详解:目标函数与硬约束
2.1 成本目标:五个部分怎么算
目标函数是调度问题的“指挥棒”,它决定优化器往哪个方向使劲。我的模型中目标函数包含以下几项:
第一项是购电成本。微电网与大电网之间通过联络线交换功率,从电网买电要花钱。购电成本按下式折算:
C_grid = sum( P_buy(t) * price_buy(t) * dt )其中P_buy(t)是t时段的购电功率,price_buy(t)是分时电价,dt一般取1小时。如果允许向电网售电,则还需要加一项卖电收入,通常卖电价低于买电价,体现电网对微电网并网的“峰谷套利限制”。
第二项是燃气轮机燃料成本。燃气轮机用天然气发电,输出功率越高燃料消耗越大,通常用二次函数拟合,但线性化处理后变成分段线性函数。简化模型中,直接用一个发电效率常数折算燃料成本也可以,即成本与出力成正比。
第三项是储能充放电损耗成本。虽然储能充放电本身不直接消耗燃料,但每次充放电循环都会带来电池容量衰减,这一部分成本可用折算方式计入。有些模型直接忽略,把它合并到电池寿命维护费中,我的建议是做研究时加一个较小的损耗系数,可以增加模型的真实性。
第四项是需求响应补偿成本。用户响应削减负荷,微电网运行方需要付补偿费,这部分成本激励用户参与削峰填谷。补偿价格一般是固定值或者阶梯价格,实际工程中要和用户签订协议,确定每度电补偿多少钱。
第五项是弃风弃光惩罚成本。如果调度方案选择切掉一部分风电光伏出力,相当于浪费了免费电能,模型中用惩罚系数乘以弃电量。惩罚系数的取值要足够高,确保只有在系统安全约束无法满足时才去弃,一般取几千元/MWh,明显高于正常购电电价。
2.2 功率平衡:所有能量进出的“总账”
功率平衡约束是整个模型的中枢:每一时刻,所有“发电源”出力加上“储能放电”和“购电”,必须等于“负荷用电”加上“储能充电”和“售电”,再考虑需求响应切掉的负荷量。
用公式表示就是:
P_wind(t) + P_pv(t) + P_gt(t) + P_dis(t) + P_buy(t) = P_load(t) - P_dr(t) + P_ch(t) + P_sell(t)这个等式必须每个时段成立,它是微电网稳定运行的底线。如果这一条不满足,其他约束再完美也不是合法调度方案。
这里有个细节值得注意:需求响应削减负荷P_dr(t)是在负荷侧做减法,而不是在电源侧做加法。很多初学者理解成“需求响应就是多了个电源”,这种理解容易导致后续成本核算出错。我个人习惯把需求响应看作“虚拟机组”:它提供的是负荷削减量,成本是补偿单价,上限是可削减容量,优先级低于常规机组、高于弃风弃光,这样放进优化框架里更直观。
2.3 火电机组约束:上下限与爬坡
燃气轮机虽然单机容量小,但照样要遵守机组发电的物理规则。首先是出力上下限约束,即:
P_gt_min <= P_gt(t) <= P_gt_max这是由机组铭牌参数决定的。其次更重要的是爬坡约束:
-P_gt_ramp_down <= P_gt(t) - P_gt(t-1) <= P_gt_ramp_up这个约束表示相邻时段出力变化不能超过机组的爬坡能力。我在第一次建模时漏掉了爬坡约束,结果求解结果里燃气轮机出力从20kW直接跳到80kW,现实中根本做不到。这个约束是调度结果“可执行性”的关键。
如果燃气轮机还可以启停,那需要引入0-1启停变量来描述机组的运行状态。启停状态变量与出力上下限之间还需要关联约束,避免出现“停机状态但仍有出力”的矛盾情况。
2.4 储能约束:SOC连续性和充放电互斥
储能是微电网的“调节核心”,约束也是最容易出问题的部分。首先是SOC(荷电状态)递推公式:
SOC(t+1) = SOC(t) + (P_ch(t) * eta_ch - P_dis(t) / eta_dis) * dt / Cap其中eta_ch和eta_dis分别是充电和放电效率,Cap是储能容量。这个公式的意义是:充电给电池“加能量”,放电从电池“取能量”,效率让这个过程有损耗。我建议用这个“效率分别标在充/放两侧”的写法,它比统一用一个效率更符合实际。
其次是SOC上下限约束:
SOC_min <= SOC(t) <= SOC_max实际工程中SOC不会放到0或者充到100%,因为这会严重损害电池寿命,一般会限制在20%~90%区间。再次是储能充放电功率上限约束:
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) <= 1u_ch和u_dis是0-1变量,分别表示充电状态和放电状态。最后那条约束保证同一个时段储能只能充或只能放,不能“一边充电一边放电”。这个互斥约束用大M法或直接乘以0-1变量来实现,是MILP建模的标准操作。
还有一个容易忽略的点:调度周期结束时的SOC要恢复到初始值,或者设定一个目标终值。因为日前的调度是周期循环的,如果今天结束时电没充满,第二天早上就没有充足电量应对突发负荷。通常设为SOC(24) = SOC(0) = 0.5,或者设一个终值区间。
2.5 需求响应约束:削减不是无限度
需求响应模型的约束要看响应类型。如果是可削减负荷,比如空调、照明这一类可以临时关掉一部分的负荷,那么限制条件是削减量上限:
0 <= P_dr(t) <= P_load(t) * dr_ratio_max通常dr_ratio_max取10%~20%。如果是可转移负荷,比如洗衣机、洗碗机这类可以在时间上平移的负荷,建模会更复杂,需要引入“总用电量守恒”约束,也就是转移前后总用电量不变。我在基础模型中用的是可削减模式,因为它实现简单、参数少、结果分析直观,适合作为项目第一版。
还有一类约束说的是削减时长和削减次数:比如同一用户一天内最多参与响应2次,每次持续时间不超过3小时。这些约束用连续时段标志变量加累计求和约束实现。实际项目中这些约束是从需求响应协议中来的,模型里体现的是合同条款的数学化表达。
3. Matlab实现的核心代码拆解
3.1 整体框架:数据、变量、约束、求解四步走
一套完整的Matlab代码实现,大体分成四个模块。数据准备模块负责读入风电、光伏、负荷24小时预测曲线和分时电价;参数设置模块定义储能容量、机组出力限值、爬坡率、需求响应比例、补偿单价等;模型搭建模块用Yalmip工具定义优化变量和约束;求解与后处理模块调用求解器计算并画图。
我强烈建议用Yalmip工具箱来搭模型,而不是直接用Matlab的optimproblem或linprog。原因很简单:Yalmip支持用sdpvar和binvar直接创建连续变量和0-1变量,约束条件可以像写数学公式一样写进代码,可读性和修改效率都高出几个量级。
代码框架大概是这样的:
%% 数据与参数 T = 24; P_wind = [...]; % 风电预测曲线 P_pv = [...]; % 光伏预测曲线 P_load = [...]; % 负荷预测曲线 price_buy = [...]; % 分时购电价 %% 决策变量 P_gt = sdpvar(1, T); % 燃气轮机出力 P_ch = sdpvar(1, T); % 储能充电功率 P_dis = sdpvar(1, T); % 储能放电功率 SOC = sdpvar(1, T+1); % SOC,多一个初始时刻 P_buy = sdpvar(1, T); % 购电功率 P_dr = sdpvar(1, T); % 需求响应削减量 u_ch = binvar(1, T); % 充电状态 u_dis = binvar(1, T); % 放电状态 %% 约束 Constraints = []; for t = 1:T % 功率平衡 Constraints = [Constraints, ... P_wind(t) + P_pv(t) + P_gt(t) + P_dis(t) + P_buy(t) + P_dr(t) ... == P_load(t) + P_ch(t)]; end % ... 其余约束 %% 目标 Objective = sum(P_buy .* price_buy) + sum(P_gt .* fuel_cost) ... + sum(P_dr .* dr_cost) + ... %% 求解 ops = sdpsettings('solver', 'cplex', 'verbose', 2); optimize(Constraints, Objective, ops);3.2 储能约束的代码写法与坑
储能约束写起来比较绕,我直接给出我常用的代码片段:
%% 储能约束 eta_ch = 0.95; eta_dis = 0.95; Cap = 1000; % kWh SOC_0 = 0.5; SOC_min = 0.2; SOC_max = 0.9; P_ch_max = 200; P_dis_max = 200; M = 1000; % 大M Constraints = [Constraints, SOC(1) == SOC_0]; for t = 1:T % SOC 递推 Constraints = [Constraints, ... SOC(t+1) == SOC(t) + (P_ch(t)*eta_ch - P_dis(t)/eta_dis) * 1 / Cap]; % 充放电互斥 Constraints = [Constraints, ... 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]; % SOC上下限 Constraints = [Constraints, ... SOC_min <= SOC(t) <= SOC_max]; end Constraints = [Constraints, SOC(24) == SOC_0];这里M是用来辅助实现0-1变量与连续变量联动的。其实利用Yalmip的约束写法,可以直接把二元变量乘在功率上限上,不需要显式写出大M常数。上面代码里我直接用P_ch_max * u_ch(t)作为上界,这是一种变量耦合约束,Yalmip会内部把它转化为混合整数约束,效果与大M法等价,而且代码更直观。
不过要注意:如果P_ch_max和P_dis_max相差很大,或者求解器提示数值问题,那就要回到显式大M法:
Constraints = [Constraints, P_ch(t) <= P_ch_max * u_ch(t) + 0, ... P_ch(t) >= 0];这里加大M的目的是把约束“松弛”掉,当u_ch=1时,P_ch最大值为P_ch_max;当u_ch=0时,P_ch被强制为0。第一次用大M法写的模型经常把大M设得过大,比如100000,导致求解器数值问题。经验取值是M取该变量物理上限的1.5~2倍,够用就行。
3.3 求解与结果输出
求解环节的代码很简短,但要注意几个细节:
ops = sdpsettings('solver', 'cplex', 'verbose', 2, 'showprogress', 0); result = optimize(Constraints, Objective, ops); if result.problem == 0 P_gt_value = value(P_gt); P_ch_value = value(P_ch); P_dis_value = value(P_dis); SOC_value = value(SOC); P_dr_value = value(P_dr); else disp('求解失败'); yalmiperror(result.problem); endresult.problem是求解状态的返回码,0表示求解成功。如果不检查这个返回值,很可能会出现后面绘图用到NaN数据还不自知的情况。我调试代码时经常干这种事:优化出的结果全是NaN,还在那里纠结约束哪里写错了,最后发现是求解器根本没找到最优解。所以一定要养成检查result.problem的习惯。
绘图阶段,一般要画四张图:功率平衡堆叠图、储能SOC变化曲线、需求响应安排图、各时段购电/售电功率。堆叠图用area或者bar,能直观看出24小时能量流向。SOC曲线要检查是否在上下限之内、是否首尾相接,这两个细节对了,通常模型基本没问题。
时间: 00:00 01:00 02:00 ... 23:00 风电: 120 95 78 ... 130 光伏: 0 0 0 ... 0 负荷: 280 260 240 ... 310 电价: 0.35 0.35 0.35 ... 0.85上面是一个典型的数据样例。我把数据放进Excel里管理,Matlab代码只负责读取,这样改数据不用改代码,很方便。模型参数比如储能容量、燃气轮机最大出力、需求响应比例,我建议全部常量集中放在代码头部,加注释,这样别人拿到代码也能快速改参数跑通。
4. 调参过程中真实踩过的坑
4.1 储能SOC振荡问题
我第一次跑通模型时,发现SOC曲线在凌晨时段出现锯齿状振荡:一会儿充电、一会儿放电、再过一小时又充电。乍一看好像合理,仔细想了下根本不合理。原因出在目标函数里没有储能充放电的“使用成本”,导致电价低谷时段优化器为了满足功率平衡,频繁在充电和放电之间切换,反正不花钱嘛。
解决方案是给储能充放电加上一个很小的单位损耗成本,比如0.01元/kWh。加了这个成本后,优化器会尽量减少不必要的充放电切换,SOC曲线就平滑了。这个细节在很多论文和开源代码里不会明说,但在工程实现阶段非常关键。
4.2 需求响应被“滥用”
另一个问题是需求响应削减量被优化器无脑拉满。原因是补偿单价设得太低,低于购电电价,优化器发现“切电比买电便宜”,于是只要负荷高峰期就拼命切负荷,甚至把可削减负荷全切了。然而现实中这会导致用户强烈不满,违背需求响应的设计初衷。
正确调法是让需求响应补偿单价介于正常电价和峰值电价之间,比如正常电价0.4元/kWh、峰时电价1.0元/kWh,补偿单价设成0.65元/kWh。这样优化器会在峰时适当切负荷、在平时尽量不切。还可以加一个日最大削减次数约束,避免整天都在砍负荷。遇到这类问题,我通常先检查参数区间是否合理,再调整补偿价格,而不是盲目调目标函数权重。
4.3 求解器选错导致的性能灾难
同样的模型,用不同的求解器求解速度可以差出好几倍。用默认的求解器有时候会慢得离谱,尤其是存在大量0-1变量的MILP模型。我常用的组合是Yalmip加CPLEX,或者Yalmip加Gurobi。如果没有商业求解器,Matlab内置的intlinprog也能求解,但性能差一些,大规模场景会非常吃力。
这里要提醒一下:Yalmip默认调用求解器的优先级可能不符合你的预期。建议在sdpsettings里显式指定求解器,比如:
ops = sdpsettings('solver', 'cplex', 'verbose', 0);如果没装CPLEX,还可以用Gurobi:
ops = sdpsettings('solver', 'gurobi', 'verbose', 0);如果有学生或者个人使用环境,可以考虑用SCIP或者Matlab自带的intlinprog,这些开源方案也能求解中小规模MILP。
4.4 可行域为空的排查路径
模型写好后最怕的就是求解器提示“infeasible problem”,即无可行解。第一次遇到这种情况,我从目标函数到约束检查了无数遍,后来总结出一套排查思路。
先检查数据边界。看看风电、光伏预测值是否太小,而负荷又太大,导致即使所有电源满发、储能满放、购电拉满都无法满足功率平衡。这时要么增加联络线容量,要么允许一定程度的切负荷。再检查SOC初始值约束。如果SOC初始值和SOC终值都设成0.5,中间又严格要求SOC_min=0.6,那就直接矛盾了。
还有一类隐蔽问题出现在辅助0-1变量上。比如储能同时有充电功率0.95倍效率和放电0.95倍效率,而SOC的范围又比较窄,加上功率平衡约束,可能就会出现无解。建议先把需求响应约束注释掉,把储能互斥约束替换成简单的功率上下限,逐步解除约束来看是哪一条导致无解。
4.5 结果合理性需要“肉眼验证”
最后也是最重要的一条经验:求解器给出“最优解”不代表结果真的合理。一定要把结果画出来,用眼睛检查几个关键点:功率平衡等式两边是否一致、SOC是否保持在上下限内、充放电是否存在同一时段同时进行、需求响应是否频繁触发、购电功率是否超过联络线最大允许值。
有一次我运行完模型,目标函数值非常可观,结果检查SOC曲线发现中间时段冲到1.0,超过了设定上限。排查后发现是我在SOC约束里写的是SOC(t)而不是SOC(t+1),导致最后一个时段的SOC没被约束到。这种边界细节问题,纸上谈兵很难发现,实际画图检查一目了然。所以我的习惯是:每改一次模型,跑完必须画全套图,看图讲一个“能量故事”:什么时候充电、什么时候放、什么时候买电、切了多少负荷,整个流程说得通,模型才算是真正做对了。
5. 下一步扩展:从基础模型走向实战
5.1 加入旋转备用约束
工业级的微电网调度不会只满足功率平衡,还会要求系统留有旋转备用容量。旋转备用是应对风电、光伏预测误差和负荷突变的安全冗余。一般建模方法是在每个时段加一个备用约束:
P_wind(t) + P_pv(t) + P_gt_max + P_dis_max + P_buy_max >= P_load(t) + reserve_required(t)备用需求可以设成负荷的5%~10%加上风光出力的某个比例。增加这个约束后,调度方案会更“保守”,储能充电时机会更谨慎,因为要留出放电能力作为备用。我建议在做完基础版本后,先增加这个约束,这是从学校模型走向工程模型的重要一步。
5.2 多场景随机优化
基础模型假设风电、光伏出力是确定的预测值,实际这几乎不可能。一种更接近实战的做法是引入多场景随机优化:生成多个风光出力场景,每个场景有不同概率,目标函数变成所有场景的期望成本加权和。
实现上需要构建场景树,决策变量分两阶段。第一阶段变量是日前必须提前决定的,比如机组启停状态、需求响应计划;第二阶段变量是在每个场景下可以调整的,比如储能实际充放电量、购电功率。求解规模会显著上升,但更贴近真实决策过程。如果刚接触这部分,建议先尝试三到五个典型场景,比如“大风光”“小风光”“常规天气”三种,看结果差异,再逐步增加场景数量。
5.3 碳排放约束与“双碳”目标下的扩展
近年来很多微电网调度项目都会加入碳排放约束。基本做法是把碳排放作为约束条件,设定总排放上限,或者把碳价纳入目标函数。燃气轮机每发一度电对应一定的碳排放量,储能和风光充放电不产生直接碳排放,购电则要看电网侧碳排放因子。
这些扩展在Matlab代码层面改动并不大:加一个排放约束,再加一个碳价参数到目标函数里。但带来的结果差异很明显:碳价高了,调度方案会倾向多用风光、多用储能放电、减少购电和燃气轮机出力。这套扩展做下来,模型的价值直接从“省钱工具”升级成“兼顾经济性和低碳性的决策工具”,实际意义更大。
5.4 从日前到日内滚动
最后一个扩展方向是日内滚动调度。日前的预测到了中午可能偏差很大,这时可以做4小时或1小时的滚动优化:保留日前方案里已执行的变量值,只对未来一个时间窗重新优化。代码实现上用Matlab循环调用优化函数即可,关键是把当前SOC状态传给下一次优化,作为初始条件。
滚动优化的好处是调度方案能够“一只眼睛看前方,一只脚踩现实”,提升系统应对突发情况的能力。这个方向做下来,差不多就是一套完整的多时间尺度微电网优化调度平台了,放到实际工程项目里也完全站得住脚。
我个人实际运行中的体会是:这个题目真正的难点不在数学推导,也不在算法复杂,而在于保持模型的物理合理性与工程可解释性。约束不够,结果空有最优名头却不现实;约束太多,模型脆弱又难调。建议从最简单的模型开始,每次只增加一类约束或一个扩展模块,跑通后再往下一步走。等你能清晰地解释清楚每一个约束对应现实里的什么设备限制、每一个成本项对应运营里的哪笔开销,这套代码和模型就算真正变成你自己的东西了。后续完全可以继续往不确定性优化、多微电网协同、虚拟电厂聚合这些方向延伸,前景很开阔。