
做微电网优化调度绕不开“不确定性”这道坎。光伏和风电出力靠天吃饭负荷预测总有偏差如果调度方案不考虑这些波动实际运行时要么切负荷要么弃风弃光怎么算都亏。业界通常用随机优化但随机优化需要精确的概率分布实际工程里往往拿不到用传统鲁棒优化又过于保守优化出来的成本高得离谱。两阶段鲁棒优化恰好在这两者之间找到了平衡点——它把决策拆成“事先安排”和“事后调整”两个阶段用不确定性集合描述变量波动范围目标是在最恶劣场景下依然能交出可行的调度计划。这套方法论在微电网优化里非常实用而Matlab Yalmip Cplex的组合堪称落地实现这套算法最顺手的工具链。这篇文章就围绕这一套组合拆解两阶段鲁棒微电网优化的建模思路、代码实现以及实际运行中会踩的各种坑。适合谁看如果你在搞微电网经济调度、园区综合能源系统优化或者在读电气工程相关专业、毕业论文刚好卡在鲁棒优化这块这篇文章能给你一条相对完整的实操路径。读完你至少能明白两阶段鲁棒优化到底在求什么CCG算法列与约束生成怎么一步步迭代出最优解以及用Yalmip建模时那些“看起来没问题但一跑就报错”的坑怎么绕开。1. 整体设计与思路拆解为什么选两阶段鲁棒而不是随机优化1.1 鲁棒优化解决的核心矛盾微电网调度面临的核心矛盾就是可再生能源出力的“不可控”和负荷需求的“必须满足”。你想制定一个日前调度计划决定每台机组何时启停、何时充放电、何时从主网购电但你并不知道明天9点15分光伏到底出力多少——可能比预测高20%也可能低30%。随机优化处理这个问题的方式是为每个场景分配一个概率然后求期望值最小的方案。听起来很完美但有两个实际问题第一你很难拿到准确的风光出力概率分布即便有历史数据分布也会随季节和天气变化第二随机优化求解的是“平均意义上的最优”可能导致在某个极端场景下方案完全不可行。比如某个低概率高烈度的场景恰好发生了你的调度方案可能直接让系统失稳。两阶段鲁棒优化换了个思路我不和你赌概率我直接划一个不确定性集合比如“光伏出力在预测值的±20%范围内波动”然后求解“在这个集合内最恶劣情况下依然能保证系统安全运行的最优方案”。这有点像买保险——不是买最便宜的而是买最坏情况下也能扛住的。对于微电网这种对供能可靠性要求极高的场景这种保守性恰恰是工程上最需要的。1.2 两阶段决策的物理含义两阶段鲁棒中的“两阶段”对应到微电网调度里有着非常清晰的工程含义第一阶段决策是在不确定性实现之前就要拍板的事情。典型的就是机组的启停状态0-1变量、与主网的购售电协议、储能的充放电计划基线。这些决策的特点是决策周期长、调整代价高或者根本来不及调整。你不可能等到明天9点15分光照已经出来了再决定发电机要不要开机——冷启动时间都要好几个小时。第二阶段决策则是在不确定性实现之后系统采取的“校正动作”。比如调整某台机组出力、调用储能放电、调整向主网购电的功率。这些决策的特点是响应快、可在短时间内完成但代价比日前计划高比如不按计划购电可能要付惩罚电价。这种“事前安排 事后校正”的结构非常符合微电网的实际运行逻辑也比单阶段鲁棒优化“一刀切”的做法经济得多。单阶段鲁棒要求一个方案在不确定性集合的所有可能实现下都可行往往导致机组配置冗余、运行成本极高。两阶段鲁棒则允许在不确定性实现后进行调整用更小的代价换取可行性。1.3 为什么选Yalmip Cplex这套组合做两阶段鲁棒优化数学上绕不开的是“min-max-min”三层结构。这类问题直接扔给求解器是解不了的必须通过算法把它变换成可求解的形式主流方法有Benders分解对偶法和列与约束生成CCG。无论哪种都需要一套顺手建模工具做支撑。Yalmip是Matlab下的一个建模语言它的价值在于把“数学表达式”和“求解器”解耦。你用Yalmip写约束和目标函数就像在纸上写数学公式一样自然不需要关心求解器的具体调用语法。Cplex则是全球性能顶级的数学规划求解器之一处理大规模线性规划和混合整数规划问题非常强。两阶段鲁棒优化的主问题是一个混合整数线性规划MILP子问题是一个对偶后的线性规划LP或整数线性规划MILPCplex对这两类问题都支持得很好。这套组合相比纯手写求解器接口效率能提升数倍。你只需要把注意力集中在建模逻辑上Yalmip负责把模型翻译成Cplex能理解的矩阵形式Cplex负责在底层做单纯形法、分支定界这些脏活累活。而且Yalmip和Cplex的官方教程、开源示例都很多遇到问题搜索引擎能给出大量参考。2. 核心细节解析与实操要点模型构建与算法选择2.1 不确定性集合的建模方式做鲁棒优化的第一步是先定义不确定性集合。这不是拍脑袋定的它直接影响优化的保守度和计算复杂度。微电网里最常见的是盒式不确定性集合U {u : |u - u_hat| ≤ Δ}其中u_hat是预测值Δ是偏差上界。这个集合写起来最简单但有一个问题它允许所有不确定性变量同时达到最恶劣值结果往往过于保守。实际运行中光伏和负荷不太可能同时跑到极端。于是有了更精细的预算不确定性集合U {u : |u - u_hat| ≤ Δ, Σ|u_i - u_hat_i| ≤ Γ}这个Γ就是预算系数限制不确定变量中“同时偏离预测值”的个数。Γ越小方案越经济Γ越大方案越保守。工程上Γ可以通过历史数据的统计来确定比如保证95%的时间内实际场景都落在集合内这个集合的“容积”就比较合理。这个设计非常关键因为两阶段鲁棒优化最终解的质量和你划的这个集合边界有直接关系——集合划小了极端情况下方案可能不可行集合划大了成本又上去了。2.2 主问题与子问题的标准结构写出来是什么样把两阶段鲁棒优化写成标准形式长这样主问题MP min c^T x θ s.t. Ax ≤ b θ ≥ d^T y h^T z 对每个已识别的最恶劣场景 B x C y D z ≤ g x ∈ {0,1}y, z ≥ 0这里的x是第一阶段决策变量机组启停、购电状态等θ是对最恶劣场景下运行成本的估计值y和z是第二阶段决策变量机组出力调整量、储能功率等。主问题实际上是一个“带着若干已确定场景约束”的混合整数规划每轮迭代会往里面加一组新的场景约束。子问题SP则是在给定第一阶段决策x*后寻找让第二阶段成本最大的不确定性实现SP: max_{u∈U} min_{y,z} d^T y h^T z s.t. B x* C y D z ≤ g - E u子问题是一个max-min结构直接求解困难需要用线性规划对偶理论把它变成单层的max问题。对偶变换是这里最核心的数学操作——把内层的最小化问题先写出拉格朗日对偶变成一个关于对偶变量和不确定变量u的max问题目标函数里会多出u的线性项这样整个子问题就变成了一个可解的单层优化。2.3 CCG和Benders分解到底选哪个两阶段鲁棒优化的经典求解算法有两种Benders分解又叫outer approximation和CCG列与约束生成。两者的区别在于Benders对偶法通过引入对偶变量把子问题的信息以“割平面”的形式反馈给主问题每次迭代向主问题添加一条割约束CCG则直接把子问题求解得到的最恶劣场景对应的约束和变量全都加进主问题相当于“原封不动搬过去”。实操中我更推荐CCG原因有三条第一CCG的收敛速度比Benders快得多。Benders割平面在早期迭代中比较“松”往往需要几十轮甚至上百轮迭代才能逼近最优解而CCG通常十几轮就能收敛到很紧的界。第二CCG不需要对第二阶段问题做复杂的对偶推导——虽然这个例子中还是需要对偶变换但整体实现比Benders要简单直观代码维护成本低。第三CCG处理存在整数变量的第二阶段问题时不需要像Benders那样做离散凸包近似适应性更强。唯一的代价是主问题会随着迭代轮数增加而越来越大求解时间变长但现代Cplex处理几千个约束的MILP完全不是问题。我个人在实际项目里的经验是先写CCG框架跑通一个简单算例再逐步增加约束和变量。如果迭代超过30轮还不收敛先检查对偶变换是否出错再检查不确定性集合的写法——八成问题出在这两个地方。3. 实操过程与核心环节实现完整算例与代码骨架3.1 微电网算例的参数设计为了演示整套流程我们设计这样一个简单的微电网系统一台燃气轮机PG、一套储能系统BT、一个光伏电站PV以及一个不可控负荷PL。系统与主网之间有一条联络线可以购电也可以售电。不确定性主要来自光伏出力和负荷预测偏差取预算型盒式集合Γ取5。参数如下参数数值单位燃气轮机最大出力800kW燃气轮机最小出力100kW储能容量600kWh储能最大充放电功率200kW光伏预测出力300kW负荷预测值峰值600kW联络线最大购电功率400kW购电电价峰值时段0.8元/kWh购电电价谷值时段0.3元/kWh调度周期是24小时步长1小时。第一阶段决策量是燃气轮机的启停状态第二阶段决策量是燃气轮机出力、储能充放电功率以及向主网购电功率的调整量。目标函数是总运行成本最小包括燃料成本、购电成本和弃光惩罚。3.2 CCG迭代框架的Matlab实现整个CCG算法的实现可以拆成四个模块我用Matlab的伪代码加示意来展示核心逻辑。主函数框架%% 主函数两阶段鲁棒微电网优化 CCG % 加载参数 [param, system] load_system_config(); % 初始化 LB -1e9; UB 1e9; iter 0; x0 initial_decision(param, system); % 初始可行解 best_scenario []; % 存储每次迭代找到的最恶劣场景 % 迭代 while UB - LB 1e-3 iter 30 iter iter 1; % 第一步求解主问题 [x_next, theta_val] solve_master_problem(param, best_scenario); LB calc_lower_bound(param, x_next, theta_val); % 第二步求解子问题给定第一阶段决策 [worst_cost, worst_u] solve_subproblem(param, system, x_next); UB min(UB, worst_cost calc_first_stage_cost(param, x_next)); % 第三步累加最恶劣场景更新主问题的约束集合 best_scenario{iter} worst_u; end主问题的Yalmip建模代码重点看约束怎么组装function [x, theta] solve_master_problem(param, scenarios) % Yalmip变量声明 x binvar(param.N_gen, param.T, full); % 机组启停 theta sdpvar(1, 1); % 成本估计值 P_g sdpvar(param.N_gen, param.T, full); % 机组出力二阶段 P_bat sdpvar(1, param.T, full); % 储能功率正为放电 Soc sdpvar(1, param.T, full); % 储能电量 P_grid sdpvar(1, param.T, full); % 联络线功率 % 目标函数一阶段成本 最恶劣场景成本 objective sum(sum(param.fuel_cost .* P_g)) ... sum(param.grid_price .* P_grid) theta; constraints []; % 基础运行约束 for t 1:param.T constraints [constraints, ... P_g(:,t) param.P_g_min .* x(:,t)]; constraints [constraints, ... P_g(:,t) param.P_g_max .* x(:,t)]; constraints [constraints, ... Soc(:,t1) Soc(:,t) param.eta_c * max(P_bat(:,t),0) ... - (1/param.eta_d) * max(-P_bat(:,t),0)]; constraints [constraints, ... -param.P_bat_max P_bat(:,t) param.P_bat_max]; constraints [constraints, ... 0 Soc(:,t) param.Soc_max]; constraints [constraints, ... P_g(:,t) P_bat(:,t) P_grid(:,t) ... param.P_load(:,t) - param.P_pv(:,t)]; constraints [constraints, ... -param.P_grid_max P_grid(:,t) param.P_grid_max]; end % 每个最恶劣场景对应的第二阶段约束CCG核心 k 0; for i 1:length(scenarios) u_pv scenarios{i}.pv; u_load scenarios{i}.load; P_g_s sdpvar(param.N_gen, param.T, full); P_bat_s sdpvar(1, param.T, full); P_grid_s sdpvar(1, param.T, full); for t 1:param.T constraints [constraints, ... P_g_s(:,t) P_bat_s(:,t) P_grid_s(:,t) ... (param.P_load(:,t) u_load(:,t)) - ... (param.P_pv(:,t) u_pv(:,t))]; constraints [constraints, ... P_g_s(1,t) param.P_g_max(1)]; end % 第二阶段成本也要计入目标函数用新求和项 k k 1; objective objective ... % 这里实际应根据迭代需求累加成本 0; end options sdpsettings(solver, cplex, verbose, 0); optimize(constraints, objective, options); end子问题需要先做对偶变换再求解。对偶变换的过程比较绕简单说把内层的min问题对偶成max问题后子问题变成“最大化一个带不确定变量的线性目标函数同时满足对偶可行域约束”。这个对偶可行域并不依赖于不确定变量u所以u的最优解会落在不确定性集合的顶点上——这是盒式集合的重要性质也是为什么能用枚举法或再嵌套一层优化来求解的原因。在代码实现上我习惯直接调用Yalmip的多参数规划能力或者更朴素一点用“对偶变量 强对偶定理”把max-min写成单层问题然后用Cplex求解。很多初学者在这里容易栽跟头我建议先把子问题内层LP写成标准矩阵形式再用对偶性质推导一遍最后落到代码上而不是直接照搬网上零散片段。3.3 收敛判据与迭代记录的细节处理CCG算法的收敛判据就是上界UB和下界LB的间距当间距小于某个阈值时停止迭代。但实操中有几个细节值得注意。第一初始UB可以用一个“太保守”的可行方案算出来初始LB可以设成负无穷。第一次迭代时主问题只包含第一阶段约束和一个松散的θ求解出来的下界往往非常松别慌这是正常的。第二子问题求解的最恶劣场景如果和上一轮完全相同说明已经收敛到最优了不需要再继续迭代。这是一个特别实用的提前终止判断可以省很多计算时间。第三记录每一轮的UB、LB、gap值和迭代耗时一方面方便后续调参对比另一方面这也是论文或报告里的必要内容。4. 常见问题与排查技巧实录这一节全是实战踩过的坑4.1 求解器报错“infeasible”的边界排查用Yalmip Cplex最常见的报错就是主问题或子问题直接不可行。遇到这种情况我的排查顺序是固定的先检查功率平衡约束的等号方向。微电网优化里功率平衡写成等号还是不等号结果差异很大。现实中系统可以接受少量弃光或切负荷但等号约束要求必须严格平衡一旦参数设置矛盾比如负荷大于机组加联络线上限问题必然不可行。解决办法是引入松弛变量把等号改成不等号然后在目标函数里加惩罚项。这样做既保证模型有解又在经济性上惩罚了松弛行为贴合实际运行逻辑。再检查储能SOC递推公式里充放电效率的处理。如果充放电同时用max和min操作Yalmip可能会把它变成非凸约束导致求解器报错。更稳妥的建模方式是把充功率和放功率拆成两个非负变量分别约束上限再在SOC递推中分项处理。这是我踩过最多次数的坑——模型表面上看起来没问题但Cplex就是解不了排查半天发现是效率公式引入了非凸性。最后检查不确定性集合的顶点枚举。盒式集合的最恶劣点一定在边界上如果你的集合形状写成了椭球或者更复杂的形状对偶变换后可能是二次约束Cplex的LP求解器处理不了。这种情况需要用Cplex的QP求解器或者把不确定性集合的约束线性化。4.2 迭代不收敛或者收敛极慢这个问题基本上是鲁棒优化项目里排查率最高的。我调试过一个实际微电网项目CCG迭代了40多轮还在震荡最后发现问题出在子问题的对偶变换上。第二阶段问题里含有整数变量比如储能充放电状态对偶变换后多了一组与整数变量关联的互补松弛条件但这些条件在Cplex默认设置下没有被很好地处理导致子问题返回的不是真正的上界。解决思路有两个方向一个是把第二阶段模型里的所有状态变量都写成连续变量用等式约束和边界约束来替代逻辑约束另一个是干脆用枚举法对不确定性集合的顶点遍历反正盒式集合的顶点数量可控枚举的精度还更高。我最终选择了后者——24小时调度不确定性变量就光伏和负荷两个组合起来顶点数量完全在可接受范围内。子问题变成了一组并行求解的确定性优化问题取最大值作为UB数学上不存在任何含糊的地方。另一个收敛慢的原因是主问题中θ约束的逐步添加方式。CCG的精髓就是每次只加一条约束这个“一条”必须是子问题返回的最恶劣场景对应的完整约束集。如果只返回了部分约束或者返回了冗余约束主问题会膨胀得越来越慢。建议在主问题代码里打印每一轮约束数量变化如果发现约束数量增长异常那么检查子问题返回的场景变量是否携带了完整的第二阶段决策约束。4.3 Cplex许可证与求解速度的工程问题Cplex是商业求解器新版本许可证激活确实令人头疼。如果你只是学习验证Cplex的社区版Community Edition能解决一定规模的问题但限定了求解变量数量超出后直接跑不了。这在大规模微电网场景下是个硬约束我的建议是先用小算例跑通算法再考虑换用的学术版或企业版许可证。需要注意一点Cplex新旧版本的接口是向下兼容的但Yalmip某些版本对Cplex 12.10以上的支持有细微变化求解选项配置可能会报错必要时把Yalmip更新到GitHub最新release。求解速度方面24时段微电网两阶段鲁棒优化CCG迭代通常不会超过15轮单轮主问题求解时间在几秒到几十秒级别整个流程算下来在5~10分钟内都是正常范围。如果单轮求解时间超过几分钟大概率是模型中引入了不必要的整数变量。比如储能充放电状态如果只是为了避免同时充放可以用一组约束限制而不是引入额外的二进制变量。减少二进制变量数量对Cplex求解速度的影响是成数量级的。4.4 结果合理性校验算法对了还不行代码能跑通、收敛也正常程序就结束了吗远远不是。算法收敛到的最优解还需要经过结果合理性校验否则算法的每个迭代那都是白跑的。最基础的校验是看最恶劣场景下第二阶段决策是否满足所有物理约束。我把每一轮迭代的最恶劣场景存储下来事后单独对每个场景做一次完整的潮流计算或功率平衡校验看是否出现弃光、切负荷过重或储能SOC越限的情况。如果这些校验不过说明模型里遗漏了某些约束算法收敛了但收敛到了一套“物理上不可行”的方案。第二个校验是看不确定性集合的边界行为。把Γ分别设成0、5、10、20观察总成本的变化趋势。Γ0意味着不含不确定性方案最便宜Γ逐渐增大成本应该单调上升。如果出现成本下降的异常情形说明子问题求解有误最恶劣场景没找对。第三是和经济调度常识对照。鲁棒优化结果通常比确定性优化结果贵5%~15%如果差距超过30%大概率是模型过度保守或者目标函数权设置有误如果几乎没差别大概率是不确定性集合划得太小等同于不确定性没有真正参与决策。这两条经验基准是我在做项目验收和发表论文前必查的。5. 一点实操心得与扩展思路两阶段鲁棒微电网优化这套体系真正难的不是会调solve函数而是对模型的理解和判断。我的个人体会是不要急着把代码写得复杂先把不带不确定性集合的确定性经济调度模型跑通得到最优解作为基准再加上预算不确定性集合把两阶段框架一步步搭起来每一步都要对比输出是否有合理性最后才轮到CCG迭代收敛的调优。这样即使出了问题也容易定位是哪一层引入了bug。这个项目后续还有几个很自然的扩展方向一是把储能寿命衰减模型引入第二阶段目标函数让优化结果对储能过度充放电更敏感二是将日前调度和实时调整两个时间尺度统一到同一个两阶段框架里做模型预测控制MPC与鲁棒优化的嵌套三是把更多可再生能源品种风电、小水电和需求响应资源纳入不确定性集合的建模范围这套方法论的适应能力很强值得继续深挖。