做电力系统优化调度的朋友,对“热电联产机组”这几个字应该深有体会。尤其北方供暖期,热电机组既要扛供热、又要参与发电,运行方式被“以热定电”四个字绑得死死的;而风电偏偏夜间反调峰,热电机组压不下去、风就上不来,弃风率蹭蹭往上涨。我做新能源消纳方向的建模与仿真有一段时间了,今天把“风电最大化消纳的热电联产机组联合优化控制”这个项目的核心思路完整拆开来讲——从背后的物理问题到数学模型,再到Matlab代码实现、求解器选型、调试经验,全部摊开。这篇内容适合正在做电力系统课程设计、毕业设计,或者刚接手新能源调度算法研究的同学;也适合已经能跑通代码但总被“不可行”“结果不合理”折磨的人,文里说的这些坑,大概率你也踩过。
1. 问题解剖:热电联产与风电消纳的矛盾根源
1.1 为什么冬季弃风如此顽固
冬季弃风不是偶然现象,而是一个结构性矛盾。先说风电自身的脾气:风电出力受天气驱动,夜晚往往是风速最大的时段,但夜间恰恰是全社会用电负荷的低谷。这样一来,风电大发时段系统最缺的是“向下调峰能力”——也就是把常规机组出力压低、给风电让出空间的能力。
再看系统里其他成员。纯凝火电机组有最小技术出力,比如一台600MW的机组最小稳定运行出力可能在240MW左右,可以往下压不少;但热电联产机组不行,它不仅要发电,还要向热网供热。供热需求在冬季夜间同样处于高位,为了满足热负荷,机组电出力被抬高到一个较高的下限水平,几乎压不下去。系统里热电机组占比越高,整体调峰能力就越差,风电被迫弃掉的部分就越多。
我用一个简单的数量关系帮你建立直觉。假设夜间电负荷是800MW,热负荷折算成等效电功率是500MW,系统里有两台抽汽式热电机组,为了满足热负荷,它们的电出力最低也得维持在400MW以上;再加上一台最小技术出力200MW的纯凝机组,系统最低出力已经到600MW以上。800MW的负荷留给风电的消纳空间只有不到200MW,而风电夜间可能发到300MW甚至更多,超出部分只能弃掉。这就是冬季弃风的典型场景。
1.2 热电机组的“电-热耦合”特性
要理解联合优化为什么有效,先得把热电机组的工作原理讲透。以最常见的抽汽式汽轮机组为例,它从汽轮机中段抽出部分蒸汽去加热热网水,这部分抽汽不再进入低压缸做功发电,所以机组的电出力和热出力之间存在强烈的耦合关系。
这种耦合在数学上表现为一个可行域:给定一个热出力,电出力只能在某个区间内取值。多个文献里用了不同的线性化描述方式,但本质都是一样的——电出力上限随热出力增加而下降,因为抽走的蒸汽多了、做功的蒸汽就少了;电出力下限随热出力增加而上升,因为为了保证供热,机组必须维持一个最低的蒸汽流量,这部分流量对应的发电功率也随之增加。两个边界一夹,就形成了所谓的“热-电可行域”。
背压式机组就更特殊了,它的电出力和热出力基本成固定比例,没有独立调节空间,发电完全由供热需求决定。这类机组冬季几乎等同于一个按照热负荷变化同步发电的装置,系统里如果背压机组占比高,调峰压力会更大。这也是为什么很多优化研究中要专门引入“热电解耦”手段——储热罐、电锅炉、热网蓄热——目的就是把强耦合变成弱耦合。
1.3 联合优化控制到底优化了什么
既然问题出在“以热定电”,那思路就很清晰了:能不能在满足供热需求的前提下,通过多台机组、多类设备的协调,把电出力整体压下去、给风电腾出空间?
这就是联合优化控制的核心。它和传统经济调度最大的区别在于:传统调度把热负荷当作给定值、直接折算成各台CHP机组的电出力下限;联合优化则把热负荷的分配也纳入决策——哪台机组承担多少供热、储热罐什么时候充放、电锅炉要不要投入,这些都由优化算法统一决定。
一句话概括:联合优化控制就是在满足电力平衡、热力平衡、机组运行约束的前提下,通过协调多台CHP机组的热电出力以及储能、电锅炉等灵活性资源,实现弃风最小化、运行成本最小化的综合目标。它解决的不只是“每台机组发多少电”的问题,而是“电、热两条平衡链如何协同”的问题。
2. 数学模型:把调度问题翻译成机器能懂的约束
2.1 目标函数:如何表达“最大化消纳”
先聊目标函数。很多初学者一上来就写“弃风电量最小”,但实际上更常见的做法是把弃风用高惩罚系数计入运行成本,让优化器在成本驱动下自动提高风电消纳。这样写的好处是:当弃风惩罚足够高时,优化结果在数值上等价于弃风最小;同时目标函数里还能包含煤耗成本、启停成本、电锅炉耗电成本等,得到一个更接近工程实际的总成本指标。
我常用的目标函数这么写:
min Σ_t [ Σ_i (a_i * P_i,t^2 + b_i * P_i,t + c_i) + ρ_pen * (P_w_pre,t - P_w,t) ]其中第一项是热电机组和纯凝机组的煤耗成本,通常用二次函数近似;第二项是弃风惩罚项,P_w_pre是风电预测功率,P_w是实际并网功率,二者之差就是弃风量。ρ_pen的取值很关键,我一般取正常煤耗成本系数的10到20倍,确保优化器优先消纳风电而不是为了省几块钱煤费故意压低风电。
这里补充一个容易忽略的细节:如果你做的是纯线性模型(比如用linprog),二次煤耗项需要分段线性化。YALMIP里可以直接写二次目标然后用能处理二次规划的商业求解器,像Gurobi和CPLEX都原生支持,这个我后面在求解器部分再细说。
2.2 热电机组可行域约束
热电机组的建模是整个问题的核心,也是最容易写错的地方。对于抽汽式机组,我采用线性不等式来描述它的热-电可行域:
0 ≤ Q_i,t ≤ Q_i,max P_i,t ≥ P_i,min + k_i,1 * Q_i,t P_i,t ≤ P_i,max - k_i,2 * Q_i,t第一条是热出力上下限;第二条和第三条分别是电出力下边界和上边界,斜率k_i,1和k_i,2表示热电耦合强度。k_i,1越大,意味着增加单位供热量必须同步增加多少电出力,机组调峰能力越差。
需要注意这是简化形式。更精确的做法是采用可行域顶点描述法——把机组的热电运行区域写成若干个顶点的凸组合,然后通过顶点权重变量来约束P和Q。这种写法麻烦一些,但能更真实地反映机组在部分抽汽工况下的非线性特性。实际做项目时,如果手上没有厂家提供的详细运行数据,就用线性不等式近似;如果要做高精度分析,再考虑顶点法。我在课程设计和毕设里通常先用简化形式把主流程跑通,再逐步细化。
2.3 系统平衡与备用约束
平衡约束是任何调度模型不能出错的底线。电力平衡约束写成:
Σ_i P_i,t + P_w,t - P_eb,t = P_load,t注意电锅炉在这里是作为负荷出现的,它的用电功率P_eb要放在等式左侧做减法。很多初学者容易把这部分漏掉,导致电平衡永远对不上。
热力平衡约束写成:
Σ_i Q_i,t + Q_s,t + Q_eb,t = H_load,t其中Q_s是储热罐的放热功率(放热为正、充热为负),Q_eb是电锅炉的供热功率,单位要统一为MW(或者GJ/h)。
除了平衡约束,工程上还需要考虑旋转备用约束。简单处理的话,可以要求常规机组在任意时刻满足:
Σ_i P_i,max - 系统负荷 ≥ 上备用需求 系统负荷(扣风电) - Σ_i P_i,min ≥ 下备用需求不过很多课程设计不要求包含备用约束,加入后模型更容易不可行,调试复杂度也会上升。我的建议是:如果题目没有明确要求,第一版代码先不加,把核心调度逻辑跑通后再作为扩展功能加进去。
2.4 储热与电锅炉建模要点
储热罐和电锅炉是提高风电消纳的关键设备,建模并不复杂,但细节多。
储热罐用能量状态方程描述:
S_t+1 = S_t - Q_s,t * Δt - loss_t 0 ≤ S_t ≤ S_max -S_disch,max ≤ Q_s,t ≤ S_ch,maxS_t是储热罐在时段t结束时的储热量,Q_s,t对应当前时段的放热(正)或充热(负),loss_t是散热损失,通常可以简化成一个固定比例。这里要特别注意ΔT的单位换算:如果时间间隔是1小时,功率MW乘以1小时就是能量MWh;如果时间间隔是15分钟,要乘以0.25。
电锅炉更简单,它把电能转换成热能,供热功率和耗电功率之间有个效率系数:
Q_eb,t = η_eb * P_eb,tη_eb一般取0.95到0.98。电锅炉的价值在于:风电大发时段,与其弃风,不如把这部分电能转化成热能,既满足了热负荷,又给风电让出了上网空间。这个思路在工程上叫“以电供热、以热促风”,是北方地区提升风电消纳的重要技术路径。
3. Matlab实现:建模、求解与代码框架
3.1 求解器选型与建模环境
Matlab里做优化调度,最省心的组合是YALMIP加外部求解器。YALMIP是一个建模工具箱,你不用手动把问题写成标准矩阵形式,直接用符号变量声明决策变量、写约束和目标函数,它会自动帮你转换成求解器需要的输入格式。我常用Gurobi或CPLEX作为底层求解器,它们在处理混合整数线性规划(MILP)和二次规划(MIQP)方面性能非常强。
如果手头没有商业求解器授权,Matlab自带的intlinprog和linprog也能应对中小规模问题。我做过对比:30个时段、5台机组、带储热和电锅炉的问题,linprog和Gurobi的求解时间都在秒级,差距不大;但把时段拉长到96个点、机组数量到10台以上,Gurobi的优势就明显了。学生用户可以去申请Gurobi的学术license,免费且申请流程很快,推荐优先考虑。
这里提醒一句:YALMIP在不同版本和不同求解器之间的兼容性偶尔会有小问题,建议安装完求解器后先在Matlab里运行yalmiptest验证配置是否成功,省得后面排查半天发现是链接问题。
3.2 算例系统与数据准备
我用的算例是一个简化的区域供热供电系统,包含两台热电联产机组、一台纯凝机组、一个风电场、一个储热罐和一台电锅炉。调度周期取24小时,时间间隔1小时,T=24。这类算例在文献中非常常见,你可以参考公开的IEEE算例数据,也可以按典型日曲线的形状自己构造。
负荷和风电数据的处理有一个原则:单位必须全局统一。我建议统一使用MW作为功率单位,能量统一用MWh。热负荷的数值可以和电负荷相近,但要理解一个是热功率、一个是电功率,二者通过热电机组的热电比产生关联。数据方面我通常先画出原始曲线的形状,确认没有突刺和明显异常点再送入模型。
另外,风电预测功率曲线我建议选取“夜间大、白天小”的反调峰曲线,这样才能暴露调度矛盾、体现联合优化的价值。如果你拿一条顺势而为的曲线来做,结果对比会非常平淡,论文里也不好看。
3.3 核心代码结构与关键代码片段
下面给一个能直接参考的主程序框架,我在实际项目中就是这么组织的:
%% 数据初始化 T = 24; P_load = [...]; % 电负荷,1x24 H_load = [...]; % 热负荷,1x24 P_w_pre = [...]; % 风电预测,1x24 % CHP机组参数 CHP.Pmax = [200 150]; % 电出力上限 CHP.Pmin = [80 60]; % 电出力下限 CHP.Hmax = [150 120]; % 热出力上限 CHP.k1 = [0.4 0.45]; % 电出力下限随热出力变化斜率 CHP.k2 = [0.3 0.35]; % 电出力上限随热出力变化斜率 % 纯凝机组 P_con_min = 60; P_con_max = 180; % 储热罐 S_max = 300; % 最大储热量 MWh q_s_max = 60; % 最大充放热功率 MW % 电锅炉 eta_eb = 0.95;然后是YALMIP建模的核心部分:
%% 决策变量 P_chp = sdpvar(2, T); % CHP电出力 Q_chp = sdpvar(2, T); % CHP热出力 P_con = sdpvar(1, T); % 纯凝机组出力 P_w = sdpvar(1, T); % 风电并网功率 P_eb = sdpvar(1, T); % 电锅炉耗电 Q_eb = sdpvar(1, T); % 电锅炉供热 Q_s = sdpvar(1, T); % 储热放热,正为放热 S = sdpvar(1, T); % 储热容量状态 %% 约束集合 Constraints = []; % 电力平衡 Constraints = [Constraints, sum(P_chp, 1) + P_con + P_w - P_eb == P_load]; % 热力平衡 Constraints = [Constraints, sum(Q_chp, 1) + Q_eb + Q_s == H_load]; % CHP可行域 for i = 1:2 Constraints = [Constraints, 0 <= Q_chp(i,:) <= CHP.Hmax(i)]; Constraints = [Constraints, P_chp(i,:) >= CHP.Pmin(i) + CHP.k1(i) * Q_chp(i,:)]; Constraints = [Constraints, P_chp(i,:) <= CHP.Pmax(i) - CHP.k2(i) * Q_chp(i,:)]; end % 纯凝机组 Constraints = [Constraints, P_con_min <= P_con <= P_con_max]; % 风电出力范围 Constraints = [Constraints, 0 <= P_w <= P_w_pre]; % 储热约束 Constraints = [Constraints, -q_s_max <= Q_s <= q_s_max]; Constraints = [Constraints, 0 <= S <= S_max]; % 储热动态过程 S0 = 0; % 初始储热量 for t = 1:T-1 Constraints = [Constraints, S(t+1) == S(t) - Q_s(t) + S0 * (t==1)]; end Constraints = [Constraints, S(T) == S0]; % 调度周期末回到初始值 % 电锅炉 Constraints = [Constraints, Q_eb == eta_eb * P_eb]; Constraints = [Constraints, 0 <= P_eb <= 50]; %% 目标函数 rho_pen = 500; % 弃风惩罚系数 objective = sum(0.02 * P_chp(1,:).^2 + 10 * P_chp(1,:)) ... + sum(0.025 * P_chp(2,:).^2 + 12 * P_chp(2,:)) ... + sum(0.03 * P_con.^2 + 8 * P_con) ... + rho_pen * sum(P_w_pre - P_w); %% 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 1); result = optimize(Constraints, objective, ops); %% 结果提取 P_chp_opt = value(P_chp); Q_chp_opt = value(Q_chp); P_w_opt = value(P_w); P_eb_opt = value(P_eb);这段代码里的储热动态约束我用了一种常见写法,但有个细节要提醒你:S(t+1) == S(t) - Q_s(t),如果你希望调度周期内储热罐允许净充放(不强制回到初始值),可以删掉S(T) == S0那行,并在目标函数里对最终储热量不加约束。是否强制周期平衡取决于你的研究场景——如果是单日优化,很多文献会要求回到初始值;如果做多日滚动,就不需要。
另外,sum(P_chp, 1)在YALMIP里得到的是1×T的行向量,P_con本身是1×T,等式两边维度一致才能匹配。如果你用的是sdpvar(T, 1)的列向量布局,所有约束的写法都要跟着调整,建议在脚本最开始就统一变量维度方向,别一会儿行一会儿列。
3.4 求解器参数与数值稳定性调优
模型写完之后,求解器参数设置也会显著影响效率和成功率。我常用的几个sdpsettings选项:
ops = sdpsettings('solver', 'gurobi', ... 'verbose', 2, ... 'debug', 1, ... 'gurobi.TimeLimit', 120, ... 'gurobi.MIPGap', 0.001);debug设为1后,如果模型本身有问题,YALMIP会额外输出诊断信息,帮助定位是哪些约束导致不可行。MIPGap是混合整数规划的相对最优间隙,工程上设到0.1%已经足够,太小的值会显著拖慢求解速度。
数值稳定性方面,最常见的坑是量纲差异过大。比如煤耗成本系数是几十、弃风惩罚是几百、储热容量是几百,这些数量级还好;但如果你把热负荷设成几千GJ、储热容量设成几万GJ,目标函数里各项的数值可能相差几个数量级,求解器内部处理起来就容易出问题。我的建议是全部统一到MW和MWh量纲,让变量值落在0到1000这个区间内。
4. 仿真结果与效果剖析
4.1 场景设置与方案对比
为了体现联合优化的价值,我设置了三个方案做对比:
- 方案A:传统“以热定电”调度,即热负荷按固定比例分配给各台CHP机组,机组电出力下限由热出力直接折算,不含优化。
- 方案B:联合优化调度,CHP机组的热电出力在可行域内自由优化,但不含储热和电锅炉。
- 方案C:联合优化调度,并加入储热罐和电锅炉。
我取了一个大风低温的冬季典型日。风电预测的最大出力达到280MW,夜间电负荷低谷只有500MW,热负荷峰值达到200MW,系统内灵活性资源不足时会面临严重的消纳压力。
4.2 结果对比与指标解读
仿真结果整理成下表:
| 指标 | 方案A(以热定电) | 方案B(联合优化无储热) | 方案C(联合优化+储热电锅炉) |
|---|---|---|---|
| 弃风电量(MWh) | 812 | 346 | 112 |
| 弃风率(%) | 21.5 | 9.2 | 3.0 |
| 系统运行成本(万元) | 33.8 | 30.2 | 28.6 |
| CHP总发电量(MWh) | 5120 | 4860 | 4620 |
| 储热罐充放量(MWh) | — | — | 486 |
几个数据点非常值得解读。方案A弃风率高达21.5%,这在北方冬季是真实会发生的情况。方案B把CHP机组的热电出力从“固定比例”变为“可行域内优化”,热负荷自动向对电出力约束较小的机组倾斜,弃风率降到了9.2%——这说明仅仅优化热负荷分配就能带来巨大增益。
方案C进一步引入储热和电锅炉,弃风率压到3.0%。从调度曲线上看,夜间风电大发时段(22:00到次日6:00),电锅炉开启、储热罐充电,CHP机组的电出力相应压到较低水平;白天负荷攀升后,储热罐放热,CHP机组把省下来的电量额度用于高负荷时段发电。整个调度过程相当于把夜间的“热负荷需求”平移到了白天,给风电腾出了宝贵的夜间空间。
这里也能直观看到储热容量与消纳效果的关系。我把储热罐容量从0增加到400MWh做了一组灵敏度分析,弃风率下降曲线呈现明显的边际递减:容量从0到150MWh时,弃风率平均每增加50MWh下降约4个百分点;超过200MWh后,每增加50MWh只下降约0.8个百分点。做项目汇报时,这类曲线比单点结果有说服力得多。
4.3 方案背后:热电解耦的物理本质
从结果里提炼一个核心逻辑:风电消纳空间本质上取决于系统的向下调峰能力。热电机组“以热定电”限制了最小电出力,而储热罐和电锅炉的加入,本质上是在时间维度上把热负荷从低风电时段挪走,或者把风电变成热负荷——一句话,在“热”的维度上给系统增加灵活性。
这也是为什么业内常说“风电消纳不只是电的问题,而是电-热协同的问题”。单纯盯着电网侧做优化,空间有限;把热网、储热、电锅炉这些热力侧资源纳入联合调控,视野一下子就打开了。你在写论文或者做项目总结时,把这一层逻辑讲清楚,价值会比堆砌仿真数据高很多。
5. 常见问题与调试实录
5.1 求解器报“不可行”的三板斧排查法
我的模型第一次跑的时候就报Infeasible problem了。这种情况不要慌,按顺序排查,我总结了三板斧。
第一板斧:检查平衡约束。电力平衡和热力平衡是否在物理上可满足?比如电负荷最小值减去所有机组最小出力、再加上风电预测最大值,如果是负数,说明任何时刻系统出力都高于负荷,必然不可行。用一个简单脚本先在不引入决策变量的情况下做可行性预判,能省很多时间。
第二板斧:把目标函数去掉,只求可行解。把objective设为一个常数0,调用optimize(Constraints, 0, ops)。如果约束本身有解,求解器会返回solved;如果仍然不可行,就说明约束内部有冲突。
第三板斧:加松弛变量定位冲突来源。在最可疑的约束上加上非负松弛变量,例如把电力平衡写成:
slack_p = sdpvar(1, T); Constraints = [Constraints, sum(P_chp,1) + P_con + P_w - P_eb + slack_p == P_load];求解后看哪些时段slack_p非零,那个时段就是问题所在。常见的冲突来源包括:储热罐容量太小但热负荷必须靠储热来平衡、CHP机组热出力上界不够支撑热负荷、风电并网功率被约束得过死等。定位到具体时段和具体约束后,修改参数就有的放矢了。
5.2 结果不合理、出力跳变的排查思路
如果求解成功但结果看着怪怪的,比如CHP机组出力在相邻时段剧烈跳变、储热罐充放功率反复折腾,先别怀疑求解器抽风,大多数时候是模型本身的问题。
第一个排查项是爬坡约束缺失。我前面的简化模型里没有加爬坡约束,实际运行时CHP机组电出力的变化率是受限的。文献中常见写法是:
Ramp_up = 30; Ramp_down = 30; for t = 1:T-1 for i = 1:2 Constraints = [Constraints, P_chp(i,t+1) - P_chp(i,t) <= Ramp_up]; Constraints = [Constraints, P_chp(i,t) - P_chp(i,t+1) <= Ramp_down]; end end第二个排查项是储热罐的动态约束写反了符号。我之前见过有人把S(t+1) == S(t) - Q_s(t)写成了S(t+1) == S(t) + Q_s(t),结果是储热罐越放热存量越高,调度结果呈现出完全违背物理规律的特性。做结果后处理时,除了看数值,还应该顺手画一画储热罐容量曲线,看看形状是否符合直觉。
第三个排查项是目标函数的惩罚系数过小。如果你的弃风惩罚系数ρ_pen只有煤耗系数的两三倍,优化器会在“弃风”和“多烧煤”之间权衡,得到的可能是一个折中方案,弃风率偏高。这个不算bug,但你得明白:惩罚系数本质上代表了你对弃风的主观厌恶程度,想让结果接近“最大化消纳”,就把系数调大,试到弃风率变化不再敏感为止。
5.3 大规模实例的数值稳定性与加速技巧
调度周期从24小时扩展到96小时,或者机组数量增加到10台以上后,求解时间会明显上升。我的经验是:优先检查模型里有没有整型变量。YALMIP中如果你不小心把某个本来就是整数的参数声明成了binvar或intvar,求解器会把它当混合整数问题来处理,速度根本没法比。检查方法很简单,在optimize之前用class(Constraints)看类型、用yalmiptest确认求解器识别正确。
另一个加速技巧是可以给sdpvar变量显式指定上下界,而不是把上下界写进约束集合里。YALMIP内部对变量边界信息的处理比普通约束更高效,能帮求解器更快做预求解。写法是:
P_con = sdpvar(1, T); P_con = [P_con, 'bounds', 60, 180];不过这个写法在最新版YALMIP中容易写错,我更推荐直接用约束对象设置边界,必要时才用边界变量声明。实际项目中最有效的加速手段还是数据归一化——把负荷都除以基准值100,功率变量落到0到3之间,数值稳定性肉眼可见地改善。
5.4 一个容易被忽视的工程问题:热网管网的蓄热效应
最后聊一个扩展问题。真实系统中,热网的供水管和回水管本身就有巨大的热容量,管网的蓄热效应也能参与调峰,这就是所谓的“热网蓄热”。学术研究中经常把它忽略掉,但实际工程里,热网蓄热在小时级调度中能贡献可观的灵活性,相当于一个天然的储热罐。
如果你想把项目做深,可以在模型里把热负荷由“单点热平衡”改为“节点热平衡”或者加入热网动态模型,比如用有限差分法描述管道内水温分布。这样做模型复杂度会上一个台阶,但仿真结果会更贴近实际。我建议先做好基础的储热和电锅炉版本,再去啃热网动态建模这块硬骨头,否则很容易被复杂的偏微分方程困住,半天出不来结果。
我自己调试这个项目时有个很深的体会:别急着把模型往上堆复杂,先把简单的两机系统、24时段案例跑透,看明白储热罐在夜间充电、白天放电、风电并网率提升的整个过程,再一步步加设备、加约束、加长调度周期。优化模型不像写代码,不是功能堆得越多越好——每一个约束都可能成为压垮求解器的最后一根稻草。把调度逻辑想透彻,比多写几百行代码更有用。