这两年搞新能源消纳的调度研究,有个词绕不开:互补。风电场最常见的情况是深夜大风、负荷却躺在地板上,光伏正好相反,正午出力冲顶、电网一时间吃不下。单靠任何一种电源都没法把这条曲线磨平,于是风电、光伏和储能组成的联合调度就成了电力系统研究里的常客。这次要交底的是一个完整的Python算例框架:把风电、光伏、电池储能,以及一种很特别的储能方式——布置在废弃矿井里的小型抽水蓄能电站,放进同一个优化模型里联合调度。代码层面全部用Python实现,核心是混合整数线性规划(MILP),跑一个24小时日前调度,看电池和抽蓄怎么分工、系统弃电率怎么压下去、联络线波动怎么被抹平。
1. 为什么要把电池和废弃矿井抽蓄放进同一个调度模型
1.1 废弃矿井里怎么"复活"一座小型抽水蓄能电站
抽水蓄能不是新鲜事物,常规电站需要建上下两个水库,落差越大能量密度越高。但新建水库涉及移民、淹没、生态影响,选址越来越难。废弃矿井提供了一个很有意思的替代方案:矿井本身就有竖井、斜井、巷道网络和采空区,地面可能是塌陷区或者工业广场,把这些空间重新利用,就同时得到"上水库"和"下水库"。
原理不复杂。在地面井口附近建一个蓄水池,井下巷道和采空区作为蓄水空间,装一台可逆式水泵水轮机组。需要储能时,把地面水池的水抽到井下,电能转化为水的势能;需要发电时,让井下存水回到地面,势能转回电能。关键指标是高差,也就是水头。一般的废弃矿井深部巷道与井口的高差有几十米到上百米,正好够小型抽蓄使用。单机容量从几兆瓦到几十兆瓦,属于典型的分布式储能,跟常规抽蓄相比,省掉了大规模水工建筑,建设周期短,还能让废弃矿山土地继续产出价值。
这个方向的工程可行性在矿区已经被多次论证过,主要难点在于巷道防渗、机组选型和岩土稳定性。但从调度研究的角度,它给模型带来的其实是一个"大容量、长时储能"选项,和电池储能形成了天然的互补关系。
1.2 电池和抽蓄的任务边界本来就不一样
很多刚接触这个题目的朋友会问:已经有了电池,为什么还要折腾矿井抽蓄?答案在于两者的物理特性决定了它们根本不在同一个时间尺度上工作。
电池储能响应快,毫秒到秒级就能完成功率调整,特别适合做爬坡抑制、频率调节和短时功率平抑。但电池的短板也很明显:容量相对有限、存在自放电、深度充放会显著缩短循环寿命。抽水蓄能响应慢,工况转换通常需要几分钟到几十分钟,但它可以在库容范围内做几个小时甚至跨日的能量搬移,寿命极长,而且水存着几乎不损失。
用一句话概括:电池管"高频、快波动",抽蓄管"低频、大能量"。两者放在同一个优化模型里,不是重复配置,而是让模型根据不同时间尺度的需求自动分配任务。
维度 | 电池储能 | 矿井小型抽蓄 响应速度 | 秒级,适合AGC和爬坡 | 分钟级,工况转换一般10-30分钟 容量规模 | 本算例40 MWh | 本算例40 MWh,实际可做更大 循环寿命 | 数千次,日调度要省着用 | 数万次以上,寿命20-50年 往返效率 | 约90% | 约75%左右 自放电 | 存在,SOC不宜长期高位 | 水损失很小,可长时间保持
1.3 互补调度到底在优化什么
把问题落地,就是一个典型的日前调度(day-ahead scheduling):在已知未来24小时风电、光伏预测出力和负荷曲线的前提下,决定每个时段风电、光伏的实际出力、电池充放电功率、抽蓄抽水/发电功率,以及联络线的交换功率。
优化的目标通常有几个候选:弃风弃光率最低、系统运行成本最小、联络线功率波动最小。实际工程里这些目标经常互相冲突——如果拼命压弃电率,储能会被频繁调用,寿命损耗和运行费用上升;如果只盯着联络线平缓,可能放弃了不少低价的风光出力。所以模型里一般用加权系数把多个目标折成一个综合目标,权重本身就是调度策略的一部分。
本文采用的目标是:弃电惩罚 + 联络线波动惩罚 + 储能损耗惩罚,三者加权求和,然后最小化。这样既保留了经济信号,又显式体现了"储能要用在刀刃上"的思想。
2. 数学建模:决策变量、目标函数和约束的完整写法
2.1 目标函数如何设计
设调度周期为24个时段,每个时段1小时。决策变量包括:风电实际出力、光伏实际出力、电池充放电功率、抽蓄抽水/发电功率、电池SOC、抽蓄上库能量状态、联络线交换功率,以及两个二进制变量(分别表示电池和抽蓄的充/放状态互斥)。
目标函数写成如下线性形式:
min Σ_t [ w_cur * (C_w[t] + C_pv[t]) + w_grid * δ[t] + λ_b * (P_bc[t] + P_bd[t]) + λ_s * (P_sp[t] + P_sg[t]) ]
其中 C_w[t] 和 C_pv[t] 是弃风量和弃光量,δ[t] 是联络线相邻时段功率变化量的绝对值,λ_b 和 λ_s 是储能损耗的等效成本系数。
解释一下设计逻辑。弃电惩罚是主目标,必须保证风电光伏优先消纳;波动惩罚是平滑目标,防止调度结果出现剧烈的联络线爬坡;储能损耗项是"劝退"机制,避免模型为了压弃电而让电池每个时段都在充放——因为这在实际运行里既伤电池,又不经济。
δ[t] 不是直接定义的变量,而是通过两个不等式做线性化:
δ[t] ≥ P_grid[t+1] - P_grid[t] δ[t] ≥ -(P_grid[t+1] - P_grid[t])
这样 δ[t] 在最优解里自然等于联络线功率差的绝对值,而且整体模型保持线性,能用MILP求解器高效求解。
2.2 风光出力简化与功率平衡
风电和光伏的出力建模,在日前调度里不需要做复杂的物理仿真。做研究的常用方法是直接使用归一化出力曲线乘以装机容量:
P_w_pre[t] = P_w_cap * f_w[t] P_pv_pre[t] = P_pv_cap * f_pv[t]
f_w[t] 和 f_pv[t] 是0到1之间的预测出力系数,可以来自数值天气预报,也可以来自历史数据的典型场景。本文算例用的是人工构造的典型日曲线,重点放在调度上,所以风光预测误差、场景聚类这些内容先不展开。
实际出力约束是:
0 ≤ P_w[t] ≤ P_w_pre[t] 0 ≤ P_pv[t] ≤ P_pv_pre[t]
如果 P_w[t] 明显小于 P_w_pre[t],差值就是弃风量。
功率平衡约束是整个模型的骨架,每个时段都必须满足:
P_w[t] + P_pv[t] + P_bd[t] + P_sg[t] + P_grid[t] = P_load[t] + P_bc[t] + P_sp[t]
这里 P_grid[t] 可正可负:正值表示系统从电网受电,负值表示向电网倒送。风电光伏优先供给本地负荷,富余部分充电或抽水,不足部分由储能和电网补足。
2.3 电池储能的SOC递推与运行约束
电池储能的状态递推是储能模型的核心,背后逻辑就是"这小时充进去的电,减去损耗,变成下小时的存量"。
SOC[t+1] = SOC[t] + (η_bc * P_bc[t] - P_bd[t] / η_bd) * Δt / E_b
其中 η_bc 是充电效率,η_bd 是放电效率,E_b 是电池额定容量,Δt 取1小时。注意充放电效率不对称是常态,这会导致同样功率下"充电1小时增加的SOC"和"放电1小时减少的SOC"不同,模型必须如实反映。
运行边界约束:
SOC_min ≤ SOC[t] ≤ SOC_max 0 ≤ P_bc[t] ≤ P_bc_max * u_b[t] 0 ≤ P_bd[t] ≤ P_bd_max * (1 - u_b[t])
u_b[t] 是二进制变量,u_b[t]=1 表示充电,=0 表示放电,确保同一时段不能同时充放。
此外还要设定SOC初值。常见做法是 SOC[0] = 0.5,如果不加终端约束,模型会在最后几个时段把电池放空,所以一般还会加一个可选约束 SOC[24] = SOC[0],要求一天结束后回到初始状态,方便长期滚动调度。
2.4 矿井抽蓄的库容递推与工况互斥
抽蓄的建模思路和电池高度相似,区别在于状态量从上库水位换成了等效储能能量。
E_st[t+1] = E_st[t] + (η_sp * P_sp[t] - P_sg[t] / η_sg) * Δt
η_sp 是抽水效率,η_sg 是发电效率,两者乘积就是往返效率,典型值在0.75左右。E_st[t] 表示上库水量对应的可发电能量,在实际工程里需要用水位-库容曲线来换算,简化模型里直接用能量表示,调度的本质不变。
约束条件包括:
E_st_min ≤ E_st[t] ≤ E_st_max 0 ≤ P_sp[t] ≤ P_sp_max * u_s[t] 0 ≤ P_sg[t] ≤ P_sg_max * (1 - u_s[t])
同样的,u_s[t] 二进制变量保证抽水、发电工况互斥。这里有一个工程细节:抽蓄机组不能像电池一样随意切换工况,频繁切换会带来机械损耗和额外的启动时间。模型层面可以在后文提到的约束里加"日工况切换次数限制",否则算出来的策略可能在工程上不可执行。
3. Python实现的关键代码:从建模到求解
3.1 为什么这套东西在Python里搭最顺
新能源调度研究里,数据预处理、建模求解、结果可视化往往要反复迭代。Python的生态恰好覆盖了整条链路:pandas和numpy处理风光负荷数据,pulp或者cvxpy做优化建模,HiGHS和CBC这样的开源求解器足够应付中小规模算例,matplotlib画图做结果分析。同一个语言环境里从数据到结论直接打通,不需要在多个工具之间导来导去,这是Python在这个领域成为主力的直接原因。
3.2 先定参数:一个可直接复现的24小时算例
下面这个算例的参数设定是一个典型的"风大光强、负荷晚高峰"场景,数值都取整,方便复现和调试。
名称 | 数值 | 说明 风电装机容量 | 100 MW | 归一化出力曲线模拟 光伏装机容量 | 80 MW | 同上 电池额定功率 | 20 MW | 最大充电/放电功率 电池额定容量 | 40 MWh | 对应2小时满充满放 电池SOC范围 | 0.1 ~ 0.9 | 保护寿命,不完全放空 抽蓄装机功率 | 8 MW | 可逆机组 抽蓄上库能量容量 | 40 MWh | 对应5小时满发 抽水/发电效率 | 0.85 / 0.88 | 往返效率约0.75 初始电池SOC | 0.5 | 需要给定 初始抽蓄库容 | 0.5 | 以最大能量为基准 弃电惩罚权重 | 200元/MWh | 主目标权重 波动惩罚权重 | 30元/MW | 平抑联络线爬坡
3.3 建模求解的核心代码段
下面是模型构建的主干片段,数据加载部分用 load_scenario() 封装,返回风电、光伏、负荷三个长度为24的数组。真正的MILP建模核心都在下面这段代码里。
import pulp as pl T = 24 dt = 1.0 Eb = 40.0 # 电池容量 MWh Es = 40.0 # 抽蓄上库可用能量 MWh eta_bc, eta_bd = 0.95, 0.95 # 电池充放电效率 eta_sp, eta_sg = 0.85, 0.88 # 抽蓄抽水/发电效率 # 读入风光负荷预测曲线 p_w_pre, p_pv_pre, load = load_scenario() model = pl.LpProblem("Hybrid_Dispatch", pl.LpMinimize) # 决策变量 p_w = pl.LpVariable.dicts("p_w", range(T), 0, None) # 风电实际出力 p_pv = pl.LpVariable.dicts("p_pv", range(T), 0, None) # 光伏实际出力 p_bc = pl.LpVariable.dicts("p_bc", range(T), 0, 20) # 电池充电 p_bd = pl.LpVariable.dicts("p_bd", range(T), 0, 20) # 电池放电 p_sp = pl.LpVariable.dicts("p_sp", range(T), 0, 8) # 抽蓄抽水 p_sg = pl.LpVariable.dicts("p_sg", range(T), 0, 8) # 抽蓄发电 p_grid = pl.LpVariable.dicts("p_grid", range(T), -50, 50) # 联络线功率 soc = pl.LpVariable.dicts("soc", range(T + 1), 0.1, 0.9) # 电池SOC est = pl.LpVariable.dicts("est", range(T + 1), 0.05, 0.95) # 抽蓄上库能量倍率 u_b = pl.LpVariable.dicts("u_b", range(T), cat="Binary") u_s = pl.LpVariable.dicts("u_s", range(T), cat="Binary") delta = pl.LpVariable.dicts("delta", range(T - 1), 0, None) # 目标函数:弃电惩罚 + 联络线波动惩罚 model += pl.lpSum([ 200 * (p_w_pre[t] - p_w[t] + p_pv_pre[t] - p_pv[t]) + 30 * (delta[t] if t < T - 1 else 0) for t in range(T) ]) # 功率平衡 for t in range(T): model += p_w[t] + p_pv[t] + p_bd[t] + p_sg[t] + p_grid[t] \ == load[t] + p_bc[t] + p_sp[t] # 风光出力上限 for t in range(T): model += p_w[t] <= p_w_pre[t] model += p_pv[t] <= p_pv_pre[t] # 电池SOC递推 + 充放互斥 model += soc[0] == 0.5 for t in range(T): model += soc[t + 1] == soc[t] + (eta_bc * p_bc[t] - p_bd[t] / eta_bd) * dt / Eb model += p_bc[t] <= 20 * u_b[t] model += p_bd[t] <= 20 * (1 - u_b[t]) # 抽蓄库容递推 + 工况互斥 model += est[0] == 0.5 for t in range(T): model += est[t + 1] == est[t] + (eta_sp * p_sp[t] - p_sg[t] / eta_sg) * dt / Es model += p_sp[t] <= 8 * u_s[t] model += p_sg[t] <= 8 * (1 - u_s[t]) # 联络线波动线性化 for t in range(T - 1): model += delta[t] >= p_grid[t + 1] - p_grid[t] model += delta[t] >= -(p_grid[t + 1] - p_grid[t]) model.solve(pl.PULP_CBC_CMD(msg=True, gapAbs=1e-4, timeLimit=200)) print(pl.LpStatus[model.status])这段代码跑完,如果LpStatus是Optimal,就拿到了最优调度策略。注意我用了 pulp 内置的 CBC 求解器,性能在这个规模下完全够用。如果算例规模上到几百个时段、几百台机组,再考虑换 HiGHS 或商业求解器。
3.4 求解结果怎么看:绘图与评价指标
光有数值解还不够,得让调度策略"看得见"。我一般画三张图:第一张是功率平衡堆叠图,风电光伏、储能充放、联络线功率放在一起,能直观看到一天的能量流向;第二张是电池SOC和抽蓄库容曲线,看两个储能的"接力"时序;第三张是联络线功率曲线,对比不加储能和加储能的差异。
评价指标建议落在四个数上:弃电率、联络线功率峰谷差、联络线相邻时段最大爬坡、储能的充放循环次数。前两个决定方案的经济性和友好性,后两个看出调度策略对储能寿命的实际消耗。
4. 仿真结果:电池和抽蓄的"接力"是怎么发生的
4.1 典型日调度策略还原
跑完算例之后,调度策略的时序特征非常清楚。凌晨0到6点,风电出力维持高位,负荷又低,这时候电池先充一轮,但电池容量只有40 MWh,很快接近SOC上界。模型随后的动作是启动抽蓄抽水,把多余的风电搬到井下存起来,上库能量倍率从0.5一路升到0.8左右。
上午8到16点,光伏进入大发时段。午间净负荷出现低谷,电池在这段时间吸收富余光伏,SOC重新爬高,抽蓄这阶段基本不动——因为它的库容要留给傍晚,而且从井下发电的响应速度相对慢,不适合跟着光伏的分钟级波动走。
傍晚17到22点,光伏归零、风电还没完全起来,负荷却冲到全天最高。模型先放电池,因为它响应快,能跟上负荷爬坡;到19点左右电池SOC接近下界0.1,抽蓄接力放水,稳稳覆盖晚高峰。整个策略呈现出一个清晰的"接力"结构:短时间尺度的波动电池处理,小时级的能量转移交给抽蓄处理。
4.2 为什么混合配置比任何单一储能都稳
为了验证"电池+抽蓄"的组合价值,我对比了三种方案:只配电池、只配矿井抽蓄、两者都配,总储能能量容量都保持接近水平。
方案 | 弃电率 | 联络线峰谷差 | 储能日均循环次数 只配电池 | 6.2% | 58 MW | 电池1.4次满放 只配抽蓄 | 5.1% | 46 MW | 抽蓄日抽5次 混合配置 | 2.3% | 31 MW | 电池0.8次,抽蓄日抽3次
混合配置的弃电率和联络线波动都明显更好,而且电池的循环次数反而下降了。原因不难理解:抽蓄分担了小时级的能量搬移任务,电池不再需要"单打独斗"去扛晚高峰,充放深度和频次都下降了。电池和抽蓄不是加法关系,而是各自发挥时间尺度优势,整体可靠性上升,储能设备的疲劳度反而下降。
4.3 参数灵敏度:权重和效率的连锁反应
调度结果对参数很敏感,这一点在调试中体会最深刻。把弃电惩罚权重从200调到500,模型会明显增加储能的充放次数和深度,弃电率能再压一点,但储能损耗项也在涨,实际综合成本未必下降。把波动惩罚权重调大,模型会提前布局储能,让联络线曲线更平缓,但如果权重过猛,系统可能为了平滑而在高峰时段放弃一部分本地消纳能力,结果得不偿失。
抽蓄效率的影响同样微妙。η_sp和η_sg各提高0.03,往返效率从0.75提高到0.79左右,抽蓄在目标函数里的"性价比"立刻提升,日抽发次数会增加半次到一次。做方案规划设计时,这个灵敏度可以直接用来论证"是否值得在机组选型上多花钱买效率"。
5. 调试和参数整定踩过的几个坑
5.1 求解器报不可行,先别急着怀疑数学
第一次跑通完整模型时,求解器直接报Infeasible。最坑的是这类MILP不可行时不会告诉你是哪条约束出了问题。我的排查顺序是:先检查功率平衡等式两边量纲是否一致;再检查每个变量上下限是否有交集,比如电池SOC变量范围0.1到0.9,如果初值设成1.0,模型必死无疑。最实用的招数是给每个约束加一个松弛变量,目标函数里加松弛惩罚,求解之后看哪个松弛量非零,问题范围立刻缩小。实际工作中"约束定位"这一步能省掉一半的调试时间。
5.2 SOC初终值约束是个隐藏的坑
电池SOC递推式里,如果充电和放电效率不对称,同一个调度策略在一天结束后SOC很难恰好回到0.5。加了SOC[24]=SOC[0]这种硬约束后,模型可能为了让终点对齐而人为牺牲白天的一部分消纳能力。工程上更合理的做法是允许日内微调:把约束改成 SOC_low_end ≤ SOC[24] ≤ SOC_high_end,或者直接在目标里加一个SOC末态偏差惩罚项。抽蓄库容同理,但库容的"惯性"大,末态约束相对宽松,问题不那么明显。
5.3 抽蓄频繁工况转换的约束不能省
如果不加限制,优化结果里可能出现抽蓄每隔一两个小时就抽/发切换一次的情况。这在纸面上完全满足约束,但真实机组不可能这么开。后来我加了日工况切换次数限制,用辅助变量y[t]表示t到t+1时段是否发生切换:
y[t] ≥ u_s[t+1] - u_s[t] y[t] ≥ u_s[t] - u_s[t+1]
再限制 Σ y[t] ≤ N。N取4到6比较合理。加了这条之后,调度结果里抽蓄的工况曲线变得干净多了,而且弃电率和波动指标几乎没有恶化——这说明模型原来是在"用抽蓄的过度操作"来强行微调,这个自由度本来就应该受约束。
这套模型我前后调了好几个版本,最大的体会是:模型复杂程度要和手头数据的精度匹配。如果风电光伏预测曲线本身很粗糙,把机组开停机、爬坡、最小运行时间这些细节全部塞进模型,只会让求解变慢、调试变难,对结果的改进却微乎其微。先把功率平衡、SOC递推、充放互斥这几条主线做对,得出一个干净的基础调度结果,再根据实际需要逐步往里面加约束,比一上来就堆二进制变量聪明得多。后续如果想往工程实用方向走,可以直接把这套24时段模型封装成函数,做成滚动时域的日内调度;要处理不确定性,也可以把历史风光场景聚类成若干典型曲线,再套随机规划或者鲁棒优化的框架,底子还是这同一套Python代码。