
最近我花了不少时间把一个“考虑火电机组储热改造的电力系统低碳经济调度”的模型跑通了全程用Matlab实现。这活儿不算新但真正落地时还是有不少坑尤其是储热模型的充放热约束、碳成本量化、以及MILP求解器的调试每一步都可能让结果变得很离谱。今天就把整个项目的拆解思路、建模过程、代码实现要点和踩坑记录整理出来给正在做类似方向的朋友一个参考。这个项目做的是这么一件事在传统火电机组基础上增加储热装置让机组的热、电出力解耦然后在一个含风电的电力系统里做低碳经济调度。目标函数里同时考虑了煤耗成本、碳排放成本、弃风惩罚和储热运行损耗通过优化各机组出力、储热充放热功率在满足负荷平衡和运行约束的前提下找到总成本最小的调度方案。适合电力系统优化调度方向的研究生、工程师以及刚接触MatlabYalmip做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(t1) SOC(t) η_c * P_ch(t) * Δt - P_dis(t) / η_d * Δt其中SOC(t)是t时段末储热量MWhP_ch(t)是t时段充热功率MWP_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, T1); 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:T1) 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(T1) 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足够。二次目标直接传给GurobiYalmip会自动识别为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.644.243.1碳排放量吨1023968941弃风量MWh180450机组1最小出力时段数1042从结果看储热改造的收益非常明显。场景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返回problem1不可行时使用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^2b*Pc计算 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(t1)-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代码的价值就在于可以快速调整参数观察系统行为的边界在哪里。如果你也在这个方向探索建议先从单机模型跑通再逐步扩展不要一上来就做大规模区域电网否则问题排查会让你怀疑人生。