
1. 项目概述与核心思路1.1 这个项目到底解决什么问题先说结论微网电源容量优化配置本质上是一个“在钱有限的前提下怎么装风机、光伏、储能和柴油机才能既不浪费又够用”的数学问题。我做了几年的微网规划最直观的感受是——大多数初版方案都不是算出来的而是拍脑袋拍出来的。要么按峰值负荷打1.5倍的富裕系数直接把投资成本拉满要么干脆照抄一个类似项目完全不看本地风光资源。这两个极端都不可取。风光出力的随机性才是微网配置最大的变量。同样是10kW的光伏晴天和阴雨天的出力差距能到80%。如果你用一个固定的出力曲线去做优化那得到的结果在真实天气下可能非常危险——要么电源装少了遇到连续阴雨天直接限电要么装多了大半年的设备利用率低得没法看。两阶段鲁棒优化就是冲着这个问题来的。它把决策拆成两层第一层决定“装什么、装多大”这是现在就要拍板、花钱、施工的事情叫做“这里和现在”here and now第二层是“在知道风光出力最恶劣的情况下怎么调度已有设备才不崩溃”这部分是“看到结果再行动”wait and see。两层嵌套在一起形成了min-max-min这种让人头皮发麻的三层结构。这个项目最适合谁看我推荐两类人。一类是在校研究生论文方向涉及微网容量规划、主动配电网或者单纯想找一套鲁棒优化在电力系统落地实例另一类是光伏、储能、园区综合能源的设计人员想把手里的经验公式换成一套有理论支撑的配置方案。前者能学到算法框架后者能直接改参数就跑自己的算例。1.2 为什么确定性优化不够用初学者最容易犯的第一个错误是把风光出力当作已知条件。比如假设光伏出力是某天的实测曲线或者取全年的平均值然后在这个“既定剧本”下做容量优化最后得到一组配置结果。这种做法在数学上很干净一个线性规划或者混合整数线性规划几秒钟就能出结果代价是你把一个不确定性问题强行降维成了确定性问题。确定性优化的隐患在极端场景才暴露出来。少光、无风、负荷高峰这三件事同时出现时如果预留的旋转备用不足系统就只能切负荷。切负荷在工程上意味着罚款、投诉、甚至生产事故这是规划阶段绝不允许的。随机优化stochastic optimization是另一种常见路线。它给不确定性赋予概率分布然后生成大量典型场景参与优化目标函数变成期望成本最小化。这个思路理论上很完善但有一个致命前提——你得准确知道风光出力的概率分布。现实世界的季节性波动、气候异常、预测误差分布很难用某个参数化分布完整刻画。你估计的概率分布稍微偏一点优化结果可能比确定性优化还不靠谱。鲁棒优化走的是第三条路我不赌概率我就假设风光出力落在某个集合里然后保证无论集合里哪个场景出现系统都能安全运行。这样做出来的配置会主动往“装备用、多装储能”的方向倾斜看似“保守”了一点换来的是系统最恶劣情况下的安全性。这种取舍让很多人纠结。后面在算例里我会专门对比同一套数据下“确定解”和“鲁棒解”的差别你看完就明白那点“保守”到底值不值。2. 两阶段鲁棒优化的数学框架与模型构建2.1 三层结构的物理含义两阶段鲁棒优化的标准形式可以写为$$\min_{x} ; c^T x \max_{u \in U} \min_{y \in F(x,u)} b^T y$$这个公式看起来吓人拆开看其实不复杂。外层最小化是第一阶段决策对应变量$x$一般就是各类电源的安装容量和投资成本。这一层一旦定下来就要花钱、施工后续好多年都改不了。内层的第二层是一个max-min结构。max是在挑“最恶劣的风光出力场景”min是在这个最恶劣场景下做运行调度目标是最小化运行成本。注意顺序是关键先由大自然或者说对手玩家挑出一个对你最不利的出力值然后你再根据这个值去调整柴油机出力和储能充放电同时保证不切负荷、不越限。你可以把整个过程理解成下棋。你先落子——决定装多少光伏、风机、储能、柴油机。然后你的对手大自然走一步棋——把光伏出力压到最低、把负荷拉高。然后你才根据对手这步棋走第三步——调整储能、启动柴油机、必要时切除部分负荷。整个决策过程是分先后顺序的这就是“两阶段”的由来也直接决定了我们求解时要拆主问题和子问题。2.2 目标函数与约束条件的工程化处理在微网电源配置这个具体问题里第一阶段的目标函数一般写成设备年化投资成本$$C^{inv} \sum_{i} \alpha_i \cdot c_i^{inv} \cdot P_i^{rated}$$其中$i$表示设备类型光伏、风机、储能、柴油机等$c_i^{inv}$是单位容量投资成本$P_i^{rated}$是待优化的安装容量$\alpha_i$是年化因子把一次性投资折算成等年值。这块有一个容易忽略的细节储能和多类设备要乘以一个“容量系数”或者用日功率乘以日时长换算成容量很多初版代码在这里容易混淆单位。我自己第一次写就把储能的容量上限直接当成功率上限在做约束结果一直报无解后来对了一遍量纲才缓过来。运行阶段的约束可以分成几类第一类是功率平衡约束也就是系统任意时刻发电和用电必须相等。这里要注意双向电能流动的符号约定我习惯把储能放电记为正、充电记为负这样功率平衡等式写出来更直观。第二类是设备运行约束。柴油机有出力上下限和爬坡约束储能电池有充放电功率上限和荷电状态SOC的范围光伏和风机在给定出力场景下有个可发功率上限你只能少发不能多发。第三类是网络约束。如果做的是单母线微网只考传输功率上限如果做多节点配电网还要加线路潮流约束。为了简化示例我们一般先做单母线模型网络约束先放一放。这里最体现鲁棒优化特点的是风光出力$u$不是固定参数而是不确定变量。你需要把它单独剥出来和第一阶段的$x$、第二阶段的$y$分开建模。2.3 不确定集合怎么选不确定集合是鲁棒优化的灵魂也是新手最容易走极端的地方。最粗糙的做法是盒式集合每个不确定量的范围给一个上下界比如光伏出力在预测值的0.3~1.0倍之间波动。这个集合的好处是直观、好写坏处是太悲观——它默认所有不确定量同时取最坏值而真实风光场景里几乎不可能“所有时刻所有电源全部废了”。工程上更合理的是预算不确定性集合budget uncertainty set。它额外加一个约束所有不确定量偏离标准值的“惩罚总量”不能超过一个预定义预算值$\Gamma$。这个$\Gamma$控制了系统对极端场景的容忍程度——$\Gamma$设为0时退化成确定性模型$\Gamma$越大系统越保守。我在实际代码中用的是“光伏、风电联合出力偏差的预算约束”$$\sum_{t} \frac{|u_t - \hat{u}_t|}{\Delta u_t} \leq \Gamma$$这里的$\hat{u}_t$是预测出力$\Delta u_t$是允许偏差范围$\Gamma$控制所有时段里累计的总偏差不能超过多少。调试$\Gamma$的过程其实挺有意思——改一个数看最优容量组合怎么变这就是后面要专门讲“鲁棒性与经济性权衡曲线”的基础。这个集合还有一个好处它不会让模型太悲观也不会让计算量爆炸。对每个不确定量引入一个辅助变量加一组线性约束就行不破坏模型的MILP结构。2.4 从三层结构到可直接求解的两层结构min-max-min模型不能直接丢给MILP求解器解所以必须对它做转换。经典转换策略有两个一个是基于强对偶的对偶变换把内层的min替换成其对偶问题的max再把两层max合并成一层max原来的min-max-min就会变成一个min-max。好消息是这个max部分对第二个阶段变量不是独立的还可以把对偶后的max-max合并成一个max整体就变成单层的min-max问题可以用割平面法或行与约束生成法CCG迭代求解。具体到实现CCG的全过程是第一步给定第一阶段变量$x^*$代入第二层问题求解$$\max_{u \in U} \min_{y \in F(x^*,u)} b^T y$$这个子问题会同时告诉你两件事最恶劣的场景$u^$以及对应该场景下的运行成本$b^T y^$。注意这个子问题本身还是一个max-min嵌套结构要把它落地求解需要再写一层对偶变换。第二步取在最恶劣场景$u^$下新增的运行约束添回到第一阶段的主问题中主问题再重新算新的$x^$。每迭代一轮就是在逼着第一阶段的主问题面对一个更严苛的场景。第三步循环以上两步直到子问题目标值和主问题目标值之差小于收敛阈值比如$10^{-4}$。次优间隙越小说明你找到的最优配置越接近真实最优解。这套算法的收敛性在文献里证明得很充分实测下来通常需要5~15轮迭代即可收敛具体迭代次数取决于不确定集合的大小和算例规模。CCG相比老版的Benders分解实现起来直观得多因为Benders分解每次只给主问题加一个约束割平面而CCG会一次性加入一整套新场景的完整运行约束。对于容量配置这种每个场景本身就是一个大规模运行子问题的情况CCG的收敛效率明显高不少。3. MATLAB代码实现与核心细节拆解3.1 代码架构总览与YALMIP工具箱选择很多老派工程师喜欢纯手写对偶求解但我不推荐。这个优化问题涉及大量的矩阵运算、对偶推导、不确定集合建模手写代码容易受人一不小心的下标错乱影响而且改条件非常难受。我建议用MATLAB下的YALMIP工具箱做高层的优化建模求解器用CPLEX或者Gurobi二选一建议优先Gurobi因为它对MILP的启发式效果更省时间。整个代码架构大概分成几个模块% 数据输入模块 % 负荷曲线曲线、风光出力标幺值序列、设备参数、价格参数 % 注意这里预测出力先按0.5小时间隔给出全年8760个点但求解时 % 不是直接跑全年而是抽取典型日/小时代表场景数据 % 参数设置模块 % 不确定预算Gamma、收敛阈值epsilon、最大迭代次数Kmax % 主问题循环模块 % 每次迭代先求解主问题MILP得到第一阶段决策x_k % 然后将上一轮产生的恶劣场景约束加入主问题 % 子问题求解模块 % 固定x_k求解max-min子问题得到最恶劣u_k % 同时返回对偶信息用于生成新约束 % 结果输出模块 % 容量配置结果、迭代间隙曲线、运行成本分解流程上最重要的是CCG迭代循环所以总控脚本最好只做一个循环核心逻辑放在两个独立的函数文件里一个是主问题一个是子问题。调试时你可以单独调用子问题函数传入特定场景看看调度结果对不对。3.2 主问题建模如何正确加入场景约束主问题的目标是最小化投资成本加上一个辅助变量$\eta$这个$\eta$代表“面对最恶劣场景时系统能接受的运行成本上限”。在第一次迭代时这个$\eta$可以设一个很大的初值比如$10^6$后面每迭代一轮就往主问题里加一个场景的完整约束同时加一个形如$b^T y_k \le \eta$的约束来收紧$\eta$的上界。用YALMIP建模的时候YALMIP把复杂的矩阵细节都封装掉了你可以直接用变量名写线性约束表达式代码几乎和论文里给的数学式子一一对应。主问题的关键代码片段长这样% x: 决策变量各设备容量 x sdpvar(1, n_devices_unit, full); % eta: 辅助变量表示最恶劣场景下的运行成本 eta sdpvar(1, 1); % 目标函数年化投资成本 eta objective cost_investment * x eta; % 基本容量约束 Constraints [x x_lb, x x_ub]; % 每一轮迭代产生的恶劣场景约束会被循环追加进Constraints for k 1:K_current % y_k是第k个场景下的运行变量 Constraints [Constraints, A_operation * y_k B_operation * u_k]; Constraints [Constraints, cost_operation * y_k eta]; end这个追加约束的做法是有讲究的。你去查文献会发现有的论文叫它“列与约束生成”或者“动态场景库方法”本质就是让主问题的可行域随着迭代不断缩小最终逼近真实的最优解。务必记住一点每次迭代不需要重新定义变量而是复用一个“场景索引”来区分哪些约束属于哪个场景。这样MATLAB的数据结构清晰后面并行化也容易。3.3 子问题求解把max-min转成MILP子问题是整个代码的难点。需要用强对偶把内层min替换掉然后和外层max合并成一个max问题。这个对偶推倒在论文里一般占半页纸但实操时有一个省力的做法——直接用YALMIP的dual函数自动从求解器拿对偶乘子。子问题的目标是最优化“在最恶劣场景下的运行成本”。YALMIP里modeldual的写法是这样的% 固定x_k后新建场景变量u u sdpvar(n_u, 1, full); % 不确定集合约束 U_Constraints [u u_lb, u u_ub, sum(abs(u - u_hat) ./ delta_u) Gamma]; % 运行约束 Op_Constraints [A_op * y B_op * [u; x_k], y_lb y y_ub]; % 子问题目标是最小化运行成本但这里是max-min结构 % 需要先对min部分做对偶再与max合并 % 具体的对偶推导这里不展开可以直接用YALMIP辅助 Objective_sub cost_op * y; % 对偶化处理示意 [dual_obj, dual_cons, dual_vals] dualize(Objective_sub, Op_Constraints);这里要注意并不是所有约束都做对偶而是只对与运行调度相关的连续性约束做对偶。如果运行子问题里含二进制变量比如机组启停状态那么强对偶就不成立了。实际工程中有个常见妥协我把机组启停变量放开为连续变量约束其出力范围在0和额定值之间用“功率可调范围”近似启动能力。这样做误差在大规模容量规划里是可接受的因为容量的时间尺度以年为单位机组每小时怎么开的细节没必要刻画到那么细。另一种办法是把子问题拆成两层外层max遍历一组预先生成的场景全集内层min对每个场景独立求解——这其实就是枚举法。如果场景数量多比如几百上千个那速度会非常慢。实测下来还是对偶法子问题实用。3.4 CCG迭代循环与收敛判据迭代循环的逻辑其实简洁真正的坑都在细节里。我直接给一个能跑通的循环骨架% 初始化 x_k initial_guess; % 初始可行解 LB -inf; % 下界 UB inf; % 上界 K 0; % 迭代次数 while (UB - LB) / abs(LB) 1e-4 K Kmax % Step 1: 求解子问题获得最恶劣场景 u* [u_star, sub_obj] solve_subproblem(x_k); % 更新上界当前市场配置的投资成本 最恶劣运行成本 UB min(UB, investment_cost(x_k) sub_obj); % Step 2: 把新场景加入主问题重新求解 K K 1; [x_new, main_obj, eta_val] solve_mainproblem(K, u_star); % 更新下界主问题目标值是真正的下界 LB max(LB, main_obj); % Step 3: 判断收敛 if abs(UB - LB) / abs(LB) 1e-4 break; end x_k x_new; end注意上界的更新方式。主问题给出的目标值是松弛的下界因为$\eta$在约束不完整时被放得太松。而把当前$x_k$代入子问题得到的“投资成本最恶劣运行场景总成本”是实际可行方案的真实成本可作为上界。两者只差收敛间隙。你要牢牢记住子问题目标值要加上第一阶段的投资成本才是完整的上界。这个细节我在第一次写代码的时候犯了错只拿了子问题的运行成本去和主问题比结果迭代七八轮都不收敛还一直以为是对偶出了bug。4. 实例算例设计与结果分析4.1 基础数据与参数设定这里我用一个简化但五脏俱全的微网来做演示。假设系统包含光伏PV、风机WT、储能ESS、柴油机DE供给一个日峰值120kW的工业园区负荷。设备参数按常见工程经验设定忽略安装费用差异统一按容量折算投资成本| 设备类型 | 单位投资成本元/kW或元/kWh | 年化因子 | 运行成本元/kWh | 容量范围 | | 光伏 | 3500 元/kW | 0.08 | 0.01 | 0~150 kW | | 风机 | 6000 元/kW | 0.08 | 0.01 | 0~100 kW | | 储能 | 1800 元/kWh | 0.10 | 0.02含充放损耗 | 0~300 kWh | | 柴油机 | 1200 元/kW | 0.10 | 0.65 燃料成本 | 0~80 kW |负荷曲线采用某园区实测数据归一化后的典型日曲线光伏出力按当地纬度与气象数据生成标幺值序列风机出力按威布尔分布风速拟合换算。重点参数是负荷时序分辨率和求解时长。我建议先用24点的日前调度尺度跑通整个流程最大迭代次数设10次以内。一开始就用8760小时跑全年只会让代码跑一个通宵还找不出bug在哪。4.2 两阶段鲁棒优化结果与确定性结果对比先看确定性方案的结果在预测出力完全准确的前提下最优配置是光伏105kW、风机35kW、储能150kWh、柴油机20kW年总成本约28.6万元。然后再看鲁棒优化取$\Gamma8$的结果最优配置变为光伏90kW、风机40kW、储能220kWh、柴油机30kW年总成本约33.2万元。对比就非常直观了。确定性方案省了约16%的成本代价是系统面对连续阴雨天基本没有抵抗能力。把不确定性场景代入确定性方案去校核会发现失负荷时间LOLH达到每年37小时这已经超过很多微网合同的允许上限了。鲁棒方案虽然前期多投了钱但它最恶劣情况下依旧能保住全部负荷电动汽车充电、数据中心服务器这些敏感负荷不会被波及。用了多少额外成本换多少可靠性一目了然。这个就是鲁棒优化的精髓——它不给你“最优期望收益”它给你的是一个确保“不会出事的方案”。工程规划上后者的价值通常远高于那省下的百分之十几的账面投资。4.3 不确定预算对配置方案的影响我把不确定预算$\Gamma$从0逐步增加到24每档重新跑一遍完整CCG得到一组很有信息量的结果。当$\Gamma0$时对应确定性解光伏比例最大储能最小。随着$\Gamma$增大光伏容量缓慢下降风机持平或略升储能容量明显爬升柴油机在某个拐点后也开始增加。原因是恶劣场景下光伏出力几乎为零储能成了调节主力。同时留意到年总成本曲线呈阶梯状上升并非线性——每增加1单位预算前期成本增速较慢到后期快速攀升。这说明鲁棒性并不便宜越接近“绝对不出事”的边界边际代价越高。实际操作时你可以绘制这样一条“鲁棒性与经济性权衡曲线”方便决策者直接读取如果预算一共就这么多系统允许最大失负荷多少小时那就选对应预算阈值的配置方案。这个图表通常也是论文里最容易被审稿人表扬的第一张图。4.4 迭代收敛过程记录以$\Gamma8$的这组参数为例完整迭代记录如下| 迭代次数 | 主问题目标下界 | 子问题上界 | 相对间隙 | | 1 | 18.3 万 | 46.7 万 | 60.8% | | 3 | 29.4 万 | 38.5 万 | 23.6% | | 5 | 32.5 万 | 35.2 万 | 7.7% | | 7 | 33.0 万 | 33.5 万 | 1.5% | | 9 | 33.1 万 | 33.3 万 | 0.6% | | 11 | 33.2 万 | 33.2 万 | 0.01% |可以看到前期间隙下降很快到后期收敛速度放缓。这里需要强调一个工程经验没必要把间隙压到0.1%以下收效甚微。规划问题对未来很多变量预测本身就带有误差追求百分之零点零几的精确解意义不大。我在实际项目里经常把间隙阈值设在1%左右求解时间往往能压缩到原来的三分之一。5. 常见问题与调试实录5.1 子问题对偶失败或数值报错遇到子问题里的对偶变量报错最常见的原因是原问题不可行。产生不可行的原因通常是固定$x_k$后柴油机出力上限不够满足最恶劣场景下的功率缺额。这种情形下鲁棒模型会告诉你“该配置不能满足所有场景”从算法角度反而好办——它说明当前场景约束超出了系统能力范围。调试时先检查矩约束和变量上下界是否有冲突再用Gurobi或CPLEX各自的诊断工具看当前不可行子集irreducible inconsistent subsystemIIS输出快速锁定是哪一条约束出问题。5.2 迭代不收敛或间隙震荡如果迭代五六轮后间隙还在10%以上来回摆我建议优先检查子问题里不确定变量$u$的取值是否真的达到了集合边界。CCG要求子问题每次都必须返回一个极值场景如果最优解落在集合内部说明对偶变量对应值没有暴露最恶劣情况。可能的坑是预算约束写成了$\le$而不是$$导致解倾向于集合中心。另一个常见问题是目标函数里忘记把运行子问题中的固定成本项剔除造成上下界口径不一致。5.3 MATLAB内存溢出与求解时间过长容量配置问题如果直接拿全年8760小时跑来建模型内存绝对会炸。我的做法是先做场景削减把8760个小时的序列用k-means聚类法或者相似日法压缩到4~12个典型日再用这些典型日参与优化。单母线模型加上30个典型日规模不大CCG每一轮主问题大概就是一两千个约束加几十个整型变量的规模Gurobi单轮几十秒内一般能解完。如果是配网模型节点多了就要考虑使用分段线性近似压缩输电约束这都是实操经验了。5.4 工具箱安装与许可问题YALMIP本身免费但底层求解器有重的license问题。我建议优先试Gurobi学术版免费且MILP性能最好。如果学校没买CPLEX的社区版在功能上稍有阉割但对于几百个整型变量的规模完全够用。注意版本兼容MATLAB R2023b和YALMIP的最新release没问题求解器接口之间偶尔因Java版本导致启动报错碰到就先重启MATLAB再试一次。这里补充一个特定注意点在调试代码前先把YALMIP自带的testsalt跑一遍确保环境正常。很多“代码没毛病但报错奇怪”的问题其实都是环境配置问题不必浪费时间去挖业务逻辑bug。6. 核心实操经验与扩展思考6.1 几个必须死记的建模习惯做鲁棒优化建模三年多踩过的坑大部分都能归到下面这几条第一所有变量建议显式声明上下界不要依赖YALMIP自动推断边界。否则对偶变换时容易多出不可控的自由度求解器在内部处理时会给出无用解。第二阶段区分要彻底。第一阶段变量在子问题中要当常数处理贴matlab代码时可以在函数间以参数形式传入不要在主问题函数里又去声明它。第三计价时注意年化因子换算。投资费用是一次性的而运行费用是逐年发生的两者不能直接相加必须统一成等年值再加上运行时间系数。这个坑几乎没有新手不踩。第四不确定集合的尺寸不要盲目扩大。集合越大主问题和子问题的规模增长是非线性的。预算参数从10调到15你可能觉得只是更严格一点但实际运行时间可能会翻倍。6.2 从容量配置扩展到更实际的问题这套两阶段鲁棒代码不是只解决“微网电源容量”这一个问题的。换一下约束和变量含义它能直接迁移到以下几类场景光储充一体化充电站规划第一阶段决定储能和充电桩数量第二阶段应对的是无序充电负荷不确定性和光伏波动。微网-主网交互规划增加与上级电网购售电交互约束第一阶段决定联络线容量第二阶段优化购售电策略。综合能源系统容量规划把电负荷扩展成冷热电联供加入燃气轮机、余热锅炉等模型第二阶段要处理多能耦合的调度问题。本质上只要问题结构能写成“初始投资决策 不确定性场景下的运行响应”嵌套CCG框架都能套上去。改模型时最需要注意的是新增设备会不会引入二进制状态变量如果引入强对偶失效就需要考虑加线性化辅助或者启发式近似处理。我个人在实际操作中的体会是两阶段鲁棒优化最难的不是算法推导而是把工程问题里的各种运行约束干干净净地翻译成数学约束并在代码里正确编码。你多写几十行约束容易但错一个符号、漏一个边界条件求解器跑出来的结果就可能完全不是你想要的意思。翻译这个环节多花点时间比反复调bug高效得多。这个内容后续还可以这样扩展把单一不确定预算换成更精细的时空相关不确定集合或者用分布鲁棒优化distributionally robust optimization结合历史数据拟合模糊集。前者提升场景真实性后者鲁棒性和经济性的平衡通常更好。两条路线都不需要颠覆现有CCG框架离真正工程实用就更近一步。