两阶段鲁棒优化微网容量配置:模型、CCG算法与Matlab实现
搞微网多电源容量配置最常被问的一句话是你这套容量面对实际运行里各种“不按剧本”的光照、风速和负荷到底扛不扛得住我最近把两阶段鲁棒优化算法完整落到了Matlab代码里从建立两阶段模型、设计不确定集、写列与约束生成算法到最后出容量配置结果整个过程踩了不少坑。这篇就把背后的建模思路、求解机制和可复现的代码组织方法一起写出来。准确说这篇适合三类人。一是正在做微网规划、分布式电源和储能容量配比的研究者二是需要在并网方案里回答“最坏情况也能扛”的工程人员三是被两阶段鲁棒优化理论绕晕想找一篇能照着调Matlab代码资料的人。我不打算把问题拖到纯数学层面而是按做项目的顺序讲先说为什么要两层再说怎么建模型然后讲CCG求解最后给算例结果和调试经验。1. 容量配置为什么天然是“两层决策”1.1 投资搭台运行唱戏微网多电源容量配置表面上是一道“买多大设备”的问题光伏装多少千瓦风电装多少储能电池配多少功率和多少电量柴油机要不要留。但在容量确定之后设备怎么用才是关键。光伏、风电出力只跟天气走储能何时充电何时放电要跟着负荷走柴油机在什么电价和缺电条件下启动外网购电和售电怎么分配这些都是运行层面的问题。所以规划问题的完整形式必定是投资决策带着运行优化一起做的。用大白话理解第一层是“搭台子”的人先把舞台大小定了第二层是“唱戏”的人每天根据实际上台的“演员”安排节目。舞台搭小了厉害的节目排不下舞台搭大了空着也是一种浪费。两阶段鲁棒优化就是让这两层决策按照“先拍板、后应对”的顺序放在同一个目标函数里权衡而不是像传统确定性方法那样把运行过程简化成一条平均曲线。1.2 确定性模型低估了什么很多初始方案会先做确定性优化把光伏、风电出力曲线和负荷曲线取一个典型日或者全年平均然后在投资约束下最小化投资加运行成本一次性求解得到容量配置。这个做法不是没用但会系统性低估风险。原因在于容量配置的价值往往不是在“平均场景”里体现的而是在“不利场景”里体现的。比如储能容量如果只按平均日照和平均负荷来配遇到连续阴雨天叠加晚高峰可能连续几个小时都切负荷。这时候再想补容量已经晚了因为设备容量是第一阶段拍板的。这个场景听着极端但规划恰恰要回答这种极端问题。确定性优化把不确定性参数固定成一个期望值把所有“意外”当作不会发生结果自然是投资偏低、运行成本在真实环境中偏高。更好的思路是把不确定性描述成一个范围然后让优化模型自己去找这个范围内对方案最不利的场景并围绕这个场景来配置设备。1.3 两阶段的结构刚好对上“容量-运行”耦合把两阶段决策和鲁棒优化放在一起逻辑就顺了。第一阶段变量是Here-and-Now决策需要在参数实现之前就确定下来典型的是各类电源和储能的配置容量第二阶段变量是Wait-and-See决策可以在看到实际光伏、风电、负荷之后再做典型的是各时段的机组出力和储能充放电功率。目标函数写成先最小化投资成本和“最坏运行成本”之和数学上就是min_x c^T x max_u min_{y in F(x,u)} d^T yx 代表容量设备u 代表不确定参数y 代表运行变量F(x,u) 是给定容量和参数后的运行可行域。这个式子读出来是先决定容量然后在所有可能的不确定场景里找出让运行成本最高的那个场景再按最低运行成本去应对它两者的总和一起被优化。读到这里你会发现这本质上是一个三层嵌套优化直接解几乎不可能。后面的列与约束生成算法核心工作就是把它拆成主问题和子问题像剥洋葱一样一层层剥开。2. 两阶段鲁棒模型先定不确定集再谈优化2.1 不确定参数从哪里来微网里的不确定性至少有三类。第一类是新能源出力光伏受云层、辐照影响风电受风速随机性影响第二类是负荷居民和工商业负荷在一天内有很强的波动预测误差不可忽略第三类是市场电价如果允许与主网双向购售电电价波动也会进入运行模型。工程上最常用的做法是先把这三种参数分别给出预测基准值再在基准值上下定义一个波动范围代表预测误差和随机波动。这里需要明确一个概念两阶段鲁棒优化并不试图预测最坏场景出现在哪个具体时刻而是把“可能出现的偏离范围”交给模型。这样得到的容量配置对任何落在不确定集内的场景都有一定的适应能力而不是只对某一条预测曲线所有。2.2 三种常见不确定集对比不确定集的形式决定了问题的难度和方案保守度。我把三种常用形式放在一起对比不确定集类型形式优点缺点盒式不确定集每个参数独立在上下界内任意取值建模简单约束直观全取边界的极端场景几乎不可能发生结果偏保守预算式不确定集每个参数可以在上下界内偏离但总偏离量受预算限制能控制保守度反映“坏情况不会同时全来”需要调预算参数椭球式不确定集参数偏离量被控制在椭球范围内可保留统计意义保守度连续可调模型变成二阶锥问题求解难度明显上升我在算例里用的是预算式不确定集原因很实际微网的负荷和新能源出力扰动来源相互独立但不可能百分之百同时偏离到边界。预算一定程度上控制了“天时不利”的强度又保持模型是线性规划可以直接用成熟求解器跑起来快调参也方便。2.3 预算式不确定集的数学表达预算式不确定集的标准写法是对于每个时段 t不确定参数 u_t 在基准值上下一定比例内波动例如 u_t u_ref,t * (1 deviation_t * z_t)z_t 在 [-1,1] 之间。同时整个调度周期内所有时段的 |z_t| 之和不超过预算 Gamma。Gamma 越小越接近确定性预测Gamma 越大越接近盒式也就是每个时段都允许取最不利边界。实际设置时Gamma 的物理含义可以理解为整个调度周期内最多允许多少个时段的不利偏差同时达到最大。比如一天24个时段Gamma 取6意思是最多6个时段可以同时处在最坏偏离状态其余时段要么偏离不大要么方向有利。这个参数在Matlab里通常就是一维向量或者一个常数求解时不需要特殊处理它只是把 z_t 的L1约束加入不确定变量的可行域。调Gamma比换不确定集类型方便得多所以我的项目默认都是从预算式开始试。2.4 两阶段模型的标准结构两阶段模型分三层写。第一层是投资层目标函数包含单位投资成本和设备容量变量第二层是运行层目标函数包含燃料成本、购电成本、运维成本和切负荷惩罚第三层是各层约束。投资约束通常只有设备容量上限和非负约束负责把可行域框在一个实际范围内。运行约束则要写全功率平衡、储能SOC递推、储能充放电功率上下限、柴油机出力限幅、外网交互功率限幅、备用约束、以及每个时段的功率非负约束。运行层的所有约束都需要包含不确定参数并且这些约束都会被引入到最坏场景的子问题中。这就是这个模型和普通确定性规划最大的差异约束不是针对某一条曲线而是针对一个集合。3. CCG算法主流程与Matlab代码骨架3.1 为什么CCG而不是Benders两阶段鲁棒规划最经典的做法是Benders分解和列与约束生成。Benders的思路是把子问题的最优性信息用割平面反馈到主问题每次迭代只添加一条割平面CCG则更进一步把最恶劣场景下整个运行变量集合都加进主问题形成完整的场景约束而不是只传达一条最优性信息。这样主问题对原问题的逼近速度更快需要的迭代次数也更少在处理包含大量运行约束的微网模型时效果明显更好。我最早是用Benders试的二十几轮迭代还没收敛换成CCG之后七八轮就基本收敛。而且主问题的规模是可控增加的不会像Benders那样始终在“信息不足”和“信息碎片化”之间摇摆。当然CCG不是没有代价每迭代一次主问题就会多一组完整的运行变量和约束模型规模按场景数递增。实际算例中这种代价很小完全值得。3.2 主问题怎么建立给定当前已生成的一系列最恶劣场景集合主问题中第一阶段容量变量 x 仍然待优化同时引入一个标量 theta 来近似第二阶段的最坏运行成本。针对每个已发现的场景 k需要添加一组完整的运行变量 y_k 和运行约束。主问题的写法分三部分。第一部分是 x 和 theta 共同参与目标函数最小化第二部分是对每个场景 k运行变量 y_k 必须满足给定 x 和场景参数 u_k 下的功率平衡、储能SOC等约束第三部分是目标函数额外添加 theta d^T y_k让 theta 至少不低于每一个已发现场景的运行成本。这套写法在Matlab里非常好实施因为本质上就是按场景索引把同一组约束复制多次。最容易出错的地方是索引对应关系后面我会专门讲。3.3 子问题与最恶劣场景识别子问题是在第一阶段方案 x 给定的情况下求解 max_u min_y d^T y也就是找出让运行成本最大的不确定场景并给出对应的最低运行成本。这是一个最大值套最小值的双层优化不能直接交给线性规划求解器。常规处理方法是把内层 min_y 写成一个线性规划然后取它的对偶问题。由于内层变量 y 通常是连续变量线性规划强对偶成立min_y 的目标函数值等于其对偶目标的最大值。于是内层的最小化就变成了对偶变量的最大化与外层对不确定变量 u 的最大化合并最终形成一个只包含 u 和对偶变量的单层最大化问题。这个单层问题仍然可以被线性规划或混合整数线性规划求解器处理。求解完这个单层问题后能拿到两样东西一是当前 x 下的最恶劣场景 u*二是这个场景下的运行成本也就是给当前方案兜底的最坏运行成本。u* 会被作为新场景加入主问题运行成本则用来更新上界。3.4 迭代流程、上下界更新与终止条件CCG的迭代框架并不复杂具体步骤如下初始化给出一个可行容量方案 x0设置上界为正无穷、下界为负无穷将初始场景通常是预测名义场景加入主问题求解主问题得到 x* 和 theta*下界更新为当前主问题目标值固定 x*求解子问题得到最恶劣场景 u* 和运行成本上界更新为当前主问题中的投资成本加子问题运行成本如果上界减下界小于设定的误差阈值停止否则把 u* 对应的完整运行变量和约束加入主问题回到第2步。终止条件的设置平时很关键。工程上我会用相对误差而不是绝对误差因为成本和容量量级可能上万绝对误差取1往往不现实。Matlab实现时可以在每次迭代后打印当前上下界观察差值变化趋势。如果在十几轮迭代中持续震荡先不要纠结阈值应该回去检查子问题有没有漏约束。3.5 Matlab代码骨架我用结构化方式组织代码主体是主函数、主问题模型、子问题模型三个部分。核心骨架大致如下%% 参数读入与初始化 load(case_data.mat); % 设备参数、基准曲线、不确定集参数 x0 zeros(n_equip,1) init_cap; % 初始容量 nScen 0; LB -1e10; UB 1e10; eps_gap 1e-3; while (UB - LB)/abs(UB) eps_gap % 1. 构建并求解主问题MP [x_now, theta_now, MP_obj] solve_MP(x0, ScenSet); LB MP_obj; % 主问题目标值 % 2. 固定x_now求解子问题SP得到最恶劣场景u_star和运行成本 [u_star, run_cost, SP_status] solve_SP(x_now); if SP_status 1 UB inv_cost(x_now) run_cost; end % 3. 将u_star添加到场景集合供下一轮主问题使用 ScenSet [ScenSet, u_star]; nScen nScen 1; end每个函数内部我都按“先定义变量集再写约束最后设目标”的顺序组织。这个顺序能省去很多调试时候“这行约束到底加到哪去了”的困惑。真正落到项目里主问题函数里会循环所有场景把每个场景的运行约束追加进去子问题函数里则要把内层对偶后的变量和约束写完整。4. 算例某园区微网多电源配置全过程4.1 算例参数我用一个虚构的园区微网算例跑通了这套代码。设备包括光伏、风电、储能电池和柴油发电机负荷曲线取24小时的预测基准值时间分辨率1小时。主要参数如下设备类型参数数值光伏单位投资4500元/kW光伏运维成本0.02元/kWh风电单位投资6800元/kW风电运维成本0.03元/kWh储能单位功率投资1200元/kW储能单位容量投资1800元/kWh储能充电/放电效率0.95/0.95储能SOC上/下限0.9/0.1柴油机单位投资2000元/kW柴油机燃料成本0.68元/kWh外网购电上限2000kW外网售电上限500kW外网购电价/售电价0.85/0.45元/kWh负荷切负荷惩罚10元/kWh容量配置的决策变量包括光伏容量、风电容量、储能功率和储能容量、柴油机容量。投资成本按日折算把设备寿命和年利率折算到典型日成本这样投资成本和运行成本可以放在同一个目标函数里。折算系数虽然不影响算法结构但直接影响两类成本的比例项目上要按实际的设备寿命和资金成本算不能随便拍脑袋。4.2 不确定集参数设置光伏和风电的预测基准值是我给定的一条曲线实际使用的偏离范围取了基准值的正负20%负荷偏差取了正负10%。预算Gamma取不同的值分别做敏感性测试从Gamma2到Gamma12默认Gamma6。z_t 允许在-1到1之间取连续值。因为变量连续最恶劣场景不一定出现在所有时段同时取边界而是一个在预算限制下的“比较坏又没那么坏”的组合。有一点容易被忽略光伏、风电、负荷的不确定变量应该分开建模因为它们有不同的预测误差特性和物理含义。把它们混在一个系数矩阵里虽然在代码上省事但会导致预算无法正确分配算出的最恶劣场景常常失真。4.3 运行约束的矩阵化拼装Matlab里跑优化最大的工作量不是写逻辑而是拼矩阵。我会把运行约束统一写成 A_g * y A_u * u A_x * x b 这种形式其中 y 是第二阶段所有运行变量u 是各时段不确定变量x 是第一阶段容量变量。这样主问题和子问题都能直接复用同一套系数矩阵只是变量自由度不同。储能SOC递推约束必须按时段顺序连续堆叠否则求解器会解出一个“能量凭空转移”的方案。功率平衡约束里光伏、风电、柴油机出力、储能放电、购电作为电源项负荷、储能充电、售电作为负荷项两边要严格对齐。每条约束我都在代码里留了注释标明它属于哪个时段、哪个设备这在实际调试时太重要了。这个算例最折磨人的地方不是模型逻辑而是检查每个时段的功率平衡是否真的成立。我会额外写一段校验脚本把一个已知场景代进去验证每个时段电源项之和与负荷项之和的差值是不是小于一个极小值。校验脚本通过后再交给求解器能省掉大量后续排错时间。4.4 求解与结果在这个算例参数基础上确定性优化配出的结果是光伏容量偏大储能偏小柴油机几乎没有。看着总成本很低但把最恶劣场景硬塞进去做运行模拟切负荷量非常可观。两阶段鲁棒优化的结果则是另一套容量储能功率和能量明显上调柴油机也保留了一部分光伏风电的装机相对调整幅度小一些。求解时间方面默认Gamma6情况下CCG迭代约8轮收敛每轮主问题规模增长一组运行变量总求解时间几十秒完全在可接受范围内。这说明预算式不确定集加线性规划的处理方式在微网规划尺度上计算效率是够用的。如果换成更细的时间分辨率或加入0-1变量求解时间可能会上升一到两个数量级那时才需要考虑场景压缩或子问题加速。5. 结果解读与鲁棒性验证5.1 鲁棒方案与确定性方案的差别下面给出确定性方案和鲁棒方案的容量配置对比数据来自我跑的虚构算例指标确定性方案鲁棒方案光伏容量3200kW3000kW风电容量1500kW1500kW储能功率800kW1500kW储能容量1600kWh3500kWh柴油机容量200kW1000kW投资成本10.2万元/日12.8万元/日最坏场景运行成本8.5万元/日2.1万元/日最坏场景切负荷量4200kWh0kWh鲁棒方案比确定性方案多花了两成多的投资但换来的是最坏场景下运行成本的大幅下降而且完全消除了切负荷。这正好印证了前面的判断容量配置的多余度不是浪费而是对抗不确定性的资产。运行成本的下降额完全覆盖投资成本的上升额说明鲁棒配置在综合成本上是占优的。5.2 预算Gamma如何左右配置Gamma是两阶段鲁棒模型里最值得调的旋钮。我把Gamma从2调到12做了一组测试结果明显分成三个区间。Gamma较小时配置接近确定性方案Gamma中等时储能和柴油机容量开始上升Gamma很大时变化趋缓因为模型已经接近盒式不确定集再增加预算只会让约束变松不会继续改变最恶劣场景。实际操作中我建议项目上不要只跑一个Gamma而是画一条“总成本-Gamma”曲线。曲线上拐点通常是方案比较经济的位置因为再提高Gamma容量配置增加不大但投资不再划算。这里面没有统一的最优Gamma要结合项目对可靠性的要求来选。如果项目对停电非常敏感宁可让Gamma偏大一些如果对投资成本敏感可以选在拐点左侧。5.3 怎么把代码改到自己的项目如果有人想把这套代码接到自己的微网项目里需要修改的地方主要集中在参数区、不确定集构造和运行约束三个位置。设备参数按实际报价和运行数据替换不确定集偏差比例按历史预测误差统计设置运行约束则要补充自己的设备特性比如储能循环寿命约束、柴油机启停时间约束。整体框架不用动CCG主流程对所有线性或混合整数线性两阶段模型都适用。另外要注意如果引入了柴油机启动变量这类0-1变量第二阶段就从LP变成了MILP子问题的对偶变换方式需要换成强对偶或KKT系统求解复杂度会明显上升。无特殊需要时我建议先把设备建模成连续变量确认整体流程没问题再逐步加整数变量否则排查问题会很痛苦。6. 我踩过的坑和调试建议6.1 维度不匹配是Matlab优化最常见的雷两阶段鲁棒模型里到处都是按场景、按时段、按设备索引的变量维度不匹配几乎无法避免。我遇到过一种情况某个约束里引用了时段索引但矩阵行数只按设备维度构造求解器直接报维度不一致。排查办法其实不复杂第一次写代码时就在每个约束矩阵拼完后打印size给每个约束块命名比如Cons_soc_bal、Cons_power_bal一旦报错就能定位到具体哪块。另一个常见问题是主问题中新增场景时新场景的运行变量和旧场景的运行变量混在一起导致约束只描述了其中一个场景。为了避免这种问题我会给每个场景的变量加独立前缀例如y_k1、y_k2这样在构建主问题时能确保“一个场景一套约束”不会互相覆盖。6.2 大M取值要卡住设备物理上限储能在同一时段通常不允许同时充电和放电但很多模型不会显式加这个约束只是靠目标函数自然避免。如果必须显式约束就绕不开大M法。大M取太小会限制可行域比如电池满电时强制不能充电取太大会导致数值病态求解器容易找错方向。我一般把M设为对应功率变量的物理上限乘1.2再在求解后检查解里有没有违反原来物理意义的情况。更稳妥的做法是尽量不加这种互斥约束而是让充电功率和放电功率使用同一组上限约束配合SOC递推大多数场景下解会自动符合物理规律。这样省掉一批大M也少了一类数值坑。6.3 子问题无界或不可行先检查SOC和惩罚项子问题求解不收敛或报不可行时我通常优先查两处。第一处是SOC的上下界和初始电量有没有给全SOC约束缺失会造成储能可以无限放电子问题目标无界。第二处是切负荷惩罚项有没有作为松弛变量加进功率平衡约束没有它模型在最坏场景下可能没有任何可行运行方案。给切负荷松弛一个高惩罚单价等于告诉求解器“切负荷可以做但代价很高”。这个设置不能省略否则模型会为了可行性而偷偷放宽某些物理约束最恶劣场景的结果就不可信。我习惯把切负荷惩罚设成正常运行成本的十倍以上确保求解器只在万不得已时才选择切负荷。6.4 相对误差收敛和迭代震荡CCG理论上有限迭代收敛实际代码却可能在一轮轮迭代中反复震荡。我的经验是把终止条件设为相对误差并且在下界更新时用主问题的完整目标值不要只用投资成本部分。如果震荡严重先调小Gamma试试Gamma大的时候最恶劣场景变化更剧烈主问题加入新约束后产生较大波动是正常现象。迭代初期也不需要过于追求精确前几轮主问题求解出的x变化很大这时没必要反复把子问题精度调得很高浪费计算时间。一般迭代到第5轮后x的变动会明显放缓再提高求解精度比较划算。小算例可能无所谓但规模一上去初期精度设置能差出好几倍求解时间。最后再分享一个处理这类项目的体会两阶段鲁棒优化并不是越保守越好它真正的价值在于让你用一套可调试、可复现的流程把“最坏情况”从模糊的担心变成明确的场景和明确的成本。把Gamma旋钮拿在手里把CCG循环跑通你才能在容量配置上做到既不盲目堆设备又对不确定性有底。实际动手时先从48时段甚至24时段的简化算例开始确认主问题和子问题闭环了再往完整模型上扩展这个顺序能帮你避开绝大多数看不懂的报错。