最近一段时间,我把一个“分布鲁棒机会约束 + N-1准则 + 低碳经济调度”的模型在Matlab里完整跑通并复现了。这类工作在电力系统优化里属于比较典型、也特别容易让人“卡住”的题目:一方面要处理N-1故障的离散组合,另一方面又要面对风电、负荷的不确定性,再叠加低碳指标,模型规模稍一大,求解器就直接罢工。这篇文章把我从建模到编码再到调参的过程,以及踩过的坑,完完整整地捋一遍,希望能给正在做相关方向的同学一点参考。
先给没接触过的朋友快速定位一下。题目里的“DRCC”,是Distributionally Robust Chance Constraint的缩写,可以理解成传统机会约束的“防御加强版”。普通机会约束假定不确定量的概率分布已知,只要求约束违限概率小于某个阈值;DRCC则是在一个可能分布的集合里,找到最恶劣的那个分布,并保证在这个最恶劣分布下约束依然满足。相当于把“概率风险”转成“分布风险”,即便真实分布和预测分布存在偏差,也不会轻易翻车。而“N-1准则”指的是在电力系统任意一个元件(线路、变压器、机组等)故障退出后,系统依然能保持安全运行,这是电网调度与规划里绕不开的安全底线。
适合谁看?如果你只会用求解器做确定性经济调度,想往鲁棒、随机方向进阶;或者你手头已经有论文代码但跑不通、不会改,这篇应该能帮上忙。我也尽量少写教科书术语,多写实际能落地的细节。
1. 先搞清楚:这个调度模型究竟在算什么
1.1 从“低碳”到“经济”:目标函数为什么需要两个分量
传统经济调度只关心一件事:让总发电成本最低,机组的启停、出力按最低成本匹配。然而在“双碳”背景下,系统的碳排放约束越来越硬,单纯让成本最小可能换来的是高煤耗机组满发、低碳机组压低,碳指标却严重超标。于是低碳经济调度把两件事揉进目标函数:一是常规机组的煤耗、启停成本,二是碳排放成本或碳排放额度惩罚。
处理碳排有两种常见思路。一种是排放配额制:给定一个总排放上限,作为硬约束。另一种是碳价机制:把每吨碳排放折算成费用,放进目标函数,让模型自己去权衡“多排碳”和“多花钱”之间的关系。我做的是第二种,理由是它更符合经济调度的底层逻辑,而且目标函数里增加一个线性碳价项,Matlab建模时几乎不增加复杂度。
要注意的是,碳价系数不是越大越好。我见过有人把碳价调得极高,结果机组强行全停,系统直接失负荷。合理做法是把碳价和机组边际成本的量级对齐,比如煤电机组边际成本0.3元/kWh,碳价取0.1~0.2元/kg,让模型在成本和碳排之间自然折中。实际工程中,碳价通常来自碳市场交易价格或企业年度配额折算,这比拍脑袋定数值科学得多。
1.2 N-1准则:不是锦上添花,而是安全底线
N-1准则的含义是:在系统运行中,任意一台机组或一条线路发生故障并退出运行,剩余系统仍然能够承受潮流转移并保持稳定。对调度模型来说,这意味着我们不能只算一个正常运行工况,还要同时校验一组故障工况下的潮流是否越限。
听起来很简单,但真正建模时会发现它是块硬骨头。最朴素的做法是枚举所有N-1场景,对每个场景各加一组直流潮流约束。一个IEEE 30节点系统,支路和机组加起来可能几十个,每个场景都要引入一组约束和变量,模型规模直接翻几十倍。更麻烦的是,约束里如果涉及机组启停和节点电压,故障场景和正常场景不一定能完全解耦。
所以复现这类工作要先明确:这里考虑的是安全约束经济调度,也就是在调度计划生成时,让最终机组出力组合在任一预想故障下都能满足线路和发电容量约束。数学上相当于把确定性调度里的基态潮流约束替换成一组“基态+故障态”约束。有些复现代码为了简化,只在N-1场景里校核线路潮流,并不考虑故障后的再调度,这在工程上是偏保守的,但在学术复现阶段完全可行。
1.3 分布鲁棒机会约束:用最坏的分布做最好的决策
这部分是整个模型的核心,也是最容易让人绕晕的地方。普通机会约束写成:
Pr{F(x, ξ) ≤ 0} ≥ 1-ε
它假设不确定量ξ的真实分布P是已知的。但实际中风电、负荷这类不确定量的分布往往只能从历史数据里估计,估计出来的分布和真实分布一定存在误差。如果你用这个估计分布去做机会约束,可能会因为分布偏差导致违限概率远超ε。
分布鲁棒的思路是构造一个“分布的集合”,称为模糊集或置信集,真实分布大概率落在这个集合里。比如最常用的Wasserstein模糊集,它是以经验分布为中心、以Wasserstein距离为半径的“球”,把所有与经验分布距离小于r的分布都纳入考虑。然后要求机会约束对这个集合里的每一个分布都成立:
inf_{P∈B_r} Pr_{ξ∼P}{F(x,ξ)≤0} ≥ 1-ε
注意是inf,也就是对最坏情况做保证。这样一来,即使分布估计有偏差,数学上也能提供一个可靠的解。
我复现的时候把模糊集换成Wasserstein距离的1-范数或∞-范数版本,处理起来会有不同的对偶表达式,但整体逻辑一样:先把机会约束转成带场景的对偶线性约束,再塞进主问题里迭代求解。Wasserstein距离的直觉可以这样理解:它衡量的是把一种分布“搬运”成另一种分布所需的最小代价,搬得越近说明两个分布越像。用这个距离圈出来的置信集,天然包含了那些“和历史数据看起来很像,但又不完全一样”的分布,非常合适描述电力系统中的概率预测误差。
2. 核心模型数学表达的逐步推导
2.1 目标函数和运行约束怎么写
为了方便叙述,我把模型简化成如下形式:
目标函数:
min ∑_{g∈G} [ a_g P_g² + b_g P_g + c_g ] + C_carbon ∑_{g∈G} e_g P_g
其中a_g、b_g、c_g是煤耗成本系数,e_g是单位发电碳排放强度,C_carbon是碳价。如果机组有启停变量,还要加上启停成本和最小开停机时间约束。二次项在YALMIP配Gurobi下可以直接处理,但如果你用的是某些开源求解器,建议把二次目标改成线性目标加惩罚项,或者做分段线性化,这样求解稳定性会好很多。
系统约束包括这些:
- 功率平衡:∑P_g + P_renewable = P_load
- 备用需求:∑P_g,max ≥ P_load + R
- 线路潮流:用直流潮流,F = PTDF × (节点有功注入),限制在[-Fmax, Fmax]
- 机组出力上下限和爬坡约束
如果只做单一时刻调度,爬坡约束可以简化;如果做日前24小时,则需要考虑时间耦合,模型会大很多。很多复现版本只做单时段,因为核心是验证DRCC和N-1,不必纠缠多时段。不过单时段模型有一个缺点:它没办法体现风电预测误差的时间相关性,所以如果你的场景生成里出现了跨时段状态量,建议至少做3~5个时段的滚动验证。
2.2 故障约束如何线性化处理
对N-1故障场景,DC潮流方程会随线路通断改变。一种做法是对每个预想事故c,重新计算此时系统的PTDF矩阵。对于支路开断,可以用补偿法或因子表修正,但Matlab里最直接的办法是枚举开断支路后重新计算PTDF。在模型里对每个场景c额外加入:
-F_c,max ≤ PTDF_c × (P_gen_c - P_load_c) ≤ F_c,max
同时还要考虑场景c下的发电重新调整。简单模型通常假设正常运行时的机组出力参考值可以直接应用于故障态,而故障态的发电机调节能力用备用或次数约束来近似。复杂模型则会引入故障态再调度变量,并加上与原调度偏差的约束。
这个环节最大的坑就是PTDF矩阵维度。模型写复杂后很容易出现矩阵乘法维度错误,我建议先把基态PTDF跑通,再加故障态。另外,如果有线路开断导致系统解列,PTDF矩阵可能奇异,这时候需要在故障筛选阶段把这类故障剔除,或者用“只校验连通子系统”的方式重新计算,不然求解器会直接报“Singular matrix”。
2.3 机会约束与Wasserstein置信集构造
接下来是DRCC部分。我用的场景集是风电出力的S个样本,记为ξ_i。经验分布 P̂ = (1/S)∑δ_{ξ_i}。Wasserstein置信集定义为 B_r(P̂) = { P : d_W(P, P̂) ≤ r }。
根据Wasserstein距离的对偶性质,如果原机会约束是线性的,即约束为 a(x)^T ξ + b(x) ≤ 0,那么最坏分布下的机会约束可以转化为场景和对偶变量的线性约束。具体来说,当模糊集半径r足够小,可以通过引入辅助变量和1-范数/∞-范数距离计算,将原约束变成有限个线性不等式。这个过程需要做变量代换和收敛性证明,但落到Matlab里就是一组由样本点驱动的大规模线性约束。
我建议在编码前先在纸上推一遍单约束情形,确认当r→0时能退化成样本均值约束,再写代码。这里有一个简单的验证例子:如果S=3、ε=0,此时机会约束要求所有样本都满足约束,分布鲁棒解应该和“对每个样本逐一加约束”的结果一致。如果你的代码解出来结果不同,那多半是模糊集半径或违限变量写错了。
3. Matlab实现:从公式到可运行代码
3.1 工具链选择与求解器配置
我用的环境是Matlab R2021a + YALMIP + Gurobi 9.5。YALMIP的优点是建模速度快,写目标函数和约束很接近数学表达式,之后可以一键切换求解器。如果没有Gurobi授权,可以用CPLEX或Mosek,但对大规模混合整数凸优化问题,前两者明显比开源SCIP快出几个量级。
安装配置时注意:YALMIP的版本要和Matlab版本兼容,Gurobi的路径也要正确加入。我最早因为只添加了Gurobi路径而漏掉了YALMIP路径,导致报一堆“Undefined function or variable 'sdpvar'”。建议把YALMIP和Gurobi都加到Matlab路径里,然后运行yalmiptest,确认求解器状态正常后再跑模型。
3.2 数据组织和故障集生成
我建议把所有系统数据封装在一个struct里,比如参考MATPOWER格式。如果不想手写,可以直接加载MATPOWER案例再转换成YALMIP变量。但注意:MATPOWER的bus类型定义与一般潮流数据略有差异,需要核对。
生成故障集时,我写了一个函数,遍历所有支路或机组序号,把第i号元件移除后重新计算PTDF。如果不想重新计算,可以用近似修正公式,但Matlab里直接乘一个稀疏矩阵也没问题。唯一要注意的是,开断线路后系统可能解列,此时PTDF会出现奇异,需要在故障集中过滤掉那些导致分区的故障,只保留连通系统部分。
下面是我常用的数据组织表格:
| 数据类别 | 存储方式 | 说明 |
|---|---|---|
| 节点参数 | bus矩阵 | 负荷、电压等级、节点类型 |
| 线路参数 | branch矩阵 | 首末端节点、电抗、容量 |
| 机组参数 | gen矩阵 | 成本系数、上下限、碳排强度 |
| 风电场景 | wind_scenarios矩阵 | 每列代表一个样本 |
| 故障集 | contingency_cell | 每个元素存储故障场景编号 |
3.3 关键代码:YALMIP建模与分布鲁棒约束处理
下面给一个核心片段,展示如何声明决策变量和基本约束:
P = sdpvar(n_gen, 1); % 机组出力 theta = sdpvar(n_bus, 1); % 电压相角 u = binvar(n_gen, 1); % 机组启停 cost = a'*(P.^2) + b'*P + c'*u; carbon_cost = C_carbon * e' * P; objective = cost + carbon_cost; Constraints = [Pmin.*u <= P <= Pmax.*u]; Constraints = [Constraints, sum(P) + wind_forecast == load]; Constraints = [Constraints, -Fmax <= PTDF * (P + wind_forecast - load_vec) <= Fmax];对于N-1约束,写到循环里:
for c = 1:n_cont Constraints = [Constraints, -Fmax <= PTDF_cell{c} * (P - load_vec) <= Fmax]; end如果每个场景的PTDF只和拓扑相关,而P是统一变量,那么这一步不会增加新变量,只是增加约束条数。如果让故障场景下允许机组再调度,则还需要引入P_c变量和相应的关联约束,模型会变成大规模含二进制变量的优化问题,求解时间会明显上升。
DRCC的处理比较技巧,先将所有样本场景的随机约束写成for循环:
z = binvar(S, 1); M = 1e4; % big-M for i = 1:S Constraints = [Constraints, a_var' * xi(:, i) + b_var <= M * (1 - z(i))]; end Constraints = [Constraints, sum(z) <= floor(S * epsilon)];这里z是0/1变量,表示第i个样本是否允许违反约束。这其实是“样本近似”机会约束。如果要严格做到分布鲁棒,需要在此基础上加上对偶变量和Wasserstein约束。我用1-范数时的模型片段是这样的:
lambda = sdpvar(1, 1); t = sdpvar(S, 1); % 对偶辅助变量 for i = 1:S Constraints = [Constraints, -t(i) <= a_var' * xi(:, i) + b_var <= t(i)]; end Constraints = [Constraints, mean(t) + lambda * r <= 0];这个写法的理论基础是Wasserstein模糊集下的线性约束对偶形式。如果你用的是∞-范数,表达式会略有变化,但思路一致。整体上,DRCC比普通样本近似多了一两个辅助变量,却换来了分布层面的鲁棒性,性价比很高。
3.4 结果验证与图表输出
跑完模型后,不要直接看目标函数值。先检查基态和所有故障态下线路潮流是否都在限值内。我习惯把每个场景下的最大线路负载率画出来,用热力图看哪些支路接近满载。还可以对比确定性调度、传统机会约束和DRCC三者的机组出力曲线,直观展示DRCC在提高安全裕度方面的代价是成本小幅上升。这组对比图在论文里很常见,也是证明模型有效性的核心论据。
结果输出时,建议把每个场景的目标值、碳排放量、最大负载率、违限次数都保存成表格,方便后续调整参数时做比较。我自己写了一个简单的result log函数,每次优化完自动追加一行记录,这样调参时能清楚看到哪个参数改动带来了什么影响。
4. 我踩过的坑:完整排查实录和避坑指南
4.1 故障枚举太慢怎么办
最开始我把所有线路开断都放进模型,30节点系统还撑得住,但换成118节点后,模型连续求解两小时没动静。主要问题不是约束过多,而是Gurobi内置预求解在处理大量故障态约束时耗时严重。后来我把故障场景做了筛选:只考虑那些开断后会造成潮流越限的预想故障,也就是“关键故障集”。方法是在确定性最优解下先扫一遍所有故障,用单一故障安全校核筛出负载率超过80%的支路,再把这些故障显式加进N-1约束里。
这样做的理论依据是:N-1校验本质上只对临界故障敏感,非临界故障即使不加约束,解也在安全域内。实测下来模型规模缩减了70%,求解时间从两小时降到十几分钟。当然,严格来说这个筛选可能不严谨,但在工程复现时可以接受。
4.2 二进制变量和非线性项把模型搞得无法收敛
有一次我把碳排和启停状态相乘,比如 e_g * P_g * u_g,结果模型变成MINLP,Gurobi直接报错说不支持非凸二次约束。YALMIP尝试把它转成双线性项,求解器也处理不了。解决办法很简单:碳排量只和出力相关,启停成本单独建模,不要把启停状态和出力相乘。遇到机组启停,使用big-M线性化,不要直接写非线性乘积。
另一个经典坑是目标函数里的二次项a_g P_g²。如果a_g为负或者模型含0-1变量后变成非凸,Gurobi会拒绝。我发现有些复现代码用分段线性化把二次目标转成线性,为了保险也可以这么做,代价是引入更多辅助变量。如果你的系统比较小,直接留着二次项没问题;但系统一变大,建议改成线性化。
4.3 Wasserstein球半径该取多大
这是DRCC最关键的参数。半径r太小,模型退化为样本均值优化,失去分布鲁棒意义;r太大,模糊集过度保守,目标函数值飙升,甚至无解。我建议用样本数量S自适应:r ∝ 1 / sqrt(S)。对于S=1000,r大概在0.05~0.2之间。具体取值可以通过交叉验证确定:把历史数据分成train和test,用train拟合分布并做优化,再把得到的决策放在test分布上计算违限概率,选择违限概率最接近目标ε的r。这个方法虽然多跑几次优化,但比拍脑袋取数可靠得多。
下面是我实测下来的经验表:
| 样本数 | 建议球半径 | 违限风险 | 目标函数增幅 |
|---|---|---|---|
| 100 | 0.2~0.4 | 低 | 5%~10% |
| 500 | 0.1~0.2 | 低 | 3%~8% |
| 1000 | 0.05~0.15 | 低 | 2%~5% |
| 5000 | 0.02~0.08 | 低 | 1%~3% |
注意这个表只针对风电出力标幺化后的场景,如果你的数据量纲不同,需要先归一化再做Wasserstein距离,否则球半径会失去可比性。
4.4 分布鲁棒解不稳健?问题出在场景生成
我也遇到过DRCC解比普通机会约束还差的情况,最后发现是场景生成有误。很多代码用拉丁超立方抽样生成风电场景,但忽略了风电出力的空间相关性,导致样本分布过度集中。后来改用copula或基于历史误差的扰动法,就是把实测出力作为均值,加上一个符合历史残差分布的扰动项,得到的样本更接近真实。生成后还要做场景削减,否则S太大会让机会约束部分的二进制变量爆炸,求解器内存直接顶不住。
这里有一个排查建议:做完DRCC优化后,把最优解放在所有原始历史样本上做蒙特卡洛回检,统计实际违限概率。如果实际违限概率和目标ε差太多,八成是场景生成阶段出了问题,而不是求解器的问题。我排查过几次,最后发现是样本序列没打乱,相邻样本高度相关,导致经验分布严重失真。
5. 从复现到扩展:我的经验和下一步建议
5.1 如果想继续做深度研究,可以从哪里改进
我这套复现模型仍有不少可以深挖的地方。比如把单时段变成多时段,加入机组爬坡、储能和需求响应,模型会更贴近实际,但计算难度也会指数上升。还可以研究模糊集的构造,除了Wasserstein,还有KL散度模糊集、矩模糊集等,不同模糊集对解的保守度有很大影响。
另外,N-1校核也可以从全枚举换成优化选择,比如用Benders分解把故障约束作为子问题迭代校验,这与实际工程中安全约束调度加安全校核的闭环流程更一致。如果时间充裕,我建议把DRCC和N-1分成两个模块做解耦:先跑不加故障的DRCC,再把结果送到N-1安全校核里,如果不通过就生成割约束回头迭代。这种方式在处理几万条支路的大系统时会比一次性建模更高效。
5.2 一个小技巧:用场景削减减轻计算负担
这里分享一个我在复现过程中觉得最有用的技巧。分布鲁棒机会约束最怕的就是样本太多导致模型爆炸。我用密度聚类场景削减,先对风电样本做K-means聚类,把上千个场景压成50~100个聚类中心,每个聚类中心再按簇内样本数量加权。这样既保留了原样本的整体分布特征,又显著减少约束数量。实测下来目标函数变化不到2%,求解时间下降近80%。当然,严格来说场景削减会引入一定的近似误差,如果你的研究目的是要精确证明收敛性,最好保留原始场景或者使用经过理论分析保证的削减方法。
下面是一个简单的k-means场景削减片段:
rng(1); S_raw = 2000; wind_raw = normrnd(wind_mean, wind_std, S_raw, 1); [idx, centers] = kmeans(wind_raw, 80, 'Replicates', 5); weights = histcounts(idx, 80) / S_raw; S_reduced = length(weights); xi = centers'; p = weights';之后在DRCC约束里使用xi和p,样本数量从2000降到80,模型体积大幅减小,但分布鲁棒强度依然保留。
5.3 给你的一点点实际建议
如果你也在复现DRCC相关模型,有一条路径比较省时间:先把不带N-1的DRCC小系统调通,再叠加故障态约束。每个模块都设置一个最小可行版本去验证,比如先只跑IEEE 6节点,确定结果合理,再逐步扩大系统规模。另外,手头保留一个完整的确定性经济调度作为baseline,任何改动都能立刻看出DRCC增加的保守度和成本,这样后续调参会非常顺。
我从建模到最终跑通,中间改了不下几十版代码。回头再看,最难的不是数学推导,而是把论文里省略掉的细节在代码里补齐。特别是Wasserstein球的对偶转换,书上只会写两页推导,实际编码时会遇到各种维度匹配、符号翻转、big-M取值问题。只要你能把最小案例跑通,再大的系统也只是一个扩展问题。希望这篇文章帮你少走一点弯路。