搞过微电网调度优化的朋友应该都有同感:确定性模型算出来的"最优方案"往往只存在于Excel和论文里,真到了实际运行那天,光伏被一片云遮了,风电整夜趴窝,负荷还往上蹿,计划彻底崩盘。这时候你才发现,真正值钱的不是那个所谓的最优解,而是方案在"现实和预测不一致"的时候还能不能扛得住。这就是鲁棒优化存在的意义。
这篇内容围绕“含可再生能源和储能的区域微电网最优运行”展开,重点拆解基于多阶段鲁棒调度模型的MATLAB完整复现过程。适合正在做微电网经济调度、综合能源系统优化的研究生,以及刚接触鲁棒优化、想从公式走向代码的工程技术人员。我会把建模思路、不确定集合设计、CCG算法(列与约束生成)求解流程、MATLAB代码架构和我在复现中踩过的坑一次性讲透,尽量做到直接看、直接抄、直接改。
1. 先弄清楚:这个项目要解决的核心矛盾
1.1 微电网最优运行解决的是成本与安全的两难
区域微电网的典型构成不复杂:光伏、风电这类分布式可再生电源,一组储能,常规负荷,再加上和主网的连接点。运行调度的目标也很朴素——在满足电压、功率平衡、设备容量等约束的前提下,让整个系统一天的运行成本最低。成本包括购电费用、可再生能源运维成本、弃风弃光惩罚,以及极端情况下的切负荷惩罚,如果模型里考虑了电池循环损耗,储能充放电成本也要算进去。
但这里有个天然的矛盾:可再生能源出力有强烈的随机性,光伏夜里出力为零,白天还受云层影响;风电更是时有时无。如果你按预测值去做日前计划,一旦实际光照和风况偏离预测,系统就可能出现功率缺口。缺口小,可以通过储能和主网交互补上;缺口大,就得切负荷。切负荷在技术上意味着供电可靠性下降,在商业上意味着高额赔偿,在工程上属于严重事故。反过来,如果为了应对不确定性预留太多备用,系统又长期运行在保守低效的状态,成本上完全不可接受。
所以“最优运行”这四个字并不是单纯做一个最小化成本的数学问题,而是在成本最优和风险可承受之间找平衡点。确定性优化把未来当作已知,随机优化用概率场景描述未来,鲁棒优化则直接假设未来有一个“最坏情况”,要求调度方案在任意可能的出力区间内都可行、成本上界可控。这个项目标题里同时出现“鲁棒性”和“不确定性”,说明它的定位不是给一个确定性的最优解,而是给一堆不确定性扰动下依然安全的最优策略。
1.2 三种处理不确定性的思路对比
很多刚开始接触这个方向的朋友会混淆三类模型,我直接拿一句话把它们区分清楚:
- 确定性模型:认为光伏预测值就是实际值。优化问题只有一层决策,求解最简单,但方案抗风险能力最弱。
- 随机规划模型:给不确定量设定若干概率场景(比如光照高/中/低三种),优化目标是期望成本最小。问题规模随场景数成倍扩大,而且场景概率往往拍脑袋定,容易失真。
- 鲁棒优化模型:不给概率,只给不确定变量的取值范围(区间),优化目标是让最坏情况下的成本最小。没有分布假设,不需要历史概率数据,计算上通常比大规模随机规划更可控,方案也偏保守。
在工程实践中,我个人的经验是:随机规划适合有充分历史数据的场景,鲁棒优化适合数据少、但安全要求高的场景。微电网恰恰是这两条都占——新能源场站数据质量参差不齐,很多小区域连完整的气象历史都没有,但供电可靠性要求一点不能含糊。所以鲁棒优化在这个领域落地非常自然。
2. 多阶段鲁棒调度的建模思路拆解
2.1 什么叫“多阶段”:从日前计划到实时调整
严格说,电力系统调度天然是分阶段的:日前做启停计划和购电计划,日内根据最新预测滚动修正,实时再根据实际出力微调。鲁棒优化里的“多阶段”对应的就是这个决策时序,而不是简单的“两个阶段”。
经典两阶段鲁棒模型的第一阶段决策,通常指需要提前确定的离散或长期变量,比如机组启停状态、储能的充放电状态;第二阶段决策,是在不确定量(光伏、风电、负荷)实际到来后,对这些预决策做出的经济调整,比如实际购电量、弃电量、储能出力修正。用数学语言说,两阶段鲁棒的一般形式是:
[ \min_{\mathbf{y}} ; \mathbf{c}^\top \mathbf{y} + \max_{\mathbf{u} \in U} \min_{\mathbf{x} \in F(\mathbf{y},\mathbf{u})} \mathbf{d}^\top \mathbf{x} ]
其中 (\mathbf{y}) 是第一阶段决策,(\mathbf{u}) 是不确定变量,(\mathbf{x}) 是第二阶段决策。这个“min-max-min”嵌套结构是鲁棒优化最劝退的地方,也是它区别于普通优化的根本原因。
多阶段鲁棒模型在此基础上进一步放宽:它不再假定“所有不确定量在第二阶段一次性揭晓”,而是随着时间轴推进,每个时段都有新的不确定量实现,并要求当前时段的决策能够根据已经发生的不确定量进行调整。常见的实现方式是把整个调度周期按小时切分成24个阶段,每一个小时就是一个“决策-观测-再决策”的闭环。这种模型的复杂度和保守度比两阶段更低,更贴近微电网实际的滚动调度流程,但建模和求解难度也更高。
我当时复现这个项目时用了“两阶段等价转换”的思路:把原来24个阶段的滚动决策问题,通过引入状态变量和可行域递归,合并成一个两阶段问题的主问题结构,其中第二阶段的“最坏场景”不再是一个静态向量,而是一条满足预算约束的多时段不确定轨迹。这样既能享受多阶段滚动决策带来的保守度下降,又能继续复用CCG算法框架,是工程上比较划算的做法。
2.2 目标函数怎么写才合理
目标函数看着简单,但每一项的取舍直接决定调度行为。我给出的通用形式是:
[ \min \sum_{t=1}^{T} \Big( C_{\text{buy},t} P_{\text{buy},t} - C_{\text{sell},t} P_{\text{sell},t} + C_{\text{om}}^{\text{pv}} P_{\text{pv},t} + C_{\text{om}}^{\text{wt}} P_{\text{wt},t} + C_{\text{curt}} P_{\text{curt},t} + C_{\text{load}} P_{\text{load},\text{shed},t} + C_{\text{es}} |P_{\text{es},t}| \Big) ]
各部分的含义和要点如下:
- 购电成本 (C_{\text{buy},t} P_{\text{buy},t}):主网购电按分时电价算,这是成本的大头,也决定了储能的套利行为。
- 售电收入 (C_{\text{sell},t} P_{\text{sell},t}):微电网向主网反送电时获得的收益。注意很多区域电网不允许随意反送,模型里要加边界限制或者上网电价远低于购电电价,否则优化会疯狂倒卖电能。
- 运维成本:光伏和风电的单位运维成本很低,但必须加,否则模型可能为了让不确定集合下的最坏情况可行,无意义地多用新能源。
- 弃风弃光惩罚 (C_{\text{curt}}):鲁棒模型倾向于多预留调节空间,表现为“少用一点不可控的新能源”。加惩罚项可以把这种过度保守拉回来一些。
- 切负荷惩罚 (C_{\text{load}}):数值必须设得足够大(通常是购电价的几十倍),这样才能保证优化结果只在万不得已时才会选择切负荷。
- 储能充放电损耗成本:这里的绝对值函数在MILP里要拆成两个非负变量之和,避免模型出现“同时充电又放电”的荒谬解。
有人会问:为什么惩罚项系数设得不对会导致结果“假最优”?我举个生活化的例子:如果你罚款比停车费还便宜,那大家的理性选择就是乱停车。目标函数里的惩罚项就是那个“罚款”,设小了,优化器就会把弃电和切负荷当成便宜的调节手段,方案表面上成本很低,实际根本不可用。
2.3 不确定集合的设计与保守度调节
鲁棒优化里最讲究的部分其实不是目标函数,而是不确定集合 (U) 怎么界定。常用的是盒式集合(Box):
[ P_{\text{pv},t} \in \left[ P_{\text{pv},t}^{\text{pred}} - \hat{P}{\text{pv},t}, ; P{\text{pv},t}^{\text{pred}} + \hat{P}_{\text{pv},t} \right] ]
(\hat{P}) 是预测偏差,通常取预测值的15%-25%。但纯粹的盒式集合太悲观,它假设所有时段同时达到最坏偏差,现实中几乎不可能发生。所以工程上更常用带预算约束(Budget)的集合:
[ \sum_{t=1}^{T} \left| \frac{P_{\text{pv},t} - P_{\text{pv},t}^{\text{pred}}}{\hat{P}_{\text{pv},t}} \right| \le \Gamma ]
(\Gamma) 就是鲁棒预算。(\Gamma=0) 时退化为确定性模型;(\Gamma) 越大,方案越保守、成本越高,但抗风险能力越强。实际调参时,我通常先跑一个确定性模型,再逐步增大 (\Gamma),观察总成本的变化曲线,从中选一个“成本增长可接受、最坏情况不切负荷”的临界点。比如我的算例里 (\Gamma=6) 时成本比确定性模型高约8%,但最坏情况下储能有足够空间兜底,这就是一个比较均衡的取值。
多阶段模型中,不确定集合往往还要加时间耦合约束,比如限制连续偏差时段的数量,或者限制偏差爬坡速率。这些约束让最坏场景更符合物理实际,但也会让子问题变得更难解。后面我会专门说子问题怎么处理。
3. 可再生能源与储能模型的核心细节
3.1 光伏与风电的区间出力建模
光伏和风电在鲁棒模型里的处理方式其实是类似的:先给出预测出力,再在约束中把实际出力表达为“预测值 + 偏差变量”。区别在于两者的出力特性和偏差形态。
光伏出力可以近似表示为:
[ P_{\text{pv},t} = \eta_{\text{pv}} \cdot S_t \cdot A_{\text{pv}} ]
其中 (S_t) 是光照强度,(A_{\text{pv}}) 是光伏阵列面积,(\eta_{\text{pv}}) 是综合效率。实际项目中,你手里往往没有光照原始数据,只有预测出力和历史误差统计。所以更实用的做法是直接对 (P_{\text{pv},t}) 的偏差做区间建模:
[ P_{\text{pv},t} = P_{\text{pv},t}^{\text{pred}} + \xi_{\text{pv},t} \cdot \hat{P}{\text{pv},t}, \quad \xi{\text{pv},t} \in [-1, 1] ]
风电的出力特性则有明显的“尾流”和“爬坡”特征,单区间建模容易低估相邻时段的出力相关性。如果数据支持,我建议对风电加一个爬坡约束:
[ \left| \xi_{\text{wt},t} - \xi_{\text{wt},t-1} \right| \le R_{\text{wt}} ]
这个约束可以防止最坏场景在相邻时段之间做“过山车式”的剧烈跳变。别忘了鲁棒优化的目标本来就是找最坏情况,如果你不约束爬坡,子问题求解器会把不确定性序列构造得非常极端,导致整个方案过度保守。
3.2 储能SOC模型与防同充同放约束
储能是微电网里最重要的灵活性资源,也是鲁棒调度中的“调节器”。它的核心约束是荷电状态(SOC)递推方程:
[ SOC_{t+1} = SOC_t + \frac{\eta_{\text{ch}} P_{\text{ch},t} \Delta t}{E_{\text{es}}} - \frac{P_{\text{dis},t} \Delta t}{\eta_{\text{dis}} E_{\text{es}}} ]
其中 (\eta_{\text{ch}}) 和 (\eta_{\text{dis}}) 分别是充放电效率,(E_{\text{es}}) 是储能额定容量。这里有两个我在复现时深有体会的细节:
第一,充放电功率的上下限约束要分别写:
[ 0 \le P_{\text{ch},t} \le u_{\text{ch},t} P_{\text{ch}}^{\max}, \quad 0 \le P_{\text{dis},t} \le u_{\text{dis},t} P_{\text{dis}}^{\max} ]
并且要加 (u_{\text{ch},t} + u_{\text{dis},t} \le 1) 这个互斥约束。很多人说“我已经把充放电功率都定义成非负变量了,还需要二进制变量吗?”答案是必须的。如果没有互斥约束,数值求解时可能出现同时充放电、能量在电池内部“空转”的情况,即使不计损耗,也会虚增SOC、掩盖功率平衡的真实问题。
第二,SOC的上下限不要取0和1,建议取0.1到0.9。原因很简单:工程上锂电池深度放电会折寿,而鲁棒优化在最坏情况下几乎一定会把SOC推向边界。如果不预留安全裕度,你算出来的“最坏场景可行”在实际中就是“电池过放报警停机”。这个细节属于典型的“代码能跑、方案不能用”的坑。
另外有些模型会要求 (SOC_T \ge SOC_0),即调度周期结束时的电量不小于初始电量。这在孤岛型微电网里是硬约束,在并网型微电网里可以根据实际运行需求决定要不要加。加了之后,储能无法在一天内“用完”电量,约束了套利空间,但保证了第二天的调节能力。
3.3 负荷与并网功率约束
负荷模型在鲁棒调度里有两种处理方式。简单做法是把负荷当作确定性高精度预测量,只在光伏和风电上构造不确定集合。这种做法计算量小,适用于负荷预测误差远小于新能源出力误差的区域。另一种做法是把负荷也纳入不确定集合,在实际项目中考虑到温度敏感型负荷(空调)的波动幅度确实不小,把负荷不确定性加上更符合真实情况。
并网功率约束是很多人复现时容易忽略的一块。实际微电网和主网的交换功率受到变压器容量、线路载流量和并网协议的共同限制:
[ -P_{\text{grid}}^{\max} \le P_{\text{buy},t} - P_{\text{sell},t} \le P_{\text{grid}}^{\max} ]
同时在目标函数里,(P_{\text{buy},t}) 和 (P_{\text{sell},t}) 各自独立非负,不能用一个可正可负的变量 (P_{\text{grid},t}) 替代,否则电价倒挂时会出现“既买又卖”的逻辑矛盾。处理好这个细节,后面的功率平衡方程才能写干净。
4. MATLAB工程实现:从公式到代码
4.1 总体架构与求解器选型
MATLAB复现鲁棒优化这类问题,最主流的路线是:YALMIP建模 + 商用求解器求解。YALMIP负责把优化问题翻译成求解器能懂的标准形式,你只需要用接近数学公式的语法描述问题。求解器方面,Gurobi 和 CPLEX 是首选,因为它们对MILP(混合整数线性规划)的支持成熟度远高于MATLAB自带的 intlinprog。如果你手头没有商用求解器授权,用intlinprog跑小算例练手没问题,但想复现24时段的鲁棒调度,求解时间会让人崩溃。
我推荐的结构是三层分开:
- 数据层:把所有算例参数(预测出力、负荷、电价、设备参数、不确定度比例)集中到一个脚本或Excel里,用结构体或字典传给上层。
- 模型层:独立函数文件,分别构建主问题(MP)和子问题(SP)。
- 算法层:CCG迭代主循环,负责控制MP和SP的交替求解、更新上下界、收敛判定。
这样分层的好处是后期调试时不用满目录翻代码。尤其是迭代算法里主问题和子问题会反复重构,如果写成一个大脚本,每改一个参数就要跑一遍全局,极其浪费时间。
4.2 主问题构建与关键代码
主问题在CCG的第 (k) 次迭代中写成如下形式:
[ \min_{\mathbf{y}, \eta, \mathbf{x}_1,\dots,\mathbf{x}_k} ; \mathbf{c}^\top \mathbf{y} + \eta ]
[ \text{s.t.} \quad \mathbf{A}\mathbf{y} \ge \mathbf{b} ]
[ \eta \ge \mathbf{d}^\top \mathbf{x}_l, \quad \forall l = 1,\dots,k ]
[ \mathbf{G}\mathbf{x}_l \ge \mathbf{g} - \mathbf{E}\mathbf{y} - \mathbf{M}\mathbf{u}_l, \quad \forall l = 1,\dots,k ]
其中 (\mathbf{u}_l) 是第 (l) 次迭代时由子问题算出来的最坏场景,(\mathbf{x}_l) 是对应场景下的第二阶段变量。注意每轮迭代都会“记住”一个最坏场景,并把对应的决策变量和约束添加到主问题中,这就是“列与约束生成”名称的由来。
MATLAB代码骨架如下:
% 主问题构建(第k次迭代) % y:第一阶段变量(储能充放电状态、购售电状态等) % eta:鲁棒成本上界变量 % x_cell:存储各历史最坏场景对应的第二阶段变量 Constraints = []; Objective = c_y' * y + eta; for l = 1:k Constraints = [Constraints, ... eta >= d_l' * x_cell{l}, ... G * x_cell{l} >= g - E * y - M * u_history{l}]; end ops = sdpsettings('solver', 'gurobi', 'verbose', 0); sol = optimize(Constraints, Objective, ops);这段代码里最关键的是x_cell这个单元数组,它承载了所有历史场景的决策变量。随着迭代次数增加,主问题规模线性增长,求解时间也会逐步上升。这是CCG的正常现象,不是bug。
4.3 CCG算法的迭代流程
CCG算法的整体流程可以用下面几步概括,这也是我在项目代码里实际执行的顺序:
- 初始化:设置下界 (LB = -\infty),上界 (UB = +\infty),迭代计数 (k=0),初始最坏场景取预测值。
- 求解主问题MP,得到最优解 ((\mathbf{y}^{(k)}, \eta^{(k)})),更新下界 (LB = \mathbf{c}^\top \mathbf{y}^{(k)} + \eta^{(k)})。
- 固定 (\mathbf{y}^{(k)}),求解子问题SP,得到最坏场景 (\mathbf{u}^{(k+1)}) 和最优值 (f_{\text{SP}}(\mathbf{y}^{(k)})),更新上界: [ UB = \min(UB, ; \mathbf{c}^\top \mathbf{y}^{(k)} + f_{\text{SP}}(\mathbf{y}^{(k)})) ]
- 如果 (UB - LB \le \varepsilon),停止;否则把 (\mathbf{u}^{(k+1)}) 作为新列加入主问题,(k = k+1),返回第2步。
这里有一个很容易忽视的细节:初始场景为什么要取预测值?因为第一次迭代时主问题里没有任何“不确定场景”,如果直接设一个空集合,主问题会得到一个“永远乐观”的解,导致后续上界更新异常。用预测场景初始化可以让算法在一个物理合理的起点上启动。
另一个细节是收敛阈值。我建议不要一开始就设 (10^{-6}) 这种高精度,而是先跑一遍看迭代趋势。正常情况下CCG在10-20次迭代内能收敛到 (10^{-3}) 量级,如果超过30次还不收敛,几乎可以肯定模型或代码里有问题,而不是精度不够。
4.4 子问题的对偶处理
子问题是整个项目里最“劝退”的部分,因为它的结构是 max-min 嵌套:
[ \max_{\mathbf{u} \in U} \quad \min_{\mathbf{x} \in F(\mathbf{y}^*, \mathbf{u})} \mathbf{d}^\top \mathbf{x} ]
MATLAB的优化工具箱不认识这种嵌套结构,所以必须把内层min用对偶定理转换成max,把双层问题变成单层问题。转换后的子问题变成:
[ \max_{\mathbf{u}, \boldsymbol{\lambda}} ; \boldsymbol{\lambda}^\top (\mathbf{g} - \mathbf{E}\mathbf{y}^* - \mathbf{M}\mathbf{u}) ]
[ \text{s.t.} \quad \boldsymbol{\lambda}^\top \mathbf{G} = \mathbf{d}, \quad \boldsymbol{\lambda} \ge 0 ]
其中 (\boldsymbol{\lambda}) 是内层最小化问题的对偶变量。但这里还有个麻烦:目标函数里出现了 (\boldsymbol{\lambda}^\top \mathbf{M}\mathbf{u}) 这种双线性项。针对这个双线性项,工程上常用的处理办法是:
- 如果不确定变量 (\mathbf{u}) 的集合是连续盒式集合,可以通过考察目标函数对 (\mathbf{u}) 的单调性,直接把 (\mathbf{u}) 取到区间端点,把双线性项变成线性项。
- 如果加了预算约束,(\mathbf{u}) 的取值受整体约束影响,不能逐个取端点,需要用KKT条件或引入辅助变量线性化。更常见的做法是对带预算约束的子问题再做一次对偶,把原来对 (\mathbf{u}) 的max“拉平”,形成纯线性规划。
- 在MATLAB里,最省事的路线是用
bisect或直接对几个候选极端场景做枚举,配合稀疏化技巧截断大规模不确定集合。虽然理论上不严格,但工程上速度快、结果也经常足够好。
我个人的建议是:第一阶段复现时先不要加预算约束,用纯盒式集合把整套CCG跑通,确认真实性没问题后,再加预算约束并重写子问题。这样能大幅降低初期的调试难度。
5. 完整复现过程:一个24时段算例
5.1 算例参数设置
为了让大家有一个能直接对照的基准,我给出一个小型区域微电网的算例参数。这个规模用MATLAB加Gurobi在普通笔记本上可以比较舒服地跑完。
| 参数 | 数值 |
|---|---|
| 调度周期 | 24小时,单位时段1小时 |
| 光伏装机 | 3 MW,预测偏差±20% |
| 风电装机 | 2 MW,预测偏差±25% |
| 储能额定功率 | 1 MW |
| 储能额定容量 | 2 MWh |
| SOC范围 | [0.1, 0.9] |
| 充放电效率 | 0.95 / 0.95 |
| 主网购电上限 | 2 MW |
| 负荷峰值 | 5 MW |
| 电能量成本基准 | 0.6 元/kWh |
| 切负荷惩罚 | 10 元/kWh |
| 鲁棒预算Γ | 6 |
负荷数据和新能源预测数据我建议手动构造一组“有峰谷特征、有一定相关性”的时间序列。所谓相关性,是指光伏出力白天高、夜间低,负荷在傍晚出现晚高峰,风电夜间略强于白天。如果数据是完全随机生成的,模型虽然能跑通,但结果图没有任何业务含义,不方便你判断方案是否合理。
5.2 逐步跑通流程
第一步,把数据整理好,画出预测曲线。这一步花10分钟,能帮你省下后面几小时的排错时间。比如一眼就能看出预测数据里有没有突兀的尖峰或负值,这些都是后续模型不收敛的常见诱因。
第二步,先跑一个不带不确定性的确定性模型。如果你连确定性模型都跑不出合理结果,就不要碰鲁棒。确定性模型的功率平衡长这样:
[ P_{\text{pv},t}^{\text{pred}} + P_{\text{wt},t}^{\text{pred}} + P_{\text{dis},t} + P_{\text{buy},t} = P_{\text{load},t} + P_{\text{ch},t} + P_{\text{sell},t} + P_{\text{curt},t} ]
注意这个公式里我把习惯上的负荷放到了等式左边、销售放到了右边,这样写更直观反映物理含义:左侧是“出力侧”,右侧是“消纳侧”。
第三步,加入不确定集合,只对光伏和风电构造区间。这时功率平衡改写为:
[ (P_{\text{pv},t}^{\text{pred}} + \xi_{\text{pv},t} \hat{P}{\text{pv},t}) + (P{\text{wt},t}^{\text{pred}} + \xi_{\text{wt},t} \hat{P}{\text{wt},t}) + P{\text{dis},t} + P_{\text{buy},t} = P_{\text{load},t} + P_{\text{ch},t} + P_{\text{sell},t} + P_{\text{curt},t} ]
不确定变量 (\xi) 只出现在约束右侧的常数项吗?不是,它出现在功率平衡的等式中。这带来一个棘手的问题:等式约束在不确定性下几乎不可能对所有场景都严格满足。处理办法是把功率平衡拆成两个不等式,或者把等式“嵌入”子问题中,让第二阶段变量 (\mathbf{x}) 去平衡不确定性。实际建模中,我倾向于把功率平衡拆成两个不等式约束,分别对应“出力不足时必须从主网/储能上调”和“出力过剩时必须弃电/下调”,这样鲁棒约束的可解性会好很多。
第四步,实现CCG迭代。建议在循环里打印每轮迭代的LB和UB,格式类似:
Iter 1: LB = 12834.2, UB = 15921.7, Gap = 19.4% Iter 2: LB = 13620.5, UB = 14880.3, Gap = 8.5% ...看着Gap一点点缩小,是复现这个项目最让人安心的时刻。
5.3 结果怎么看、怎么验证
算完之后不要急着截图写报告,先做三件事:
第一,对比确定性方案和鲁棒方案在“最坏场景”下的表现。把确定性方案带入子问题,计算其最坏场景成本;再把鲁棒方案带入同一个子问题,观察成本差异。这个差异就是“买鲁棒性”的价格。如果差异只有2%-3%,说明你的不确定性集合太宽松,方案偏浪费;如果差异超过20%,要审视是不是预算(\Gamma)设得太大,或者子问题里最坏场景构造得太极端。
第二,检查SOC曲线。正常的调度结果里,储能在电价低谷时充电、电价高峰时放电,SOC曲线呈现“U形”或“驼峰形”交替。如果看到SOC在一天内剧烈震荡,很可能是充放电互斥约束没生效,或者效率参数设置有问题。
第三,检查是否出现弃电和切负荷同时发生的现象。逻辑上,如果系统还能切负荷,说明它处于缺电状态,就不应该再弃掉新能源出力;反过来,如果已经有弃电,说明电源过剩,就不应该切负荷。两者同时出现的最常见原因,是惩罚系数设置失衡或者功率平衡约束写错,需要回头排查。
6. 复现途中踩过的坑与排查方法
6.1 子问题对偶推导出错
这个坑几乎每个做CCG的人都踩过。内层min的对偶推导涉及约束方向、变量符号、对偶变量可行性条件,任何一个符号写错,子问题求出来的“最坏场景”就不是真正的最坏场景,CCG主问题会往错误方向迭代,最终结果发散或者收敛到一个虚假的最优值。
我的建议是对照“原问题-对偶问题”转换表逐条检查,重点看等式约束对应的对偶变量是否自由、不等式方向是否一致。另外强烈建议先写一个小规模的随机数值测试:固定 (\mathbf{y}^*),手动枚举几个候选极端场景,对比子问题数值解和枚举结果是否一致。这一步能快速定位对偶写错的位置。
6.2 收敛慢怎么办
CCG收敛慢的原因通常有三个。第一个是初始场景太离谱,导致前几轮迭代主问题规模膨胀很快。解决办法是用“预测场景+若干历史极端场景”共同初始化。第二个是子问题存在多个等价最优解,每次返回的最坏场景跳来跳去,主问题始终无法“钉住”最坏方向。可以在子问题目标里加一个极小值的正则项来稳定场景输出。第三个是数值缩放不当,变量量纲跨度太大,比如功率用MW、容量用MWh、成本用元,三者数值相差好几个数量级,导致求解器数值困难。做法是统一定标到同一单位,比如都用kW、kWh、元,成本指标控制在千元的量级。
6.3 数值病态与求解器选择
还有一个我印象很深的坑:大M参数设置不当。很多人在把逻辑约束线性化时引入大M,结果M取成了 (10^6),导致LP松弛质量极差,MILP求解时间暴增,甚至出现“看似满足互斥约束、实际违反物理约束”的病态解。我的经验是用“物理上合理的最大可能值”来取M,而不是随手写一个很大的数。比如购电量上限是2MW,那么与购电状态相关的M取2.5就够,完全没有必要取 (10^6)。
另外实测下来,同一问题在Gurobi和CPLEX上的求解速度可能有明显差异,但更重要的差异在MIP gap的默认公差设置。如果你发现求解结果中出现“极小负数”的功率或SOC,多半是求解器默认的整型容忍度和可行性容忍度太松。在求解器参数里把IntFeasTol和FeasibilityTol收紧一个数量级,通常能解决。
6.4 常见问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| CCG迭代不收敛,Gap震荡 | 子问题对偶错误或场景不稳定 | 固定y枚举场景验证子问题;加正则项稳定场景输出 |
| 结果中出现同时充放电 | 互斥约束缺失或大M过松 | 检查二进制变量互斥约束;收紧M值 |
| 最坏场景纯属胡闹(功率剧烈跳变) | 不确定集合缺少时间耦合约束 | 对ξ加爬坡约束或预算约束 |
| SOC越过上下限 | SOC递推方程符号写反 | 对照充电/放电定义逐时段验证SOC递推 |
| 求解时间过长 | 大M不合理、模型规模大、求解器公差松 | 收紧公差、优化参数缩放、加初始场景 |
| 鲁棒解成本远高于确定性解 | 预算Γ过大或不确定区间过宽 | 做Γ-成本敏感性分析,取合理折中点 |
7. 从复现到落地的一些扩展想法
CCG和鲁棒优化在微电网调度里只是第一步。我跑通这个项目之后,最大的感受是:鲁棒模型给你的是“底牌”,而真正提升运行效益的,是底牌之上的策略套件。
一个很容易叠加的方向是需求响应。把一部分柔性负荷(空调、热水器、充电桩)纳入可调范围,它们可以在最坏场景下主动削减用电,既降低了鲁棒性的成本溢价,又提高了系统调节能力。另一个方向是把鲁棒框架从“单微电网”扩展到“多微电网互联”,此时不确定集合还要考虑多个微电网之间的功率交互和分布式协调。这两个方向在建模本质上没有跳出CCG的框架,只是变量规模和约束数量上了一个台阶。
还可以在求解端做文章。当24时段模型规模太大、CCG迭代次数增多时,可以考虑把不确定集合按时段块分解,或者用“场景聚合”减少子问题求解次数。我见过有些实现把CCG和Benders分解混合使用,用Benders处理子问题里的线性部分,用列生成处理不确定性场景,收敛速度比纯CCG快不少。不过混合算法的调试难度也跟着上去了,适合团队协作而不是单打独斗。
回到这次复现本身,我的体会是:鲁棒优化这个方向,纸面上最难的是数学公式,实际上最难的永远是“公式到代码”的翻译过程。对偶推导、大M取值、约束方向、单位统一,任何一个小地方出错,浪费的时间都是以天计。建议每个准备复现的朋友都养成一个习惯——每写完一个模块,立刻用极小规模算例做数值验证,不要等整套代码跑起来再debug。那样的话,排查时间和心智负担都会小很多。最后再分享一个小技巧:所有鲁棒相关的模型函数里,给每个变量和约束写清楚物理单位注释,三个月后回头看代码,你会感谢当时的自己。