1. 微电网优化调度的不确定性问题
1.1 风光负荷随机性为何让确定性模型失效
做微电网优化调度这几年,我踩过最深的一个坑,就是一开始把光伏、风电和负荷预测值当成确定值来建模,直接用预测曲线丢进优化模型求机组启停和出力计划。调度方案看起来确实算得完、跑得快,但一旦到了实际运行那天,光伏被云层盖住、负荷峰值比预测高出几个点,整个优化结果就容易崩——不是弃风光弃到亏本,就是被迫高额买电甚至切负荷。
问题的根源在于,微电网本质上是个“小系统”,容量小、惯性弱,单台分布式电源或单段线路的波动就能对整个系统的功率平衡产生显著影响。大电网动辄上千台机组、几十万负荷节点,局部预测偏差可以被整个系统“稀释”掉;微网没有这个冗余。所以,微网调度对预测误差的容忍度非常低,确定性优化自然站不住脚。
处理这种不确定性的主流思路有三条路:随机规划、鲁棒优化、分布鲁棒优化。随机规划需要给不确定性变量设定概率分布,然后通过采样或场景树把随机变量离散化,最终变成一个大规模混合整数规划问题。这条路的缺点是概率分布拿不准、场景数量太多、计算量爆炸,而且“概率小但损失大”的极端事件容易被忽略。鲁棒优化的逻辑相反,它不追求某个概率意义下的最优期望,而是让方案在“最坏情况下”也能行得通——只要不确定变量落在预设的集合内,约束就不该被破坏。这正好和微网“小系统、怕极端”的特点对上了。两阶段鲁棒优化,就是在这个大方向上最常用、最落地的一套方法。
一个更现实的类比:确定性调度相当于“照着天气预报穿衣服”,极端降温就只能硬扛;随机规划相当于“带上一件薄外套,温度差不离就行”;两阶段鲁棒优化则是“把最冷那天的羽绒服带上,同时也在车里备一件短袖”,为最坏情况兜底,又保留充裕的调整空间。
1.2 从单阶段到两阶段:为什么需要“调整”而不是“硬抗”
如果直接在鲁棒框架下把所有决策都放到同一个优化问题里求解,本质上是在让“机组的启停决策”和“实时出力调整决策”共享同一个信息状态。这会产生一个问题:为了保证所有不确定情形可行,优化器会保守到令人绝望——微燃机几乎全开,储能常备高电量,购电协议锁满,结果经济性大打折扣。
原因不难理解:像“机组启停”这类整数决策是慢变的,夜间做完计划,白天就不能频繁变了;而像“微燃机出力、储能充放电功率、与主网交互功率”这类连续决策,是可以随着风光和负荷的实际波动实时调整的。单阶段模型把这两个时间尺度搅在一起,只能把两类决策都做得极端保守。
两阶段鲁棒优化的思想,就是把决策拆成两波。第一阶段(Here-and-Now)只决定那些必须提前拍板、事后难以修改的整数变量,例如机组启停状态、储能的充放电状态标志;第二阶段(Wait-and-See)则根据第一阶段决策确定后、不确定性逐步揭开的情况,对连续变量做即时调整,目标是让总成本尽可能低,同时保证所有约束在不确定集内都可满足。
用一句话概括核心思想:“先拍板关键的、再根据现实微调次要的”。这种结构天然贴合微网调度的实际时序——日前做机组组合,日内做经济调度。这也是为什么学界和工程界普遍用两阶段鲁棒来建模微网调度问题,而不是简单套用单阶段鲁棒模型。
2. 两阶段鲁棒优化调度模型怎么搭
2.1 模型里的变量、参数与目标函数怎么设计
搭建两阶段鲁棒微网调度模型,第一步是把决策变量分类。我在实际写代码时,习惯在最前面用注释把变量清单列清楚,不然写到后面非常容易混淆。以下是我常用的一套变量体系。
第一阶段变量(在不确定性实现之前就必须确定):
- 微燃机(MT)的启停状态,0/1 整数变量
- 储能(ESS)的充电/放电状态标志,0/1 整数变量
- 与主网交互的购售电状态标志,0/1 整数变量,防止同时购售电这种“倒爷”行为
第二阶段变量(在不确定性实现后可以调整):
- 微燃机出力
- 储能充放电功率
- 与主网购/售电的有功功率
- 切负荷量,一个约束松弛变量,保证模型在最坏场景下不会直接无解
目标函数通常是第一阶段启停成本 + 第二阶段预期运行成本的极小化。但在鲁棒框架下,第二阶段不是一个具体的期望值,而是“最坏场景下的运行成本”,即:
[ \min_{\mathbf{x}} \left( c^T \mathbf{x} + \max_{\mathbf{u} \in U} \min_{\mathbf{y} \in F(\mathbf{x}, \mathbf{u})} d^T \mathbf{y} \right) ]
其中,x 表示第一阶段决策,u 表示不确定性变量(光伏出力、负荷系数等),y 表示第二阶段调整变量。F(x, u) 表示在第一阶段决策和不确定性实现值都已指定的情况下,第二阶段变量对应的可行域。
这个式子初看有点绕,但拆开其实很清楚:外层 min 是寻找最优的第一阶段决策;内层 max 是在对抗不确定性,即“坏天气最不给面子时,我的运行成本最高能到多少”;最内层 min 是在给定坏场景下,通过调整连续变量来尽量把成本压住。整体传递的信号是:“我做决策的时候,要默认天气和我作对,但我手里还握着一把可以随时调整的牌”。
2.2 不确定集:不是每个误差都要考虑,要有“预算”
鲁棒优化的关键步骤之一是构造不确定集 U。如果 U 取得太大,所有风险都兜住,结果基本上等于没有分布式电源,成本高到离谱;如果 U 取得太小,风险漏到外面,鲁棒性又不够。工程上最常用的不确定集是“盒式 + 1 范数预算约束”的多面体形式。
以光伏出力和负荷需求为例,我习惯把不确定性建模成如下形式。
设 (P_{PV,t}) 为第 t 时段光伏实际可用出力,预测值为 (\hat{P}{PV,t}),偏差范围为 (\pm \varepsilon{PV,t} \hat{P}{PV,t});负荷实际值 (P{L,t}) 同理。于是第 t 时段的不确定变量可以描述为:
[ P_{PV,t} = \hat{P}{PV,t} + \Delta P{PV,t}, \quad -\varepsilon_{PV,t} \hat{P}{PV,t} \le \Delta P{PV,t} \le \varepsilon_{PV,t} \hat{P}_{PV,t} ]
同时,为了让所有时段不可能“同时达到极端值”,加上预算约束——即整个调度周期内,不确定变量与其预测值的偏差总和不能超过某个上限 Γ:
[ \sum_{t=1}^{T} \frac{|\Delta P_{PV,t}|}{\hat{P}_{PV,t}} \le \Gamma ]
Γ 这个参数的物理意义非常直观:它表示整个调度周期内,最多允许多少个时段的预测偏差同时达到极端值。Γ 越大,系统就越保守;Γ = 0 退化为确定性模型,Γ = T 则退化为最保守的盒式集合。实际调参时,我一般把 Γ 设在总时段数的 20%~50% 之间,这样既能覆盖大多数恶劣天气场景,又不会让成本彻底失控。
这种不确定集的好处是:它是多面体的顶点,最优解一定能在顶点处取得,这让后面的对偶转化有了理论保证,也避免了直接用连续分布采样带来的规模爆炸问题。用一个生活中的例子来类比:你不必为“这个月30天每天都下雨”去买装备,而是要考虑“这月最多下大约10天雨”的情况,预算 Γ 就是在回答“到底按几天雨来准备”。
2.3 完整数学模型框架与约束清单
下面是一个我在Matlab代码里反复用过的标准两阶段鲁棒微网调度模型骨架。模型中以微燃机、储能、主网交互和可控负荷为主要资源,并加入需求响应柔性负荷的调节空间。
第一阶段目标项:
- 微燃机开机的固定成本
- 储能的充放电切换成本(可加可不加,建议加上,否则结果会出现频繁充放电抖动的现象)
第二阶段目标项(最坏场景下):
- 微燃机的燃料成本
- 与主网购电成本
- 切负荷惩罚成本(通常设置一个很大的罚系数)
- 储能运行维护成本
约束条件大致分为以下几类:
- 功率平衡约束:每个时段内,微燃机出力 + 光伏出力 + 储能放电 - 储能充电 + 购电 - 售电 = 负荷(可切部分)
- 微燃机爬坡约束和出力上下限约束
- 储能SOC递推关系、充放电功率限制、容量限制
- 主网交互功率限制,以及不自相矛盾的购售电状态约束
- 第二阶段变量在不确定集内所有实现下均满足上述约束(鲁棒可行)
这套模型框架本身并不神秘,真正容易出问题的,是后面的求解算法,尤其是子问题里的 max-min 怎么处理。
3. 关键场景辨别算法:让CCG加速的核心
3.1 为什么不能把所有“坏场景”全枚举出来
两阶段鲁棒优化最直接的解法,就是把不确定集 U 内所有顶点场景都枚举出来,然后一次性交给求解器。问题在于顶点数量随维度指数爆炸。假设有 24 个时段、光伏和负荷两个不确定量,即使预算 Γ 压制了一部分组合,关键顶点场景的数量仍然可以轻松上万。这些场景一旦全部塞进优化模型,主问题的规模会迅速膨胀到求解器啃不动的程度。
实际的微网算例里,我见过有人把所有顶点全枚举后,MILP 模型的约束数量突破百万,CPLEX 跑了一天也收敛不了;而同样的问题,用CCG和场景辨别算法,往往十几到二十几轮迭代就能稳定收敛。差距如此明显,核心原因在于:真正对最优解产生约束作用的“关键场景”数量非常有限。大量场景虽然也在不确定集内,但它们的恶劣程度远低于最坏情况,对优化结果毫无影响。
关键场景辨别算法的思路就是:每一轮迭代时,不去看所有场景,而是通过子问题求解,找出“当前最危险的那一个或几个场景”,把它们加入主问题,再重新求解。这相当于把“大规模枚举”换成了“定向寻优”,每一轮只把石子扔进水里,看哪一圈涟漪能碰到岸边。
3.2 场景辨识的数学实现与代码逻辑
在CCG算法框架下,关键场景辨别算法的核心体现在子问题的求解上。子问题的形式为:
[ \max_{\mathbf{u} \in U} \min_{\mathbf{y} \in F(\mathbf{x}^*, \mathbf{u})} d^T \mathbf{y} ]
这是一个 max-min 双层的优化问题,不能直接求解,常规做法是把它转换成单层优化:内层 min 问题是关于 y 的线性规划(假设主问题已经给定了 x*),那么根据强对偶定理,这个 min 的最优值等于其对偶 maximize 问题的最优值。于是原问题变成:
[ \max_{\mathbf{u}, \mathbf{\lambda}} \left( \mathbf{\lambda}^T \mathbf{A}(\mathbf{u}) \right) ]
其中 λ 是内层问题的对偶变量,A(u) 是对偶问题中与 u 相关的系数矩阵。此时,u 和 λ 都是变量,且约束中出现 u 与 λ 乘积形成的双线性项。处理这个双线性项,通常有两种做法:一种是使用大M法在u的每个取值可能上做线性化展开,把问题变成混合整数线性规划;另一种是枚举不确定集的顶点,然后用 MILP 求解。
在实际代码中,我经常用顶点枚举配合大M约束来处理双线性项。原因很实际:不确定集是盒式多面体,顶点可枚举,而且对偶变量的取值有物理边界,大M参数也相对好选。我会先预先计算出所有不确定变量的“极端取值方向”,然后只在这些方向上开启大M开关,把 min 的对偶问题放到 max 的外层,构成了一个可求解的 MILP。
这个 MILP 的解,就是当前第一阶段的 x* 所面临的最坏场景。这个场景正是“关键场景”——它代表着当前方案最薄弱的地方。把它带回主问题进行下一轮迭代,本质上是逼迫方案在“当前最躲不开的风险”上改进。
3.3 关键场景辨别与CCG的协同机制
CCG(列与约束生成)算法是两阶段鲁棒优化最常用的精确求解框架,和经典 Benders 分解相比,CCG 在每轮迭代中是往主问题里“加变量、加约束”,而不只加割平面,这样收敛速度通常更快。关键场景辨别算法本质上是CCG框架中的一个关键工程化模块——它负责在迭代中挑选“值得加入主问题”的场景变量。
整个CCG迭代流程可以写成下面这个循环:
- 初始化:给定一个任意可行的一阶段解 x,通常先求解一个确定性的经济调度问题来获得初始解
- 重复以下步骤,直到上下界间隔满足阈值:
- 第一步:固定当前 x,求解子问题(即 max-min 问题),得到最坏场景下运行成本,作为上界
- 第二步:把该最坏场景对应的第二段变量和约束加入主问题
- 第三步:求解扩展后的主问题,得到新的第一阶段决策和总的预期成本,作为下界
- 第四步:若上下界的相对误差小于阈值(例如 0.5%),则停止迭代;否则回到第一步
在这个循环中,所谓“关键场景辨别”,就是在第二步里决定“这次把哪个场景加进主问题”。朴素CCG的做法是把子问题最优解对应的完整场景加入;而关键场景辨别算法会多做一些信息挖掘:从对偶变量中识别出哪些时段、哪些不确定方向对成本影响最大,把这些影响因子最高的小场景片段作为独立场景直接加入,从而减少迭代轮数和主问题规模的膨胀速度。
举个实际例子。我处理过一个微网模型,光伏24时段出力偏差和负荷24时段偏差,预算 Γ=6。整个不确定集的顶点数量约 万级,如果全枚举加入,主问题直接在几千个二元变量上困死。但用关键场景辨别算法后,前两轮迭代识别出的关键场景集中在“午间光伏骤减”和“晚间负荷高峰”这两段。这两个场景一加入,主问题的下界迅速逼近最终收敛值,总共只花了 9 轮迭代就达到 0.5% 的收敛精度。这说明工程上大部分风险,其实就是那么几个关键时段、关键方向叠加出来的,场景辨识让模型“抓住主要矛盾”,而不是“面面俱到”。
4. Matlab代码实现与调试实录
4.1 环境依赖与工具箱准备
写这套代码,我用的是Matlab + YALMIP + 求解器的组合。YALMIP 是Matlab环境下非常流行的建模语言,能大幅简化优化问题的书写和调试;求解器方面,如果只处理连续线性规划和二次规划,可以用默认的LinProg,但两阶段鲁棒模型的主问题本质上是混合整数线性规划,必须用CPLEX或Gurobi这种商用求解器。我个人的经验是,Gurobi在小规模微网算例上速度略优,CPLEX在大规模整数问题上更稳,两者差别没有特别夸张,选自己熟悉的就好。
有一点要重点提醒:YALMIP 只是一个“翻译官”,它负责把优化问题翻译成求解器能读的标准格式,真正干活的是底层的求解器。所以安装时必须保证 YALMIP 能正确调用 CPLEX/Gurobi,可以用yalmiptest命令检查求解器是否匹配成功。这一步很多新手容易忽略,导致代码明明写对了,却报出“No suitable solver”或者直接卡死。
Matlab版本建议 R2021b 以上,主要是对稀疏矩阵和MILP求解的支持更好;如果条件允许,可以在优化结束后用optimize返回的sol.info查看求解器返回的原始信息,这对排查不可行问题非常关键。
4.2 主程序架构与CCG迭代流程
下面给出一个我常用的主循环框架,只保留核心逻辑,方便理解整体骨架。
%% 参数初始化 T = 24; % 调度时段数 Gamma = 6; % 预算参数 r = 1; % 迭代计数器 LB = -inf; UB = inf; eps = 0.005; %% 初始解:先求一个不考虑不确定性的确定性调度 x0 = solve_deterministic(); % 返回第一阶段的启停等0/1变量 %% CCG 主循环 while (UB - LB) / abs(UB) > eps % 步骤1:固定 x_{r-1},求解子问题(关键场景辨别) [obj_sub, u_critical, lambda_record] = solve_subproblem(x0, params); UB = min(UB, obj_sub + stage_one_cost(x0)); % 步骤2:依据子问题结果识别关键场景,加入主问题 mp = build_master_problem(r); % 把第r轮的关键场景加入主问题 mp = add_key_scenarios(mp, u_critical, r, params); % 步骤3:求解主问题,得到新的一阶段解和新的下界 [x_new, obj_mp] = optimize(mp); LB = obj_mp; % 步骤4:判断收敛,继续迭代 x0 = x_new; r = r + 1; end这个框架的写法适合所有CCG类算法微调。solve_subproblem是整个代码中最核心、也最容易出错的地方。我在实际编码时,子问题不是单独用YALMIP建模一次就完事,而是每次传入当轮的一阶段解,重新构建模型并求解。这会让代码慢一点,但能避免共享变量污染导致的错误——那种错误排查起来极其痛苦。
另一个要注意的是上下界的更新时机。我见过很多人把上界更新放到子问题求解前,或者把下界更新放到主问题求解后,结果迭代了好几次上下界都没有下降,还以为是收敛问题,其实是更新顺序写反了。正确逻辑是:子问题(内层max-min)的目标函数值对应上界,因为它代表当前最优方案在“最坏场景”下的运行成本;主问题的目标函数值对应下界,因为主问题是不确定性放松后的近似,目标只会偏低。
4.3 子问题对偶转化与双线性项处理
子问题内部是一个 max-min 问题,下面用一个简化写法说明转化步骤。
假设子问题内层的 min 问题可以写成以下标准形式:
[ \min_{\mathbf{y}} ; d^T \mathbf{y} \quad \text{s.t.} \quad \mathbf{A} \mathbf{y} \ge \mathbf{B} \mathbf{u} + \mathbf{C} \mathbf{x}^*, \quad \mathbf{y} \ge 0 ]
取对偶后可得:
[ \max_{\mathbf{\lambda} \ge 0} ; \lambda^T (\mathbf{B} \mathbf{u} + \mathbf{C} \mathbf{x}^*) \quad \text{s.t.} \quad \mathbf{A}^T \lambda \le d ]
于是子问题变成:
[ \max_{\mathbf{u}, \mathbf{\lambda}} ; \lambda^T \mathbf{B} \mathbf{u} + \lambda^T \mathbf{C} \mathbf{x}^* \quad \text{s.t.} \quad \mathbf{A}^T \lambda \le d, ; \lambda \ge 0, ; \mathbf{u} \in U ]
这里面出现了 λ 和 u 相乘的双线性项。我的处理方式如下。因为 u 的取值范围是离散的盒子顶点集,所以可以引入辅助二元变量 z 表示“u 是否取极端值”。用大M法把双线性项线性化:
假设某个时段的光伏不确定变量取上界方向时,令布尔变量 (z_{PV,t}^+ = 1),则定义辅助连续变量 (w_t^+ = \lambda_{s,t} \cdot z_{PV,t}^+),并加入以下约束:
[ 0 \le w_t^+ \le M \cdot z_{PV,t}^+, \quad 0 \le \lambda_{s,t} - w_t^+ \le M \cdot (1 - z_{PV,t}^+) ]
其中 M 是对偶变量 λ 的一个足够大的上界。这里 M 选多大,是个频繁踩坑的点。M 太小时模型会错误地切掉可行解;M 太大时数值稳定性直线下降,CPLEX 甚至会报“整型不可行”或者干脆卡死。我的经验是:先求解一次不包含双线性项的对偶问题,看看 λ 的初始取值范围,然后选比最大量级高一个数量级的值,同时不能超过 1e6。
如果你觉得大M调参太麻烦,还有一个替代方案:直接对不确定集做 KKT 替代,把内层 min 变成 KKT 条件,然后组合成互补约束问题。但那样复杂度更高,在Matlab中需要额外定义互补约束,YALMIP 支持不怎么完美,我自己很少在微网调度这种规模的问题里使用。
4.4 代码中的几个关键实现细节
首先是场景变量的“历史记录”保存。主问题每轮会新增一批第二阶段的决策变量和约束,我在代码里用了一个 cell 数组scenario_list来保存每一轮加入的场景信息。这样不仅方便后续 debug,还能在收敛后输出每个关键场景对应的不确定变量分布,为论文或报告里的结果分析提供素材。
其次是变量索引的处理。两阶段模型涉及 t 时段、k 迭代轮次、不确定方向等多个维度,很容易索引错位。我个人的习惯是所有涉及不确定变量识别的部分,可以写成小矩阵形式,一次性为全部时段建立第二阶段的变量,而不是循环T次逐步添加约束。别小看这个写法差异,用YALMIP时用for循环逐个添加约束,约束数量一多,建模时间会呈现指数级上升。用矩阵方式建模,24时段模型在性能上差距能到几十倍,实际对比非常明显。
再次是储能SOC的动态约束写法。SOC的递推表达式里存在一阶段变量(充放电状态标志)和二阶段变量(充放电功率)相乘的情况,这是双线性,直接写会破坏线性性。我的处理是用大M法把这种关联拆开,例如:
% z_ch 是储能充电状态0/1变量 % P_ch 是储能充电功率连续变量 % 需要满足: 0 <= P_ch <= z_ch * P_ch_max constraints = [constraints, 0 <= P_ch <= z_ch * P_ch_max];这样写在YALMIP中会形成一个包含1/0变量的乘积项,YALMIP会自动big-M化,或者更稳妥的做法是手动引入辅助变量,写成两组线性约束。同时,为了避免储能“既充又放”的抖动,我建议额外加上状态切换成本项,哪怕只有很小的切换代价,也能有效防止求解器为了数值便宜让储能高频切换——这对分析结果的工程合理性非常重要。
4.5 一个可运行的最小案例:数据怎么设、结果怎么读
为了说清楚怎么把这套模型落到代码上,我给出一个最小化案例的关键参数,你可以直接照着设置然后扩展,不必一开始就追逐复杂的模型。
- 时段数:T = 24,步长 1 小时
- 微燃机:额定功率 100 kW,爬坡率 30 kW/h,启动成本 8 元,运行成本系数 0.6 元/kWh
- 储能:容量 200 kWh,初始SOC 0.5,最大充放电功率 40 kW,效率 0.95
- 主网交互:购电上限 200 kW,售电上限 50 kW,购电价格按分时电价,峰值时段 1.2 元/kWh、低谷 0.4 元/kWh
- 光伏预测出力:围绕 60 kW 的正弦状曲线,预测偏差系数 0.15
- 负荷预测值:围绕 150 kW 的双峰曲线,偏差系数 0.1
- 预算约束:Gamma = 6
- 收敛阈值:上下界相对偏差 0.5%
运行完代码后,我通常会先检查三样东西:第一,上下界的收敛曲线是不是逐轮收窄,而且不震荡;第二,最坏场景下的光伏出力曲线是否在预测线下方出现明显的“塌陷”时段,这用于确认模型确实构造出了关键场景;第三,微燃机的启停方案是否合理,比如夜间负荷低谷时不应该频繁启停停。若这三样都符合直觉,才说明模型得到了一个可以用于业务分析的方案。
5. 常见问题排查与性能调优
5.1 上下界不收敛或震荡怎么办
迭代过程中最常见的异常,是上界和下界在某些轮次出现跳跃甚至发散。排查这种问题,我一般按以下顺序来。
先检查子问题是否找对“最坏场景”。很多发散场景其实来源于子问题里没有强制使用当前的一阶段解。比如主问题求解出来微燃机是开着的,但子问题里用的是上一轮的旧启停状态,导致子问题目标值虚高或过低,上下界就彻底对不上了。解决方法是每次进入子问题前,先把当前一阶段解作为参数传入,而不是在子问题里重新定义一阶段变量。
其次检查对偶转化的符号和方向。子问题对偶时,如果有的约束方向写反,对偶变量非负/非正约束搞错,结果会出现子问题目标值比主问题还低的情况,这在逻辑上是不可能的,一旦出现,基本可以断定对偶出了问题。此时建议把所有连续变量和约束逐条核对一遍,尤其注意等式约束对应的对偶变量是自由变量,不是非负变量。这个细节困扰了我很久,是最容易被忽视的地方。
最后,还要检查大M参数是否足以覆盖对偶变量取值范围。如果M设置得偏小,某些极端场景下的 λ 会被错误截断,导致子问题求不出真正的最坏场景,表现为迭代循环迟迟不能收敛。
5.2 数值稳定性与求解器选择
两阶段鲁棒优化落地到Matlab,最常见的数值问题是主问题的约束规模随着迭代次数线性增长,导致后面的迭代变慢,甚至求解器直接报“Out of memory”或“Numerical issues”。针对这个问题,我有几个实操建议。
- 尽量使用稀疏矩阵表达约束。YALMIP 虽然会自动处理稀疏性,但如果你手动写约束时用了稠密矩阵,性能会明显下降。可以用
sparse关键字预定义矩阵再传入约束。 - 不要一上来就设置过小的收敛阈值。比如把收敛误差目标设为 0.0001,很可能导致CCG迭代几十轮甚至上百轮,主问题计算时间急剧膨胀。工程上 0.5%~1% 的收敛误差完全够用,因为风光预测误差本身就在10%级别,调度的精度没必要远高于数据精度。
- CPLEX 和 Gurobi 在MILP上都有“求解精度”参数,通常默认即可,但如果出现数值警告,可以尝试把求解精度从默认的 1e-6 放宽到 1e-5 或 1e-4,往往能大幅减少求解时间,而且不牺牲实用精度。
- 高版本MATLAB + 对应版本的YALMIP + 求解器之间兼容性也可能出问题,如果遇到莫名其妙的报错,可以考虑是不是某次系统更新导致调用路径丢失,重新用
savepath保存路径,重启Matlab运行yalmiptest检查。
数值稳定性问题往往是“长尾消耗”,解决后通常能明显加速迭代。我在实际算例中调试时,曾因为大M参数太大,导致Gurobi报告“Numeric range error”,把M从1e6降到1e4后立刻恢复正常。
5.3 计算耗时优化与场景剪枝
如果你发现模型跑得太慢,先别急着怀疑算法复杂度,看看是不是主问题场景数量膨胀得太快。CCG每轮加入一个场景,如果迭代了 30 轮,主问题里就有 30 组第二段变量和约束,对于 24 时段、几十台设备的场景,规模已经不小了。这时候可以用下面几种办法压时间:
- 减少初始场景:主问题初始化时只放确定性场景(u取预测值),不要一上来就放所有考虑过的历史关键场景,让CCG自己迭代产生关键场景,往往比人为预置场景更省时间。
- 使用场景聚合:如果两轮迭代加入的关键场景在某些时段上“长得极其相似”,可以考虑把相似度高的场景合并掉。需要明确的是,严格意义上的 CCG 不能随便删场景,但在工程近似场景,可以设置一个小规则:如果新增场景与已有场景在同一时段上偏差小于 1%,就继承已有场景的第二阶段变量而不新增变量组。这个方法能有效抑制主问题规模爆炸,代价是理论上损失一点点严格性,但工程影响基本可忽略。
- 子问题的优化间隙放宽:子问题只需要求得“当前阶段相对足够坏”的场景,不一定要做到绝对最优。求解子问题时把MIP Gap从默认的 1e-4 放宽到 1e-2,能显著减少单轮耗时。需要提醒的是,这里放宽只影响单轮场景辨识的质量,CCG主循环仍然会通过迭代修正,一般不会改变最终收敛值。
5.4 结果校验:如何确认算法没跑偏
代码跑通、输出结果符合直觉后,很多人直接拿去做决策,但我想提醒你:至少要做一次结果校验,我最常用的校验方法是蒙特卡洛模拟对比。
思路很简单。用不确定集 U 和概率分布假设,随机抽取几千条光伏和负荷场景,然后把CCG求得的调度方案在每一条场景下做一次确定性再调度(即固定一阶段决策,优化二阶段变量),统计各场景的运行成本,并记录其中是否出现切负荷量大于0或功率越限的情况。如果运行成本几乎都不超过CCG计算出的上界,而且越限比例极小,说明算法的鲁棒性预期兑现了;反之,如果大量场景下都会切负荷,说明你用的不确定集或预算参数有问题,风险被严重低估了。
另外还有一个快速检查的方法:把预算参数 Γ 从0逐渐调大,观察总成本曲线。理论上成本应该随 Γ 单调上升,如果出现某些 Γ 值下成本反而下降,多半是数值问题或场景辨识有bug,需要回头检查子问题的求解精度。这种“参数敏感性检验”虽然简单,但能快速暴露模型中很多隐蔽的问题。
最后再分享几个实际体会
两阶段鲁棒优化调度在微网里能做的事不止于日前调度,它同样适用于日内滚动调度、多微网协同调度、以及碳交易约束下的低碳调度等复杂场景。做这套Matlab代码时,我个人最大的体会是:算法框架不难,真正难的是把模型“写对、算稳、跑得动”。尤其是关键场景辨别算法,它绝对不是可有可无的“优化装饰”,而是在不确定集合规模膨胀后,让模型从“理论上可解”变成“工程上可解”的决定性模块。
如果你刚开始接触这个方向,我的建议是先别急着追求大算例、复杂模型,而是把一个 24 时段的小微网跑通、跑透,亲手把上下界迭代曲线、关键场景分布、大M参数影响都调一遍。这个过程会踩不少坑,但踩完之后,你对两阶段鲁棒优化、对CCG、对YALMIP的理解都会比单纯读论文深刻得多。等到真需要处理复杂场景时,你手里这套调试经验,会是最值钱的资产。