风电、光伏与储能(含电池和废弃矿井小型抽水蓄能)互补调度运行研究,这个课题我刚接手的时候,以为就是一个普通的“储能配置加日前机组组合”的小项目,写点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(f"x_wind_{t}", 0, None) for t in range(T)} x_pv = {t: LpVariable(f"x_pv_{t}", 0, None) for t in range(T)} P_buy = {t: LpVariable(f"P_buy_{t}", 0, 100) for t in range(T)} P_sell = {t: LpVariable(f"P_sell_{t}", 0, 50) for t in range(T)} P_bat_c = {t: LpVariable(f"P_bat_c_{t}", 0, 20) for t in range(T)} P_bat_d = {t: LpVariable(f"P_bat_d_{t}", 0, 20) for t in range(T)} SOC = {t: LpVariable(f"SOC_{t}", 0.1, 0.9) for t in range(T)} P_phs_p = {t: LpVariable(f"P_phs_p_{t}", 0, 20) for t in range(T)} P_phs_g = {t: LpVariable(f"P_phs_g_{t}", 0, 20) for t in range(T)} E_res = {t: LpVariable(f"E_res_{t}", 50, 500) for t in range(T)} curtail = {t: LpVariable(f"curtail_{t}", 0, None) for t in range(T)} # 整数变量:电池充放电互斥、抽蓄工况互斥 u_bat = {t: LpVariable(f"u_bat_{t}", 0, 1, cat=LpInteger) for t in range(T)} u_phs = {t: LpVariable(f"u_phs_{t}", 0, 1, cat=LpInteger) 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]), f"balance_{t}" # 消纳上限 for t in range(T): model += x_wind[t] <= P_wind_fc[t], f"wind_{t}" model += x_pv[t] <= P_pv_fc[t], f"pv_{t}" model += curtail[t] >= P_wind_fc[t] - x_wind[t] + P_pv_fc[t] - x_pv[t], f"curtail_{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], f"bat_c_{t}" model += P_bat_d[t] <= 20 * (1 - u_bat[t]), f"bat_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], f"phs_p_{t}" model += P_phs_g[t] <= 20 * (1 - u_phs[t]), f"phs_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'], alpha=0.7) ax1.plot(t, P_load, 'k--', label='Load') ax1.set_ylabel('Power (MW)') ax2 = ax1.twinx() ax2.plot(t, [E_res[i].value() for i in range(T)], 'r-', label='PHS 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框架依然可以平稳承接。