搞微网多电源容量配置,最常被问的一句话是:你这套容量,面对实际运行里各种“不按剧本”的光照、风速和负荷,到底扛不扛得住?我最近把两阶段鲁棒优化算法完整落到了Matlab代码里,从建立两阶段模型、设计不确定集、写列与约束生成算法,到最后出容量配置结果,整个过程踩了不少坑。这篇就把背后的建模思路、求解机制和可复现的代码组织方法一起写出来。
准确说,这篇适合三类人。一是正在做微网规划、分布式电源和储能容量配比的研究者;二是需要在并网方案里回答“最坏情况也能扛”的工程人员;三是被两阶段鲁棒优化理论绕晕,想找一篇能照着调Matlab代码资料的人。我不打算把问题拖到纯数学层面,而是按做项目的顺序讲:先说为什么要两层,再说怎么建模型,然后讲C&CG求解,最后给算例结果和调试经验。
1. 容量配置为什么天然是“两层决策”
1.1 投资搭台,运行唱戏
微网多电源容量配置表面上是一道“买多大设备”的问题:光伏装多少千瓦,风电装多少,储能电池配多少功率和多少电量,柴油机要不要留。但在容量确定之后,设备怎么用才是关键。光伏、风电出力只跟天气走,储能何时充电何时放电要跟着负荷走,柴油机在什么电价和缺电条件下启动,外网购电和售电怎么分配,这些都是运行层面的问题。所以规划问题的完整形式,必定是投资决策带着运行优化一起做的。
用大白话理解:第一层是“搭台子”的人,先把舞台大小定了;第二层是“唱戏”的人,每天根据实际上台的“演员”安排节目。舞台搭小了,厉害的节目排不下;舞台搭大了,空着也是一种浪费。两阶段鲁棒优化就是让这两层决策按照“先拍板、后应对”的顺序放在同一个目标函数里权衡,而不是像传统确定性方法那样,把运行过程简化成一条平均曲线。
1.2 确定性模型低估了什么
很多初始方案会先做确定性优化:把光伏、风电出力曲线和负荷曲线取一个典型日或者全年平均,然后在投资约束下最小化投资加运行成本,一次性求解得到容量配置。这个做法不是没用,但会系统性低估风险。原因在于,容量配置的价值往往不是在“平均场景”里体现的,而是在“不利场景”里体现的。
比如储能容量如果只按平均日照和平均负荷来配,遇到连续阴雨天叠加晚高峰,可能连续几个小时都切负荷。这时候再想补容量已经晚了,因为设备容量是第一阶段拍板的。这个场景听着极端,但规划恰恰要回答这种极端问题。确定性优化把不确定性参数固定成一个期望值,把所有“意外”当作不会发生,结果自然是投资偏低、运行成本在真实环境中偏高。更好的思路是把不确定性描述成一个范围,然后让优化模型自己去找这个范围内对方案最不利的场景,并围绕这个场景来配置设备。
1.3 两阶段的结构刚好对上“容量-运行”耦合
把两阶段决策和鲁棒优化放在一起,逻辑就顺了。第一阶段变量是Here-and-Now决策,需要在参数实现之前就确定下来,典型的是各类电源和储能的配置容量;第二阶段变量是Wait-and-See决策,可以在看到实际光伏、风电、负荷之后再做,典型的是各时段的机组出力和储能充放电功率。
目标函数写成先最小化投资成本和“最坏运行成本”之和,数学上就是:
min_x c^T x + max_u min_{y in F(x,u)} d^T y
x 代表容量设备,u 代表不确定参数,y 代表运行变量,F(x,u) 是给定容量和参数后的运行可行域。这个式子读出来是:先决定容量,然后在所有可能的不确定场景里找出让运行成本最高的那个场景,再按最低运行成本去应对它,两者的总和一起被优化。读到这里你会发现,这本质上是一个三层嵌套优化,直接解几乎不可能。后面的列与约束生成算法,核心工作就是把它拆成主问题和子问题,像剥洋葱一样一层层剥开。
2. 两阶段鲁棒模型:先定不确定集,再谈优化
2.1 不确定参数从哪里来
微网里的不确定性至少有三类。第一类是新能源出力,光伏受云层、辐照影响,风电受风速随机性影响;第二类是负荷,居民和工商业负荷在一天内有很强的波动,预测误差不可忽略;第三类是市场电价,如果允许与主网双向购售电,电价波动也会进入运行模型。
工程上最常用的做法,是先把这三种参数分别给出预测基准值,再在基准值上下定义一个波动范围,代表预测误差和随机波动。这里需要明确一个概念:两阶段鲁棒优化并不试图预测最坏场景出现在哪个具体时刻,而是把“可能出现的偏离范围”交给模型。这样得到的容量配置,对任何落在不确定集内的场景都有一定的适应能力,而不是只对某一条预测曲线所有。
2.2 三种常见不确定集对比
不确定集的形式决定了问题的难度和方案保守度。我把三种常用形式放在一起对比:
| 不确定集类型 | 形式 | 优点 | 缺点 |
|---|---|---|---|
| 盒式不确定集 | 每个参数独立在上下界内任意取值 | 建模简单,约束直观 | 全取边界的极端场景几乎不可能发生,结果偏保守 |
| 预算式不确定集 | 每个参数可以在上下界内偏离,但总偏离量受预算限制 | 能控制保守度,反映“坏情况不会同时全来” | 需要调预算参数 |
| 椭球式不确定集 | 参数偏离量被控制在椭球范围内 | 可保留统计意义,保守度连续可调 | 模型变成二阶锥问题,求解难度明显上升 |
我在算例里用的是预算式不确定集,原因很实际:微网的负荷和新能源出力扰动来源相互独立,但不可能百分之百同时偏离到边界。预算一定程度上控制了“天时不利”的强度,又保持模型是线性规划,可以直接用成熟求解器,跑起来快,调参也方便。
2.3 预算式不确定集的数学表达
预算式不确定集的标准写法是:对于每个时段 t,不确定参数 u_t 在基准值上下一定比例内波动,例如 u_t = u_ref,t * (1 + deviation_t * z_t),z_t 在 [-1,1] 之间。同时整个调度周期内所有时段的 |z_t| 之和不超过预算 Gamma。
Gamma 越小,越接近确定性预测;Gamma 越大,越接近盒式,也就是每个时段都允许取最不利边界。实际设置时,Gamma 的物理含义可以理解为:整个调度周期内,最多允许多少个时段的不利偏差同时达到最大。比如一天24个时段,Gamma 取6,意思是最多6个时段可以同时处在最坏偏离状态,其余时段要么偏离不大,要么方向有利。
这个参数在Matlab里通常就是一维向量或者一个常数,求解时不需要特殊处理,它只是把 z_t 的L1约束加入不确定变量的可行域。调Gamma比换不确定集类型方便得多,所以我的项目默认都是从预算式开始试。
2.4 两阶段模型的标准结构
两阶段模型分三层写。第一层是投资层目标函数,包含单位投资成本和设备容量变量;第二层是运行层目标函数,包含燃料成本、购电成本、运维成本和切负荷惩罚;第三层是各层约束。
投资约束通常只有设备容量上限和非负约束,负责把可行域框在一个实际范围内。运行约束则要写全:功率平衡、储能SOC递推、储能充放电功率上下限、柴油机出力限幅、外网交互功率限幅、备用约束、以及每个时段的功率非负约束。运行层的所有约束都需要包含不确定参数,并且这些约束都会被引入到最坏场景的子问题中。这就是这个模型和普通确定性规划最大的差异:约束不是针对某一条曲线,而是针对一个集合。
3. C&CG算法主流程与Matlab代码骨架
3.1 为什么C&CG而不是Benders
两阶段鲁棒规划最经典的做法是Benders分解和列与约束生成。Benders的思路是把子问题的最优性信息用割平面反馈到主问题,每次迭代只添加一条割平面;C&CG则更进一步,把最恶劣场景下整个运行变量集合都加进主问题,形成完整的场景约束,而不是只传达一条最优性信息。这样主问题对原问题的逼近速度更快,需要的迭代次数也更少,在处理包含大量运行约束的微网模型时效果明显更好。
我最早是用Benders试的,二十几轮迭代还没收敛,换成C&CG之后七八轮就基本收敛。而且主问题的规模是可控增加的,不会像Benders那样始终在“信息不足”和“信息碎片化”之间摇摆。当然C&CG不是没有代价:每迭代一次,主问题就会多一组完整的运行变量和约束,模型规模按场景数递增。实际算例中这种代价很小,完全值得。
3.2 主问题怎么建立
给定当前已生成的一系列最恶劣场景集合,主问题中第一阶段容量变量 x 仍然待优化,同时引入一个标量 theta 来近似第二阶段的最坏运行成本。针对每个已发现的场景 k,需要添加一组完整的运行变量 y_k 和运行约束。
主问题的写法分三部分。第一部分是 x 和 theta 共同参与目标函数最小化;第二部分是对每个场景 k,运行变量 y_k 必须满足给定 x 和场景参数 u_k 下的功率平衡、储能SOC等约束;第三部分是目标函数额外添加 theta >= d^T y_k,让 theta 至少不低于每一个已发现场景的运行成本。
这套写法在Matlab里非常好实施,因为本质上就是按场景索引把同一组约束复制多次。最容易出错的地方是索引对应关系,后面我会专门讲。
3.3 子问题与最恶劣场景识别
子问题是在第一阶段方案 x 给定的情况下,求解 max_u min_y d^T y,也就是找出让运行成本最大的不确定场景,并给出对应的最低运行成本。这是一个最大值套最小值的双层优化,不能直接交给线性规划求解器。
常规处理方法是把内层 min_y 写成一个线性规划,然后取它的对偶问题。由于内层变量 y 通常是连续变量,线性规划强对偶成立,min_y 的目标函数值等于其对偶目标的最大值。于是内层的最小化就变成了对偶变量的最大化,与外层对不确定变量 u 的最大化合并,最终形成一个只包含 u 和对偶变量的单层最大化问题。这个单层问题仍然可以被线性规划或混合整数线性规划求解器处理。
求解完这个单层问题后,能拿到两样东西:一是当前 x 下的最恶劣场景 u*,二是这个场景下的运行成本,也就是给当前方案兜底的最坏运行成本。u* 会被作为新场景加入主问题,运行成本则用来更新上界。
3.4 迭代流程、上下界更新与终止条件
C&CG的迭代框架并不复杂,具体步骤如下:
- 初始化:给出一个可行容量方案 x0,设置上界为正无穷、下界为负无穷;
- 将初始场景,通常是预测名义场景,加入主问题,求解主问题,得到 x* 和 theta*,下界更新为当前主问题目标值;
- 固定 x*,求解子问题,得到最恶劣场景 u* 和运行成本,上界更新为当前主问题中的投资成本加子问题运行成本;
- 如果上界减下界小于设定的误差阈值,停止;否则,把 u* 对应的完整运行变量和约束加入主问题,回到第2步。
终止条件的设置平时很关键。工程上我会用相对误差而不是绝对误差,因为成本和容量量级可能上万,绝对误差取1往往不现实。Matlab实现时,可以在每次迭代后打印当前上下界,观察差值变化趋势。如果在十几轮迭代中持续震荡,先不要纠结阈值,应该回去检查子问题有没有漏约束。
3.5 Matlab代码骨架
我用结构化方式组织代码,主体是主函数、主问题模型、子问题模型三个部分。核心骨架大致如下:
%% 参数读入与初始化 load('case_data.mat'); % 设备参数、基准曲线、不确定集参数 x0 = zeros(n_equip,1) + init_cap; % 初始容量 nScen = 0; LB = -1e10; UB = 1e10; eps_gap = 1e-3; while (UB - LB)/abs(UB) > eps_gap % 1. 构建并求解主问题MP [x_now, theta_now, MP_obj] = solve_MP(x0, ScenSet); LB = MP_obj; % 主问题目标值 % 2. 固定x_now,求解子问题SP,得到最恶劣场景u_star和运行成本 [u_star, run_cost, SP_status] = solve_SP(x_now); if SP_status == 1 UB = inv_cost(x_now) + run_cost; end % 3. 将u_star添加到场景集合,供下一轮主问题使用 ScenSet = [ScenSet, u_star]; nScen = nScen + 1; end每个函数内部,我都按“先定义变量集,再写约束,最后设目标”的顺序组织。这个顺序能省去很多调试时候“这行约束到底加到哪去了”的困惑。真正落到项目里,主问题函数里会循环所有场景,把每个场景的运行约束追加进去,子问题函数里则要把内层对偶后的变量和约束写完整。
4. 算例:某园区微网多电源配置全过程
4.1 算例参数
我用一个虚构的园区微网算例跑通了这套代码。设备包括光伏、风电、储能电池和柴油发电机,负荷曲线取24小时的预测基准值,时间分辨率1小时。主要参数如下:
| 设备类型 | 参数 | 数值 |
|---|---|---|
| 光伏 | 单位投资 | 4500元/kW |
| 光伏 | 运维成本 | 0.02元/kWh |
| 风电 | 单位投资 | 6800元/kW |
| 风电 | 运维成本 | 0.03元/kWh |
| 储能 | 单位功率投资 | 1200元/kW |
| 储能 | 单位容量投资 | 1800元/kWh |
| 储能 | 充电/放电效率 | 0.95/0.95 |
| 储能 | SOC上/下限 | 0.9/0.1 |
| 柴油机 | 单位投资 | 2000元/kW |
| 柴油机 | 燃料成本 | 0.68元/kWh |
| 外网 | 购电上限 | 2000kW |
| 外网 | 售电上限 | 500kW |
| 外网 | 购电价/售电价 | 0.85/0.45元/kWh |
| 负荷 | 切负荷惩罚 | 10元/kWh |
容量配置的决策变量包括光伏容量、风电容量、储能功率和储能容量、柴油机容量。投资成本按日折算:把设备寿命和年利率折算到典型日成本,这样投资成本和运行成本可以放在同一个目标函数里。折算系数虽然不影响算法结构,但直接影响两类成本的比例,项目上要按实际的设备寿命和资金成本算,不能随便拍脑袋。
4.2 不确定集参数设置
光伏和风电的预测基准值是我给定的一条曲线,实际使用的偏离范围取了基准值的正负20%;负荷偏差取了正负10%。预算Gamma取不同的值分别做敏感性测试,从Gamma=2到Gamma=12,默认Gamma=6。
z_t 允许在-1到1之间取连续值。因为变量连续,最恶劣场景不一定出现在所有时段同时取边界,而是一个在预算限制下的“比较坏又没那么坏”的组合。有一点容易被忽略:光伏、风电、负荷的不确定变量应该分开建模,因为它们有不同的预测误差特性和物理含义。把它们混在一个系数矩阵里,虽然在代码上省事,但会导致预算无法正确分配,算出的最恶劣场景常常失真。
4.3 运行约束的矩阵化拼装
Matlab里跑优化,最大的工作量不是写逻辑,而是拼矩阵。我会把运行约束统一写成 A_g * y + A_u * u + A_x * x <= b 这种形式,其中 y 是第二阶段所有运行变量,u 是各时段不确定变量,x 是第一阶段容量变量。这样主问题和子问题都能直接复用同一套系数矩阵,只是变量自由度不同。
储能SOC递推约束必须按时段顺序连续堆叠,否则求解器会解出一个“能量凭空转移”的方案。功率平衡约束里,光伏、风电、柴油机出力、储能放电、购电作为电源项,负荷、储能充电、售电作为负荷项,两边要严格对齐。每条约束我都在代码里留了注释,标明它属于哪个时段、哪个设备,这在实际调试时太重要了。
这个算例最折磨人的地方不是模型逻辑,而是检查每个时段的功率平衡是否真的成立。我会额外写一段校验脚本,把一个已知场景代进去,验证每个时段电源项之和与负荷项之和的差值是不是小于一个极小值。校验脚本通过后再交给求解器,能省掉大量后续排错时间。
4.4 求解与结果
在这个算例参数基础上,确定性优化配出的结果是:光伏容量偏大,储能偏小,柴油机几乎没有。看着总成本很低,但把最恶劣场景硬塞进去做运行模拟,切负荷量非常可观。两阶段鲁棒优化的结果则是另一套容量:储能功率和能量明显上调,柴油机也保留了一部分,光伏风电的装机相对调整幅度小一些。
求解时间方面,默认Gamma=6情况下,C&CG迭代约8轮收敛,每轮主问题规模增长一组运行变量,总求解时间几十秒,完全在可接受范围内。这说明预算式不确定集加线性规划的处理方式,在微网规划尺度上计算效率是够用的。如果换成更细的时间分辨率或加入0-1变量,求解时间可能会上升一到两个数量级,那时才需要考虑场景压缩或子问题加速。
5. 结果解读与鲁棒性验证
5.1 鲁棒方案与确定性方案的差别
下面给出确定性方案和鲁棒方案的容量配置对比,数据来自我跑的虚构算例:
| 指标 | 确定性方案 | 鲁棒方案 |
|---|---|---|
| 光伏容量 | 3200kW | 3000kW |
| 风电容量 | 1500kW | 1500kW |
| 储能功率 | 800kW | 1500kW |
| 储能容量 | 1600kWh | 3500kWh |
| 柴油机容量 | 200kW | 1000kW |
| 投资成本 | 10.2万元/日 | 12.8万元/日 |
| 最坏场景运行成本 | 8.5万元/日 | 2.1万元/日 |
| 最坏场景切负荷量 | 4200kWh | 0kWh |
鲁棒方案比确定性方案多花了两成多的投资,但换来的是最坏场景下运行成本的大幅下降,而且完全消除了切负荷。这正好印证了前面的判断:容量配置的多余度不是浪费,而是对抗不确定性的资产。运行成本的下降额完全覆盖投资成本的上升额,说明鲁棒配置在综合成本上是占优的。
5.2 预算Gamma如何左右配置
Gamma是两阶段鲁棒模型里最值得调的旋钮。我把Gamma从2调到12做了一组测试,结果明显分成三个区间。Gamma较小时,配置接近确定性方案;Gamma中等时,储能和柴油机容量开始上升;Gamma很大时,变化趋缓,因为模型已经接近盒式不确定集,再增加预算只会让约束变松,不会继续改变最恶劣场景。
实际操作中,我建议项目上不要只跑一个Gamma,而是画一条“总成本-Gamma”曲线。曲线上拐点通常是方案比较经济的位置,因为再提高Gamma,容量配置增加不大,但投资不再划算。这里面没有统一的最优Gamma,要结合项目对可靠性的要求来选。如果项目对停电非常敏感,宁可让Gamma偏大一些;如果对投资成本敏感,可以选在拐点左侧。
5.3 怎么把代码改到自己的项目
如果有人想把这套代码接到自己的微网项目里,需要修改的地方主要集中在参数区、不确定集构造和运行约束三个位置。设备参数按实际报价和运行数据替换;不确定集偏差比例按历史预测误差统计设置;运行约束则要补充自己的设备特性,比如储能循环寿命约束、柴油机启停时间约束。整体框架不用动,C&CG主流程对所有线性或混合整数线性两阶段模型都适用。
另外要注意,如果引入了柴油机启动变量这类0-1变量,第二阶段就从LP变成了MILP,子问题的对偶变换方式需要换成强对偶或KKT系统,求解复杂度会明显上升。无特殊需要时,我建议先把设备建模成连续变量,确认整体流程没问题,再逐步加整数变量,否则排查问题会很痛苦。
6. 我踩过的坑和调试建议
6.1 维度不匹配是Matlab优化最常见的雷
两阶段鲁棒模型里到处都是按场景、按时段、按设备索引的变量,维度不匹配几乎无法避免。我遇到过一种情况:某个约束里引用了时段索引,但矩阵行数只按设备维度构造,求解器直接报维度不一致。排查办法其实不复杂,第一次写代码时就在每个约束矩阵拼完后打印size,给每个约束块命名,比如Cons_soc_bal、Cons_power_bal,一旦报错就能定位到具体哪块。
另一个常见问题是主问题中新增场景时,新场景的运行变量和旧场景的运行变量混在一起,导致约束只描述了其中一个场景。为了避免这种问题,我会给每个场景的变量加独立前缀,例如y_k1、y_k2,这样在构建主问题时能确保“一个场景一套约束”,不会互相覆盖。
6.2 大M取值要卡住设备物理上限
储能在同一时段通常不允许同时充电和放电,但很多模型不会显式加这个约束,只是靠目标函数自然避免。如果必须显式约束,就绕不开大M法。大M取太小会限制可行域,比如电池满电时强制不能充电;取太大会导致数值病态,求解器容易找错方向。我一般把M设为对应功率变量的物理上限乘1.2,再在求解后检查解里有没有违反原来物理意义的情况。
更稳妥的做法是尽量不加这种互斥约束,而是让充电功率和放电功率使用同一组上限约束,配合SOC递推,大多数场景下解会自动符合物理规律。这样省掉一批大M,也少了一类数值坑。
6.3 子问题无界或不可行,先检查SOC和惩罚项
子问题求解不收敛或报不可行时,我通常优先查两处。第一处是SOC的上下界和初始电量有没有给全,SOC约束缺失会造成储能可以无限放电,子问题目标无界。第二处是切负荷惩罚项有没有作为松弛变量加进功率平衡约束,没有它,模型在最坏场景下可能没有任何可行运行方案。
给切负荷松弛一个高惩罚单价,等于告诉求解器“切负荷可以做,但代价很高”。这个设置不能省略,否则模型会为了可行性而偷偷放宽某些物理约束,最恶劣场景的结果就不可信。我习惯把切负荷惩罚设成正常运行成本的十倍以上,确保求解器只在万不得已时才选择切负荷。
6.4 相对误差收敛和迭代震荡
C&CG理论上有限迭代收敛,实际代码却可能在一轮轮迭代中反复震荡。我的经验是把终止条件设为相对误差,并且在下界更新时用主问题的完整目标值,不要只用投资成本部分。如果震荡严重,先调小Gamma试试,Gamma大的时候最恶劣场景变化更剧烈,主问题加入新约束后产生较大波动是正常现象。
迭代初期也不需要过于追求精确,前几轮主问题求解出的x变化很大,这时没必要反复把子问题精度调得很高,浪费计算时间。一般迭代到第5轮后,x的变动会明显放缓,再提高求解精度比较划算。小算例可能无所谓,但规模一上去,初期精度设置能差出好几倍求解时间。
最后再分享一个处理这类项目的体会:两阶段鲁棒优化并不是越保守越好,它真正的价值在于让你用一套可调试、可复现的流程,把“最坏情况”从模糊的担心变成明确的场景和明确的成本。把Gamma旋钮拿在手里,把C&CG循环跑通,你才能在容量配置上做到既不盲目堆设备,又对不确定性有底。实际动手时,先从48时段甚至24时段的简化算例开始,确认主问题和子问题闭环了,再往完整模型上扩展,这个顺序能帮你避开绝大多数看不懂的报错。