储能调度与火电联合优化:MILP建模到滚动优化的完整实践

📅 发布时间:2026/9/28 1:59:10
储能调度与火电联合优化:MILP建模到滚动优化的完整实践
简介火电与储能联合调度是提升电网灵活性的重要手段这份代码资源围绕该主题给出一个完整的Matlab源文件用于在1分钟级实时调度中实现火电机组与储能电站的协同优化。资源适合电力系统优化方向的研究生、工程师以及对调度算法感兴趣的入门者可应用于可再生能源渗透率升高导致调峰困难、弃电率上升等实际场景。压缩包内共1个文件为m脚本整体大小仅1KB内容精练便于快速定位核心算法逻辑并进行参数修改。该资源已有433人浏览学习。通过阅读源码读者可梳理出目标函数设定、约束条件处理、充放电策略迭代等关键环节了解如何用遗传算法或粒子群等智能方法寻求最优运行方案同时可对比不同权重设置下的成本与稳定性结果为自身研究或项目提供可直接复用的代码基础。1. 储能调度这件事Joint optimization到底比“先充后放”省在哪把火电和储能放进同一个优化问题里联立求解这就是Joint optimization在储能调度里的真实含义。很多项目组习惯先做负荷预测再把剩余负荷丢给火电储能只按峰谷价差“低价充、高价放”独立算一遍。这种分步做法看似省事实际上把火电爬坡、最小出力和储能SOC变化之间的耦合关系全切断了算出来的计划常常互相打架火电为了迁就储能曲线频繁爬坡煤耗和磨损全被藏在看不见的地方。我接过几个电网储能联合调度的实际项目后共识很明确——储能调度必须跟thermal power plant的出力计划一起建模求解才算真正把储能的灵活性用起来。这篇文章不绕弯子直接从数学模型、可跑代码、参数整定和踩坑记录讲起。2. 把储能调度写成数学优化问题目标函数与约束的取舍Joint optimization的建模核心是回答两个问题我们在最小化什么以及哪些物理规律不能被打破。目标函数回答前者约束条件回答后者。这一章不堆公式只讲怎么落地。2.1 目标函数煤耗成本要不要二次曲线储能成本怎么折算火电的煤耗成本本质上是一条二次曲线出力越高边际成本越大。直接丢给MILP求解器会很难处理所以常见做法是把二次曲线分段线性化用若干段直线去逼近。分段数太少最优解会偏向断点结果和真实煤耗对不上分段数太多变量数量翻倍求解时间变长。我一般用4到6段精度和速度平衡得比较好。储能侧的成本有三个来源充放电效率带来的能量损耗、循环造成的容量衰减退化成本、以及SOC偏离目标区间带来的调度风险。前两个要写进目标函数第三个通常写成SOC惩罚项偏离50%基准时让目标值增加防止模型在边界处“摆烂”。很多初版模型只写火电煤耗结果储能被当成免费工具疯狂调用算出来的方案实际执行时电池寿命损失严重这是储能优化里最常见的翻车点之一。目标函数的完整形态大致是煤耗分段成本 火电启动成本 储能退化成本 SOC偏离惩罚 弃负荷惩罚。弃负荷惩罚必须加否则模型在无解时直接砍负荷得到的调度计划根本不能执行。惩罚系数设一个比煤耗成本高一个量级的值比如煤耗成本几百元每MWh弃负荷惩罚就设几千元每MWh让求解器只有在万不得已时才碰它。2.2 约束条件功率平衡、火电爬坡、储能SOC三大件怎么耦合功率平衡是每时段的铁律火电出力加储能放电功率减去储能充电功率等于负荷减去可再生能源出力。这里有个易错点充电功率和放电功率必须用二元变量做互斥约束不能让求解器同时充和放——虽然物理上不允许但如果不加约束求解器会在目标函数里“套利”同时充放电制造虚假收益。火电约束包括出力上下限、爬坡约束和最小启停时间。爬坡约束是储能调度的主要耦合来源火电爬坡慢储能爬坡快联合优化会让储能去顶火电爬坡跟不上的时段。最小启停时间这个约束很多人一开始不加结果模型让火电每两小时启停一次启停成本远超收益方案根本没法用。储能侧约束包括SOC递推方程、SOC上下限、充放电功率上限和首尾SOC约束。SOC递推方程要特别注意效率放在哪一侧充电时SOC增加量要乘以充电效率放电时SOC减少量要除以放电效率。如果把效率放反算出的调度计划实际执行时SOC会逐渐偏离跑一天下来要么提前放空要么充不进去。末端SOC约束也很重要不加的话模型会在最后时段把储能放空而实际运行中第二天还要继续调度。这一章的取舍原则就一句话目标函数决定经济性约束决定可行性二者必须在同一个模型里同时存在这就是Joint optimization区别于分步调度的本质。3. 用Python解Joint optimizationMILP建模与可复现代码模型建好了下一步就是把它变成能跑的代码。我用的是Python加PuLP开源免费CBC求解器对中等规模问题完全够用读者可以直接复现。3.1 准备数据96点负荷曲线与火电、储能参数时间尺度先选24个时段每时段1小时跑通后再改成96点15分钟粒度。负荷曲线用一组典型日数据包含早晚两个峰值时段这样储能才有发挥空间。火电参数用一台200MW机组的典型值储能按电网侧储能电站的常见配置来。先定义基础数据代码里每一步都有注释。# -*- coding: utf-8 -*- # 火电-储能联合优化调度Joint optimization最小可跑示例 # 时间尺度24时段步长 dt 1h import pulp # 1. 数据准备 T 24 # 调度时段数 dt 1 # 步长小时 # 典型日负荷曲线单位MW含早晚两个高峰 load [100, 95, 90, 88, 90, 95, 110, 135, 150, 160, 165, 155, 150, 145, 150, 160, 175, 190, 200, 185, 160, 140, 120, 105] # 火电参数 g_min, g_max 50, 200 # 出力上下限MW ramp 60 # 爬坡速率MW/h a_coal, b_coal, c_coal 0.0008, 20, 50 # 煤耗成本系数 start_cost 2000 # 单次启动成本元 # 储能参数 e_max 80 # 储能容量MWh p_c_max p_d_max 30 # 最大充/放电功率MW eta_c eta_d 0.92 # 充/放电效率 soc_min, soc_max 0.1, 0.9 # SOC 上下限 soc_0 0.5 # 初始 SOC deg_cost 50 # 储能退化成本元/MWh penalty_load 5000 # 弃负荷惩罚元/MWh数据准备阶段最容易忽略的是量纲统一。负荷单位是MW储能容量单位是MWhSOC是标幺值三者通过时间步长dt串联。这里的dt1小时所以功率乘以时间就是能量可以直接和MWh相加。如果把15分钟数据配dt1能量会差四倍目标函数和SOC轨迹全部错乱。我建议先把单位写在注释里每写一个参数都问一句它是MW还是MWh这个习惯能省掉大量排查时间。3.2 构建并求解PuLP代码与关键参数说明模型构建部分把目标函数和约束逐段写进PuLP。变量定义这里火电出力和储能功率用连续变量启停状态和充放电互斥用二元变量。SOC变量额外多一个时段因为递推关系要覆盖从初始时刻到末尾的全过程。# 2. 模型构建 prob pulp.LpProblem(Thermal_Storage_CoOptimization, pulp.LpMinimize) # 决策变量 p_g pulp.LpVariable.dicts(p_g, range(T), lowBoundg_min, upBoundg_max) u pulp.LpVariable.dicts(u, range(T), catBinary) # 火电启停 start_var pulp.LpVariable.dicts(start, range(T), catBinary) # 启动标志 p_c pulp.LpVariable.dicts(p_c, range(T), lowBound0, upBoundp_c_max) p_d pulp.LpVariable.dicts(p_d, range(T), lowBound0, upBoundp_d_max) z_c pulp.LpVariable.dicts(z_c, range(T), catBinary) # 充电互斥标识 z_d pulp.LpVariable.dicts(z_d, range(T), catBinary) # 放电互斥标识 soc pulp.LpVariable.dicts(soc, range(T 1), lowBoundsoc_min, upBoundsoc_max) curtail pulp.LpVariable.dicts(curtail, range(T), lowBound0) # 弃负荷量 # 目标函数煤耗线性近似 启动成本 储能退化 弃负荷惩罚 prob ( pulp.lpSum(b_coal * p_g[t] c_coal for t in range(T)) pulp.lpSum(start_cost * start_var[t] for t in range(T)) pulp.lpSum(deg_cost * p_d[t] for t in range(T)) pulp.lpSum(penalty_load * curtail[t] for t in range(T)) pulp.lpSum(20 * (soc[t] - 0.5) ** 2 for t in range(T 1)) ) # 3. 约束条件 for t in range(T): # 功率平衡火电 储能放电 - 储能充电 弃负荷 负荷 prob p_g[t] p_d[t] - p_c[t] curtail[t] load[t] # 火电出力受启停状态约束 prob p_g[t] g_max * u[t] prob p_g[t] g_min * u[t] # 启动标志只有从停机变开机才算一次启动 if t 0: prob start_var[t] u[t] - 0 # 初始默认停机 else: prob start_var[t] u[t] - u[t - 1] # 爬坡约束相邻时段出力变化不能超过爬坡速率 if t 1: prob p_g[t] - p_g[t - 1] ramp * dt prob p_g[t - 1] - p_g[t] ramp * dt # 储能充放电互斥 prob p_c[t] p_c_max * z_c[t] prob p_d[t] p_d_max * z_d[t] prob z_c[t] z_d[t] 1 # SOC递推充电加效率放电除效率 prob soc[t 1] soc[t] (eta_c * p_c[t] - p_d[t] / eta_d) * dt / e_max # 首尾 SOC 约束 prob soc[0] soc_0 prob soc[T] 0.3 # 末端 SOC 不小于 0.3目标函数里煤耗部分我用了线性近似只取了b_coal和c_coal两项二次项改用惩罚项的平方形式做柔性约束这样代码简洁且数值稳定。如果读者用Gurobi或CPLEX可以加二次项直接求解。SOC平方项用于压低SOC偏离50%的趋势系数20是经验值太大SOC会被钉死在50%失去调节能力太小又起不到约束作用。启动标志的逻辑需要仔细看只有u[t]从0变1时才计一次启动成本这个约束在混合整数规划里是标准写法写反了启动次数会错好几倍。3.3 读结果从调度曲线看优化质量模型求解后把关键结果打印出来看三大指标火电出力曲线是否平滑、SOC是否在合理区间内来回摆动、以及弃负荷量是否为零。如果SOC曲线像锯齿一样频繁触及上下限说明储能退化成本设得太低如果火电出力频繁爬满爬坡速率说明储能容量或功率配置偏小。把结果画成曲线检查是优化的最后一步也是最容易省略的一步。# 4. 求解与结果 prob.solve() print(求解状态, pulp.LpStatus[prob.status]) print(总成本%.2f 元 % pulp.value(prob.objective)) for t in range(T): print(ft{t:2d} 负荷{load[t]:3d} 火电{p_g[t].varValue:6.2f} f充{p_c[t].varValue:4.2f} 放{p_d[t].varValue:4.2f} fSOC{soc[t].varValue:.3f} 弃负荷{curtail[t].varValue:.2f})求解状态如果是Infeasible先去检查SOC递推方程的效率方向再检查功率平衡约束是不是写成了单向。总成本里每一项可以拆开看占比煤耗通常是最大头启动成本如果占比超过10%说明模型在频繁启停要加最小启停时间约束或用更平滑的目标。SOC轨迹要重点看如果SOC只在几个时段大幅变化说明储能没有被充分利用如果SOC每个时段都在满幅摆动说明目标函数没有给储能合理的调节激励。4. 储能优化参数整定容量、SOC、爬坡率怎么配合才不出废案跑通模型只是第一步真正考验功夫的是参数整定。同一个模型换一组参数可能从经济调度变成“电池折磨器”。这一章用一组算例参数说明每个参数对结果的影响方向和调整原则。参数取值影响方向调参建议SOC上限0.9上限越高储能可放能量越多锂电建议0.9过高加速衰减SOC下限0.1下限越低储能可充空间越大不要低于0.1避免过放充/放电效率0.92效率越低储能套利空间越小用实测值不要用铭牌值储能容量80 MWh容量越大削峰填谷能力越强按负荷峰谷差和持续时间算最大充放电功率30 MW功率决定响应速度功率容量比一般0.25到0.5C火电爬坡速率60 MW/h爬坡越慢储能越忙从机组设计值获取火电最小出力50 MW最小出力高时储能被迫充电深度调峰改造后可调低末端SOC约束0.3防模型终结时放空按次日预测手动设4.1 储能侧SOC上下限、效率、初始SOC的影响SOC上下限是储能优化最容易“拍脑袋”的参数。上限设太低储能的可放容量变小削峰能力受限上限设太高电池长期处于高SOC区间循环寿命下降。我的经验是锂电按0.1到0.9设置如果项目方对寿命敏感压缩到0.2到0.8更保守。效率参数一定要用实测值很多项目用铭牌效率0.95实际运行只有0.88左右效率低估会让模型高估储能收益调度结果执行时SOC对不上计划。初始SOC和末端SOC是联合优化的隐性约束初始SOC设太高储能一上来就能放电前期火电压力小末端SOC约束保留余量第二天调度才有缓冲。我建议把末端SOC从0.3起步然后跑不同值对比成本变化如果提高末端SOC只增加少量成本说明储能本来就该在最后时段留有余量。4.2 火电侧爬坡率与最小出力对储能调度的制约火电爬坡速率决定储能“不得不干活”的量级。爬坡越快储能越轻松联合优化的价值越小爬坡越慢储能越忙Joint optimization的价值越明显。实际机组爬坡率在30到90MW每小时的区间取低值映射到模型里是储能调度方案更保守也更好执行的路线。火电最小出力是一个更隐蔽的约束夜间负荷低谷时段如果火电最小出力高于负荷储能就必须充电吸收多余电力这就是强制充电场景。遇到这种情况要么降火电出力下限要么扩大储能充电功率两个参数要一起调整才能让模型有可行解。我见过一个项目火电最小出力调低了20MW储能容量需求直接少了四分之一这就是联合优化的连锁效应。4.3 目标函数权重惩罚项别拍脑袋惩罚项系数是最难标定的参数。储能退化成本如果按电池全生命周期折算可以这样算电池造价除以总循环寿命再乘以放电深度得到一个元每MWh的值。一套80MWh的锂电系统如果造价4000万元循环寿命6000次可放电总量约40万MWh退化成本就是100元每MWh上下。这个量级经常比峰谷价差还高意味着纯套利根本不划算储能的真正价值在减少火电启停和爬坡损耗。弃负荷惩罚建议设成煤耗成本的20倍以上让它在任何情况下都不被主动触发。SOC惩罚系数没有物理对应值需要不断调赋太低SOC轨迹剧烈波动赋太高储能在每个时段都往回拉失去削峰能力。一个可参考的做法是先设0跑一遍看SOC轨迹再逐步加大到轨迹明显平滑为止。5. 储能调度实战避坑从求解失败到结果不可行的排查清单联合优化模型跑起来不难跑好很难。整理几个最常见的翻车记录每一条都是真实项目里踩过的坑按“现象、原因、解决”的思路写方便对号入座。5.1 现象一模型报Infeasible功率平衡还是SOC出了问题模型返回Infeasible是最典型的初版症状。原因通常有三个SOC递推方程效率放反了导致储能每时段都在“凭空多出来”或“凭空消失”能量功率平衡约束忘了加弃负荷松弛变量负荷高峰时火电和储能拼尽全力也够不着SOC上下限太紧比如把下限设为0.5夜间低谷时段储能没处充电直接无解。排查顺序我一般先看SOC边界再看功率平衡的松弛变量最后检查初始SOC和末端SOC约束是否自洽。解决方法是先加宽SOC边界到0到1跑通再逐步收紧到合理值每次只动一个参数找到临界点。5.2 现象二SOC轨迹“充满即放空”来回震荡储能SOC像锯齿一样在上下限之间反复弹跳说明目标函数里缺少SOC平滑机制或退化成本过低。充放电本身有损耗每次循环都在消耗能量如果目标函数没有为循环次数买单求解器就会滥用储能。这种震荡在实际执行中非常伤电池一个循环一次的调度方案电池寿命按年缩水。解决方法是给储能加退化成本系数按放电电量和充放电深度双重计费再加上SOC偏离惩罚项让SOC倾向于工作在中部区间。调完参数后看SOC变化率一个时段内SOC变化超过20%就要警惕。5.3 现象三目标值过大或为负成本系数量纲不对目标值是负的或者数值大到离谱基本可以断定是量纲混乱。最常犯的错是把储能容量写成MW而不是MWh或者把成本系数按小时费用和按时段费用混在一起。我处理过一个案例煤耗的二次项系数少写了一个数量级目标值直接变成原来的零头调度结果里火电一直顶在最大出力。排查量纲的方法是手算一个简单场景把负荷变平储能不动作用目标函数手工计算该时段的成本和模型输出对比。如果一致说明量纲没错不一致就逐项检查每条成本项的物理单位。5.4 现象四火电频繁启停启动成本被无视模型给出的调度计划里火电每三四个小时启停一次启停成本变成总成本的大头。原因是启动标志约束写成了双向判断或者干脆没加启动成本导致求解器把火电当开关用。好在混合整数规划能正确处理启停成本但前提是代码写对。解决方法是给启动标志加约束同时把启停成本设成足够大的值如果仍然频繁启停就加最小启停时间约束给火电至少连续运行4小时或停机2小时的限制。这个约束会让模型更符合实际机组运行规程还能显著缩短求解时间。5.5 现象五备用约束没建模结果在实际运行中不抗扰动很多初版模型只做功率平衡不做备用约束导致调度结果完全没有安全裕度。实际运行中负荷预测误差、新能源波动、机组跳闸都需要备用容量兜底。如果储能为了省钱在某个时段放电放到SOC下限此时电网需要储能提供上备用储能根本没有能力响应。解决办法是加旋转备用约束每个时段火电可用的上调容量加上储能剩余可放电量必须大于备用需求。备用需求通常取预测负荷的5%或最大单机容量二者取较大值。加了备用约束后火电出力计划会上移储能SOC会保留更多余量整体成本增加但调度方案才有实用价值。这一章五条记录实际项目里至少能命中两条。排查时核心原则只有一个先查约束再看目标函数先查量纲再调参数。6. 把静态调度升级成滚动优化一个能应对负荷波动的进阶写法前面写的都是全天开环调度适合做规划和评估。真正部署到生产环境时负荷预测值每15分钟刷新一次全天固定计划根本扛不住波动。进阶做法是滚动时域控制也叫模型预测控制每个周期只做未来N个时段的优化但只执行第一个时段的指令下个周期重新滚一遍。这样既能保留Joint optimization的全局视角又能吸收最新预测信息是储能调度从仿真走向实用的必经之路。# 滚动时域调度伪代码每15分钟滚一次 for step in range(total_steps): window build_prediction_window(step, horizon8) # 取未来8时段预测负荷 model build_optimization_model( windowwindow, soc_currentmeasured_soc, # 用实测SOC不用模型推算值 p_g_lastmeasured_p_g, # 用实测火电出力 ) model.solve() dispatch(step, model.p_g[0], model.p_d[0]) # 只执行第一个时段指令滚动优化有两个关键习惯。一个是每个周期用实测SOC和实测火电出力做初值而不是用上一轮模型的推算值。模型推算在长周期里误差累积实测值回灌能把偏差归零这是避免调度计划渐渐漂移的有效手段。另一个是预测窗口长度的取舍窗口太短模型只看得到眼前储能会在窗口末尾频繁放空充电窗口太长预测误差变大约束可能反向误导。我常用的配置是火电爬坡慢用8到12个时段储能容量大用12个时段具体数字要对比不同窗口下的累计执行成本再定。验证滚动优化效果时对比开环调度和滚动优化的三条曲线SOC轨迹、火电出力曲线、以及日累积成本。好的滚动优化SOC轨迹平稳火电出力变化小成本虽然可能略高于开环理想值但它是真正可执行的数字。我习惯在滚动模型里加一个执行偏差统计项每步记录计划功率和实际功率的差偏差超过3%就检查是不是储能效率参数没校准。这个习惯帮我提前发现过两次电池实际容量衰减导致的调度偏差比事后盘点成本划算得多。储能调度这个方向坑都在细节里效率方向、SOC初值、量纲、爬坡约束每一个看起来都不起眼积累起来足以让一个项目的上线计划推迟一个月。Joint optimization本身不是银弹但把火电和储能放在同一个模型里对话确实是当前电网储能调度性价比最高的路径。如果你正在做类似方案把模型跑通后先别急着上先进算法用一周的实测数据做滚动回测把参数一个个校准好我相信你也能看到调度成本肉眼可见地降下来。希望帮到你。本文还有配套的精品资源点击获取