两阶段微电网鲁棒优化这套东西,这几年做微电网调度、储能配置、园区综合能源的人基本都绕不开。坦白说,刚接触“两阶段+鲁棒”的组合时挺容易懵——每个词都认识,合在一起就不知道代码该怎么落笔,更别说把min-max-min这种三层结构跑通了。这篇博客我打算把两阶段微电网鲁棒优化的代码实现从头到尾拆一遍,讲清楚数学模型是怎么来的、CCG算法在代码层面到底怎么迭代、子问题对偶那一步的坑在哪,最后再分享一些我实际调试时踩过的雷。无论你是刚上手鲁棒优化的小白,还是已经调过YALMIP/Gurobi但被数值问题卡住的老手,这篇都应该能给你些参考。
1. 两阶段鲁棒优化:在“最坏情况”里找最优解
1.1 为什么是两阶段:先拍板后调整的天然结构
微电网调度的实际业务场景里,决策天然就分两步。第一阶段的决策是在不确定量还没实现之前就必须敲定的,比如机组启停、是否从主网购电、储能是否参与调峰这类“今天定下来明天才能执行”的硬性安排。第二阶段的决策则是等风光出力、负荷这些不确定量落地之后,再根据实际情况做出的调整,比如柴油机具体发多少、储能充放电功率怎么调、联络线功率怎么修正。
把这种时序关系抽象成数学语言,就是两阶段随机优化或鲁棒优化里的经典结构:第一阶段决策变量用x表示,第二阶段决策变量用y表示,目标是最小化第一阶段的成本加上第二阶段在最坏情况下的运行成本。这里的“两阶段”不是算法上的两轮迭代,而是决策逻辑上的两个层级。搞懂这一点,代码才不会写歪——很多新手一上来就想着“两阶段是不是要循环两次”,其实完全不是一回事。
我常用一个生活化类比来理解:就像你出门前决定带不带伞(第一阶段决策,此时不知道下不下雨),到了楼下看到天阴了再决定是快步走还是打车(第二阶段决策,此时天气已经“实现”了)。两阶段鲁棒优化就是在“最坏天气”假设下,同时优化带伞和后续应对的总成本。这个类比虽然简单,但能很直观地解释为什么目标函数里会出现max-min嵌套:天气要跟你对着干(max),而你又会在已知天气的前提下理性应对(min)。
1.2 从随机优化到鲁棒优化:不确定集合的选择逻辑
既然要处理不确定性,第一个绕不开的问题就是:为什么选鲁棒优化,而不是更传统的随机优化(stochastic optimization)?
随机优化的思路是给不确定量赋予概率分布,然后用场景抽样或解析方法求期望成本最小化。它的优点是结果在经济性上更贴近现实,缺点是计算量大,而且需要足够可靠的分布信息。问题是,微电网里光伏出力、风速、负荷的分布信息往往洗不准,尤其是历史数据不足的新建园区或者极端天气频发的区域,概率分布本身就不靠谱。这时候用随机优化,容易出现“对概率分布敏感、对尾部风险忽视”的问题。
鲁棒优化则换了个思路:我不去猜概率分布,而是给不确定量圈一个范围——也就是不确定集合——然后在范围内找最坏情况的方案。这种“在坏天气里买菜”的思路,得到的解天然就带了抗风险能力。代价是结果偏保守,因为你在为最坏情况买单。为了缓解这种过度保守,实际工程里常用盒式不确定集加预算约束(budget constraint),相当于告诉模型:“最坏的情况不可能所有节点同时发生,最多同时出现K个极端偏差。”这样既保留了鲁棒性,又不至于把成本推到离谱的水平。
在这里我把不确定集合的几种常见建模方式放一起对比一下,方便新手选型:
| 集合类型 | 数学表达 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 盒式集合 | |u-u0|≤Δ | 最简单,求解快 | 过于保守,所有变量同时取极端 | 初步验证、保守规划 |
| 盒式+预算 | |u-u0|≤Δ, |Σ(u-u0)/Δ|≤Γ | 控制保守程度 | 需要调参Γ | 工程实际最常用 |
| 椭球集合 | (u-u0)ᵀΣ⁻¹(u-u0)≤ρ | 更精细,可对应正态分布 | 非线性约束,求解难度大 | 数据充足、概率信息可靠 |
| 数据驱动/多面体 | u∈conv(历史场景) | 贴合实际数据 | 集合构建复杂 | 有海量历史数据 |
我做微电网项目时,90%的场景都用“盒式+预算”这个组合。原因很简单:它能把鲁棒模型保持在线性规划(LP)或混合整数线性规划(MILP)框架内,Gurobi和Cplex可以高效求解。而椭球集合一引入,问题就变成二阶锥规划(SOCP),虽然也能解,但数值稳定性差不少,代码调试成本直线上升。
1.3 三层min-max-min结构:把模型拆开看
两阶段鲁棒优化的核心结构是一个三层嵌套:外层min是第一阶段的成本最小化,内层max是自然界/不确定性在跟你博弈,最内层min是第二阶段的运行优化。连起来就是:
minₓ (cᵀx + max_u min_y dᵀy)
这个结构刚看确实反直觉——怎么优化问题里面还能套一个“对手”?其实这就是博弈论里Stackelberg博弈的数学表达:你先决策,然后大自然(或者市场、或者故障)在你决策之后选择最不利于你的场景,你再在这个场景下做出最优响应。是的,你在和最坏情况“下棋”。
理解这个结构对代码调试极其重要。因为实际写代码时,你没法直接把这个三层结构扔给求解器一次解得——任何商业求解器都处理不了内嵌max-min这样的“非标准形式”。所以你需要在算法层面把它拆解成一个迭代过程。最常见的做法就是CCG(列约束生成)算法:把原问题拆成主问题和子问题,主问题想办法最小化成本,子问题则负责在当前方案下找到最恶劣的场景。
我在自己的代码里,习惯先把这个三层结构用注释写在文件头部,再开始写代码。别小看这一步,三个月后回来看代码,能帮你省下大量回忆时间。
2. 数学模型搭建:目标函数与约束的细节
2.1 决策变量怎么划分:先分清哪些是x哪些是y
动手写代码前,第一件事就是把变量分门别类。我在实际代码里是这样划分的:
第一阶段变量x通常包括:
- 微型燃气轮机或柴油发电机的启停状态(01整数变量)
- 与主网交互的购售电状态(01整数变量,很多简化模型里也会省略)
- 储能是否参与调度的模式选择(如果你做的是日前级决策)
- 有些模型里还包括电容器投切、变压器分接头位置这类离散控制量
第二阶段变量y通常包括:
- 各分布式电源的有功无功出力(连续变量)
- 储能充放电功率(连续变量,可能还有充放电状态01变量)
- 弃风弃光量(连续变量)
- 切负荷量(连续变量,作为惩罚项兜底)
- 联络线交换功率(连续变量)
为什么要严格区分?因为在CCG算法里,主问题只优化x和一部分y(针对已经生成的场景),子问题则是在固定x的前提下优化y并寻找最坏u。如果变量划分不清,后面写对偶或者KKT条件时十有八九会出错。
2.2 目标函数:投资成本和运行成本怎么平衡
两阶段鲁棒优化的目标函数一般长这样:
min [ cᵀx + max_u min_y (dᵀy + penalty) ]
第一阶段cᵀx如果做的是日前调度,一般就是机组的启动成本、停机成本和固定运行成本;如果做的是规划问题,那就变成设备投资成本的年化值。第二阶段dᵀy是运行成本,包括燃料成本、购电成本、运维成本、弃风弃光惩罚、切负荷惩罚等。
实际工程里我特别提醒一点:第二阶段的罚函数系数不能拍脑袋定。如果惩罚系数设置得太小,模型可能在最坏场景下选择弃风或者切负荷来“保成本”,这在物理上不合理——因为在实际运行中,安全性和供电可靠性是硬约束。如果罚系数设得太大,又会造成数值病态,影响求解精度。我一般会用一个简单粗暴的校验方法:把惩罚系数调到正常运行成本的10倍以上,再观察最优解中切负荷量是否为0。如果为0,说明系数足够大,场景“正常”,如果还有切负荷,就得检查约束是否写错了。
2.3 约束条件:功率平衡、机组出力、储能SOC与线路潮流
约束条件按物理环节来拆,大致就这几类:
功率平衡约束是微电网模型的地基。母线功率平衡要求所有电源出力加上购电功率等于负荷加上售电功率,如果考虑损耗还需要加网损项。网损项如果是非线性函数,可以先用线性近似处理,不然模型直接变成非线性规划,求解困难指数级上升。
机组约束里最核心的是出力上下限约束和爬坡约束。出力上下限很简单,但爬坡约束容易被新手忽略——它把相邻两个时段的出力变化限制在一定范围内,这是保证物理可实现性的关键。储能的约束稍微复杂一点:除了充放电功率上下限,还有SOC递推方程和SOC上下限,如果模型还要求储能不能同时充放,就得引入01变量。初学者可以先忽略同时充放约束,用纯连续变量跑通,后面再逐步加复杂度。
不确定变量通过不确定集合进入模型,通常是用光伏出力实际值 = 预测值 + 偏差这一形式,把u嵌入功率平衡方程。这也是鲁棒模型里不确定性与决策变量“耦合”的具体方式。
3. CCG算法与代码实现:主问题子问题迭代怎么落地
3.1 CCG vs Benders:为什么我最终选了CCG
解两阶段鲁棒优化,学术界和工业界的主流算法有两个:Benders分解和列约束生成(CCG)。早期教材喜欢用Benders,因为它在随机优化里根深蒂固,理论上也更成熟。但我在实际项目里更推荐CCG,原因就一条:收敛速度。
CCG在每一轮迭代时,会向主问题添加一组新的变量(也就是新场景下的第二阶段决策变量y),以及一组对应的约束(也就是这个场景下的运行约束)。随着迭代进行,主问题的规模不断变大,但每一轮添加的量是可控的。Benders则是向主问题添加割平面约束(Benders cut),切的是对偶空间,从理论上讲,收敛需要的迭代次数往往远多于CCG。
说句扎心的话,我在一个中等规模微电网算例上做过对比,CCG大概10轮以内收敛,Benders要跑四五十轮甚至更多。所以在实际工程里我几乎无脑选CCG,尤其是当你用Gurobi这种高性能求解器作为底层时,CCG的“每轮多算一点”策略执行效率远高于Benders的“每轮切一刀”。
3.2 主问题MP和子问题SP:代码框架先立起来
先直接上这类项目的代码框架逻辑,我用类Python伪代码来写,但重点不在具体语法,而是整体结构:
主问题MP在每一轮迭代时面临的输入是:已经找到的一系列最坏场景u*(初始会给定一个预测场景),在这些场景下,同时优化x和每个场景对应的y,并把总成本下界不断收紧。伪代码是:
初始化: 设置UB = +∞, LB = -∞, iter = 0 选取初始最坏场景 u0(通常取预测值) LB 的初始值:求解主问题(只含u0),得到x* while UB - LB > epsilon: # 1. 求解主问题MP MP输入: 已发现的场景集合 {u1, u2, ..., uk} 求解 min 目标函数 得到: 第一阶段最优解 x*, 目标值 η* LB = max(LB, η*) # 2. 固定x=x*,求解子问题SP SP输入: 第一阶段决策 x* 求解 max_u min_y (运行成本) 得到: 最坏场景 u_new, 子问题目标值 f_SP # 3. 更新上界并判断收敛 UB = min(UB, cᵀx* + f_SP) if UB - LB < epsilon: break # 4. 如果没收敛,把新场景u_new加进主问题的场景集合,跳回步骤1 MP添加新场景u_new对应的变量和约束 iter += 1这个框架几乎是所有两阶段鲁棒优化代码的通用骨架。我第一次实现CCG时就是照这个逻辑搭的,后面换不同的研究对象——从微电网到综合能源系统再到配电网——框架都没变过,变的只是具体的约束表达式。
3.3 子问题怎么对偶:从max-min到单层max的关键一跃
整个CCG代码里最核心,也是最多人卡住的地方,就是子问题怎么求解。
子问题本身是max_u min_y,这是一个双层优化问题,没法直接用求解器求解。一个标准做法是:固定u后,内层min_y是一个线性规划(LP),既然它是LP,就可以用强对偶定理将其转化为对偶问题,把“min”变成“max”。这样一来,max_u min_y就变成了max_u max_λ(λ是对偶变量)= 一个单层max问题。因为max和max嵌套可以直接合并成同一个max,子问题就变成了一个单层的、带双线性项(u乘λ)的优化问题。
这个双线性项怎么处理?两种思路:
第一种:如果u是连续变量,双线性项会让子问题变成非凸问题,求解困难大。这时候需要用线性化技巧,比如大M法把u的取离散化。但更常见的工程做法是把u建模成0-1变量(即不确定变量只取其上下界或预算组合对应的极端值),这样双线性项u·λ可以精确线性化——引入辅助变量和M,替换为非线性的u乘λ。这其实是鲁棒优化里一个经典的“捷径”:既然最坏场景往往出现在不确定集合的顶点,我可以直接把u限制为顶点变量(01变量),从而规避连续双线性问题。
第二种:用KKT条件替换内层min问题。把内层min的KKT条件作为约束加入子问题,同时把目标函数从min改成max下的目标,配合互补松弛条件线性化,也能把子问题转化为MILP。这条路子更通用,但实现复杂,尤其是互补松弛条件那一步容易出错。
实际写代码时,我两种都试过。对偶法在变量规模小的时候效率更高,代码也更好读;KKT法在约束复杂、强对偶需要附加条件时更稳。但新手我建议先用对偶法,代码量小,出错调试起来方便。
3.4 子问题对偶代码示例:一个简化版的功率平衡模型
这里我给出一个极度简化的子问题对偶代码思路,把核心逻辑讲透。
假设子问题模型(第二阶段)只包含一个约束——功率平衡:
min_y Σc_i·y_i s.t. Σy_i = u_load - u_pv (对偶变量λ) y_i ≤ y_max y_i ≥ 0
对偶问题变成:
max_λ,π [ λ·(u_load - u_pv) - Σπ_i·y_max ] s.t. c_i + π_i ≥ λ π_i ≥ 0 λ free
这里π对应上限约束的乘子。然后把u建模成0-1顶点变量:u_load = u_load0 + Δ_load·z_load,u_pv = u_pv0 - Δ_pv·z_pv。目标函数里会出现λ·z_load和λ·z_pv这种双线性项。用一个辅助变量r替换λ·z(z为0-1),加上约束:
r ≤ M·z r ≤ λ + M·(1-z) r ≥ λ - M·(1-z) r ≥ -M·z
就能把双线性项精确线性化。把这个线性化后的MILP丢给Gurobi,子问题就解完了。
这段逻辑几乎是所有两阶段鲁棒优化代码的“灵魂”。一定要亲手推一遍对偶过程,而不是直接抄代码——因为模型一换,对偶形式就变,抄代码改起来太痛苦了。
3.5 迭代细节与收敛判断:别等UB=LB才停
CCG的收敛判断有个实操心得:不用等UB和LB完全相等再停,误差容限设成相对值就行。我一般用:
UB - LB ≤ ε·|UB|
其中ε通常取0.01或者0.005。因为在实际求解中,UB和LB在小数点后三四位抖动是正常现象,硬等到完全相等,可能要多迭代三分之一轮次,收益却微乎其微。
另外还有一个值得注意的细节:当主问题添加新场景后,主问题的目标值会上升(因为更多的约束限制了可行域),而子问题在当前x下的目标值可能比上一轮更大(因为u搜索到了更恶劣的场景)。UB取的是“本轮cᵀx + 本轮f_SP”和历史UB的最小值,LB取的是主问题目标值的最大值,这个趋势是对的。如果你调试时发现LB比UB还大,那一定是代码逻辑错了——我碰到过好多次,最后发现是主问题里没有把历史场景全部保留导致的问题。
4. 实操中的坑:数值稳定性、大M参数与不可行子问题
4.1 大M怎么取:拍脑袋的代价
大M法在鲁棒优化代码里无处不在:线性化双线性项要用,处理逻辑约束也要用。M取太小,会剪掉实际可行的解,导致模型失真;M取太大,会造成数值病态,Gurobi报numerical trouble,解出来的结果你自己都不敢信。
我常用的经验是:针对具体约束取一个“物理上合理上界”的M值。比如子问题对偶后出现的λ是功率平衡约束的对偶乘子,它的物理含义是边际电价,那M就可以取成电价合理上界的10倍——比如取5000到10000元/MWh,而不是随手写个1e9。这样既保证了线性化约束有效,又不至于让求解器在巨大数值尺度里“迷路”。同样的道理,储能SOC递推公式里如果用大M处理充放电状态约束,就按储能容量和最大充放电功率来估算M的范围。
4.2 不可行子问题:最容易被忽视的“隐藏炸弹”
CCG迭代过程中,子问题偶尔会报不可行。新手第一反应是“我的约束写错了吧”,但我告诉你,很多时候是主问题求出的x方案在当前“最坏场景”下根本无法满足功率平衡约束,也就是说,第一阶段决策在第二阶段的可行域里没有可行解。这在实际物理问题里对应的情况可能是:机组启停方案在极端场景下无法保证供电,需要切负荷。
解决思路有一个很成熟的做法:在子问题中引入松弛变量(比如切负荷量、弃风量),并在子问题目标中加入惩罚项。这样即使子问题在极端场景下物理不可行,也不会直接报错,而是会给出一个“带惩罚”的目标值,CCG仍然可以继续迭代。最终生成的方案会自动规避那些在极端场景下需要大量切负荷的方案。
这个经验是我在一次做微电网孤岛运行场景时踩坑得来的。当时子问题一遇到光伏出力为零的场景就报infeasible,排查了半天才发现不是约束写错,而是柴油机爬坡约束让它在极端场景下带不起负荷。引入切负荷松弛变量后,迭代顺利跑通,最终方案也会自动把机组容量和爬坡能力“留有余量”。
4.3 对偶还是KKT:两条路线的实战对比
子问题从max-min转成单层max,有对偶和KKT两条路线。这里把我的实战对比写出来:
对偶法的核心是把内层min问题替换成其对偶问题,然后用强对偶等式把内层目标值与原目标关联起来。优点是代码短,约束数量少;缺点是对偶形式推导容易出错,尤其是模型比较复杂时——相位考虑、储能状态、多母线功率平衡,对偶变量会非常多,容易漏项或错位。
KKT法则更机械化,每一步都有公式可循,不容易漏,但代码里会多出一堆互补松弛约束,这些约束都要用大M法线性化,直接导致模型规模膨胀,求解速度变慢。在小规模算例中感觉不明显,但如果你的微电网有几十个节点、上百个设备,KKT法可能比对偶法慢一个量级。
我的建议是:子问题结构比较规整(比如只有功率平衡和设备上下限约束)时果断用对偶法;如果子问题里包含了复杂非线性段(比如AC潮流、网损、电压约束)导致强对偶不成立,那只能走KKT加线性化路线。
4.4 Gurobi/YALMIP联合使用的经验
在MATLAB环境里我习惯用YALMIP封装,求解器用Gurobi或Cplex。YALMIP的优点是建模语法高度贴近数学表达,包含binvar、sdpvar定义变量,写约束时可以直接用“>、<”符号,用optimize求解。CCG的主问题和子问题可以用两个YALMIP模型来写,循环迭代时用assign和value来传初始解,效率也不错。
在Python环境下我推荐用CVXPY搭配Gurobi。CVXPY的语法分层清晰,但是需要注意:CVXPY对整数变量和双线性项的支持并不如YALMIP那么顺手,特别是在子问题对偶时需要用cp.Variable再手动加约束来做线性化,代码量会大一些。我个人更推荐新手在MATLAB/YALMIP环境里跑通第一个两阶段鲁棒优化模型,然后再迁到Python工程环境。不是因为Python不行,而是YALMIP的容错率和对数学表达式的直译能力确实是新手友好很多。
还有一个细节:求解器参数设置。Gurobi默认参数对MILP的求解是够用的,但在我这个场景里,我习惯设置MIPGap为1e-4甚至更低,同时开启NumericalFocus=1。代价是求解时间变长,但换来了更稳定的数值结果。当子问题规模大时,我还会给每一个模型设置TimeLimit,防止某个子问题卡在某个穷举分支里出不来。
5. 常见问题与排查技巧实录
5.1 问题速查表:从症状到根因
我把实现两阶段鲁棒优化过程中最容易碰到的几个典型问题和对应解法整理成了表格,全是实战中踩过的雷:
| 问题 | 典型原因 | 解决思路 |
|---|---|---|
| 子问题报infeasible | 主问题方案在极端场景下无可行解 | 给子问题加松弛变量和惩罚项 |
| CCG迭代不收敛,UB/LB震荡 | 主问题场景集合没有完全保留 | 检查MP中是否添加了所有历史场景的变量和约束 |
| 子问题求解结果出现非零双线性残差 | 大M取值过小 | 按物理上界估算,重新调整M |
| 求解器报警numerical warnings | M取值过大或变量尺度差异悬殊 | 设NumericalFocus=2,把成本单位换成万元/千元级 |
| 对偶问题推导后目标值对不上原LP | 对偶约束里有符号错误 | 用一个小规模LP手动验证对偶等价性 |
| 鲁棒解比确定性解成本高太多 | 不确定集合定义过大或预算Γ取得太大 | 调小Δ或者Γ,或者用历史数据校准集合参数 |
| 第一阶段整数变量连续化后结果差很远 | 01变量松弛成连续变量后可行域扩大 | 保留整数约束,不要轻易用线性松弛近似 |
5.2 调试技巧:从一个残缺模型开始验证
写两阶段鲁棒优化代码,最容易犯的错误是一上来就写全套,然后跑不通不知道怎么查。我的习惯是从“残缺模型”开始验证:先把不确定集合的所有Δ都设成0,让u固定等于预测值。这时两阶段鲁棒优化退化成确定性两阶段优化,CCG第一轮迭代就应该收敛。如果连这一步都跑不对,说明主问题或子问题的建模有问题,先去修基础模型。
当确定性退化版本通过后,再把Δ调成一个很小的值(比如预测值的5%),继续跑。如果CCG能稳定收敛,且结果跟确定性模型相比变化不大,说明实现正确。最后再把Δ调到实际水平,观察解的保守程度是否在合理范围。这套“由简化到复杂”的调试流程,帮我省了无数个排查 bug 的夜晚。
5.3 性能优化:大规模微电网模型的加速技巧
如果你的微电网规模比较大(比如几十个节点、上百个可控设备),CCG每轮迭代都在求解一个大MILP,计算时间会直线上升。我的加速经验有这几条:
第一,热启动。在CCG迭代中,上一轮的主问题解对下一轮主问题是一个很好的初始解。Gurobi可以用其提供的热启动API把上一轮的x值作为MIP start传入,能显著减少求解时间。我自己实测一个中等规模模型,热启动能让每轮MP求解时间减少30%到50%。
第二,子问题场景剪枝。当迭代进行到中后期,后添加的场景往往与已有场景比较相似,对主问题解的改进很小。可以在每轮迭代结束后,检查一下该场景下的对偶乘子值,如果乘子非常小,说明这个场景对目标函数影响微乎其微,可以在下一步考虑不再扩展该场景的变量。不过这个优化实现起来要谨慎,弄不好会破坏收敛性。
第三,求解器参数调优。我一般把Gurobi的MIPFocus设为1(侧重快速找到可行解),这有利于CCG每轮快速得到一个x方案,然后子问题再去判断是否需要生成新场景。如果一味追求每轮MP的全局最优,可能在前期花大量时间在一个还没被恶劣场景约束的主问题上,得不偿失。
6. 项目延展:矿山微电网与工程应用场景
6.1 矿山微电网:典型的鲁棒优化应用场景
这两年矿山微电网的企业和项目特别多,跟矿业领域的电气化转型和绿色矿山政策密切相关。矿山负荷的特殊性在于:采矿设备启停频繁、负荷波动剧烈,而且矿区往往地处偏远,与大电网联结弱,很多矿山实际上处于孤岛运行或者弱联网状态。这种场景下,光伏和储能本来就是天然搭档——矿区阳光充足、空地大,储能则可以缓解采矿设备的冲击性负荷。
但矿山的负荷不确定性比一般园区还难搞。比如大型电铲、破碎机的启停,瞬间功率能拉高好几倍;再加上矿区可能处在高寒、高海拔地区,光伏预测偏差随季节和天气剧烈变化。这种情况下,确定性调度模型很容易在极端场景下“翻车”——我见过不少实际案例,调度的结果在常规场景下非常经济,但遇到连续阴天加采矿高峰同时出现时,系统频率跌得离谱,甚至导致设备保护跳闸。
鲁棒优化在矿山微电网的价值就在这里:它把“连续阴天+负荷高峰”这种组合作为最坏场景纳入了优化过程,得到的调度方案会预留更多的备用容量,确保在极端工况下系统仍然能安全运行。虽然发电成本会比确定性调度高一些,但换来了更高的供电可靠性和更低的设备损坏风险。这两个量级放在矿山上对比,安全性永远是第一位的。
6.2 微电网仿真的工具链与代码生态
围绕微电网仿真做文章,市面上的工具链已经比较成熟了。Matlab/Simulink和OPAL-RT在做电磁暂态仿真上有绝对优势,适合验证控制策略的动态性能;但在做日前调度、经济优化这种时间尺度较长的规划类问题时,这些工具反而不太合适——它们本质上不是为优化问题设计的。我的经验是,用Python或MATLAB+YALMIP做优化决策,先把调度方案算出来,再用Simulink做动态仿真验证这个方案在物理上能不能跑通,两边闭环验证,这样最稳妥。
代码层面这两年也热闹,GitHub上有不少开源的微电网优化仓库,但质量参差不齐。我见过很多仓库给出的两阶段鲁棒优化代码,一跑全是坑:要么子问题对偶推导有问题,要么CCG迭代根本不收敛,要么参数设置全是魔法数字。这也是为什么我一直认为,抄代码不是问题,问题是你有没有能力判断代码写得对不对。我的建议是,拿到任何一份开源代码,先用5.2节说的“退化测试”验证一遍——把不确定集中所有偏差设为0,看看模型是否退化成确定性模型。如果连这个基本测试都过不了,那就别在它基础上改了,直接自己从零写更快。
6.3 从两阶段鲁棒到分布鲁棒:下一步扩展方向
把两阶段鲁棒优化跑通之后,如果还想在这个方向深入,我建议可以往分布鲁棒优化(distributionally robust optimization, DRO)扩展。两者的区别在于,鲁棒优化是在不确定集合内找最坏场景,分布鲁棒则是在一族可能的概率分布中找最坏期望成本。相当于把“最坏天气”升级成“最坏的天气概率模型”。DRO在微电网场景中的应用最近非常热,因为它既保留了对不确定性的抗风险能力,又不像纯鲁棒那样过度保守——在数据驱动的框架下,只需要历史数据的一阶矩和二阶矩信息,就可以构建模糊集。
代码实现上,DRO和两阶段鲁棒在总体框架上几乎一样,区别主要在于子问题的形式从max-min变成了max-期望-min,把期望算子展开后通常需要引入辅助变量和对偶变换。我在一个实际的油田微电网项目里做过DRO和传统鲁棒的对比,结果显示DRO的总成本比纯鲁棒低了差不多8%,而最坏场景下的可靠性指标几乎没有下降。这个8%的提升,对实际运营来说是非常可观的。
我自己把这个从鲁棒到分布鲁棒的演进路线总结成三步走:第一步,用盒式集合鲁棒把两阶段框架跑通,理解CCG的迭代逻辑;第二步,引入预算参数,学习调节保守度;第三步,切换到数据驱动的分布鲁棒,利用历史矩信息做模糊集。走到第三步,你在微电网优化这个细分领域的积累就已经非常扎实了。
6.4 这行做久了,我的几个真实感受
最后说点这行做久了才会有的体会吧。两阶段鲁棒优化代码这件事,技术上并不神秘,核心就那三板斧:模型拆解、对偶变换、迭代求解。但真正拉开差距的,其实是“对物理问题的理解”。
比如你写功率平衡约束时,知不知道哪台机组在实际工程里不能频繁启停?你写爬坡约束时,知不知道实际柴油发电机在60秒内最多能抬多少出力?你写储能SOC递推时,知不知道磷酸铁锂在低温环境下可用容量会缩水?这些物理知识不会出现在任何优化教材里,但恰恰是它们决定了你的模型是否符合工程实际、你的解是否真的能被现场执行。
还有一个体会是,数值稳定性远比大多数人想象的更重要。我在项目初期吃过一次大亏:因为某个大M参数设置得不合理,Gurobi在中间轮次给出了一个数值有瑕疵的可行解,导致CCG多迭代了二十几轮才收敛,而且最终的方案居然比理论最优差了15%。后来我把数值稳定性检查写进了代码的每一轮迭代里,这个坑才彻底填上。做鲁棒优化,你不仅要对算法和模型负责,更要对数值计算过程负责。
两阶段微电网鲁棒优化,说到底是“用计算换确定性”。它换来的不是一个“最优解”,而是一个“在坏情况下也不至于太差”的稳妥方案。对于任何涉及安全性和可靠性的系统,这种思维方式都值得借鉴。代码可以复制,模型可以套用,但对背后的物理原则和计算细节的敬畏,才是这行真正的门槛。