如果你做过微网优化调度,一定遇到过这种情况:预测曲线明明很漂亮,但实际跑下来的调度方案总是“差一口气”——光伏一云遮就面临缺电风险,负荷一波动就要高价买电。我上半年做微网调度课题时,确定性优化模型已经写得很顺手了,但每次用真实数据回测,总有几个时段让人心惊肉跳。直到我把模型改成两阶段鲁棒优化,并用关键场景辨别算法去压缩最坏场景的搜索空间,调度结果才终于从“纸面最优”变成了“实际扛打”。这篇就把我从建模到Matlab代码落地的完整过程拆开讲,包括两阶段鲁棒优化的建模细节、关键场景辨别算法怎么和CCG(列与约束生成)配合、以及我在调试中踩过的坑。适合正在做微网优化调度、鲁棒优化入门、或者想用Matlab复现两阶段鲁棒调度论文的同学参考。
1. 初始确定性模型为什么会翻车,以及两阶段鲁棒优化如何补位
1.1 一个真实回测场景:预测误差怎么击穿调度方案
先说我最早做的确定性调度模型。目标函数是典型的经济调度:
- 决策变量:机组出力、储能充放电、向主网购售电功率
- 约束条件:功率平衡、机组上下限、爬坡约束、储能SOC递推、线路容量
- 输入数据:光伏出力预测、负荷预测、分时电价
模型本身没有任何问题,用YALMIP加Gurobi很快就收敛了。但我把调度方案放到真实天气数据里回测,问题就出来了:某天下午两点预报光伏出力是120kW,实际只有70kW,缺口只能靠储能和购电补,结果那一时段的购电价格恰好是峰时电价,直接导致当天运行成本比模型预测高了18%。
这就是确定性优化的通病——它把所有预测值当成准确值,没有给“预测错了怎么办”留余地。一旦实际值和预测值偏差较大,最优解就会变成次优解甚至不可行解。
1.2 两阶段鲁棒优化的基本姿态:先决策,再应对
两阶段鲁棒优化的思路非常直观,和“先定计划,再根据实际调整”的做事方式完全一致:
- 第一阶段(here-and-now):在不确定参数(光伏、负荷)实现之前,做出必须提前确定的决策,比如机组启停、是否购电这类需要提前安排的0/1变量;
- 第二阶段(wait-and-see):等到光伏出力、负荷这些不确定量真正暴露之后,再在已有第一阶段决策的基础上,做最小成本的实时调整,比如机组出力、储能充放功率。
目标函数是典型的三层结构:
[ \min_{x} \left( c^T x + \max_{u \in \mathbb{U}} \min_{y \in \Omega(x,u)} d^T y \right) ]
其中 (x) 是一阶段决策变量,(u) 是不确定参数,(y) 是二阶段调整变量。(\mathbb{U}) 是不确定集合,(\Omega(x,u)) 是给定 (x) 和 (u) 后二阶段问题的可行域。
这个式子表达的含义是:第一阶段决策要在“最坏情况下”的二阶段运行成本最小,也就是确保无论光伏、负荷怎么波动,系统都能用最小的代价完成调度和功率平衡。
1.3 什么场景下值得用两阶段鲁棒
不是所有微网调度问题都需要上两阶段鲁棒,我在项目里总结了几条判断标准:
- 光伏/风电渗透率超过30%,且预测误差在尖峰时段会显著改变功率平衡;
- 微网存在孤岛运行要求,或者对主网购电功率有严格上限,不确定性造成的缺额无法随意外购;
- 调度决策中存在明显的“先定后调”结构,比如机组启停、储能日前充放电计划这类需要提前锁定的决策;
- 需要评估最坏情况下的运行成本边界,作为报价、容量配置或风险评估的依据。
如果只是普通的并网型微网,预测误差可以通过主网完全消化,那确定性模型加备用约束就够了,没必要引入鲁棒优化。但如果你面对的课题或实际项目有上面几条中的任意两条,两阶段鲁棒大概率是值得投入的方向。
2. 关键场景辨别算法的定位和作用机制
2.1 为什么两阶段鲁棒优化需要“场景”这个概念
在连续不确定集 (\mathbb{U}) 上直接求解 max-min 问题,数学上很漂亮,但计算机没法枚举无穷多个不确定参数组合。常规做法是把不确定集离散化,或者通过CCG算法逐步生成“最坏场景”。
CCG的迭代逻辑是这样的:
- 求解主问题(MP),得到第一阶段决策 (x^*) 和目标下界;
- 固定 (x^),在不确定集合中寻找使二阶段成本最大的不确定参数 (u^),即求解子问题(SP);
- 把 (u^*) 对应的一组二阶段变量和约束加入主问题(也就是生成一个新场景的列);
- 重复直到上下界间隙满足收敛条件。
本质上,CCG每一轮迭代都会“发现”一个最恶劣的不确定参数组合,这个组合就是一个“场景”。主问题的约束规模随迭代轮数线性增长,所以CCG的收敛速度和场景数量直接相关。
2.2 场景辨别在做什么:从“有多少场景”到“哪些场景生效”
我在实际跑代码时观察到,CCG迭代到后期,主问题里积累了大量场景约束,但真正在最优解处成立的场景约束其实只有少数。大部分场景要么是冗余的,要么是被其他场景支配的。
关键场景辨别算法要解决的,正是这个冗余场景膨胀的问题。它在CCG子问题生成新场景之后增加一步操作:判断新场景是否真的会对主问题的可行域或目标函数产生实质影响。
我做了一个三维项目里比较实用的策略组合:
- 对已加入主问题的场景,检查其对应的二阶段约束在当前主问题最优解处的对偶乘子是否为0。如果乘子为0,说明该场景约束没有“卡住”主问题,标记为候选移除对象;
- 对新生成的候选场景,先做一个快速校验:把该场景加入主问题并求解一次松弛版本,如果目标函数变化量小于设定阈值(比如0.1%),说明该场景对成本边界没有显著影响,可以暂时不加入;
- 定期清理冗余场景约束,保留最近N轮内真正活跃的场景。
这样做的效果非常明显。我用24个典型日的实际数据测试,不加场景辨别时CCG迭代结束主问题里有47个场景约束,加了辨别和清理机制后,最终只保留9个关键场景约束,且最优值差别不到0.3%。这个差距在工程上完全可接受,但主问题的求解速度提升了接近4倍。
2.3 关键场景辨别和传统场景削减的区别
需要区分一下:传统场景削减(比如同步回代消除、K-means聚类)是发生在建模阶段的预处理,目的是把大量历史场景缩减成少量代表性场景;而关键场景辨别是在CCG迭代过程中实时进行的“后处理”,目的是判断求解过程中生成的场景是否值得加入主问题。
两者可以配合使用,不能互相替代。我的做法是:
- 建模阶段:用K-means把8760小时的历史数据聚成若干个典型场景,缩小初始场景池;
- 求解阶段:用关键场景辨别算法控制CCG主问题的场景约束数量。
如果只做前者不做后者,CCG每轮还会继续生成新场景,主问题照样膨胀;如果只做后者不做前者,初始的不确定集合描述可能不够贴合实际分布。
3. 鲁棒建模:目标函数、约束与不确定集的完整设计
3.1 微网系统结构和变量定义
我测试用的微网拓扑包含以下组成部分:
- 一台柴油发电机(容量80kW,爬坡率20kW/15min)
- 储能系统(容量120kWh,充/放功率20kW,效率约0.95)
- 光伏阵列(峰值功率100kW)
- 与主网的联络线(购电上限100kW,售电上限50kW)
- 可调节负荷(占总负荷的15%,可以按比例削减)
变量定义:
| 符号 | 含义 | 类型 |
|---|---|---|
| (v_{t}^{on/off}) | 机组启停状态 | 0/1变量 |
| (P_{t}^{shed}) | 负荷削减量 | 0~连续 |
| (P_{t}^{g}) | 机组出力 | 连续变量 |
| (P_{t}^{ch}, P_{t}^{dis}) | 储能充放电功率 | 连续变量 |
| (P_{t}^{buy}, P_{t}^{sell}) | 与主网购售电功率 | 连续变量 |
| (P_{t}^{pv}, P_{t}^{L}) | 光伏出力、负荷需求 | 不确定参数 |
这里要强调一个建模细节:柴油发电机组的启停变量必须放在第一阶段,因为启停计划需要提前确定,而具体的出力大小可以放到第二阶段调整。储能充电/放电状态指示变量也要放在第一阶段吗?不一定。如果充放电效率相同且不考虑寿命损耗,可以将充放电功率统一建模为一个带正负号的连续变量,此时不需要0/1变量来避免“同时充放”。但如果项目要考虑电池循环寿命,就需要在第一阶段引入充放电状态变量,否则会导致模型失真。
3.2 不确定集设计:盒式加预算约束
不确定集 (\mathbb{U}) 的选取直接决定鲁棒解的保守程度。我采用的是工程中最常用的盒式加预算约束:
[ \mathbb{U} = \left{ u_t = u_t^0 + \Delta u_t \cdot z_t, ; z_t \in [-1, 1], ; \sum_{t=1}^{T} |z_t| \leq \Gamma \right} ]
其中 (u_t^0) 是预测值,(\Delta u_t) 是最大偏差,(\Gamma) 是预算参数,控制整个调度周期内不确定参数偏离预测值的“总幅度”。
为什么不用纯盒式集合?纯盒式集合允许每个时段的光伏和负荷同时达到最恶劣值,结果会极其保守,运行成本可能比确定性模型高30%以上,实际工程中很难接受。引入预算约束 (\Gamma) 的意义在于:允许调度计划对“有限个时段的极端偏差”保持鲁棒,但不要求同时对“全部时段的极端偏差”鲁棒。这更符合实际——光伏连阴三天和负荷尖峰同时出现的概率本身就很低。
(\Gamma) 的取值影响非常直接:
- (\Gamma = 0):退化为确定性模型,不抵抗任何不确定;
- (\Gamma = T/3):大约允许三分之一的时段出现最坏偏差,工程上推荐;
- (\Gamma = T):退化回纯盒式集合,最保守。
我在实际测试中把 (\Gamma) 从4调到了12(24时段系统),运行成本从确定性的4860元上升到最保守的6420元,其中 (\Gamma = 8) 时成本是5680元,且回测中未出现功率缺额和联络线越限。这个取值区间比较适合做敏感性分析。
3.3 目标函数和约束的完整表述
完整的目标函数:
[ \min \sum_{t=1}^{T} \left( a_{g} P_{t}^{g} + b_g v_t + \lambda_t^{buy} P_t^{buy} - \lambda_t^{sell} P_t^{sell} + c^{shed} P_t^{shed} \right) + \max_{z \in \mathbb{U}} \min_{y \in \Omega(x,z)} \sum_{t=1}^{T} \left( a_{g} \Delta P_t^{g} + c^{buy}_{res} P_t^{res,buy} \right) ]
一阶段成本包括机组燃料成本、启停成本、购售电成本和弃负荷成本;二阶段成本是在最坏场景下的机组调整成本、紧急购电成本等。
约束条件包括:
- 功率平衡:(P_{t}^{g} + P_{t}^{dis} - P_{t}^{ch} + P_t^{buy} - P_t^{sell} + P_t^{pv} = P_t^L - P_t^{shed})
- 机组出力上下限及爬坡约束
- 储能SOC递推方程:(SOC_{t+1} = SOC_t + \eta^{ch} P_t^{ch} - P_t^{dis}/\eta^{dis})
- 联络线功率上下限
- 第二阶段功率平衡在不确定性实现后依然成立
这里有个容易出错的点:功率平衡方程中包含不确定参数 (P_t^{pv}) 和 (P_t^L),所以这个约束必须同时出现在二阶段问题中,由二阶段决策变量来承担平衡责任。如果错误地把平衡约束只放第一阶段,第二阶段就没有可行域了,子问题会直接不可行。
4. Matlab代码实现路线:主问题、子问题与关键场景辨别
4.1 求解器选型:YALMIP加Gurobi的组合最省心
做两阶段鲁棒优化,Matlab环境下最成熟的方案是YALMIP建模加外部求解器求解。我在项目里用的组合是:
- Matlab R2022b
- YALMIP(最新release)
- Gurobi 10.0(学术许可免费)
- 部分线性子问题也可以直接用内置的linprog
不建议用Matlab内置的intlinprog解主问题,因为两阶段鲁棒的主问题里含有大量二阶段场景变量和约束,规模稍大后内置求解器速度会明显拖后腿。Gurobi的MIP求解效率在同类产品里优势明显,特别是热启动特性对CCG循环里的重复求解帮助很大。
4.2 主问题MP的Matlab代码结构
主问题是在已知一阶段决策 (x) 和一组已经发现的关键场景 ({u^{(k)}}_{k=1}^{K}) 下,最小化总成本。核心代码如下:
% 主问题MP:min 一阶段成本 + theta % theta >= sum(对每个已发现场景k的二阶段成本) % 每个场景k对应一组二阶段变量y_k % 一阶段变量 v = binvar(T, 1); % 机组启停 P_buy = sdpvar(T, 1); % 购电功率 P_sell = sdpvar(T, 1); % 售电功率 % 二阶段变量(对每个场景k定义一组) for k = 1:K P_g{k} = sdpvar(T, 1); % 机组出力 P_ch{k} = sdpvar(T, 1); % 充电功率 P_dis{k} = sdpvar(T, 1); % 放电功率 P_shed{k} = sdpvar(T, 1); % 负荷削减 SOC{k} = sdpvar(T+1, 1); % 储能SOC end theta = sdpvar(1, 1); % 辅助变量 % 目标函数 objective = sum(a_g*P_g_ref + b_g*v + lambda_buy.*P_buy - lambda_sell.*P_sell) + theta; % 约束组装 Constraints = []; % 机组启停与出力上下限(一阶段关联约束) for t = 1:T Constraints = [Constraints, 0 <= v(t) <= 1]; end % 对每个关键场景添加二阶段约束 for k = 1:K P_pv_k = P_pv_base + delta_pv .* z{k}; % 场景k的光伏值 P_L_k = P_load_base + delta_load .* z{k}; % 场景k的负荷值 for t = 1:T Constraints = [Constraints, ... P_g{k}(t) + P_dis{k}(t) - P_ch{k}(t) + P_buy(t) - P_sell(t) + P_pv_k(t) == P_L_k(t) - P_shed{k}(t)]; Constraints = [Constraints, 0 <= P_g{k}(t) <= P_gmax * v(t)]; Constraints = [Constraints, 0 <= P_ch{k}(t) <= P_chmax]; Constraints = [Constraints, 0 <= P_dis{k}(t) <= P_dismax]; Constraints = [Constraints, 0 <= P_shed{k}(t) <= 0.15 * P_L_k(t)]; end % SOC递推 Constraints = [Constraints, SOC{k}(1) == SOC_init]; for t = 1:T Constraints = [Constraints, SOC{k}(t+1) == SOC{k}(t) + eta_ch*P_ch{k}(t) - P_dis{k}(t)/eta_dis]; Constraints = [Constraints, SOC_min <= SOC{k}(t) <= SOC_max]; end % theta与场景成本的关系 Constraints = [Constraints, theta >= sum(a_g*P_g{k} + c_shed*P_shed{k})]; end % 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 0); optimize(Constraints, objective, ops);注意几个实现要点:
- 一阶段决策变量((P_{buy}, P_{sell}, v))对所有场景共享,不能在每个场景里重复定义。否则不同场景对购电功率的要求互相矛盾,主问题会变得过紧。
- 储能SOC对每个场景独立定义,因为不同场景下充放电策略不同,SOC轨迹自然不同。但初始SOC是固定的,因为它是日前已知的状态。
- (\theta \geq) 每个场景的二阶段成本,这个约束是CCG主问题的核心,它把二阶段成本“提升”到一阶段目标函数里。
4.3 子问题SP的求解:从max-min到单层max
子问题SP要解决的是:给定一阶段决策 (x^*),在不确定集合内寻找使二阶段成本最大的不确定参数 (u)。形式是 max-min 双层结构。
对线性规划而言,内层min问题可以取对偶,把 max-min 变成 max-max 的单层问题。对偶转换的关键公式是:内层min的原问题和对偶问题有相同的最优值(强对偶成立),所以:
[ \max_{u \in \mathbb{U}} \min_{y} d^T y = \max_{u \in \mathbb{U}} \max_{\lambda} \left{ b(u)^T \lambda \mid A^T \lambda \preceq d, \lambda \geq 0 \right} ]
其中 (b(u)) 是包含不确定参数的约束右端项。对偶之后,原问题的不确定参数 (u) 会进入目标函数的乘积项,和目标函数中的 (u) 以及对偶变量 (\lambda) 形成双线性项。这是两阶段鲁棒子问题求解中最麻烦的非线性来源。
YALMIP处理双线性项的能力有限,我的做法是保留双线性项,直接把子问题写成含有双线性目标函数的优化问题,交给Gurobi的非凸二次规划求解器处理。对于小规模系统(24时段、单机组、单储能),实测求解速度是够用的,平均每次子问题求解约1~3秒。
子问题的核心代码如下:
% 子问题SP:给定一阶段变量x_star,求解最坏场景u % 转换为对偶问题后,最大化 (b(u)^T * lambda) % 对偶变量定义 lambda_g = sdpvar(T, 1); % 机组出力约束对偶 lambda_balance = sdpvar(T, 1); % 功率平衡对偶 % 不确定变量 z(盒式+预算) z = sdpvar(T, 1); % 目标:最大化 对偶表达式 % 注意这里的b(u)包含P_pv_base + delta_pv*z等项 objective_sub = sum(lambda_balance .* (P_L_base - P_pv_base + delta.*z)) ... + 其他对偶项; % 约束:对偶可行域 + 不确定集 Constraints_sub = [对偶变量非负约束, A'*lambda <= d]; Constraints_sub = [Constraints_sub, -1 <= z <= 1]; Constraints_sub = [Constraints_sub, sum(abs(z)) <= Gamma]; % 求解 optimize(Constraints_sub, -objective_sub, ops); % Gurobi最小化,取负号 z_star = value(z);这个子问题求解完成后,(z^*) 就是当前迭代轮次发现的最坏场景。
4.4 关键场景辨别算法的代码实现
我在CCG循环里加入的关键场景辨别逻辑,核心是一个冗余度判断函数:
function [flag, delta_cost] = check_scene_redundance(mp_result, new_z, params) % 输入:主问题当前结果mp_result,新场景new_z % 输出:flag=1表示该场景需要加入主问题,delta_cost为成本变化量 % 构建临时主问题:在现有场景基础上加入新场景 K = length(params.existing_scenes); params.existing_scenes{K+1} = new_z; % 求解加入新场景后的主问题 [obj_new] = build_and_solve_MP(params); % 计算与当前最优值的差异 delta_cost = abs(obj_new - mp_result.obj_current) / mp_result.obj_current; % 阈值判断 if delta_cost > 0.001 % 0.1% flag = 1; else flag = 0; end end在主循环里,加入场景辨别后,CCG的收敛逻辑变成:
% CCG主循环(带关键场景辨别) for iter = 1:max_iter % 求解主问题MP [x_star, obj_MP] = solve_MP(existing_scenes); % 求解子问题SP,得到最坏场景z_new [z_new, obj_SP] = solve_SP(x_star); % 关键场景辨别:检查z_new是否冗余 [flag, delta_cost] = check_scene_redundance(obj_MP, z_new, params); if flag == 0 % 场景冗余,进入下一轮迭代 iter = iter + 1; continue; end % 场景有效,加入主问题 existing_scenes{end+1} = z_new; % 计算上下界间隙 UB = obj_SP; LB = obj_MP; gap = abs(UB - LB) / abs(LB); % 定期清理冗余场景 if mod(iter, 5) == 0 existing_scenes = clean_redundant_scenes(existing_scenes, x_star); end if gap < tol break; end end场景清理函数里,我对每个已加入的场景约束检查其在最新主问题解处的对偶乘子(或称为“影子价格”),乘子接近0的场景标记为冗余,如果在后续连续三轮迭代中都没被重新激活,就从主问题中删除。这个策略实测效果不错,既保持了主问题规模的压缩,又不会误删真正起作用的场景。
需要说明的是,不同论文对关键场景辨别算法的实现侧重不同,有的是对场景做聚类去重,有的是对场景的“劣化程度”做排序。我用的这套“冗余检测+定期清理”是从工程加速角度出发的,它不改变CCG的理论收敛性,但对求解效率的提升非常显著。
5. 仿真结果解读与参数标定
5.1 基准案例设置
为了验证两阶段鲁棒模型和关键场景辨别算法的实际效果,我设计了三个对比案例:
| 案例 | 模型 | 预算参数 (\Gamma) | 场景辨别 |
|---|---|---|---|
| A | 确定性优化 | 不适用 | 无 |
| B | 两阶段鲁棒(纯CCG) | 8 | 无 |
| C | 两阶段鲁棒(CCG+场景辨别) | 8 | 有 |
光伏预测最大偏差设为预测值的20%,负荷预测最大偏差设为10%,可调节负荷比例为15%。仿真周期为24小时,时间粒度1小时。
5.2 运行成本与求解效率对比
结果如下:
| 指标 | 案例A | 案例B | 案例C |
|---|---|---|---|
| 日前调度成本(元) | 4862 | 5680 | 5691 |
| 回测实际成本(元) | 5890 | 5640 | 5648 |
| 主问题最终场景数 | 无 | 47 | 9 |
| 总求解时间(秒) | 3.4 | 215 | 58 |
这个结果很能说明问题。确定性模型的日前成本最低,但回测实际成本最高——因为它低估了不确定性的影响,真实运行时需要大量高价现货购电来弥补缺口。两阶段鲁棒模型的日前成本比确定性高16.8%,但回测实际成本反而低4.3%,因为鲁棒方案预留了足够的安全余量,实际运行时不需要惊慌失措地高价买电。
案例B和案例C的日前成本和回测成本几乎相同(差异都在0.3%以内),但求解时间从215秒降到了58秒,主问题场景数从47个压缩到9个。关键场景辨别算法在这里的贡献非常清晰:它没有显著改变最优解的质量,但用更少的场景约束达到了几乎相同的鲁棒水平,把求解时间压缩到了原来的四分之一。
5.3 预算参数 (\Gamma) 的敏感性分析
我在案例C的基础上扫描了 (\Gamma) 从0到12的取值范围:
| (\Gamma) | 日前成本(元) | 回测实际成本(元) | 主问题场景数 |
|---|---|---|---|
| 0 | 4862 | 5890 | 0(确定性) |
| 2 | 5120 | 5740 | 4 |
| 4 | 5380 | 5685 | 6 |
| 8 | 5680 | 5650 | 9 |
| 12 | 6120 | 5610 | 12 |
这里有一个很重要的工程结论:(\Gamma) 从0增加到8的过程中,日前成本单调上涨,回测实际成本单调下降;但超过8以后,回测成本几乎不再下降,而日前成本还在继续上涨。这意味着 (\Gamma=8) 附近是这个24时段系统的最优保守度——再多设预算参数只会白白增加计划成本,却换不来实际运行收益。
这个“平台期”现象是选择 (\Gamma) 的最直观依据。我在项目里通常建议直接用二分法找这个拐点,而不是拍脑袋定值。
5.4 主问题场景数的动态变化
我还记录了关键场景辨别算法作用下的场景数量变化过程。CCG迭代过程中,主问题场景数的增长曲线明显变缓:
- 前3轮迭代:每轮都加入新场景(此时子问题发现的场景都显著提升下界);
- 第4到8轮:大约每两轮加入一个新场景(部分场景被辨别为冗余);
- 第8轮之后:连续多轮找不到显著提升下界的场景,触发收敛判定。
从子问题的视角看,越到后期,新生成的最坏场景和已有场景的“区分度”越低。场景辨别算法实际上是在量化和利用这个区分度,加速了“无意义迭代”的退出。
在实际使用中,我把阈值设在0.1%时收敛最快;阈值太小(比如0.01%)会导致几乎每轮都添加场景,退化成普通CCG;阈值太大(比如1%)会让鲁棒解偏乐观,回测时可能出现越限风险。0.1%是我这个系统比较理想的平衡点。
6. 调试中反复踩的坑和最终心得
6.1 对偶问题不可忽略的强对偶条件
子问题从max-min取对偶转成单层max,前提是内层min问题满足强对偶。对线性规划而言,这要求原问题有可行解。但二阶段问题在极端场景下可能出现不可行——比如负荷极高、光伏为零时,即使机组满发、储能全放,也不一定能满足功率平衡。
我最初调试时,CCG跑到第5轮突然报错,Gurobi返回infeasible,原因就是某个极端场景下二阶段问题没有可行解。解决办法有两种:
- 在二阶段模型中加入可调负荷削减变量,允许极端场景下切负荷,确保问题始终有解;
- 修改变不确定集边界,让负荷偏差不超过可调节水平。
推荐第一种方案,因为切负荷本身是有实际意义的运营商决策,而且代价可以设为较高但有限,不影响正常场景下的优化行为。
6.2 双线性项的处理:不要过度线性化
子问题对偶之后,目标函数里会出现对偶变量和不确定变量相乘的双线性项。我在第一次实现时试图引入辅助变量和Big-M把所有双线性项线性化,结果不仅模型膨胀得非常厉害,求解速度反而更慢了,还引入了额外的数值稳定性问题。
后来我直接保留双线性项,把子问题当作非凸二次规划交给Gurobi求解。对于24时段的微网模型,实测效果很好。如果你的Gurobi版本或许可证不支持非凸二次规划,有一个替代方案:固定对偶变量,先对不确定变量最大化;再固定不确定变量,对偶变量最大化,交替迭代几次也能收敛到局部最优,但需要注意它不保证全局最优。
6.3 一阶段变量和二阶段变量的耦合要反复检查
这是两阶段鲁棒建模中最隐蔽的错误来源。一阶段变量在二阶段问题中表现为常数(由主问题求得),因此在写子问题时,凡是出现一阶段变量的约束,都要用value提取后的数值代入,而不是保留符号变量。我踩过的一次坑是:机组出力上限本应是 (P_{g,t} \leq P_{g,max} \cdot v_t^*),结果代码里写成了 (P_{g,t} \leq P_{g,max} \cdot v_t),v_t被当成符号变量传进了子问题,YALMIP报了一长串未知错误,排查了大半天才发现是把标量赋值写成了向量表达式。
6.4 热启动对CCG迭代的帮助远超想象
CCG是串行结构:主问题和子问题交替求解,后者以上一轮的最优解为输入。Gurobi的热启动机制对这类问题效果显著。我在第一次实现时没有显式设置热启动,每轮主问题都从零开始,效率很低。后来通过YALMIP的assign和sdpsettings('gurobi.Start', 1)接口,把上一轮的主问题解作为初始可行解传给下一轮,主问题求解时间平均下降了30%到40%。
子问题由于结构每轮都在变化(一阶段变量值不同),热启动收益有限,但主问题几乎每次都是相同的变量集合、不同的场景约束,热启动非常适合。
6.5 最后一点经验:不要盲目追求最坏场景的精确性
两阶段鲁棒优化本质上是一个保守的决策框架,它的输出是“在最坏情况下的最优决策”,而不是“在所有情况下的最优决策”。在实现过程中我逐渐认识到,与其花大量精力让子问题精确求解到最优,不如把门槛设置在“找到足够恶劣的场景”即可。工程上,子问题的次优解(比如间隙5%以内)已经足以推动CCG收敛到接近最优的鲁棒解,而节省下来的计算时间可以用来做更多参数敏感性分析。
这也是我为什么推荐关键场景辨别算法的另一个原因——它本质上是把计算资源从“枚举所有可能的糟糕情况”转移到“只关注真正影响决策的少数情况”上,这个思路在任何鲁棒优化项目里都适用。如果你现在正被CCG迭代慢、主问题规模爆炸或者收敛曲线迟迟压不下来困扰,不妨沿着这个方向试试,也许你的系统也会有同样的提速效果。