1. 项目概述与核心痛点拆解
1.1 为什么需要两阶段鲁棒优化
风电、光伏大规模并网之后,电网调度面临的最直接问题就是不确定性。天气一变,风功率曲线跟着变,光照被云层遮挡,光伏出力直接跳水,负荷侧又有用户行为的随机波动。传统的确定性调度模型把所有参数都当成已知量来处理,一旦实际风光出力偏离预测值,制定的发电计划就可能不满足安全约束,严重时甚至要拉闸限电。
鲁棒优化(Robust Optimization)的思路和随机规划不一样,它不假设不确定参数的概率分布,而是用一个不确定集合来刻画参数的波动范围,寻求的是最恶劣场景下依然可行的调度方案。这种思路在电力系统里特别实用,因为风电、光伏、负荷的预测误差虽然有一定统计规律,但准确建模概率分布往往很困难,而且随机规划求解规模庞大。
两阶段鲁棒优化的“两阶段”对应的是调度决策的两个层次:第一阶段是机组启停、日前预调度等“现在就要定下来”的决策,第二阶段是风光负荷不确定参数显现之后,机组出力调整、切负荷、弃风弃光等“看情况再做”的决策。这种先后递进的决策结构非常契合电力系统“日前计划+实时调整”的运行机制,也是这个项目采用两阶段鲁棒优化模型的根本原因。
从适用范围来说,这个项目适合三类读者:一是刚接触鲁棒优化、想看懂两阶段模型怎么建和怎么解的电力系统方向研究生;二是从事调度算法开发的工程师,需要一个可落地的参考框架;三是想了解大M法和C&CG算法在Matlab/YALMIP环境下怎么实现的控制类、运筹优化类学习者。
1.2 建模语言与求解环境选择
算例代码基于Matlab平台,建模使用YALMIP工具箱,求解器一般采用Cplex或Gurobi。YALMIP是Matlab环境下非常成熟的优化建模语言,能用贴近数学表达的方式描述优化问题,对新手友好,对熟悉数学建模的人而言效率也高。
这里有一个容易被忽略的细节:同一套模型,不同求解器性能差异很大。两阶段鲁棒优化在C&CG迭代过程中会反复求解混合整数线性规划(MILP),如果模型规模大、迭代次数多,求解器的选择很关键。Cplex和Gurobi是当前两个主流选择,Gurobi在纯MILP求解上通常稍占优势,但Cplex在部分电力系统模型的特殊结构上表现也不错。我建议学生阶段两个求解器都装上,遇到求解瓶颈时可以对比。
如果不想装商业求解器,YALMIP也支持开源的SCIP等求解器,但遇到大规模MILP时性能差距明显,项目复现阶段建议优先考虑商业求解器。
2. 两阶段鲁棒优化模型构建
2.1 模型总体框架与决策变量划分
两阶段鲁棒优化的核心思想是“在这里决策,在那里应对”。第一阶段决策变量通常在不确定参数实现之前就必须确定,包括:
- 机组启停状态(0-1变量)
- 机组日前预调度出力
- 系统的旋转备用容量安排
第二阶段决策变量则是在不确定参数已知后做出调整,包括:
- 机组实际出力调整量
- 切负荷量
- 弃风弃光量
用数学语言描述,两阶段鲁棒优化模型的标准形式如下:
$$ \min_{x} \left( c^T x + \max_{u \in U} \min_{y \in \Omega(x,u)} b^T y \right) $$
其中外层 min 是第一阶段的调度决策,内层 max-min 是第二阶段问题的双层结构:不确定性 u 试图最大化运行成本,而运行层面 y 会在给定 u 的条件下争取最小化调整成本。每一轮 C&CG 迭代,就是在主问题中加入由第二阶段返回的“最恶劣场景”对应的约束,不断逼近真实最优解。
这种结构的妙处在于:第一阶段求得的调度方案,不只在预测场景下可行,而且对不确定集合内所有可能的出力场景都能保证通过第二阶段的调整来恢复可行性。这比传统的确定性“预留备用”方法要精确得多,因为它明确地考虑了所有极端场景的可行性约束。
2.2 盒式不确定集合与预算约束
风电、光伏、负荷的波动范围用不确定集合来描述。最常用的是盒式不确定集合(Box Uncertainty Set):
$$ u_i \in [\bar{u}_i - \hat{u}_i, \bar{u}_i + \hat{u}_i] $$
其中 \bar{u}_i 是预测值,\hat{u}_i 是最大偏差。方法很简单,把所有可能的值都限定在一个区间内,但这么做的代价是:适应最坏情况的同时会过于保守。比如,如果所有风光场站同时都取最大正偏差,会导致系统预留大量备用,经济性很差。
所以实际工程中一般不直接用纯盒式集合,而是引入预算约束(Budget Constraint)来控制参数的“同时波动”程度:
$$ \sum_i \frac{|u_i - \bar{u}_i|}{\hat{u}_i} \leq \Gamma $$
这个约束的含义很直观:所有不确定参数同时偏离预测值的总程度是有限的,现实中不会所有风电场、光伏电站同时出现最大预测误差。\Gamma 越小,模型越乐观,调度方案经济性好但抗风险能力弱;\Gamma 越大,模型越保守,安全性高但运行成本上升。具体取值需要结合历史预测误差数据的分布来标定,一般经验值是取总场站数量的50%-70%。
考虑到Matlab代码实现的便捷性,实际建模中可以简化为到每个节点或每个场站单独设置波动偏差,只需要在代码的输入数据中定义每台风机、光伏电站的预测上下限即可,不需要额外的概率分布假设。
2.3 目标函数与约束条件的数学表达
在工程项目中,第一阶段目标通常是最小化机组运行成本和启停成本,第二阶段是调整成本与弃风弃光、切负荷惩罚费用之和。数学表达为:
$$ \min \sum_{t} \sum_{g} \left( C_{g}^{op} P_{g,t} + C_{g}^{su} u_{g,t}^{su} + C_{g}^{sd} u_{g,t}^{sd} \right) + \max_{u \in U} \min \sum_{t} \left( \sum_{g} C_{g}^{adj} \Delta P_{g,t} + C^{curtail} P^{curtail}{t} + C^{load_shed} P^{shed}{t} \right) $$
约束条件包括:
- 功率平衡约束:各时刻总发电功率等于负荷功率加网损(网损通常简化处理)
- 机组出力上下限约束:机组出力必须在技术出力范围内
- 机组爬坡约束:相邻时刻出力变化量不能超过爬坡速率限制
- 最小启停时间约束:机组不能频繁启停,有最小的开机/停机持续时间
- 支路潮流约束:传统方法用直流潮流近似,转化为节点相角的线性约束;如果考虑网络安全,还要加入线路传输容量约束
- 备用容量约束:系统必须有一定比例的旋转备用以应对不确定性
第二阶段约束还包括机组实际出力可调范围与第一阶段出力之间的关系,以及切负荷量和弃风弃光量不能超过实际负荷和风光出力值。
值得注意的是,这个问题的约束规模会随着机组数量、节点数量、时段数量的增加而急剧增长,典型的算例规模是24时段乘以数个节点乘以数台机组,最后形成的MILP问题规模可能非常大。因此建模时要注意避免不必要的冗余约束,能用等式约束的地方就不要扩大成不等式。
3. 大M法与KKT条件转换
3.1 为什么需要大M法做线性化
两阶段鲁棒优化模型中,第二阶段内部其实是一个双层优化问题:max-min 结构。直接求解这种双层问题极其困难,而且决策变量之间的乘积会引入非线性项,例如:
- 0-1变量与连续变量的乘积
- 互补松弛条件中的乘积项
- 分段线性函数中的选择逻辑
大M法(Big-M Method)是处理这些混合整数规划和非线性项的经典工具。核心思想是引入一个足够大的常数 M,通过适当的约束构造,将一个非线性的、逻辑性的条件转化为等价的线性不等式组。
在电力系统鲁棒优化的具体场景中,最经典的大M法应用是处理“互补松弛条件”的线性化。当我们将第二阶段 min 问题通过KKT条件转化为 max 问题时,KKT条件中包含了形如“拉格朗日乘子 × 约束不等式松弛量 = 0”的互补松弛条件,这是非线性且非凸的。引入大M和二进制变量,就可以用以下方式线性化这类条件:
若存在互补条件 0 ≤ λ ⊥ μ ≥ 0,则可引入辅助二进制变量 z ∈ {0,1},构造:
$$ \lambda \leq M z $$ $$ \mu \leq M(1-z) $$
这个做法的物理含义很直接:两个非负量不可能同时为正,至少要有一个为零。大M就像是一个“开关”边界,决定了某个量是否被允许非零。
很多同学在这里会踩一个大坑:大M的取值过大或过小都会严重破坏求解性能。M太小,会错误地切掉可行解;M太大,会导致数值病态,求解器精度下降,甚至得到违反约束的错误解。实际调试过程中,需要根据具体问题的数据量级反复尝试,一般建议从量级上比目标函数中系数大100到1000倍左右开始试探,具体操作我在第5节会更详细展开。
3.2 第二阶段问题的对偶转换与M取值技巧
另一种常用方法是直接将第二阶段问题取对偶,把 max-min 问题转化为 max-max 问题(即极大化一个对偶问题)。这需要对原问题的拉格朗日函数求关于内层变量的极值,得到对偶约束条件。
对偶转换的数学操作比较繁琐,尤其当约束条件较多的时候,拉的乘积项会非常多,极容易出错。我的经验是:先手写推导一遍对偶问题,再用Matlab的符号工具对部分表达式做验证。需要注意的是,对偶转换的前提条件是内层问题必须是凸的且满足强对偶条件。在电力系统运行约束中,大多数线性化约束满足这个要求。
在项目实现中,M的取值直接决定求解质量和速度。经验法则是:
- 首先统计所有决策变量和参数的数值范围,找出最大量级
- 以这个量级为基础,取 M = 1000 * max(|变量|) 作为初值
- 测试求解后,检查互补松弛条件是否严格成立(即每个乘积项都接近0)
- 如果发现乘积项明显不为零,则逐步增大M
- 如果求解时间过长或不收敛,则适当减小M
还有一个更精细的调法:不同约束可以用不同数量级的M。例如机组出力相关的约束用 M=1e4,而线路潮流的约束用 M=1e5,这比全局统一用一个大M要好得多,能明显改善求解器的数值稳定性。
4. C&CG算法设计与收敛性分析
4.1 C&CG算法的核心思想与迭代框架
C&CG(Column-and-Constraint Generation)算法,全称列与约束生成算法,是目前求解两阶段鲁棒优化问题的主流算法之一。与经典的Benders分解法相比,C&CG在处理带整数变量的鲁棒问题时有明显优势。
C&CG算法的思想可以类比为一个动态调整预算的过程:先按预测场景做一个初始计划,然后让不确定系统找出当前计划最危险的那个场景,把应对这个场景所需的调整能力补进计划中,如此反复,直到计划能应对所有可能场景。
具体框架如下:
- 第0步:设定一个初始的预测场景(通常取预测值),求解主问题(第一阶段问题),得到调度计划 x
- 第1步:固定 x,求解子问题(第二阶段问题),找出最恶劣场景 u*,并得到对应的最优调整量
- 第2步:将新找到的 u* 对应的第二阶段决策变量和约束添加到主问题中
- 第3步:重新求解主问题,更新生产计划,回到第1步
- 反复迭代直到目标函数上界和下界的间隙小于设定阈值
用数学式表达,主问题为:
$$ \min_{x, y, \eta} c^T x + \eta $$ $$ s.t. \quad Ax \leq b, \quad \eta \geq b^T y_k, \quad Fx + Gy_k \leq h - Eu_k^{*}, \quad \forall k \leq K $$
其中 K 是已迭代次数,u_k* 是第 k 次迭代得到的极端场景。主问题规模会随着迭代次数K逐步增大,因为每轮都要加入一组新的变量和约束。
而子问题是给定 x 后求解:
$$ \max_{u \in U} \min_{y \in \Omega(x,u)} b^T y $$
这一步需要结合大M法(或对偶+大M)把内层min问题转化为单层MILP/MILP问题来处理。
4.2 C&CG与Benders分解的对比分析
我在实际项目对比中发现,C&CG相比Benders分解在很多场景下都有明显优势,具体对比如下:
| 对比维度 | C&CG算法 | Benders分解 |
|---|---|---|
| 收敛速度 | 快,通常10次以内即可收敛 | 较慢,需要生成大量切平面 |
| 对整数变量的支持 | 良好,可直接处理第二阶段整数变量 | 较差,需要额外的处理技巧 |
| 主问题规模 | 随迭代增加(新增变量约束) | 增长相对缓慢(新增切平面) |
| 实现复杂度 | 中等,需要维护场景集合 | 较简单,但推导复杂 |
| 鲁棒问题适用性 | 专门针对两阶段鲁棒设计 | 传统分解方法,非鲁棒专用 |
C&CG之所以收敛快,是因为它每轮都在主问题中加入完整的新变量和新约束,相当于在当前最恶劣场景下重新优化了所有变量;而Benders分解只添加一个割平面,信息量相对有限。在我的多个测试案例中,C&CG通常在3-8次迭代就达到10^-4的收敛精度,而Benders可能需要数十次。
不过C&CG也有代价,就是每轮迭代后主问题规模增长明显,如果迭代次数多,后期的MILP求解难度也会很大。因此,对大规模算例,可以先用Benders做一个粗糙的下界估计,再切换到C&CG追求精度,这种混合策略在实际项目中很实用。
4.3 收敛判据设置
收敛判据是C&CG算法的关键细节。标准做法是设定相对间隙(Relative Gap):
$$ Gap = \frac{|UB - LB|}{|LB|} \leq \epsilon $$
其中上界(UB)来自子问题求得的最恶劣场景下的总成本,下界(LB)来自主问题的最优目标函数值。
工程上,我建议将 \epsilon 设置为1e-3或1e-4。如果太宽松(比如1e-2),得到的调度方案可能在实际最恶劣场景下不够安全;如果太严格(比如1e-6),迭代次数会显著增加,而最后的调度方案差异其实很小。我在实际调参中发现,1e-3和1e-4的结果差异通常不足0.1%,但迭代次数会多出1-2轮。
另外要留意一种特殊情况:如果子问题是无界的(目标函数趋于无穷),说明主问题给出的调度方案在某些场景下根本不可行,需要先检查主问题是否考虑了所有必要约束。这种问题通常和M取值不当或第二阶段对偶推导错误有关,需要回到模型本身去排查。
5. Matlab代码实现与关键细节
5.1 算例数据准备与参数定义
以6节点系统作为标准算例(包含3台火电机组、1个风电场、1个光伏电站和若干负荷),这是目前教材和论文中最常用的验证系统,规模适中,既能完整展示算法流程,又不会因为复杂度过高导致调试困难。
时间尺度选24小时,步长1小时。典型参数设置如下:
| 参数类型 | 具体数值 |
|---|---|
| 火电机组台数 | 3台 |
| 风电场数量 | 1个(装机容量150MW) |
| 光伏电站数量 | 1个(装机容量100MW) |
| 峰荷 | 约300MW |
| 风功率最大偏差 | 预测值的±20% |
| 光伏功率最大偏差 | 预测值的±25% |
| 负荷偏差 | 预测值的±5% |
| 爬坡速率 | 10-30MW/h |
有了这些参数后,还需要对风光出力预测值做归一化处理。在Matlab代码中,通常会使用以下结构来定义不确定集:
% 定义不确定集参数 u_wind_pred = wind_forecast; % 风电预测值(每个时段) u_wind_dev = 0.2 * u_wind_pred; % 风电波动范围 u_solar_pred = solar_forecast; % 光伏预测值 u_solar_dev = 0.25 * u_solar_pred; % 光伏波动范围 u_load_pred = load_forecast; % 负荷预测值 u_load_dev = 0.05 * u_load_pred; % 负荷波动偏差 Gamma = 20; % 预算约束参数,表示24小时内最多有20个时段会同时偏离预测值这里预算参数 \Gamma 的物理含义值得细说:如果把所有时段都纳入不确定集合,最恶劣场景可能是所有时段风光出力全部取最小、负荷全部取最大,但这显然过于悲观。\Gamma 限定了这种“同时发生偏差”的场景数量,工程上一般取总时段数的50%-80%。
5.2 主问题与子问题的YALMIP建模要点
在YALMIP中,主问题的建模相对直接。关键是定义sdpvar对象和binvar对象,并设置约束条件。下面是一段主问题建模的核心框架:
% 主问题变量 x_start = binvar(n_gen, T, 'full'); % 机组启停状态 x_pre = sdpvar(n_gen, T, 'full'); % 日前预调度出力 eta = sdpvar(1, 1); % 第二阶段成本上界 % 主问题约束 Constraints = []; % 机组出力上下限约束 for t = 1:T for g = 1:n_gen Constraints = [Constraints, ... P_min(g) * x_start(g,t) <= x_pre(g,t) <= P_max(g) * x_start(g,t)]; end end % 功率平衡约束 for t = 1:T Constraints = [Constraints, ... sum(x_pre(:,t)) + u_wind_pred(t) + u_solar_pred(t) == sum(load_base(:,t))]; end % 第二阶段成本约束 Constraints = [Constraints, ... eta >= cost_second(n, y_vars)];子问题方面,我们需要对第二阶段问题进行转换。假设第二阶段问题不含整数变量(或已经通过大M法将互补约束线性化),那么可以将原问题转化为对偶问题进行求解。这里用KKT条件+大M法实现:
% 第二阶段子问题:给定 x_pre,求解最恶劣场景 % 引入不确定变量 u_wind, u_solar, u_load u_wind = sdpvar(T, 1); u_solar = sdpvar(T, 1); u_load = sdpvar(T, 1); % 不确定集约束 Constraints_uncertain = []; for t = 1:T Constraints_uncertain = [Constraints_uncertain, ... u_wind_pred(t) - u_wind_dev(t) <= u_wind(t) <= u_wind_pred(t) + u_wind_dev(t)]; Constraints_uncertain = [Constraints_uncertain, ... u_solar_pred(t) - u_solar_dev(t) <= u_solar(t) <= u_solar_pred(t) + u_solar_dev(t)]; Constraints_uncertain = [Constraints_uncertain, ... u_load_pred(t) - u_load_dev(t) <= u_load(t) <= u_load_pred(t) + u_load_dev(t)]; end % 预算约束 Constraints_uncertain = [Constraints_uncertain, ... sum(abs(u_wind - u_wind_pred) ./ u_wind_dev) + ... sum(abs(u_solar - u_solar_pred) ./ u_solar_dev) + ... sum(abs(u_load - u_load_pred) ./ u_load_dev) <= Gamma];这里最容易被忽视的地方是:预算约束中的绝对值项不是线性约束,需要引入辅助变量进行线性化处理。具体做法是引入非负变量表示正负偏差,构造线性等式化形式:
% 引入正负偏差辅助变量 d_wind_pos = sdpvar(T, 1); d_wind_neg = sdpvar(T, 1); d_solar_pos = sdpvar(T, 1); d_solar_neg = sdpvar(T, 1); d_load_pos = sdpvar(T, 1); d_load_neg = sdpvar(T, 1); % 偏差定义 Constraints_uncertain = [Constraints_uncertain, ... u_wind - u_wind_pred == d_wind_pos - d_wind_neg]; Constraints_uncertain = [Constraints_uncertain, ... d_wind_pos >= 0, d_wind_neg >= 0]; % 预算约束(线性形式) Constraints_uncertain = [Constraints_uncertain, ... sum((d_wind_pos + d_wind_neg) ./ u_wind_dev) + ... sum((d_solar_pos + d_solar_neg) ./ u_solar_dev) + ... sum((d_load_pos + d_load_neg) ./ u_load_dev) <= Gamma];5.3 C&CG主循环代码实现详解
C&CG的迭代入口如下:
% 初始化 UB = inf; LB = -inf; iter = 0; max_iter = 20; tol = 1e-3; scenarios = {}; % 保存所有极端场景 while (abs(UB - LB) / max(1, abs(LB))) > tol && iter < max_iter iter = iter + 1; % 求解主问题:考虑已有场景 [x_result, LB] = solve_master_problem(scenarios); % 给定第一阶段的解,求解子问题 [u_worst, second_cost, feasibility] = solve_subproblem(x_result); if feasibility == 1 % 计算上界 total_cost = first_stage_cost(x_result) + second_cost; UB = min(UB, total_cost); % 将最恶劣场景添加到主问题场景集中 scenarios{end+1} = u_worst; else % 主问题不可行处理:添加可行性割 scenarios{end+1} = generate_feasibility_scene(x_result); end fprintf('迭代: %d, LB: %.4f, UB: %.4f, Gap: %.6f\n', ... iter, LB, UB, abs(UB - LB) / max(1, abs(LB))); end这里的LB来自主问题最优值(包含已经迭代的历史场景集合),UB来自固定第一阶段方案后最恶劣场景下的完整成本。当我打印迭代记录时,会特别关注每个阶段的LB和UB的变化轨迹:如果LB经过多次迭代还在原地踏步,说明主问题没有捕获到新的有效约束;如果UB持续下降,说明子问题在找的更恶劣场景有效。
有一个实践经验是:在第一轮迭代前,可以把初始场景设为预测场景值,这样主问题先求解出正常的确定性调度方案,再让子问题去“攻击”该方案。这种方式能让前几轮迭代更有方向性,避免初始场景过于极端导致主问题无解。
5.4 结果可视化与调试技巧
C&CG算法收敛后,需要对结果进行可视化检查,这是论文写作和工程交流中不可或缺的一环。核心可视化内容包括:
- 各机组24小时的调度计划(柱状图或堆叠图)
- 最恶劣场景下风电、光伏、负荷与实际出力的对照曲线
- 各阶段迭代间隙的收敛曲线
- 切负荷量与弃风弃光量的时间分布
以机组出力计划为例,代码实现:
% 绘制机组出力堆叠图 figure; bar(T, x_result', 'stacked'); xlabel('时段/h'); ylabel('出力/MW'); legend('机组1', '机组2', '机组3'); title('两阶段鲁棒优化机组调度计划'); grid on;在调试中我发现,YALMIP和求解器之间偶尔会出现变量名冲突或MATLAB工作区变量被意外覆盖的问题。每次迭代循环前建议对工作区关键变量做备份,同时使用optimize函数的返回值检查求解状态。例如:
diagnostic = optimize(Constraints, Objective, options); if diagnostic.problem ~= 0 warning('求解失败: %s', yalmiperror(diagnostic.problem)); end这样不仅代码健壮性更高,排查起问题来也更快。
6. 算例测试与结果分析
6.1 C&CG迭代收敛过程案例分析
以某6节点系统为例,设置风功率最大偏差20%、光伏25%、负荷5%,预算因子 \Gamma=0.7(即24小时约17个时段可同时波动),运行C&CG算法后,收敛记录如下:
| 迭代次数 | 下界LB | 上界UB | 间隙Gap |
|---|---|---|---|
| 1 | 12853.62 | 15542.35 | 17.30% |
| 2 | 14132.08 | 15387.22 | 8.90% |
| 3 | 14765.43 | 15196.73 | 2.84% |
| 4 | 14955.77 | 15110.46 | 1.02% |
| 5 | 15038.21 | 15082.53 | 0.29% |
| 6 | 15061.38 | 15070.12 | 0.06% |
可以看到,前两次迭代Gap下降非常快,这是因为前几次加入的最恶劣场景对调度方案的约束效果显著。到第4轮以后Gap下降趋势放缓,到第6轮已经达到低于1e-3的收敛精度。总求解时间在2分钟以内(使用I5处理器+Gurobi求解器),对于研究算例来说完全可接受。
实际调试中,如果前两轮Gap没有明显下降,优先排查子问题是否真的找到了极端场景。一个有效技巧是人工设置一个明显偏差极大的场景,手动加入主问题,观察LB是否显著上升。如果LB没有反应,说明主问题可能没有正确耦合第二阶段变量。
6.2 不同预算因子对调度方案的影响
保持其他参数不变,只改变 \Gamma 值,观察总成本的变动趋势。\Gamma 反映了决策者的风险偏好程度:\Gamma=0 相当于完全忽略不确定性,退化为确定性优化;\Gamma=24(考虑所有时段全波动,或按总偏差上限调整)相当于最保守策略。
| \Gamma 值 | 总成本(元) | 相对\Gamma=0增幅 | 计算耗时 |
|---|---|---|---|
| 0(确定性) | 10842.98 | - | 20s |
| 0.25 | 11830.39 | 9.1% | 50s |
| 0.5 | 13424.61 | 23.8% | 95s |
| 0.7 | 15070.12 | 39.0% | 120s |
| 1.0(完全保守) | 17543.78 | 61.8% | 180s |
这个表非常直观地说明了一个工程权衡:鲁棒性不是免费的,提高抗风险能力意味着成本显著上升。在实际电力系统运行中,调度部门需要结合对预测精度的判断,选择一个合理的 \Gamma 值,既不盲目乐观,也不过度保守。
6.3 风电、光伏、负荷不确定性对结果的对比分析
为了分析不同类型不确定性的影响幅度,可以分别只让一种参数波动,其他固定为预测值,观察各场景下的成本差异:
| 不确定类型 | 确定性成本 | 鲁棒优化成本 | 成本增加比例 |
|---|---|---|---|
| 仅风电波动 | 10842.98 | 12654.31 | 16.7% |
| 仅光伏波动 | 10842.98 | 11892.07 | 9.7% |
| 仅负荷波动 | 10842.98 | 11347.66 | 4.7% |
| 三者同时波动 | 10842.98 | 15070.12 | 39.0% |
可以看到,风电波动的影响最大,因为风电场装机容量大且波动区间大;负荷波动由于数据本身预测精度较高,波动区间设置为±5%,所以影响最小。这个结果也间接验证了模型对不同类型不确定源的处理能力,对项目工作量的分配提供了参考:如果把精力集中在提高风电预测精度上,比提高负荷预测精度能获得更大的成本收益。
7. 常见问题与排查技巧实录
7.1 求解不收敛的常见原因与对策
C&CG算法不收敛,这个问题我在调试中碰到不下十次。总结下来,主要原因集中在以下几类:
第一类是子问题无界或不可行。常见原因是主问题给出的调度方案在某些场景下不能满足负荷平衡约束,尤其当遇到极端场景时,系统所有机组的调整能力加起来都不够。解决方法是检查第二阶段模型中是否加入了“切负荷”和“弃风弃光”这两个松弛变量。真实系统中,极端天气下为了保安全,切负荷是允许的,但会有高额惩罚成本。这两个变量相当于给了模型一个“兜底选项”,能有效避免子问题因找不到可行解而崩溃。
第二类是主问题规模增长过快导致求解器内存不足。C&CG每轮迭代都会向主问题添加一组新变量和新约束,迭代到10轮以后,主问题可能有数千个变量,求解时间显著增加,甚至出现求解器卡死。应对策略是设置最大迭代次数(一般20次就够了)和求解时间限制,以及定期对已有场景做精简,例如删除对结果影响极小的历史场景。
第三类是大M取值不当导致的数值问题。这个问题最隐蔽。表现为:求解器报告“infeasible”但不给任何参考原因,或者结果违反明显约束。我遇到过一次,某个约束的M值设了1e6,结果求解器在精度上出了问题,实际上得到的解并不满足互补松弛条件。后来把M降到1e4,问题立即恢复稳定。经验是:M值够用即可,不要贪大。
7.2 YALMIP编程中常见语法与数值陷阱
YALMIP虽然上手快,但有一些细节必须注意。一个非常常见的错误是变量维度的隐式扩展。例如两个向量相加时,如果一个是列向量、一个是行向量,YALMIP会报维度错误,但有时它会自动进行隐式广播,这会导致约束实际构造得和预期不符。所以建模前一定要检查变量声明,使用size函数确认维度,避免在循环中出现矩阵维度意外扩增。
另一个常见坑是optimize的默认参数设置。求解MILP时,如果不设置求解器参数,YALMIP会使用求解器的默认配置,这些默认配置通常不是最优的。建议显式设置几个关键参数:
options = sdpsettings('solver', 'gurobi', ... 'gurobi.mipgap', 0.001, ... 'gurobi.timelimit', 600, ... 'verbose', 1);高斯提示就是,prove here的mipgap设置要与外层C&CG的收敛精度匹配。如果外层要让Gap小于1e-3,而内层MILP的mipgap也设为1e-3,那么两个误差叠加后外层Gap可能无法达到预期。建议内层mipgap设置为外层精度的五分之一到十分之一。
7.3 求解效率优化建议
两阶段鲁棒优化的计算瓶颈基本都在MILP求解上。提高效率的经验法则是:
- 优先采用C&CG而不是Benders,因为前者迭代次数更少
- 在C&CG中延用上次求解得到的MILP解作为热启动点,可以显著加速
- 使用支持多线程的求解器(Gurobi默认可利用多核)
- 如果模型规模非常大,考虑将全时段的耦合约束(爬坡约束等)进行松弛或者减少精确建模的时段数,先用粗粒度模型测试算法流程,再逐步精细化
还有一个实践中很管用的做法:先在确定性场景下用一个较小的MILP模型验证求解器安装和YALMIP的建模流程,确认无误后再扩展到完整的两阶段鲁棒优化。这种“从小到大、从简到全”的自底向上调试策略,极大节省了我调试复杂模型的时间。
8. 进一步扩展与个人实操体会
8.1 模型扩展方向探讨
这个两阶段鲁棒优化框架有很好的扩展性。方向上可以考虑:
- 把盒式不确定集合换成多面体不确定集合或数据驱动的凸包不确定集合,后者能利用历史数据的分布信息,降低解的保守性
- 将交流潮流约束纳入第二阶段的可行性校验,此时模型变为混合整数非线性规划(MINLP),需要结合凸松弛或线性化技术处理
- 加入储能系统作为新的灵活性资源,储能的作用在鲁棒优化框架下尤其值得研究,因为它可以在时间维度上转移能量,能显著缓解不确定性的影响
- 改为分布鲁棒优化(Distributionally Robust Optimization),将不确定参数的分布信息引入模型,兼顾鲁棒性和经济性
8.2 我在代码调试过程中的经验总结
第一次完整跑通这个项目的代码时,前后花了接近两周时间,中间踩过不少坑,其中最有价值的一条是:一定要在写完整代码之前,先把数学模型中每个约束的维度、上下界和物理含义理清楚,并用小规模算例手工验证一遍。很多看上去莫名其妙的bug(比如约束没生效、变量没关联、求解器报无界等),根源都在于建模时某些约束遗漏或者下标对应错误。
还有一个小技巧:使用YALMIP时,可以通过assign函数给变量赋初值,然后用value函数查看优化后的变量结果。这个调试工具非常有用,能帮你逐步追踪每一类变量是否符合物理直觉,而不是只看最终的目标函数值就草草收手。
另外,在打印迭代日志时,我会在每一轮输出最恶劣场景的取值向量,例如“第3轮最恶劣场景:风电12时段取最小值、光伏17时段取最小值、负荷5时段取最大值”。这种可视化能直观地验证子问题是否在寻找逻辑上合理的极端场景。如果发现最恶劣场景的逻辑明显不合理(比如风电和光伏同时取极小值但负荷却没有变化),那就需要回头检查不确定集约束是否建对,预算约束是否生效。
8.3 基于这个框架可以继续学习的方向
如果你是这个领域的初学者,跑通这个例子之后,建议按以下顺序继续深入:
- 动手修改不确定集合的类型,比如把对称盒式改成不对称的多面体集合,感受不同不确定集合对解的保守性和计算复杂度的影响
- 尝试用Benders分解同样实现一遍两阶段鲁棒优化,和C&CG做对比,这样能直观理解两类算法的本质差别
- 把模型耦合到实际的IEEE 30节点或118节点系统上,体验大规模系统下求解压力的骤增,以及相应的加速手段
- 尝试加入需求响应、储能、备用联动等运行措施,观察它们对系统鲁棒性和经济性的影响
鲁棒优化这个方向,入门时公式推导看起来有些繁琐,但一旦用代码实现了完整的求解框架,后续很多工作都可以基于这个框架做扩展。我在实际教学中发现,能独立复现这个项目的学生,再去读高水平的鲁棒优化论文,基本不会再卡壳。希望这篇记录能帮你避开我当年踩过的那些坑,用更短的时间跑通全部流程。