这半年“数据驱动+分布鲁棒”这个词在电力系统优化里热度确实高,期刊和学位论文里几乎到处都能看到类似工作。我第一眼看到“高热点算法!数据驱动+多离散场景分布鲁棒+电热综合能源系统优化(Matlab代码实现)”这个标题,就知道它大概在讲什么:两阶段分布式鲁棒优化,第一阶段定机组出力和热力计划,第二阶段应对风电和热负荷预测误差的“最坏情况分布”。用大白话说,就是从历史数据里生成一批可能出错的场景,把不确定性不只看成随机变量,而是当成一个“最不肯合作的对手”,同时用分布空间上的约束把这个对手限制在合理范围内。
如果你是想做综合能源系统方向的研究生,或者已经在做能源调度但对分布鲁棒优化还处于“听过名字没上手”阶段的工程师,这篇文章应该能帮上忙。我会把建模思路、数学变换、Matlab实现、调试踩坑一次讲清楚,尽量让新手也能照着自己写出来。
1. 为什么偏偏是“数据驱动+分布鲁棒”这条路
1.1 随机规划的“过于相信概率”
传统随机规划(SP)的做法是假设风电、热负荷预测误差服从某个已知分布,比如正态分布,然后按照这个分布采样生成几百个场景,目标函数取期望成本。这个思路本身没毛病,但前提是“你知道真实的误差分布长什么样”。实际工程里,风速的偏度、峰值形态随着季节和地理位置变化很大,你很难用一个正态分布去精确刻画。更麻烦的是,预测模型本身可能在更换时段后误差分布就变了,你基于旧分布采样出来的场景,对未来的代表性并不好。
我记得有一次用历史一年的风电数据做随机优化,结果在极端大风天气下,实际弃风量比模型预测的高了一倍。原因很简单——我采样的场景是从正态分布生成的,但真实误差有明显的厚尾特征,场景根本没覆盖到那个尾巴。
1.2 传统鲁棒优化的“过于悲观”
为了克服“分布假设不可靠”,传统鲁棒优化(RO)干脆不谈分布,只给不确定参数画一个区间,比如“风电出力在预测值的正负20%之间波动”,然后让方案在最坏组合下都可运行。这个思路的优点是绝对稳妥,缺点是代价极高。
做个对比就很直观:假设预测值是100 MW,区间鲁棒要求你在234567几个节点同时考虑最坏情况,最后算出来的机组组合往往要开更多高成本机组,弃风量也明显偏大。现场运行人员看了方案通常会问一句:“这种最坏情况一年能发生几次?每次都按这个备着,成本谁出?”这就是鲁棒优化在实际落地时最大的痛点——过度保守。
1.3 分布鲁棒正好卡在中间
分布鲁棒优化(DRO)没有完全拒绝概率,也没有完全依赖概率。它做的是:从历史数据里估计出一个“参考分布”,然后在所有与参考分布“距离不超过某半径”的分布集合里,取期望成本最大的那个分布来决策。这个“距离”通常用Wasserstein距离来定义,半径ε控制对手的自由度。
这个思想放在生活里特别好理解。随机规划像“天气预报说明天30%概率下雨,我按这个概率决定要不要带伞”;传统鲁棒像“不管天气预报告诉我什么,我都按大暴雨做准备”;分布鲁棒像“我看过这预报员过去两个月的记录,知道他平均误差有多大,那我就在他的预报基础上,假设误差可能达到某个上限,按最不利的情况做准备,但也不会离谱到假设明天刮台风”。
所以DRO的保守程度介于两者之间,而且它的保守程度可以通过半径ε来调节,工程上非常灵活。这也是这个方向能成为高热点算法的重要原因——它既尊重数据,又承认模型不完美,还给工程师留了一个调节旋钮。
下面用一个表格把三者的核心差异列清楚,方便对照理解:
| 方法 | 对概率分布的处理 | 决策目标 | 保守程度 | 工程可调性 |
|---|---|---|---|---|
| 随机规划 | 假设已知精确分布 | 期望成本最小 | 低 | 低,依赖分布假设 |
| 传统鲁棒 | 只给不确定集合 | 最坏情况下可行 | 高 | 中,只能调区间大小 |
| 分布鲁棒 | 用模糊集描述分布不确定性 | 最坏分布下期望成本最小 | 中 | 高,调半径ε |
2. 电热综合能源系统里到底在优化什么
2.1 系统架构和耦合设备
先明确研究对象:电热综合能源系统(IES)是电力系统与热力系统耦合在一起的能源网络。电侧有常规机组、风电场、电锅炉、储能;热侧有热电联产机组(CHP)、电锅炉、储热罐、热负荷。这里的核心耦合点是CHP机组和电锅炉,二者同时消耗或生产电与热,让电网和热网不再是两条孤立的线路。
优化调度要回答的问题是:未来24小时(或者其他调度周期),每一台CHP机组出多少电、多少热?电锅炉什么时候启动、功率多少?储热罐什么时候充、什么时候放?以及从上级电网买多少电?所有决策要在满足电、热负荷需求的前提下,让系统运行成本最低,同时处理好风电和热负荷的不确定性。
2.2 电网热网双平衡约束
模型里的硬约束分两部分。电网侧,每个时段的电功率平衡要满足:
电负荷 + 电锅炉耗电 = 常规机组出力 + CHP电出力 + 风电出力 + 购电 − 弃风
热网侧,每个时段的热功率平衡要满足:
热负荷 = CHP热出力 + 电锅炉热出力 + 储热罐放热 − 储热罐充热
除了平衡约束,每台设备还有自己的运行约束。CHP机组的电出力与热出力之间存在可行域限制,不是想发多少就发多少。抽气式CHP的可行域是一个凸多边形,源代码里通常用一组线性不等式来描述。电锅炉的输入输出是固定效率关系:热功率等于电功率乘以效率。储热罐有容量上下限、充放热速率限制,以及动态方程:当前时段的储热量等于上一时段的储热量,加上充电量,减去放电量,再扣除一小部分自然热损耗。
我做这个项目时为了聚焦算法验证,电网侧用的是简化功率平衡,没有引入复杂潮流方程。如果要把网架约束加进来,可以用线性化的直流潮流或者Distflow二阶锥形式,但那样求解规模会明显增加,代码结构也要调整。对于算法原理验证阶段,先抓住双平衡和设备约束就够了,网络细节可以后续再叠加。
2.3 目标函数与风险度量
目标函数首先是常规的运行成本:燃料成本、购电成本、弃风惩罚。燃料成本通常用CHP电出力的二次函数表示,在优化里可以分段线性化。购电成本就是分时电价乘购电量。弃风惩罚是为了避免模型随意牺牲风电。
在分布鲁棒这个框架下,目标函数不能只写成“期望成本最小”,而是要写成“在最坏分布下,第二阶段调整成本的期望加上第一阶段计划成本”。这里有个很常用的工具叫条件风险价值(CVaR),它衡量的是“尾部最差的那部分情景的平均损失”。把CVaR嵌入分布鲁棒模型,可以让决策者对极端场景更加警惕,又不至于像区间鲁棒那样把所有场景都按最坏情况处理。
我在实际代码里是把目标写成这样的结构:
总成本 = 第一阶段计划成本 + ε·λ + (1/N)·Σ s_i
其中ε是模糊集半径,λ是和Wasserstein距离相关的对偶变量,s_i是每个场景对应的辅助变量,代表该场景下“超出阈值的那部分调整成本”。这个形式看起来抽象,但Matlab里用YALMIP写起来并不复杂,下一节会展开说。
3. 多离散场景与模糊集:算法的灵魂
3.1 历史场景怎么来
所谓“多离散场景”,指的不是人为假设的场景,而是从历史数据里提取出来的预测误差样本。具体做法是:
采集一段时间内风电预测功率与实际功率的差值,以及热负荷预测值与实际值的差值,把每一天的误差序列作为一个历史样本。然后从历史样本里随机抽取(可以放回抽样),生成N个用于优化的场景。如果历史数据量足够大,抽500或1000个场景都很常见。
场景数量不是越多越好。每个场景都会给优化问题增加一组变量和约束,场景太多会让模型体量爆炸,求解时间成倍上升。实际项目里我常用200到300个场景,既能覆盖误差的主要分布形态,又不会让YALMIP建模时内存吃紧。如果你觉得场景代表性不够,可以用k-means聚类或者同步回代消除法做场景削减,削减后给每个场景重新分配概率权重,这样可以用少量场景逼近原始分布。
3.2 什么是Wasserstein球模糊集
有了经验分布P̂_N,也就是所有场景等概率组成的离散分布之后,我们在它周围画一个“球”,球内所有分布都被视为可能的真实分布。这个球就叫模糊集,用数学语言写:
B_ε(P̂_N) = { Q | W(Q, P̂_N) ≤ ε }
W是Wasserstein距离,本质上衡量的是“把一个分布搬运成另一个分布需要的最小成本”。两个分布差异越大,Wasserstein距离越大。半径ε就是允许真实分布偏离经验分布的限度。ε越大,决策就越保守,因为你把“最坏分布”的搜索范围放得更宽了。
这个思路为什么先进?因为它把“分布不确定”这个抽象概念变成了一个带半径的几何对象,工程师只要调ε就能控制鲁棒程度,完全不用重新建模。而且ε的选择可以和数据量挂钩,理论上可以给出置信水平的表达式,这在实践中有很强的解释性。
3.3 对偶变换与模型等价形式
原问题是个min-max问题:先选第一阶段的机组计划,再让自然界(或者市场)在模糊集内选一个最坏分布,使得期望调整成本最大。这种双层结构没法直接交给求解器,必须把它改写成单层优化。
这里用到的是Wasserstein分布鲁棒优化的强对偶定理。我不展开冗长的数学推导,直接给结论:在第二阶段成本关于不确定性满足一定光滑性条件时,原问题等价于一个单层线性规划,形式是:
min 第一阶段成本 + ε·λ + (1/N)·Σ s_i
约束条件里额外增加了两组:s_i ≥ 0,以及s_i ≥ 第二阶段调整成本(场景i) − λ。同时λ ≥ 0。
这个变换是代码实现的桥梁。你把min-max问题变成标准线性规划后,就可以直接用Gurobi或者Cplex求解。如果不做这个对偶变换,直接套数值迭代去找最坏分布,计算量会大一个数量级,而且收敛性也无法保证。
3.4 半径ε怎么标定
ε的选择是有讲究的,太小会让模糊集退化成一个点,结果基本等于随机规划;太大又会让模型过度保守,成本飙升。
我常用的办法是扫值法:先在0.001到0.1范围内按对数均匀取几个候选值,分别求解模型,观察总成本和弃风量的变化曲线。通常曲线会有一个“膝盖点”——超过这个点后成本上升明显加速,弃风量下降却开始变缓,那就选这个位置的ε。这个方法不需要复杂的统计公式,工程上很直接,审稿人一般也接受。
如果想更严谨,可以用历史数据做交叉验证:留出一部分历史样本作为验证集,看不同ε下决策在验证集上的实际期望成本,选表现最好的ε。这个思路更接近机器学习里的超参调优,需要额外写一段仿真代码,但结果更让人信服。
4. Matlab代码实现全过程拆解
4.1 主程序结构
我把代码组织成了六个模块,方便调试和复用:
- 参数初始化:设备参数、负荷曲线、预测误差数据、电价。
- 场景生成:从历史误差中抽取N个场景。
- 模糊集半径计算:扫值或按经验公式设定。
- 模型构建:定义决策变量、约束和目标,统一交给YALMIP。
- 求解与结果提取:调用求解器,把变量数值取出来。
- 后处理与画图:输出机组出力、热储能状态、总成本指标。
这种结构的好处是,以后换数据集或者换设备参数,只需改第一模块,模型构建部分基本不动。
4.2 核心数据准备
先加载历史误差数据并生成场景。假设风电误差矩阵是wind_err,每一行是一天的误差曲线,列数为调度时段数:
% 场景生成:从历史误差数据中放回抽样 load('wind_err.mat'); % wind_err: N_hist x T load('heat_err.mat'); % heat_err: N_hist x T N = 300; % 场景数 idx = randsample(size(wind_err,1), N, true); scen_wind = wind_err(idx, :); % N x T scen_heat = heat_err(idx, :); % N x T prob = ones(N,1) / N; % 等概率经验分布这里的T是调度时段数,如果做24小时日前调度,T=24,如果是96时段,T=96。场景生成后,建议先画几幅图检查一下场景的离散程度,别一上来就建模。我习惯把场景集合画成覆盖带图,能看到风电误差的取值范围,心里有数。
4.3 YALMIP建模核心
YALMIP是Matlab里最方便的优化建模工具箱,它让你用符号变量描述优化问题,然后自动转成求解器能识别的标准形式。建模第一步是定义决策变量:
% 第一阶段决策变量 P_chp = sdpvar(T, 1); % CHP电出力 H_chp = sdpvar(T, 1); % CHP热出力 P_eb = sdpvar(T, 1); % 电锅炉耗电 H_eb = sdpvar(T, 1); % 电锅炉产热 S_tank = sdpvar(T+1, 1); % 储热罐储热量 P_buy = sdpvar(T, 1); % 上级购电 delta_w = sdpvar(T, 1); % 弃风量 % 分布鲁棒辅助变量 lambda_w = sdpvar(1, 1); % 对偶变量λ s_aux = sdpvar(N, 1); % 场景辅助变量s_i变量定义好之后,写约束。我习惯把约束按模块组织,每一组约束用注释隔开,报错时容易定位。
Constraints = []; % CHP运行可行域(简化的抽气式机组) Constraints = [Constraints, P_chp >= 0]; Constraints = [Constraints, H_chp >= 0]; Constraints = [Constraints, P_chp <= P_chp_max]; Constraints = [Constraints, H_chp <= H_chp_max]; Constraints = [Constraints, P_chp >= 0.15 * H_chp + 5]; % 线性可行域约束 % 电锅炉效率关系 Constraints = [Constraints, H_eb == 0.98 * P_eb]; % 储热罐动态与容量约束 Constraints = [Constraints, S_tank(2:T+1) == S_tank(1:T) * 0.98 + H_eb(1:T) * 0.9 - H_eb(1:T) * 0.9]; % 示意,实际需分离充放热变量 Constraints = [Constraints, S_tank >= 0, S_tank <= S_max];这里我只写了示意性写法,真正工程代码里储热罐的充放热是分开的两个变量,否则会出现既充电又放电的退化情况。建议写成S(t+1) = S(t) * (1-μ) + η_ch * H_ch_tank - (1/η_dis) * H_dis_tank,并加约束H_ch_tank和H_dis_tank不能同时大于0。
然后是电功率平衡、热功率平衡,以及分布鲁棒部分的辅助约束:
% 电功率平衡(基态场景) Constraints = [Constraints, ... P_load + P_eb == P_chp + P_wind_forecast - delta_w + P_buy]; % 热功率平衡 Constraints = [Constraints, ... H_load == H_chp + H_eb + H_dis_tank - H_ch_tank]; % 分布鲁棒对偶约束 Constraints = [Constraints, lambda_w >= 0]; Constraints = [Constraints, s_aux >= 0]; for i = 1:N % 场景i下的功率不平衡量(第二阶段调整) delta_P = scen_wind(i,:)' - P_wind_forecast(1:T); % 风电偏差 imbalance = delta_P + delta_w; % 简化示意 % 该场景下的调整成本 c_bal = gamma * abs(imbalance); % gamma为切负荷/弃风惩罚单价 Constraints = [Constraints, s_aux(i) >= c_bal - lambda_w]; end注意这里for循环是在模型构建阶段执行,每循环一次往约束集合里加一条约束。如果N=300,就有300条约束,YALMIP完全能处理。
目标函数把常规成本和分布鲁棒项合在一起:
% 第一阶段成本 cost_fuel = sum(a .* P_chp.^2 + b .* P_chp + c); % 需要先分段线性化或使用二次规划 cost_buy = sum(price .* P_buy); cost_waste = sum(omega * delta_w); obj = cost_fuel + cost_buy + cost_waste + epsilon * lambda_w + (1/N) * sum(s_aux);如果求解器支持二次目标(Gurobi可以),CHP成本可以直接写成二次函数。如果不支持,就用分段线性化。我强烈建议能用线性就用线性,可以显著缩短求解时间,尤其是场景数大的时候。
4.4 求解配置与结果输出
YALMIP调用Gurobi只需要一行配置:
ops = sdpsettings('solver', 'gurobi', 'verbose', 1, 'dualize', 0); optimize(Constraints, obj, ops);求解完成后用value函数提取变量数值:
P_chp_val = value(P_chp); H_chp_val = value(H_chp); S_val = value(S_tank); obj_val = value(obj);我一般还会输出几个关键指标:总成本、购电总量、弃风量、储热罐最终剩余热量。这些指标可以直接放进对比表格,用来做后续的敏感性分析。
5. 调试记录:我踩过的五个坑
5.1 模糊集半径设成了0,模型退化成随机规划
有一次我为了测试对偶变换是否正确,把ε设成0,结果得到的方案和普通随机规划一模一样。后来想想这是对的:ε为0意味着模糊集只有一个点,那就是经验分布本身,model自然退化成期望优化。但反过来也说明,如果你画的成本-ε曲线没有明显变化,很可能不是ε不合适,而是你的场景生成有问题、场景方差太小,导致最坏分布和参考分布之间的差异本来就很小。
排查方法是:计算每个场景下第二阶段调整成本的标准差。如果标准差接近于0,说明场景没信息量,增大ε也没用。
5.2 辅助变量s_i漏了非负约束,目标值变负
分布鲁棒对偶变换里,s_i ≥ 0这条约束特别容易在写代码时遗漏。一旦漏掉,求解器可能给出一个非常夸张的负目标值,因为s_i可以任意取负来拉低目标。这种错误在YALMIP里不报错,直到你检查value(obj)才发现数值不对。
这类问题最好的排查方法不是人眼盯代码,而是把对偶变量和辅助变量单独打印出来检查。如果某个变量取值落在物理上不合理的范围,优先回查变量定义和非负约束。
5.3 热力管网约束写太细,模型直接解不动
我第一次尝试把热网管道温度、节点流量、回水温度全部建模,结果模型从线性规划变成了混合整数非线性规划,求解时间从几十秒变成了几小时。后来我把热网简化为节点热功率平衡加储热罐动态,问题规模立刻降下来,而且由于热力系统的慢动态特性,简化后的模型对调度结果的影响在可接受范围内。
做算法验证时,先跑简化模型;论文需要展示完整热网模型时,再逐步添加温度动态约束,并用迭代法处理非线性项,每加一类约束就重新测试一次求解时间。
5.4 场景数和时段数同时增大,内存爆了
当T=96、N=500时,YALMIP构建约束的时间明显变长,Matlab内存占用也会飙升。这种情况有两个对策。一是降低场景数到200,用聚类方法提高场景质量;二是利用YALMIP的批量建模功能,避免在constraint循环中创建大量临时变量。还有一个简单办法:把每个场景当成独立的子问题,用并行循环先算出各场景的目标函数表达式,再合并到主模型里。
5.5 储热罐初始值设置不当,首时段出现不可行解
储热罐动态约束里,S(1)一般取初始储热量。如果把它设为0,而热负荷又很重,模型可能在首时段找不到可行解。我习惯把初始储热设为最大容量的20%到30%,并在模型里加一个“最终储热量不低于初始值”的约束,这样调度结果不会为了省成本把储热全部放空,第二天还能继续运行。
这里列一个调试排查速查表,方便遇到问题时快速定位:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 目标值为负且不合理 | s_aux缺少非负约束 | 检查辅助变量定义 |
| 求解器报不可行 | 储热罐初始值过低或约束矛盾 | 调整初始储热,逐步注释约束定位 |
| 结果与随机规划完全一致 | ε太小或场景方差过小 | 增大ε,检查场景分布 |
| 求解时间爆炸 | 热网非线性约束或场景数过多 | 简化热网模型,削减场景数 |
6. 结果要怎么做对比才不浪费模型
6.1 三方法对比实验设计
论文或项目结题时,大家最关心的不是模型本身,而是“和随机规划比,我多花了多少钱?和传统鲁棒比,我省了多少钱?”。这个对比实验建议这样设计:
固定同一套历史数据和负荷曲线,分别跑随机规划、传统鲁棒、分布鲁棒三种模型。随机规划用经验分布求期望成本;传统鲁棒用区间盒式不确定集;分布鲁棒选择合适的ε。然后统计三个指标:总成本、弃风量、最坏情景成本。
正常情况下结果应该是:随机规划总成本最低但最坏情景成本最高;传统鲁棒最坏情景成本最低但总成本最高;分布鲁棒两个指标都居中。这种对比是审稿人和导师最容易买账的曲线,因为它直观地展示了DRO“用少量成本换稳健性”的特性。
6.2 敏感性分析不要只扫一个参数
我见过很多项目只扫ε一个参数,然后画一条成本曲线就结束了。其实更完整的敏感性分析应该包括:不同场景数量N下,DRO结果是否稳定;不同置信水平对应的ε选择;以及当历史数据量变少时,分布鲁棒的优越性是否更明显。
历史数据量对DRO的影响很有意思——数据越多,经验分布越接近真实分布,最优半径可以取得越小,DRO带来的额外成本也越低。这个结论可以在报告中重点说明,体现“数据驱动”的价值:数据质量高的时候,你就不需要过度防御。
6.3 后续扩展方向
做完这个基础版模型,有几个自然的扩展方向:加入碳捕集装置或者氢储能,形成电-热-氢多能耦合;把确定性网络约束换成机会约束;或者把DRO和强化学习结合,用在线数据不断更新经验分布,实现自适应调度。
我个人最推荐先加碳捕集方向,因为电热IES和碳捕集的耦合逻辑比较清晰,而且碳排放约束现在几乎成了必选项,加进去之后模型的实际应用价值立刻提升。
最后说点实操体会
这个项目我前前后后跑了三版代码,最大的体会是:分布鲁棒优化的门槛不在数学推导,而在于“模型简化与场景构造的平衡”。你对系统建模越细致、场景生成越真实,结果越可靠,但代价是求解时间和调试难度快速上涨。工程上合适的做法是从简化模型开始,先跑通闭环,确认DRO对偶变换实现正确,再逐步加约束、加细节。
调试阶段可以用小规模数据做试验:取24个时段、50个场景,看结果是否合乎直觉,然后再放大到完整数据规模。这样能省大量时间,也更容易发现模型结构上的问题。最后再分享一个小细节:YALMIP里把约束分组并用注释分隔,报错时能快速定位到具体模块。这看起来是无足轻重的小习惯,但当你面对上千条约束的时候,它真的能救命。