ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

两阶段鲁棒微网优化调度中的关键场景辨别算法与Matlab实现

两阶段鲁棒微网优化调度中的关键场景辨别算法与Matlab实现 接手这个项目的时候我盯着“基于关键场景辨别算法的两阶段鲁棒微网优化调度”这行标题看了很久。以我在微电网优化调度方向做了几年代码的经验判断这里面其实叠了三层硬骨头第一层是两阶段鲁棒优化本身的数学建模和求解逻辑第二层是“关键场景辨别”这个听起来很唬人的算法到底在解决什么问题第三层才是Matlab代码实现落地的各种细节。这篇博文我就按这三个维度展开把我实际建模、写代码、调参数时踩过的坑和验证过的做法都梳理一遍希望能帮到正在做微网调度、不确定性优化或者想入门两阶段鲁棒的同学。1. 微网调度的难点与两阶段鲁棒的解题逻辑1.1 微网调度为什么绕不开不确定性微电网的优化调度本质上是在“源—荷—储”构成的系统里找一个最经济的发电/购电/储能充放电方案。目标函数通常是运行成本最小化约束包括功率平衡、机组出力上下限、爬坡约束、储能SOC约束、联络线功率限制等等。这些本身不难标准的确定性优化就能做。真正让问题复杂化的是源侧的光伏出力和负荷侧的用电需求都存在强烈的随机性——光伏中午可能超发傍晚陡降负荷高峰可能提前也可能延后。如果你用确定性的预测值去调度系统往往会在实际运行中面临功率失衡或者经济性大幅恶化。所以微网调度的核心矛盾不在“优化”本身而在“如何在不确定环境下做决策”。1.2 两阶段鲁棒到底“鲁棒”在哪里两阶段鲁棒优化的思想其实很直观。第一阶段变量对应“现在就要拍板”的决策比如机组启停状态、储能充放电计划、与主网的交换功率协议第二阶段变量对应“等不确定量实现之后再调整”的决策比如各机组实际出力、储能实际充放电功率、切负荷量。决策结构是你先做第一阶段决策然后在最坏情况下做第二阶段调整目标是在所有可能的不确定性实现下保证约束可行且总成本最低。这个“最坏情况”的假设是鲁棒优化区别于随机规划的核心——随机规划考虑的是期望值鲁棒优化盯住的是最恶劣场景。对于微网这种对供电可靠性极其敏感的系统鲁棒方案虽然会比期望方案保守一些但换来的是更强的安全边界。我当时选两阶段鲁棒而不是纯随机规划还有一个现实考虑微网调度往往面临的是历史数据稀缺的分布式场景你很难给出准确的概率分布。鲁棒优化不需要精确分布只需要知道不确定量的范围这在实际工程里容易获取得多。1.3 关键场景辨别算法要解决的是什么问题传统的两阶段鲁棒优化求解最主流的思路是CCG列与约束生成算法把原问题分解成主问题和子问题不断迭代寻找最坏场景。但这里有个效率痛点CCG迭代过程中每次子问题求解都会识别出一个场景加入主问题如果初始场景集合太大或者最坏场景分布比较分散迭代次数会显著增多主问题的规模也会越来越大整体求解时间可能让人抓狂。关键场景辨别算法就是用来“预筛”或者“迭代剪枝”的——从大量不确定场景中识别出对调度结果影响最大的那些关键场景剔除掉冗余或高度相似的场景让主问题规模可控同时保证鲁棒性不打折扣。说白了这个算法是在“求解精度”和“计算效率”之间找平衡点。2. 关键场景辨别算法的原理与实现路径2.1 场景筛选的直觉与数学逻辑场景筛选的直觉其实很朴素如果一千个采样场景里有八百个场景的净负荷曲线形状几乎一样只是峰值差了0.5%那它们对优化结果的影响是可以忽略的——你把这一簇场景聚成一个代表场景优化结果不会有明显变化但模型规模一下子降了一个量级。这就是典型的风电、光伏、负荷场景缩减思路。具体到关键场景辨别算法它跟普通的蒙特卡洛场景缩减还不太一样。普通场景缩减比如基于概率距离的快速前向选择法是根据场景之间的几何距离做聚类而关键场景辨别更强调“对目标函数和约束的边际影响”——一个场景是否关键要看它被加入鲁棒模型之后是否显著改变了最优解或者拉大了最坏情况下的切负荷量。所以它的核心步骤通常是先用少量初始场景求解鲁棒模型得到当前最优调度方案然后在一个大的场景池里搜索让这个调度方案“最难受”的场景也就是最大化第二阶段调整成本的场景。如果这个场景带来的成本增量超过阈值就把它加入主问题重新求解。这个过程跟CCG主从迭代天然契合等于是在CCG的框架内嵌入了一层场景库的主动筛选。2.2 关键场景辨识的常见做法从我调研和实测的情况来看关键场景辨别算法在工程上通常有三种做法。第一种是“枚举判别”法。把不确定性空间按历史数据划分成若干个典型区间比如光伏出力高中低、负荷大中小在每个区间里枚举边界组合然后用CCG子问题的对偶乘子来判断哪些边界组合是“紧约束”——乘子越大说明该场景对调度方案约束越紧就是关键场景。这种方法思路清晰但容易受区间划分粒度影响。第二种是“自适应场景迭代”法。先随机采一个大场景池用聚类的办法粗筛出一批候选场景然后把这批候选场景逐一丢进鲁棒子问题里做判定保留那些能显著抬高目标函数或者导致约束越限的场景剔除冗余场景。这个方案计算量相对小适合Matlab环境也是我在这个项目里主要采用的路径。第三种是“深度嵌入”法。把场景判别跟主从迭代耦合主问题求解一次之后不是被动等子问题返回最坏场景而是主动从场景池里挑最坏的若干个同时加入主问题相当于“一次迭代多场景增强”。这种方法收敛更快但对求解器的内存占用更多适合场景池规模不是特别巨大的情况。我实测下来对典型的微网系统比如含光伏、风机、储能、柴油机组、联络线场景池规模300个左右时方法二的迭代次数一般在6到10轮就能收敛相比纯CCG每次只加一个场景速度能提升40%以上。而且关键场景筛出来的场景往往很有代表性——大概率集中在净负荷极端偏大和极端偏小这两端。2.3 场景缩减方法对比方法核心思路优点缺点适用情况蒙特卡洛直接采样随机采样大量场景直接建模简单粗暴覆盖面广场景量大主问题规模爆炸小规模系统快速前向选择FPS按概率距离逐步选择代表性场景理论成熟场景压缩率高对极端场景保留不足分布较规则K-means聚类缩减聚类取中心场景实现简单速度快容易把极端场景平滑掉粗筛候选场景关键场景辨别以对目标函数影响为标准筛选鲁棒性保持好规模控制佳实现复杂度高两阶段鲁棒优化我为什么最终选了关键场景辨别而不是单纯K-means聚类原因很简单K-means聚类出来的中心场景都是“平均脸”但鲁棒优化恰好需要关注的是“最丑的那张脸”——极端场景。如果用聚类缩减很可能把最坏情况给磨平了最后算出来的调度方案在真正的极端天气下根本扛不住。关键场景辨别直接以目标函数和约束的边际影响为筛选标准本质上跟鲁棒优化的目标是一致的。3. Matlab建模实践从数学公式到可运行代码3.1 模型变量的维度设计先想清楚再动手建模之前最忌上来就写约束。我自己的习惯是先盘变量把变量表列清楚再写约束。以这个项目为例我设定调度周期为24小时时间间隔1小时。第一阶段变量包括储能各时段充放电的0-1状态变量用来防止同时充放、机组启停状态变量如果含多台机组、联络线购售电0-1状态变量。第二阶段变量包括储能充放电功率、机组出力、切负荷量、购售电功率。不确定性变量是光伏出力和负荷功率我把它们写成“预测值预测误差”的形式误差区间根据历史数据按95%置信区间取上下界。这里有一个特别容易踩坑的地方储能的无同时充放约束如果直接用两个连续变量乘一个0-1变量来约束在YALMIP里用binvar和sdpvar混合建模是可以的但要注意求解规模一上来0-1变量增加会显著拖慢求解速度。我在调试中把储能充放状态、机组启停、购售电状态统一合并成一个二进制变量向量然后用reshape和逻辑索引去写约束效率会高很多。3.2 YALMIP建模的关键细节用YALMIP建模两阶段鲁棒优化有几个细节是我摸索了很久才搞顺的。第一不确定集合的建模要用uncertain命令标记不确定性变量同时在optimize命令里指定求解算法。YALMIP支持用boss算法直接求解鲁棒优化问题但对于两阶段混合整数鲁棒问题直接用YALMIP内置鲁棒求解器的效果并不理想更稳妥的做法是自己写CCG迭代外循环把子问题中的max-min用强对偶或者KKT条件转换成单层优化问题。第二子问题的内层max-min结构是两阶段鲁棒求解最容易出错的地方。如果第二阶段问题是一个LP没有0-1变量可以直接用强对偶把内层min问题对偶成max问题然后与外层max合并成一个单层max问题。这个方法我用YALMIP做dualize的时候需要极其小心因为YALMIP的dual函数返回的对偶变量顺序和原问题约束顺序有关一旦约束顺序写乱了对偶约束就对不上号。我的建议是对偶化这一步干脆不用YALMIP自动处理而是自己在草稿纸上把原问题写成标准形式min cx, s.t. Ax≤b然后手写对偶问题。虽然费点功夫但出错了好排查。第三目标函数里如果有绝对值或者max-min嵌套优先引入辅助变量线性化。比如第二阶段的目标是最小化运行成本其中包含购电成本和切负荷惩罚成本这些都可以直接写成线性表达式不需要额外处理。只有当第二阶段包含机组启停即存在整数变量时才需要特殊设计而两阶段鲁棒中第二阶段一般假设为连续可调所以在建模时就尽量避免在第二阶段放整数变量否则强对偶就用不了了。3.3 求解器配置Cplex还是Gurobi这个项目我先后试过Cplex和Gurobi两种求解器。如果只是求解确定性单层优化问题两者差距不大但要解两阶段迭代模型我的体会是Gurobi的单纯形法在对偶子问题求解上表现更稳定尤其在模型规模变大、约束数量增多之后Gurobi的重新求解warm start机制优势明显。Cplex在许可证获取上更省心很多学术机构有旧版license性能也完全够用。Yalmip的配置很简单optimize调用之前确保求解器已经加入MATLAB路径即可。我建议用Gurobi 10以上版本搭配YCALMIPYALMIP的Gurobi接口稳定版本实测下来整数变量500个以内连续变量2000个以内单次求解时间基本能控制在10秒上下。4. 关键代码逻辑与参数整定4.1 主程序框架CCG迭代流两阶段鲁棒求解的完整逻辑我用一个精简的伪代码结构来展示以下是Matlab风格的示意代码需要的话可以直接改造成.m文件% 主程序框架 % 初始化 LB -inf; UB inf; iter 0; x_opt []; % 第一阶段最优解 scenario_set initial_scenarios; % 关键场景集合 while (UB - LB) / abs(UB) 0.01 iter 20 iter iter 1; % 主问题给定场景集合scenario_set求解第一阶段和第二阶段联合优化 [x_opt, theta, obj_mp] solve_MP(scenario_set); LB obj_mp; % 子问题固定x_opt在场景池中寻找最坏场景 [worst_obj, worst_scenario, duals] solve_SP(x_opt, scenario_pool); if UB - eps worst_obj theta UB worst_obj theta; end % 关键场景辨别判断worst_scenario是否应该加入scenario_set if marginal_importance(worst_scenario, duals) threshold scenario_set [scenario_set, worst_scenario]; else % 当前最坏场景影响不显著结束迭代 break; end end注意这里面有一个容易被忽略的逻辑主问题的目标函数值在第一阶段决策确定后其实包含了第二阶段所有已加入场景的期望成本或者最大成本取决于你用的是鲁棒还是鲁棒-随机混合目标。我在写代码时把theta定义为第二阶段成本的辅助变量通过约束theta 每个场景的第二阶段成本来实现“最坏情况上界”的效果。这个写法在两阶段鲁棒里是标准做法但如果你混用了不同场景集的鲁棒约束很容易出现theta被某个历史场景的约束绑架、导致新场景加进来也无法改善界的情况。4.2 子问题与强对偶的实现细节子问题是两阶段鲁棒优化的灵魂。固定第一阶段决策x_opt之后子问题可以写成sp(x_opt) max_{u∈U} min_{y∈Ω(u, x_opt)} c^T y内部min问题是关于第二阶段变量y的LP外部max是选择不确定量u。使用强对偶内部min的对偶问题是一个max问题那么整个子问题转换成max-max问题即单层max问题。我在实现这段代码时的几个具体注意点对偶变量与非负约束要对齐。第二阶段原问题是标准形式的LP对偶问题的约束数目等于原问题变量数目对偶变量的数目等于原问题约束数目。写错了这个对应关系对偶约束就会发散。对偶转化后的目标函数是非线性的因为涉及u和对偶变量的乘积。这时候需要一个处理技巧不确定量u的取值在区间内而双线性项u * y_dual可以通过大M法或者McCormick包络线性化。在我的代码里由于不确定量的区间比较简单上下界已知我直接用大M法线性化同时配合求解器开启presolve数值稳定性还不错。子问题求解完成之后dual输出里对应功率平衡约束的对偶乘子这个值恰恰是关键场景辨别的判别依据之一。当最坏场景对应对偶乘子的绝对值偏大时说明该场景对调度方案的约束最紧应当加入主问题。4.3 参数整定与收敛性判断两阶段鲁棒迭代的收敛判据我用了相对间隙(UB-LB)/abs(UB) 0.01。注意初始时刻UB可能是无穷大所以要设置一个合理的初值。我的做法是先随便解一个确定性调度不确定性取预测值得到一个可行解作为UB的初值。这个初值如果太紧会导致迭代提前收敛太松会导致多迭代几轮。在这个项目里我取确定性最优解加上10%的裕量作为UB初值效果比较稳定。阈值threshold是关键场景判别的超参数。我建议先跑10个场景的小规模测试记录每个场景的对偶乘子和成本增量分布然后取中位数乘子值作为初始阈值。如果迭代次数过少小于3轮说明阈值设太大关键场景都被滤掉了如果迭代次数过多大于15轮说明阈值太小冗余场景太多主问题膨胀严重。经验值一般在0.05到0.2之间归一化后。还有一个实际调试中发现的细节场景池的采样不能纯随机。我用的是拉丁超立方采样LHS来生成场景池这样能保证采样点在不确定性空间的投影均匀分布关键场景不容易被漏掉。随机采样的场景池经常会出现某个极端区域一个点都没有的情况识别出的关键场景反而不够“关键”。5. 常见问题与调试记录5.1 迭代过程振荡不收敛怎么办我在第一版代码里踩过一个典型坑主问题和子问题交替迭代但UB和LB的差值来回震荡怎么也压不到0.01以下。后来定位到问题是子问题里第二阶段成本的最小值没有加约束下界导致子问题目标函数在某些极端场景下比真实的最坏成本偏高或偏低UB和LB自然对不上。解决方法是在子问题里增加第二阶段变量必须非负、功率平衡必须满足等硬约束同时把主问题里每一轮新加入的场景对应的第二阶段成本变量初始值设为一个合理上界而不是从0开始优化。这个细节不处理干净迭代十有八九会振荡。另一个比较隐蔽的问题是主问题规模膨胀导致的数值退化。每加一个场景主问题就多一组第二阶段决策变量和对应的约束。迭代到第10轮左右主问题的变量规模会比初始时大好几倍这时候如果不做任何预处理求解时间会呈指数级上升。我的对策有两个一是每轮加入场景前做一次相关性检查如果新场景与已有场景的相关系数大于0.95就只合并场景概率不新增变量二是对主问题开启求解器自带的缩放scaling和大M值缩减避免数值病态。5.2 求解器报“Infeasible”问题定位两阶段鲁棒优化最让人头疼的就是子问题不可行。子问题不可行通常不是因为模型本身无解而是因为第一阶段决策x_opt在某些极端u下根本无法满足约束。出现这种情况时我的排查顺序是第一步检查第二阶段约束是否存在“硬冲突”。比如储能SOC约束加上充放功率上下限如果充放电效率不是1循环会带来能量损耗SOC的闭合约束始末SOC一致可能在极端连续充放下无解。第二步检查功率平衡约束的松弛量是否给了足够的调节空间。我通常会在功率平衡约束上加一个虚拟的切负荷变量和弃光变量它们的惩罚系数设为一个足够大的值保证子问题永远“有解”——有解的前提是把切负荷成本作为优化目标的一部分而不是硬约束这样在最极端情况下系统还能通过切负荷或弃光来保住可行域。这个技巧在工程实践中几乎是必须的。第三步检查不确定集合上下界。有次我调试了好久最后发现是光伏出力误差区间设置得过于激进上界已经超过装机容量导致子问题里的虚拟切负荷变量被惩罚成本疯狂拉高看起来就是“解的数值怪异”。避开这个问题的办法是在场景生成之后做一轮边界剪裁把超出物理可行域的采样点拉到边界上。5.3 场景辨别算法为什么选出的场景不合理关键场景辨别算法最大概率翻车的地方是我一开始提到的“聚合过度”。如果你用的是K-means预聚类来减少候选场景再喂给辨别算法很容易出现聚类的代表点不是极端场景的问题。我在实验里发现把场景池从500个先用K-means聚到100个再用辨别算法最终筛选出的关键场景跟直接用500个场景做辨别时差了不少表现在鲁棒模型的目标值上最坏成本偏低了3%左右。3%看起来不大但对于一个强调“安全边界”的鲁棒模型来说这种偏差直接影响调度的可靠性。我的最终方案是场景池不再做预聚类而是保留全部500个候选场景但在关键场景判别时设置两级筛选。第一级用二阶范数距离做快速粗糙筛查剔除明显雷同的场景第二级对剩余的50个左右场景做完整的子问题求解确认边际影响。这个两段式流程兼顾了精度和速度实测下来与直接全量判别相比关键场景集合基本一致但时间能省掉60%。如果你也想用这个方案建议把二级筛选的面定在30到80之间太小容易漏场景太大则速度优势体现不出来。5.4 代码性能优化建议Matlab跑两阶段鲁棒优化最大的瓶颈是循环中的重复建模。YALMIP建模本身是有开销的如果在CCG迭代的每一轮都用全新的sdpvar重新建整个主问题Matlab会花大量时间在符号变量构建上。我的经验是主问题的变量结构在迭代中其实不变变的只是新增的一部分约束。因此可以在第一轮迭代时用sdpvar建好主问题的全部变量框架包括预留一部分扩展空间后续迭代里通过constraint [constraint, 新增约束]的方式增量更新然后直接调用optimize。这种做法配合solvesdp的sdpsettings(solver,gurobi,gurobi.bariterlim,... )选项能显著降低建模和求解耗时。如果场景池特别大上千个还有一个思路把子问题求解并行化。我在Matlab里用parfor并行求解候选场景的子问题因为每个候选场景的LDualization和LP求解互相独立并行化之后整体判别速度的收益非常可观加速比基本随核数线性增长。要注意的是parfor里如果调用了YALMIP的sdpvar需要确保每个worker都正确加载了YALMIP和求解器路径不然会出现诡异的空指针错误。最后再分享一个我在这个项目里体会最深的细节。两阶段鲁棒微网调度很多人在代码实现上栽跟头不是栽在算法原理没搞懂而是栽在对偶转化和场景筛选这两个“中间地带”上——理论上讲得通但落到Matlab代码里就是一堆维度不匹配、数值不稳定的麻烦。我后来养成一个习惯每完成一轮迭代就把主问题的解、子问题的最坏场景、对应场景的功率平衡约束对偶乘子三个量打印出来盯几轮数据的变化趋势再决定是调整阈值还是修正约束。别小看这个笨办法它能帮你快速发现模型里隐藏的逻辑漏洞比翻文档查报错快得多。希望这篇梳理能让你少走点弯路有具体实现上的细节问题欢迎在评论区交流。
返回列表