两阶段鲁棒优化微网容量配置:CCG算法与Matlab实战
从微网容量配置的实际痛点切入两阶段鲁棒优化是处理风光不确定性的关键手段。本文以一个実際に运行的Matlab代码项目为例拆解从数学建模到代码落地的完整链路重点讲清楚min-max-min结构怎么理解、CCG算法怎么迭代、以及我在调试过程中踩过的那些坑。1. 项目在解决什么实际问题1.1 微网容量配置的决策困境微网里的光伏、风电、储能、柴油发电机每种电源都有完全不同的成本结构和运行特性。光伏和风电的度电成本低但出力不可控阴天没光、无风天没风出力可能直接掉到零储能能削峰填谷但单位容量的购置成本高而且充放电循环有寿命损耗柴油发电机响应快、出力稳定但燃料成本和碳排放都不好看。容量配置要做的事情就是在这些特性互相矛盾的电源之间找到一个最优的组合方案既要满足全年任意时段的负荷需求又要把总成本压到最低。这里的成本不仅包括设备购置的初始投资还要算上整个寿命周期内的运行成本、维护成本、燃料成本甚至要折算设备的置换成本。这个问题难在哪难在决策变量有两种完全不同的时间尺度。容量配置是“建多少个风机、装多少块光伏板、配多大容量储能”这种一锤子买卖的决策属于长期规划而每个小时怎么调度这些电源、储能是充电还是放电、柴油机开几台属于短期运行决策。规划决策确定了系统的天花板运行决策决定实际表现两者耦合在一起形成一个典型的双层优化问题。1.2 不确定性才是真正的挑战如果光伏出力、风电出力、负荷大小都是确定性的数值这个容量配置问题用常规的混合整数线性规划就能解。但现实世界里这些量全是随机变量——明天的光照强度取决于云层厚度一周后的风速谁也说不准负荷曲线更是随季节、天气、人们的生产生活方式剧烈波动。传统的确定性优化方案会在这种不确定性面前暴露出脆弱性按夏季典型日照设计的系统到了连续阴雨的一月份可能频繁切负荷按平均风速配置的风机台数遇到小风年会直接导致供电不足。反过来如果为了保险起见把所有不确定量都取最恶劣值配置出来的系统成本会高到无法接受——就好比为了应付百年一遇的洪水把房子的地基修到大坝的标准。面对这种矛盾业界实际上有随机规划、鲁棒优化、区间优化等多种路线。随机规划需要知道不确定量的精确概率分布但在工程实践中很难拿到可靠的分布数据传统鲁棒优化虽然不需要分布信息但要求所有不确定量同时取最坏情况保守性过强。两阶段鲁棒优化恰恰在两者之间找到了平衡点第一阶段做规划决策时不依赖不确定量的实现值第二阶段在不确定量显露后再做最经济的调度调整用min-max-min结构同时兼顾了鲁棒性和经济性。2. 两阶段鲁棒优化的数学框架2.1 min-max-min结构的直观理解这个项目里用的两阶段鲁棒优化核心是一个三层嵌套的数学结构min-max-min。乍一看很唬人但拆开来看并不难。最外层的min对应第一阶段的容量规划决策也就是“我现在要买多少设备”。这一层决策的特点是“现在定了就不能反悔”专业术语叫here-and-now决策。中间那层max对应不确定性“跟我对着干”——真实的风速、光照、负荷会朝着让系统运行成本最高的方向去实现。最内层的min对应第二阶段的运行调度决策也就是“不管天气怎么变我都要在给定设备条件下找出最省钱的调度方案”这层决策的特点是“可以等不确定量实现后再做”术语叫wait-and-see决策。三层结构的意思翻译成大白话就是我要在设备投资和后期运行之间找一个平衡点使得——即使自然界以最不利的方式呈现风速、光照和负荷我依然能通过最优调度把损失控制在最小。这是一个“先定容量再博弈运行”的思路数学上严整工程上也有明确的现实对应。2.2 CCG算法怎么把这个问题解出来min-max-min三层嵌套问题没法直接扔给求解器硬解工程上一般用Benders分解或者列约束生成算法CCG来处理。这个项目采用的是CCG它比经典Benders分解收敛速度更快尤其在两阶段鲁棒问题里表现稳定。CCG的思路很朴素把原问题分解成主问题和子问题通过迭代逼近最优解。主问题是一个混合整数线性规划包含第一阶段的容量决策变量和第二阶段的运行决策变量只不过第二阶段变量对应的约束是随着迭代逐渐增加的。初始时主问题只包含一部分约束求解得到一个容量配置方案作为当前最优解的上界。子问题是在固定主问题给出的容量方案后求解内层max-min结构的优化问题。因为内层是max和min嵌套需要先用对偶变换把内层min变成max把max-min变成max-max合并成一个单层的max问题这样就能直接求解。子问题的目标函数值给了原问题最优解的下界。每一次迭代把子问题找到的最恶劣不确定场景提取出来生成一组新的第二阶段变量和对应的约束加入主问题重新求解。这样主问题的约束越来越多上界和下界不断逼近当两者的间隙小于设定阈值时迭代收敛得到的就是满足鲁棒性要求的最优容量配置方案。这里有个细节值得注意CCG可以在主问题中加入第二阶段变量的全部约束而不只是加了最优割这使得每次迭代获得的下界质量远好于经典Benders分解实际迭代次数通常只有个位数到十几轮。2.3 不确定集合怎么构造不确定集合决定了鲁棒优化模型的保守程度也直接决定计算复杂度。这个项目用的是盒式不确定集合加预算约束的构造方式这也是目前工程应用最广的选择。对每个不确定参数比如风速、光照强度、负荷功率给定一个预测基准值和上下波动范围用区间刻画可能出现的范围。但所有不确定参数同时取极端值的情况在现实中几乎不可能发生所以还需要引入一个预算约束限制所有不确定参数偏离基准值的总量。这里面的关键参数是预算值Γ。Γ0时不确定集合退化为基准场景模型退化成确定性优化保守性最低Γ取到最大时集合退化为完整盒式所有参数都允许取极端值保守性最高。实际工程中通常取Γ为某个中间值比如总时段数的某个百分比来平衡鲁棒性和经济性。我在实测中试过不同预算值对结果的影响发现一个比较明显的分水岭预算值从0增加到一定程度时最优容量配置中的光伏和风电装机会明显增加储能容量也跟着上升因为系统需要用更多的可控手段来对冲更恶劣的不确定场景而达到某个阈值后继续增大预算值对配置结果的影响就比较有限了因为此时约束已经足够保守新增的极端场景不再是制约配置方案的瓶颈。3. 微网多电源容量配置的数学建模3.1 决策变量与目标函数的确定这个项目考虑的是典型孤岛微网包含四类电源光伏、风电、储能、柴油发电机。为了简化建模把一年按典型日进行聚类处理每个典型日分为24个时段用几个典型场景代表全年运行特性。第一阶段的决策变量是各电源的配置容量光伏装机容量、风电装机容量、储能额定容量、储能额定功率、柴油发电机台数。这些变量都是离散或连续的整数/实数变量直接决定初始投资成本。第二阶段的决策变量是每个典型日在每个时段的运行变量柴油发电机的出力、光伏实际出力、风电实际出力、储能充放电功率、储能荷电状态、切负荷量等。目标函数是总成本最小化包括两大部分第一阶段的年化投资成本把设备购置成本按寿命年限和折现率等额分摊到每年第二阶段的年运行成本包含柴油燃料成本、运维成本、切负荷惩罚成本。目标函数把这两部分统一折算成等年值保证不同寿命的设备之间可以公平比较。一个容易踩坑的地方是设备寿命的折算。光伏寿命通常25年风电20年储能可能只有10年柴油发电机15年。如果简单地用“初始投资除以寿命年限”来年化忽略了资金的时间价值配置结果会偏向寿命长的设备。正确做法是用等额年值公式把折现率考虑进去。我在项目中取了8%的折现率这个值在电力系统经济分析里是常用假设。3.2 约束条件的层次结构模型的约束条件数量庞大需要分层理清楚。第一层是容量配置约束。光伏和风电的实际出力不能超过装机容量储能充放电功率不能超过额定功率储能荷电状态约束保证能量守恒。这些约束把第一阶段的投资决策和第二阶段的运行决策耦合在一起是整个模型的核心纽带。第二层是功率平衡约束。任意时段内所有电源的出力加上储能放电、减去储能充电必须等于负荷功率如果不平衡允许切负荷但要付出高额惩罚成本。这个约束在微网里是硬约束直接对应现实中“电灯不能灭”的基本要求。第三层是设备运行约束。柴油发电机有最小技术出力限制、爬坡速率限制运行台数有上限储能有充放电效率、荷电状态上下限、充放电功率上下限。这些约束反映了设备真实的物理特性忽略任何一个都会导致配置方案在现实中不可行。第四层是不确定变量约束。风速、光照、负荷在不确定集合范围内变化对应的光伏出力、风电出力、切负荷量都会跟着调整。这一层直接体现鲁棒优化的特征也是模型计算量的大头。我实测的一个体会是储能约束的建模方式对求解时间影响很大。如果把储能荷电状态在每个时段都写成等式约束再套上鲁棒优化的对偶变换整个模型的变量数量和约束数量会急剧膨胀。后来我把储能荷电状态的递推关系做了线性化处理并且约束只在必要的时段强制满足上下限求解时间缩短了将近一半。3.3 不确定变量与运行模拟的衔接风速到风电出力、光照到光伏出力中间还要经过转换模型。项目里采用分段线性化的方式把非线性功率转换曲线近似为分段线性函数这样既能保证精度又不会把模型变成非线性规划导致求解困难。负荷不确定性相对简单直接用预测负荷乘以一个波动系数。但这里有个容易忽略的细节负荷的不确定性和风光出力的不确定性耦合在一起时预算约束的分配需要小心处理。如果负荷、风速、光照分别设置独立的预算约束理论上也可以但会明显增加对偶变量的数量。我在建模时把三者合并到同一个预算约束里用权重系数区分各参数的波动贡献这样既控制了保守度又兼顾了计算效率。光伏出力和风电出力在处理上还需要区分“最大可用出力”和“实际调度出力”。不确定场景决定的是光伏和风电最大能发多少电但实际调度中出于安全或经济考虑可能弃光弃风实际出力小于最大可用出力。这两个变量在模型里必须分开定义否则会把不确定性错误地传递到调度决策里。4. Matlab代码实现与实操过程4.1 代码整体结构与数据预处理这个项目的代码用Matlab编写建模部分用了Yalmip工具箱求解调用商用求解器。整体结构分四层数据预处理模块、主问题构建模块、子问题构建模块、CCG迭代主程序。数据预处理模块承担的工作包括读取风速、光照、负荷的历史数据按典型日聚类生成场景计算各时段的不确定区间初始化预算参数。这部分代码虽然逻辑简单但耗时不少尤其是典型日聚类环节。项目里用K-means聚类把全年8760小时压缩成几个典型日聚类数目的选取需要做灵敏度分析——太少会丢失季节性特征太多会导致计算量成倍增加我在项目中取4个典型日分别代表春夏秋冬。主问题模块构建第一阶段的容量配置约束以及已经迭代产生的第二阶段场景对应约束。每个新场景加入时需要新增一整块连续变量的定义包括该场景下所有时段的调度变量。随着迭代进行主问题规模会逐渐膨胀这是CCG算法的正常现象。子问题模块相对复杂需要完成对偶变换。首先把内层min的约束写成拉格朗日形式然后通过强对偶条件把min-max变成max-max得到一个单层的maximization问题。这里最需要注意的是第二阶段的连续变量和0-1变量处理方式不同如果第二阶段存在整数变量对偶变换就会遇到困难需要做特殊处理。我在建模时特意避免了第二阶段引入整数调度变量采用连续变量近似柴油机的启停行为这样才保证了对偶变换可以顺利进行。4.2 主问题与子问题的核心代码骨架主问题代码的框架大致如下% 主问题构建 MP []; % 约束集合 obj_MP 0; % 第一阶段变量 x_pv binvar(1, 1); % 光伏配置决策 % 实际项目中通常用整数变量表示装机容量档位 % 或用连续变量并配合投资成本系数 % 第一阶段约束 MP [MP, x_pv 0]; % ... 其他配置变量和约束 % 迭代过程中加入的新场景对应的第二阶段变量和约束 for k 1:iter_count % 每个场景定义自己的第二阶段变量 P_dg{k} sdpvar(n_time, 1); P_bat_ch{k} sdpvar(n_time, 1); % ... 其他变量 % 加入功率平衡、设备运行约束 MP [MP, ...]; end % 求解主问题 optimize(MP, obj_MP, sdpsettings(solver, gurobi));这里有几个实际编码的细节值得多说几句。一是Yalmip中变量定义要区分连续变量sdpvar和整数变量binvar/intvar两阶段问题中第一阶段的整数变量会出现主问题里这是CCG能够处理的因为主问题本身就是MILP。二是约束拼接时如果循环次数多直接用MP [MP, constraint]这种方式效率不高工程上建议先预分配cell数组再拼接对于大规模问题能减少不少时间。子问题代码的核心是构造对偶问题这一段的实现是整段代码中最容易出错的地方。实操中我建议不要手推完整对偶表达式而是利用Yalmip的dual函数直接从原始约束中提取对偶变量信息减少推导错误的概率。当然对偶理论本身还是需要理解清楚的不然Yalmip报错时根本看不出问题在哪。4.3 CCG迭代流程的参数设置CCG迭代主程序的流程是这样的第一步初始化。设迭代次数iter0下界LB-inf上界UBinf初始不确定场景设为基准场景。第二步求解主问题。得到容量配置方案和主问题目标值由于主问题是松弛版本只包含部分场景约束主问题目标值提供原问题的下界更新LB。第三步把主问题得到的容量配置代入子问题求解子问题找到最恶劣场景。子问题目标值加上第一阶段的投资成本得到原问题的一个可行上界更新UB。第四步计算相对间隙gap (UB-LB)/UB。如果gap小于设定阈值工程上常取1%或0.5%停止迭代输出结果否则提取子问题的最优解对应的不确定场景生成新的约束加入主问题iter加1回到第二步。我在具体实现时发现收敛阈值取0.5%和1%对最终配置结果的影响很小但求解时间差异明显。对于容量配置这类长期规划问题1%的间隙完全够用贪心取太小阈值只会白白增加计算负担。预算参数的设置也需要单独说明。对于负荷波动工程上常见取值是总时段数的5%-10%对于风速和光照由于预测误差通常更大预算占比可以适当提高。我的经验是把三类不确定参数分开设置预算值风速取15%光照取10%负荷取5%实测下来鲁棒性和经济性的平衡效果不错。4.4 结果输出与敏感性分析代码的最终输出包括最优容量配置方案、对应的总成本拆分、各典型日的运行调度曲线、以及最恶劣场景的分布情况。这些输出不只是用来呈现最终结果更重要的是用于验证模型的合理性。最恶劣场景的分布情况值得特别关注。CCG迭代找到的最恶劣场景对应的风、光、负荷水平组合揭示了这个系统在面对什么样的情况时最脆弱。如果发现最恶劣场景全部落在某个典型日类别里说明这个时段的配置裕量不足需要考虑增加对应电源的配置。敏感性分析也是代码配套的重要部分。我习惯在基础案例跑通后做两组敏感性分析一是预算值从0到最大值的全区间扫描看配置结果和总成本的变化趋势二是折现率从5%到12%的扫描看不同资金成本假设下配置方案的偏移方向。这两组分析做完项目的说服力会强很多。5. 常见问题与排查技巧5.1 子问题对偶变换老是报错怎么办这是两阶段鲁棒优化代码实现中出现频率最高的问题。Yalmip报错通常会提示原始问题不可行或对偶问题不可行但实际原因往往是建模本身引入了隐含错误。排查时我有一套固定流程。先检查第二阶段约束是否有冗余矛盾比如功率平衡约束与储能约束连立时在某些极端参数组合下可能出现无解。再检查不确定变量的取值范围风速、光照的波动范围设置过大会导致子问题不可行。然后检查是不是有变量忘记定义或维度不匹配Yalmip对维度一致性的检查并不总是能给出清晰提示。还有一个非常隐蔽的坑跟对偶变量非负性有关。等式约束对应的对偶变量是自由变量如果代码里误写成了非负变量对偶问题求出的目标值会系统性偏大导致UB计算不准确最终影响收敛判据。我在第一次调试时就踩过这个坑花费了大量时间才定位到只是对偶变量定义里少了一个维度。5.2 主问题求解时间爆炸式增长CCG迭代过程中主问题规模不断增大这是正常的但如果出现求解时间爆炸式增长就要回头审视了。最常见的原因是第二阶段变量的数量过多——每个场景24个时段的众多设备变量叠加起来主问题的变量数量会迅速达到数十万级别。一个有效的规避手段是场景压缩。不必把每个典型日的所有24小时都作为独立时段处理可以按负荷峰谷特性合并为8到12个代表时段。项目实测下来从24时段压缩到12时段求解时间能缩短约70%而容量配置结果的变化不超过2%完全可以接受。另一个技巧是给求解器设置合理的MIP间隙容忍度。主问题内部的整数求解不需要每次都达到非常高的精度因为在CCG迭代的前期主问题本身就是一个不完整的松弛版本解得很精确也没有意义。我通常把主问题的MIPGap设为0.5%到1%每轮迭代省下的时间非常可观。5.3 结果中储能容量过大或过小的经验判断储能容量是微网容量配置中最敏感、也最容易出现异常结果的变量。如果储能容量过大通常是切负荷惩罚成本设置过高系统宁愿多配储能也不愿承担任何切负荷风险如果储能容量过小则可能是柴油发电机成本偏低系统选择用柴油来覆盖所有峰荷时段。我在调试中对这个问题形成了两条判断经验。第一切负荷惩罚成本应当参考实际供电合同中约定的缺供电赔偿标准来取值而不是随意给一个很大的数字。这个值在模型里主导着储能配置规模对最终成本的影响非常显著。第二储能投资成本的取值要考虑近年锂电池价格下降的趋势有些文献里用的还是好几年前的单价这样算出来储能容量会明显偏小配置方案直接失真。另一个值得留意的细节是储能的寿命衰减建模。简单的模型只考虑储能的额定能量容量和功率容量两维约束但对边际成本影响很大。我在项目里增加了储能循环寿命的折算进运行成本这样在总成本对比中储能和柴油发电机不会因为简化建模导致比较失真。5.4 求解结果的可靠性与合理性验证最后一步还要做结果验证。一个简单可靠的方法是拿得到的容量配置方案去做蒙特卡洛模拟验证随机生成大量风速、光照和负荷的样本放到这个配置方案下做逐时段的运行模拟统计系统的失负荷概率和平均运行成本。如果模拟得到的失负荷概率水平与模型中预设的预算参数对应一致说明模型构建和求解结果是可信的。这个验证步骤在不少项目里被省略了但我建议不要省尤其是如果这个容量配置的结果要用于实际规划决策。我在实际项目中做过一次验证发现虽然模型设定预算值为总时段数的15%但蒙特卡洛模拟显示失负荷概率很低说明盒式预算约束在最恶劣场景的寻优上还不够激进保守度比预期偏高。这并不算错误但它提示了一个改进方向可以考虑用数据驱动的方法从历史样本中提取更贴近真实分布的凸包作为不确定集合在保证鲁棒性的前提下进一步降低总成本。回到CCG的迭代次数和收敛行为我观察到在预算值中等水平时通常6到8次迭代就能达到1%的间隙收敛而预算值偏小或偏大时收敛往往更快3到5次就结束了。这说明模型的复杂度集中在“既有挑战性又不至于完全无解”的中间地带。理解了这一点后续在调参时心里会更有数。6. 工具选型与运行环境建议6.1 Matlab版本与工具箱的搭配代码对Matlab版本没有特别硬性的要求R2020a以上的版本都能正常运行但建议使用较新版本因为Yalmip对新版本Matlab的适配更好一些。Yalmip是一个建模工具箱需要单独下载并添加到Matlab路径中安装过程就是解压后把文件夹加入路径没有太多坑。求解器方面项目调用的是商用求解器但实际测试下来Yalmip内置的默认求解器在中型规模问题上也能完成求解任务只是速度明显慢一些。如果只是做教学演示或小规模验证不装商用求解器也可以跑通全流程如果要做敏感性分析或大规模场景模拟还是建议安装一套完整的商用求解器许可。6.2 运行时间的分布与优化跑通一个完整案例我观察到的运行时间分布大概是数据预处理和典型日聚类约占5%以下整个CCG迭代过程占绝大多数时间其中主问题求解约占70%到80%子问题求解约占15%到25%。子问题的求解相对快瓶颈始终在主问题的MILP求解上。针对这个瓶颈除了前面提到的时段压缩和MIPGap放宽还有一个更进一步的优化手段热启动。在CCG迭代中相邻两轮主问题只有新增的约束之前的最优解是一个很好的初始猜测。Yalmip支持设置初始解我实测这能再缩短主问题求解时间约20%到30%而且实现非常简单就是在主问题求解前用assign函数给变量赋值。最后说一个我在实际跑这个项目时特别有体会的点。两阶段鲁棒优化的容量配置模型数学符号繁复、代码量大初次接触的人很容易被细节淹没。但一旦把框架理清楚——主问题管规划、子问题找恶劣场景、迭代连接两者——后续所有调整都是在这个骨架上做文章。参数怎么改、场景怎么换、约束怎么加都是在同一套迭代体系里运转代码的可维护性就会高很多。这个项目做完之后我自己最大的收获是鲁棒优化不是越保守越好关键是把不确定集合的形状描述准确。盒式加预算约束只是最基础的起点后续如果你有精力继续深入可以试试数据驱动的不确定集合构造方法或者把两阶段扩展成多阶段自适应鲁棒模型那会是另一个值得探索的深水区了。