风电、光伏与储能含电池和废弃矿井小型抽水蓄能互补调度运行研究这个课题我刚接手的时候以为就是一个普通的“储能配置加日前机组组合”的小项目写点Python跑完就收工。真正做进去才发现坑不在储能本身而在于怎么让两种物理特性完全不同的储能——一个十几毫秒就能响应的电池一个要几分钟才完成工况切换的小型抽水蓄能——在同一个优化模型里和谐共处还得让风电和光伏的预测误差得到有效吸收。这篇文章把整个研究从问题拆解、数学建模到Python实现的完整过程铺开讲清楚。内容偏工程和科研中间的灰色地带适合正在做电力系统优化调度课题的研究生、搞园区微网或综合能源系统设计的工程师以及所有想快速把MILP调度模型跑通的朋友。1. 项目整体设计与思路拆解1.1 为什么把风电、光伏、电池和废弃矿井抽蓄绑在一起先说一个最简单的道理风电场和光伏电站出力靠天吃饭调度员最怕的就是“午间光伏拉满、负荷趴在低位”和“夜间风电猛发、负荷早睡了”这两类时段。单一储能可以削峰填谷但纯电池方案有个尴尬——容量做大之后初期投资和度电成本双双走高每天循环几百次还心疼寿命纯抽水蓄能呢响应速度按分钟算要让它去平抑分钟级的风速波动只能说杀鸡用了牛刀。把两者放在同一个调度框架里是近几年的主流做法典型的叫“混合储能”或“多时间尺度储能”。电池负责秒级到分钟级的功率波动平抑和高频分量跟踪抽水蓄能负责小时级、日级的能量搬移。举个例子某天中午光伏大发四小时电池可以在这四小时里持续充电但它容量只有几十兆瓦时撑满两小时就到顶了抽蓄如果上库容量够大可以四小时持续抽水、晚上再放水发电正好覆盖一个完整的负荷早高峰或晚高峰。再看“废弃矿井”这个关键词。这不是天方夜谭我们平时看到的大型抽水蓄能要新建上库、下库修大坝筑拦水坝选址极其苛刻。而废弃矿井不一样竖井本身就是天然的垂直落差井底巷道和硐室是现成的下库空间井口周边低洼地或矸石山改造后就是上库岩体已经开挖支护好了水工结构改造量大幅减少。对矿区来说这等于把一块已经报废的地下空间重新变成了储能资产从工程经济性角度非常划算。所以这个课题的调度对象有三个电源风电、光伏、抽蓄发电、两个负荷型储能电池充电、抽蓄抽水再加一个电网交互端口购售电。核心逻辑可以概括成一句话在满足负荷平衡的前提下通过日内各时段的有功协调让出力波动尽量被本地储能吸收减少弃风弃光同时算清楚每种储能参与调度后的综合经济性。1.2 总体技术路线三层架构与Python工具链我把整个系统拆成三层来设计这样代码结构清晰后面扩展也容易。第一层是数据与预测层。风电场和光伏电站的出力预测是调度模型的输入很多研究直接用历史实测数据或者气象预报数据。如果课题条件有限也可以用典型出力曲线加随机扰动的方式构造。这一层在Python里就是Pandas和NumPy的地盘处理时间序列对齐、缺失值插补、滚动窗口平滑。第二层是优化决策层核心是一个混合整数线性规划模型。决策变量包括各时段的电池充放电功率、抽蓄发电/抽水功率、弃风弃光量、购售电功率有些模型还会加机组启停变量。目标函数根据课题定位来定我做的是最小化系统总运行成本同时加弃电惩罚项。求解用开源求解器CBC通过PuLP调用或者商业求解器Gurobi/CPLEX模型规模可控CBC在24时段、变量几百个的情况下完全跑得动。第三层是结果分析与可视化层。调度结果需要还原成功率曲线和储能状态曲线用Matplotlib把风电、光伏、负荷、电池SOC、抽蓄库容画在一张图上看互补效果同时计算弃电率、储能利用率、运行成本等指标输出调度报告。三层之间的数据流是这样的预测层产出96点或24点的功率序列优化层读入并求解分析层把求解结果落成图表和指标表格。整个链路用Python串起来脚本化运行换一组数据就能重新调度这也是为什么我选Python而不是专业仿真软件——后者封闭、难改、贵前者虽然有性能上限但做研究足够灵活。2. 调度模型的数学表达与参数设计2.1 目标函数成本最小之外还得算电池寿命和弃电惩罚调度模型的目标函数直接决定了系统在边缘工况下怎么决策。我做的最基础版本是纯经济目标即最小化系统日运行总成本。表达式拆开来看min Σ [ C_buy(t)·P_buy(t) - C_sell(t)·P_sell(t) ] Σ [ C_om_wind·P_wind(t) C_om_pv·P_pv(t) C_om_phs·(P_pump(t)P_gen(t)) ] Σ [ C_bat_degradation·(|P_bat_c(t)| |P_bat_d(t)|) ] Σ [ λ_curtail·(P_wind_avail(t)-P_wind(t) P_pv_avail(t)-P_pv(t)) ]第一项是购售电成本买电为正费用卖电为负费用即收入。第二项是风电、光伏和抽蓄的运行维护成本单位运维成本一般取每兆瓦时几元到几十元。第三项是电池寿命损耗折算成本这是很多初版模型会漏掉的一项却是混合储能经济性对比的关键——如果电池损耗成本不进入目标函数求解器会让电池在低价时段疯狂充电、高价时段全放完实际工程中这种运行方式会大幅缩短寿命。第四项是弃风弃光惩罚目的是防止系统为了节省储能损耗成本而主动放弃清洁能源惩罚系数一般取比售电价格略低但又高于储能边际损耗的值。做研究时还可以在目标函数里叠加更多维度比如碳排放目标、最小化弃电率目标用加权法或约束法转成单目标。我的建议是第一步先把经济目标设置好把电池寿命成本纳入进去先得到一个合理基线再逐步增加目标维度。另外如果手头有分时电价数据买入价和卖出价不对称时求解器会自然利用价差套利这正是储能盈利的重要渠道建模时一定要区分购电和售电两条路径不能合并成一个净交换功率。2.2 约束体系功率平衡、电池SOC、抽蓄水量与状态互斥约束是调度模型的骨架我按四大类组织。第一类是系统功率平衡约束每个时段所有电源出力和负荷、储能充电功率必须实时相等P_wind(t) P_pv(t) P_phs_gen(t) P_bat_d(t) P_buy(t) P_load(t) P_phs_pump(t) P_bat_c(t) P_sell(t)这条等式是模型的核心缺失它整个调度就散了。在实际代码里我推荐为每条平衡约束命名比如power_balance_t_14一旦出现无解求解器的松弛报告能直接指向是哪个时段出了问题。第二类是电池约束。电池的荷电状态按小时递推SOC(t) SOC(t-1) η_bat_c·P_bat_c(t)·Δt / E_bat_max - P_bat_d(t)·Δt / (η_bat_d·E_bat_max)SOC要限制在安全区间比如0.1到0.9这是防止过充过放。充放电功率各有一条上限约束同时需要引入一组二进制变量实现充放电互斥——不能同一时段既充电又放电否则求解器可能会用“充1兆瓦、放1兆瓦”这种自欺欺人的方式满足平衡约束白白损耗能量。第三类是抽蓄约束。我做了一个工程简化把上下水库的水量用“等效能容量”E_res来表示单位是兆瓦时不显式建模水位与面积的曲线关系。递推式为E_res(t) E_res(t-1) η_pump·P_pump(t)·Δt - P_gen(t)·Δt / η_genE_res也有上下限对应水库死水位和正常蓄水位之间的有效容量。抽蓄发电和抽水工况同样用二进制变量互斥这比电池的互斥更重要——抽蓄机组不能同时抽水和发电这是物理上的一票否决。第四类是电网交互约束。购电功率和售电功率分别设上限取决于联络线容量变压器限值。这一条常常被初学者忽略导致调度结果里出现高达几百兆瓦的不切实际购电功率一算联络线根本承载不了。2.3 预测误差怎么处理滚动调度与场景法选型调度研究绕不开预测误差。风电和光伏的预测精度有限日前预测误差可以轻松达到15%到25%如果调度模型假设预测完全准确执行时就会出大问题。我的处理思路分三个层次按课题深度可以选择。最基础的是确定性日前调度把预测值当作真实值求解24小时最优计划。这个方案适合作为研究基线简单、稳健、可复现。进阶一点的是滚动调度每隔15分钟或1小时把最新预测和实测状态拿进来重新求解未来4小时到8小时的计划只执行第一个时段。这种方式对代码结构要求高一些但效果提升明显相当于用执行频率弥补预测精度。再往上就是随机优化场景法。用历史误差分布生成几十组风电光伏场景把目标函数改成所有场景的期望值约束在每组场景下都必须满足。这个方案计算量成倍增长但结果更贴近实际运行。我个人经验是先用确定性模型把业务逻辑跑通再升级到滚动调度场景法作为论文深化方向写进去就够没必要一上来就全做。3. Python代码实现全流程解析3.1 数据准备把气象、负荷、设备参数整理成可计算的数据结构调度模型再漂亮数据不干净什么都白搭。我的数据准备过程分四步。第一步是时间序列对齐。风电场、光伏电站、负荷数据来自不同系统采样间隔不一致是常态。统一到同一时间索引推荐用Pandas的reindex加interpolate不要直接dropna否则会把夜间的光伏零出力时段丢掉后面模型会误判。第二步是数据清洗。风速仪卡涩、光伏逆变器停机、负荷传感器漂移都会产生异常值我用的规则很简单超过物理限值的点置为限值连续三个点跳变超过50%的口径就标记异常并插值替换。这一步宁可保守不要激进过度清洗会把真实波动抹掉。第三步是构造预测序列。如果只有历史实测数据可以用三种方法生成调度输入直接取前一天实测、取近七天同一时段平均值、或用简单自回归模型外推。作为研究用途近七天平均往往比复杂深度学习模型更稳因为调度模型更关心的是趋势和量级不是逐点精度。第四步是统一参数成字典结构。所有设备参数集中放在一个params {}字典里包括电池容量、充放电效率、抽蓄额定功率、库容、上下限、电价序列、惩罚系数。这样后面调参、做灵敏度分析非常方便也方便复现实验。我吃过亏的是把参数散落在代码各个角落换一组工况时改漏一个结果全部白算。3.2 MILP模型求解基于PuLP的核心代码拆解优化主体我用PuLP实现它是Python生态里最友好的线性规划建模库之一。完整模型代码约150行核心结构如下。先定义问题并创建决策变量import numpy as np from pulp import LpProblem, LpMinimize, LpVariable, LpStatus, lpSum, LpContinuous, LpInteger T 24 dt 1.0 # 时间分辨率单位小时 # 预测与负荷数据示例实际读csv P_wind_fc np.array([30, 32, 34, 35, 33, ...]) # 24时段风电预测出力 MW P_pv_fc np.array([0, 0, 2, 8, 15, ...]) # 光伏预测出力 MW P_load np.array([45, 42, 40, 38, 39, ...]) # 负荷 MW # 分时电价元/MWh price_buy np.array([0.45]*8 [0.65]*4 [0.5]*8 [0.8]*4) price_sell np.array([0.30]*24) model LpProblem(Hybrid_Complementary_Scheduling, LpMinimize) # 连续变量 x_wind {t: LpVariable(fx_wind_{t}, 0, None) for t in range(T)} x_pv {t: LpVariable(fx_pv_{t}, 0, None) for t in range(T)} P_buy {t: LpVariable(fP_buy_{t}, 0, 100) for t in range(T)} P_sell {t: LpVariable(fP_sell_{t}, 0, 50) for t in range(T)} P_bat_c {t: LpVariable(fP_bat_c_{t}, 0, 20) for t in range(T)} P_bat_d {t: LpVariable(fP_bat_d_{t}, 0, 20) for t in range(T)} SOC {t: LpVariable(fSOC_{t}, 0.1, 0.9) for t in range(T)} P_phs_p {t: LpVariable(fP_phs_p_{t}, 0, 20) for t in range(T)} P_phs_g {t: LpVariable(fP_phs_g_{t}, 0, 20) for t in range(T)} E_res {t: LpVariable(fE_res_{t}, 50, 500) for t in range(T)} curtail {t: LpVariable(fcurtail_{t}, 0, None) for t in range(T)} # 整数变量电池充放电互斥、抽蓄工况互斥 u_bat {t: LpVariable(fu_bat_{t}, 0, 1, catLpInteger) for t in range(T)} u_phs {t: LpVariable(fu_phs_{t}, 0, 1, catLpInteger) for t in range(T)}目标函数把买电成本、卖电收益、弃电惩罚放进一个lpSum里model lpSum(P_buy[t] * price_buy[t] - P_sell[t] * price_sell[t] for t in range(T)) lpSum(100 * curtail[t] for t in range(T))接下来添加约束。功率平衡、新能源消纳上限、电池SOC递推、抽蓄库容递推、充放电互斥这几组循环写下来注意每个约束都带一个名字排查时能直接定位# 功率平衡 for t in range(T): model (x_wind[t] x_pv[t] P_phs_g[t] P_bat_d[t] P_buy[t] P_load[t] P_phs_p[t] P_bat_c[t] P_sell[t]), fbalance_{t} # 消纳上限 for t in range(T): model x_wind[t] P_wind_fc[t], fwind_{t} model x_pv[t] P_pv_fc[t], fpv_{t} model curtail[t] P_wind_fc[t] - x_wind[t] P_pv_fc[t] - x_pv[t], fcurtail_{t} # 电池SOC递推 E_bat_max 40 for t in range(T): if t 0: model SOC[t] 0.5 (0.95 * P_bat_c[t] * dt - P_bat_d[t] * dt / 0.95) / E_bat_max else: model SOC[t] SOC[t-1] (0.95 * P_bat_c[t] * dt - P_bat_d[t] * dt / 0.95) / E_bat_max # 充放电互斥 for t in range(T): model P_bat_c[t] 20 * u_bat[t], fbat_c_{t} model P_bat_d[t] 20 * (1 - u_bat[t]), fbat_d_{t} # 抽蓄等效能容量递推500 MWh库容效率泵0.85、发0.90 for t in range(T): if t 0: model E_res[t] 250 (0.85 * P_phs_p[t] * dt - P_phs_g[t] * dt / 0.90) else: model E_res[t] E_res[t-1] (0.85 * P_phs_p[t] * dt - P_phs_g[t] * dt / 0.90) # 抽蓄工况互斥 for t in range(T): model P_phs_p[t] 20 * u_phs[t], fphs_p_{t} model P_phs_g[t] 20 * (1 - u_phs[t]), fphs_g_{t} # 求解 model.solve() print(求解状态:, LpStatus[model.status])这段代码可以直接运行验证只要把数组填满24个数值。求解状态Optimal代表模型在可行域内找到了全局最优解如果状态是Infeasible或Undefined先回头查约束冲突——90%的情况是初始SOC和容量范围设置矛盾。3.3 结果可视化与调度报告调度结果一定要用图说话否则审稿人、导师和合作方都不会满意。我用Matplotlib画一张“功率平衡堆叠图”和一张“储能状态曲线图”。功率平衡图把风电、光伏、抽蓄出力、电池放电作为正向堆叠负荷加上电池充电、抽水功率作为反向需求两条区域的包络线能直观看出系统是否平衡。储能状态图把电池SOC和抽蓄E_res画在双纵轴上就能看到电池高频小幅度波动、抽蓄低频大幅度搬运的互补特征。import matplotlib.pyplot as plt t np.arange(T) fig, ax1 plt.subplots(figsize(12, 5)) ax1.stackplot(t, [x_wind[i].value() for i in range(T)], [x_pv[i].value() for i in range(T)], [P_bat_d[i].value() for i in range(T)], [P_phs_g[i].value() for i in range(T)], labels[Wind, PV, Battery Discharge, PHS Generation], alpha0.7) ax1.plot(t, P_load, k--, labelLoad) ax1.set_ylabel(Power (MW)) ax2 ax1.twinx() ax2.plot(t, [E_res[i].value() for i in range(T)], r-, labelPHS Reservoir) ax2.set_ylabel(Reservoir Energy (MWh)) plt.tight_layout() plt.show()除了画图调度报告还要输出三个关键指标弃电率、储能利用率和日运行成本。弃电率等于弃电总量除以新能源可用发电量储能利用率等于实际吞吐电量除以额定容量乘时段数日运行成本直接取目标函数值。这三个指标是后面做灵敏度分析的核心抓手。4. 典型日案例与调度效果分析4.1 案例场景与参数设定矿区复用的典型日我构造了一个典型场景来验证模型参数均参考实际工程量级。风电装机50兆瓦光伏装机30兆瓦电网联络线购电上限100兆瓦、售电上限50兆瓦。电池储能容量20兆瓦/40兆瓦时充放电效率95%SOC运行区间10%到90%。小型抽蓄额定功率20兆瓦等效能容量500兆瓦时这相当于一座蓄水量几十万立方米、落差约100米左右的废弃矿井改造项目泵工况效率0.85发电压水效率0.90。负荷曲线采用工业园区的典型形状凌晨低谷35兆瓦左右上午爬升午间受高温空调用电影响摸到70兆瓦晚高峰再冲到75兆瓦。风电预测在夜间有一波高峰光伏预测在中午出现尖峰。这样的场景设置故意制造了两个矛盾时段午间光伏大发但负荷还没到最高夜间风电大发但负荷已经回落——正好考验两种储能的协同能力。4.2 结果解读互补效果、储能利用率与经济性求解完成后把调度结果拉出来看几点现象。第一抽蓄在午间连续四小时抽水把光伏顶峰期的富余电量吸入上库在晚高峰开始后发电顶峰承担了大约18兆瓦的跨时段移峰能力。电池则是在光伏爬坡的短时波动时段小功率充电、午间快速补充抽蓄功率上限吃不完的部分、傍晚放电应对负荷爬坡充放电切换频繁但单次吞吐量小。第二弃电率显著下降。如果系统里只有电池没有抽蓄午间四小时的富余电量会超过电池容量上限弃电率约7.6%加入抽蓄后弃电率降到1.2%。这个数字对比是我觉得这个课题最有说服力的地方——电池管“波动”抽蓄管“缺口”两者缺一不可。第三经济账也算得过来。日运行成本在无抽蓄工况下约45万元含抽蓄工况约39.6万元下降约12%。降幅来自两个渠道弃电惩罚减少以及晚高峰多发高价电、午后低谷少买高价电带来的价差套利。抽蓄机组的启停只有两到三次机械损耗可控电池的等效循环次数也从无抽蓄时的约1.8次/日降到1.1次/日这对电池寿命是肉眼可见的利好。5. 实操中的常见问题与排查经验5.1 模型总是无解先检查这四类约束我调试时最崩溃的就是求解器返回Infeasible而且不告诉你哪里有问题。踩过多次坑后我总结了一个排查顺序。第一查初始状态与边界的矛盾。比如电池SOC初值给了0.9但SOC上限也是0.9那么第一个时段只要有充电行为就立刻越界模型无解。抽蓄同理E_res初值接近上限时抽水功率约束可能和库容约束打架。解决办法是给边界留5%的裕量或者把SOC上下限作为软约束加惩罚项。第二查功率平衡方程里是不是漏了某项。最常见的是漏掉电池充电功率和抽蓄抽水功率这两个都是负荷侧项漏掉后等式永远左边大于右边。第三查互斥变量的取值范围。整型变量u_bat为0时如果充电功率下界不小心设成了大于0会导致问题无解。连续变量下界一律设0不要给下限加值。第四查极端场景。某些时段负荷极低而新能源出力极高既不让弃电又不能让储能全吸收就会无解。此时需要把弃电惩罚系数调低让模型有弃电的出口。5.2 求解速度慢整数变量与求解器选型当调度时段从24小时扩展到96点15分钟分辨率时MILP的整数变量数翻了四倍CBC求解时间可能从几秒膨胀到几分钟甚至不收敛。三个解决办法按性价比排序。第一个办法是减少整数变量。如果抽蓄在日内最多启停一次可以引入分段线性约束替代每时段的互斥变量或者直接用“状态变量只在少数几个时段取1”的稀疏约束。第二个办法是换求解器开源的HiGHS比CBC快一个量级商业Gurobi又快一个量级。第三个办法是把模型拆成主问题和子问题用Benders分解但研究阶段不建议一开始就上这个复杂度。这几种方法我在调试中都试过最推荐第二招直接把求解器从CBC换成HiGHS解96时段模型的时间从270秒压到20秒左右改动只是pulp.PULP_CBC_CMD换成pulp.HiGHS_CMD性价比极高。5.3 数据质量与预测误差时间戳、缺测和滚动修正数据坑是最隐蔽的坑。第一是时区混乱风电场数据用北京时间光伏数据用世界标准时间差8小时直接导致午间光伏曲线错位到凌晨模型必出离谱结果。第二是缺测值用零填充夜间光伏本来就是零默认没问题但风速仪断线时段也填零等于让模型以为风电站完全停机下网电量会被高估。第三是预测序列和实测序列混用导致评估结果虚高。我的经验是数据管道里加三道保险统一时区并在代码里注释清楚缺测值用邻近时段数据插补并用标记列标注是否缺测评估指标只计算在相同数据口径下的对比组。预测误差方面如果做滚动调度每个周期重新读取最新预测把上一周期的执行偏差作为下一周期的初值修正这个闭环能明显提升调度计划的可行性。最后再分享一个我个人的体会这套模型的价值不在代码本身而在于你愿不愿意花时间去理解两种储能各自的物理边界。电池的SOC递推和抽蓄的库容递推在数学形式上几乎一样但物理意义完全不同——电池看的是荷电状态抽蓄看的是上下库水量建模时一旦混淆后面的结果解释就会失真。项目跑到最后能把这层逻辑讲清楚的才算真正吃透了这个课题。如果后续要扩展可以把弃矿井抽蓄的库容曲线从线性改成非线性或者加上机组启停成本模型复杂度上来之后这套Python框架依然可以平稳承接。
阅读完成 · 觉得有帮助?