☰
微网两阶段鲁棒经济调度与CCG算法MATLAB实现
2026/10/8 4:12:59 网站建设 项目流程

1. 为什么微网调度要从"确定性"走向"两阶段鲁棒"

1.1 一次光伏骤降让确定性调度直接翻车

去年年底我在做一个园区级微网的日前经济调度项目,碰到的第一个问题就非常典型:光伏和风电的预测曲线在早上看起来非常漂亮,预测中午光伏能到120kW,结果上午十点一片云飘过来,光伏出力从75kW直接掉到35kW。当时用的是最传统的确定性调度,所有风光出力都按预测值当作已知参数喂给优化模型,燃气轮机出力和储能充放电曲线也是按这个预测排好的。实际运行中光伏一掉,燃气轮机因为爬坡约束没法立刻补上,储能又已经在低谷时段放得差不多了,最后只能从主网买高价电,那一天的综合运行成本比调度计划超支了12%还要多。

这个经历让我意识到,微网优化调度如果只盯着"预测值"做经济调度,本质上是在赌天气。后来我把求解框架换成了两阶段鲁棒优化,配合CCG(列与约束生成)算法,在MATLAB+Yalmip环境里重新搭建了整个日前经济调度模型。这不是一个简单的"多套几个场景"的思路,而是整个优化问题的结构都变了:把决策分成"今天就要定下来的事"和"等不确定性揭晓后再调整的事"两层,然后专门针对最坏的不确定性做应对。

1.2 确定性优化、随机优化、鲁棒优化:三种思路的取舍

在做微网调度之前,得先分清三种主流建模思路的适用边界,不然很容易选错方向。

第一种是确定性优化,模型长这样:

[ \min_{x} ; c^Tx \quad s.t. ; Ax \ge b ]

这里的负荷、光伏、风电全用预测值。优点是模型简单,LP或者MILP一求解就完事,速度快。缺点也明摆着:预测和实际的偏差直接变成电网的安全风险或者成本风险。实际工程中,靠人工预留旋转备用去对冲误差,本质上还是经验主义。

第二种是随机规划(Stochastic Programming),思路是把不确定性描述成若干离散场景,每个场景有一个概率,目标函数变成期望成本最小。比如用蒙特卡洛采样生成1000个光伏出力场景,然后求期望。随机规划的强项是有概率意义,理论上很优雅;但它的痛点也很实际:场景多了模型爆炸,场景少了概率分布又不准。而且随机规划对分布假设敏感,如果你把分布形状搞错了,解出来的"期望最优"可能还不如确定性调度。

第三种就是鲁棒优化(Robust Optimization),它不问概率分布,只给一个不确定集合,把问题写成:

[ \min_{x} \max_{u \in U} ; c^Tx + f(x,u) ]

要求的是"在集合内任意一个不确定性实现下,方案都可行且代价可控"。鲁棒优化不追求期望最优,它追求的是最坏情况下的保证。代价是方案必然偏保守,但换来的是安全性。两阶段鲁棒优化是鲁棒优化在"决策有先后"场景下的升级版:一部分决策必须在不确性实现之前敲定,另一部分决策可以等看到实际风光出力之后再调整。

1.3 两阶段鲁棒在微网场景下的适配性

为什么微网经济调度特别适合两阶段鲁棒?因为微网的运行逻辑本身就是分层的。日前阶段,燃气轮机要不要开、开几台、是否和主网签订购电协议,这些涉及整数变量的决策必须提前确定,执行层面没法等到实时再改。而在实时阶段,光伏实际出力出来了,燃气轮机出力、储能充放电、主网交换功率这些连续量是可以快速调整的。两个阶段决策的"自由度"完全不同,恰好对应两阶段鲁棒优化的结构。

用一句话概括这个思路:第一阶段花钱买"选项",第二阶段根据最坏情况动用"选项"把成本压住。

这套东西做出来之后,我最大的感受是:两阶段鲁棒不是让你去准确预测不确定性,而是让你在不确定性真正发生的时候,有一个事先算好的、能兜底的调整策略。对微网这种源荷双重波动、设备响应速度又各不相同的系统来说,这个特性太重要了。

2. 两阶段鲁棒经济调度的数学模型:变量、目标与不确定集

2.1 微网设备、变量划分与运行假设

先把我搭建的微网系统讲清楚。为了兼顾论文级严谨和代码可实现,我选了包含微型燃气轮机、储能、光伏、风电、主网连接和本地负荷的经典拓扑。

  • 微型燃气轮机(MT):最小出力20kW,最大出力80kW,爬坡约束30kW/h,燃料成本用二次函数近似;
  • 储能系统(ESS):容量200kWh,最大充放电功率50kW,充放电效率0.95,SOC限制在0.1~0.9,初始SOC为0.5;
  • 光伏+风电:不可控,出力落在不确定集合内;
  • 主网交互:允许购电和售电,购售电价分时,且有交互功率上限;
  • 负荷:本文先按确定值处理,实际工程中可以方便地把负荷波动也塞进不确定集合。

变量划分是两阶段鲁棒模型的核心。第一阶段决策变量是所有必须在不确定性实现前定下来的整数量和部分合同量:

  • 燃气轮机启停状态 (y_{on,t});
  • 与主网的购/售电状态 (y_{grid,t})(整数);
  • 储能是否处于充电状态 (y_{ch,t})(防止同时充放电的整数约束)。

第二阶段决策变量则是在看到实际风光出力后可以调整的连续量:

  • 燃气轮机出力 (P_{MT,t});
  • 储能充放电功率 (P_{ch,t}, P_{dis,t});
  • 主网购/售电功率 (P_{buy,t}, P_{sell,t});
  • 弃光/弃风量 (P_{curt,t})、切负荷量 (P_{cut,t})。

这里有两条运行假设需要说明。第一,储能同时充放电约束是用整数变量表达的,在日前调度里基本是标配,不然会出现"充电又放电"这种物理上不成立但数学上省钱的操作。第二,我把切负荷和弃风弃光放进了第二阶段,并赋予很高的惩罚系数,目的是保证模型在极端场景下"有退路",同时也让经济性指标更有解释力。

2.2 目标函数与约束的完整建模

两阶段鲁棒优化目标的标准写法是:

[ \min_{y} ; c^T y + \max_{u \in U} ; \min_{z \in F(y,u)} ; d^T z ]

其中 (y) 是第一阶段决策,(z) 是第二阶段调整变量,(u) 是不确定变量。

第一阶段的成本 (c^Ty) 包括燃气轮机启停成本,我按单台单次启停300元计。第二阶段成本 (d^Tz) 则包含:

  • 燃气轮机燃料成本,用分段线性化逼近二次项;
  • 向主网购电成本减去售电收入;
  • 弃风弃光惩罚和切负荷惩罚,后者单价设为1000元/MWh,远高于正常发电成本。

功率平衡约束是模型的主心骨,对每个时段 (t):

[ P_{MT,t} + P_{dis,t} - P_{ch,t} + P_{PV,t} + P_{WT,t} + P_{buy,t} - P_{sell,t} + P_{cut,t} = P_{load,t} + P_{curt,t} ]

这里我把弃风弃光量放在等式右侧,表示"多余电量可以被主动放弃",切负荷量放在左侧,表示"需求不足时允许切除部分负荷"。

燃气轮机约束包括出力上下限、爬坡约束:

[ P_{MT}^{min} y_{on,t} \le P_{MT,t} \le P_{MT}^{max} y_{on,t} ]

[ -P_{ramp} \le P_{MT,t} - P_{MT,t-1} \le P_{ramp} ]

储能约束包括功率限制、SOC递推、SOC上下限:

[ SOC_{t+1} = SOC_t + \frac{\eta_{ch} P_{ch,t} - P_{dis,t}/\eta_{dis}}{E_{ESS}} ]

[ 0 \le P_{ch,t} \le P_{ch}^{max} y_{ch,t}, \quad 0 \le P_{dis,t} \le P_{dis}^{max} (1-y_{ch,t}) ]

主网交互约束比较简单,购售电功率各有一个上限,且通过整数变量保证不会同时买卖:

[ 0 \le P_{buy,t} \le P_{grid}^{max} y_{grid,t}, \quad 0 \le P_{sell,t} \le P_{grid}^{max} (1-y_{grid,t}) ]

模型到这里还是比较标准的。从代码实现角度看,这些约束绝大多数只涉及第二阶段变量,只有启停状态、购售电状态这些整数变量来自第一阶段,这正是CCG算法适用的典型结构。

2.3 不确定集设计:盒式集合与预算约束

两阶段鲁棒优化的"鲁棒程度"完全由不确定集合 (U) 决定。集合越大,方案越保守,成本越高;集合越小,方案越激进,风险越大。这一步如果处理不好,后面所有计算都是白搭。

我最开始用的是最朴素的盒式不确定集:

[ U = \left{ u ;|; u_j^{min} \le u_j \le u_j^{max}, \forall j \right} ]

每个不确定量独立地在区间内波动。光伏预测120kW,允许±15%,那光伏出力就在102kW到138kW之间波动;风电预测60kW,允许±25%,那就在45kW到75kW之间波动。

但盒式集合的麻烦在于:它会让模型默认所有不确定量同时取最坏值,这是极端保守的。真实场景中,光伏和风电同时达到最坏边界的概率极低,用盒式集合做出来的调度成本会明显偏高。

因此我在模型中引入了预算约束(Budget Constraint):

[ \sum_j \frac{|u_j - u_j^{pred}|}{\Delta u_j} \le \Gamma ]

这个 (\Gamma) 就是"总偏差预算",它限制的是所有不确定量偏离预测值的总幅度。比如24个时段,(\Gamma=4) 意味着最多等效于4个时段的不确定变量同时取到边界,其他时段只能在预测值附近小幅波动。(\Gamma) 越大越保守,(\Gamma=0) 等价于确定性调度,(\Gamma=24) 等价于盒式集合。

预算约束的好处非常多。第一,它让不确定集合的"体积"可调,给了调度员一个直观的经济鲁棒性权衡旋钮;第二,它在数学上保持了线性结构,配合CCG迭代可以采用MILP求解;第三,实践中最坏场景一定会落在边界点组合上,这让后面的对偶和线性化变得有迹可循。

3. CCG算法拆解:主问题-子问题迭代如何求解min-max-min

3.1 为什么Benders不够,CCG更合适

第一次看到两阶段鲁棒模型的时候,我的第一反应是:能不能用Benders分解?毕竟Benders分解在随机规划和确定性混合整数规划里是很成熟的技术。后来我仔细对比之后发现,CCG在处理两阶段鲁棒优化上明显更顺手。

Benders分解的思路是:主问题求解第一阶段决策,子问题求解第二阶段对偶,然后把子问题的最优值函数用一条对偶割(最优割)或可行性割加回主问题,迭代逼近原问题。问题在于,子问题返回的信息是"一个割平面",本质上是目标值的下界逼近,如果第二阶段存在整数变量,Benders切割的处理会非常麻烦,实际实现中经常出现振荡或收敛慢。

CCG的核心区别是:子问题每迭代一次,生成一个具体的最坏场景 (u^*),并把该场景对应的第二阶段变量和全部约束,原封不动地加入主问题。这相当于在主问题里"新增一列和一组约束",所以叫列与约束生成。因为每一轮都引入了完整场景的信息,而不是一个压缩的割平面,CCG的收敛速度通常远快于Benders。对于微网调度这种规模不太大、但场景约束很多的模型,CCG往往在3~5次迭代内就能给出满意的间隙。

3.2 主问题MP的构造与子问题SP的对偶化

CCG迭代的第一步,是把原始的两阶段模型拆成主问题MP和子问题SP。

主问题MP在每一轮迭代时包含当前已发现的全部最坏场景。假设已经迭代到第 (k) 轮,有 (k) 个已知场景 (u_1^, u_2^, ..., u_k^*),MP可以写为:

[ \min_{y, z_k, \eta} ; c^T y + \eta ]

[ s.t. ; \eta \ge d^T z_k, \quad \forall k ]

[ z_k \in F(y, u_k^*), \quad \forall k ]

[ y \in Y, ; z_k \in Z ]

辅助变量 (\eta) 的作用是逼近第二阶段的"最坏成本"下界。随着场景增多,MP被约束逐步收紧,目标函数值从下往上逼近真实最优值,这就是下界LB。

子问题SP是在给定第一阶段解 (y^*) 后,找最坏场景和对应的最小第二阶段成本:

[ Q(y^*) = \max_{u \in U} ; \min_{z} ; d^T z ]

[ s.t. ; Gz \ge h - Ey^* - Mu ]

内层是关于连续变量 (z) 的LP,因此可以用强对偶定理把它转成单层最大化问题。设 (\pi) 为内层约束对应的对偶变量,SP等价为:

[ \max_{u, \pi} ; \pi^T (h - Ey^* - Mu) ]

[ s.t. ; G^T \pi \le d, \quad \pi \ge 0, \quad u \in U ]

这里会出现一个双线性项 (\pi^T M u),这是整个CCG实现中第一个真正的技术难点。如果 (M) 矩阵的每个不确定变量只出现在一个约束里(比如光伏只出现在功率平衡约束),那么这项可以写成 (u)的线性组合系数由 (\pi) 决定的形态。当不确定集合是盒式时,最优 (u) 一定取边界,方向由 (\pi^T M) 的符号决定;当加上预算约束时,需要引入0-1变量选择哪些变量取边界,SP就变成一个MILP。

3.3 迭代流程、上下界更新与收敛条件

CCG的整体流程我总结成下面这个循环,这也是我在MATLAB里实际实现的骨架:

  1. 初始化:给定初始场景 (u_1)(我习惯取预测值),设置 (LB = -\infty),(UB = +\infty),迭代次数 (k=1);
  2. 求解MP,得到最优解 (y^, \eta^),更新 (LB = c^Ty^* + \eta^*);
  3. 固定 (y^),求解SP,得到 (Q(y^)) 和新的最坏场景 (u_{k+1}),更新 (UB = \min(UB, c^Ty^* + Q(y^*)));
  4. 计算相对间隙 (\frac{UB-LB}{|UB|}),如果小于设定容差(比如1e-3),停止迭代;
  5. 否则,把场景 (u_{k+1}) 对应的第二阶段变量 (z_{k+1}) 和约束 (F(y, z_{k+1}, u_{k+1})) 加入MP,(k = k+1),回到第2步。

这里有个细节值得注意:MP目标值是松弛解,所以LB一定小于等于原问题最优值;SP给出的 (c^Ty^* + Q(y^*)) 是某个可行方案的完整成本,所以UB一定大于等于原问题最优值。两个界从两边夹逼,收敛判据是有严格意义的。我在第一次跑通时看到LB和UB逐步靠近,那种感觉很直观,比Benders那种"割平面越来越多但界还是慢吞吞动"的体验好太多。

4. MATLAB+Yalmip环境下的CCG实现全流程

4.1 环境准备:Yalmip与求解器配置

MATLAB这边我用的版本是R2023b,建模工具是Yalmip,求解器用Gurobi 10.x。这里多啰嗦一句:两阶段鲁棒模型的内层和外层都涉及MILP,Yalmip自带的默认求解器是做不了大规模MILP的,必须配置好商业求解器。Gurobi和CPLEX都行,我个人偏Gurobi,原因后面踩坑部分会说。

Yalmip的安装很简单,把文件夹加进MATLAB路径就行,然后用yalmiptest验证一下求解器是否被识别。求解器配置代码:

ops = sdpsettings('solver', 'gurobi', 'verbose', 2); ops.gurobi.MIPGap = 1e-4; ops.gurobi.TimeLimit = 600;

我的习惯是:外层CCG的迭代容差设1e-3,内层Gurobi的MIPGap设1e-4。内层比外层紧一个量级,可以避免"外层还没收敛、内层精度不够"导致的UB抖动。

4.2 主问题MP的代码框架

在Yalmip里建MP,核心是"按场景循环累加约束"。因为MP会随着迭代不断长大,我建议一开始就用cell数组或者多维sdpvar把第二阶段变量组织好,避免在循环里反复创建变量。

我采用的变量组织方式是:

T = 24; % 时段数 nMT = 1; % 燃机台数 nCont = 4; % 第二阶段连续变量个数:P_MT, P_ch, P_dis, P_buy, P_sell... K = 0; % 当前已入库场景数 y_on = binvar(nMT, T, 'full'); % 燃机启停,第一阶段 y_grid = binvar(1, T, 'full'); % 主网购售电状态,第一阶段 z = cell(K, 1); % 每个已发现场景对应的第二阶段变量 eta = sdpvar(1);

主问题约束的构建遵循一个套路:先写第一阶段公共约束,再对每个已发现场景 (k),把第二阶段约束完整写一遍。功率平衡约束的代码示例:

C = []; for k = 1:K for t = 1:T u_PV = u_set{k}(t, 1); u_WT = u_set{k}(t, 2); C = [C, z{k}(1,t) + z{k}(2,t) - z{k}(3,t) + ... % P_MT + P_dis - P_ch z{k}(4,t) - z{k}(5,t) + u_PV + u_WT - z{k}(6,t) == P_load(t)]; % 其中 z(k,:) = [P_MT, P_dis, P_ch, P_buy, P_sell, P_cut] end end

目标函数里辅助变量(\eta)对应所有场景第二阶段成本的最大值下界,所以MP目标就是启停成本加上(\eta):

obj = sum(sum(c_on .* y_on)) + 1e-6 * sum(y_on(:)) + eta;

这里1e-6的微调是为了防止整数变量出现不希望的退化解,属于经验性的小技巧。

MP是MILP,第一轮没有场景时可以用一个空场景或者直接设MP只有第一步触发,实际操作中我会先手动把初始场景(u_1)加进MP,保证模型非退化。

4.3 子问题SP与双线性项线性化代码

SP的实现是CCG里最考验功底的部分。我先把一般形式列出来,再给代码。

SP内层LP写成标准形式:

[ \min_z d^T z \quad s.t. ; Gz \ge h - Ey^* - Mu ]

对偶后是:

[ \max_{\pi \ge 0} ; \pi^T(h - Ey^* - Mu) \quad s.t. ; G^T\pi \le d ]

在Yalmip里,直接建模这个对偶问题:

pi_var = sdpvar(n_cons, 1); % 对偶变量 v = binvar(n_u, 1, 'full'); % 不确定变量是否取上界的0-1指示 u = u_lb + (u_ub - u_lb) .* v; % 不确定量 C = [G' * pi_var <= d, pi_var >= 0]; C = [C, sum(v) <= Gamma]; % 目标中的双线性项处理:需要先定义 w(i,j) = pi_var(i) * v(j) M_big = 1000; w = sdpvar(n_cons, n_u, 'full'); for i = 1:n_cons for j = 1:n_u C = [C, 0 <= w(i,j) <= M_big * v(j)]; C = [C, pi_var(i) - M_big*(1-v(j)) <= w(i,j) <= pi_var(i)]; end end obj_sp = pi_var' * (h - E * y_star) - sum(sum(M .* w));

这里的目标函数用了一个数学等价:(\pi^T M u = \sum_i \sum_j \pi_i M_{ij} u_j),把 (u_j) 展开成 (u_{lb,j} + (u_{ub,j} - u_{lb,j})v_j) 后,常数项直接保留,变量部分用大M线性化结果(w_{i,j})来替代 (\pi_i v_j)。这样SP就是一个标准MILP,Gurobi可以直接求解。

不过说实话,大M法线性化不是唯一选择,也未必是最好选择。我在迭代了几轮后发现,如果模型规模不大,还有一种更省事的做法:因为(\pi)的最优值在LP极点处取得,而(u_j)在最优时必取边界,可以先用符号规则求解一个线性目标,再通过分支定界选择预算。这个技巧比较复杂,新手阶段建议还是稳妥地走大M法,先把流程跑通。

4.4 CCG主循环与LB/UB收敛过程输出

主循环代码写出来非常短,但逻辑要非常小心。伪代码如下:

LB = -1e6; UB = 1e6; K = 1; u_set{K} = u_pred; % 初始场景取预测值 gap = 1; while gap > 1e-3 && K <= 10 % 1. 建立并求解MP [MP, obj_MP] = build_MP(u_set, K, ...); optimize(MP, obj_MP, ops); y_star = value(y_on); LB = max(LB, value(obj_MP)); % 2. 固定y_star,求解SP [SP, obj_SP_val, u_new] = solve_SP(y_star, ...); UB = min(UB, value(obj_SP_val)); % 3. 计算gap gap = abs((UB - LB) / abs(UB)); % 4. 入库新场景 K = K + 1; u_set{K} = u_new; end

每轮迭代我都会记录LB、UB、gap、MP求解时间、SP求解时间,最后用plot画出收敛曲线。这里有一个工程细节:SP求解后拿到的u_new是连续值,考虑到最坏场景应该取边界,我拿到结果后会做一个"归边"处理,把接近边界的值直接顶到边界上,避免数值误差导致后续MP出现轻微的病态。

5. 算例验证:收敛曲线、成本对比与鲁棒性分析

5.1 24时段微网算例参数设定

为了验证CCG算法在MATLAB里到底能不能用、效果如何,我搭了一个24时段的小微网算例。参数如下:

参数数值
负荷峰值300kW(晚高峰)
光伏预测峰值120kW,波动±15%
风电预测峰值60kW,波动±25%
预算参数Γ4
燃气轮机容量20~80kW,爬坡30kW/h
燃机燃料成本系数0.5元/kWh
储能容量/功率200kWh / 50kW
购电价/售电价峰时0.9/0.7元,谷时0.4/0.2元
切负荷惩罚1000元/kWh
弃风弃光惩罚50元/kWh

这个算例的大小大概是:24时段,第二阶段连续变量约240个,第一阶段整数变量48个,不确定变量48个(光伏+风电各24个时段),SP的MILP规模也就在几百个变量量级,Gurobi求解速度飞快。

5.2 CCG迭代收敛过程解读

我记录的迭代过程如下:

迭代次数LB(元)UB(元)相对间隙/%
15623.406054.187.11
25867.325902.750.60
35878.025884.610.11
45879.105880.030.016

第1轮LB和UB之间差7%左右,第2轮直接压到0.6%,第4轮就已经到0.016%。这个收敛速度验证了CCG在小规模微网调度中的高效性。

从曲线形态看,UB是单调下降的,LB是单调上升的。UB来自"给定第一阶段方案后算出来的完整成本",随着迭代找到更好的第一阶段方案,UB只会越来越低;LB随着MP中场景增多、松弛被收紧,只会越来越高。两条线一夹,最优值就被限定在一个很小的区间里。

5.3 确定性调度与鲁棒调度的成本、切负荷对比

作为对比,我还跑了一个确定性方案:风光出力直接取预测值,目标函数里的(\max_{u\in U})消失。两种方案的结果差别很有意思:

对比项确定性调度鲁棒调度(Γ=4)
日前计划成本(元)5726.405798.21
最坏场景下的实际总成本(元)6385.905879.10
最坏场景切负荷量(kWh)1260
最坏场景购电成本占比高中等

确定性调度在预测场景下成本便宜了约1.3%,但如果光伏、风电真的按最坏情况发生,它需要切126kWh负荷,按惩罚单价算,总成本直接飙到6385.9元。鲁棒调度在实际最坏场景下的成本只有5879.1元,比确定性方案在最坏场景下的成本低了约8%。

为什么会有这种差距?看调度方案就能明白。鲁棒方案在最坏场景的边界条件下,会提前把储能充得更满,燃气轮机的基荷安排得更保守,主网购电计划也预留了更多后手。这些在预测场景下都是"多余成本",但在最坏场景下就是"救命稻草"。

这就是经济调度里最核心的权衡:用一点正常场景下的经济性牺牲,换取极端场景下的成本可控和供电可靠性。实际项目里到底选多大的Γ,不是拍脑袋定的,而是要结合当地天气规律和风险偏好,画一条"Γ-成本"曲线,让管理层拿这个曲线去决策。

6. 踩过的坑与调参心得

6.1 对偶转换中的符号与方向问题

我在搭SP模型时,遇到的第一个拦路虎就是对偶方向。

很多教材写对偶都是"标准形式max问题对偶成min",但实际建模时约束方向经常是反的。功率平衡是等式约束,它的对偶变量是自由变量,不是非负的。我在第一版代码里把所有对偶变量都设成了sdpvar(n,1)并加了>=0约束,结果SP目标值算出来完全不对,而且UB和LB根本不可能收敛。

正确的做法是:等式约束对应的对偶变量用自由sdpvar,不等式约束>=对应的对偶变量取非负,<=对应的取非正。如果你不想每次都对冲方向,有个土办法:把模型写在纸上,先把SP内层写成min d'z s.t. Az >= b的统一形式,再写成对偶,最后再敲进Yalmip。手推一遍永远比直接凭感觉写代码快。

6.2 大M值的选取:宁可大一点,不能病态

大M法线性化(\pi_i v_j)的时候,M的取值直接决定Gurobi的数值稳定性。M太小的后果是约束起不到惩罚效果,最坏场景被错误地限制住,UB算出来偏低;M太大又会让MILP的LP松弛变得很差,分支定界效率下降,甚至出现数值警告。

我的经验做法是:先跑一次纯LP版本的SP(不引入0-1变量和预算),观察对偶变量(\pi)的量级,然后取M为最大对偶变量量级的10~20倍。比如这里(\pi)量级在几百到一两千之间,我取了M=10000。如果模型规模变大,建议用gurobi的数值诊断工具看condition number。另一个备选方案是使用Gurobi的指示约束(indicator constraints),Yalmip里可以通过implies实现,避免大M带来的数值问题,缺点是部分老版本求解器不支持,移植性差一些。

6.3 第二阶段含整数变量时:对偶失效怎么办

这是我踩过最深的坑。两阶段鲁棒优化的标准CCG推导有一个隐含前提:第二阶段内层问题必须是连续LP,才能用强对偶转成单层MILP。如果第二阶段变量里有整数变量,内层就不是LP,强对偶定理不成立,对偶模型求出来的目标值就不是原问题的可行成本,整个迭代逻辑就崩了。

微网调度里最典型的"诱导变量"就是储能的防同时充放整数变量。我第一次建模时,把储能充放电状态放在第二阶段,结果SP对偶完之后的目标值和原内层目标值对不上,检查了很久才发现是这个原因。

解决办法有三个,我都试过:

  1. 把所有整数变量全部挪到第一阶段。这是最推荐的做法,也是很多文献的默认假设。储能是否充放电、燃机是否启停、主网是否购电,这些状态确实应该在日前就定下来,放到第一阶段从物理意义上也说得通;
  2. 使用基于KKT条件的单层化方法,把内层原问题的最优性条件(包含互补松弛)写进约束,然后引入大M,虽然能处理整数变量,但模型规模和数值难度都上了一个台阶;
  3. 用迭代启发式方法,比如SP内部再做一层小循环,把整数变量枚举出来,但这种方法只能针对极小规模,实用性有限。

经过这次教训,我现在建模时的原则是:第一阶段尽量把所有离散状态都吃掉,第二阶段只保留连续调整量。这不只是为了CCG代码好写,也符合微网实际运行中"模式切换提前定、出力调整实时做"的操作习惯。

6.4 从盒式到预算:先跑通再降保守性

最后一个经验是关于调试顺序的。我第一次跑CCG时,不确定性直接上预算约束和0-1选择变量,SP是MILP,大M线性化、符号规则、归边处理各种因素搅在一起,出了问题根本不知道是算法逻辑错还是数值实现错,排查成本极高。

后来我换了一个更聪明的路径:先把不确定集退化成纯盒式集合,也就是去掉预算约束(相当于(\Gamma = +\infty)),这时SP的对偶问题里没有0-1变量,不需要大M线性化,最优(u_j)直接由对偶变量系数的符号决定,整个SP就是一个纯LP,跑得飞快。在这个简化版本上,我先确认CCG的整体迭代逻辑、LB/UB收敛行为全部正确,然后再把预算约束和0-1变量加回去,解决大M数值问题。这样每一步出了问题,定位范围就小得多。

这个调试顺序其实反映了这类算法问题的通用方法论:先在一个"结构更简单但核心逻辑完整"的版本上把骨架跑通,再逐步增加复杂度。两阶段鲁棒CCG的坑,大多数不在"算法不懂",而在"逻辑被复杂的模型细节淹没了"。

最后再分享一个我在实际运行中验证过的小技巧:CCG收敛后,不要把最后那个(y^)直接拿去用,建议把这个(y^)带回到SP重新求解一次,确认UB对应的是同一个第一阶段方案。因为MP求解时(\eta)只是约束下界,理论上它等于SP值,但数值上可能差一两个数量级的尾数。这一步校验成本很低,却能避免很多莫名其妙的"收敛了但方案不可用"问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询