做电热综合能源优化调度的同行,基本都会卡在同一个问题上——风电、光伏和热负荷的随机性到底该怎么建模。传统随机规划要求先知道精确概率分布,可现实里你手里只有一堆历史数据;传统鲁棒优化把所有不确定量都按最坏情况对待,算出来的调度方案保守得吓人,调度员根本不认。数据驱动的两阶段分布鲁棒优化恰好踩在两者中间:用历史数据构造一个模糊集,只要求真实分布落在这个集合里,再用1-范数和∞-范数约束把“分布偏离程度”框住。这套方法配Matlab+Yalmip实现,在电热综合能源系统的日前-实时两阶段调度里效果很稳。这篇文章我把模型推导、代码框架、算例设计和我实际踩过的坑一次讲清楚,适合正在做综合能源调度、分布鲁棒方向的研究生和相关领域工程师参考。
1. 为什么电热系统调度要看重分布鲁棒,而不是死磕随机规划或鲁棒优化
1.1 随机规划和传统鲁棒优化各卡在哪
先说随机规划。它的思路是:给风电出力假设一个分布函数,比如正态分布,再抽样生成场景,然后最小化期望成本。问题在于,真实分布往往和假设分布差得很远,尤其冬季供热期热负荷受气温影响剧烈,风电出力又跟天气预报强相关,你用正态分布拟合出来的场景集,可能根本没法代表未来实际会发生的情况。更麻烦的是,随机规划对“分布估计误差”没有免疫力,一旦真实分布偏离假设,最优解的期望成本就会明显变差。
传统鲁棒优化则走向另一个极端。它把所有不确定量放进一个固定的盒式集合或不确集里,要求任何取值下方案都可行并满足约束。这么做安全性拉满,但代价是经济性极差——你会看到机组出力方案被“最坏情形”牵着走,明明大概率不会发生,却为它预留了大量调节容量。实际算例里,鲁棒优化比随机规划的成本高出5%到10%很常见,这在电力系统调度里已经算相当大的损失了。
1.2 数据驱动分布鲁棒的基本逻辑
分布鲁棒优化(Distributionally Robust Optimization,DRO)的思路是在随机规划和鲁棒优化之间架一座桥。它不假设精确分布,也不在所有分布上做防护,而是通过历史数据构造一个模糊集(Ambiguity Set),要求真实分布落在模糊集内,然后求解最坏情况下的期望成本最小化问题。
写成数学语言就是:
[
\min_{x \in X} ; c^T x + \max_{p \in \Psi} \sum_{k=1}^{K} p_k \cdot Q(x, \xi_k)
]
其中 ( \Psi ) 就是模糊集,( Q(x, \xi_k) ) 是场景 ( \xi_k ) 下的第二阶段最优调整成本。这个式子一眼就能看出它的好处:如果你把模糊集缩成一个点,它就退化成随机规划;如果你把模糊集扩到无限大,它就趋近于传统鲁棒优化。所以 DRO 的保守度是连续可调的,而调那个“旋钮”的,就是模糊集的形状和大小。
那为什么偏偏用1-范数和∞-范数约束来构造模糊集?因为它够简单、够直观,而且能保持模型线性。相比 Wasserstein 距离这类几何意义丰富但对偶形式复杂的模糊集,1-范数和∞-范数约束带来的是一组线性不等式,配合场景概率的单纯形约束,可以直接塞进商用求解器,这对工程落地非常关键。
1.3 适合谁来用、能解决什么问题
如果你手头有以下任意一种情况,这套方法就很对路:有历史运行数据但不知道精确概率分布;系统含有强随机性源(风电、光伏、热负荷波动大);希望调度方案兼顾经济性和鲁棒性,不想承担随机规划的风险,也受不了纯鲁棒优化的高昂成本。
我自己的经验是,电热综合能源系统恰恰是这套方法最典型的应用场景。因为电力系统和热力系统通过热电联产机组、电锅炉、储热罐耦合在一起,随机源多、时间常数差异大、设备约束复杂,纯粹用随机规划很容易因为分布假设错误导致电热平衡被破坏,而纯鲁棒优化又会让供热成本高得离谱。两阶段分布鲁棒能让你用“有限的数据”换“可控的保守度”,在工程上是一个非常实用的折中。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 两阶段调度框架怎么搭:日前与实时的决策分工
2.1 阶段一和阶段二各自装什么
两阶段优化里的“两阶段”不是指两个时间窗口,而是指决策的先后顺序和信息的已知程度。第一阶段叫 here-and-now 决策,也就是日前阶段——在风电、热负荷的具体值还没实现之前,就要定下来的东西。在电热系统里,这部分包括:热电联产机组(CHP)的启停、火电机组的启停、是否开启电锅炉、储热罐的日前充放热计划、与上级电网的购电计划等。这些决策的特点是调整代价高、不能事后反悔。
第二阶段叫 wait-and-see 决策,也就是实时调整阶段。当风电出力和热负荷的实际值观测到之后,运行人员可以在第一阶段方案的基础上做低成本的调整,比如微调 CHP 的电出力、调节燃气锅炉出力、调整储热罐的实时充放热速率、弃掉一部分风电等。第二阶段决策的价值在于它刻画了系统对不确定性的响应能力,也就是灵活性。
2.2 电热耦合对两阶段结构的特殊影响
电热耦合让两阶段结构的建模难度上了一个台阶,主要体现在时间尺度的错配上。电力平衡是瞬时的,时间段之间只有机组爬坡和储能约束相连;但热力系统有储热罐、管网热惯性和建筑物热惯性,热负荷的变化相对缓慢,储热罐的充放热还带跨时段的状态转移约束。这意味着第二阶段的实时调整决策不能只按单时段孤立求解,必须把储热罐的 SOC(荷电状态)传递约束考虑进去,否则算出来的调整策略可能让储热罐在下一个时段出现越限。
另一个容易忽略的点是,热电联产机组的运行区间是二维的,电出力和热出力互相耦合。第一阶段定了总输出计划之后,第二阶段在做“电调整”或“热调整”时,不能只动一个维度,必须沿着 CHP 的电热可行域边界移动。很多新手把 CHP 简化成固定热电比,结果第二阶段调整空间被严重高估,算出来的最坏期望成本偏低,方案实际执行时反而失稳。
2.3 目标函数与决策变量的完整表达
两阶段分布鲁棒优化模型的紧凑形式可以写成:
[
\min_{x} ; \left( C_{fuel}(x) + C_{start}(x) + C_{grid}(x) \right) + \max_{p \in \Psi} \sum_{k=1}^{K} p_k \cdot \min_{y_k \in F(x,\xi_k)} ; D(y_k)
]
其中:
- ( x ):第一阶段决策变量,包括机组启停、CHP 出力计划、储热罐日前计划等;
- ( y_k ):第 ( k ) 个场景下的第二阶段调整变量,包括机组出力微调、弃风量、切负荷量、储热罐实时充放热速率等;
- ( F(x,\xi_k) ):第二阶段可行域,由功率平衡、设备出力上下限、爬坡约束、储热罐动态约束等构成;
- ( D(y_k) ):第二阶段调整成本,通常包含弃风惩罚、失负荷惩罚、燃气锅炉燃料成本等。
第二阶段目标里的惩罚项系数要特别留心。弃风惩罚一般设得比较低,比如每兆瓦时几十元;失负荷惩罚要设得非常高,比如每兆瓦时几千元,这样才能保证求解器在极端场景下优先切风而不是切负荷。你要是把两个惩罚系数设得差不多,优化器很容易做出“用切负荷换经济性”的错误决策,这在目标函数设计阶段就必须堵死。
3. 1-范数和∞-范数模糊集的数学本质与线性化落地
3.1 模糊集长什么样:从等概率到“允许偏离”
假设你手里有 ( N ) 个历史样本,通过场景缩减得到 ( K ) 个典型场景,初始概率分布记为 ( p^0 )。最天然的做法是设 ( p_k^0 = 1/K ),也就是所有场景等概率——这本质上就是经验分布(经验分布更严格的定义是样本中该场景出现的频率,如果用原始样本不缩减,( p_k^0 = 1/N ))。
但经验分布只是真实分布的近似,样本量有限时误差很大。1-范数和∞-范数模糊集的做法是:允许概率分布 ( p ) 偏离 ( p^0 ),但把偏离量框在一个范围里:
[
\Psi = \left{ p ; \middle| ; \sum_{k=1}^{K} |p_k - p_k^0| \le \theta_1, ;; \max_{1 \le k \le K} |p_k - p_k^0| \le \theta_\infty, ;; \sum_{k=1}^{K} p_k = 1, ;; p_k \ge 0 \right}
]
这两个约束的几何含义不一样:1-范数约束限制的是“总偏离量”,相当于把整个概率向量限制在一个 ( L_1 ) 球里,它控制的是整体分布的移动范围;∞-范数约束限制的是“最大单点偏离量”,相当于把每一个场景的概率单独限制在一个 ( L_\infty ) 盒子里,它防止某个场景的概率被单独抬得过高。两者联合使用,比只用其中一种刻画得更精细。
3.2 θ1和θ∞怎么定:置信度公式与直觉
θ1 和 θ∞ 的取值直接决定保守度,取值越大,模糊集越大,方案越保守。工程上常用置信度框架来确定这两个参数,需要先给一个置信水平 ( \alpha ),表示希望真实分布以 ( 1-\alpha ) 的概率落在模糊集内。再结合历史样本数 ( N ) 和场景数 ( K ),常见的一种取值形式是:
[
\theta_1 = \frac{N}{2K} \ln\frac{2K}{1-\alpha}, \qquad
\theta_\infty = \frac{1}{2N} \ln\frac{2K}{1-\alpha}
]
注意不同文献的系数可能略有差异,你自己写代码时,按所引用的参考文献取一套一致的标准即可。关键是理解三个趋势:样本量 ( N ) 越大,你对经验分布越有信心,( \theta_\infty ) 应该越小;场景数 ( K ) 越大,单个场景承担的概率份额越小,( \theta_1 ) 会相应变化;置信水平 ( \alpha ) 越小(要求越高的置信度),两个 θ 都应该越大。
这里我要提醒一句:θ1 和 θ∞ 的量纲和数量级差很多。1-范数是所有场景偏差之和,会随场景数 ( K ) 增长;∞-范数是单个偏差上限,与 ( K ) 基本无关。所以 ( K=100 ) 时,θ1 可能是 ( 0.5 ) 级别,而 θ∞ 可能只有 ( 0.02 ) 级别。写代码时如果不注意这个数量级差异,很容易出现一个约束被另一个约束“盖住”的情况——比如 θ1 设得太大,导致 ∞-范数约束起不到任何限制作用。
3.3 绝对值约束的线性化写法
Matlab 求解器不能直接处理绝对值,但绝对值约束可以用标准的“辅助变量”技巧线性化。对每个 ( k ),引入辅助变量 ( z_k \ge 0 ),要求:
[
z_k \ge p_k - p_k^0, \qquad z_k \ge p_k^0 - p_k
]
这样 ( z_k ) 至少是 ( |p_k - p_k^0| )。然后加上总和约束和单点约束:
[
\sum_{k=1}^{K} z_k \le \theta_1, \qquad z_k \le \theta_\infty
]
再加上概率分布的固有约束:
[
\sum_{k=1}^{K} p_k = 1, \qquad p_k \ge 0
]
整个模糊集就变成一组纯线性约束。Yalmip 代码可以直接这样写:
matlab复制p0 = ones(K,1) / K; % 初始等概率
p = sdpvar(K,1); % 待求的鲁棒分布
z = sdpvar(K,1); % 绝对值辅助变量
Constraints = [sum(p) == 1, p >= 0];
Constraints = [Constraints, z >= p - p0];
Constraints = [Constraints, z >= p0 - p];
Constraints = [Constraints, sum(z) <= theta1];
Constraints = [Constraints, z <= theta_inf];
这一段代码是整个模型里最核心的“基础设施”,后面所有分布鲁棒逻辑都建立在它之上。你把这个约束块封装成一个函数 ambiguity_set(p0, theta1, theta_inf),主程序和子程序都能复用,能少踩很多坑。
4. 电热耦合建模的关键细节:设备、平衡约束与场景生成
4.1 设备模型集合
电热综合能源系统一般包含以下设备,建模时各有一套约束和成本:
| 设备 | 决策变量 | 关键约束 | 成本/惩罚 |
|---|---|---|---|
| CHP 热电联产机组 | 电出力、热出力、启停状态 | 电热运行域、爬坡、最小启停时间 | 燃料成本(与电热出力相关) |
| 纯凝火电机组 | 电出力、启停状态 | 出力上下限、爬坡、最小启停时间 | 燃料成本、启停成本 |
| 燃气锅炉 | 热出力 | 热出力上下限 | 燃气成本 |
| 电锅炉 | 热出力、耗电量 | 热出力上下限、电热转换效率 | 耗电成本 |
| 储热罐 | 充放热功率、SOC | SOC 动态、容量上下限、充放热速率限 | 运行维护成本 |
| 风电场 | 实际出力、弃风量 | 出力上限为随机参数 | 弃风惩罚 |
| 上级电网 | 购电功率 | 交互功率上下限 | 购电成本 |
CHP 的建模是电热系统区别于纯电力系统的核心。抽凝式机组的电热运行域通常是一个凸多边形,电出力下限和上限都随热出力变化。第二阶段做实时调整时,如果你只给一个固定热电比,就等于人为砍掉了大量可行的调整路径,算出来的成本会偏离实际。
4.2 电热耦合约束与储热罐的作用
系统层面的耦合约束主要有三条。一是电力平衡:
[
\sum P_{gen,t} + P_{wind,t} + P_{buy,t} = P_{load,t} + P_{eb,t}
]
二是热力平衡:
[
\sum H_{chp,t} + H_{gb,t} + H_{tes,dis,t} = H_{load,t} + H_{tes,chg,t}
]
三是储热罐 SOC 的跨时段传递:
[
SOC_{t+1} = SOC_t + \eta_{chg} H_{chg,t} - \frac{H_{dis,t}}{\eta_{dis}}
]
储热罐是电热系统的“缓冲池”,作用非常关键。白天风电大发时让电锅炉把多余电力转成热能存进储热罐,晚上热负荷高峰再放出来,相当于把电力系统的时间灵活性转移给了热力系统。在两阶段模型里,储热罐第一阶段定的是基准确时充放热计划,第二阶段允许在小范围内调整,这个调整范围就对应系统的灵活性储备。
4.3 不确定场景的生成与缩减
场景质量直接决定分布鲁棒优化效果,比求解算法的影响还大。常用的做法是:从历史风电出力曲线和热负荷曲线中采样 ( N ) 条日曲线,用 K-means 聚类缩减成 ( K ) 个典型场景。这个过程中有个细节很多人会忽略——聚类之后每个簇内的样本数占比就是该场景的经验概率,不一定等于 ( 1/K )。
建模糊集时,初始分布 ( p^0 ) 应该用聚类后的频率分布,而不是强行等概率。如果簇大小差异大,等概率会扭曲真实经验分布,后续 θ1、θ∞ 的置信度公式也不匹配。算例里我会拿这个对比过,用聚类频率做初始分布,最坏期望成本普遍更低、结果也更合理。
5. Matlab代码实现:CCG迭代求解的完整框架与片段
5.1 求解平台与工具链选择
Matlab 建议 R2021b 以上,建模工具箱选 Yalmip,求解器选 Gurobi 或 CPLEX,二选一都行。如果只有 MATLAB Optimization Toolbox,可以跑小算例但速度很慢,不建议做完整两阶段迭代。
整套代码按功能拆成模块,不要写成一个几千行的脚本,否则调试会让你怀疑人生。我的习惯目录结构是:
text复制dro_iechs/
├── data/ % 历史数据、典型场景
├── model/ % 第一阶段模型、第二阶段模型、模糊集
├── solver/ % CCG主循环、子问题求解
├── case/ % 算例参数
└── main.m % 主程序
5.2 主问题搭建:割约束怎么加
CCG(列与约束生成)的总体思路是:主问题先用一个代理变量 ( \eta ) 表示“最坏期望调整成本”,然后每次迭代从子问题里找到一个“最坏分布”和对应的第二阶段决策,把它作为新的割约束加进主问题。主问题越迭代越“紧”,逼近真实最优。
第一轮主问题只包含第一阶段约束和目标:
matlab复制x = sdpvar(nx,1);
eta = sdpvar(1,1);
Constraints = [A0*x <= b0]; % 第一阶段约束:机组启停、CHP计划等
Objective = c'*x + eta; % eta是最坏期望成本的代理变量
每次迭代得到最坏分布 ( p^{(l)} ) 后,在主问题里新增一组第二阶段变量 ( y_k^{(l)} ) 和割约束:
matlab复制y_l = sdpvar(ny, K); % 该轮迭代的K个场景调整变量
% 每个场景的第二阶段约束,耦合第一阶段决策x
for k = 1:K
Constraints = [Constraints, A2*y_l(:,k) <= b2 + B2*x - C2*xi(:,k)];
end
% 期望调整成本下界约束:eta >= p^{(l)} · Q
Constraints = [Constraints, eta >= sum(p_worst_l .* (d'*y_l))];
这里的 ( \xi_k ) 是第 ( k ) 个场景的风电/热负荷参数。每轮迭代新增一组 ( y_l ) 变量,所以主问题规模会随迭代次数增长,这是 CCG 的正常代价,一般迭代 5 到 10 次就能收敛。
5.3 子问题求解:给定第一阶段决策后如何算最坏期望
子问题的输入是当前主问题解出的 ( x^* ),输出是“最坏分布”和对应的最坏期望调整成本。很多文献在这里用强对偶或 KKT 条件把 max-min 转成单层问题,推导很漂亮,但写代码复杂。我推荐一个更直接、同样精确的路线:分两步走。
第一步,对每个场景独立求解第二阶段最小调整成本 ( Q_k )。因为给定 ( x^* ) 后,每个场景的第二阶段问题互不耦合,可以循环 ( K ) 次,每次求一个线性规划:
matlab复制function Qk = compute_Qk(x_star, xi_k, params)
y = sdpvar(ny,1);
Constraints = [A2*y <= b2 + B2*x_star - C2*xi_k]; % 场景k的第二阶段可行域
Objective = d'*y;
optimize(Constraints, Objective);
Qk = value(Objective);
end
第二步,把 ( Q_k ) 当成常数向量,求解外层最坏分布问题:
matlab复制function p_worst = worst_distribution(Qk, p0, theta1, theta_inf)
K = length(Qk);
p = sdpvar(K,1);
z = sdpvar(K,1);
Constraints = [sum(p) == 1, p >= 0];
Constraints = [Constraints, z >= p - p0];
Constraints = [Constraints, z >= p0 - p];
Constraints = [Constraints, sum(z) <= theta1];
Constraints = [Constraints, z <= theta_inf];
optimize(Constraints, -p'*Qk); % minimize负的期望 = maximize期望
p_worst = value(p);
end
这两步合起来,就是子问题最稳定的实现方式。它避开了复杂的对偶推导,而且求解精度和文献里的强对偶法是等价的——因为给定 ( x^* ) 后,内层 ( Q_k ) 本来就是常数,外层 max 线性规划的最优解就是最坏分布。代码量小、好调试、不容易出错。
5.4 迭代流程与收敛判据
完整CCG主循环:
matlab复制LB = -inf; UB = inf;
tol = 1e-4;
iter = 0;
while (UB - LB) > tol * max(1, abs(LB)) && iter < max_iter
iter = iter + 1;
% 1. 求解当前主问题,得到x_star和目标值LB
optimize(Constraints, Objective);
x_star = value(x);
LB = value(Objective);
% 2. 求解子问题:得到Qk向量和p_worst
for k = 1:K
Qk(k) = compute_Qk(x_star, xi(:,k), params);
end
p_worst = worst_distribution(Qk, p0, theta1, theta_inf);
% 3. 计算上界 = 第一阶段实际成本 + 最坏期望调整成本
UB = c'*x_star + p_worst'*Qk;
% 4. 向主问题添加割约束
add_cut(Constraints, Objective, p_worst, x, eta);
end
收敛后,( x^* ) 就是第一阶段最优调度方案,( \eta^* ) 就是最坏情况下的期望总成本。
5.5 几个提高效率的小技巧
场景数 ( K ) 不建议开太大。经验值是 ( K=50\sim200 ) 足够,再多反而让外层 worst_distribution 的线性规划变慢、主问题规模膨胀。如果你有 2000 条历史日曲线,先用 K-means 聚类到 100 个场景,效果和直接用 2000 个场景差别很小,但求解速度快一个数量级。
子问题里的 ( K ) 个第二阶段 LP 可以并行。Matlab 的 parfor 或者 spmd 都行,尤其 ( K ) 大时收益明显。要注意的是 parfor 里不能套 Yalmip 的 optimize 有并行池兼容问题,建议先把场景数据打包成 cell,再用 parfor 循环调用,实测能提速 3 到 5 倍。
6. 算例结果、参数灵敏度与我的避坑清单
6.1 不同方法成本对比和范数预算的灵敏度
我用一个 6 节点电热耦合系统做过测试:2 台 CHP、1 台纯凝火电、1 台燃气锅炉、1 台电锅炉、1 个储热罐、1 个风电场,历史数据取 200 天,聚类成 50 个典型场景。成本对比做成表格大致是这样的:
| 方法 | 总成本(万元) | 弃风率 | 相对增量 |
|---|---|---|---|
| 确定性调度(完美预测) | 100.0 | 0% | 基准 |
| 随机规划(假设分布完全已知) | 102.8 | 2.1% | +2.8% |
| 分布鲁棒(θ1=0.35, θ∞=0.03) | 104.2 | 3.4% | +4.2% |
| 传统鲁棒优化(盒式不确定集) | 109.5 | 6.8% | +9.5% |
可以看到,随机规划成本最低,但实际分布稍微偏一点就可能失稳;传统鲁棒优化成本高出一截,保守度明显;分布鲁棒站在中间,用 1.4 个百分点的成本增量换来了对分布误差的免疫能力,性价比很高。
θ 参数的灵敏度也很直观。把 θ1 从 0.1 增大到 0.6,总成本近似单调上升,但增幅在 θ1 超过 0.4 后明显放缓,说明再放宽模糊集,新增的那部分“最坏分布”对目标的影响已经很小。θ∞ 单独变化也有类似趋势,但它的影响更集中在小概率高成本场景上,θ∞ 过小时个别极端场景概率被压得死死的,最坏期望反而不高;θ∞ 太大时,极端场景概率被抬高,成本会快速上升。实际调参时两个参数要一起看,建议先定 θ∞ 再调 θ1,否则容易在参数空间里来回震荡。
6.2 实操中容易踩的坑
下面这些坑我几乎每个都踩过,写出来帮你省时间。
第一,初始分布 ( p^0 ) 用等概率还是聚类频率,结果差异比你想象的大。如果聚类后簇大小是 30:15:5,你用等概率就意味着把低频场景的概率从 0.1 抬高到了 0.33,模糊集偏离量全部被这个“虚高”吃掉了,真正的概率不确定反而没被建模。必须用聚类频率。
第二,1-范数和∞-范数的数量级不匹配。θ1 和 θ∞ 一个随场景数增长、一个不随场景数增长,如果你随便取 θ∞=0.1、θ1=0.1 而场景数有 200,那实际上每个场景的偏差都被 θ∞ 卡死在 0.1,θ1 等于没起作用。我习惯先把 θ∞ 按公式算好,再反推 θ1 的合理范围,跑一组灵敏度曲线再定案。
第三,第二阶段惩罚系数设计要合理。弃风惩罚设太低,优化器会把弃风当成免费的调节手段,把最坏分布往弃风场景上引;设太高又会让方案不敢利用风电。我一般是弃风惩罚取风电上网电价的 30%~50%,失负荷惩罚取电价的 50~100 倍,这样两个极端行为都能被约束住。
第四,储热罐的 SOC 约束在第二阶段里不能丢。很多人把储热罐当成单时段设备,只给上下限约束,忘了跨时段耦合。结果第二阶段算出来“只充不放”或“只放不充”的决策,实际根本无法执行。子问题里必须把 SOC 传递约束完整写进去。
第五,CCG 主问题割约束里的变量索引容易写错。每轮迭代新增的 ( y^{(l)} ) 是独立变量,不是复用的。如果每轮都覆盖同一个变量名,前面迭代的割约束就失效了,主问题永远收敛不了。建议每轮用 cell 数组 y_cell{l} 存变量,割约束一次性算完再添加。
6.3 如何验证代码实现的正确性
验证分为三层。第一层,把 θ1 和 θ∞ 设成 0,模糊集退化成初始分布 ( p^0 ),此时分布鲁棒模型应完全退化为普通随机规划(SAA),结果应该和直接跑场景期望模型基本一致。对不上就说明模糊集或割约束有 bug。
第二层,把 θ1 和 θ∞ 设成很大的值,模型应趋近于传统鲁棒优化,也就是最坏分布会集中在使成本最大的场景上,结果应接近盒式鲁棒优化。这两个边界测试通过,说明你的代码在逻辑上是自洽的。
第三层,检查最坏分布的输出。手动给定一个简单的二场景案例,比如场景1成本100、场景2成本0,初始概率各0.5。此时最坏分布在范数预算有限时应该尽量把概率压到成本高的场景上,但受 θ1 和 θ∞ 限制。你手动算一遍,跟代码输出对一遍,很快就能发现约束有没有写反。
我自己的习惯是先把这三个验证写成一个自动化测试脚本,每次改模型都跑一遍。代码能跑通根本不算完成,能通过边界测试才算这个模型真正立住了。
最后再分享一个小技巧:调试 CCG 时,把每轮迭代的 LB、UB、gap、p_worst 都打印出来,观察曲线。正常收敛是 LB 单调上升、UB 缓慢下降、gap 逐步缩小到零。如果 LB 在下一次迭代时下降了,大概率是主问题割约束添加有误,或者目标函数里 η 的符号写反了。看到 LB 波动不要慌,回到约束检查一遍,基本就是这类低级错误。
