ARTICLE DETAIL

资讯详情

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

Python建模求解:电池与废弃矿井抽蓄互补的储能调度策略

Python建模求解:电池与废弃矿井抽蓄互补的储能调度策略 做新能源并网调度的人每天都要面对风电和光伏的“暴脾气”风一来整排风机满发风一停出力直接掉到零光照按天周期波动一片云飘过光伏出力可能砍半。要把这些波动消化掉储能几乎是绕不开的选项。但这个项目的特别之处在于我把电池储能和废弃矿井改造的小型抽水蓄能电站放进了同一个互补调度框架用Python建模求解研究它们在不同场景下怎么分工、怎么协同。这篇文章把建模思路、代码实现、求解结果和一堆踩坑记录完整梳理一遍适合正在做多能互补、微电网调度或者新能源消纳研究的朋友参考也适合刚接触优化调度、想用Python把MILP模型跑起来的新手。1. 风电光伏的天性决定了储能的配置逻辑先讲一个我做这个项目时反复体会到的结论调度策略不是凭空设计的而是由风光出力的“时间结构”逼出来的。风电和光伏虽然都是清洁能源但它们的出力曲线性格完全不同。1.1 风电出力的反调峰特征风电的出力主要取决于风速。我做了个简化模型用Weibull分布生成风速序列再通过风机功率曲线映射成出力。典型结果是这样的白天负荷高的时候风速可能很小风机出力接近于零到了凌晨负荷跌到低谷风速反而上来了风机满发。这个现象叫反调峰——风大的时候电网不需要那么多电电网需要电的时候风又没了。没有储能的情况下凌晨弃风几乎是必然的因为负荷吃不下联络线又送不出去。风电还有一个特点就是出力波动是“分钟级”的。一阵强风过来几十台风机可能同时增加出力风突然停了出力又快速回落。这种短时间尺度的变化对调频和备用容量都提出了要求。所以风电配储能首先要解决的是“多出来的电往哪放”和“缺电的时候谁先顶上来”这两个问题。1.2 光伏出力的日内规律与气象扰动光伏的出力规律比风电好预测得多太阳出来就有功率中午达到峰值晚上回到零这是确定性的大趋势。但叠加云层遮挡之后光伏出力会出现明显的短时锯齿形波动。我在数据生成里加了Beta分布的随机扰动模拟多云天气下辐照度的剧烈变化。光伏带来的调度难题是“午间高峰拥挤”。尤其在晴朗天气中午光伏大发如果此时负荷不高又没有足够的储能充电能力逆变器就不得不降功率运行这就是弃光。和风电反调峰不同光伏弃电的高发时段是明确的——每天中午那三四个小时。这给储能调度提供了一个很直接的目标把午间的光伏电量搬到晚高峰去用。1.3 为什么单一储能方案不够用如果只配电池短时波动能压住但遇到连续两三天阴天加小风电池容量再大也会被掏空而且电池的循环寿命有限每天高频深充深放衰减非常快。如果只配抽水蓄能它的响应速度慢、功率密度低无法处理分钟级的光伏扰动。所以把电池和废弃矿井小型抽蓄放在一起让它们各管一段才是合理的思路。这也是这个项目名字里“互补”二字的真正含义——不只是风光互补更是两类储能特性之间的互补。2. 电池与废弃矿井小型抽蓄的互补性拆解要在一个模型里同时调度两种储能首先得弄清楚它们各自适合干什么。这一节不绕弯子直接对比工程特性。特性电池储能废弃矿井小型抽水蓄能典型功率等级10~100MW2~10MW典型容量20~200MWh30~150MWh综合往返效率85~95%65~75%响应速度毫秒级分钟级工况切换循环寿命数千次深充深放衰减明显数十年循环次数限制小主要成本功率和容量成本都高改建成本为主容量成本低适合的角色短时高频调节、调频、削峰日级能量搬移、长时间填谷这个表是我做模型参数标定时实际采用的逻辑。注意看效率那一行电池效率高所以如果同样一度电存进去再放出来电池损失小模型会很自然地优先用电池做短时循环抽蓄效率低但如果需要存5个小时、放5个小时电池的容量成本会撑不住抽蓄的优势就出来了。2.1 电池储能短时高频响应的主力电池在调度模型里可以看作一个“响应极快、状态连续”的能量容器功率边界和SOC边界是核心约束。它适合跟随光伏的分钟级波动也适合在早晚负荷爬坡的时候快速顶上去。我在目标函数里加了电池吞吐退化成本所以模型不会让电池做无意义的频繁充放这很关键——不加退化成本的话求解器可能为了让电池动而动产生大量无意义的循环。2.2 废弃矿井小型抽蓄被低估的长时能量库废弃矿井改造抽蓄的原理并不复杂矿井关闭后地下巷道和竖井还在巷道空间作为下水库地面的旧井口设施或者矿坑改造为上水库利用矿井本身的落差形成水头。它不像传统抽蓄需要建坝淹地而是把废弃的煤矿基础设施重新利用起来属于典型的资源盘活项目。模型里我把它简化为一个带双向效率的能量容器抽水时消耗电能把水从下库抽到上库相当于是“充电”发电时水从上库流下来推动机组相当于是“放电”。但和电池不同它的转换效率不是对称的抽水功率和发电功率也可以不一样。常规做法是分别设置水泵工况最大功率和发电工况最大功率。2.3 两种储能在调度模型中的角色边界建模型之前我先给两种储能定了角色基调电池承担高频分量负责把风电出力的锯齿抹平抽蓄承担低频分量负责把凌晨的风电或者午间的光伏搬到晚高峰。这个角色边界不需要硬编码到约束里它会在优化求解中自然涌现——因为电池退化成本高、效率高抽蓄容量大、边际成本低求解器会自动把短时调整分配给电池把长时搬移分配给抽蓄。我在第5章会给出具体场景的调度结果来验证这一点。3. 调度模型的数学表述与目标函数设计这是整个项目的核心。调度运行的本质就是在满足功率平衡和各类设备物理约束的前提下最小化综合运行成本、最大化清洁能源消纳。我用的是确定性混合整数线性规划MILP时间粒度取15分钟调度周期24小时即96个时段。3.1 决策变量与时间尺度的选择决策变量分三类。第一类是连续功率变量风电实际出力、光伏实际出力、电池充电功率、电池放电功率、抽蓄抽水功率、抽蓄发电功率、从电网购电功率、向电网售电功率、弃风量和弃光量。第二类是状态变量电池的SOC、抽蓄的储能量。第三类是二进制变量电池充放电状态指示、抽蓄抽水/发电状态指示。时间尺度这里有个权衡。1小时粒度模型简单、求解快但看不清光伏云层遮挡造成的波动也体现不出电池高频调节的价值。15分钟粒度能反映日内调度细节但MILP规模成倍增加。如果后续要扩展到一周甚至一个月我建议降到1小时粒度做长周期规划再对重点日做15分钟精细化调度。这个分层思路比一上来就全用细粒度更实用。3.2 目标函数经济性、消纳率与惩罚项的平衡目标函数表面上是成本最小化实际上是把多个目标加权聚合成单目标min Σ_t ( c_buy[t] * P_buy[t] - c_sell[t] * P_sell[t] λ_w * P_w_curt[t] λ_pv * P_pv_curt[t] c_bat_deg * (P_bat_ch[t] P_bat_dis[t]) )每项的含义拆开说一下。购电成本和售电收益构成了与电网交互的净成本。我采用分时电价低谷时段0.25元/kWh、平段0.50元/kWh、高峰0.85元/kWh上网售电价格低于购电价防止模型利用买卖价差做没有物理意义的套利。弃风弃光惩罚系数λ设置得比较高取1.0元/kWh相当于告诉求解器只要系统还有能力消纳清洁能源就不要轻易弃电。电池退化成本我取0.05元/kWh的吞吐量这个值可以根据电池厂商的循环寿命数据折算出来。这里要说明一个经验惩罚系数的量级很重要。如果λ太小模型会宁可弃掉100MWh风电也不愿意让抽蓄多抽几十MWh的水因为抽蓄效率低有损耗如果λ太大模型又会不顾电网交换功率限制强行消纳导致约束无解。我在调参时保持λ高于购电价格、低于停电损失估算值才得到相对合理的折中方案。3.3 功率平衡、SOC与周期约束的推导功率平衡约束是所有调度模型的基石它的含义是每一时刻系统发出的功率必须等于消耗的功率。P_w_use[t] P_pv_use[t] P_bat_dis[t] P_psh_gen[t] P_buy[t] P_load[t] P_bat_ch[t] P_psh_pump[t] P_sell[t]注意左侧是电源右侧是负荷漏掉任何一项都会导致无解或者结果失真。我最早写这个约束时漏了售电项导致有光伏剩余时模型只能弃电结果弃光率被严重高估。风电和光伏的出力约束写法是P_w_use[t] P_w_curt[t] P_w_forecast[t] P_pv_use[t] P_pv_curt[t] P_pv_forecast[t]电池的状态转移方程是SOC[t] SOC[t-1] η_ch * P_bat_ch[t] * Δt - P_bat_dis[t] / η_dis * Δt其中Δt是时段时长15分钟就是0.25小时。这里的效率放置和单位换算必须小心充电时电量增加效率乘在充电功率上放电时电量减少放电功率要除以效率。如果两个效率都简单乘在功率前面能量守恒就错了SOC会在几天模拟里逐渐漂移。抽蓄的能量状态方程类似但两个工况效率分开E_psh[t] E_psh[t-1] η_pump * P_psh_pump[t] * Δt - P_psh_gen[t] / η_gen * Δt还需要加周期约束也就是调度结束后储能回到初始状态SOC[0] SOC_T 0.5 E_psh[0] E_psh_T 0.5 * E_psh_max这个约束的处理是第6章无解问题的关键。充电和放电的互斥约束用大M法加二进制变量实现P_bat_ch[t] ≤ M * u_bat_ch[t] P_bat_dis[t] ≤ M * u_bat_dis[t] u_bat_ch[t] u_bat_dis[t] ≤ 1M取该功率变量的上限值即可不要取一个巨大的数否则会破坏求解器的数值稳定性我实际踩过这个坑后面详说。4. Python实现数据序列、建模接口与求解器选型模型数学化之后Python部分其实是体力活但有一些工程组织的细节值得展开。整体我拆成了三个模块数据准备、模型构建、求解与结果分析。不要把所有东西写在一个脚本里后续调参和换场景会非常痛苦。4.1 输入数据准备风电、光伏、负荷序列怎么来研究阶段如果没有实测数据可以先生成合成数据验证模型逻辑。风电出力我用Weibull分布生成风速再通过风机功率曲线映射import numpy as np def wind_power_series(n_hours, rated_power100, v_cut_in3, v_rated12, v_cut_out25): rng np.random.default_rng(42) v rng.weibull(2, n_hours) * 8.0 p np.zeros(n_hours) for i, vi in enumerate(v): if v_cut_in vi v_rated: p[i] rated_power * (vi**3 - v_cut_in**3) / (v_rated**3 - v_cut_in**3) elif v_rated vi v_cut_out: p[i] rated_power return p光伏出力则按日内正弦模板加云层扰动def pv_power_series(n_hours, peak80): t np.arange(n_hours) % 24 solar np.sin(np.pi * (t - 6) / 12) * (t 6) * (t 18) cloud np.random.default_rng(7).beta(2, 5, n_hours) p peak * solar.clip(0, 1) * cloud return p注意这里的功率单位统一为MW时间单位统一为小时。负荷曲线我用了典型的双峰形状峰值120MW早晚各一个高峰。实际项目里这些序列应该来自数值天气预报或者历史出力数据。但即便是合成数据也要注意一个原则数据的物理合理性比数据的精确性更重要。光伏出力不会在夜间突然冒出一百多兆瓦风速也不会在同一分钟里从零跳到额定值这些边界条件如果不符合物理模型会花费大量计算资源去处理一个现实中不存在的场景。4.2 用Pyomo把数学模型翻译成代码我用Pyomo建模因为它能无缝对接多个求解器而且约束写法和数学公式的对应关系非常直观。核心模型结构如下import pyomo.environ as pyo model pyo.ConcreteModel() model.T pyo.Set(initializerange(96), doc15分钟粒度24小时) # 参数 model.dt pyo.Param(initialize0.25) model.p_w_forecast pyo.Param(model.T, initializew_data) model.p_pv_forecast pyo.Param(model.T, initializepv_data) model.p_load pyo.Param(model.T, initializeload_data) # 变量 model.p_w_use pyo.Var(model.T, withinpyo.NonNegativeReals) model.p_w_curt pyo.Var(model.T, withinpyo.NonNegativeReals) model.soc pyo.Var(model.T, bounds(0.1, 0.9), initialize0.5) model.u_bat_ch pyo.Var(model.T, withinpyo.Binary) def balance_rule(m, t): return (m.p_w_use[t] m.p_pv_use[t] m.p_bat_dis[t] m.p_psh_gen[t] m.p_buy[t] m.p_load[t] m.p_bat_ch[t] m.p_pump[t] m.p_sell[t]) model.balance pyo.Constraint(model.T, rulebalance_rule)这里只展示了骨架实际完整代码还包括风电出力上限约束、光伏出力上限约束、抽蓄状态方程、周期约束、互斥约束和联络线功率限制。每类约束单独写一个rule函数命名要能看懂是为了哪个物理约束服务的不然调试的时候自己都搞不清哪条约束报错。4.3 求解器选型开源、商用与实用经验同样一个96时段的MILP模型求解器选错了体验天差地别。我实测比较过几种求解器授权96时段MILP求解时间备注GLPK开源跑不完或极慢适合几十个变量的教学模型CBC开源数分钟到十几分钟勉强可用调参空间小HiGHS开源几十秒开源里性价比最高推荐优先试Gurobi商业学术免费秒级大规模场景下的首选CPLEX商业学术免费秒级与Gurobi同级如果只是跑通逻辑、验证模型正确性先用HiGHS就够了完全免费Pyomo可以通过appsi_highs接口直接调用。如果目标是做更长时间维度、更多随机场景的研究比如一个月8760个时段加蒙特卡洛抽样那建议申请Gurobi的学术授权省下的时间远比配置成本值。4.4 工程代码结构模块化而非脚本堆叠我把整个项目分成四个文件目录结构如下data_generator.py生成风电、光伏、负荷序列保存成csvmodel.py定义Pyomo模型输入是数据文件路径输出是模型对象solve.py调用求解器、读取结果、把关键变量转成DataFramevisualize.py画功率平衡曲线、SOC曲线、弃电率堆叠图这样做的最大好处是换一组数据不需要改模型代码换一个求解器只需要改solve.py里的一行调用。我在做参数灵敏度分析时循环改数据文件、批量调用solve.py输出结果对比表整个流程非常顺畅。如果全堆在一个脚本里改一次参数要重新跑一遍从头到尾的代码浪费的时间不可小觑。5. 三种典型场景下的调度结果与储能分工模型建好后真正有意思的部分来了看不同场景下储能怎么分工。我构造了三个典型日场景分别对应晴朗大风日、夜间大风白日阴天日、持续弱风阴天日。系统参数不变风电装机100MW、光伏装机80MW、负荷峰值120MW、电池功率50MW容量100MWh、抽蓄功率10MW容量80MWh、联络线交换功率上限25MW。5.1 晴朗大风日光伏午间高峰与电池削峰填谷这个场景最简单风电和光伏都充足。调度结果里电池表现非常活跃上午10点到下午15点之间光伏出力超过负荷需求电池开始大功率充电把多余的光伏电量存起来傍晚17点以后光伏出力快速下降负荷进入晚高峰电池开始放电。抽蓄在这个场景里几乎没有存在感全天发电量只有几MWh。原因不难理解抽蓄的往返效率只有70%左右电池有90%以上同样是搬移电量电池的损耗小得多而且电池的功率上限高能在午间短短几个小时内完成充电。这说明在电量富余但窗口时间不长的场景高效率储能天然占优。5.2 夜间大风白日阴天日抽蓄的长时能量搬移这个场景最能体现抽蓄的价值。凌晨1点到5点风速很高光伏出力为零负荷处于低谷风电远远超过负荷需求。此时电池开始充电但电池容量只有100MWh而凌晨富余的风电总电量可能达到两三百MWh电池根本装不下。于是抽蓄在凌晨启动抽水把富余风电转化为水的势能。白天天亮了但云层很厚光伏出力只有额定值的百分之十几午间负荷又要往上走。此时电池因为凌晨已经基本充满白天主要任务是平抑光伏波动晚高峰来临时抽蓄开始发电持续释放4到6个小时正好覆盖负荷晚高峰。这就是我前面说的角色分工电池解决短时问题抽蓄解决长时能量搬移。5.3 持续弱风阴天日储能耗尽后的边界行为这个场景是最考验模型的。风电出力全天很低光伏几乎为零负荷只能主要由电网提供。此时储能策略变成了“保底”电池在凌晨低电价时少量充电在晚高峰高电价时放电尽量降低购电成本抽蓄也做类似操作但因为它效率低只在预计晚高峰电价较高时才动作。值得注意的结果是模型允许少量弃电率为零因为可再生能源出力本来就小于负荷同时购电成本大幅上升。这时候储能的作用不是消纳新能源而是削峰填谷、降低购电成本。这说明同一个调度模型在不同场景下会自动切换优化目标的重心储能容量的价值也随之变化。5.4 结果量化对比消纳率与运行成本我把三个场景的关键指标整理成一张表这样对比更直观指标晴朗大风日夜间大风日弱风阴天日弃风弃光率0.8%2.1%0%电池总充放电量 (MWh)9612842抽蓄总抽水电量 (MWh)86812抽蓄总发电量 (MWh)6529从电网购电量 (MWh)180320820清洁能源占比96%85%61%表格里的具体数值取决于我生成的合成数据不同人跑出来会有差异但这个趋势是稳定的风光充足时弃电率可以压到很低电池负责高频、抽蓄负责低频的分工模式清晰风光不足时储能转为削峰填谷角色系统对电网的依赖显著上升。做研究报告时这类对比表比单独一张调度曲线有说服力得多。6. 调试过程中踩过的坑与排查链路这个项目前前后后调试了两周大部分时间都耗在MILP的“无解”“结果异常”“求解慢”这三类问题上。下面把排查链路写完整这些都是常规文档里不会讲的实战经验。6.1 模型无解从去掉约束到逐个加回的排查法第一次跑模型求解器直接返回infeasible也就是找不到可行解。新手遇到这种情况最容易懵因为哪里错了都不知道。我的排查步骤是先固定所有二进制变量为某一组合理值把MILP退化成LP看是否可行不行的话再逐步去掉约束看哪一个约束是被卡住的关键。实际操作中我最快的方法是把目标函数改成常数然后逐个禁用约束组把“无解”定位到某条约束上。这次的问题出在周期约束。我把SOC初始值设为0.5同时要求调度期末SOC也等于0.5但电池容量和风电出力边界下执行周期约束的唯一方法是提前预留一部分电量而我把SOC下限设成了0.1初始值0.5看起来可行实际却和某个时段的充电上限冲突导致夜间某时段必须强制放电但电量不够。最终我把初始SOC调到0.3并把周期约束从严格等式放宽到允许终端SOC在0.3到0.7之间问题就解决了。经验是周期约束和SOC边界是天生有张力的初始值不能拍脑袋写要先用不含周期约束的模型跑一遍看自然的端到端SOC轨迹落在哪个区间再设置合理的周期约束边界。6.2 电池同时充放电Big-M与二进制互斥约束的数值问题第二个坑是我故意在一次关闭互斥约束后踩的。现象是结果里同一时段电池充电功率和放电功率都不为零两个功率在平衡方程里互相抵消看起来没有违反功率平衡但实际上白白浪费了效率还消耗了寿命。根本原因是没有物理约束禁止同时充放电而求解器发现同时充放可以在某些边界条件下“优化”目标函数比如用低价电充电的同时向电网报高价放电在中性平衡的掩护下套利。解决方法是加二进制互斥变量但这引出了新的坑——Big-M取值。我一开始图省事把M取成1e6结果求解器数值稳定性变差出现一些荒谬的结果比如充电功率只有0.001MW但状态变量是1浪费二进制变量精度。正确做法是M取该变量的物理上限电池充电上限50MW就取50抽蓄抽水上限10MW就取10。取最小有效Big-M对求解速度也有正面帮助分支定界的搜索空间会小得多。6.3 抽蓄SOC漂移η不一致带来的能量单位换算错误抽蓄状态方程我改过三版才写对。第一版用的是“充放都用效率乘系数”结果96个时段跑完储能量凭空多出来几十MWh直接突破了容量上限约束的检查。后来查出来问题在于单位混用时间步长不小心用分钟数代入而功率单位是MW导致能量单位变成了MWh的16倍。这种错误在模型能求解的条件下很难肉眼发现只有在检查SOC边界是否越界时才会暴露。正确的写法是抽水工况乘效率、发电工况除效率时间步长严格用小时。我还加了一层防御性检查结果出来后立刻统计一个调度周期内能量变化的净增减再计算系统总的输入输出电量差值两者应当吻合。这样就能快速判断状态方程有没有写对。6.4 求解时间爆炸时间分辨率与MIP gap的平衡96时段的MILP问题二进制变量有几百个用CBC求解要花十分多钟来回调参很痛苦。解决思路有三个。第一个是换求解器HiGHS比CBC快很多Gurobi更快这是最直接的手段。第二个是放宽MIP gap默认gap是1e-4实际上是过分优化调度问题允许1%甚至3%的gap误差完全可接受求解时间能缩短到原来的十分之一。第三个是砍时间分辨率96时段改成24时段变量数量锐减到原来的四分之一求解时间可以压缩到秒级缺点是调度曲线细节丢失。我在项目中采用的是“分级求解”预调度用24时段粗粒度模型快速求解确定储能大致的充放电模式再用96时段细粒度模型将粗粒度结果作为初始可行解传入显著加速收敛。这个技巧在有MIP start功能的求解器上效果尤其明显。7. 后续扩展从确定性调度走向滚动优化当前模型是确定性调度也就是假设风电、光伏的预测值完全准确一次性算出全天计划。实际运行中预测误差是常态所以下一步的扩展方向非常明确。7.1 从确定性到随机规划比较自然的扩展是场景法随机优化。用风速和辐照度的概率分布生成多个离散场景每个场景对应一组风电光伏出力序列目标函数变成所有场景下的期望成本最小化。代价是模型规模成倍扩大96时段、20个场景MILP求解时间可能会从秒级变成分钟级。这个方向适合做学术研究能定量回答“预测误差对调度成本的影响有多大”这类问题。7.2 滚动时域模型预测控制工程上更实用的做法是模型预测控制每15分钟或每小时滚动重新求解一次只执行当前时段到未来几个时段的决策然后用最新的预测数据刷新模型。这样调度决策始终在跟踪最新的风光预测对突发云层遮挡、风速骤变都有很好的适应性。实现上只需要把当前模型包进一个循环每次更新预测参数后重新求解再提取当前时段的控制量下发复杂度比随机优化低很多。7.3 把调度模型接到电力市场或虚拟电厂框架如果再往前走一步这套模型可以和现货市场价格联立让储能参与能量市场和辅助服务市场。电池响应快可以同时参与调频和能量套利抽蓄容量大、爬坡慢更适合参与日前的能量交易。虚拟电厂场景下这套模型就是内部能量管理系统的核心求解引擎上面接聚合商策略下面接各类分布式资源。我现在跑这个模型的标准动作是先验证数据物理合理性再检查约束单位和效率系数然后跑粗粒度可行性测试最后才上细粒度和多场景。这个流程能避免90%的调试返工。互补调度这件事本质不是把模型建得多么复杂而是搞清楚每一种储能适合出现在一天中的哪个时段、承担什么角色。模型会给你一张漂亮的调度曲线但你能不能解释清楚每条曲线的成因才是真正检验理解深度的时刻。
返回列表