1. 项目概述:为什么用Yalmip + Cplex做两阶段鲁棒微电网优化
做微电网优化的人大概率都经历过这种纠结:用确定性模型跑出来的方案,风光一波动、负荷一变,第二天直接失配,储能充放电策略全乱套。于是很多人开始转向鲁棒优化,但一上手就发现,传统的单阶段鲁棒模型过于保守,把所有不确定性都压缩到同一层决策里,算出来的结果往往牺牲太多经济性。两阶段鲁棒优化的思路就是把决策拆成“先现在做”和“等不确定量揭晓后再调整”两个层次,在保证系统安全的前提下尽量找回经济性。这个思路在理论上很成熟,但落地到实际工程,最卡人的环节往往不是模型本身,而是求解器怎么配、不确定集怎么建、以及双层min-max结构怎么在代码里正确转成可计算的模型。本文用Yalmip + Cplex这套组合,完整拆解一个两阶段鲁棒微电网优化项目从建模到求解的整个过程,适合正在做微电网调度、新能源消纳、储能优化配置的在校研究生和刚入行的工程师参考。
为什么选Yalmip + Cplex这个组合?Yalmip是Matlab下的一个建模层,最大的价值在于它把优化问题的“描述”和“求解”分开了。你只需要把目标函数、约束条件、决策变量告诉Yalmip,它帮你自动识别问题类型(线性、二次、整数、混合整数等),再决定调用哪个求解器。Cplex则是业界公认的数学规划求解器,在处理混合整数线性规划和大规模线性规划时性能极其稳定,尤其适合电力系统这种动辄几十万条约束的模型。两阶段鲁棒优化经过对偶变换后,本质上会变成一个嵌套在大循环里的混合整数线性规划问题,这正是Cplex的主场。而且Yalmip天然支持Cplex接口,你只需要在代码里assign求解器选项,剩下的事Yalmip会帮你处理好。
这套方案在学术界和工业界都不是什么冷门选择,IEEE Trans、Applied Energy这些期刊上大量微电网两阶段鲁棒优化的论文,附带的代码十有八九都是Matlab + Yalmip + Cplex的搭配。它的优势在于开发效率高,建模过程写在Matlab里可视化方便,可以随时打印约束、检查模型规模,出了问题也容易定位;求解能力有Cplex兜底,不至于因为模型太大而直接卡死。缺点也有,Cplex的license费用不低,对没有授权的同学不太友好,但这个问题通过学术版或者免费的Community Edition可以在一定程度上解决,后面我会仔细讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与工具箱选型:把Matlab、Yalmip、Cplex三件套搭稳
2.1 版本兼容性:不同Matlab版本对Yalmip和Cplex支持的差异
先说版本问题,这是很多人入坑的第一道坎。Yalmip的兼容性其实相当好,目前主流的R2019b到R2024a版本都支持,最新的R2025a和R2026b在测试中也没有出现大问题。但Cplex的兼容性就要严格得多,IBM官方对每个版本明确标注了支持的Matlab版本范围。比如Cplex 12.9最多支持到Matlab R2019a,Cplex 12.10支持到R2020b,Cplex 20.1支持到R2021b左右,Cplex 22.1支持到R2022b到R2023a。如果你是2024年后的新版本Matlab,建议直接用Cplex 22.1及以上版本,否则在加载Mex接口文件时会直接报link错误。这个兼容性问题是matlab下载安装教程里出现频率最高的坑,也是Cplex安装教程里最容易被忽略的细节。
实操中还有一个技巧:Yalmip对Cplex版本的识别是通过yalmiptest自动完成的,如果你装了多个版本的Cplex,必须在Matlab的setenv里指定动态库路径。我见过不少人在装了Cplex 12.10和22.1两个版本后,activate总是调到旧版,折腾很久才发现是PATH顺序的问题。建议只保留一个版本的Cplex,别给自己找麻烦。
2.2 安装流程与路径配置:Yalmip和Cplex不报错的隐藏要点
Yalmip的安装相对简单,从GitHub下载压缩包后解压,然后在Matlab里addpath(genpath('yalmip文件夹路径')),之后savepath保存路径即可。问题往往出在Cplex上,它的Matlab接口需要单独的安装步骤。以Windows系统为例,安装完Cplex主程序后,还需要进入Cplex安装目录/cplex/matlab,在Matlab命令行里运行make,把Mex接口编译出来。这一步经常因为缺少C编译器而报错,建议先配好MinGW-w64或者Visual Studio的C++工具链再操作。
路径配置方面,比较稳妥的做法是把Cplex的cplex/examples/src/matlab和cplex/cplex/matlab两个目录都加入Matlab路径,然后在启动脚本里写这么一段启动脚本示例:
matlab复制% 在 startup.m 中配置
addpath('D:/Program Files/IBM/ILOG/CPLEX_Studio221/cplex/matlab');
addpath('D:/Program Files/IBM/ILOG/CPLEX_Studio221/cplex/examples/src/matlab');
% Yalmip
addpath('D:/Tools/yalmip');
savepath;
之后用yalmiptest验证是否全部通过。如果出现No suitable solver for KYP这类信息不用慌,那是Yalmip社区版求解器相关的提示,只要核心的LP/QP/MILP行能通过,就说明Cplex接入成功。小坑提醒:Cplex安装目录路径中不要出现中文和空格,Cplex对路径解析比较严谨,路径带空格会在调用外层优化时意外找不到动态库。
2.3 Cplex Community Edition的坑与替代方案
Cplex Community Edition(社区版)是免费使用的,但问题是它的模型规模有限制——变量数、约束数、非零元数都有上限(Variable limit 1000,Constraint limit 1000,Nonzero limit 10000)。做两阶段鲁棒微电网优化时,仅一个节点的微电网模型很容易就超过1000条约束,所以Community Edition只适合用来熟悉Yalmip建模语法、跑通小规模示例,真正的工程应用还是需要正式license。
对于没有正式license、又需要跑大规模模型的场景,替代方案有两个主流选择:开源的Cbc和Gurobi。Yalmip对Cbc的接口支持不错,但求解速度比Cplex慢一个量级;Gurobi性能和Cplex相当,且给学术用户提供免费license,申请流程不复杂,只需用学校邮箱在官网注册,个人版有效期一年。如果项目时间紧,我建议把Cplex和Gurobi都装上,Yalmip的solvesdp会自动选择可用求解器,多数情况下你无需手动指定。
3. 两阶段鲁棒优化建模:核心决策变量、目标函数与约束体系
3.1 两阶段鲁棒优化的基本思想:不确定量“揭晓”前与后的博弈
两阶段鲁棒优化在数学上可以写成通用形式:
text复制min_x { c'x + max_u∈U min_y { d'y : F(x, y, u) ≤ 0 } }
外层的min_x是第一阶段决策,也叫做“here and now”决策,必须在不确定参数实现之前就确定下来。对应到微电网场景,典型的第一阶段决策包括:机组的启停状态、储能的充放电计划、与大电网的购售电计划。内层的max_u min_y则代表了最坏情况下的运行调整,u是不确定参数,比如风光出力和负荷的预测误差,y是第二阶段决策,也叫做“wait and see”决策,在不确定量真实出现后可以做调整,比如微燃机的实际出力、储能的实际充放电量、可削减负荷的削减量。
初学者最困惑的点往往是:为什么内层是max?因为鲁棒优化追求的不是最可能场景下的最优,而是最坏场景下的可行性和经济性。内层的max_u相当于一个“恶劣天气制造器”,它在允许的不确定集范围内努力寻找一个最不利于你的场景,然后让你在这个场景下做最优调整。如果你在最坏场景下都算得出可行且较优的调度方案,那真实运行中大概率也不会出问题。
3.2 不确定集建模:盒式、预算和椭球不确定集的选择逻辑
不确定集的设计是两阶段鲁棒优化的灵魂,它直接决定了模型的保守程度和解的质量。微电网中常用的不确定集有三种:
盒式不确定集最简单,形式为u ∈ [u_min, u_max],它假设每个不确定参数都在各自的区间内任意取值,完全不考虑参数之间的相关性。这种做法模型表达最简单,但保守度很高,因为你得同时应对所有参数都取极端值的最坏情况。用生活化类比就是:你给全家每个人都准备了棉衣棉裤,但实际情况是只有一个人可能觉得冷。
预算不确定集是在盒式的基础上增加了总偏离程度约束,形式为Σ |u_i - u_i^mean| / Δu_i ≤ Γ。Γ是预算参数,它限制了所有不确定参数同时偏离预测值的“总量”,这样避免了所有参数同取极端值的过度保守问题。预算不确定集在微电网中应用最广,因为这个思想很直观:气象预报误差可能同时影响光伏和风电,但两者同时达到极端偏差的概率并不大,用Γ来控制联合偏离程度是工程上很自然的做法。
椭球不确定集用二次型约束(u-u_mean)' Σ^(-1) (u-u_mean) ≤ Ω描述不确定参数,可以捕捉参数之间的协方差信息,但因为引入了二次项,处理难度明显提升,通常需要对偶后用二阶锥约束近似求解。我之前做项目时对这三种都做过对比:在相同数据下,盒式集的目标函数最差(成本最高),椭球集居中,预算集通常能给出最好的经济性,但椭球集求解最慢。所以一般建议先用盒式集跑通代码流程,再逐步升级到预算集。
3.3 微电网两阶段鲁棒优化的完整约束体系拆解
两阶段鲁棒微电网优化的约束通常包括功率平衡约束、分布式机组出力约束、储能运行约束、与大电网交互约束等。每一步都要仔细想清楚哪些属于第一阶段,哪些属于第二阶段,因为第一阶段决策要满足的是“必须满足的硬条件”,第二阶段决策要满足的是“在最坏场景下也必须满足的条件”。
以一个典型的交流微电网为例,主要约束可以列表表示:
| 约束类型 | 表达式 | 所属阶段 | 说明 |
|---|---|---|---|
| 功率平衡 | ΣP_G + P_ES + P_PV + P_WT + P_grid = P_load - P_cut | 第二阶段 | 任一运行时刻必须满足 |
| 机组出力上下限 | P_G_min ≤ P_G ≤ P_G_max | 第二阶段 | 受制于实际运行状态 |
| 爬坡约束 | |P_G(t+1) - P_G(t)| ≤ R_G | 第二阶段 | 相邻时段出力变化限制 |
| 储能SOC更新 | SOC(t+1) = SOC(t) + (η_ch·P_ch - P_dis/η_dis)·Δt/E_cap | 第二阶段 | 储能动态特性 |
| 储能SOC上下限 | SOC_min ≤ SOC(t) ≤ SOC_max | 第一阶段/第二阶段 | 第一阶段确定的是计划曲线,第二阶段调整时仍需满足 |
| 购售电功率限制 | 0 ≤ P_buy ≤ P_line_max;0 ≤ P_sell ≤ P_line_max | 第一阶段 | 联络线功率上限 |
| 可削减负荷上限 | 0 ≤ P_cut ≤ α·P_load | 第二阶段 | 削减比例不超过某个阈值 |
注意SOC约束的特殊性:虽然SOC的期望变化轨迹在第一阶段就会算出,但第二阶段的最坏场景下你可能会调整储能的充放电,所以SOC约束必须同时出现在两个阶段的模型中,只是第二阶段模型里SOC是变量而第一阶段模型里SOC是一个线性表达式。
3.4 为什么两阶段鲁棒优化能有效抑制可再生能源波动影响——直观解释
用一个数字实验来说明可能会更直观。假设光伏预测出力100kW,实际出力区间是[60, 140]kW,负荷预测100kW,实际区间是[80, 120]kW。如果采用确定性优化,你会认为供电缺口恰好为0;但如果光伏实际只发60kW、负荷实际达到120kW,则缺口高达60kW,需要从电网购买或者启停机组。两阶段鲁棒优化会在第一阶段就把这种极端情形纳入考量:第一阶段决定机组启停和储能可用容量时,就会为“光伏少发+负荷超预测”同时发生的场景留好裕量。所以哪怕实际场景非常恶劣,系统也不会因为容量不足而失负荷。
但这里要注意鲁棒优化的“度”:过度鲁棒会导致成本显著上升。比如把不确定集设得过大,Γ取到最大值时,相当于你要为“光伏完全不发同时负荷翻倍”的场景设计系统,成本可能翻倍不止。因此工程上通常建议结合历史数据的概率分布信息,将预算参数Γ取在合理区间,同时通过敏感性分析确定最合适的保守度。这也是为什么很多论文在算例分析部分会专门做一个“不同Γ取值下的成本对比图”——这本身就是两阶段鲁棒优化的重要组成部分。
4. 问题转换与求解算法:C&CG算法如何与Yalmip结合
4.1 为什么不能直接套用单阶段鲁棒优化:min-max-min结构的嵌套本质
两阶段鲁棒优化最棘手的地方在于它是一个内嵌双层优化的问题:外层min,内层max-min。直接套用单阶段鲁棒优化的求解方法(比如简单地对偶转化)行不通,因为内层的min_y决策变量与外面max_u的交互关系无法一次性线性化。
一种最直接的思路是“穷举场景法”:把不确定集离散化,枚举每一个可能的场景,然后对每个场景求一个最小化成本,最后在所有场景中挑最坏情况对应的最优决策。但在微电网场景中,不确定变量的维度通常很高——如果风、光、负荷各有24个时段,每个时段的取值区间离散成10档,组合数就是天文数字,穷举法完全不可行。因此需要更智能的算法框架。
4.2 C&CG(列与约束生成)算法原理与迭代流程
C&CG(Column-and-Constraint Generation,列与约束生成)算法是目前求解两阶段鲁棒优化最主流的算法之一,它的核心思想是通过“主子问题迭代”的方式不断逼近最优解。算法流程如下:
主问题是一个相对简化的两阶段模型:它包含第一阶段决策变量x、第一阶段约束,以及针对有限个已知最坏场景u_1, u_2, ..., u_k的二阶段决策变量y_k和二阶段约束F(x, y_k, u_k) ≤ 0。主问题的目标函数是c'x + min_y_k d'y_k,其中y_k是对应于已识别的最坏场景的二阶段决策。这个主问题是一个规模不断增大的混合整数规划(因为每迭代一次就要加一组新的二阶段变量和约束)。
子问题则是给定主问题解出的x,求解内层max_u min_y { d'y : F(x, y, u) ≤ 0 }的最坏场景。子问题本身还是一个双层规划,需要通过对偶变换转成单层。由于子问题中x是固定参数,所以内层min_y的对偶可以显式写出,从而把子问题变成一个max问题(即对偶后与max_u合并)。
C&CG算法的核心迭代步骤可以总结为:
- 初始化:选择一个初始的不确定场景
u_0(通常是预测均值),作为主问题的首个场景。 - 求解主问题,得到最优解
x_k和最优值LB_k。 - 将
x_k代入子问题,求解最坏场景u_{k+1}和子问题目标值f_sub,得到上界UB_k = c'x_k + f_sub。 - 判断上界与下界之差是否满足收敛条件,如果
UB_k - LB_k < ε则停止;否则更新主问题:新增一组变量y_{k+1}以及对应于u_{k+1}的约束F(x, y_{k+1}, u_{k+1}) ≤ 0,转回第2步。
工程上收敛阈值的选取需要注意,太容合的阈值会导致主问题规模迅速膨胀、迭代次数过多,建议先设一个较宽的阈值(如1%)来观察迭代趋势,确认模型正确后再收紧到0.1%。
4.3 内层max-min子问题的对偶变换实操
子问题的对偶变换是数学细节较多的环节。为了说明清楚,我这里用一个简化模型作为例子。设子问题为:
text复制max_u∈U min_y { d'y : A·y ≤ b - B·u, y ≥ 0 }
固定x(它隐含在右侧常数项里)后,内层min_y的对偶形式为:
text复制max_λ { (b - B·u)'λ : A'λ ≥ d, λ ≥ 0 }
其中λ是对偶变量。于是子问题转成:
text复制max_{u, λ} { (b - B·u)'λ : A'λ ≥ d, λ ≥ 0, u ∈ U }
注意出现了乘积项u'λ,这是一个双线性项,直接求解很麻烦。但如果U是预算不确定集且u是离散区间变量,可以通过引入0-1变量和大M法线性化。对于盒式不确定集,有更简洁的处理方式:因为每个u_i的极值场景会在区间的端点取得,可以直接把端点枚举代入。
实际用Yalmip实现时,你不需要手工推导每一处线性化——Yalmip支持max和min的表达式,并且可以自动处理部分双线性项的线性化。但有个关键前提:双线性项中必须有一个变量是0-1变量,Yalmip才能借助大M法完成线性化。如果两个都是连续变量,Yalmip也会无能为力,还得自己手动处理。所以建模时可以在不确定变量的离散化表示上做文章。
4.4 收敛判据与迭代次数控制
C&CG算法的收敛速度在不同问题上有显著差异。在微电网场景中,通常3到5次迭代就能达到0.1%的相对间隙,但如果在不确定集设置较大、且系统中有大量的机组组合变量时,迭代次数可能会明显增加。一个重要经验:第一次迭代通常能提供最好的下界改进,后面几次迭代的改进幅度快速衰减。所以如果只关心一个大致结果,3次迭代基本够用;如果需要严格收敛,再考虑增加迭代上限。
另外,主问题规模的膨胀速度是值得关注的——每迭代一次,主问题会增加一组完整的二阶段变量和约束。假设第一阶段有机组开停机变量z_u(t),第二阶段有连续决策变量P_g(t)、储能SOC(t)和负荷削减P_cut(t),每一个时间段的变量数量大约是4到6个,每个z_u(t)还伴随整数变量约束。跑第10次迭代时主问题会比第一次大10倍,求解时间随之上升。因此推荐一个工程技巧:在每次迭代后固定那些已经确定但不会改变的整数变量(比如机组启停状态),只让连续变量参与迭代,这能显著降低主问题的求解压力。
5. Yalmip建模实操:从单阶段到两阶段鲁棒的代码实现
5.1 数据准备与场景参数设置
做两阶段鲁棒优化的第一步是初始化系统数据。以一个标准微电网系统为例,典型参数如下:
matlab复制% 系统参数设置
T = 24; % 调度时段
n_gen = 4; % 分布式机组数量
% 机组参数
P_gen_max = [1000; 800; 600; 400]; % 机组出力上限 (kW)
P_gen_min = [200; 150; 100; 50]; % 机组出力下限 (kW)
C_gen = [0.55; 0.65; 0.75; 0.85]; % 发电成本系数 (元/kWh)
R_gen = [300; 250; 150; 80]; % 爬坡速率 (kW/h)
% 储能参数
E_cap = 1000; % 储能容量 (kWh)
P_ch_max = 300; % 最大充电功率 (kW)
P_dis_max = 300; % 最大放电功率 (kW)
eta_ch = 0.95; % 充电效率
eta_dis = 0.95; % 放电效率
SOC_min = 0.1;
SOC_max = 0.9;
SOC_init = 0.5;
% 负荷预测(kW)——24时段
P_load = [850 820 790 760 740 730 780 900 1150 1280 1380 1420 ...
1450 1430 1380 1300 1260 1220 1180 1100 1020 950 900 870];
% 光伏预测(kW)
P_pv = [0 0 0 0 0 20 80 200 350 480 600 650 620 580 500 420 320 220 100 30 0 0 0 0];
% 风电预测(kW)
P_wt = [180 170 150 140 160 180 200 210 220 230 240 250 260 270 280 ...
275 265 240 220 200 190 185 180 175];
注意这些负荷和可再生能源出力数据只是初始预测值,后续在鲁棒框架中会围绕它们构建不确定性区间。
5.2 单阶段确定性模型的Yalmip建模
为了对比和验证,通常先搭建一个单阶段确定性模型作为基准。这个模型不包含不确定性,直接用预测值求解。将Yalmip建模过程拆开来看:
matlab复制% 定义连续变量
P_g = sdpvar(n_gen, T, 'full'); % 机组出力
SOC = sdpvar(1, T+1, 'full'); % 储能SOC
P_ch = sdpvar(1, T, 'full'); % 充电功率
P_dis = sdpvar(1, T, 'full'); % 放电功率
P_buy = sdpvar(1, T, 'full'); % 向主网购电
P_sell = sdpvar(1, T, 'full'); % 向主网售电
P_cut = sdpvar(1, T, 'full'); % 负荷削减
% 定义整数变量
z_gen = binvar(n_gen, T, 'full'); % 机组启停状态
z_ch = binvar(1, T, 'full'); % 储能充电状态
z_dis = binvar(1, T, 'full'); % 储能放电状态
z_buy = binvar(1, T, 'full'); % 购电状态(避免同时购售)
目标函数是全天总成本最小化,包括燃料成本、与大电网交互成本、负荷削减惩罚:
matlab复制% 目标函数
obj = sum(sum(C_gen .* P_g)) ... % 机组燃料成本
+ sum(C_buy .* P_buy) - sum(C_sell .* P_sell) ... % 与大电网交互净成本
+ sum(C_cut .* P_cut); % 负荷削减惩罚
约束条件需要逐条建立。功率平衡是等式约束,SOC动态更新是储能的核心协变约束,机组出力范围、爬坡约束和启停逻辑共同限制机组行为,这些都是本模型的基本盘。完整代码示范:
matlab复制cons = [];
% 功率平衡约束:发电 + 储能放电 + 电网购电 = 负荷 - 削减 + 充电 + 光伏 + 风电
cons = [cons, sum(P_g,1) + P_dis + P_buy + P_pv + P_wt == ...
P_load - P_cut + P_ch + P_sell];
% 机组出力上下限
cons = [cons, P_gmin .* z_gen <= P_g <= P_gmax .* z_gen];
% 爬坡约束(注意t=1时无历史出力,稍做简化处理)
cons = [cons, -R_gen(:,2:end) <= P_g(:,2:end) - P_g(:,1:end-1) <= R_gen(:,2:end)];
% 储能SOC递推
cons = [cons, SOC(2:end) == SOC(1:end-1) + (eta_ch*P_ch - P_dis/eta_dis)/E_cap];
% 储能充放电互斥
cons = [cons, z_ch + z_dis <= 1];
% 功率限制
cons = [cons, 0 <= P_ch <= P_ch_max .* z_ch];
cons = [cons, 0 <= P_dis <= P_dis_max .* z_dis];
cons = [cons, 0 <= P_buy <= P_line_max .* z_buy];
cons = [cons, 0 <= P_sell <= P_line_max .* (1-z_buy)];
% SOC上下限
cons = [cons, SOC_min <= SOC <= SOC_max];
% 负荷削减限制
cons = [cons, 0 <= P_cut <= 0.1 * P_load];
使用solvesdp求解:
matlab复制ops = sdpsettings('solver','cplex','verbose',2,'cplex.mip.tolerances.mipgap',1e-4);
optimize(cons, obj, ops);
这个基准模型的求解时间在个人电脑上一般不超过1秒,结果可以作为两阶段鲁棒模型的对照。
5.3 两阶段鲁棒模型的Yalmip实现框架
两阶段鲁棒模型不能像单阶段那样直接写成一个solvesdp调用,而是需要把主问题和子问题分别封装成函数,在C&CG迭代框架中反复调用。
首先定义不确定集。假设光伏出力P_pv和负荷P_load都存在预测误差,分别设为±20%和±10%,采用预算不确定集:
matlab复制P_pv_mean = P_pv; % 预测值
P_pv_dev = 0.2 * P_pv_mean; % 光伏波动区间 ±20%
P_load_mean = P_load;
P_load_dev = 0.1 * P_load_mean; % 负荷波动区间 ±10%
Gamma = T/2; % 预算参数,表示最多允许T/2个时段存在偏差
预算不确定集的完整线性化需要引入0-1变量w_pv(t)和w_load(t),表示该时段是否需要偏离预测值:
matlab复制w_pv = binvar(1, T, 'full');
w_load = binvar(1, T, 'full');
% 实际取值 = 预测值 + 偏差方向 * 偏差幅度
P_pv_actual = P_pv_mean + P_pv_amp .* w_pv; % 简化:只考虑上偏或下偏的极端
P_load_actual = P_load_mean + P_load_amp .* w_load;
% 偏差总量约束(预算约束)
cons_budget = [cons_budget, sum(w_pv) + sum(w_load) <= Gamma];
% 上下限约束(每个时段的具体取值区间)
cons_budget = [cons_budget, P_pv_mean - P_pv_dev <= P_pv_actual <= ...
P_pv_mean + P_pv_dev];
cons_budget = [cons_budget, P_load_mean - P_load_dev <= P_load_actual <= ...
P_load_mean + P_load_dev];
实际中如果同时考虑风、光、负荷三种不确定性,需要分别设置预算参数。
主问题函数接受一组已知的最坏场景U_k(一个矩阵,每列代表一个场景下的光伏和负荷取值),思路可以封装如下:
matlab复制function [x_sol, LB] = solve_master(U_k, data)
% 定义第一阶段变量(机组启停、储能计划、购售电计划)
z_gen = binvar(n_gen, T, 'full');
...
% 定义第二阶段的“场景变量”:每个已知场景对应一组二阶段变量
n_k = size(U_k, 2); % 当前已知的场景数
for k = 1:n_k
P_g_k{k} = sdpvar(n_gen, T, 'full');
SOC_k{k} = sdpvar(1, T+1, 'full');
P_ch_k{k} = sdpvar(1, T, 'full');
P_dis_k{k} = sdpvar(1, T, 'full');
P_buy_k{k} = sdpvar(1, T, 'full');
P_sell_k{k} = sdpvar(1, T, 'full');
P_cut_k{k} = sdpvar(1, T, 'full');
end
% 目标函数:第一阶段成本 + 所有已知场景下方差成本的期望(此处简化为若干已知场景的目标和)
obj = sum(sum(C_gen .* P_g_1st)) + sum_{k}(sum(sum(C_gen .* P_g_k{k})) + ...);
% 约束条件:第一阶段约束 + 每个场景对应的二阶段约束
...
end
子问题函数则在给定第一阶段x后,求解最坏场景:
matlab复制function [u_worst, sub_obj] = solve_sub(x_fixed, data)
% 根据固定的x,定义不确定变量u(利用预算不确定集)
w_pv = binvar(1,T,'full');
w_load = binvar(1,T,'full');
...
% 定义二阶段决策变量
P_g = sdpvar(n_gen, T, 'full');
...
% 对偶方法:子问题直接写max-min的等价单层模型
% 这里使用“枚举端点 + 对偶”或者“直接建立max形式模型”
% 由于Yalmip不能直接表达min,我们需要用对偶重写
% 更实际的做法:将内层min的KKT或对偶条件写入,形成单层模型
...
end
有一个非常实用的设计模式:子问题内部的对偶处理不需要你用纸笔一步步推导,你可以在Matlab里借助Yalmip的dual函数对偶,或者使用Yalmip自带的plot和export等功能先把表达式导出来检查。很多论文中的做法是:给定x_fixed后,二阶段决策变量的最优值表现为线性规划,直接调用linprog或者dualsol,再把对偶变量信息拿回来构造子问题的目标。下面是子问题的核心对偶代码示例:
matlab复制% 子问题:给定x后,求解最坏场景下的最小运行成本
function [u_worst, sub_obj, lambda] = solve_sub(x_fixed, data)
% 创建二阶段变量
P_g = sdpvar(n_gen, T, 'full');
...
% 二阶段约束(全部是关于u的线性约束)
cons_sub = [...];
objective_sub = sum(sum(C_gen .* P_g)) + ...;
% 对偶:将min问题的约束和代价函数转置到对偶空间
% 首选方法是直接调用Yalmip的dual
[~, ~, lambda] = export(cons_sub, objective_sub, sdpsettings('solver','cplex'));
% 最坏场景u从对偶问题中解出
ops = sdpsettings('solver','cplex','verbose',0);
optimize([cons_dual], -objective_sub, ops); % 对偶问题的目标最大化
% 提取最坏场景
u_worst = value([P_pv_actual; P_load_actual]);
sub_obj = value(objective_sub);
end
这里有一个容易迷糊的地方:export函数的作用是将Yalmip模型转化成求解器能读的结构体,同时返回对偶变量lambda的索引。但实际使用中,大部分情况下你不需要手动获取lambda的具体数值,你只需要用Yalmip的原生语法构建好子问题模型,然后直接调用optimize。因为Yalmip内置的自动对偶转化能力足以处理标准形式的线性/混合整数模型。
5.4 C&CG主循环代码实现与结果验证
主循环是整个程序的核心驱动,整合主问题和子问题:
matlab复制% 未知场景集合初始化:先假设预测场景作为第一个最坏场景
U_k = [P_pv_mean; P_load_mean]; % 第一个已知场景:预测值
UB = inf;
LB = -inf;
gap = inf;
iter = 0;
max_iter = 10;
while gap > 1e-3 && iter < max_iter
iter = iter + 1;
% 1. 求解主问题:获得调度决策x与下界LB
[x_sol, LB_new] = solve_master(U_k, data);
LB = LB_new;
% 2. 代入子问题求最坏场景:获得上界UB
[u_worst, UB_sub] = solve_sub(x_sol, data);
UB_temp = UB_sub;
UB = min(UB, UB_temp);
% 3. 更新gap
gap = (UB - LB) / abs(UB) * 100;
fprintf('迭代%d:LB=%.4f, UB=%.4f, gap=%.4f%%\n', iter, LB, UB, gap);
% 4. 将新的最坏场景加入主问题
U_k = [U_k, u_worst];
end
这里有个细节需要注意:UB = min(UB, UB_temp)与直接取UB_temp的选择。理论上每轮迭代得到的最坏场景对应的总成本都应该是可行上界,但数值求解误差可能导致某些轮次的UB偏离,取历史最小值更稳妥。而LB则直接取当前主问题的最优值,因为主问题的可行域随迭代扩大,最优值单调非降,所以LB@iter必然是越来越紧的下界。
在实际运行中,第一轮迭代后的LB可能会明显小于UB,gap可能高达百分之十几,但随着第二轮、第三轮将最坏场景纳入主问题,gap会迅速缩小。跑完整个迭代后,输出最终的第一阶段调度结果(机组启停、储能计划、购售电计划),可以用plot查看曲线是否合理,比如储能是否在光伏大发时段充电、负荷高峰时段放电,机组出力是否在上下限范围内等。
5.5 一套可复用的完整项目代码结构
为了让项目更工程化,这里推荐一种清晰的文件组织方式:
text复制project_root/
├── main.m % 主程序入口
├── data/
│ └── system_data.m % 系统参数与预测数据
├── models/
│ ├── build_master.m % 主问题构建函数
│ ├── build_subproblem.m % 子问题构建函数
│ └── uncertain_set.m % 不确定集定义函数
├── algorithms/
│ └── ccg_solver.m % C&CG主循环
└── utils/
├── save_results.m % 结果保存
└── plot_result.m % 结果可视化
每次跑新算例时,只需要修改system_data.m中的基础参数和预测数据即可,无需改动算法框架。这套结构在后续扩展时也很有用——比如要加储能寿命损耗约束、需求响应弹性负荷或碳交易成本,只需要在对应模型文件中新增约束块。
6. 两阶段鲁棒优化结果分析:经济性、保守度与运行安全的多维对比
6.1 确定性模型与鲁棒模型的成本构成对比
跑完程序后,首先要做的是结果对比分析。一个典型算例的结果可能如下:
| 模型类型 | 总成本(元) | 购电成本(元) | 机组燃料成本(元) | 负荷削减成本(元) |
|---|---|---|---|---|
| 确定性 | 8230.5 | 1420.3 | 6810.2 | 0 |
| 鲁棒(Γ=4) | 9310.8 | 1680.5 | 7620.3 | 10.0 |
| 鲁棒(Γ=8) | 10420.6 | 2100.7 | 8280.9 | 39.0 |
| 鲁棒(Γ=12) | 12130.4 | 2780.2 | 9230.5 | 119.7 |
| 鲁棒(Γ=24,等于T) | 16540.2 | 4320.1 | 11320.3 | 899.8 |
从这个表格中可以直观看出:鲁棒度越高,成本越高,但同时也意味着对不确定性的应对能力越强。特别是当Γ达到最大值24时,相当于为每个时段都预留了极端应对能力,成本翻倍,运行裕量极大但经济性极差。工程实践中,应该根据系统的风险评估需求选择合适的Γ,而不是一概取最大或最小。
6.2 不同不确定集下的结果对比
除了预算不确定集,还可以对比盒式、椭球不确定集。在相同测试数据下,盒式不确定集的成本往往比预算集高出15%到25%,椭球集居中。展现在博文或报告中的话,建议先生成一张对比表格,再用plot绘制储能SOC曲线、机组出力曲线,直观展示不同不确定集下的调度策略差异。
在风电渗透率高的场景中,不同不确定集对储能充放电策略的影响尤为显著:盒式集下储能几乎会预留一个宽大的安全SOC带,导致日常调峰容量变小;预算集下储能利用率明显更高,SOC能进入更深放电区间;椭球集则介于两者之间,且对预测置信区间的依赖更大。
6.3 鲁棒优化结果在真实微电网中的落地考量
两阶段鲁棒优化并不是一个孤立的数学问题,它最终要接到实际运行中。有几个工程细节值得特别注意。一是预测误差分布的更新,鲁棒不确定集的构建应该定期基于历史预测误差数据重新校准,而不是一劳永逸地设定固定比例。比如光伏预测误差在晴天和阴天的偏差幅度差别很大,如果统一用±20%,可能在某些天气下过于保守,在另一些天气下又不够保守。二是与实时调度层的接口,两阶段鲁棒的“第一阶段决策”通常是日前计划,但在日内运行中还需要根据实际气象数据做滚动修正,所以两阶段鲁棒优化更适合作为日前调度的决策工具,配合日内滚动MPC形成双层控制框架。
7. 实际测试与常见问题排查:从报错信息定位模型Bug的经验公式
7.1 Yalmip建模层面的常见报错及对策
报错1:Double click on error: "In 'optimize' (line 287), YALMIP could not find a suitable solver"
这个报错通常出现在Yalmip无法识别问题类型,或者没有可用求解器时。排查步骤如下:
- 先运行
yalmiptest确认Cplex是否正常挂载。 - 检查模型类型是什么——如果模型中含有非凸二次项,Cplex不一定支持非凸二次规划(除非设置
cplex.mip.strategy.integer等参数)。 - 检查约束中是否存在非线性的等式约束(比如
P_ch * P_dis == 0),这是新手最常犯的错误。储能充放电互斥通常需要用0-1变量或者互补约束来表示,直接乘两个连续变量会导致模型变成非凸二次约束,Cplex很可能会报No suitable solver。
报错2:Failed: NaN detected in optimization variables
这个大概率是因为第一阶段的决策变量在子问题代入时没有正确赋值。在C&CG循环中,子问题函数接收的x_fixed必须通过value()函数提取数值,不能在子问题里继续把第一次阶段的sdpvar传进去。正确的做法是:
matlab复制% 在主循环中
x_fixed = value(x_vars); % 提取数值
[u_worst, UB_sub] = solve_sub(x_fixed, data);
然后在子问题函数内部,用assign或者直接把x_fixed作为常数代入约束:
matlab复制cons = [cons, P_g >= P_g_min .* x_fixed.z_gen]; % z_gen是数值矩阵
报错3:Cplex error 5002: Objective is not convex
这个报错通常是目标函数或者约束条件中存在非凸二次项。检查是否使用了二次形式的目标(比如储能充放电成本写成P_ch^2),Cplex默认只处理凸二次项;非凸问题需要设置cplex.optimalitytarget = 3,但不推荐依赖这个操作。更合理的做法是换用分段线性化处理非线性成本。
7.2 算法收敛层面的异常处理
问题1:gap不下降,一直停在某个值附近
这通常意味着主问题或子问题中有一个出了问题。常见原因是主问题中缺少了某些必要的第二阶段约束,导致下界无法逼近上界。检查方法:每次迭代后打印主问题和子问题的目标分解,如果一个合理增长(LB)而另一个明显跳动(UB),多半是子问题漏了约束。
问题2:子问题最优值出现负无穷
这说明子问题可能是无界的。典型原因是目标函数里缺少对二阶段变量的合理惩罚项,比如负荷削减惩罚系数C_cut设置得太小或为0,导致为了保证功率平衡,削减负荷变为“免费”手段,子问题会不对地扩大削减量。解决方法:给负荷削减设置合理的惩罚成本C_cut(通常设为购电成本的2到3倍)。
问题3:第一阶段决策变量出现非物理值
比如机组启停状态在相邻时段不断震荡,储能SOC出现折返跳变。这通常是约束缺失或惩罚系数失衡导致的。检查是否缺少爬坡约束、是否缺少启停最小持续时间约束。很多模型简化时会把启停最小持续时间省掉,跑确定性模型可能还行,但在鲁棒框架的worst-case场景驱动下,这种简化会引发严重振荡。
7.3 性能优化:当模型规模过大时的降维与加速技巧
两阶段鲁棒优化模型本身规模就大,迭代之后还在膨胀。如果你的算例有几十个节点,求解时间可能从几十秒飙到几十分钟。这时候有几个非常实用的加速技巧:
一是场景约简。在加入新的最坏场景到主问题时,可以预先对子问题求出的最坏场景做相似性判断。如果新场景与已有场景的偏差向量的欧氏距离很小(比如低于某个阈值),就不必加入主问题,这能在几乎不损失精度的前提下显著降低迭代次数。二是固定整数变量。迭代到3轮以后,机组的启停状态往往趋于稳定,可以在后续迭代中固定这些0-1变量,只优化连续变量。这能让主问题从混合整数规划降级为线性规划,求解速度快一个数量级。三是热启动。Cplex支持从上次求解的变量初值开始新一轮求解,通过sdpvar初始值设置和solvesdp的warmstart参数可以缩短求解时间。
8. 扩展方向与进阶建议:从微电网到园区综合能源系统
两阶段鲁棒优化的方法论不只局限于微电网。我后来做园区综合能源系统时,发现这套框架稍加改动就能扩展到冷热电联供系统、含氢储能系统、光储充一体化场站等场景。需要调整的主要是:能源种类从单一电能扩展到电能、热能、冷能、氢能等多能流耦合;不确定性来源从风光负荷扩展到热负荷、气价、碳价等;设备模型从简单的机组约束扩展到热电联产机组、热泵、电锅炉、电解槽、燃料电池等耦合设备。
Yalmip + Cplex这套工具链天然支持混合整数线性规划,因此多能流的线性化模型都可以被高效求解。不过要注意,当引入非线性效率曲线(比如电解槽的效率随负载率变化)时,模型规模会急剧膨胀,适度线性化往往是实用性的关键。我的经验是:在鲁棒优化的框架里,能线性化的地方尽量线性化,非线性精度损失远小于不确定性带来的偏差。
另外一个热门方向是分布鲁棒优化(Distributionally Robust Optimization),它是对两阶段鲁棒优化的重要升级。分布鲁棒优化用一组概率分布来描述不确定性,而不是只给定区间。求解上大同小异,也是Yalmip + Cplex可以覆盖的范畴,只是不确定集的构造和对偶推导会更复杂一些。如果读者在完成本文模型后想进一步进阶,分布鲁棒是一个自然的学习路径。
最后分享一个我个人的体会:两阶段鲁棒优化这种模型,最大的价值不是算出那个“最优成本数值”,而是它逼着你把系统的物理约束、运行逻辑、不确定性来源想得清清楚楚。我在做第一个微电网项目时,光是在SOC递推约束和充放电互斥逻辑上的bug就折腾了两天半。但模型一旦调试通过,整个系统的运行逻辑就变得异常清晰,后续扩展其他场景时几乎是一通百通。如果你是研究生,这篇博文的代码框架完全可以作为你论文的核心算法模块,结合你自己的场景数据稍加调整就能出结果。
