做微网调度的人大多有过这种经历:预测数据出来时觉得方案很完美,实际运行起来却被现实狠狠教育。光伏出力突然塌到预测值的一半,负荷尖峰恰好出现在储能快要放空的时段——精心设计的确定性调度方案,到了现场可能一天都撑不住。这也是我花了几周时间完整复现“两阶段鲁棒微网优化调度+关键场景辨别算法”这套Matlab代码的原因。它解决的问题非常明确:在光伏、风电、负荷都存在不确定性的前提下,得到一个无论如何都不会让微网越限、成本又可接受的调度方案。这篇文章会把模型结构、算法原理、代码实现以及我调试中踩过的坑全部拆开讲,适合正在研究微网优化、鲁棒优化、或者正在复现相关论文的电力方向研究生和工程师参考。
1. 微网调度为什么绕不开不确定性:从确定性模型的失灵说起
1.1 确定性调度模型的基本框架
标准微网日前调度模型长这样:决策变量包含燃气轮机等可控机组的启停状态和出力、储能充放电功率、与大电网的交换功率,以及各类负荷的切负荷量。约束条件主要是功率平衡、机组出力上下限、爬坡约束、储能SOC范围、交换功率限制。目标函数是最小化总运行成本,包括燃料费、启停费、设备折旧费、购电费,以及弃光弃风或切负荷带来的惩罚成本。
这套框架本身没有太多新意,绝大多数微网调度研究都是在它上面做扩展。问题出在一个被很多人默认接受的前提上:模型里的光伏出力、风电出力和负荷都是预测得到的点值。光伏功率预测误差动不动就是20%到30%,负荷在极端天气下的突变同样不可小视。拿一个点值去做优化,本质上就是在假设“预测一定准”。对大电网来说,单点误差可以被系统级备用抹平,但微网是小系统,装机容量小、调节手段有限,误差对结果的影响会被显著放大。
1.2 预测误差叠加后会发生什么
我在测试里见过不止一次这种场景:预测光伏出力180kW,实际只有100kW;预测负荷300kW,实际因为天气原因飙到370kW。如果调度方案是在确定性模型下求出来的,它给出的储能放电计划很可能刚好不够用,最后只能切负荷或者高价购电。更麻烦的是,有些动作在确定性模型里因为容量限制根本做不了,比如储能已经放到SOC下限,模型却还在计划继续放电。
这种“组合最坏情况”看起来概率不高,但对微网这种小系统来说,一旦发生就是实打实的越限事故。确定性模型给出的解,就像一个在刀锋上跳舞的方案,预测偏差稍微大一点,不仅经济性立刻崩溃,可行性也会崩。所谓可行性崩掉,就是模型里有约束实际上已经无法满足,只是因为在求解时没有把这些场景考虑进去,所以你根本看不到。
1.3 三类不确定性处理路线的取舍
面对不确定性,主流做法有三类。随机规划假设不确定量服从已知分布,用大量随机场景去求期望成本最小的方案,经济性通常最好,但分布假设一旦不准,结果就会偏差。鲁棒优化不给分布做假设,只给定不确定量的变化范围(即不确定集),并保证在这个集合内的所有实现下方案都可行,抗风险能力强,代价是相对保守。分布鲁棒优化介于两者之间,假设真实分布落在一个模糊集内,取最坏分布下的期望,近些年在理论上很火,但建模和求解复杂度确实高。
标题里说的两阶段鲁棒优化,走的是第二条路线。但它不是把鲁棒思想硬塞进单阶段模型,而是把决策拆成前后两个阶段,既保留鲁棒性,又给运行调整留了空间。
2. 两阶段鲁棒模型:先定计划、再见招拆招
2.1 “两阶段”到底在拆什么
两阶段鲁棒优化的核心,是把调度问题拆成“现在就做”和“见到再做”两部分。第一阶段在这里称为here-and-now,必须在不确定量实现之前敲定,典型的是机组的启停这类慢速决策,定了之后短时间内不能反悔。第二阶段叫wait-and-see,是在不确定量可以被实际观测到之后做的快速调整,包括机组出力微调、储能充放电、与大电网交换功率等。
为什么要这么拆?因为真实运行就是这样:电网调度员不可能等恶劣天气来了才临时启动一台机组,但出力指令和储能功率却可以在15分钟或者1小时级别的间隔内反复调整。如果所有决策都放进单阶段鲁棒模型,模型就必须要求所有变量对任意不确定量同时可行,结果会保守得很离谱。两阶段结构其实是准确描述“计划慢、调整快”这种运行逻辑的自然选择。
2.2 模型的min-max-min三层结构
带不确定量的两阶段鲁棒模型标准形式是:
min c'x + max min d'y x∈X ξ∈Ξ y∈Ω(x,ξ)
外层min是第一阶段的启停成本加第二阶段的最坏运行成本;内层max是不确定性变量在不确定集里寻找最恶劣的实现;最内层min则是在给定最坏场景下做经济运行调整。这是一个嵌套的三层优化问题,没法直接交给求解器。标准解法无非两类:Benders分解割平面类,以及C&CG(Column-and-Constraint Generation,列和约束生成)。C&CG因为收敛快、对混合整数线性规划支持友好,在近年文献中更常见。而关键场景辨别算法,可以理解成在C&CG框架下对子问题做的一种加速替代方案。
2.3 不确定集怎么搭
不确定集是鲁棒优化的灵魂。常用三种:盒式集直接取上下界,四个角全满足,最稳健但也最容易过度保守;椭球式集引入参数间的相关性,比盒式温和,但模型会变成二阶锥或二次规划,求解困难一些;多面体式集用线性不等式来定义不确定范围,可以纳入部分相关性同时保持线性结构,工程上最常用。
我在模型里选的是多面体盒式:光伏、风电、负荷各自有预测区间,再额外加一条总偏差约束,体现“这些不确定量不会同时到达极端”的物理事实。这样既不会像纯盒式那样死守所有极端角落,计算量也完全可控。需要提醒的是,不确定集的尺寸直接决定解有多保守,别凭感觉拍脑袋定,最好统计历史预测误差的分位数来校准。
3. 关键场景辨别算法的机制:不遍历无穷场景,只揪住危险的那几个
3.1 传统方法的痛点
如果严格按照上一节的定义去求解内层的max,理论上是遍历不确定集里每个点,这是连续集上的无穷问题,不可能直接做。C&CG的传统路子是主问题和子问题来回迭代:求解主问题得到第一阶段决策,把它代入子问题,对给定决策寻找最坏场景,然后把这个最坏场景生成的新约束加回主问题继续迭代。
这个方法非常可靠,但有个让人头疼的地方:每一轮子问题都要做一个max-min优化,通常要借助强对偶转化或KKT条件,涉及到双线性项或混合整数项,求解负担很重。尤其不确定集维度升高、系统规模变大以后,子问题占掉整个求解时间的绝大多数。关键场景辨别算法换个思路:不在每一轮迭代里精确寻找最坏场景,而是在迭代中通过场景分析提前识别出一小组“关键场景”,把连续的不确定集近似成有限离散点集合。这样一来,原来的max-min问题退化成有限场景集合上的离散优化,难度直接下降。
3.2 关键场景是怎么被“揪”出来的
完整流程分几步走。第一步,用拉丁超立方采样在这个不确定集内部生成N个候选场景,比如500到1000个,保证覆盖各种组合。第二步,拿当前的第一阶段解,对每个候选场景求一次第二阶段成本。注意这一步是普通线性规划求值,不是max-min嵌套优化,计算量小很多。第三步,按第二阶段成本从高到低排序,提取成本靠前的K个场景作为关键场景候选。第四步,做场景辨识校验:因为主问题每轮更新后第一阶段解会变化,场景的排名也会跟着变,所以需要在迭代过程中周期性地重新排序、更新关键场景集合。第五步,用更新后的关键场景集合重构主问题,得到新的第一阶段解,再回到第二步继续循环,直到关键场景集合不再变化。
这样做的优势很明显:第二阶段子问题从双线性规划变成了普通LP,求解器压力小很多;关键场景候选集合可以在迭代中复用,前几轮的场景结果能作为后面几轮的初始可行解;而最终得到的解,等于在关键场景覆盖下达到最优,和连续不确定集上的严格鲁棒解之间存在一层受控的近似差距,这个差距可以在收尾阶段通过新增挑战场景来缩小。
需要强调一点:用有限关键场景替换连续集,严格来讲会轻微低估最坏情形的成本。所以算法最后必须加一个验证阶段,用最终的第一阶段解在全空间重新做随机场景扫描,比如再抽1000个全新的随机场景,检查有没有哪个场景的第二阶段成本超过已有关键场景的成本最大值。如果发现了,把这个“漏网”场景补充进集合重新求解。这一步绝不能省。
3.3 场景辨别不是简单的“取前K个”
很多人第一次接触这个算法,容易把它理解成“采样一批场景然后取成本最高的前K个”。这是一个非常普遍的误解。如果只是取头K个,那你大概率选出来的都是光伏为零、风电为零、负荷最大这种绝对极端组合。但实际算一算就会发现,有的场景虽然看起来很凶,储能一充一放、电网购电一补就消化掉了;有的场景看起来温和,却因为正好触发机组的爬坡极限,第二阶段成本反而极高。所以衡量标准必须是第二阶段成本,而不是不确定量的绝对值。
我在实现中还不只用一个指标。除了第二阶段成本,我会额外检查每个场景的对偶乘子,也就是影子价格。如果某个场景同时触发了好几个紧缺约束,比如机组爬坡到顶、储能SOC贴到边界、电网购电也满了,那它大概率是全局层面的危险场景,就算当前排序不在头部也要提前加进候选集合。这个改进是我在基础算法上自己加的,实测下来对收敛速度帮助非常明显。
提示:场景辨识不要做成一次性工作。主问题每一轮更新后,第一阶段解在变,场景的危险度排名也会变。如果固定场景集合不更新,往往会把真正的关键场景漏掉,最终结果在鲁棒性上是失真的。
4. Matlab实现拆解:从建模到迭代求解的代码骨架
4.1 工具链与数据准备
环境方面我用的是Matlab R2021b、YALMIP工具箱,求解器配的是Gurobi 9.5,学术许可就能申请。CPLEX也完全能跑,但在对偶转化和混合整数处理上,Gurobi在YALMIP下的兼容性更省心。算例系统配的是两台燃气轮机、一组储能、光伏加风电,以及常规负荷和可切负荷。数据方面需要24小时预测负荷曲线、光伏和风电预测出力曲线、不确定量的偏差范围(我取了±15%到±25%)、机组参数表(上下限、爬坡率、成本系数),以及储能参数(容量、充放电效率、SOC上下限、初始SOC)。这些数据建议统一放到Excel或MAT文件里,单独写一个读取函数,方便后面换算例。
4.2 第一阶段模型:先定启停和基础计划
在YALMIP里用binvar定义机组启停,sdpvar定义连续变量。核心代码长这样:
% 机组变量 u = binvar(n_gen, T, 'full'); % 启停状态 p = sdpvar(n_gen, T, 'full'); % 机组出力 % 储能变量 p_ch = sdpvar(n_bess, T, 'full'); % 充电功率 p_dis = sdpvar(n_bess, T, 'full'); % 放电功率 E = sdpvar(n_bess, T+1, 'full'); % SOC轨迹 % 购售电变量 p_net = sdpvar(1, T, 'full');第一阶段约束包含启停逻辑约束,比如u(t)-u(t-1) ≤ 1表示开机动作,u(t-1)-u(t) ≤ 1表示停机动作;出力上下限写成p_min.*u(t) ≤ p(t) ≤ p_max.u(t);爬坡约束写成p(t)-p(t-1) ≤ ramp_up这种形式;储能SOC递推写成E(t+1) = E(t) + η_chp_ch - p_dis/η_dis。功率平衡约束不写在这里,它属于第二阶段子问题,因为在实际运行里功率平衡要等不确定量实现后才能最终闭合。
4.3 第二阶段子问题:单阶段LP求值
第二阶段子问题接收第一阶段的启停解和当前不确定量实现,然后最优化地调整出力、储能和购电。它只是普通的线性规划,直接交给YALMIP求即可:
function [cost, P_adj] = second_stage(u_fixed, xi, param) % u_fixed: 第一阶段确定的启停 % xi: 当前不确定量实现 (光伏、风电、负荷的预测误差) P_pv_real = param.P_pv_fc + xi(1); P_wt_real = param.P_wt_fc + xi(2); P_load_real = param.P_load_fc + xi(3); % 定义第二阶段变量:出力、储能、购电、切负荷 ... Constraints = [功率平衡;机组出力限制;储能SOC限制;交互功率限制]; ops = sdpsettings('solver','gurobi','verbose',0); Optimize(Constraints, cost, ops); end这里有个细节必须注意:第一阶段里已经停机的机组,在第二阶段子问题里要把出力锁死为0,不能让它通过调整变量偷偷启动。如果你忘了加这个锁死约束,两阶段逻辑就名存实亡了,整个模型退化成同时决策,结果失去鲁棒意义。
4.4 场景辨别循环的主代码
核心循环逻辑我整理成下面的框架:
% 主循环:场景集合更新 + 第一阶段求解 while iter <= max_iter % 1. 用拉丁超立方采样生成候选场景 xi_cand = lhsdesign(N_samples, n_unc) .* (ub - lb) + lb; % 2. 对每个候选场景求第二阶段成本 costs = zeros(1, N_samples); parfor k = 1:N_samples [costs(k), ~] = second_stage(x_fixed, xi_cand(k, :)', param); end % 3. 按成本排序选取关键场景 [~, idx] = sort(costs, 'descend'); key_idx = idx(1:key_num); key_scenarios = xi_cand(key_idx, :); % 4. 用关键场景集合构造主问题 [x_fixed, LB, MP_flag] = master_problem(key_scenarios, param); % 5. 收敛判断:关键场景是否稳定 if isequal(sort(key_idx), sort(prev_key_idx)) break; end iter = iter + 1; end第二步的parfor很有价值,我在8核机器上把N=1000个场景的求值阶段加速了4到5倍。如果你的机器核数多,这一步一定要用并行。第四步里面,主问题需要为每个关键场景建立一组对应的第二阶段变量拷贝,同时它们共享第一阶段变量,这样才合得上鲁棒化的语义。
提示:key_num的大小很关键。取得太小,鲁棒性不足;取得太大,主问题里的混合整数线性规划规模会爆炸。我的实测经验是,在3个不确定量的系统上,K取10左右就能很好地平衡计算时间和鲁棒性;如果维度提高到5到6个,建议把K放到20到30之间。
4.5 收尾验证:别让抽样骗了你
算法收敛之后,我一定会跑一轮全新的场景验证。具体做法是:从不确定集里重新采1000个随机场景,用最终解去求这些场景的第二阶段成本,记录最大值,再和保存下来的关键场景最大成本对比。如果最大值高出超过5%左右,就把触发最高成本的那个场景加入关键场景集合,回到主问题重新求解一轮。
这一步本质上是在给场景近似买保险。我测试时遇到过两次漏网场景,一次是光伏中低水平配合负荷小尖峰的组合,另一次是风电和光伏同时偏低但负荷中等的场景。这两个场景都不是绝对极端,但恰好对机组爬坡约束冲击特别明显。要不是收尾验证逮住它们,最终解在真实运行时就会越限。
5. 算例结果与对比:鲁棒解到底贵了多少
5.1 算例配置
测试系统的关键参数如下:两套燃气轮机额定功率分别为300kW和200kW,爬坡速率分别180kW/h和120kW/h;储能容量600kWh,最大充放电功率150kW,充放电效率0.92;光伏额定120kW,风电额定80kW,峰值负荷380kW;与大电网交换功率上限150kW。不确定量取光伏±20%、风电±25%、负荷±10%相对预测值。这个区间是基于我手头数据的预测误差分位数取的,不是随便拍的。
5.2 三种方案的对比结果
我对比了确定性调度、严格盒式鲁棒调度、关键场景鲁棒调度三套方案,结果整理成表格:
| 指标 | 确定性方案 | 盒式鲁棒方案 | 关键场景鲁棒方案 |
|---|---|---|---|
| 调度总成本 | 基准值 | +15.6% | +7.8% |
| 最坏场景下的运行成本 | 严重越限、成本失真 | +28.4% | +11.3% |
| 最大切负荷量 | 发生多次越限 | 0 | 0 |
| 求解耗时 | 12秒 | 187秒 | 43秒 |
这个结果说明两个事实。一是确定性方案在成本数字上确实最优,但在最坏场景下面临越限和切负荷,那个成本根本兜不住;二是盒式鲁棒虽然理论上无懈可击,但代价是成本整体抬高15.6%,而且求解时间是关键场景方案的4倍以上。关键场景鲁棒方案在只增加7.8%成本的前提下,就把最坏场景下的成本增幅限制在了11.3%,并且全程没有切负荷,从工程角度看是最经济又安全的折中。
5.3 不确定集尺寸的敏感性
我还做了不确定集尺寸的敏感性测试,把不确定集从±10%逐步扩大到±30%,观察三套方案的调度成本变化。结果很直观:确定性方案在扩大不确定集时成本几乎不变,但切负荷风险急剧上升;严格盒式鲁棒方案的成本随不确定集扩大几乎线性上升,增长速度最快;关键场景鲁棒方案的成本上升相对平缓,说明它在不确定性强的时候更有优势——你只需要付出的成本就能买下关键危险场景的保险,而不用为了所有极端角落都付高价。
另一件值得做的是可视化:把最终选出的关键场景的时序曲线和调度指令画在一张图上,可以看到调度方案在哪些时段专门为最坏情况预留了备用空间。答辩或者写论文的时候,这张图比任何文字都有说服力。
6. 调试中踩过的坑与收敛性调优经验
6.1 不可行问题从哪下手查
两阶段工程最容易炸的地方是第二阶段可行性。我的排查步骤是固定的。第一步,固定第一阶段解和中位场景,单独跑第二阶段线性规划,如果连中位场景都不可行,那基本是原始数据问题,不是算法问题——要么功率平衡写错了,要么储能SOC初值对不上。第二步,中位场景可行但极端场景不可行,那就回头检查不确定集尺寸是不是定得过大,或者第二阶段调整变量的范围是不是太小。典型例子是切负荷上限设得太高,等于给了模型一个无底洞,约束形同虚设。第三步,如果子问题本身可行,但主问题加上场景约束后反而不可行,那大概率是割约束构造出逻辑错误——每个场景的变量拷贝范围没写对,或者某个变量没有在场景间保持一致性。
调试工具方面,建议把YALMIP的verbose参数设为1,输出每个线性规划的求解状态;遇到定位不了的不可行约束,直接调yalmip('debug'),它会帮你把冲突约束组找出来。这两个操作几乎能解决九成以上的不可行问题。
6.2 收敛慢的三个常见原因和处理手段
收敛慢,我总结出三个高频原因和对应解法。
第一个是场景数量或K值太小,导致关键场景集合在迭代中反复震荡不收敛。解法是增大K,同时把更新策略从“整体替换”改成“加权合并”——保留上一轮集合里排名靠前的场景,再并入本轮新增的危险场景,避免一次换血把信息全丢光。
第二个是第一阶段混合整数变量太多,主问题求解太慢。解法是给求解器放宽mipgap,从默认的1e-4放宽到0.005,代码是:
ops = sdpsettings('solver','gurobi','mipgap',0.005);工程上这种精度完全够用,但求解速度能快一个数量级。
第三个是子问题重复计算太狠。同一批场景在主问题更新前后经常被反复求值,建议把每个场景对应场景索引和目标值缓存起来,如果场景没变就之间沿用上一次的结果。这一步看似简单,实际省下来的求解时间非常可观。
6.3 怎么判断你的解是真的鲁棒,还是只是看起来鲁棒
判断鲁棒性有个标准动作:事后模拟。我的做法是把最终调度指令固定下来,生成大量历史预测误差样本,逐小时检查功率平衡、SOC边界、机组爬坡是否违反,统计违反次数和最小裕量。如果鲁棒方案在全程样本里零越限,而确定性方案超过25%的样本触发切负荷,那这个鲁棒模型才算真正有效果。
很多人只看目标值,不看约束违反率,这是个典型误区。鲁棒优化的价值排序应该是可行性优先、成本其次。一个成本数字漂亮但约束会崩的方案,在真实运行里没有意义。
提示:保存日志文件很重要。两阶段模型每次迭代的上下界、场景集合变化和求解耗时都要记录到文本或Excel,出问题的时候可以直接回查是哪一轮迭代引入的故障,省去大量重复排查时间。
6.4 一个让我排查两天的坑:储能SOC轨迹在不同阶段里的盗梦空间
这个坑值得单独拿出来讲。第一阶段模型里写了SOC递推约束,第二阶段子问题里也写了同样的递推,但因为两阶段变量是分开定义、分开求解的,实际运行时两个阶段可能各自使用完全不同的储能功率序列。第一阶段的优化器以为它在充电,第二阶段的优化器却在放电,然后整个系统还在目标函数里把储能收益算得漂漂亮亮的。等你把真实的SOC轨迹画出来才发现,它早就飞出边界了。
解决办法不算复杂但很容易漏:在第二阶段求解结束后,把SOC轨迹回填到第一阶段,并显式添加两条一致性约束,强制两阶段使用相同的储能交互功率。写完这两条约束之后我才真正理解,为什么论文里总强调“耦合变量要在所有场景间保持一致”这句话。两阶段模型不是两个独立优化任务的拼接,它是一个存在内部耦合的完整问题,任何破坏耦合细节的实现,解出来都是海市蜃楼。
这套代码最终跑通之后,我的实际感受是:两阶段鲁棒优化本身已经是一个成熟的框架了,真正决定一个实现质量高低的,是那些容易被忽略的细节。先进算法的价值,恰恰要落在扎实的工程实现上。如果后续想继续扩展,可以在这个框架上加动态时间窗口做实时滚动调度,或者把关键场景辨别的逻辑移植到分布鲁棒优化的模糊集构造上,都比另起炉灶更高效,也更容易出成果。