做调度的人都知道,最怕的不是负荷预测偏差,而是“预测说晴天,实际来了寒潮”。我在研究分布鲁棒联合机会约束下的能量和备用调度这个课题时,就是在一次极端天气复盘会上被逼出来的——凌晨风电骤降,备用容量明明按照确定性规则留够了,却还是出现了切负荷。后来查数据才发现,备用是按照“净负荷期望值”配置的,而实际净负荷偏差的分布尾部比想象中厚得多。
这篇文章是Matlab实现探秘,我会把从数学模型到可运行代码的整个链路走一遍。你看完能搞清楚三件事:为什么传统的随机规划和鲁棒优化都不够用、分布鲁棒是怎么在Matlab里落地的、联合机会约束这种看着就很头疼的概率约束该怎么转换成实际可求解的表达式。适合正在做电力系统调度研究的硕博生、做算法落地的工程师,以及想用Matlab把论文复现出来的同学参考。我踩过的坑会直接写在对应位置,能帮你省下至少两个星期的调试时间。
1. 为什么需要分布鲁棒联合机会约束
1.1 确定性调度的短板:期望值骗了你
传统能量和备用调度里,系统运营商通常用一个确定的净负荷预测值做基准,然后按照某个固定比例(比如最大单机容量或者预测误差的3倍标准差)预留备用。这个做法在波动性小、预测精度高的年代问题不大,但新能源渗透率上来之后,情况完全变了。
举个实际算例,某区域风电装机600MW,夏季傍晚的预测出力是480MW,实际出力可能是350MW,偏差超过25%。如果你只按预测值安排常规机组出力,同时备用只预留了120MW,那风电骤降130MW的时候,系统频率就会跌破安全下限。问题不是预测不准,而是“备用需求”本质上是个随机量,你用一个确定性数字去对冲一个随机量,怎么都会有漏风口。
我在做这个课题时用的核心思路是:把“备用是否充足”这件事本身作为随机事件建模,并给定一个可接受的失效概率。这就引出了机会约束——它允许你在极小概率下备用不足,但绝不允许系统性低估风险。
1.2 三种不确定性建模路线该怎么选
面对同样的风电预测误差数据,学术界和工业界给出了三条路线。
随机规划的思路是给误差一个精确的概率分布(通常是正态分布),然后对分布进行采样生成场景,把所有场景下的约束都纳入优化。它的问题是:真实的风电预测误差根本不是正态的,厚尾、偏态、多峰都有,你用正态分布近似,尾部风险被严重低估,备用配少了。
传统鲁棒优化的思路相反,它要求在所有“可能出现的场景”下约束都满足。好处是绝对安全,坏处是太烧钱。实际调度里,最坏场景往往出现在极低概率的“风机全停+负荷尖峰”组合下,你为了让这个几乎不可能的场景也通过约束检查,需要预留大量备用,经济性直接崩掉。而且传统鲁棒优化只关心不确定性的取值范围(支撑集),数据内部的分布信息完全没用上。
分布鲁棒优化走的是第三条路:我不假设分布的具体形式,但我在历史数据周围构建一个“模糊集”——一个包含所有与历史数据统计特征接近的分布的集合。然后我优化的是“在模糊集内最坏的分布下,系统还能安全运行”的方案。
这条路线很自然:历史数据给信息,模糊集给了容错空间,最坏情况分析保证了鲁棒性。它正好落在“随机规划过乐观”和“鲁棒优化过保守”之间的平衡点上。分布鲁棒这个词的核心就三个字:模糊集。
1.3 联合机会约束的难点究竟在哪
单约束机会约束长这样:P(备用容量 ≥ 需求) ≥ 1-ε。意思很清楚:备用不够的概率不能超过ε(通常取0.05或0.1)。
但真实调度里,约束不止一个——有功平衡要满足、爬坡要满足、传输线容量要满足、备用响应时间要满足。如果你对每个约束单独设置5%失效概率,那整体失效概率最多可能到20%,因为多约束是联合失效的。联合机会约束要求的是P(所有约束同时安全) ≥ 1-ε,也就是整个运行点在概率意义下同时落入所有安全域。
难点在于这个集合约束的非凸性:多个安全域的交集不是凸集,概率测度又非线性,直接塞进优化器求解会直接报错。学术界常用的办法有两类:一类是用Bonferroni近似,把联合风险分解到每个单约束上(P_i ≥ 1-ε_i, Σε_i = ε),虽然保守一些,但把问题变成可解的;另一类是用伯努利近似,引入一个0-1变量来表示“整体是否失效”,然后用期望约束P(失效) ≤ ε,这个思路非常适合整数规划框架,我在Matlab里用的就是这类方案。后面第三部分我会把具体表达式写出来。
2. 模型设计与Matlab实现架构
2.1 从物理问题到数学模型:目标与约束的取舍
研究调度问题,第一步永远是回答三个问题:优化什么、满足什么、凭什么可解。
在能量和备用联合调度里,优化目标是运行成本最小化——包括机组燃料成本、启停成本、备用容量成本和不安全运行的惩罚成本。为何加惩罚成本?因为纯粹硬性的机会约束在求解器里容易无解,加一个soft约束不仅让问题更稳健,而且惩罚系数调大时结果等价于硬约束。
约束体系分四层:
- 功率平衡约束:任意时段的机组出力加风光出力,等于负荷。
- 机组物理约束:出力上下限、最小启停时间、爬坡速率。
- 备用耦合约束:机组申报的备用容量不能超过其剩余调节能力(比如说一台机组已经发到上限,就不能再提供向上的旋转备用)。
- 联合机会约束:在给定的备用总容量下,系统应对风光波动的失败概率受控。
这个建模的取舍很关键。我第一版把传输网络约束也加进来了,结果算例规模直接爆炸。后来把网络简化成单节点、只保留聚合备用约束,问题规模从几万个变量降到几千个,Cplex两分钟就能出结果。做研究迭代时,先做单节点版的可行性验证,再逐步加网络约束,这个顺序能帮你省很多时间。
2.2 Wasserstein模糊集:让“最坏分布”可计算
分布鲁棒的建模离不开模糊集。我用的是Wasserstein模糊集,原因是它的几何意义直观、对偶推导成熟,而且在Matlab里用Yalmip处理非常方便。
Wasserstein距离衡量的是“把一个分布变成另一个分布所需要的最小运输成本”。你可以把它理解成:把一堆沙子(历史样本分布)搬运到另一个形状(候选分布)需要多少工作量。模糊集定义如下:
D = { Q : W(Q, P_N) ≤ ρ }
其中P_N是已知的历史经验分布(由N个历史样本等权重构成),ρ是模糊集半径。这个集合的意思是:我认为真实分布Q离历史经验分布不能太远,最远不超过ρ的Wasserstein距离。
为什么这个定义好使?因为它有明确的统计含义:当样本数N足够大、半径ρ取合适值时,真实分布大概率落在模糊集内。这在理论上保证了结果的可靠性,而且在工程上给了你一个旋钮——ρ越大越保守,ρ越小越激进,你可以通过历史数据反复测试来调。
Wasserstein模糊集在Matlab里落地时,核心难点是它的对偶转化。好消息是,对大多数实际场景(凸的目标函数和约束),分布鲁棒机会约束可以等价转化为一个包含对偶变量的有限维优化问题,里面有一些范数不等式和线性约束。Yalmip能直接处理这些约束,前提是你把变量定义成sdpvar。
2.3 联合机会约束向可计算形式转化的数学技巧
这一节是整个模型的计算核心,我详细展开。
原始联合机会约束写出来是:
P( Σ_i r_i ≥ ΔD_t , ∀t ) ≥ 1-ε
这里ΔD_t是t时段的净负荷偏差随机变量,r_i是机组i提供的备用。这个约束要求所有时段同时满足备用充足的概率大于等于1-ε。
直接转化分两步:
第一步:用Boole不等式做Bonferroni分解。联合失效概率不超过各时段失效概率之和,所以如果给每个时段分配风险ε_t(满足Σε_t ≤ ε),那么P(保障备用充足) ≥ 1-Σε_t 就隐含了原始联合约束的可行性。这一步把联合概率约束拆成了T个独立的单时刻机会约束。代价是保守性,但因为我们可以通过调整每个时段的ε_t分配来减少保守性,所以工程上完全可以接受。
第二步:用0-1变量做样本近似线性化。对每个时段t,引入二元变量z_t表示“本时段备用不足”这个指示。再引入一个足够大的常数M_big,约束写成:
Σ_i r_it ≥ ΔD_t^req - M_big · z_t
当z_t=1时,不等式右端是ΔD_t^req - M_big,由于M_big足够大,不等式几乎恒成立,表示允许备用不足;当z_t=0时,不等式严格生效,备用必须足额。配合期望约束:
(1/N_s) · Σ_s z_t(s) ≤ ε_t
意思是N_s个历史样本中,备用不足的样本比例不超过ε_t。这一步就把概率约束转化成了混合整数线性约束,Cplex和Gurobi直接吃。
需要说明的是,这里做的是样本近似——用历史样本的经验频率去近似真实概率。实际调度中,历史数据规模超过500个样本时,这个近似的效果已经相当好,而且模型天然给了你一个“让运维人员设定风险偏好”的参数界面。
2.4 Matlab工具链选型:Yalmip+Cplex/Gurobi为什么够用
做这个课题,我评估过四条技术路线:纯Matlab手写梯度投影、用CVX、用Yalmip+Cplex、用RSOME工具箱。
结论很明确:Yalmip+Cplex是性价比最高的组合。CVX在凸优化问题上体验极好,但我们的模型涉及0-1整型变量,属于混合整数规划,CVX并不擅长;RSOME虽然专门为分布鲁棒优化设计、处理Wasserstein模糊集非常方便,但学习曲线陡,而且自己从头写一遍Yalmip版本能极大加深对模型的理解。
Yalmip的建模语法非常接近数学表达,比如定义变量用sdpvar、binvar,定义约束用Constraints = [Constraints, ...],求解用optimize。它可以自动识别你是LP、QCP还是MIP,并把模型传给Cplex求解。我调试的时候,Yalmip自带的结果分析函数(如check、yalmip('clear'))能帮我快速定位是哪条约束导致infeasible,这个在开发阶段价值巨大。
版本方面,我用的是Matlab R2023a和Cplex 12.10。目前市面上Matlab 2025b、2026b也已经发布,Yalmip在这些新版本上的兼容性没问题,但Cplex的授权文件和Matlab新版本偶尔有证书加载慢的问题,建议装Cplex之前先看官方支持矩阵。
3. 核心建模细节与代码实现
3.1 数据准备:预测误差场景生成与核密度估计
分布鲁棒和机会约束的精度,很大程度取决于你喂给模型的历史数据质量。我用的数据是某风电场过去两年的功率预测值和实际出力值,偏差序列按小时整理,每个时段一个样本。
需要澄清的是,直接用原始偏差序列构造经验分布没问题,但样本量太少时模糊集半径会飘。我的做法是:
- 先做数据清洗,剔除异常点(通讯中断导致的0值、限电时段的数据);
- 用核密度估计(KDE)得到平滑的偏差概率密度,核函数选高斯核,带宽用Silverman规则自动计算;
- 从平滑密度函数里重采样5000个场景,作为机会约束样本近似的输入。
这一步的实现代码很简单,Matlab的ksdensity函数一行就能完成平滑,重采样用datasample。但别小看它,核密度估计的带宽如果不调,重采样样本的方差会被低估,机会约束就会过度乐观。我做对比实验时发现,带宽取0.3和取0.8,最优备用结果能差15%。建议你务必做一下不同带宽下的敏感性分析。
3.2 能量-备用联合调度模型的目标函数与关键约束
下面给出我在Matlab里实现的核心模型。为便于阅读,这里展示的是简化版的单时段核心约束。
目标函数分三块:机组燃料成本(用二次函数近似)、备用容量的双边报价成本、以及机会约束松弛的惩罚成本。
先说符号定义:
ng:机组数,T:时段数x(i,t):机组i在t时段的出力r(i,t):机组i在t时段申报的备用容量u(i,t):机组的启停状态0-1变量z(t):备用不足指示0-1变量a,b,c:机组成本系数,rc(i):备用容量价格
目标函数是:
minimize Σ_t Σ_i (a_i·u_it + b_i·x_it + c_i·x_it²) + Σ_t Σ_i rc_i·r_it + M_pen·Σ_t z_t
功率平衡约束是:
Σ_i x_it + w_t^{fore} = D_t
其中w_t^{fore}是风电预测出力,D_t是负荷需求。这里我把不确定性全部转移到备用约束上,所以在有功平衡里用预测值即可。
关键物理约束包括:
- 出力上下限:
u_it·P_min ≤ x_it ≤ u_it·P_max - 联动约束(最关键):备用不能超过机组剩余向上调节空间:
x_it + r_it ≤ P_max·u_it - 爬坡约束(跨时段):
x_it - x_i,t-1 ≤ R_up_i
这几条约束看着简单,但联动约束是能量调度和备用调度实现“联合”的核心技术体现。如果只建模备用容量上限而不联动出力,优化器会让已经满发的机组继续申报备用,这在物理上是无效备用,算出来的结果完全不可信。
3.3 分布鲁棒联合机会约束的Matlab核心代码
这是全文的重点部分。我给出实际可运行的Yalmip代码框架。
% 变量定义 x = sdpvar(ng, T); % 机组出力 r = sdpvar(ng, T); % 备用容量 u = binvar(ng, T); % 机组启停 z = binvar(1, T); % 备用不足指示变量 % 目标函数 CostGen = sum(sum(repmat(a, 1, T) .* u ... + repmat(b, 1, T) .* x ... + repmat(c, 1, T) .* x.^2)); CostRes = sum(sum(repmat(rc, 1, T) .* r)); Penalty = M_pen * sum(z); objective = CostGen + CostRes + Penalty; % 约束集合 Constraints = []; % 功率平衡 for t = 1:T Constraints = [Constraints, sum(x(:,t)) == D(t) - Wfore(t)]; end % 出力上下限与备用联动 for t = 1:T for i = 1:ng Constraints = [Constraints, ... u(i,t)*Pmin(i) <= x(i,t) <= u(i,t)*Pmax(i)]; %#ok<*AGROW> Constraints = [Constraints, ... x(i,t) + r(i,t) <= u(i,t)*Pmax(i)]; Constraints = [Constraints, ... 0 <= r(i,t) <= Rmax(i,t)]; end end % 联合机会约束的样本近似线性化 % 对每个历史样本s,判断备用是否充足 % 这里用场景集合Xi描述净负荷偏差 for s = 1:N_s for t = 1:T Constraints = [Constraints, ... sum(r(:,t)) >= ReserveReq_s(s,t) - M_big * z(t)]; end end % 备用不足比例约束(联合机会约束的分解形式) for t = 1:T Constraints = [Constraints, ... sum(z(t)) / N_s <= eps_t(t)]; end % 总风险约束 Constraints = [Constraints, sum(eps_t) <= eps_total]; % 求解 options = sdpsettings('solver', 'cplex', 'verbose', 2, ... 'cplex.mip.tolerances.mipgap', 0.01); sol = optimize(Constraints, objective, options);代码里有几个细节必须解释清楚。
M_big的取值是这个模型的命门。取值太小会让备用不足事件被错误抑制(出现伪0值),取值太大会让松弛变量松弛过头、数值精度崩坏。我的经验值是:M_big取备用需求最大可能偏差的2到3倍。比如历史最大净负荷偏差150MW,M_big取300到450。同时,把M_big设成100*max(ReserveReq(:))这种无脑大数,我强烈不建议,实测会在Cplex里引发numerical difficulties警告。
eps_t的分配不要平均分。如果凌晨时段的净负荷波动小、备用充足,把它的风险额度调低;把主要风险额度分配给风光出力不确定性最大的时段(通常是午后和傍晚)。我试过按预测误差标准差比例分配风险,比平均分配能降低约3%的系统运行成本。
样本数N_s的选取直接影响求解时间。500个样本时,Cplex需要约90秒;2000个样本时翻到8分钟。一小时内调度问题对实时性要求不高,但如果要做日内滚动优化,建议用场景削减(比如用同步回代削减)把样本压到200以内,精度损失控制在5%以内。
3.4 从“写好模型”到“验证模型”:保守度分析
模型能跑通不等于结果可用。我强烈建议在输出了最优调度结果后,做一次独立的事后评估:把最优备用结果固定下来,拿一套全新的历史真实偏差数据做回测,统计备用不足的实际频率是否真的低于ε。
我自己的实验结果是这样的:取ε=0.1、Wasserstein半径ρ=0.2倍标准差时,新数据回测的备用不足频率在0.08左右,低于目标值,说明模型有一定保守性但不过度。当ρ取0.5倍标准差时,回测频率掉到0.03,代价是总成本上升6%。这说明半径参数的调节非常灵敏,建议做成一张ρ-成本-风险曲线,调度员可以拿这张曲线跟领导拍板定参数,比纯理论说服有用得多。
4. 调试经验与常见问题实录
4.1 Big-M的数值病态问题
这是我遇到的第一个大坑。第一版代码里M_big设了1e6,Cplex直接报numerical issues并且解出来的备用结果很奇怪——备用容量在某些时段莫名变成0,明显不合逻辑。
排查后发现,带大M的约束在求解器内部做了大量浮点运算,1e6和常规量级(几百)相差太大,导致预求解阶段就出现舍入误差。解决办法有两个:
- 把模型里所有物理量归一化到同一量级。我是把功率统一换算成MW、成本换算成
$/MWh,确保模型矩阵中非零元素量级都在1e-3到1e3之间。 - M_big本身也不宜过大,取最大偏差的2倍配合电纳值。这样Cplex数值稳定性明显好转,求解时间也从300秒降到40秒。
4.2 “Infesible problem”的定位法
模型遇到infeasible是最崩溃的,尤其是复杂模型里完全不知道哪个约束出了问题。我的排查流程是固定顺序的:
- 先把所有机会约束和整数变量去掉,只求解连续变量的能量调度问题,确认基础可行域不空。
- 加回备用联动约束,检查系统总备用上限是否大于备用需求均值。很多时候infeasible的根源是
ΣRmax < ReserveReq,物理上就没有可行解,怎么调都是白费。 - 再加机会约束,同时给
z_t一个松弛。此时如果infeasible消失,说明风险参数ε设得太严,调大一点即可;如果还infeasible,回到第2步查备用上限。
使用Yalmip的check函数可以逐条查看约束残差,快速定位是哪条约束卡住了。
4.3 Cplex求解慢的破解思路
混合整数规划求解速度慢,主要瓶颈是二元变量太多。我试验过三个有效提速手段。
- 固定机组启停状态:如果做日内滚动调度,上一时段的启停状态可作为本时段的初始解,用
x0设定初值之后,Cplex的热启动时间减少一半以上。 - 收紧Cplex的MIP容差:
cplex.mip.tolerances.mipgap从默认的0.0001放宽到0.01,成本误差大约0.5%,但求解时间从几分钟压到十几秒。 - 削减机会约束的样本数:用聚类方法把5000个场景削减到100个典型场景,联合机会约束的精度和求解时间的平衡最优。这个降阶方式不会让你的模型变得不可信,反而因为它去掉了冗余样本,数值稳定性更好。
4.4 Matlab运行环境相关的三个小问题
写Matlab代码过程中,也遇到过几个跟环境相关的琐碎问题,一并记下。
Matlab启动闪退的应用很常见,多数是许可证失效或加载器冲突,重装对应版本的许可证即可,别急着重装整个Matlab。
Yalmip在Matlab 2025b、2026b这类新版本下偶发函数命名冲突,表现为sdpvar变量无法定义,建议升级Yalmip到最新版或在安装时用yalmip('clear')清空缓存。
用movefile批量转移.mat数据文件时,若路径含中文和空格,建议先cd到目标目录,否则偶尔报权限错误,花了半天才定位到是路径问题。
4.5 参数标定的实战表格
最后整理一个我在实际项目中验证过的参数参考表。注意这是基于我的具体算例(6机组、24时段、2000场景),不同系统需要重新标定,但量级可作为起步参考。
| 参数 | 经验范围 | 对结果的影响 | 我的最终取值 |
|---|---|---|---|
| 联合风险ε | 0.03-0.15 | 越小备用越多、成本越高 | 0.10 |
| 单时段风险ε_t | 按偏差方差比例分配 | 分配不当会浪费备用容量 | 0.004-0.01 |
| Wasserstein半径ρ | 0.1-0.5倍标准差 | 越大越保守、回测风险越低 | 0.2倍标准差 |
| M_big | 最大偏差2-3倍 | 影响数值稳定性和求解时间 | 300MW |
| M_pen | 10-20倍边际备用成本 | 低于边际成本时约束松弛失效 | 5000 |
| 削减后样本数 | 100-200 | 越多精度越高、求解越慢 | 150 |
5. 扩展方向与个人体会
5.1 从单时段到多时段滚动调度
我目前做的是离线版本的联合机会约束调度,但实际运行中更常见的是40分钟滚动一次的多时段调度。多时段模型的核心扩展点有两个:一是机组爬坡约束跨时段耦合,二是机会约束要考虑预测时域内信息的序贯更新。
在Matlab里实现滚动调度的思路是:把模型封装成一个函数文件,输入当前预测状态和机组状态,输出本时段的调度指令;外层用一个for循环模拟一整天的滚动过程。注意每次滚动只执行第一个时段的指令,其余时段的计划只是“参考计划”,到下一个调度周期再重新优化。这个闭环回测方式能更真实地评估调度策略的鲁棒性,而不是只在离线数据上做静态验证。
5.2 和深度学习预测、OOP架构封装结合的实践思路
现在做调度的人都绕不开预测和代码工程化两个话题。预测方面,可以用LSTM或Transformer先给出未来4小时的负荷和风光出力预测,再把预测误差的残差分布作为分布鲁棒模糊集的输入,效果比我用的历史统计分布更好,因为它捕捉了"当前天气条件下的条件分布信息",模糊集半径也可以跟着缩窄。
代码架构方面,如果你的课题组内有多个人共用这套调度模型,我建议用Matlab OOP架构把模型重构成三个类:ScenarioGenerator负责数据处理和场景生成,DispatchOptimizer负责构建和求解优化模型,ResultAnalyzer负责回测和指标分析。这样换数据、换参数、换求解器都只需要改对应类的属性,不用动整体逻辑。这个重构本质上是把"研究者思路"变成"软件产品思路",在发论文阶段价值不大,但在工程项目里价值很高。
5.3 最后说点掏心窝的话
做这个课题最大的体会是:数学模型再漂亮,落不了地就是自嗨。分布鲁棒优化的很多文献写的模糊集极其复杂、证明极其完备,但实际调度员只会问你一句“这个参数跟以前比是贵了还是便宜了”。
我的建议是一定要拿出ρ-成本-风险曲线跟实际运行人员对表。他们不需要懂Wasserstein距离是什么,但一看“风险容忍度提一倍,成本涨多少”的曲线,马上就能做决策。这个角度能让你的研究成果从论文真正走向生产环境。
另外给新入门者的建议:不要一上来就啃完整的分布鲁棒对偶推导,先用我代码里的样本近似把联合机会约束跑起来,理解0-1指示变量在这里扮演的角色,然后用RSOME或自己推对偶做对比,模型间的偏差会让你对方法本质有更立体的认识。我当初跳过了这个对比步骤,导致理解Wasserstein对偶时卡了很久,回头看这就是最值得补的一课。