每年供暖季一过,做电力调度和新能源消纳项目的人基本上都要忙活一阵。原因很简单:热电厂一旦背上供热任务,发电曲线就被“绑架”了,风电来了也送不出去,只能眼睁睁看着弃风。最近我把一套“风电最大化消纳的热电联产机组联合优化控制”完整跑了下来,从模型搭建到Matlab代码实现再到多场景对比,踩了不少坑,也攒了不少经验。这篇就围绕这个项目,把整套思路、模型细节、代码要点和调试心得一次讲清楚,给正在做电力系统优化调度、热电联产或者新能源消纳方向的朋友一个可以直接参考的底稿。
这个项目解决的是什么问题呢?一句话概括就是在保证热负荷供需平衡的前提下,通过协调火电机组、热电联产机组、储热罐和电锅炉,最大限度提高风电并网功率、降低弃风率。适合的人群很明确:电气工程和能源动力方向的研究生、做电网调度或综合能源系统规划的工程师,以及想快速上手Yalmip建模做优化调度的同学。内容不追求顶刊级别那么复杂的模型,而是先把主干逻辑打通,让你能跑通结果、看懂输出、知道接下来往哪个方向加复杂度。
1. 项目概述与整体思路
1.1 “以热定电”到底卡在哪里
国内“三北”地区的供暖期长,热电厂承担着主要的供热任务。传统热电联产机组运行时,电出力和热出力之间存在强耦合关系,简单说就是你让它多发点热,它必须跟着多发点电,这个电没法“只发一部分”。这就导致一个问题:冬季夜间热负荷依然很大,机组为了供热被迫维持较高电出力,而此时电网负荷恰恰处于低谷,风电又在夜间往往是大发时段,风电上网空间被高电出力的热电机组挤占,电网只能选择弃风。这就是业内常说的“以热定电”困局。
很多人第一次接触这个概念时不太理解,我常用一个类比来解释:热电联产机组就像一个既要做饭又要发传单的兼职员工,他接了一个做100份盒饭的活,就必须同时跑出去发对应数量的传单,没得商量。风电则像一个只发传单不干活的人,想多干点活,但兼职员工已经占了太多发传单的份额,只能闲着。所以问题的根源不在风电本身,而在热电耦合约束。
这种强耦合会让电网的可调空间变得非常小。常规火电可以压低出力到额定容量的40%左右,部分深度调峰机组能压到30%,但热电机组在供热期最小电出力往往被抬到额定容量的60%到70%。你在调度台前看着风电曲线往上涨,热电机组的最小技术出力却纹丝不动,那种无力感做过调度的人应该都懂。
1.2 打破热电耦合的三条主流路线
要想把风电消纳空间撑开,工程上主要有三种手段,思路都是直接或间接“解耦”热电关系。
第一种是加储热罐。储热罐的作用相当于一个热量缓冲池,在热负荷较低的时段,让热电机组多发一些热存进罐里,同时多发一些电来给风电让路;在热负荷较高的时段,再从罐里放热,减轻机组供热压力。这么一来,机组的电出力和热负荷就不需要时时绑定了,灵活性大大增加。
第二种是上电锅炉。电锅炉直接消耗电能转化为热能,它提供了一条全新的“电转热”通道。风电大发、电网难以消纳时,把多余风电引到电锅炉,电变成热,热再进入热网或储热系统,相当于给风电找了一个消纳出口。
第三种是火电深度调峰。通过改造机组燃烧系统或运行方式,把常规火电机组的最小技术出力压得更低,从根源上腾出空间,但设备改造周期长、投资大,而且对机组寿命有一定影响。
这个项目选择的是“储热罐+电锅炉”联合方案,也是目前工程上性价比最高、实施难度相对较低的配置。比较关键的地方在于,这两者不是各干各的,而是需要在一个优化框架下统一调度,否则容易出现储热罐在放热、电锅炉也在发热的“打架”局面。这个统一调度的任务,就是这套联合优化控制模型要解决的问题。
1.3 整体方案架构
这个项目的整体架构可以分三层理解。
数据层负责输入一整天的预测数据,包括电负荷预测曲线、热负荷预测曲线、风电出力预测曲线,以及机组参数,如额定容量、爬坡速率、热效率、储热罐容量和充放热功率上限等。
模型层是核心,用Matlab结合Yalmip工具箱搭建混合整数线性规划模型,决策变量涵盖常规火电机组电出力、热电联产机组电出力和热出力、储热罐充放热功率、电锅炉消耗电功率、风电机组实际并网功率,以及部分机组的启停状态0-1变量。
输出层则输出各机组出力计划、储热罐调度策略、电锅炉运行计划、弃风率以及总运行成本等结果,并用图的形式直观呈现风电消纳效果。
整个方案最核心的设计哲学是:不改变物理设备结构,只靠优化调度策略,就能显著提升风电消纳空间。这一点在实际工程中非常有吸引力——调度系统改造比设备改造便宜太多了。
2. 优化模型的搭建:目标函数与约束条件
2.1 目标函数:弃风惩罚与经济性怎么权衡
建模的第一步是确定目标函数。这个项目的目标当然是最大化风电消纳,但直接写“最大化风电并网功率”在数学上会带来一个不太好处理的问题:目标函数和约束条件之间的主导关系不够清晰,而且完全不计成本地消纳风电可能导致系统运行经济性极差。
实际建模中更常用的做法是把风电消纳目标通过“弃风惩罚成本”的形式放进目标函数。也就是说,目标函数由两部分组成:系统运行成本和弃风惩罚成本。系统运行成本包括常规火电机组和热电联产机组的煤耗成本、启停成本和电锅炉购电成本;弃风惩罚成本则是理论可用风电功率与实际并网功率之差的线性函数,乘上一个惩罚系数。
这样处理的好处一是数学表达简洁,弃风惩罚项是线性的,可以直接嵌入混合整数线性规划框架;二是权重可调,把惩罚系数设得足够大,优化器就会优先消纳风电,而在风电不足或电网无法消纳时,又能自动在成本和消纳之间做一个平衡。
>> 提示:惩罚系数的取值直接影响结果。我测试的时候发现,惩罚系数设为煤耗单价的5到10倍时,风电消纳效果最好且不会出现因为过度追求消纳导致成本异常攀升的情况。系数太小则模型会“觉得”弃风比调机组更划算,系数过大则可能出现某些极端工况下调度方案不够平滑。
2.2 核心约束条件逐条拆解
约束条件是模型的骨架,任何一条写错都可能导致无解或结果不合理。我把这个项目里最关键的几条约束逐一说明。
电功率平衡约束是最基础的等式约束,指任意时刻所有电源出力之和必须等于电负荷与电锅炉耗电功率之和。注意这里电锅炉作为一个用电负荷出现在等式右侧,这是电锅炉参与风电消纳的关键机制。
热功率平衡约束同样重要,指任意时刻热电机组热出力加上储热罐放热功率减去充热功率,再加上电锅炉产热功率,必须等于热负荷。储热罐的充放热状态在这里体现出解耦作用——热量不再是“即产即用”,有了存储空间。
热电联产机组耦合约束是这个模型的灵魂,描述机组电出力和热出力之间的可行域。严格来说,抽汽式热电联产机组的可行域是一个由多个顶点围成的多边形区域,工程上常常对这个多边形做线性化处理。简化处理时常用两条不等式描述:电出力大于等于某一条关于热出力的线性下限,小于等于某一条关于热出力的线性上限。
风电出力约束表示实际并网功率不得超过预测可用功率,同时不能小于零。这个约束看似简单,却是弃风量计算的依据。
常规火电机组约束包括出力上下限约束和爬坡约束,爬坡约束描述机组在相邻两个时段之间电出力变化量的上限。热电联产机组同样需要爬坡约束,这里项目简化处理为只约束电出力的爬坡,工程中更精细的模型还可以考虑热出力爬坡。
储热罐约束包括充放热功率上限、储热罐储热量上下限和储热量动态平衡方程,即上一时段储热量加上本时段充热量减去放热量等于当前时段储热量。很多人容易漏掉的是储热罐不能同时充热和放热,这条约束可以用一个0-1变量配合大M法实现,也可以用两个非负变量加一个互补约束的线性化形式实现。
电锅炉约束比较简单,电锅炉消耗电功率乘以制热效率等于产热功率,功率有上下限,且锅炉有启停状态0-1变量,停机时功率为零。
2.3 求解器选型:为什么用混合整数线性规划
整套模型中的0-1变量主要来自机组启停状态和储热罐充放热状态互斥条件,这让模型成为一个混合整数线性规划问题。MILP模型在Matlab里有好几种求解路径:直接用内置的intlinprog、用Yalmip调用外部求解器,或者用国产求解器。
我强烈建议用Yalmip+Rm求解器(Cplex或Gurobi)的组合。原因很简单:Yalmip把Matlab和求解器之间的接口封装得很干净,你不需要记忆Cplex底层的复杂调用接口,只需用标准语法定义变量、目标函数和约束,最后一句话就能调用求解器。Cplex和Gurobi求解中小规模的MILP问题性能很好,像这里24个时段、几十个变量的问题,求解时间基本在几秒以内。
时间尺度的选择也很关键。这个项目采用24小时调度周期、1小时为时间步长,共24个时段。如果你想看到更细的波动效果,可以改成15分钟一个时段,即96个时段,模型规模会相应扩大,但求解器依然能够胜任。
3. Matlab代码实现与关键细节
3.1 数据准备:仿真场景怎么搭
仿真数据的质量直接决定结果的可靠性,很多第一次做这个项目的人上来就写代码,结果输出一堆完全不符合物理规律的数据,回头查半天才发现问题出在输入数据上。
我建议先把数据整理成结构体或表格,便于统一管理。典型的数据包括电负荷数据、热负荷数据、风电预测出力数据以及各类机组参数。表1给出一组示例数据,方便对照验证。
| 参数名称 | 数值 | 单位 |
|---|---|---|
| 常规火电机组额定功率 | 300 | MW |
| 常规火电机组最小出力 | 150 | MW |
| 热电联产机组额定电功率 | 200 | MW |
| 热电联产机组最小电出力 | 100 | MW |
| 热电联产机组最大热出力 | 150 | MWth |
| 储热罐容量 | 300 | MWth |
| 储热罐最大充放热功率 | 50 | MWth/h |
| 电锅炉额定功率 | 40 | MW |
| 电锅炉制热效率 | 0.95 | - |
| 风电装机容量 | 300 | MW |
风电场数据建议实测曲线或标准测试系统数据。如果手头没有实测数据,可以用一个正弦基函数叠加随机波动来生成,但注意风电夜间大发的特征要保留,否则很难体现消纳矛盾。
电负荷和热负荷曲线要有典型日特征:电负荷通常在上午和傍晚出现两个高峰,热负荷则比较平缓且夜间略高。数据做好后先画出来看一眼,确认符合基本物理规律再进模型。
3.2 Yalmip建模核心代码
建模部分我直接给出核心代码骨架,结构是标准的Yalmip三步走:定义变量、写目标函数、加约束、调用求解器。
% 定义变量 P_c = sdpvar(1, T); % 常规火电出力 P_chp = sdpvar(1, T); % 热电联产电出力 H_chp = sdpvar(1, T); % 热电联产热出力 P_w = sdpvar(1, T); % 风电实际并网功率 P_eb = sdpvar(1, T); % 电锅炉耗电功率 H_eb = sdpvar(1, T); % 电锅炉产热功率 Q_s = sdpvar(1, T); % 储热罐储热量 H_ch = sdpvar(1, T); % 储热罐充热功率 H_dis = sdpvar(1, T); % 储热罐放热功率 u_chp = binvar(1, T); % 储热罐充放热状态标志目标函数的构建是整个建模过程里最能体现设计思想的部分。煤耗成本用出力的二次函数描述,这个项目为了兼容线性规划框架,做了分段线性化处理。当然,如果你手头的求解器支持二次规划,也可以用quadprog直接求解二次成本函数,精度更高。弃风惩罚项则用预测可用风电功率减去实际并网功率来计算。
% 目标函数:运行成本 + 弃风惩罚 coal_cost = sum(a1 * P_c.^2 + b1 * P_c + c1); chp_cost = sum(a2 * P_chp.^2 + b2 * P_chp + c2); curtail_penalty = lambda * sum(P_w_pred - P_w); objective = coal_cost + chp_cost + curtail_penalty;约束条件的添加是整个程序中代码量最大、最容易出错的部分。以电功率平衡约束为例,注意把电锅炉耗电功率放在负荷侧,这是很多人容易忽略的细节。热功率平衡约束则要体现储热罐的充放热状态和电锅炉的产热贡献。
constraints = []; % 电功率平衡:发电侧 = 负荷侧(电负荷 + 电锅炉) for t = 1:T constraints = [constraints, P_c(t) + P_chp(t) + P_w(t) == P_load(t) + P_eb(t)]; end % 热功率平衡:热源侧(热电机组 + 储热放热 + 电锅炉) = 热负荷 + 储热充热 for t = 1:T constraints = [constraints, H_chp(t) + H_dis(t) + H_eb(t) == H_load(t) + H_ch(t)]; end热电联产机组的电热耦合约束是这个项目里最需要深入理解的部分。在简化模型里,可以用两条线性不等式来描述电出力随热出力变化的范围,再加上电出力和热出力各自的上下限。实现时我用矩阵形式一次性添加整段时段的约束,这样代码更简洁,求解效率也更高。
储热罐的动态平衡约束要特别注意时序关系,即下一时段的储热量由当前时段储热量、本时段充热量和放热量共同决定,这是连接各时段的关键纽带,写错会导致整条储热曲线失真。
% 储热罐动态约束 for t = 2:T constraints = [constraints, Q_s(t) == Q_s(t-1) + H_ch(t) * eta_ch - H_dis(t) / eta_dis]; end求解调用的写法非常简单,注意设置求解器的输出参数以便后续判断求解状态和提取结果。这个项目用Cplex作为求解器,Yalmip会自动识别。求解完成后记得检查求解状态是否为0,即成功求解,这非常重要。
3.3 结果可视化:把消纳效果画出来
跑完模型之后,可视化是让结果“看得见”的关键一步。我通常画三张图:第一张是电功率平衡图,用堆叠面积图展示各类电源出力构成,能直观看出风电在不同时段的消纳情况;第二张是热功率平衡图,重点显示储热罐的充放热过程;第三张是风电消纳对比图,把预测可用风电和实际并网风电画在一起,两条曲线中间的面积就是弃风量。
figure; area([P_c; P_chp; P_w]'); legend('常规火电', '热电联产', '风电'); xlabel('时段/h'); ylabel('功率/MW');画图时有个技巧:把电负荷曲线也画在同一张图上做参考,可以更清楚地看到风电挤占负荷空间的情况。另外,弃风率可以直接作为分析结论,在实际项目汇报和论文写作中,弃风率通常是评审最关心的指标,我只在代码里把弃风率计算出来,公式就是1减去实际并网电量与预测可用电量之比,乘以百分之百。
4. 三组仿真场景的对比分析
4.1 场景一:无解耦措施(基准场景)
第一个场景不启用储热罐和电锅炉,相当于传统“以热定电”的基准工况。这种方式把热电联产机组的热出力直接等同于热负荷需求,热出力曲线是一条跟随热负荷变化的平滑曲线,机组的电出力则被热出力牢牢锁定,几乎没有下调空间。此时如果风电预测出力在夜间达到较高水平,超出系统消纳能力的那部分只能被舍弃。
仿真结果中基准场景的弃风率通常在20%到30%之间,具体数值取决于风电装机容量和热负荷水平。电功率平衡图上能明显看到夜间时段风电出力曲线被“削平”,实际并网功率始终维持在一个较低水平,而常规火电却仍在较高出力区间运行,整个系统呈现出“低谷时段火电不减、风电被弃”的不合理状态。
这里反映出的问题很有代表性:即便火电已经压到了最小技术出力,热电机组的最小电出力依然过高,导致系统整体下调能力不足。这也验证了题目里说的“风电最大化消纳”为什么必须是“联合优化”——单靠火电调整根本解决不了热电机组的耦合约束问题。
4.2 场景二:仅配置储热罐
第二个场景启用储热罐但不启用电锅炉。优化模型开始发挥作用,储热罐在白天热负荷相对较低、电价相对便宜的时段充热,在夜间供热需求高或风电大发时段放热,从而允许热电机组在夜间降低热出力、进而降低电出力,为风电腾出并网空间。
仿真结果显示,弃风率从基准场景的20%到30%下降到10%到15%左右。储热罐的储热量曲线呈现出明显的“白天蓄能、夜间释能”特征,热电机组在夜间的最小电出力有明显下降。这说明储热罐单独作用时,已经能有效缓解热电机组对风电空间的挤压。
不过储热罐也有它的局限性。它的本质是“平移热量”,并不能凭空创造风电消纳空间,只能把热电机组白天多发的那部分热存起来留到夜间放。当夜间热负荷特别高、风电同时大发时,储热罐容量有限,放热速度也受功率上限约束,消纳效果会遇到瓶颈。
4.3 场景三:储热罐与电锅炉联合优化
第三个场景同时启用储热罐和电锅炉,也是这个项目要呈现的完整方案。电锅炉的引入让系统在夜间出现一个“主动用电负荷”,风电大发时可以直接把电引到电锅炉变成热,为风电开辟了一条全新消纳通道,效果立竿见影。
联合优化下的结果改善非常明显。典型日的弃风率可以降到5%到8%,某些风电资源较好、热负荷适中的日子甚至可以实现零弃风。电功率平衡图上,夜间时段电锅炉的耗电功率曲线与风电出力曲线有明显的联动关系,风电出力越高,电锅炉耗电越大。电锅炉的功率上限决定它的消纳“天花板”,如果风电场容量很大,单台电锅炉可能不够用,需要进一步扩大电锅炉容量或增加储热容量。
| 对比维度 | 基准场景 | 储热罐 | 储热罐+电锅炉 |
|---|---|---|---|
| 弃风率 | 20%-30% | 10%-15% | 5%-8% |
| 系统运行成本 | 中 | 略降 | 略升 |
| 热电机组灵活性 | 无 | 中 | 高 |
| 对风电跟踪能力 | 弱 | 中 | 强 |
运行成本方面,储热罐方案因为优化空间集中在“少烧煤、多存热”上,成本相比基准场景略有下降;电锅炉方案虽然消耗了电能,看起来“多花了钱”,但考虑到这部分电能是弃风电力,边际成本极低,实际系统总体经济性反而更优。这在项目结果分析时要想明白,否则很容易被表面成本数据误导。
4.4 多场景分析带来的结论
三组场景跑完之后,结论其实已经很清晰了。储热罐解决问题的方式是“平移”,它在时间维度上转移热量,通过改变热电机组的运行点来腾出空间,效果受限于储热容量和充放热功率;电锅炉解决问题的方式是“增量”,它在空间维度上新增了一条电转热的消纳通道,效果受限于电锅炉容量和电网能够承受的额外用电负荷。二者联合起来,时间维度上的灵活性和空间维度上的消纳能力形成互补,风电最大化消纳的目标才真正落地。
这个结论不是我拍脑袋想出来的,而是我从三组场景的数值对比里看出来的。所以我也建议你自己跑完项目之后,不要只看最终弃风率,把场景二和场景三的储热曲线、电锅炉曲线放在一起对比看,你会有更直观的感受。
5. 常见问题与排查心得
5.1 模型报无可行解,先别急着调求解器
第一次跑这个模型时,我花了很多时间在排查“无可行解”的问题上。后来发现,无可行解90%以上来自约束条件自相矛盾,而不是求解器或代码语法的问题。常见的原因主要有三个。
热功率平衡约束写反了充放热方向。储热罐放热是增加供给侧,充热是增加需求侧,如果方向搞反,会让模型在物理上不可能满足平衡,直接导致无可行解。排查时把约束打印出来人工核对一次,问题很快就暴露了。
储热罐的初始储热量和末端储热量衔接不当。我习惯加上一个循环约束,让调度周期末的储热量等于初始储热量,这样模型才有长期可持续性。但如果初始储热量设置得过高,而储热罐容量又有限,模型可能找不到任何一条可行路径,这时候把初始储热量调低或放宽末端约束就行。
机组组合中的0-1变量和出力上下限约束冲突。比如某台机组在某个时段被要求停机,但另一条约束又强制它的出力大于零,这就会产生矛盾。这个问题在大型系统里很隐蔽,排查时可以用Yalmip的diagnostic功能定位涉嫌约束冲突的变量和约束序号。
5.2 结果合理但“不好看”的排查方向
有时模型能解出来,结果也满足约束,但看起来就是不符合物理直觉。我遇到过一个典型情况:电锅炉在风电出力很小的时候也在运行,明显不对,看起来很不合理。排查之后发现根本不是模型的问题,而是电锅炉的启停0-1变量没有被定义,把它当成了连续变量处理,优化器为了让目标函数更小,会让电锅炉在小功率运行和完全停机之间“取一个折中”的值,这个值在物理上没有任何意义。
解决方法很简单,电锅炉必须用0-1变量控制启停状态,停机时功率强制为零。另外,储热罐的充放热互斥约束也容易忽略,如果不加互斥约束,模型可能出现同一时段既充热又放热的“循环倒热”现象,这在数学上是个可行解,但物理上是完全错误的。排查时检查储热罐的充放热功率曲线是否存在同时大于零的时段即可。
5.3 求解时间变长和收敛异常
中小规模的MILP问题求解很快,但如果你把时间粒度细化到15分钟、场景扩展到多日连续调度,或者给机组增加启停成本等复杂约束,模型规模会急剧扩大,求解时间可能从几秒暴涨到几分钟甚至更久。
遇到这种情况,建议先检查模型中整数变量的数量,MILP的求解时间很大程度上取决于0-1变量的规模。其次可以调整求解器的MIP gap容差,Cplex默认的绝对gap很小,你可以在求解时设置相对gap,比如1%,这样求解速度会明显提升,而结果精度依然在可接受范围内。再一个技巧是给变量设置合理的初始值,减少分支定界搜索的初段计算量。
这个项目我用下来,最深的体会是数据驱动模型调试。每次修改模型或参数之后,不要只看最终目标函数值,而是把每一类最优解都画出来,用眼睛检查一遍。模型可以帮你快速找到最优解,但最优解是否物理上合理,最终还得靠人的判断。
最后再分享一个小经验。Matlab和Yalmip的环境配置是很多新手第一道坎,建议直接用较新版本的Matlab,安装Yalmip和Cplex或Gurobi之后,先跑一个简单的线性规划例程验证环境是否正常,再启动项目代码。另外,所有输入数据统一用标准单位(MW、MWh),标幺值虽然可以简化计算,但在前期调试时不容易发现量纲错误,还是用有名值更稳妥。调度优化这个东西,模型和代码各占一半,把数据管好了,后面就顺了。