ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

两阶段鲁棒优化微网容量配置:CCG算法与Matlab实战

两阶段鲁棒优化微网容量配置:CCG算法与Matlab实战

从微网容量配置的实际痛点切入,两阶段鲁棒优化是处理风光不确定性的关键手段。本文以一个実際に运行的Matlab代码项目为例,拆解从数学建模到代码落地的完整链路,重点讲清楚min-max-min结构怎么理解、C&CG算法怎么迭代、以及我在调试过程中踩过的那些坑。

1. 项目在解决什么实际问题

1.1 微网容量配置的决策困境

微网里的光伏、风电、储能、柴油发电机,每种电源都有完全不同的成本结构和运行特性。光伏和风电的度电成本低,但出力不可控,阴天没光、无风天没风,出力可能直接掉到零;储能能削峰填谷,但单位容量的购置成本高,而且充放电循环有寿命损耗;柴油发电机响应快、出力稳定,但燃料成本和碳排放都不好看。

容量配置要做的事情,就是在这些特性互相矛盾的电源之间,找到一个最优的组合方案:既要满足全年任意时段的负荷需求,又要把总成本压到最低。这里的成本不仅包括设备购置的初始投资,还要算上整个寿命周期内的运行成本、维护成本、燃料成本,甚至要折算设备的置换成本。

这个问题难在哪?难在决策变量有两种完全不同的时间尺度。容量配置是“建多少个风机、装多少块光伏板、配多大容量储能”这种一锤子买卖的决策,属于长期规划;而每个小时怎么调度这些电源、储能是充电还是放电、柴油机开几台,属于短期运行决策。规划决策确定了系统的天花板,运行决策决定实际表现,两者耦合在一起,形成一个典型的双层优化问题。

1.2 不确定性才是真正的挑战

如果光伏出力、风电出力、负荷大小都是确定性的数值,这个容量配置问题用常规的混合整数线性规划就能解。但现实世界里,这些量全是随机变量——明天的光照强度取决于云层厚度,一周后的风速谁也说不准,负荷曲线更是随季节、天气、人们的生产生活方式剧烈波动。

传统的确定性优化方案会在这种不确定性面前暴露出脆弱性:按夏季典型日照设计的系统,到了连续阴雨的一月份可能频繁切负荷;按平均风速配置的风机台数,遇到小风年会直接导致供电不足。反过来,如果为了保险起见把所有不确定量都取最恶劣值,配置出来的系统成本会高到无法接受——就好比为了应付百年一遇的洪水,把房子的地基修到大坝的标准。

面对这种矛盾,业界实际上有随机规划、鲁棒优化、区间优化等多种路线。随机规划需要知道不确定量的精确概率分布,但在工程实践中很难拿到可靠的分布数据;传统鲁棒优化虽然不需要分布信息,但要求所有不确定量同时取最坏情况,保守性过强。两阶段鲁棒优化恰恰在两者之间找到了平衡点:第一阶段做规划决策时不依赖不确定量的实现值,第二阶段在不确定量显露后再做最经济的调度调整,用min-max-min结构同时兼顾了鲁棒性和经济性。

2. 两阶段鲁棒优化的数学框架

2.1 min-max-min结构的直观理解

这个项目里用的两阶段鲁棒优化,核心是一个三层嵌套的数学结构:min-max-min。乍一看很唬人,但拆开来看并不难。

最外层的min对应第一阶段的容量规划决策,也就是“我现在要买多少设备”。这一层决策的特点是“现在定了就不能反悔”,专业术语叫here-and-now决策。中间那层max对应不确定性“跟我对着干”——真实的风速、光照、负荷会朝着让系统运行成本最高的方向去实现。最内层的min对应第二阶段的运行调度决策,也就是“不管天气怎么变,我都要在给定设备条件下找出最省钱的调度方案”,这层决策的特点是“可以等不确定量实现后再做”,术语叫wait-and-see决策。

三层结构的意思翻译成大白话就是:我要在设备投资和后期运行之间找一个平衡点,使得——即使自然界以最不利的方式呈现风速、光照和负荷,我依然能通过最优调度把损失控制在最小。这是一个“先定容量,再博弈运行”的思路,数学上严整,工程上也有明确的现实对应。

2.2 C&CG算法怎么把这个问题解出来

min-max-min三层嵌套问题没法直接扔给求解器硬解,工程上一般用Benders分解或者列约束生成算法(C&CG)来处理。这个项目采用的是C&CG,它比经典Benders分解收敛速度更快,尤其在两阶段鲁棒问题里表现稳定。

C&CG的思路很朴素:把原问题分解成主问题和子问题,通过迭代逼近最优解。

主问题是一个混合整数线性规划,包含第一阶段的容量决策变量和第二阶段的运行决策变量,只不过第二阶段变量对应的约束是随着迭代逐渐增加的。初始时主问题只包含一部分约束,求解得到一个容量配置方案,作为当前最优解的上界。

子问题是在固定主问题给出的容量方案后,求解内层max-min结构的优化问题。因为内层是max和min嵌套,需要先用对偶变换把内层min变成max,把max-min变成max-max,合并成一个单层的max问题,这样就能直接求解。子问题的目标函数值给了原问题最优解的下界。

每一次迭代,把子问题找到的最恶劣不确定场景提取出来,生成一组新的第二阶段变量和对应的约束,加入主问题重新求解。这样主问题的约束越来越多,上界和下界不断逼近,当两者的间隙小于设定阈值时,迭代收敛,得到的就是满足鲁棒性要求的最优容量配置方案。

这里有个细节值得注意:C&CG可以在主问题中加入第二阶段变量的全部约束,而不只是加了最优割,这使得每次迭代获得的下界质量远好于经典Benders分解,实际迭代次数通常只有个位数到十几轮。

2.3 不确定集合怎么构造

不确定集合决定了鲁棒优化模型的保守程度,也直接决定计算复杂度。这个项目用的是盒式不确定集合加预算约束的构造方式,这也是目前工程应用最广的选择。

对每个不确定参数,比如风速、光照强度、负荷功率,给定一个预测基准值和上下波动范围,用区间刻画可能出现的范围。但所有不确定参数同时取极端值的情况在现实中几乎不可能发生,所以还需要引入一个预算约束,限制所有不确定参数偏离基准值的总量。

这里面的关键参数是预算值Γ。Γ=0时不确定集合退化为基准场景,模型退化成确定性优化,保守性最低;Γ取到最大时集合退化为完整盒式,所有参数都允许取极端值,保守性最高。实际工程中通常取Γ为某个中间值,比如总时段数的某个百分比,来平衡鲁棒性和经济性。

我在实测中试过不同预算值对结果的影响,发现一个比较明显的分水岭:预算值从0增加到一定程度时,最优容量配置中的光伏和风电装机会明显增加,储能容量也跟着上升,因为系统需要用更多的可控手段来对冲更恶劣的不确定场景;而达到某个阈值后,继续增大预算值对配置结果的影响就比较有限了,因为此时约束已经足够保守,新增的极端场景不再是制约配置方案的瓶颈。

3. 微网多电源容量配置的数学建模

3.1 决策变量与目标函数的确定

这个项目考虑的是典型孤岛微网,包含四类电源:光伏、风电、储能、柴油发电机。为了简化建模,把一年按典型日进行聚类处理,每个典型日分为24个时段,用几个典型场景代表全年运行特性。

第一阶段的决策变量是各电源的配置容量:光伏装机容量、风电装机容量、储能额定容量、储能额定功率、柴油发电机台数。这些变量都是离散或连续的整数/实数变量,直接决定初始投资成本。

第二阶段的决策变量是每个典型日在每个时段的运行变量:柴油发电机的出力、光伏实际出力、风电实际出力、储能充放电功率、储能荷电状态、切负荷量等。

目标函数是总成本最小化,包括两大部分:第一阶段的年化投资成本,把设备购置成本按寿命年限和折现率等额分摊到每年;第二阶段的年运行成本,包含柴油燃料成本、运维成本、切负荷惩罚成本。目标函数把这两部分统一折算成等年值,保证不同寿命的设备之间可以公平比较。

一个容易踩坑的地方是设备寿命的折算。光伏寿命通常25年,风电20年,储能可能只有10年,柴油发电机15年。如果简单地用“初始投资除以寿命年限”来年化,忽略了资金的时间价值,配置结果会偏向寿命长的设备。正确做法是用等额年值公式,把折现率考虑进去。我在项目中取了8%的折现率,这个值在电力系统经济分析里是常用假设。

3.2 约束条件的层次结构

模型的约束条件数量庞大,需要分层理清楚。

第一层是容量配置约束。光伏和风电的实际出力不能超过装机容量,储能充放电功率不能超过额定功率,储能荷电状态约束保证能量守恒。这些约束把第一阶段的投资决策和第二阶段的运行决策耦合在一起,是整个模型的核心纽带。

第二层是功率平衡约束。任意时段内,所有电源的出力加上储能放电、减去储能充电,必须等于负荷功率,如果不平衡允许切负荷,但要付出高额惩罚成本。这个约束在微网里是硬约束,直接对应现实中“电灯不能灭”的基本要求。

第三层是设备运行约束。柴油发电机有最小技术出力限制、爬坡速率限制,运行台数有上限;储能有充放电效率、荷电状态上下限、充放电功率上下限。这些约束反映了设备真实的物理特性,忽略任何一个都会导致配置方案在现实中不可行。

第四层是不确定变量约束。风速、光照、负荷在不确定集合范围内变化,对应的光伏出力、风电出力、切负荷量都会跟着调整。这一层直接体现鲁棒优化的特征,也是模型计算量的大头。

我实测的一个体会是,储能约束的建模方式对求解时间影响很大。如果把储能荷电状态在每个时段都写成等式约束,再套上鲁棒优化的对偶变换,整个模型的变量数量和约束数量会急剧膨胀。后来我把储能荷电状态的递推关系做了线性化处理,并且约束只在必要的时段强制满足上下限,求解时间缩短了将近一半。

3.3 不确定变量与运行模拟的衔接

风速到风电出力、光照到光伏出力,中间还要经过转换模型。项目里采用分段线性化的方式,把非线性功率转换曲线近似为分段线性函数,这样既能保证精度,又不会把模型变成非线性规划导致求解困难。

负荷不确定性相对简单,直接用预测负荷乘以一个波动系数。但这里有个容易忽略的细节:负荷的不确定性和风光出力的不确定性耦合在一起时,预算约束的分配需要小心处理。如果负荷、风速、光照分别设置独立的预算约束,理论上也可以,但会明显增加对偶变量的数量。我在建模时把三者合并到同一个预算约束里,用权重系数区分各参数的波动贡献,这样既控制了保守度又兼顾了计算效率。

光伏出力和风电出力在处理上还需要区分“最大可用出力”和“实际调度出力”。不确定场景决定的是光伏和风电最大能发多少电,但实际调度中出于安全或经济考虑,可能弃光弃风,实际出力小于最大可用出力。这两个变量在模型里必须分开定义,否则会把不确定性错误地传递到调度决策里。

4. Matlab代码实现与实操过程

4.1 代码整体结构与数据预处理

这个项目的代码用Matlab编写,建模部分用了Yalmip工具箱,求解调用商用求解器。整体结构分四层:数据预处理模块、主问题构建模块、子问题构建模块、C&CG迭代主程序。

数据预处理模块承担的工作包括:读取风速、光照、负荷的历史数据,按典型日聚类生成场景,计算各时段的不确定区间,初始化预算参数。这部分代码虽然逻辑简单,但耗时不少,尤其是典型日聚类环节。项目里用K-means聚类把全年8760小时压缩成几个典型日,聚类数目的选取需要做灵敏度分析——太少会丢失季节性特征,太多会导致计算量成倍增加,我在项目中取4个典型日,分别代表春夏秋冬。

主问题模块构建第一阶段的容量配置约束,以及已经迭代产生的第二阶段场景对应约束。每个新场景加入时,需要新增一整块连续变量的定义,包括该场景下所有时段的调度变量。随着迭代进行,主问题规模会逐渐膨胀,这是C&CG算法的正常现象。

子问题模块相对复杂,需要完成对偶变换。首先把内层min的约束写成拉格朗日形式,然后通过强对偶条件把min-max变成max-max,得到一个单层的maximization问题。这里最需要注意的是第二阶段的连续变量和0-1变量处理方式不同:如果第二阶段存在整数变量,对偶变换就会遇到困难,需要做特殊处理。我在建模时特意避免了第二阶段引入整数调度变量,采用连续变量近似柴油机的启停行为,这样才保证了对偶变换可以顺利进行。

4.2 主问题与子问题的核心代码骨架

主问题代码的框架大致如下:

% 主问题构建 MP = []; % 约束集合 obj_MP = 0; % 第一阶段变量 x_pv = binvar(1, 1); % 光伏配置决策 % 实际项目中通常用整数变量表示装机容量档位 % 或用连续变量并配合投资成本系数 % 第一阶段约束 MP = [MP, x_pv >= 0]; % ... 其他配置变量和约束 % 迭代过程中加入的新场景对应的第二阶段变量和约束 for k = 1:iter_count % 每个场景定义自己的第二阶段变量 P_dg{k} = sdpvar(n_time, 1); P_bat_ch{k} = sdpvar(n_time, 1); % ... 其他变量 % 加入功率平衡、设备运行约束 MP = [MP, ...]; end % 求解主问题 optimize(MP, obj_MP, sdpsettings('solver', 'gurobi'));

这里有几个实际编码的细节值得多说几句。一是Yalmip中变量定义要区分连续变量sdpvar和整数变量binvar/intvar,两阶段问题中第一阶段的整数变量会出现主问题里,这是C&CG能够处理的,因为主问题本身就是MILP。二是约束拼接时如果循环次数多,直接用MP = [MP, constraint]这种方式效率不高,工程上建议先预分配cell数组再拼接,对于大规模问题能减少不少时间。

子问题代码的核心是构造对偶问题,这一段的实现是整段代码中最容易出错的地方。实操中我建议不要手推完整对偶表达式,而是利用Yalmip的dual函数,直接从原始约束中提取对偶变量信息,减少推导错误的概率。当然,对偶理论本身还是需要理解清楚的,不然Yalmip报错时根本看不出问题在哪。

4.3 C&CG迭代流程的参数设置

C&CG迭代主程序的流程是这样的:

第一步,初始化。设迭代次数iter=0,下界LB=-inf,上界UB=inf,初始不确定场景设为基准场景。

第二步,求解主问题。得到容量配置方案和主问题目标值,由于主问题是松弛版本(只包含部分场景约束),主问题目标值提供原问题的下界,更新LB。

第三步,把主问题得到的容量配置代入子问题,求解子问题找到最恶劣场景。子问题目标值加上第一阶段的投资成本,得到原问题的一个可行上界,更新UB。

第四步,计算相对间隙gap = (UB-LB)/UB。如果gap小于设定阈值(工程上常取1%或0.5%),停止迭代,输出结果;否则提取子问题的最优解对应的不确定场景,生成新的约束加入主问题,iter加1,回到第二步。

我在具体实现时发现,收敛阈值取0.5%和1%对最终配置结果的影响很小,但求解时间差异明显。对于容量配置这类长期规划问题,1%的间隙完全够用,贪心取太小阈值只会白白增加计算负担。

预算参数的设置也需要单独说明。对于负荷波动,工程上常见取值是总时段数的5%-10%;对于风速和光照,由于预测误差通常更大,预算占比可以适当提高。我的经验是把三类不确定参数分开设置预算值,风速取15%,光照取10%,负荷取5%,实测下来鲁棒性和经济性的平衡效果不错。

4.4 结果输出与敏感性分析

代码的最终输出包括:最优容量配置方案、对应的总成本拆分、各典型日的运行调度曲线、以及最恶劣场景的分布情况。这些输出不只是用来呈现最终结果,更重要的是用于验证模型的合理性。

最恶劣场景的分布情况值得特别关注。C&CG迭代找到的最恶劣场景对应的风、光、负荷水平组合,揭示了这个系统在面对什么样的情况时最脆弱。如果发现最恶劣场景全部落在某个典型日类别里,说明这个时段的配置裕量不足,需要考虑增加对应电源的配置。

敏感性分析也是代码配套的重要部分。我习惯在基础案例跑通后,做两组敏感性分析:一是预算值从0到最大值的全区间扫描,看配置结果和总成本的变化趋势;二是折现率从5%到12%的扫描,看不同资金成本假设下配置方案的偏移方向。这两组分析做完,项目的说服力会强很多。

5. 常见问题与排查技巧

5.1 子问题对偶变换老是报错怎么办

这是两阶段鲁棒优化代码实现中出现频率最高的问题。Yalmip报错通常会提示原始问题不可行或对偶问题不可行,但实际原因往往是建模本身引入了隐含错误。

排查时我有一套固定流程。先检查第二阶段约束是否有冗余矛盾,比如功率平衡约束与储能约束连立时,在某些极端参数组合下可能出现无解。再检查不确定变量的取值范围,风速、光照的波动范围设置过大会导致子问题不可行。然后检查是不是有变量忘记定义或维度不匹配,Yalmip对维度一致性的检查并不总是能给出清晰提示。

还有一个非常隐蔽的坑跟对偶变量非负性有关。等式约束对应的对偶变量是自由变量,如果代码里误写成了非负变量,对偶问题求出的目标值会系统性偏大,导致UB计算不准确,最终影响收敛判据。我在第一次调试时就踩过这个坑,花费了大量时间才定位到只是对偶变量定义里少了一个维度。

5.2 主问题求解时间爆炸式增长

C&CG迭代过程中主问题规模不断增大,这是正常的,但如果出现求解时间爆炸式增长,就要回头审视了。最常见的原因是第二阶段变量的数量过多——每个场景24个时段的众多设备变量叠加起来,主问题的变量数量会迅速达到数十万级别。

一个有效的规避手段是场景压缩。不必把每个典型日的所有24小时都作为独立时段处理,可以按负荷峰谷特性合并为8到12个代表时段。项目实测下来,从24时段压缩到12时段,求解时间能缩短约70%,而容量配置结果的变化不超过2%,完全可以接受。

另一个技巧是给求解器设置合理的MIP间隙容忍度。主问题内部的整数求解不需要每次都达到非常高的精度,因为在C&CG迭代的前期,主问题本身就是一个不完整的松弛版本,解得很精确也没有意义。我通常把主问题的MIPGap设为0.5%到1%,每轮迭代省下的时间非常可观。

5.3 结果中储能容量过大或过小的经验判断

储能容量是微网容量配置中最敏感、也最容易出现异常结果的变量。如果储能容量过大,通常是切负荷惩罚成本设置过高,系统宁愿多配储能也不愿承担任何切负荷风险;如果储能容量过小,则可能是柴油发电机成本偏低,系统选择用柴油来覆盖所有峰荷时段。

我在调试中对这个问题形成了两条判断经验。第一,切负荷惩罚成本应当参考实际供电合同中约定的缺供电赔偿标准来取值,而不是随意给一个很大的数字。这个值在模型里主导着储能配置规模,对最终成本的影响非常显著。第二,储能投资成本的取值要考虑近年锂电池价格下降的趋势,有些文献里用的还是好几年前的单价,这样算出来储能容量会明显偏小,配置方案直接失真。

另一个值得留意的细节是储能的寿命衰减建模。简单的模型只考虑储能的额定能量容量和功率容量两维约束,但对边际成本影响很大。我在项目里增加了储能循环寿命的折算进运行成本,这样在总成本对比中,储能和柴油发电机不会因为简化建模导致比较失真。

5.4 求解结果的可靠性与合理性验证

最后一步还要做结果验证。一个简单可靠的方法是拿得到的容量配置方案,去做蒙特卡洛模拟验证:随机生成大量风速、光照和负荷的样本,放到这个配置方案下做逐时段的运行模拟,统计系统的失负荷概率和平均运行成本。如果模拟得到的失负荷概率水平与模型中预设的预算参数对应一致,说明模型构建和求解结果是可信的。

这个验证步骤在不少项目里被省略了,但我建议不要省,尤其是如果这个容量配置的结果要用于实际规划决策。我在实际项目中做过一次验证,发现虽然模型设定预算值为总时段数的15%,但蒙特卡洛模拟显示失负荷概率很低,说明盒式预算约束在最恶劣场景的寻优上还不够激进,保守度比预期偏高。这并不算错误,但它提示了一个改进方向:可以考虑用数据驱动的方法,从历史样本中提取更贴近真实分布的凸包作为不确定集合,在保证鲁棒性的前提下进一步降低总成本。

回到C&CG的迭代次数和收敛行为,我观察到在预算值中等水平时,通常6到8次迭代就能达到1%的间隙收敛;而预算值偏小或偏大时,收敛往往更快,3到5次就结束了。这说明模型的复杂度集中在“既有挑战性又不至于完全无解”的中间地带。理解了这一点,后续在调参时心里会更有数。

6. 工具选型与运行环境建议

6.1 Matlab版本与工具箱的搭配

代码对Matlab版本没有特别硬性的要求,R2020a以上的版本都能正常运行,但建议使用较新版本,因为Yalmip对新版本Matlab的适配更好一些。Yalmip是一个建模工具箱,需要单独下载并添加到Matlab路径中,安装过程就是解压后把文件夹加入路径,没有太多坑。

求解器方面,项目调用的是商用求解器,但实际测试下来,Yalmip内置的默认求解器在中型规模问题上也能完成求解任务,只是速度明显慢一些。如果只是做教学演示或小规模验证,不装商用求解器也可以跑通全流程;如果要做敏感性分析或大规模场景模拟,还是建议安装一套完整的商用求解器许可。

6.2 运行时间的分布与优化

跑通一个完整案例,我观察到的运行时间分布大概是:数据预处理和典型日聚类约占5%以下,整个C&CG迭代过程占绝大多数时间,其中主问题求解约占70%到80%,子问题求解约占15%到25%。子问题的求解相对快,瓶颈始终在主问题的MILP求解上。

针对这个瓶颈,除了前面提到的时段压缩和MIPGap放宽,还有一个更进一步的优化手段:热启动。在C&CG迭代中,相邻两轮主问题只有新增的约束,之前的最优解是一个很好的初始猜测。Yalmip支持设置初始解,我实测这能再缩短主问题求解时间约20%到30%,而且实现非常简单,就是在主问题求解前用assign函数给变量赋值。

最后说一个我在实际跑这个项目时特别有体会的点。两阶段鲁棒优化的容量配置模型,数学符号繁复、代码量大,初次接触的人很容易被细节淹没。但一旦把框架理清楚——主问题管规划、子问题找恶劣场景、迭代连接两者——后续所有调整都是在这个骨架上做文章。参数怎么改、场景怎么换、约束怎么加,都是在同一套迭代体系里运转,代码的可维护性就会高很多。

这个项目做完之后,我自己最大的收获是:鲁棒优化不是越保守越好,关键是把不确定集合的形状描述准确。盒式加预算约束只是最基础的起点,后续如果你有精力继续深入,可以试试数据驱动的不确定集合构造方法,或者把两阶段扩展成多阶段自适应鲁棒模型,那会是另一个值得探索的深水区了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进