
1. 为什么微网调度要选两阶段鲁棒优化做微网调度的人大概都经历过这种纠结光伏出力预测不准负荷曲线飘忽不定按预测值排好的计划一到实际运行就被打脸。白天光伏突然被云遮住出力从100%直接掉到30%储能忙着补缺柴油机来不及爬坡考核指标一塌糊涂。传统确定性调度在这种场景下特别脆弱因为它默认一切按预测值走而现实世界从不按剧本出牌。这两年“两阶段鲁棒优化”和“关键场景辨别算法”在微网优化调度的论文里几乎成了标配但论文里公式一大片复现起来却总觉得少根拐杖。今天我就把基于关键场景辨别算法的两阶段鲁棒微网优化调度从原理到Matlab实现完整拆开讲一遍。这篇内容适合正在做微网EMS、园区光储柴系统、或者正在写电气方向研究生论文的读者。我会按“为什么这样建模—核心算法怎么运作—模型具体长什么样—Matlab怎么落地—算例怎么验证—常见坑怎么躲”的顺序展开尽量用能直接抄作业的方式讲。1.1 从确定性调度到不确定性调度先看一个最基本的微网调度模型。给定光伏预测、负荷预测通过优化计算储能每小时充放电功率、微型燃气轮机出力和联络线功率使得全天运行成本最低。这个模型本身很简单就是一条功率平衡约束加上各设备出力上下限一小时内就能在Matlab里跑通。但它的问题也很明显预测一定有误差而且微网里的光伏受天气影响波动极大。一旦实际光伏比预测低20%原本平衡的功率就被打破储能可能已按计划充满电无处可用燃气轮机也可能顶不上爬坡速率最后只能切负荷。确定性调度把所有预测值当成硬性约束来优化没有给“预测错了怎么办”留任何余量。随机优化是另一种思路它给预测误差设定概率分布生成大量场景参与优化。这个方法理论上精细但工程实现时至少有两个痛点一是真实场景的概率分布很难准确获得二是场景数量一多整数决策变量和连续变量耦合的优化问题规模会爆炸求解时间难以接受。用少量场景代替全部场景又很可能漏掉那些最危险的极端情况。单阶段鲁棒优化则是把不确定性描述成一个集合要求所有可能的不确定值都满足约束。它能保证“不管天气多恶劣都不会翻车”但代价是约束条件必须按最坏情况设计结果往往过于保守——为了应对一年只出现几次的极端场景日常运行成本被明显抬高。1.2 两阶段鲁棒的“先决策、后调整”逻辑两阶段鲁棒优化恰好从中找到一个平衡点。它把调度问题拆成两个层次第一阶段先做“现在必须定下来”的决策比如机组启停、储能是否处于待命状态、与上级电网的购电协议第二阶段等不确定性真正发生后再做“可以临时调整”的决策比如各机组实际出力、储能充放电量、切负荷量。目标函数写成 min-max-min 的形式意思是我要做的第一阶段决策在最恶劣的不确定场景下能让第二阶段调整成本尽量小。打个不那么严谨的比方。你出门前决定要不要带伞、带不带外套这是第一阶段决策到了街上真的下雨或者升温了你选择是打车还是躲在屋檐下这是第二阶段调整。你不需要穷举未来365天的所有天气但你会重点考虑“暴雨”“暴晒”这几个关键场景再评估手里的现金够不够应对。两阶段鲁棒的价值就在这它允许我“先拍板一部分计划”同时把“相机而动”的空间留给实时运行。相比单阶段鲁棒它的保守性可以通过不确定集合的松紧来调节相比随机优化它不依赖概率分布假设只需要知道不确定量的大致波动范围。这套框架微网调度天然匹配——因为日前计划必须提前定但实时出力可以在线调。2. 关键场景辨别算法从“怕最坏”到“抓关键”2.1 为什么需要关键场景辨别两阶段鲁棒模型的标准解法是CCG列与约束生成算法原理上要反复求解主问题和子问题通过迭代让上下界收敛。子问题里有一个“max”层要从连续的不确定集合里找出让系统最难受的那个场景。如果不做任何处理连续不确定集合里有无数种可能的组合计算机不可能一个一个穷举。更现实的问题是两阶段鲁棒求出来的最优解实际只被少数几个极端场景“卡住”。微网里不确定性源通常有两三个——光伏出力、负荷波动最多加一个风电。这些不确定性源相互作用时真正让调度方案失效的组合就那么几类光伏低发且负荷高峰、光伏大发且负荷低谷、以及两者叠加的极端情况。绝大多数介于中间的场景对一阶段决策几乎没有影响。关键场景辨别算法的思路就是不去枚举所有可能的不确定值而是用数学手段从不确定集合里挑出那些对调度成本影响最大的代表性场景只让这些场景参与第一阶段决策。其余大量普通场景在一阶段不做硬性约束而交给第二阶段实时调整去消化。这样既保留了鲁棒优化“不怕极端情况”的优点又避免把所有场景全部塞进主问题导致求解规模失控。2.2 关键场景辨别的实现思路具体实现上关键场景辨别不是独立于CCG的一个算法而是对CCG框架的强化。核心逻辑分三步。第一步初始化一个关键场景集合通常先用预测场景作为第一个元素再往里面添加几个明显极端的组合比如“光伏最小、负荷最大”。第二步求解主问题主问题只对关键场景集合里的场景建立二阶段约束得到一个相对松弛的调度方案。第三步求解子问题子问题会基于当前调度方案去找最恶劣的不确定场景如果找到的新场景带来的成本比当前上界还高就说明现有调度方案扛不住把新场景加入关键场景集合重新求解主问题。往复迭代上下界不断收紧直到收敛。关键场景辨别和原始CCG的区别主要体现在场景管理策略上。经典CCG每轮迭代只加入一个最恶劣场景主问题规模增长较慢而关键场景辨别会同时维护多个关键场景并对场景做去重和裁剪避免重复计算、控制主问题规模。我实际测试下来对于微网这种小规模系统两类做法收敛速度差别不大但关键场景辨别对子问题求解结果更稳定——因为它每次更新的不是一个孤立场景而是一组有代表性的场景组合降低了迭代震荡的概率。2.3 关键场景辨别与经典CCG的对比对比维度经典CCG关键场景辨别算法场景更新来源每轮只取子问题求出的最恶劣单场景从子问题中提取多个有代表性的极端场景主问题场景管理场景不断累加不做裁剪更新场景集去重并控制数量上限子问题求解要求对偶后解一个双线性问题取最恶劣点对偶后解双线性问题同时输出多个关键候选点迭代稳定性场景增量会导致UB/LB震荡通过多场景约束使上下界更平滑计算量主问题规模逐步增大场景数量受控主问题规模更可预期实际编码时这两种方案在Matlab上的实现框架几乎一致区别只在于场景更新和主问题整合方式。所以即使你只看过经典CCG的代码改成关键场景辨别也不困难。3. 微网两阶段鲁棒调度模型怎么搭3.1 系统结构与决策变量算例系统我建议这样搭一个典型的交流微网包含光伏、微型燃气轮机、储能电池以及一个与上级电网的联络线节点负荷按典型日曲线设置。这个结构能覆盖微网调度里最核心的问题——不确定的光伏、可调度的火电、有充电状态约束的储能、可以双向流动的联络线。模型里的变量分两组。第一阶段变量包括燃气轮机的启停状态、储能是否参与调度的状态——注意这里用的一阶段变量主要是整数变量它们必须在日前阶段定下来等到实际运行日无法轻易改变。第二阶段变量包括燃气轮机的实际出力、储能充放电功率、从上级电网购电和售电功率、以及弃光和切负荷量——这些可以在实时运行中根据实际情况调整。为什么要把整数变量放在第一阶段连续变量放在第二阶段因为机组启停是需要时间完成的物理动作储能电池的充电/放电状态切换也需要时间它们不可能像燃气轮机出力那样连续调节。这个划分是两阶段鲁棒模型成立的物理基础。3.2 不确定性集合怎么描述微网里的不确定性主要是光伏出力和负荷功率。假设光伏预测值为P_pv_hat实际值在 [P_pv_hat - ΔP_pv, P_pv_hat ΔP_pv] 之间波动同理负荷预测值为P_load_hat实际值在 [P_load_hat - ΔP_load, P_load_hat ΔP_load] 之间波动。如果只给上下界最极端场景就是所有不确定性源同时取最大偏差这会过于保守。实际工程中多个不确定性源同时达到极端值的概率很低所以要引入一个预算参数Gamma限制所有不确定性源的偏差加起来的最大总量。比如光伏向下偏差4个单位、负荷向上偏差2个单位总共偏差6个单位而Gamma5时这个组合就要被剪掉。这个参数直接控制模型的保守程度Gamma0等价于确定性调度Gamma取全体不确定性源能到达的最大偏差之和时模型退化为最保守的单阶段鲁棒。用式子写就是这样所有不确定性源的归一化偏差之和不能超过Gamma。这个约束在Matlab里实现非常容易只需要在子问题的不确定变量之间加一个求和不等式。实际调参时我会先取Gamma0跑一遍确定性调度再逐渐增大Gamma观察总运行成本的变化曲线看成本上升的斜率是否合理。3.3 目标函数与约束条件核心公式两阶段鲁棒微网调度的目标函数可以写成第一阶段成本机组启停成本、储能调度准备成本 第二阶段最恶劣场景下期望调整成本燃料成本、购电成本、弃光切负荷惩罚成本。这里的第二阶段成本外面有max-min两层max层是找最恶劣的不确定场景min层是该场景下最优调整策略的成本。由于是鲁棒优化我们关注的是最恶劣场景下的成本最小化而不是所有场景的期望成本。约束条件按时间断面展开核心约束包括功率平衡约束每一时刻光伏风电出力加燃气轮机出力加储能放电加购电等于负荷加储能充电加售电加弃光加切负荷。这条约束在确定性模型里是一条简单等式在两阶段模型里它要在每个关键场景下分别成立。储能SOC状态约束储能电量按时间递推上一时刻电量加充电减去放电再考虑充放电效率损耗。这里要特别注意每个关键场景下的SOC轨迹是独立的场景之间不能混用。燃气轮机爬坡约束两个相邻时段的出力变化有限制爬坡速率约束在最恶劣场景下也必须满足。联络线功率约束与上级电网交换功率不能超过物理限值。整数变量约束燃气轮机启停状态、储能状态开关等。约束条件里要重点提一下惩罚项。在实际调度中如果系统确实扛不住最恶劣场景允许切负荷但切负荷量要被严重惩罚否则优化算法会“偷懒”动不动就切负荷得到的调度方案没有任何实用价值。惩罚系数通常设为电价的5到10倍让切负荷成为最后手段。4. Matlab实现与CCG求解细节4.1 建模工具与求解器Yalmip Gurobi/CplexMatlab下做两阶段鲁棒优化最顺手的组合是Yalmip界面加Gurobi或Cplex求解器。Yalmip是一个建模语言工具包写约束、目标函数比直接调求解器API方便得多尤其适合反复迭代求解的算法框架——主问题每次只增加几个变量和约束用Yalmip的optmize接口不用手动管理矩阵索引。工具版本方面我测试过Matlab R2023b到R2025都正常新版本的2026b也可以跑没有兼容性问题。唯一要留意的是中文注释乱码问题这个问题在2023以后版本的小版本里出现过——默认编码是GBK如果你的.m文件里有中文注释在不同机器上打开容易乱码。解决办法是在Matlab偏好设置里把Editor的Encoding改成UTF-8或者统一在文件头部加一行%#encoding utf-8。这个坑看着小真遇上能卡半天。求解器安装完成后要在Matlab里设置路径。Cplex装好后在路径管理里添加.../cplex/matlab目录Gurobi也是类似操作。可以用yalmiptest命令验证Yalmip能否正常调用求解器。如果提示找不到求解器多数情况是路径没加对或者许可证没装好。4.2 CCG迭代流程整套算法的迭代框架我可以直接用伪代码写出来这个结构在我调试过的微网算例里都适用。初始化 LB -inf, UB inf 关键场景集合S {预测场景, 最极端场景} 迭代计数 k 0 循环 while (UB - LB) / abs(UB) 收敛阈值 且 k 最大迭代次数 1. 求解主问题MP 目标函数 min (一阶段成本 常数项) 约束包含所有关键场景s∈S对应的二阶段变量和约束 得到最优解一阶段决策x_k目标值obj_MP LB max(LB, obj_MP) 2. 固定一阶段决策x_k求解子问题SP 目标函数 max_{u∈U} min_y (二阶段成本) 得到最恶劣场景u_k*以及二阶段成本obj_SP UB min(UB, 一阶段成本 obj_SP) 3. 收敛判断 若 (UB - LB) / abs(UB) 阈值退出循环 否则把u_k*作为新场景加入场景集合Skk1需要强调的是主问题和子问题不是同一个优化问题。主问题里二阶段变量对应的是一个个具体场景的“反应变量”场景集合有多大主问题里就复制多少组二阶段变量而子问题里二阶段变量是“当前一阶段决策下最恶劣场景里的最优调整变量”。两者的变量定义和约束范围完全不同写代码时别混着用。4.3 子问题对偶与大M线性化子问题是整个算法里最难啃的骨头。它在给定一阶段变量x的条件下要先对内层min问题取对偶把min-max结构转成max形式再做线性化。内层min问题的目标函数和约束都是线性的对偶理论保证强对偶成立只要可行域非空有界。对偶之后原先在min层的目标函数变成了对偶层的线性表达式同时原约束的右边常数项里出现了不确定量。这时候问题就变成了外层max同时选择对偶变量和不确定变量目标函数里出现“不确定量 × 对偶变量”的乘积项。这个乘积项是非线性的也是子问题求解的难点。处理办法是用big-M法离散化。以光伏出力的不确定量为例把连续的不确定区间离散成若干个偏差档每个档位对应一个0-1变量两两组合后“不确定量 × 对偶变量”可以拆成多个“0-1变量 × 连续变量”的乘积再用大M法写成线性约束。代码层面用Yalmip实现时我会这样写核心片段% 以光伏不确定量u_pv为例 % u_pv偏差档位对应的0-1变量delta_pv_i偏差幅度dev_pv_i % 对偶变量lambda % 辅助变量pi u_pv * lambda通过big-M线性化 Constraints [Constraints, pi M * delta_pv_i lambda]; Constraints [Constraints, pi M * delta_pv_i - lambda M * (1 - delta_pv_i)];具体怎么写要看你的表达式形式但核心思路就是把乘积项拆成“逻辑条件 连续辅助变量”。大M的取值需要谨慎取得太小可能剪掉真正的最优解取得太大又会导致数值不稳定我习惯取对应物理量的1.5到2倍上限。光伏出力上限是500kW大M就取750到1000既给足余量又不至于让求解器为难。4.4 主问题更新与场景集维护主问题的编写相对简单。场景集合S初始有两个成员每轮迭代可能增加一个新场景。每增加一个场景主问题里就多复制出一组二阶段变量和相关约束。这里要特别提醒场景去重问题。CCG迭代早期容易出现一种现象第3轮加入的场景和第5轮加入的场景非常接近几乎重复。如果不去重主问题里会出现两条几乎一模一样的约束不仅浪费求解时间还可能导致迭代迟迟不收敛。我实现时会在加入新场景前计算新场景与已有场景的欧氏距离距离小于阈值就视为重复不再加入。场景数量控制也很重要。虽然微网算例通常迭代5到10轮就能收敛但为了防止极端情况下场景无限增长我一般会设置场景数量上限比如8到10个。达到上限后即使UB-LB还没达到收敛阈值也按当前上界作为最终结果输出。实际工程中这个误差通常在1%以内可以接受。% 主问题MP的Yalmip建模骨架 % 场景集合scenes是一个结构体数组每个元素包含该场景下的不确定量取值 x sdpvar(...); % 一阶段变量 for s 1:numel(scenes) y{s} sdpvar(...); % 第s个场景对应的二阶段变量 % 功率平衡约束、设备约束... end objective_mp first_stage_cost(x) sum_over_scenes_constant; % 注意主问题目标函数里的第二阶段成本实际是随迭代更新的下界估计常数主问题目标函数有个容易理解错的地方严格意义上主问题里第二阶段成本应该是对每个已知场景求最小成本之和但CCG实现时主问题目标只保留一阶段成本和必要的常数项二阶段约束只是作为可行性的“检查网”最终成本和上界是用子问题算出来的。所以你在代码里会看到主问题的目标函数比子问题简单得多这不是写错了而是CCG算法的特性。5. 典型案例算例设计与结果观察5.1 算例参数设置我给的算例参数可以作为一个标准模板。系统含400kW光伏、200kW微型燃气轮机、300kWh储能最大充放电功率100kW、与上级电网联络线限值为200kW典型日负荷基准峰值600kW谷值300kW。光伏预测偏差设为正负25%负荷预测偏差设为正负15%不确定预算Gamma先取4后续做敏感性分析可以逐档增加。参数数值光伏额定容量400 kW负荷基准峰值600 kW / 谷值300 kW储能容量300 kWh储能最大充放电功率100 kW微型燃气轮机容量200 kW燃气轮机爬坡速率50 kW / 15min联络线功率上限200 kW光伏预测偏差±25%负荷预测偏差±15%不确定预算Gamma4切负荷惩罚单价10倍电价这个规模在Matlab里求解非常快单次子问题求解时间基本在1到3秒完整CCG迭代5到6轮也就十几秒非常适合跑验证和调参。5.2 迭代收敛与调度结果分析用上述参数跑下来CCG迭代约5轮收敛。每轮LB和UB的变化很典型第1轮LB很低UB很高因为初始场景集合只有预测场景和一个极端场景主问题约束偏松第2、3轮加入新场景后LB快速上升UB缓慢下降第4轮往后两者差小于收敛阈值。把每轮的关键场景打印出来看前几轮新增的基本都是“低光伏高负荷”这类最恶劣组合后期新增场景则集中在边界组合上对决策影响很小。运行成本方面确定性调度方案的日运行成本大约2860元两阶段鲁棒方案的日运行成本在3320元左右成本上升约16%。这16%的代价换来的是可靠性在最恶劣场景测试中确定性方案需要切负荷45kWh两阶段鲁棒方案切负荷量为0系统完全扛住了极端情况。这个对比直观地说明了为什么鲁棒优化值得多花一点运行成本。关键场景分析还有一个很有意思的发现系统最忌惮的不是单纯的光伏低发而是“光伏低发同时负荷上升”这个组合储能的作用在这样的场景下格外突出——鲁棒方案会在日前阶段给储能多留一部分充电余量而不是像确定性方案那样把储能电量卡得很紧。这就是两阶段鲁棒对“运行弹性”的体现。6. 常见问题与排查技巧实录6.1 常见问题速查表问题现象可能原因解决办法Yalmip报错“No suitable solver”求解器路径没加或Yalmip版本过旧重装Yalmip用yalmiptest检查子问题对偶结果明显错误内层min问题存在冗余整数变量确认子问题内层是纯LP无整数变量big-M导致优化结果抖动大M取值过大或过小改为物理上限的1.5~2倍测试不同量级LB与UB震荡不收敛新场景与已有场景高度重复加入场景去重增加场景相似度判断主问题规模爆炸、求解越来越慢场景数量不受控设场景数量上限或裁剪冗余场景中文注释乱码Matlab编码默认GBK改成UTF-8或在文件头写%#encoding utf-8维度不匹配报错Yalmip变量形状不一致用size()逐一检查变量维度这里面的“LB与UB震荡不收敛”是我遇到最多也最头疼的问题。后来我发现绝大多数情况不是算法错了而是场景去重没做好上一轮加进来的最恶劣场景和这一轮的新场景在数学上几乎重合导致主问题约束重复、迭代循环一直在原地打转。做了去重以后收敛速度明显改善。6.2 排查思路与调试经验我调试这类代码的过程基本遵循“先小后大先静后动”的套路。第一步先把确定性调度模型跑通保证功率平衡、储能SOC、爬坡这些核心约束在可行性上完全没有问题。这一步如果都跑不通两阶段模型根本不用想。第二步单独测试子问题固定一阶段变量给定一个已知的最恶劣场景看子问题返回的二阶段成本对不对——这个结果可以用手算或者用另一个简化模型交叉验证。第三步再把主问题和子问题套进CCG循环逐轮打印LB、UB、新场景值观察收敛趋势。测试时先用小规模算例两台机组、1个调度时段把整个逻辑验证一遍。小算例的优势是你能手动算出正确结果代码问题会立刻暴露。小算例跑通后再扩到24小时、多设备、完整场景效率高得多。不确定预算Gamma的敏感性分析也是个调参利器。从Gamma0开始逐渐增大运行成本应当单调上升。如果出现成本曲线不规则波动说明代码里有隐藏bug优先检查子问题中不确定量的方向约束是否写反了。我实际调试这套东西最大的体会是两阶段鲁棒优化的难点不在公式推导而在把公式转成代码时每一步的对应关系。主问题、子问题、对偶变量、场景更新任何一个环节错位整个迭代就崩了。建议动手之前先在纸上把主问题和子问题的变量清单列出来写上哪些变量属于哪个问题、在哪个环节被固定再动键盘。最后再分享一个小经验不要一上来就想着把弃光、切负荷、储能寿命衰减、多时间尺度耦合全塞进模型那会让调试难度翻好几倍。先把两阶段鲁棒的核心骨架搭起来跑通验证逻辑正确再逐步加细节。骨架对了血和肉可以慢慢填。