
做微网调度有一段时间的朋友大概率绕不开一个问题风电和光伏出力不确定负荷也在波动怎么安排机组出力才能既经济又稳得住这个问题看着简单真正落地的时候坑很深。今天这篇就从一个完整的MATLAB实现讲起——基于两阶段鲁棒优化算法的微网经济调度代码配合YALMIP和CPLEX把建模、求解、调参、避坑全过一遍。如果你正在做电气、能源方向的课题或者刚接触鲁棒优化、想找一个能跑起来的微网算例这篇应该能帮你省不少查资料的时间。这篇内容不打算只贴一份代码我会把背后的优化逻辑拆开讲。为什么两阶段为什么用鲁棒而不是随机规划不确定集怎么设CCG算法怎么一步步迭代以及我在实际调试中遇到的几个让人抓狂的问题。文章偏实操但模型部分也不会跳过因为不理解模型代码改了只会越改越乱。1. 项目整体设计与思路拆解1.1 微网经济调度到底在优化什么微网可以简单理解成一个“小号的电力系统”里面有柴油机、燃气轮机这类可控机组有光伏、风电这类不可控电源有储能电池也可能和外部大电网有联络线。经济调度的目标就是在满足负荷需求的前提下安排每一台机组发多少电、储能充还是放、要不要从电网买电让总运行成本最低。成本一般包括燃料成本、启停成本、向电网购电成本还有弃风弃光惩罚、切负荷惩罚等。燃料成本最常用的是二次函数优化模型里通常做线性化处理用分段线性近似。这里面有一个容易忽略的点调度不是只看某一个时刻而是要把24小时甚至更长时间尺度放在一起看储能就是典型的跨时段耦合元件这一小时充的电可能下小时才放所以约束要写成时序递推的形式。如果所有参数都是确定的这其实就是一个典型的混合整数线性规划直接用CPLEX或Gurobi就能解网上也有大量确定性微网调度代码。但现实情况是光伏出力受天气影响早上还是大晴天下午一朵云飘过来出力就能掉一大截负荷也有随机波动。要是完全按预测值调度实际运行时可能出现两条路要么切负荷要么加大备用代价都不小。1.2 为什么一定要用两阶段鲁棒优化面对不确定性常见方案有三种。第一种是确定性优化把预测值当成真值算出来只能参考不具备抵御预测误差的能力。第二种是随机规划给不确定参数假设一个概率分布生成若干场景去求期望成本。随机规划的缺点是分布假设很难准而且当概率分布有偏差时得到的最优解可能在真实场景下严重越限。第三种就是鲁棒优化不依赖概率分布只要求不确定参数落在一个集合内优化结果是“在最坏情况下也满足约束”的调度方案。两阶段鲁棒优化的“两阶段”对应到微网调度里有非常直观的物理含义第一阶段是日前决策决定机组启停、是否需要预购电量、储能是否进入某种运行模式这些决策在不确定量实现之前就要定下来第二阶段是日内调整在风光实际出力出来之后微网可以在允许范围内调整机组出力、储能充放电功率甚至切除部分负荷来保证功率平衡。这个结构很像日常生活中“先定计划再按实际情况微调”。第一阶段决策不能太激进因为要为最坏情况留出调整空间第二阶段调整量又不能无限大受机组出力上下限、爬坡速率、储能SOC等物理约束限制。两阶段鲁棒优化就是研究这两者之间的平衡目标函数里同时包含第一阶段的固定成本和第二阶段在最坏场景下的运行成本。对于微网这种连续变量为主、整数变量可控的调度问题两阶段鲁棒优化的求解精度和解的可解释性都不错这也是它成为热点的主要原因。1.3 技术选型MATLAB YALMIP CPLEX我见过不少课题组用PythonGurobi做微网调度但MATLAB在电气工程领域还是基础盘尤其对刚接触优化的研究生来说理解矩阵操作和线性代数更顺手。YALMIP是一个MATLAB下的建模语言它把sdpvar、binvar、optimize这些函数封得很漂亮写约束就像写数学公式一样。CPLEX则是一个高性能的商业求解器适合解大规模混合整数线性规划和MATLAB之间有官方接口YALMIP也内建了CPLEX的调用入口。这套组合有三个好处第一建模快不需要自己实现单纯形法或分支定界第二调试直观YALMIP可以打印约束数量、变量类型也能直接导出模型方便定位问题第三两阶段鲁棒优化的CCG算法需要反复求解主问题和子问题MATLAB脚本控制循环很方便YALMIP再配合CPLEX的求解器参数可以省掉大量对象化编程的额外负担。当然也可以选Gurobi求解速度在很多测试上比CPLEX还快一点。但CPLEX在学术界的普及度高许可证申请也方便而且YALMIP的兼容性很好换求解器只需要改一行sdpsettings。下面的内容我都以CPLEX为例如果你用的是Gurobi代码逻辑完全不用改。2. 两阶段鲁棒模型的核心细节2.1 不确定集管住预测偏差鲁棒优化最难也最关键的一步是不确定集的选取。不确定集就是描述“不确定参数可能取哪些值”的集合。选得太大调度结果会非常保守成本高到离谱选得太小又失去了抵抗风险的意义。最常用的有两种。一是盒式不确定集。每个时段的预测误差独立光伏出力允许在预测值上下浮动不超过给定的偏差上限。写成形式就是u_t ∈ [p_fc_t - ε_t, p_fc_t ε_t]这个模型的优点是简单缺点是把所有时段的最坏情况同时考虑进去有点“把所有坏运气都押在一起”太保守了。二是预算不确定集。它在盒式基础上加了一个限制比如所有时段误差归一化后的绝对值之和不超过某个预算Γ。这个Γ一般叫“鲁棒预算”含义是真实场景不可能所有时段都出现最大偏差大部分情况下只有少数时段误差较大。预算不确定集能显著降低保守性而且Γ可以从0取到TΓ0时退化为确定性模型ΓT时又回到盒式模型。实际建模时我建议先用盒式不确定集把流程跑通再改成预算不确定集做对比分析。因为预算不确定集在子问题对偶后会出现双线性项虽然可以通过线性化技巧处理但对新手不太友好。我们后面写CCG时子问题会用对偶把min问题转成max问题这时如果U是盒式集合最坏场景可以直接根据对偶变量的符号确定省去很多麻烦。2.2 两阶段决策变量与目标函数拆分两阶段模型里变量要严格区分开。第一阶段变量通常包含0/1变量比如机组开机/停机状态、储能是否充电/放电状态也包含一部分连续变量比如日前预调度出力。这些变量需要在不确定量实现前确定所以在CCG主问题中它们是决策变量。第二阶段变量是“看到不确定量之后才能做出决定”的连续变量比如实际机组出力调整量、储能实际充放电功率、切负荷量、购售电功率。它们的可行域会依赖第一阶段变量和不确定参数。目标函数也要拆开。用紧凑形式可以写成min_x a^T x max_u∈U min_y∈Y(x,u) d^T y外面的min处理第一阶段变量x里面的max-min就是在找最坏不确定场景下的最小调整成本。这里a^T x对应机组启停成本、固定运行成本d^T y对应燃料成本、购电成本、切负荷惩罚、弃风惩罚等。为什么不是直接min总成本因为不确定参数u还没有实现第二阶段的收益/成本必须以最坏情况来评估。这个结构是两阶段鲁棒优化的标准范式也是后面CCG每个迭代步的数学基础。2.3 约束怎么列才不乱微网调度约束看着多其实可以分成三类。第一类是第一阶段约束只含x和辅助变量比如机组的最小启停时间约束、储能是否允许同时充放电的状态约束。注意这类约束不含不确定量所以不会因为场景变化而改变。第二类是第二阶段约束包含x和y且被不确定参数u影响。典型的是功率平衡约束∑ P_g_t P_pv_t P_es_dis_t P_buy_t L_t P_es_ch_t P_sell_t P_curtail_t这里的P_pv_t就是一个不确定参数同时在第二阶段的调整中存在弃光变量P_curtail_t允许在最坏情况下切掉一部分光伏代价是惩罚项进目标函数。第三类是机组运行约束比如出力上下限、爬坡约束、储能SOC递推约束。这些约束虽然不含u但由于第一阶段x固定后第二阶段y的可调范围被x限制它们直接决定了最坏场景下的调整能力。很多初学者喜欢把全部约束一股脑写进一个约束体然后求解。在确定性模型里没问题但在两阶段鲁棒优化里如果主问题和子问题共用同一个YALMIP约束体会导致变量嵌套、迭代混乱。我的经验是把第一阶段约束、第二阶段约束写成独立的函数或脚本段每个阶段单独构建变量和约束用函数参数传入已知变量值这样CCG循环才清晰。3. 实操过程从环境配置到CCG求解3.1 环境准备YALMIP和CPLEX装到位先说你需要的软件MATLABR2020a及以上版本YALMIP最新版CPLEX12.10或更高版本需要去IBM官网申请学术许可。装好的标准是在MATLAB命令行输入yalmiptest能看到CPLEX状态为正确addpath(genpath(D:\yalmip)); savepath; yalmiptest如果yalmiptest结果里CPLEX显示失败多半是CPLEX的MATLAB接口路径没配好。CPLEX自带的接口文件在cplex\matlab\目录下也需要加入MATLAB路径。我在Windows上第一次配置时就是漏了这一步导致YALMIP一直找不到CPLEX。另外YALMIP支持的最新CPLEX版本有对应关系太新的CPLEX配太老的YALMIP会出现内部错误最好都用当前最新版。测试一个最小调度模型能跑通就可以继续x sdpvar(1,1); y binvar(1,1); Constraints [0 x 10, 5*y x 10*y]; Objective 2*x 3*y; sol optimize(Constraints, Objective, sdpsettings(solver,cplex)); value(x)看到x5时说明环境已经没问题。3.2 确定性调度建模先把基准写出来两阶段鲁棒优化不是一上来就直接写CCG。我的习惯是先建一个确定性模型把24小时调度解出来作为鲁棒模型的基准对比同时验证约束和参数有没有明显错误。假设微网有2台燃气轮机、1台柴油机、1组储能、光伏和负荷模型参数用结构体存放mpc.T 24; mpc.gen [ % 机组: [Pmin Pmax a b] 10 80 0.03 2.1; 20 100 0.025 2.5; 5 40 0.04 1.8 ]; mpc.storage.energy_min 0; mpc.storage.energy_max 100; mpc.storage.Pch_max 20; mpc.storage.Pdis_max 20;用YALMIP定义变量Pgt sdpvar(3, mpc.T, full); % 机组出力 Pch sdpvar(1, mpc.T, full); % 充电功率 Pdis sdpvar(1, mpc.T, full); % 放电功率 E sdpvar(1, mpc.T1, full); % 储能SOC Pbuy sdpvar(1, mpc.T, full); % 购电 Psel sdpvar(1, mpc.T, full); % 售电 u_on binvar(3, mpc.T, full); % 机组状态再写约束功率平衡、机组上下限、爬坡、储能SOC递推等。最后设置目标函数Obj sum(sum(mpc.gen(:,3).*Pgt.^2 mpc.gen(:,4).*Pgt)) ... sum(2*Pbuy 0.1*Psel) 1000*sum(Pcurtail);注意燃料成本直接用二次函数会让模型变成二次约束规划部分求解器支持不好。建议对每台机组用分段线性逼近或者干脆把成本曲线按几个线性段拆开引入分段变量。我们在两阶段鲁棒优化里子问题对偶要求模型是线性规划所以成本函数必须线性化这个坑后面会细说。3.3 手写CCG两阶段鲁棒的求解套路目前求解两阶段鲁棒优化最主流的方法是CCG也叫列与约束生成。它在主问题里逐步追加最坏场景对应的约束和第二阶段变量直到主问题目标值和子问题目标值收敛。比Benders分解更直接尤其适合第二阶段线性可解的题目。主问题形式可以写成min_x a^T x η s.t. x ∈ X η ≥ d^T y_k, k1,...,K A x B y_k ≥ C u_k h, k1,...,K这里的u_k是第k次迭代时由子问题找出的最坏场景y_k是新增的场景变量。注意每迭代一次主问题会新增一块y_k和对应的约束所以CCG的主问题规模是随迭代次数增长的。子问题则是固定x求解Q(x) max_{u∈U} min_{y∈Y(x,u)} d^T y内层是LP可以用对偶把min消掉。假设原问题写成 min d^T y, s.t. B y ≥ C u - A x对偶后变成max_{λ≥0} (C u - A x)^T λ s.t. B^T λ ≤ d这样就变成了max_{u∈U, λ≥0} (C u - A x)^T λ约束里B^T λ ≤ d。目标里出现u和λ的乘积也就是双线性项。处理双线性项是子问题求解的关键。如果U是盒式集合即每个时段独立满足 u_t ∈ [u_min_t, u_max_t]那么对于目标函数中的每一项如果把系数提取成关于λ的形式就能按系数符号直接取u_t的边界。也就是说最坏场景不需要用优化求解器找直接对每个时段判断一次符号就可以得到。实践中可以先用YALMIP将子问题建模成双线性规划再调用CPLEX的非线性处理或者用穷举顶点方式求解。但更可靠的做法是写出内层对偶然后用枚举方式处理不确定集顶点这样子问题变成一个纯线性规划CPLEX直接求解。CCG的主循环框架如下LB -1e6; UB 1e6; K 0; % 初始场景用预测值 u pv_forecast u_star pv_forecast; while abs(UB - LB) 1e-3 K 10 % 求解主问题包含已发现的场景 sol optimize(MP_constraints, a*x eta, sdpsettings(solver,cplex)); x_star value(x); LB value(a*x eta); % 固定x求解子问题得到最坏场景和成本 [Q_val, u_star, lambda_star] solve_subproblem(x_star, mp_data); UB min(UB, value(a*x_star) Q_val); % 将新场景u_star加入主问题新增y_K和约束 K K 1; [y_K, MP_constraints] add_scenario(MP_constraints, u_star, K); end注意这里的LB是主问题下界UB是当前解在最坏场景下的真实成本。因为主问题始终是松弛版所以LB会一直比真实最优值小而UB用当前x和子问题计算的成本是可行解的上界。当两者相近时说明x已经足够逼近最优。子问题里如果使用枚举顶点法需要把不确定集的24个上下界组合算一遍。最朴素的枚举法是2的24次方显然不现实。好在目标函数关于u的特性表明如果U是盒式集合且每个时段独立最坏u_t的选取只取决于对偶变量λ_t的符号所以只需要一次线性规划不需要枚举。如果还加了预算约束那么需要多解一个MILP来挑选最坏u。3.4 算例参数与结果验证我给一个简化的典型算例24小时负荷在80kW到220kW之间波动光伏预测出力按典型日曲线预测误差最大20%。两台燃气轮机容量分别是80kW和100kW储能容量100kWh最大充放电功率20kW购电电价采用峰谷分时。运行CCG迭代通常在第3次到第6次迭代后上下界差小于1%。确定性模型的总成本假设是4000元鲁棒模型在最坏情况下的总成本大约到4800元多出的800元就是为了应对最坏PV偏差所支付的“鲁棒溢价”。这个溢价不是越小越好而是要和微网的可靠性要求匹配。如果切负荷惩罚设得很高鲁棒解就会更保守如果惩罚设得很低优化器会倾向于允许偶尔切一点负荷来降低总成本。验证结果是否合理我一般看三件事。第一调度结果是否满足功率平衡逐时段检查净功率是否为0。第二储能SOC曲线是否在0和最大值之间且末尾时刻SOC是否等于设定的初始值或结束值。第三把最坏场景代入确定性方案看看确定性方案会不会出现约束越界借此说明鲁棒优化的价值。4. 常见问题与排查技巧实录4.1 YALMIP/CPLEX报错速查表我把实际调试中遇到过的高频报错整理成一张表方便大家按图索骥。报错信息常见原因解决办法Solver not foundCPLEX接口未配置或未安装检查YALMIP路径和CPLEX路径在MATLAB中运行yalmiptest确认状态No suitable solver for a problem of this type模型是非线性或非凸CPLEX不支持将成本函数线性化或用整数变量重写检查是否有双线性项未处理Infeasible problem detected约束过于严格或第一阶段决策导致子问题不可行检查功率平衡约束、机组最小启停约束调整第二阶段惩罚成本为子问题增加松弛变量Index exceeds array bounds变量维度不匹配打印sdpvar的维度确认所有矩阵大小一致NaN in objective/constraint参数含NaN或不收敛检查全部输入参数设置CPLEX迭代上限和MIP gapInner problem cannot be solved子问题对偶问题无界或不可行检查第二阶段可行域增加对偶变量边界确认强对偶条件成立我这里特别想提一下“子问题不可行”。两阶段鲁棒优化要求第二阶段对任意u∈U都存在可行y。如果模型里第二阶段约束没有冗余度比如机组总出力上限刚好等于负荷与最坏光伏的下限那么在极端场景下就可能出现无解。工程处理手段是在第二阶段目标函数里加很大的切负荷惩罚本质上是允许物理上少量切负荷保证始终有可行解。这比一味收紧约束更符合实际运行。4.2 性能调优让大模型跑起来主问题反复增加场景后MILP规模会变大单次求解时间可能从几秒涨到几分钟。我常用的调优手段有几个。首先是给CPLEX设置相对MIP gap。默认是1e-4对两阶段鲁棒来说太严格了。实际调度问题精度到1%左右就足够设置成0.01能大幅提速settings sdpsettings(solver,cplex, ... cplex.mip.tolerances.mipgap, 0.01, ... cplex.timelimit, 300);其次是给主问题一个初始可行解。用确定性模型算出的x_star作为第一个场景并把它对应的第二阶段变量加入主问题冷启动效果好很多。我实验过一个本来需要迭代5次的模型用确定性解作为初始场景后第2次迭代就基本收敛了。第三是注意大M法参数的取值。处理双线性项或机组状态约束时M取太小会割掉可行域取太大会导致数值病态。我通常选1000左右如果是功率类约束M取所有机组容量之和的2倍就够了。4.3 建模正确性检查别让结果骗了你鲁棒优化代码跑通只是第一步结果对不对是另一回事。我一般做三张检查表。第一确定性极限检查。把不确定集缩到零比如ε0跑两阶段鲁棒模型结果应该和确定性模型完全一致。如果不一致说明主问题或子问题建模有误。第二单调性检查。不确定集越大目标函数值应该越大或至少不降。如果加大预测误差范围后总成本反而下降了那大概率是子问题取到了错误的场景。可以打印当前x下每个u_t对偶符号看看是否和预期一致。第三人工场景注入。手动构造一个极端场景比如光伏为零且负荷最高的时段检验第二阶段能不能通过储能放电和购电补足缺口。如果连最直观的极端情况下都失败那可能不是优化器的问题而是约束缺项。这三步都过了我对模型基本就有信心了。5. 后续扩展与实际体会5.1 从单微网到多微网扩展方向这套两阶段鲁棒优化框架不只适用于单个微网。往大了做可以扩展到多微网互联场景把微网间的功率交换作为第一阶段签订的交易计划第二阶段各微网内部再做调整。这种情况下主问题里会增加微网间联络线潮流约束子问题也要考虑多个微网同时决策不确定性可能是光伏和负荷联合随机向量。CCG框架仍然适用但子问题会变成一个多主体优化需要额外处理均衡或合作博弈关系。往深了做还可以考虑电氢耦合、电动汽车充放电、需求响应等灵活性资源。两阶段鲁棒优化的优势在于不管新增多少连续变量和约束只要第二阶段内层还是线性规划CCG的收敛性就有保障。但每加一种元件子问题对偶的符号和维度就要重新梳理一次建议先把基础版本的代码模块化用函数封装机组、储能、联络线后面扩展只需要写新的元件模块。5.2 一个过来人的几点体会我最初尝试两阶段鲁棒优化时直接抄了一个Benders分解的代码改了一周都没改明白后来才发现是子问题对偶中处理双线性项时符号写错了。如果你也是第一次接触这类问题我的建议是千万别急着上完整模型先用3个时段的超简算例把CCG迭代流程手动跑通打印每一次的主问题目标、子问题目标、新加入的场景把每一步都看明白再扩展到24时段。你花在调试简化模型上的时间会在后面节省几十倍。另一个体会是鲁棒优化不等于越保守越好也不等于永远选择极端场景。最终决策者关心的其实是“鲁棒溢价”和风险承受能力之间的平衡。你可以通过调整不确定集预算Γ生成一组成本-风险曲线让调度人员自己选择运行点。这个比单点计算更能体现两阶段鲁棒优化的工程价值。脚本本身并不复杂就是在外面包一层循环逐个Γ跑CCG把成本曲线画出来。做完这张图整个项目的说服力会强很多。最后再分享一个小技巧输出结果时把每一阶段的机组出力、储能SOC、最坏不确定场景单独画图逐条核对。很多模型逻辑错误不是求解器报错而是结果看起来“正常但不符合物理”。一张好的可视化图能帮你快速定位是哪个约束没生效比盯着矩阵看有用得多。希望这些踩坑经验能帮你少走点弯路。