微电网容量配置这件事,我是真吃过亏。早年做园区微网的源网荷储规划,拿典型日的光伏、风电、负荷曲线做确定性优化,算出配置方案时还挺满意。结果项目落地第三个月,连续一周阴雨又叠加一次强阵风,光伏出力腰斩,风电出力剧烈波动,储能很快就触底,柴发长时间半载运行,那一个月的整体运行成本比设计值高了近三成。那段经历让我彻底明白了一个道理:微网多电源容量配置的真正难点,不在容量怎么算,而在于怎么让方案在"最不利的情况下"仍然扛得住。而这个问题的标准解法,就是两阶段鲁棒优化算法。这篇文章我就把"两阶段鲁棒优化 + 微网多电源容量配置 + Matlab代码实现"这条线完整讲清楚,从原理推导到代码骨架再到调参避坑,一次讲透。
1. 从"拍脑袋选容量"到"两阶段鲁棒优化":为什么这个问题值得做
1.1 微网多电源容量配置到底在解决什么问题
微网通常是指包含风电、光伏、柴油机/燃气轮机、储能以及各类负荷的小型电力系统,可以并网运行也可以离网运行。所谓多电源容量配置,就是在满足供电可靠性、储能循环寿命、机组运行边界等一堆约束的前提下,决定风电机组装几台、光伏组件装多少、储能电池配多大、柴油机选什么规格,使得整个微网在生命周期内的总成本最低。
这里有个很容易被忽视的点:容量决策是一次性的、前置的,而运行调度是反复发生的。你把风机装多了,在风速平缓的季节,风机就是一堆闲置资产;你把储能配大了,前期投资沉没成本高,还可能常年用不满。所以这个问题本质上就带着"先投资、后运行"的时间先后特征。
1.2 传统确定性规划的局限
很多人第一步想到的是确定性规划:把风速、光照、负荷统一替换成"典型日"或"最恶劣单一场景"。我在早期也是这么做的,结果就是上面那个翻车案例。典型日方法的问题在于:它用平均值或加权平均掩盖了真实波动的相关性。连续阴雨天、夜间大负荷叠加低风速、台风前后的极端风况,这些组合根本不是一条典型日曲线能表达的。
也有人用"最恶劣单一场景"来定容量,但场景怎么选、多恶劣才算够?人工挑场景本质上是把不确定性拍成几个离散点,一旦真实场景超出预定义范围,配置方案就会失效。而且单一最恶劣场景往往会同时假设所有时段都极端,这又太过于保守,投资成本高到业主无法接受。
1.3 鲁棒优化:用"不确定集合"代替"单一场景"
鲁棒优化做的事情是把所有可能的波动描述成一个不确定集合 U,然后寻找一个容量配置方案,使得即便不确定性参数落在集合里最坏的位置,系统依然能够通过合理的运行调度满足所有约束。换句话说,它优化的是"最坏情况下的总成本",而不是"期望情况下的总成本"。这样得到的配置结果看起来会比确定性方案略贵,但换来的是很强的"不后悔"特性。
两阶段鲁棒优化和单阶段鲁棒优化的区别在于:单阶段要求容量和调度必须同时、一次性决定,这往往太保守,因为现实中调度是可以根据实际风光的观测结果来调整的;而两阶段则允许先决策容量、等不确定性参数实现后再做调度决策。这个"先决策、后调整"的时序,恰好和微网容量配置的物理过程完全一致。
2. 容量配置问题的数学建模骨架
2.1 系统假设与需要决策的东西
建模之前先把系统边界说清楚。以最常见的并网/离网混合微网为例,假设系统包含风电机组(WT)、光伏(PV)、柴油发电机(DE)、蓄电池储能(ESS)以及电负荷。
第一阶段变量是装机决策,包括各类电源的安装台数/容量。为了贴近实际工程,装机台数通常建模为整数变量,比如 x_w ∈ {0,1,2,...} 表示风电装机台数,x_pv 表示光伏装机容量等级,x_ess 表示储能容量等级,x_de 表示柴油机台数。这样问题就变成一个混合整数规划(MIP)问题。
第二阶段变量是运行调度变量,包括每个时段 t=1...T 的风电实际出力 P_w,t、光伏出力 P_pv,t、柴油机出力 P_de,t、储能充放电功率 P_ch,t 和 P_dis,t、蓄电池 SOC_t,以及可能发生的切负荷量 P_cur,t。第二阶段变量在容量确定后、不确定场景实现时才能决定,这正是两阶段结构的来源。
2.2 目标函数:年综合成本怎么算
容量配置类问题的目标函数一般写成年综合成本最小化。公式不粘贴复杂推导,直接给出工程里常用的结构:
min C_inv + 365 × (C_fuel + C_om + C_cur)其中 C_inv 是年化投资成本,把风机、光伏、储能、柴油机各自的一次性投资按利率和寿命折成每年费用;C_fuel 是柴油机燃料成本;C_om 是运维成本;C_cur 是切负荷惩罚成本。
为什么要把燃料成本和切负荷惩罚都放进来?因为鲁棒优化要在所有最坏场景里找成本最高的那个,而切负荷惩罚是约束系统可靠性的代表。没有惩罚项,优化器可能会选择最小化投资成本、然后疯狂切负荷这种"技术上可行、实际上荒诞"的方案。常见做法是切负荷惩罚设成远高于发电成本的数量级,比如 10 元/kWh 以上。
2.3 约束条件:功率平衡、储能SOC、柴油机运行边界
约束部分至少要有下面几类。功率平衡约束是所有微网模型的地基:每个时段风电、光伏、柴油机、储能放电功率、购电(并网时)之和,等于负荷加储能充电功率加切负荷。
一个关键细节是:风电和光伏在确定性模型里是固定参数,在鲁棒模型里要被替换成不确定参数。
P_w,t^u = P̄_w,t + u_w,t × Δ_w,t其中 P̄_w,t 是预测出力,Δ_w,t 是最大偏差,u_w,t 限制在 [-1,1],表示实际出力可以在预测值附近波动。
储能约束包括 SOC 递推关系、容量上限、SOC 上下限以及充放电功率上限。SOC 递推的经典写法是:
SOC_t+1 = SOC_t + η_ch × P_ch,t − P_dis,t / η_dis柴油机约束包括出力上下限、爬坡约束、最小启停时间。此外还有容量与运行变量的耦合约束,比如储能充放电功率不能超过已装容量对应的上限,风电实际出力小于等于装机容量对应的预测上限。
2.4 为什么天然是两阶段结构
我经常被问:为什么这里一定要用两阶段,不能把容量和调度写在一起同时优化吗?
可以,但那样做就没有兑现"调度决策可以根据实际天气来调整"这个信息优势。真实微网的调度是这样的:容量是年初就定死的,而风光的实际出力要等到当天、当时才知道,然后运行人员才据此安排柴油机和储能。两阶段模型把"知道不确定值之前的决策"和"知道不确定值之后的决策"分开处理,正好反映了这个信息结构。如果合成单阶段优化,等于强迫决策者在不看天气的情况下把每个时段的运行功率都定死,结果只会更保守、成本更高,而且违背物理直觉。
3. 两阶段鲁棒优化原理:min-max-min到底在解什么
3.1 不确定变量的建模方式
先看最常用的盒式不确定集合。对风电而言,实际出力 P_w,t 可以在预测值附近波动:
U = { u ∈ R^(N_T) : ||u||_∞ ≤ 1, ||u||_1 ≤ Γ }这个公式看起来有点抽象,翻译成人话就是:每个时段的风电出力相对预测值最多偏离一定百分比(无穷范数限制),同时所有时段累计偏离总量不超过一个预算 Γ(1-范数限制)。
为什么要加 Γ?因为如果允许每个时段都同时处于极端偏差,那造出来的方案会过于保守、成本高得离谱。预算 Γ 让模型可以描述"虽然极个别时段会很恶劣,但不是所有时段同时恶劣"的现实。这个思想是鲁棒优化的灵魂,也直接影响配置结果——后面第 5 节我会给一个不同 Γ 下配置变化的对照。
3.2 两层决策的嵌套结构直观解释
两阶段鲁棒优化的标准形式是:
min_x { c^T x + max_(u∈U) min_(y∈Ω(x,u)) d^T y }其中 x 是容量决策,y 是调度决策,u 是风光不确定参数。只看公式容易晕,我习惯用"买保险"来类比。
你想买一台备用发电机来应对小区停电。x 是"买多大的发电机",属于现在就要拍板的事;u 是"未来停电最严重的程度",属于你无法控制的最坏情况;y 是"停电发生时你怎么调度这台发电机、要不要给电梯优先供电",属于事到临头才能做的安排。两阶段鲁棒就是问:在最坏的停电情况下,我买的发电机和事后的调度策略配合起来,总成本还能不能接受?如果太贵,就换一个容量方案重新评估。
这样理解,min-max-min 就不是玄学了,而是一个很自然的"先定方案、再被最坏现实考验、再事中应对"的过程。
3.3 C&CG算法:主问题与子问题的拆分
两阶段鲁棒问题直接求解非常困难,因为 max 和 min 嵌套,不是一个标准可解模型。工程和学术界最常用的解法是列与约束生成(C&CG)算法,也叫场景割法。
思路是把原问题拆成一个主问题(MP)和一个子问题(SP)。主问题在一组已知的恶劣场景 u_1, u_2, ..., u_k 下做容量+调度联合优化,得到下界解;子问题固定主问题给出的容量 x,去寻找能够让运行成本最大化的新恶劣场景 u_(k+1),把该场景下的运行成本作为上界解。如果上下界差距小于阈值,迭代停止;否则把这个新场景以及对应的调度变量一起加入主问题,再重复。
注意这个关键点:主问题每次不是只加一条割约束,而是把新场景对应的整套第二阶段变量和约束都加进去,所以叫"列与约束生成",而不是经典的 Benders 割。这样处理的好处是收敛速度快很多。在容量配置这类问题上,通常迭代十几次以内就能收敛。
3.4 子问题求解的关键:对偶转换与双线性项处理
子问题固定 x 之后是关于 y 和 u 的问题,内层 min 是 LP,外层是 max。直接 max-min 无法用通用求解器处理,通常做法是先对 min 用拉格朗日对偶转成 max,再把两个 max 合并,于是目标变成关于对偶变量 λ 和不确定变量 u 的函数。
麻烦的是,这里大概率出现 λ 乘 u 的双线性项——这两个都是变量,乘积让问题变成非凸。我见过很多 demo 代码在这里含糊带过,实际落地时最常用的招数是引入辅助变量和大 M 法把它线性化。比如 λ_i × u_j,引入 z_ij 替代,并添加形如 z ≤ M×u_indicator、z ≤ λ 的约束,把原问题转换成 MILP 后用 Cplex/Gurobi 直接求解。
也可以走 KKT 路线,把内层 min 的最优性条件直接写进外层 max,变成带互补条件的 MPEC 再用求解器算。这个思路在主问题规模不大的时候没问题,但互补条件含大 M 参数,设置同样很考验经验。所以我个人更推荐对偶+线性化这条路线。
4. Matlab代码实现:从公式到可运行程序
4.1 环境准备:Yalmip + Cplex/Gurobi
Matlab 实现我建议用 Yalmip + Cplex/Gurobi 这套组合。Yalmip 能让你直接写 sdpvar、intvar 和约束,避免手写求解器 API 的体力活,而 Cplex 的 MIP 求解性能在容量配置这种问题上明显比 Matlab 内置的 intlinprog 稳。
如果你的 Matlab 版本比较新,Yalmip 安装只需要把解压后的 yalmip 文件夹加入 matlab path 就行。Cplex 或 Gurobi 记得要安装对应的 Matlab 接口,安装完成后在命令行输入yalmiptest能跑通就说明环境没问题。这个环节经常有人卡在 solver 找不到,多半是路径没配对或者 license 没激活,建议先在自带 yalmip demo 上验证再往下走。
4.2 变量定义与参数初始化
用 Yalmip 写这个问题的第一步是定义变量。我习惯把第一阶段变量和第二阶段变量分开,方便 C&CG 迭代时做场景扩展。
T = 24; % 调度周期,小时 nW = 10; % 风电候选最大台数 nPV = 20; % 光伏候选组数 nESS = 5; % 储能候选容量等级 nDE = 3; % 柴油机候选台数 % 第一阶段变量:容量决策(整数) x_w = intvar(1, nW, 'full'); x_pv = intvar(1, nPV, 'full'); x_ess = intvar(1, nESS, 'full'); x_de = intvar(1, nDE, 'full'); % 第二阶段变量:调度决策(连续) P_w = sdpvar(1, T, 'full'); P_pv = sdpvar(1, T, 'full'); P_de = sdpvar(1, T, 'full'); P_ch = sdpvar(1, T, 'full'); P_dis = sdpvar(1, T, 'full'); SOC = sdpvar(1, T, 'full'); P_cur = sdpvar(1, T, 'full');注意:装机台数用intvar而不是binvar,因为允许装多台同型号机组,不是简单的装或不装。如果候选容量是连续变量也可以换成sdpvar,但工程上设备都有标准规格,整数更真实。
4.3 主问题MP的编写
C&CG 主问题需要给每个已发现的恶劣场景 k 单独建立一套第二阶段变量。我写成 cell 数组,每轮迭代新增一个场景就新增一组变量:
% 假设已经找到场景集 u_set,尺寸为 T×K,每列是一个场景 P_w_k = cell(1, K); P_pv_k = cell(1, K); P_de_k = cell(1, K); % ... 其他调度变量同理 for k = 1:K P_w_k{k} = sdpvar(1, T, 'full'); % 对应约束:当前场景下的风电出力上限 Constraints = [Constraints, P_w_k{k} <= u_set(:,k)' .* P_w_max_pred]; % 功率平衡:P_w_k + P_pv_k + P_de_k + P_dis_k + P_buy_k % == P_load + P_ch_k + P_cur_k Constraints = [Constraints, P_w_k{k} + P_pv_k{k} + P_de_k{k} ... + P_dis_k{k} == P_load + P_ch_k{k} + P_cur_k{k}]; % ... 储能SOC、柴油机上下限等 end Objective = inv_cost(x_w, x_pv, x_ess, x_de) ... + 365 * sum(op_cost_k); % 各场景运行成本加权主问题相对好写,但它有很强的"多场景扩展"属性。每次迭代新增的不是一行约束,而是一整块,即新场景对应的全部调度变量与约束。这是 C&CG 和 Benders 在编码上的最大差异,写的时候别图省事只加约束不加变量,否则新约束引用不存在的变量会直接报错。
4.4 子问题SP的编写
子问题固定 x 后求解恶劣场景。这里按第 3.4 节的对偶+大M写法给出核心骨架,注意目标方向和约束符号要按你自己的模型推导来调整。
%% 子问题SP:固定x,找最恶劣u lambda = sdpvar(1, size(A_dual,1), 'full'); % 对偶变量 u = sdpvar(1, T, 'full'); % 不确定变量 % 双线性项 lambda_i * u_j 用大M线性化 M = 1e3; % 根据实际变量取值范围设置 z = binvar(size(lambda,2), T, 'full'); w = sdpvar(size(lambda,2), T, 'full'); for i = 1:size(lambda,2) for j = 1:T % w(i,j) 替代 lambda(i)*u(j) Constraints = [Constraints, 0 <= w(i,j) <= M*z(i,j)]; Constraints = [Constraints, lambda(i)-M*(1-z(i,j)) <= w(i,j)]; Constraints = [Constraints, w(i,j) <= lambda(i)+M*(1-z(i,j))]; Constraints = [Constraints, -M*(1-z(i,j)) <= w(i,j)-u(j); w(i,j)-u(j) <= M*(1-z(i,j))]; end end Objective_SP = -(lambda'*b0 + sum(sum(w .* c_dual)) ...); % 注意:最小化转对偶后,目标方向要调整,符号不要写反这里要特别提醒:子问题求出来的目标值,在最小化问题中对应上界 UB;主问题的目标值对应下界 LB。迭代条件是 gap = (UB-LB)/UB 小于某个阈值,我通常取 0.5% 或 1%。很多初学者把上下界方向搞反,导致明明收敛了却一直迭代,这个必须检查清楚。
4.5 C&CG迭代主循环
主循环不复杂,就是把 4.3 和 4.4 反复串起来:
tol = 0.005; % 收敛阈值 maxIter = 30; UB = inf; LB = -inf; u_set = ones(1, T); % 初始场景,可以用预测场景 for iter = 1:maxIter % 1. 把u_set加入主问题,求解当前MP optimize(Con_MP, Obj_MP, options); x_val = [value(x_w), value(x_pv), value(x_ess), value(x_de)]; LB = value(Obj_MP); % 2. 固定x_val,求解子问题SP [obj_sp, u_new] = solve_SP(x_val); UB = min(UB, obj_sp); % 上界取历史最小可行值 if (UB-LB)/UB < tol break; end % 3. 把u_new作为新场景加入主问题 u_set = [u_set, u_new]; end这里有个我踩过好几遍的坑:上界是"历史可行解中的最优点",所以要取 min;下界是"当前近似问题的最优下界",理论上随迭代单调不减。如果你看到 LB 比 UB 还大,先别急着怀疑算法,检查是不是主问题忘记加某条关键约束,或者子问题对偶方向写反了。C&CG 的收敛曲线应该是上界快速下降、下界缓慢上升,最终贴合在一起。
5. 跑通之后:结果怎么读、参数怎么调
5.1 收敛判据与迭代曲线
我自己的经验是,容量配置这类问题用 C&CG 通常 5~15 轮就能收敛到 1% 以内。如果超过 30 轮还不收敛,问题多半不在算法,而在模型本身。常见的病根有三个:
- 子问题对偶少写了约束,导致上界虚高;
- 不确定性集合参数 Γ 设得过大,导致最坏场景重复跳跃;
- 大 M 系数设置不当,导致子问题数值不稳定、场景来回反复。
调参之前先画出 UB 和 LB 随迭代次数的曲线,一眼就能分辨是哪种病:UB 迟迟不下来,通常是子问题建模问题;LB 和 UB 各自在跳动,一般是数值问题;UB 快速下降但 LB 纹丝不动,往往是主问题约束不足。
5.2 不确定性水平与容量配置结果的对应关系
我跑一个 24 时段的小算例时,把 Γ 从 0 调到 6、12、24,得到一组很有代表性的结果趋势(以下是演示数据,不是真实工程数值):
| 不确定预算 Γ | 风机(台) | 光伏(组) | 储能(等级) | 柴油机(台) | 年综合成本(万元) |
|---|---|---|---|---|---|
| 0(确定性) | 3 | 12 | 2 | 1 | 约 385 |
| 6 | 3 | 11 | 2 | 1 | 约 412 |
| 12 | 2 | 9 | 3 | 2 | 约 458 |
| 24(最保守) | 2 | 8 | 4 | 2 | 约 527 |
Γ=0 时光伏装得多、储能配得少,因为确定性模型觉得白天光照稳稳的;Γ=24(每个时段都允许极端偏差)时,储能容量几乎翻倍、柴油机也加了一台,同时光伏反而减少——因为光照不可靠时光伏的投资收益比下降了。
这个趋势提醒我们:鲁棒性不是"白送"的,它表现为投资结构的变化,而不仅仅是总成本上升。做项目汇报的时候,我最怕别人只给一套数字,所以现在习惯性地把"不确定预算-总成本-各类电源容量"放在同一张表里,让决策者能看到"每多一分鲁棒性,要花多少钱换"。
5.3 求解时间与规模控制
容量配置问题的规模很容易失控:T 取 24 还好,一旦做全年 8760 小时,第二阶段变量数直接翻 365 倍,求解时间立刻变得不可接受。工程上常用"典型日聚类"把全年压缩成 4~8 个典型日并分配权重;学术上也有用两阶段+情景缩减的做法。
还有几个能明显提速的经验:
- 主问题里的整数变量很多时,可以先用松弛解给 Cplex 提供热启动;
- 开启 Cplex/Gurobi 的 MIP emphasis 和流覆盖预处理选项;
- 别在最开始就追求 8760 全时段全场景,先用 24 时段把模型跑通比什么都重要,跑通后再延展到全年。
5.4 鲁棒性代价怎么评估
最后说说怎么向别人解释"为什么贵了"。把鲁棒方案的总成本减去确定性方案的总成本,这部分差额叫鲁棒性代价,本质是为应对不确定性支付的保险费。
评估值不值得,可以拿历史实测数据回代:把两套方案分别放进真实的历史场景里跑模拟,统计年成本、切负荷量、储能过放次数。如果确定性方案在极端年份切负荷严重,鲁棒方案就值得;如果历史数据里根本没有极端情况,那确实可以不花这个钱。
这个"回代验证"步骤看起来多花三五天,但在项目验收时价值很大,它能直接回答"你这套鲁棒方案到底比拍脑袋方案强在哪"。
6. 实操中容易踩的坑与个人经验
6.1 可行性割与最优性割必须分清楚
C&CG 迭代过程中,子问题可能出现对某个容量配置 x 根本无可行调度的情况,比如储能容量配得太小,在最坏场景下无法同时满足负荷和 SOC 下限。这时候子问题返回的不应该是最优性割,而是一条可行性割,需要额外求解一个带松弛变量的辅助问题,找到让约束条件被破坏到最小的场景,把这条场景也加入主问题。
很多公开代码不处理可行性问题,遇到不可行直接报错或返回 inf,结果迭代流程照样跑,出来的配置却是错的。我的做法是在子问题里预设几个松弛变量(切负荷、SOC 越限),求解后先看它们是否为 0。如果最优解里任意松弛变量大于阈值,就加可行性割,而不是最优性割。
6.2 整数变量和不确定变量的耦合处理
如果第一阶段容量变量是整数台数,第二阶段出力上限会写成x_w × P_W_max这类耦合形式。这本身没问题,但有些同学会在子问题里把 x 当成连续变量处理,偷偷放松整数性,结果回代出来的"最坏场景"在真实整数容量下并不存在。
我吃过这个亏,后来规定:子问题里 x 完全固定为当前整数解,取值用value(x)直接代入,而不是写成约束再让求解器去调。另一个相关坑是:可行性割返回的新场景可能让主问题又生成一个完全不同的整数解,导致场景集合来回震荡。这时可以在割里加上 x 的邻域限制,或者在目标里加一个小的正则项,让迭代曲线平滑下来。
6.3 大M参数的取值是数值稳定性的命门
对偶+大M线性化是目前工程落地的主流方案,但它对大M极其敏感。M 太小会砍掉可行域,M 太大会让单纯形表的数值条件恶化,在 Cplex 日志里通常表现为"Numerical difficulties"警告,或者场景割来回震荡。
我建议分三步处理:
- 对每个双线性项,根据 λ 和 u 各自的取值范围,分别算出局部大M——不要全局共用一个 1e6;
- 给 Cplex 开启数值强调参数(numerical emphasis);
- 用小规模随机数据验证子问题目标值,和用穷举台风场景算出的目标值做对比,确认一致后再上完整模型。
第三步很土,但确实能救命。我之前有一次迭代 100 轮不收敛,最后发现就是某个双线性项的 M 设小了,砍掉了真正最坏的场景。
6.4 我现在的固定调试顺序
被这些坑教育过之后,我形成了一套固定的开工顺序,现在做类似项目都是这么推的:
- 把 Γ 设为 0,整个模型退化成确定性规划,先确保主问题单独能跑出合理结果;
- 手动固定一个容量方案,单独调试子问题,观察"给定最坏场景时调度成本是否合理",以及"找出的场景是不是你直觉中的恶劣组合";
- 再跑完整 C&CG,重点看 UB/LB 曲线是否平滑收敛;
- 换不同 Γ 和不同预测曲线,做参数敏感性测试。
这套流程看起来慢,实际比直接写完整代码然后反复 debug 快得多。如果你现在正在做类似的项目,我建议至少留出两三天给前两步,后面会省下好几周。两阶段鲁棒优化的容量配置方案不是跑完代码就结束的,它需要你反复回到"最坏场景是什么样子"这个直觉问题上,模型才真正可信。