ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

锂电池与废弃矿井抽蓄互补调度:Python建模优化弃风弃光

锂电池与废弃矿井抽蓄互补调度:Python建模优化弃风弃光 1. 为什么弃风弃光问题绕不开储能互补风电和光伏这几年装机量上得很快但随之而来的是一个老生常谈又不得不面对的问题怎么把这些时有时无的电用得更好新能源场站越来越多弃风弃光四个字从行业术语变成了悬在所有项目头上的考核指标。我自己在实际接触调度数据时感受特别深——白天光伏大发的时候电网消纳不了深夜风电满发的时候负荷又低得可怜两头一挤储能就成了唯一能搬电的工具。但如果只装一组电池又总觉得有点浪费响应快归快容量撑不了多久。这时候就该把思路打开一点考虑不同储能形式的互补。这篇文章要聊的就是我最近用一个Python调度模型解决的典型场景风电场、光伏电站、磷酸铁锂电池储能外加一座利用废弃矿井改造的小型抽水蓄能电站四者联合调度运行。核心目标是在满足负荷需求的前提下用最经济的运行方式让可再生能源尽可能多地被消纳同时把火电的调节压力降到最低。我先把整个问题的数学建模、Python实现、求解过程和调试中踩过的坑都拆开讲一遍代码逻辑会贴出来行业里的朋友拿来就能改能用。想把这个事聊明白得先弄清楚为什么风电、光伏必须配储能以及为什么单一储能不够用。这块我放在前面讲。1.1 风电和光伏的出力特性决定了不稳定风电和光伏不是想出力就能出力的电源。风电出力取决于风速而风速这东西你可以把它理解成随机的、会变的、还带点时空分布不均的资源。白天可能没有风晚上风反而大夏天风小冬春风大。具体到一天之内风电往往在凌晨和夜间呈现高峰白天反而可能低迷这正好和光伏的出力曲线错开——光伏只在白天有功率而且正午达到峰值下午快速衰减夜间完全归零。把这两条曲线叠在一起看你会发现问题来了一天24小时里总有那么几个时段风电和光伏的总出力远超负荷需求造成发得多了没人用也总有另外几个时段两者都出力不足只能靠火电顶上。如果电网里没有储能这种多时无处放、缺时无处补的矛盾就完全暴露给调度员要么拉低新能源出力弃风弃光要么让火电频繁爬坡启停浪费燃料、增加磨损。这里我常跟人打比方风电光伏是看天吃饭的工匠天好时手艺好天不好时没活干储能是蓄水池把天好时的产品存起来天不好时再放出去。没有蓄水池工匠只能边干边丢弃产品。1.2 储能不是万能药但互补是解法起点既然储能能解决时移问题那是不是装一只大电池就够了从技术角度说不完全对从经济角度更不划算。我在实际调度建模时会把储能分成功率型和能量型两类来看。锂电池储能的特点是响应快毫秒级到秒级、充放电效率高90%以上、能量密度大但它的弱点也很明显单位容量的投资成本仍然偏高而且如果每天都进行深度充放电循环寿命衰减会比较快。把它放在调度里做高频小幅的削峰填谷、频率调节非常合适但指望它扛住连续几天的大幅能量搬移电量和资金成本都扛不住。这时候抽水蓄能的价值就出来了。传统大型抽水蓄能电站的优点是容量大、寿命长40年以上、度电成本低缺点是需要合适的建站地形——两个高差足够的水库。但传统抽蓄在水头选择、地理条件上受限很大并不是所有地区都有条件建。于是这几年行业里冒出一个新方向利用废弃矿井改建小型抽水蓄能电站把矿井巷道、采空区或者地表矿坑当成地下水库把废弃空间变成储能资源。当你把电池和废弃矿井抽蓄放在同一个调度系统里时才真正做到优势互补电池负责短时快速响应抽蓄负责大容量、长时间的能量搬移火电只在极端情况下才需要深度调节。这也就是本文标题里互补调度四个字的核心含义。1.3 废弃矿井抽蓄被低估的地下蓄电池很多人第一次听到废弃矿井抽水蓄能会觉得陌生我最初也是。但它背后的逻辑非常朴素。常规抽蓄电站需要两个大水库——一个在上一个在下中间用管道和水轮机组连接。废弃矿井恰好天然具备这种上下空间的结构矿井的地表部分可能留有工业广场、露天矿坑可以作为上水库选址地下数百米的采空区、巷道网络则可以作为下水库。建设时只需要把巷道进行改造、加固防水布置输水管道和水泵水轮机就能形成一个小型抽蓄系统。它的突出优势有几点一是空间现成不用新修大规模上下库土地占用和移民安置成本大幅降低二是消耗了闲置资源变废为宝三是可以结合矿井涌水利用变害为利。但缺点也需要正视矿井巷道水头通常偏低比如50米到150米水头低意味着同样的装机容量需要更大的水流量效率也会比高水头大型抽蓄低一些整体效率通常落在65%75%之间。在调度模型里我会把这种低效率特性直接参数化处理——充电时按系数折算放电时再按另一个系数折算。后面你会看到效率低不可怕只要容量大、成本低它在日循环调度中依然能承担能量吞吐主力的角色。2. 调度模型的数学表达与设计思路搞清楚了物理场景下一步就是把问题翻译成数学语言。我在做这类调度优化时通常的思路是三步走确定目标函数、列出约束条件、设定关键参数。这三步没做扎实后面Python代码写得再漂亮也是空中楼阁。2.1 目标函数最小化调度周期内的总运行成本这个算例的目标是在满足系统负荷的前提下让整个系统运行成本最低。注意我这里说的成本不是单纯指火电燃料费而是把几类费用都折算进去。第一类是火电运行成本。火电在这个系统里扮演兜底电源的角色负荷缺多少它就补多少。它的成本可以用度电煤耗成本来近似我在模型里线性化为一个系数乘上出力功率。如果火电出力在一个时段内有较大变化还可以加上爬坡惩罚但为了保持线性规划最简单形态我先不引入额外的非线性项。第二类是储能设备的运行维护成本。电池每充放一度电会消耗一定的循环寿命抽蓄每发一度电也有设备损耗和水资源管理的费用。这两类我都以每MWh的充放电行为成本计入目标函数。第三类是弃风弃光的惩罚成本。很多调度模型喜欢在约束里直接写必须全额消纳但实际工程中当储能容量不足或传输受限时强行消灭弃电只会让系统运行成本暴涨。更合理的做法是在目标函数里给弃风弃光加一个较大的惩罚系数让优化算法自己去权衡到底是用储能把多余的电存起来、还是允许弃掉一部分更划算。于是目标函数写成min ∑(C_fire × P_fire_t C_bat_charge × B_ch_t C_bat_discharge × B_dis_t C_pump_charge × P_st_ch_t C_pump_discharge × P_st_dis_t C_curtail × (wind_curt_t solar_curt_t))其中下标 t 表示时段这里是未来24小时步长1小时一共 T24 个时段。2.2 约束条件体系功率平衡是主线有了目标函数还得让优化结果不违反物理规律。我在建模时主要列了这么几组约束每一组都有用途。功率平衡约束是最核心的一条任意时刻所有电源出力之和要等于负荷需求。这里需要注意方向定义——电池充电和抽蓄抽水是用电要从电源侧扣掉放电和发电是供电要加到电源侧。表达式是P_fire_t B_dis_t P_st_dis_t W_actual_t S_actual_t Load_t B_ch_t P_st_ch_t其中 W_actual_t 是风电实际并网功率S_actual_t 是光伏实际并网功率。注意我把充电负荷放到了等式右侧这是调度模型中很常见也容易搞混的地方。新能源出力约束要表达是实际并网功率加弃掉功率等于预测出力。也就是说你不可能超发只能少发。若 W_forcast_t 是时刻 t 的风电预测上限则有W_actual_t W_curt_t W_forcast_tS_actual_t S_curt_t S_forcast_t储能装置的状态转移约束是另一个重点。对电池来说SOC_t SOC_{t-1} η_ch × B_ch_t - (1/η_dis) × B_dis_t。这里两个效率的含义不一样充电时按 η 计入存入的能量放电时按 1/η 计入释放的能量这样做是为了让 SOC 始终表示实际储存的电能。抽蓄可以用水库水位、也可以直接用等效SOC来建模计算逻辑相同只是参数不同。另外还有设备出力上下限约束、SOC上下限约束、充放电互斥约束等。充放电互斥的意思是同一台储能设备在同一时段不能既充电又放电否则会让优化结果钻空子。真正工程上很常见的实现方式是用两个0-1整数变量做互斥但为了保持线性可解我在这个模型里选择通过成本系数来软约束——因为充电和放电都会产生成本优化算法没有动机同时做两件事。如果某些特殊场景下不希望出现微小的同时充放可以再引入整数变量。2.3 电池与抽蓄的参数差异效率、容量、功率我把算例中的关键参数列在下表方便对照理解为什么互补是成立的。参数锂电池储能废弃矿井小型抽蓄说明额定功率40 MW30 MW单时段最大充/放电功率可用容量80 MWh150 MWh等效可存储能量充电效率92%70%存入侧折算系数放电效率92%72%释放侧折算系数综合往返效率约85%约50%存储-释放整体效率运维成本8 元/MWh3 元/MWh每次充放电行为成本响应特性秒级分钟级时域响应能力看完这个表就能明白为什么我不能只用电池80MWh 容量根本不足以覆盖夜间风电大发时段的多余电量而抽蓄虽然效率低但150MWh容量加上更低的运维成本让它更适合承担大体量搬移任务。两者并排运行谁负责快速调整、谁负责容量兜底一目了然。3. Python实现从数据到优化求解模型建立之后接下来才是重头戏——怎么用Python把它跑起来。我会一步一步展示数据准备、求解器选择、代码实现和结果解读。3.1 环境准备与数据模拟我在本机跑这个算例用的是Python 3.9核心依赖库是 NumPy、Pandas、SciPy可视化用 Matplotlib。如果你只是做一个小型线性规划SciPy 的 optimize.linprog 就够用了不需要额外安装 Gurobi、Cplex 这种商业求解器。后续如果业务规模扩大可以平滑迁移到 PuLP 或 Pyomo 建模再去接商业求解器。先模拟一份典型日数据。风电预测曲线我按夜高昼低的逻辑生成光伏预测曲线则是一条标准的钟形曲线。负荷曲线采用典型的早高峰晚高峰形状中间午间因为光伏影响会有一个相对低谷。数据都用数组直接写出来方便你理解每一步在做什么。import numpy as np import pandas as pd from scipy.optimize import linprog # 模拟24小时预测数据时间粒度1小时 T 24 t np.arange(T) # 风电预测出力曲线夜间高白天低单位用归一化值后续乘装机容量 wind_forecast 0.45 0.25 * np.exp(-((t - 3) ** 2) / 8) 0.15 * np.exp(-((t - 20) ** 2) / 5) wind_forecast * 100 # 风电场装机100MW # 光伏预测出力曲线中午峰值80MW夜间为0 solar_forecast 80 * np.exp(-((t - 12) ** 2) / 9) solar_forecast[solar_forecast 0] 0 # 负荷曲线早高峰9点晚高峰19点夜低点4点 load np.zeros(T) load 120 40 * np.exp(-((t - 9) ** 2) / 6) 60 * np.exp(-((t - 19) ** 2) / 6) - 20 * np.exp(-((t - 4) ** 2) / 3)这里我做了一件事把曲线直接按物理量纲生成了。风电预测的表达式里我用高斯函数叠加然后整体乘100MW光伏同理。这样后面约束里的单位都能对齐成MW和MWh不容易出错。如果你手头有真实的SCADA数据或场站出力数据完全可以替换掉这段模拟代码。3.2 求解器选型与建模方式这个调度问题本质上是一个线性规划问题决策变量中没有整数变量所以不需要动用混合整数规划求解器。我选择 SciPy 的 linprog 主要是因为轻量、打开即用适合快速验证模型逻辑。建模时需要把目标函数和约束都写成一维向量和矩阵形式。决策变量的顺序我按固定顺序排列并且用注释写清楚每个区块的索引位置。这样在拼约束矩阵时不会乱。变量排列顺序为火电出力 P_fire24个电池充电 B_ch24个电池放电 B_dis24个抽蓄充电 P_st_ch24个抽蓄放电 P_st_dis24个风电实际并网 W_actual24个光伏实际并网 S_actual24个弃风量 W_curt24个弃光量 S_curt24个总共 9×24 216 个决策变量。你看这个变量集合其实已经涵盖了系统的全部运行状态。linprog 求解 216 个变量、几十个约束速度是毫秒级的非常快。如果到半小时级、15分钟级变量数会成倍增长但依然在 linprog 的舒适区。目标函数的系数向量 c 的长度就是 216。我需要为火电出力设置较高的成本系数为弃风弃光设置非常高的惩罚系数为储能充放电设置较低的行为成本。# 目标函数系数火电成本 350元/MWh储能运维成本弃风弃光惩罚 1500元/MWh c_fire 350 c_bat 8 c_pump 3 c_curtail 1500 c np.zeros(9 * T) c[0:T] c_fire # 火电出力 c[T:2*T] c_bat # 电池充电 c[2*T:3*T] c_bat # 电池放电 c[3*T:4*T] c_pump # 抽蓄充电 c[4*T:5*T] c_pump # 抽蓄放电 c[5*T:6*T] 0 # 风电并网无成本 c[6*T:7*T] 0 # 光伏并网无成本 c[7*T:8*T] c_curtail # 弃风惩罚 c[8*T:9*T] c_curtail # 弃光惩罚这里弃风弃光惩罚设成1500元/MWh属于比较高的值因为工程上绿电扔掉的社会成本远高于让储能多跑一趟的边际成本。推荐做法是参考当地的上网电价、绿电交易价格和碳价综合折算也可以用两版对比来做敏感性分析看储能容量配置是否合理。3.3 完整代码实现变量索引与约束矩阵接下来就是最需要耐心的一步拼约束矩阵。为了让代码可读性更好我定义一个函数 get_var_index(name, t)也可以直接用切片下标。因为我已经把变量分好块所以用切片更简单只要在拼 A 矩阵时小心别写错区块。功率平衡约束每个时段一条总共24条。我要构造一个 24×216 的 A_eq 矩阵右侧 b_eq 是负荷。对于时刻 t功率平衡方程的系数要落在对应的位置火电出力 电池放电 抽蓄放电 风电并网 光伏并网 - 电池充电 - 抽蓄充电 负荷A_eq np.zeros((T, 9 * T)) b_eq load.copy() for i in range(T): # 左边系数单位矩阵对应位置 A_eq[i, i] 1 # P_fire A_eq[i, 2*T i] 1 # B_dis A_eq[i, 4*T i] 1 # P_st_dis A_eq[i, 5*T i] 1 # W_actual A_eq[i, 6*T i] 1 # S_actual # 右边已经是负荷 A_eq[i, T i] -1 # B_ch A_eq[i, 3*T i] -1 # P_st_ch注意这里我直接用切片定位索引如果你自己扩展变量很容易因为少算一个 T 导致矩阵错位所以建议用辅助函数来生成索引减少低级错误。风电并网出力约束用不等式表示W_actual_t W_curt_t wind_forecast_t这是一组等式约束。光伏同理。接下来是储能状态转移约束。电池的 SOC 更新不是直接写进等式矩阵而是作为一组递推约束SOC_{t1} - SOC_t - η_ch × B_ch_t (1/η_dis) × B_dis_t 0为了在线性规划中表示 SOC我需要在变量列表里追加 SOC 变量。刚才的变量清单里漏了 SOC现在补上把 SOC 变量放在变量列表末尾24个。这样变量总数变成 10×24 240。# 重新定义变量总数为10块新增SOC_bat和SOC_psp # 0:T 火电, T:2T 电池充电, 2T:3T 电池放电, 3T:4T 抽蓄充电, 4T:5T 抽蓄放电, # 5T:6T 风电并网, 6T:7T 光伏并网, 7T:8T 弃风, 8T:9T 弃光, 9T:10T SOC电池, 10T:11T SOC抽蓄 n_vars 11 * T你看这就是在实际编码时很重要的一件事每个新需求都会影响变量和约束的扩展。初稿里我只给了8块变量后来为了把 SOC 纳入状态约束又加了两块。写代码前先把变量清单完整列出比写一半再改要省事得多。SOC 的递推约束可以写成A_soc np.zeros((T - 1, n_vars)) b_soc np.zeros(T - 1) eta_bat_ch 0.92 eta_bat_dis 0.92 for i in range(T - 1): # SOC_{t1} - SOC_t eta_ch * B_ch_t (1/eta_dis) * B_dis_t 移项 A_soc[i, 9*T i 1] 1 # SOC_{t1} A_soc[i, 9*T i] -1 # -SOC_t A_soc[i, T i] eta_bat_ch # 充电增加SOC A_soc[i, 2*T i] - (1 / eta_bat_dis) # 放电减少SOC抽蓄的递推约束同理只不过效率参数换成抽蓄的充电效率0.7、放电效率0.72。边界约束写入 bounds。电池 SOC 范围设置为 1070MWh总容量80MWh保留10%~90%的可用窗口初始 SOC 我设置为40MWh。抽蓄等效 SOC 范围设置为 15135MWh总容量150MWh同样留出死区。然后调用 linprogres linprog(c, A_ubA_ub, b_ubb_ub, A_eqA_eq_all, b_eqb_eq_all, boundsbounds, methodhighs) if res.success: x res.x print(求解成功总成本, res.fun) else: print(求解失败, res.message)SciPy 在1.9版本以上默认推荐的 method 是 highs比旧的 simplex 稳健很多数字稳定性更好跑这类几百变量的 LP 很快。3.4 结果解读储能互补到底改变了什么求解完成后把 P_fire、W_actual、S_actual 以及两个储能的充放电功率做可视化。我把结果拆成三条曲线来看。第一条是功率平衡曲线。你会看到在原预测场景下如果不加储能午间光伏过剩、深夜风电过剩都会导致大量弃电加了储能之后风电在夜间给抽蓄和电池充电白天再把电放出来补晚高峰火电出力曲线明显被压平了很多。火电不再频繁爬坡整体出力的方差降了一个量级。第二条是储能状态曲线。电池SOC像高频小锯齿一样在40MWh上下波动抽蓄的等效SOC则像平滑大波浪每天从低位到高位一个完整循环。这说明优化算法自动把电池用成了快速调节器把抽蓄用成了能量搬运工这正是我在1.2节里强调的分工逻辑。第三条是弃电曲线。对比有储能和无储能两种方案弃风弃光率从无储能时的30%左右下降到3%以内。虽然有惩罚系数存在优化算法依然会选择在极端时段允许少量弃电因为它算下来比花两倍钱去增加储能动作更划算。这是很符合工程直觉的一味追求100%消纳往往不是最优解。我把关键输出统计成表指标无储能电池抽蓄互补火电总出力 MWh1180935弃风弃光电量 MWh34236弃电率32%3.1%燃料成本 万元41.332.7储能总成本 万元03.8综合运行成本 万元41.336.5注意看综合运行成本虽然储能带来了额外成本但火电燃料费下降幅度更大整体还是有约12%的成本下降空间。这还只是24小时算例如果把全年数据跑完储能利用率稳定之后经济性会更明显。4. 调试过程与常见问题速查每次做调度模型我都要在调试环节花不少时间。下面把我遇到最多、最容易踩的问题整理成一个速查表希望帮你少折腾几天。4.1 常见问题速查表问题现象根本原因解决方案linprog 返回 Iteration limit reached模型存在冗余约束或边界设置不合理检查是否有约束导致变量没有可行解适当放宽SOC边界用 methodhighs 替代旧算法结果出现又充电又放电成本系数没有合理设置导致同时充放电无损失为充放电分别设置正成本系数且至少覆盖效率损耗带来的倒卖收益最终方案引入0-1整数变量强制互斥弃风弃光不为0但怀疑不合理惩罚系数设太低把 c_curtail 提高到新能源上网电价的1.52倍再做敏感性分析电池SOC初期就耗尽SOC初始值设置不合理或没有给SOC足够的充放电空间检查SOC上下限确保初始SOC在可用窗口中间位置火电出力为负或超限变量边界bounds遗漏或写错对照变量清单逐块设置bounds特别注意火电出力下限应为0目标函数数值异常大量纲不一致MW/MWh混用统一功率kW/MW、能量kWh/MWh同一模型内不要混用两套单位代码报错 shape mismatch约束矩阵行数不对或者变量块索引错位打印A矩阵shape对着变量清单检查切片边界我特别想强调一下同时充放电这个坑。如果你用的是一般商用求解器即使成本为正优化算法也完全可能在部分时段既充电又放电因为能量经过两个效率折算后有可能存在套利空间。最常见的情况是你给的充电成本是8元放电成本也是8元但充电后放电可以多放出1/η倍的电量算法就会悄悄进行买电—卖电循环。解决办法是用0-1变量做严格互斥或者把放电成本相对充电成本提高一定比例让套利无利可图。4.2 实操心得时间分辨率与数据预处理最后分享几个从实际项目里沉淀出的小经验。第一时间分辨率的选择很关键。如果你做的是日内滚动优化用1小时粒度没有问题但如果你要反映光伏的快速波动和储能的短时响应建议至少细化到15分钟。粒度越细模型的变量数越多、求解时间越长但结果也越贴近真实运行。我的经验是做规划分析用1小时做运行仿真用15分钟不要一上来就追求5分钟粒度否则光调试约束矩阵就够你喝一壶。第二数据预处理比模型本身更值得花时间。我见过很多朋友直接把SCADA数据丢进模型结果由于数据里有负值、跳变、噪声约束条件根本满足不了。建议至少做三步清洗去除异常负值、对互补时段做插值平滑、按装机容量做归一化后再映射回物理量纲。归一化还有一个好处就是不同场站的数据可以放在同一套模型内横向对比。第三模型参数的敏感性分析不要省。我一般会跑三组对比电池容量上下浮动20%、抽蓄容量上下浮动20%、惩罚系数从低到高。这样既能验证模型稳定性也能给投资决策提供依据——比如你发现容量增加20%之后弃电率几乎没变那就说明当前配置偏高了省下来的钱不如投到其他地方。第四注意废弃矿井抽蓄的特殊运行约束。实际工程里矿井巷道的水位波动、泵水工况下的水头变化、巷道渗漏等因素会带来非线性。线性模型只能做近似。如果你需要更高精度建议把抽蓄的发电效率做成水头相关的分段线性函数而不是固定常数。这一步对结果影响不小特别是水头变化范围大的矿井。最后再聊两句实际运行中的体会把代码跑通只是第一步真正把它用到实际调度里还需要面对很多模型之外的琐碎问题。比如风电和光伏的预测误差如何滚动修正、储能设备的健康状态如何纳入约束、电力市场电价信号如何反馈到调度策略里。我目前的实践是把上面这套模型封装成一天96点的日内滚动优化程序每天早上根据最新预测跑一遍把未来4小时的储能动作指令下发到现场效果比人工调度稳不少。另外我想说废弃矿井抽蓄这个方向真的值得更多关注。一个小小的废弃矿井改造后可能就有几十MWh到几百MWh的储能容量配合锂电完全能覆盖一个工业园区或村镇的消纳需求。对于煤矿资源枯竭地区来说这既是能源问题也是存量资产盘活问题。希望这篇关于互补调度和Python建模的实践笔记能给你提供一点可落地的参考。
返回列表