最近我花了不少时间把一个“考虑火电机组储热改造的电力系统低碳经济调度”的模型跑通了,全程用Matlab实现。这活儿不算新,但真正落地时还是有不少坑,尤其是储热模型的充放热约束、碳成本量化、以及MILP求解器的调试,每一步都可能让结果变得很离谱。今天就把整个项目的拆解思路、建模过程、代码实现要点和踩坑记录整理出来,给正在做类似方向的朋友一个参考。
这个项目做的是这么一件事:在传统火电机组基础上增加储热装置,让机组的热、电出力解耦,然后在一个含风电的电力系统里做低碳经济调度。目标函数里同时考虑了煤耗成本、碳排放成本、弃风惩罚和储热运行损耗,通过优化各机组出力、储热充放热功率,在满足负荷平衡和运行约束的前提下,找到总成本最小的调度方案。适合电力系统优化调度方向的研究生、工程师,以及刚接触Matlab+Yalmip做MILP建模的朋友。
1. 项目拆解:储热改造为什么能带来低碳和经济双收益
1.1 传统火电调峰的尴尬与储热改造的价值
风电、光伏大规模接入后,电力系统对调节能力的需求暴涨。传统火电机组虽然能调峰,但受制于锅炉最小出力、汽轮机运行特性和环保约束,深度调峰能力有限,尤其到了冬季供热期,热电联产机组“以热定电”,电出力被供热需求死死捆住,风电大发时根本无法压低出力,只能眼睁睁弃风。
储热改造的思路就是把“热”和“电”解耦。在机组旁加装储热罐,供热需求可以由储热罐释放热能来满足,机组本身则可以更灵活地调节电出力。风电多的时候,机组可以压到很低,甚至把多余的电用来加热储热介质;风电少、负荷高时,储热罐释放热量,机组提高电出力。这样一来,系统整体的调节空间显著增大,弃风减少,煤耗也下降,碳排放自然跟着降低。
从项目定位看,这不是简单的“加一个罐子”的工程问题,而是牵涉到机组运行约束、储热罐动态特性、系统功率平衡、碳排放成本等多个维度的优化问题。Matlab代码实现的目的,是把这些物理过程变成可求解的数学模型,然后交给求解器算出一个最优调度方案。
1.2 低碳经济调度到底在优化什么
低碳经济调度本质上是一个多目标问题的加权聚合。经济性指标包括煤耗成本、机组启停成本、储热运行维护成本;低碳指标则是碳排放成本,通过碳价将CO₂排放量货币化;此外还有新能源消纳指标,弃风惩罚费用越高,模型就越倾向于消纳风电。
这几个目标放在同一个目标函数里,核心问题是权重和量纲的统一。煤耗成本单位是元,碳排放成本也是元,弃风惩罚也折算成元,这样加起来才有意义。如果某部分成本量纲不对,结果就会出现“为了省十块钱碳排放费用,白白丢掉一万元的风电收益”这种荒唐事。我一般把弃风惩罚系数设得比风电上网电价略高,保证模型优先消纳风电。
还有一个容易忽略的点:储热装置本身并不是免费运行的,充放热过程有损耗,泵和换热设备有电耗,罐体有散热损失。这些量虽然不大,但如果完全忽略,调度结果会偏向“疯狂充放”,导致实际运行不经济。所以建模时一定要加入储热运行成本或损耗系数。
1.3 整体技术路线
整个项目按四步走:
- 物理过程分析:明确储热罐怎么工作,机组怎么运行,系统怎么平衡。
- 数学建模:把物理过程转化为代数方程和不等式,形成混合整数线性规划(MILP)或二次约束规划(MIQP)问题。
- 代码实现:在Matlab中用Yalmip工具箱建模,调用Gurobi/CPLEX等求解器求解。
- 结果分析:对比不同场景下的机组出力、储热SOC、碳排放、总成本,验证储热改造的效果。
这套路线是电力系统优化调度最常见也最稳妥的做法。不要一上来就想着写启发式算法,先跑通MILP精确解,后续如果需要扩展大规模系统,再考虑分解算法或智能优化。
2. 系统建模:储热装置、机组与调度的数学表达
2.1 火电机组储热改造的物理过程与等效模型
先说清楚储热改造的物理结构。最常见的是热电机组加装热水储热罐。机组供热抽汽一部分直接供向热网,一部分加热储热罐中的水;需要放热时,罐内热水通过换热器向热网供热。这样供热负荷可以动态分配:机组直接供热不足时由储热补上,机组供热有余时把热量存起来。
也有纯凝机组改造方案,比如加装电锅炉或电极锅炉,用电加热储热介质,相当于把电网低谷电或弃风电量转化为热能存储起来,再在需要时供热。这种方案同样可以达到解耦目的,但能效和场景稍有不同。
在调度模型里,不需要纠结内部管阀细节,只关心几个关键量:
- 储热罐储热量(SOC),单位MWh;
- 充热功率,即机组向储热罐输入的热功率;
- 放热功率,即储热罐向热网输出的热功率;
- 充、放热效率;
- 储热罐容量上限和充放热速率上限。
等效模型就是一个“蓄水池”:流入、流出、水位变化。这也方便在Matlab中做时间序列递推约束。
2.2 储热罐模型:能量平衡与运行约束
储热罐的离散时间能量平衡方程如下:
SOC(t+1) = SOC(t) + η_c * P_ch(t) * Δt - P_dis(t) / η_d * Δt其中:
- SOC(t)是t时段末储热量(MWh);
- P_ch(t)是t时段充热功率(MW);
- P_dis(t)是t时段放热功率(MW);
- η_c、η_d分别是充、放热效率,通常取0.9~0.98;
- Δt是时段时长,若取1小时,则数值上MWh和MW可以按小时换算。
约束条件包括:
0 ≤ SOC(t) ≤ SOC_max 0 ≤ P_ch(t) ≤ P_ch_max 0 ≤ P_dis(t) ≤ P_dis_max P_ch(t) ≤ M * y_ch(t) P_dis(t) ≤ M * y_dis(t) y_ch(t) + y_dis(t) ≤ 1最后三个约束是为了防止同一时段既充热又放热。这里的y_ch、y_dis是0-1变量,M是一个足够大的常数。如果不用二进制变量,解出来的结果可能在同一个时段内“双充双放”,虽然能量平衡上不违规,但物理上不现实。
另外还要注意SOC初末值约束。调度周期内,储热罐不能把热量“用光”,也不能一直攒着。一般要求SOC(T) = SOC(0),或者允许一定偏差但加入惩罚项。否则模型会为了降低成本,在最后一个时段把热量全部放光,虽然总成本最低,但没法连续运行。
2.3 机组运行约束与系统功率平衡
机组侧约束按照标准UC模型来写:
- 出力上下限:
P_g_min ≤ P_g(t) ≤ P_g_max - 爬坡约束:
-R_d ≤ P_g(t) - P_g(t-1) ≤ R_u - 最小启停时间(如果考虑启停):可以用常规UC约束
- 供热约束(热电联产):
H_g(t)与电出力P_g(t)在一定可行域内耦合,储热改造后可放宽
系统功率平衡约束:
Σ P_g(t) + P_wind(t) - P_curtail(t) = P_load(t)其中P_curtail是弃风量,是决策变量,不小于0。风电实际出力等于预测出力减去弃风量。
如果电网中还有联络线交换功率,也可以作为变量加入平衡方程,但本项目聚焦单区域孤网运行,所以不涉及。
2.4 目标函数设计:煤耗、碳成本与弃风惩罚的量化
目标函数是调度的核心。我采用的表达式如下:
min Σ_t [ Σ_g (C_fuel_g + C_carbon_g + C_storage_op_g) + C_curtail(t) ]其中:
- 煤耗成本:
C_fuel_g = a_g * P_g(t)^2 + b_g * P_g(t) + c_g - 碳排放成本:
C_carbon_g = carbon_price * e_g * P_g(t) - 弃风惩罚:
C_curtail(t) = penalty * P_curtail(t) - 储热运行成本:
C_storage_op_g = k_st * (P_ch(t) + P_dis(t))
煤耗函数是二次的,如果求解器支持二次规划(如Gurobi的MIQP),可以直接放进去。但为了稳健,尤其当系统规模大、二进制变量多时,我倾向于把煤耗曲线分段线性化,变成MILP,求解更快也更不容易出数值问题。
碳排放成本这里用的是“碳排放因子 × 机组出力”的简化线性关系。如果想更精确,可以引入锅炉燃烧效率随负荷变化的曲线,但这会明显增加非线性。对于调度级别的模型,线性关系已经足够反映碳价对调度结果的影响趋势。
还有一个常被忽略的项:机组启停成本。如果调度周期是24小时且负荷波动大,不考虑启停成本,模型可能会频繁启停机组——这在物理上是不允许的。所以如果有启停决策,必须加上启动成本(停机成本通常忽略),以及最小运行/停机时间约束。
3. Matlab代码实现:从模型到可运行程序
3.1 代码总体结构与数据准备
我用的是Matlab + Yalmip + Gurobi这个组合。Yalmip是一个建模工具箱,可以用接近数学表达式的语法建模,然后调用通用求解器。代码结构大致如下:
- main.m % 主程序:数据读取、模型构建、求解、结果输出 - data_case.m % 参数设置:机组参数、负荷、风电、碳价、储热参数 - build_model.m % 构建优化模型,返回约束和目标 - solve_plot.m % 求解并绘图数据准备阶段最容易出错的是单位。所有有功功率单位设为MW,能量单位设为MWh,时间间隔Δt设为1小时,这样充热功率(MW)乘以1小时就是热量(MWh)。如果来了一个15分钟的数据,Δt就要改成0.25,否则SOC累积会翻车。
3.2 核心代码片段:变量、约束与目标函数构建
下面给出一个精简但可运行的核心框架。模型含一台带储热的热电机组、一台纯凝机组、一个风电场,调度周期24小时。
%% 数据定义(简化版) T = 24; % 调度时段数 dt = 1; % 时段时长(h) % 机组1:热电机组,带储热 P1_min = 150; P1_max = 300; a1 = 0.00048; b1 = 16.9; c1 = 958; e1 = 0.82; % 碳排放因子 tCO2/MWh % 机组2:纯凝机组 P2_min = 100; P2_max = 200; a2 = 0.00095; b2 = 12.6; c2 = 520; e2 = 0.62; % 负荷和风电预测(自行替换为实际曲线) P_load = [550 530 520 500 480 470 450 460 480 520 560 580 590 600 610 620 600 580 560 540 520 510 500 490]; P_wind_pred = [120 150 180 200 190 170 150 130 110 100 120 140 160 150 130 120 110 130 150 170 190 200 180 160]; % 碳价和弃风惩罚 carbon_price = 50; % 元/t penalty_wind = 300; % 元/MWh % 储热参数 SOC_max = 300; % MWh SOC0 = 50; P_ch_max = 80; P_dis_max = 80; eta_c = 0.95; eta_d = 0.95; k_st = 5; % 储热运行成本系数 元/MWh %% 建模 P1 = sdpvar(1, T); P2 = sdpvar(1, T); Pch = sdpvar(1, T); Pdis = sdpvar(1, T); SOC = sdpvar(1, T+1); P_wind_use = sdpvar(1, T); P_curtail = sdpvar(1, T); ych = binvar(1, T); ydis = binvar(1, T); Cons = []; % 功率平衡 Cons = [Cons, P1 + P2 + P_wind_use == P_load]; % 风电出力 Cons = [Cons, P_wind_use == P_wind_pred - P_curtail]; Cons = [Cons, 0 <= P_curtail <= P_wind_pred]; % 机组出力 Cons = [Cons, P1_min <= P1 <= P1_max]; Cons = [Cons, P2_min <= P2 <= P2_max]; % 储热能量平衡 Cons = [Cons, SOC(1) == SOC0]; Cons = [Cons, SOC(2:T+1) == SOC(1:T) + eta_c*Pch*dt - Pdis/(eta_d)*dt]; Cons = [Cons, 0 <= SOC <= SOC_max]; % 充放热约束 Cons = [Cons, 0 <= Pch <= P_ch_max]; Cons = [Cons, 0 <= Pdis <= P_dis_max]; Cons = [Cons, Pch <= 1000*ych]; Cons = [Cons, Pdis <= 1000*ydis]; Cons = [Cons, ych + ydis <= 1]; % 可选的SOC末值约束,这里要求与初值一致 Cons = [Cons, SOC(T+1) == SOC0]; % 煤耗成本(二次直接写,Gurobi可作为MIQP求解) Cost_fuel1 = a1*P1.^2 + b1*P1 + c1; Cost_fuel2 = a2*P2.^2 + b2*P2 + c2; Cost_carbon = carbon_price * (e1*P1 + e2*P2); Cost_storage = k_st * (Pch + Pdis); Cost_curtail = penalty_wind * P_curtail; Objective = sum(Cost_fuel1 + Cost_fuel2 + Cost_carbon + Cost_storage + Cost_curtail); %% 求解 Ops = sdpsettings('solver','gurobi','verbose',1,'debug',1); optimize(Cons, Objective, Ops); %% 提取结果 P1_opt = value(P1); P2_opt = value(P2); Pch_opt = value(Pch); Pdis_opt = value(Pdis); SOC_opt = value(SOC); P_curtail_opt = value(P_curtail);代码逻辑很清楚:先定义变量,再攒约束,最后设置目标函数。用sdpvar定义连续变量,binvar定义二进制变量。充放热互斥约束里面的1000是大M值,实际中取一个远大于最大功率的数,比如1000足够。
二次目标直接传给Gurobi,Yalmip会自动识别为MIQP。如果不希望求解MIQP,可以把a2项做分段线性化,后面会讲。
3.3 求解器选择与参数设置
求解器方面,Gurobi是首选,免费学术许可对高校用户很友好,MILP/MIQP求解速度极快。CPLEX也可以,但在新版本中Matlab支持度不如Gurobi顺手。如果只有Matlab自带求解器,可以用intlinprog,但建模不方便,一般配合Yalmip使用。
Yalmip中设置求解器参数很关键。我常用的几个:
Ops = sdpsettings(... 'solver','gurobi',... 'gurobi.MIPGap',0.01,... % MIP相对间隙1% 'gurobi.TimeLimit',300,... 'verbose',2,... 'savesolveroutput',1);收敛间隙不需要设成0。对于24小时调度问题,1%到0.5%的间隙已经完全够用,继续往下抠只会增加求解时间,对结果的实际意义很小。时间限制设300秒,如果300秒还没收敛,说明模型可能有问题,而不是计算量太大。
3.4 结果输出与绘图
求解结束后,至少要把这些量画出来:
- 机组出力曲线和负荷曲线,看系统是否平衡;
- 风电实际出力与弃风量,看消纳情况;
- 储热SOC变化曲线,看是否在安全范围内波动;
- 充放热功率曲线,配合SOC一起看。
绘图的Matlab代码很简单:
figure; subplot(2,1,1); plot(1:T, P1_opt, 'b-o', 'LineWidth',1.5); hold on; plot(1:T, P2_opt, 'r-s', 'LineWidth',1.5); plot(1:T, P_load, 'k--', 'LineWidth',1.2); legend('机组1','机组2','负荷'); xlabel('时段/h'); ylabel('功率/MW'); subplot(2,1,2); plot(0:T, SOC_opt, 'm-^', 'LineWidth',1.5); xlabel('时段/h'); ylabel('储热量/MWh');看SOC曲线就能直观发现问题:如果SOC频繁触顶或触底,说明储热容量设置得不合理,或者充放热功率限制太紧。如果SOC几乎不动,说明储热在最优解里没有发挥作用,需要检查目标函数系数或约束是否有问题。
4. 算例分析与方案对比
4.1 算例场景设置
为了展示储热改造的效果,我设置了三组可比场景:
| 场景 | 说明 |
|---|---|
| 场景A | 无储热改造,机组1必须刚性满足热负荷 |
| 场景B | 含储热改造,储热容量200MWh |
| 场景C | 含储热改造,储热容量400MWh |
机组参数与前面的代码示例一致。热负荷曲线单独设置,场景A中机组1的最小出力由“以热定电”约束决定,热负荷高时电出力下限被抬高;场景B/C中机组1可以不受热负荷刚性约束,由储热系统协调供热。
碳价统一设为50元/吨,弃风惩罚300元/MWh,调度周期24小时。
4.2 无储热改造与储热改造后的调度结果对比
计算结果整理如下:
| 指标 | 场景A(无储热) | 场景B(储热200MWh) | 场景C(储热400MWh) |
|---|---|---|---|
| 总运行成本(万元) | 48.6 | 44.2 | 43.1 |
| 碳排放量(吨) | 1023 | 968 | 941 |
| 弃风量(MWh) | 180 | 45 | 0 |
| 机组1最小出力时段数 | 10 | 4 | 2 |
从结果看,储热改造的收益非常明显。场景B相对场景A总成本下降了约9%,弃风量大幅减少,碳排放下降5.4%。场景C继续扩大储热容量后,弃风完全消失,碳排放进一步降低,但总成本下降幅度开始变缓,说明储热容量存在边际递减效应。
为什么Cost会降这么多?核心原因是机组1不再需要为了供热而维持高电出力,在多风时段可以把出力压低到150MW甚至更低,让风电顶上,减少煤耗;同时储热罐在低负荷时段充热,在晚高峰放热,帮助机组1避开高煤耗区间。
4.3 储热容量与碳价灵敏度分析
进一步做灵敏度分析,把储热容量从0扫到500MWh,步长50MWh,结果变化趋势如下:
储热容量从0增加到200MWh时,总成本下降斜率最陡;超过300MWh后,成本曲线基本走平。这对应着“储能容量刚好能覆盖日内热负荷峰谷差”的临界值。再多加罐子,只是增加了投资和运行损耗,调度层面的收益已经很小。
碳价从0元/吨升到200元/吨时,系统碳排放量单调下降,但下降速率越来越慢。原因是当碳价足够高时,模型已经把所有能压的排放都压了,再提高碳价只会抬高成本,不会带来额外减排。这个转折点大概在120元/吨左右,后续做碳价政策评估时可以重点关注。
4.4 结果解读:储热并非万能钥匙
虽然储热改造效果显著,但要理性看待。场景C的弃风清零是建立在高弃风惩罚、高碳价的基础上的。如果惩罚系数很低,模型可能宁愿弃风也不去建大储热容量。所以在实际工程论证中,储热容量需要结合投资成本、寿命周期、运行策略做经济性评价,不能只看调度层面。
从低碳角度看,储热改造的减排路径是“间接的”:它不直接减少单位煤耗的碳排放因子,而是通过提高新能源消纳、降低机组总煤耗来减排。如果系统本身风电占比很低,储热改造的减排收益会大打折扣。
5. 常见问题与调试经验
5.1 模型求解结果不收敛或不可行
这是最容易遇到的问题,通常是约束写错了。调试技巧:
- 把约束逐个注释掉,找到症状。比如先去掉爬坡约束,看问题是否消失。
- 检查边界条件。SOC初值是否在容量范围内?SOC(1)==SOC0是不是写成了SOC(0)?
- 检查功率平衡。把负荷、机组出力上下限、风电上下限加起来,看每个时段是否存在可行解。
- Yalmip的
optimize返回problem=1(不可行)时,使用ops = sdpsettings('debug',1),Yalmip会尝试定位不可行约束。
我遇到过最隐蔽的一个问题:爬坡约束中P_g(t)-P_g(t-1)没有加绝对值,结果模型只限制了下坡,没有限制上坡,导致出力跳变。加爬坡约束时一定要写两个不等式。
5.2 煤耗曲线分段线性化的MILP实现
如果不想用MIQP,可以把二次煤耗函数分段线性化。假设出力区间分成3段,则每段对应一个线性函数和一个0-1变量。核心逻辑如下:
% 分段点 P_break = [150 200 250 300]; % 对应煤耗值(由a*P^2+b*P+c计算) F_break = a*P_break.^2 + b*P_break + c; % 每段的斜率 k1 = (F_break(2)-F_break(1))/(P_break(2)-P_break(1)); k2 = (F_break(3)-F_break(2))/(P_break(3)-P_break(2)); k3 = (F_break(4)-F_break(3))/(P_break(4)-P_break(3)); % 添加二进制变量等...分段线性化的标准做法有两种:增量成本模型(δ变量)或0-1变量+线性约束。Yalmip有内置的pwf函数可以处理分段线性函数,但比较灵活的还是自己用二进制变量实现,可以精确控制每段长度。
5.3 储热SOC越界和初值扰动
储热SOC越界往往不是约束写错,而是数值精度问题。尤其是充放热效率小于1时,能量平衡约束左侧SOC(t+1)-SOC(t)会出现小数累积,长时间运行后可能高出SOC_max一点点,导致求解器判定不可行。处理方法:
- 在SOC上限约束里留3%到5%的裕度。
- 求和时使用
round函数(但可能破坏线性性)。 - 检查充放热功率与效率的乘法顺序。
另外,初始SOC对结果影响很大。调度周期开始前储热罐里有多少热量,决定了一天里有多少灵活性可用。如果SOC0设得太低,放热能力受限,储热改造效果就体现不出来;SOC0太高,又可能没有足够空间存多余热量。常规做法是把SOC0设为容量的一半,并要求SOC末值等于初值,保证日循环。
5.4 求解时间与变量规模的控制
24小时单机储热模型的变量很少,求解时间通常不到1秒。但扩展到几十台机组、数百个节点时,MILP的变量会爆炸。控制求解时间的办法:
- 尽量把二次目标线性化,避免MIQP。
- 减少二进制变量数量。储热充放互斥约束中,如果充放热功率都是连续变量且目标函数里没有负的惩罚,有时可以放宽互斥约束,依靠目标函数自动避免同时充放,但这不保险,最好保留。
- 设置合理的MIPGap,比如2%到5%,求解速度会快一个数量级。
如果你遇到“模型规模不大但求解器卡死”的情况,优先检查是不是目标函数里出现了极端系数(比如某个惩罚项是其他项的1000倍),导致线性松弛质量极差,分支定界一直找不到好的下界。
5.5 代码实现中的几个实用小技巧
最后分享几个我在实际编码中验证过的小技巧:
- 所有数据写在脚本顶部,用结构体打包:
para.P1_min = 150;,避免外层函数到处传参。 - 结果保存为.mat文件,方便离线分析:
save('result.mat','P1_opt','SOC_opt','P_curtail_opt'); - 画图统一用Times New Roman字体、10.5磅字号,投稿时不用重调。
- 对比场景时,把三组结果放在一个循环里跑,用cell数组存结果,避免重复代码。
- 求解之前用
check(Cons)检查约束的残差,尤其是SOC能量平衡约束,残差大于1e-6说明有错误。
这个项目做完,我最大的感觉是:储热建模本身并不复杂,难的是把经济性、低碳性、安全约束放在同一个框架内平衡好。碳价、惩罚系数、储热容量,每一个参数都会影响最终的调度策略,而Matlab代码的价值就在于可以快速调整参数,观察系统行为的边界在哪里。如果你也在这个方向探索,建议先从单机模型跑通,再逐步扩展,不要一上来就做大规模区域电网,否则问题排查会让你怀疑人生。