☰
风电光伏储能互补调度Python优化建模与实现
2026/10/10 7:20:40 网站建设 项目流程

风电、光伏和储能怎么配合调度,是这几年新能源领域绕不开的硬骨头。风光的随机性和间歇性大家都懂,储能作为“缓冲池”的角色也喊了很多年,但真正把风电、光伏、电池储能、甚至废弃矿井抽水蓄能放在一个框架里做互补调度优化的研究,其实还不多。这篇博客就围绕我自己用Python实现的一套“风光储互补调度运行”代码,把里面的核心逻辑、建模思路、实操细节和踩过的坑一次说清楚。代码已经跑通,结果稳定,适合正在做新能源调度、微电网优化或者储能配置研究的同行参考,也适合刚接触Python优化建模的读者拿来练手。

1. 整体设计与思路拆解

1.1 为什么要把风电、光伏和储能放在一起调度

很多人一开始会把风电和光伏当成“电源”,把储能当成“负荷或者电源”,然后分别建模再拼起来。这个思路本身没有错,但真正做互补调度的时候,问题就来了:风光出力是分时段的,用电负荷也是分时段的,储能充放电策略会影响后续时段的可用容量,而矿井抽水蓄能这种特殊储能方式又带有地理和容量约束。如果只是把各模块简单拼凑,优化结果往往不是全局最优,甚至会出现“储能白天充满、晚上没电放”这种低级问题。

互补调度的核心在于“时序耦合”和“功率平衡”。风电出力往往夜间较大,光伏出力集中在白天,负荷则在早晚出现双高峰。没有储能时,只能弃风弃光或者依赖外购电。加入储能后,调度问题的本质变成了:在满足负荷需求的前提下,决定每个时段充放电多少、什么时候充什么时候放,才能让系统运行成本最低、新能源消纳最多。

1.2 方案选型:电池储能与废弃矿井抽水蓄能

这里有一个很实际的考虑——为什么储能部分要同时包含电池和废弃矿井小型抽水蓄能?电池储能的响应速度快、布置灵活,但成本高、寿命有限;抽水蓄能容量大、寿命长,但依赖地理条件。废弃矿井改造成小型抽水蓄能,等于把“废地”变成“储能设施”,对上水库、下水库的落差要求不高,适合小型化分布式部署。

两种储能特性差异很大:

特性电池储能废弃矿井抽水蓄能
响应速度毫秒~秒级分钟级
循环寿命数千次数十年
单位容量成本较高较低(改造后)
能量转换效率85%~95%70%~80%
选址约束灵活依赖废弃矿井地理位置

在优化模型里,这两种储能不能简单合并成一个“储能模块”,否则会丢失各自运行约束的差异性。电池适合做短周期、高频次的调节,抽水蓄能适合做长周期、大规模的能量搬移。互补使用,既能提升响应能力,又能降低成本。

1.3 Python建模的整体架构

我选用Python做这个项目,核心原因有三个:一是生态成熟,尤其是优化建模和科学计算库非常完善;二是代码可读性强,方便后续扩展;三是数据可视化方便,调度曲线直接画出来对比一目了然。

整体架构分为四层:

  • 数据层:处理风电、光伏的时序出力数据,以及负荷数据;
  • 模型层:定义决策变量、目标函数和约束条件;
  • 求解层:调用优化求解器求最优解;
  • 展示层:绘制调度结果曲线、储能SOC变化曲线、成本构成图等。

这里要重点说明:调度运行研究的基础是“给定数据求最优策略”,而不是设计储能容量。风电和光伏的出力曲线,我采用历史实测数据或者基于典型日生成,负荷数据则使用典型工业/居民负荷曲线。如果数据源不足,也可以先用模拟数据把流程跑通,后续再替换真实数据。

2. 核心细节解析与实操要点

2.1 风电与光伏的出力特性处理

风光出力建模是整个调度问题的输入基础。风电出力与风速相关,光伏出力与太阳辐照度相关,两者都带有明显的时序特征和随机性。在调度模型中,我采用“时序场景”方式处理:把一天划分为24个时段,每个时段给一个出力系数,再乘以装机容量就得到该时段的出力上限。

风电的典型出力特点:夜间和凌晨出力较高,白天相对较低。光伏的典型出力特点:夜间出力为零,白天从早上升高、中午达到峰值、傍晚降回零。两者叠加之后,白天时段通常不缺电,凌晨和傍晚时段容易出现供电缺口——这正是储能发挥作用的关键时段。

有一点要提醒:不要直接用“平均出力”作为输入。调度优化对时序非常敏感,平均化会抹平峰谷差异,导致优化结果严重失真。数据预处理时,宁可保留原始波动,也不要为了“好看”而做平滑处理。

2.2 电池储能模型:SOC与充放电约束

电池储能的建模核心是SOC(State of Charge,荷电状态)。SOC表示当前电量占额定容量的百分比,是电池储能调度的核心状态变量。每个时段的SOC变化可以用下面的递推式表示:

[ SOC(t+1) = SOC(t) + \eta_c \cdot P_c(t) - P_d(t) / \eta_d ]

其中,(P_c(t)) 是充电功率,(P_d(t)) 是放电功率,(\eta_c) 是充电效率,(\eta_d) 是放电效率。这个递推式看起来简单,但实际实现时要注意几个约束:

  • SOC必须在安全范围内运行,通常取10%~90%,避免过充和过放;
  • 充放电功率不能同时为正,否则模型会在无意义的“边充边放”中浪费能量;
  • 充放电功率有上限,受限于电池的额定功率;
  • 调度周期结束时SOC要回到初始值,这是为了支持“日循环”调度模式。

实践中,边充边放的问题最隐蔽。如果不对充放电状态做互斥约束,优化器可能“钻空子”——同时充电和放电,既满足了功率平衡,又消耗了能量,这显然不符合物理实际。我采用二进制变量加连续变量的方式处理,效果很稳定。

2.3 废弃矿井抽水蓄能模型:容量与扬程约束

废弃矿井小型抽水蓄能跟电池储能的建模逻辑类似,但多了两个关键参数:水库容量和水头扬程。上水库和下水库之间通过管道连接,抽水时电能转化为水的势能,发电时水的势能释放转化为电能。

抽水蓄能的模型参数包括:

  • 上水库可用水量 (V_{max}),决定了储能容量;
  • 抽水/发电功率上限 (P_{pump})、(P_{gen});
  • 抽水效率 (\eta_{pump}) 和发电效率 (\eta_{gen});
  • 水量连续性约束:某时段的水量 = 上一时段水量 + 抽水量 - 发电用水量。

这里有个实操细节需要注意:抽水蓄能的效率是两条腿走路的。抽水效率通常指电能转化为水的势能的效率,发电效率指水的势能转化为电能的效率,总效率是两者乘积。比如抽水效率0.85、发电效率0.85,那么总效率只有0.72左右。建模时如果只用一个效率系数,会高估储能的经济性。

另外,与电池不同,抽水蓄能的水量是可以“跨日”调节的。电池储能通常按日循环,抽水蓄能完全可以按周循环甚至更长时间尺度。我在代码里用“周期结束时水位回到初始水位”作为约束,也可以根据实际需求放开这个约束。

2.4 功率平衡与系统运行约束

互补调度运行研究的本质,是解决任意时刻系统功率平衡的问题。功率平衡方程可以写为:

[ P_{wind}(t) + P_{pv}(t) + P_{bat_dis}(t) + P_{pump_gen}(t) + P_{grid}(t) = P_{load}(t) + P_{bat_ch}(t) + P_{pump_ch}(t) ]

这个公式的含义是:电源出力和储能放电,要满足负荷、储能充电和抽水蓄能抽水的总需求。如果系统允许向外部电网购电,那么 (P_{grid}(t)) 可以作为松弛变量,允许为正或负。如果要求“孤岛运行”,则 (P_{grid}(t)) 必须恒为零。

这里有一个容易忽视的细节:功率平衡方程看起来只是一个等式,但它的含义远超“数学上成立”。实际上,它隐含了“弃风弃光”的机制。如果某时段风光出力超过负荷加上储能充电的需求,多余的电量只能弃掉。在优化模型里,弃风弃光量并不是显式变量,而是通过让风光出力小于等于预测出力来隐式实现的。

3. 实操过程与核心环节实现

3.1 数据准备与预处理

好的调度模型,一半功夫在数据准备。我建议把数据整理成这样的Csv格式:

时段风电出力系数光伏出力系数负荷系数
00.320.000.42
10.380.000.38
............
230.450.000.55

出力系数乘以装机容量得到实际功率,负荷系数乘以最大负荷得到实际负荷。用系数而不是绝对功率的好处是:模型可以灵活适配不同装机规模,只需要在参数初始化时改两个数字。

我实测过的数据来源有两类:一是公开数据集(比如某些省份的风电场、光伏站历史出力数据),二是自己按典型日曲线加随机扰动生成。如果做对比实验,建议用确定性基准数据加随机扰动的方式生成多组数据,这样能测试模型的鲁棒性。

3.2 优化模型的数学表达

这个项目的优化问题可以写成标准形式:

目标函数:

[ \min \sum_{t=1}^{T} \left( C_{grid} \cdot P_{grid}(t) + C_{curtail} \cdot (P_{wind_avail}(t) - P_{wind_use}(t)) + C_{curtail} \cdot (P_{pv_avail}(t) - P_{pv_use}(t)) \right) ]

其中,(C_{grid}) 是购电电价,(C_{curtail}) 是弃风弃光惩罚成本。目标函数的设计体现了两个核心目标:降低运行成本、提高新能源消纳率。

约束条件包括:

  1. 功率平衡约束;
  2. 电池SOC递推约束与容量约束;
  3. 电池充放电功率上下限与互斥约束;
  4. 抽水蓄能水量递推约束与库容约束;
  5. 抽水蓄能抽水/发电功率上下限与互斥约束;
  6. 风光出力不超过可用预测出力;
  7. 购电功率不超过联络线功率上限。

这里要特别说明目标函数中惩罚项的设计。如果没有弃风弃光惩罚,优化器会倾向于“不用白不用”的原则,尽可能多消纳新能源——这本来没问题,但当新能源出力过大导致储能充满时,多余的电无法消化,弃风弃光就不可避免。设置惩罚成本,本质上是告诉优化器:“弃风光是一种很不划算的行为,优先避免它,实在不行再弃。”

3.3 Python代码实现与关键逻辑

代码实现上,我推荐用pulp这个线性规划库,轻量且容易上手。也可以用gurobi或cplex,求解速度更快,但需要商业许可。学术研究场景下,gurobi有免费学术版,性能和稳定性都很出色。

核心代码结构如下:

from pulp import LpProblem, LpMinimize, LpVariable, LpStatus, value import numpy as np import pandas as pd T = 24 # 基础数据 wind_coeff = [...] # 风电出力系数 pv_coeff = [...] # 光伏出力系数 load_coeff = [...] # 负荷系数 wind_cap = 50 # 风电装机 MW pv_cap = 30 # 光伏装机 MW load_max = 40 # 最大负荷 MW # 电池储能参数 bat_cap = 30 # MWh bat_power = 10 # MW bat_eff = 0.9 # 充放电效率 soc_min, soc_max = 0.1, 0.9 soc_init = 0.5 # 抽水蓄能参数 pump_cap = 60 # 水量上限 万m³ pump_power = 8 # MW pump_eff = 0.85 gen_eff = 0.85 # 定义优化问题 prob = LpProblem("Wind_PV_Storage_Dispatch", LpMinimize) # 决策变量 P_wind = [LpVariable(f"P_wind_{t}", 0, wind_cap * wind_coeff[t]) for t in range(T)] P_pv = [LpVariable(f"P_pv_{t}", 0, pv_cap * pv_coeff[t]) for t in range(T)] P_bat_ch = [LpVariable(f"P_bat_ch_{t}", 0, bat_power) for t in range(T)] P_bat_dis = [LpVariable(f"P_bat_dis_{t}", 0, bat_power) for t in range(T)] P_pump_ch = [LpVariable(f"P_pump_ch_{t}", 0, pump_power) for t in range(T)] P_pump_dis = [LpVariable(f"P_pump_dis_{t}", 0, pump_power) for t in range(T)] P_grid = [LpVariable(f"P_grid_{t}", 0, 20) for t in range(T)] SOC = [LpVariable(f"SOC_{t}", soc_min, soc_max) for t in range(T+1)] V = [LpVariable(f"V_{t}", 0, pump_cap) for t in range(T+1)] # 充放电互斥变量 u_bat = [LpVariable(f"u_bat_{t}", cat="Binary") for t in range(T)] u_pump = [LpVariable(f"u_pump_{t}", cat="Binary") for t in range(T)] # 目标函数:购电成本 + 弃风弃光惩罚 grid_price = 0.5 # 元/kWh curtail_penalty = 0.8 # 元/kWh cost = ( sum(grid_price * P_grid[t] for t in range(T)) + sum(curtail_penalty * (wind_cap * wind_coeff[t] - P_wind[t]) for t in range(T)) + sum(curtail_penalty * (pv_cap * pv_coeff[t] - P_pv[t]) for t in range(T)) ) prob += cost # 约束:功率平衡 for t in range(T): prob += P_wind[t] + P_pv[t] + P_bat_dis[t] + P_pump_dis[t] + P_grid[t] == \ load_max * load_coeff[t] + P_bat_ch[t] + P_pump_ch[t] # 约束:电池SOC递推 for t in range(T): prob += SOC[t+1] == SOC[t] + (bat_eff * P_bat_ch[t] - P_bat_dis[t] / bat_eff) / bat_cap # 约束:电池充放电互斥 for t in range(T): prob += P_bat_ch[t] <= bat_power * u_bat[t] prob += P_bat_dis[t] <= bat_power * (1 - u_bat[t]) # 约束:抽水蓄能水量递推 for t in range(T): prob += V[t+1] == V[t] + (pump_eff * P_pump_ch[t] - P_pump_dis[t] / gen_eff) * 0.01 # 约束:抽水蓄能互斥 for t in range(T): prob += P_pump_ch[t] <= pump_power * u_pump[t] prob += P_pump_dis[t] <= pump_power * (1 - u_pump[t]) # 约束:SOC、V初值与末值 prob += SOC[0] == soc_init prob += SOC[T] == soc_init prob += V[0] == pump_cap * 0.5 prob += V[T] == pump_cap * 0.5 # 求解 prob.solve() print("Status:", LpStatus[prob.status]) print("Objective:", value(prob.objective))

提示:水量递推中的系数0.01是为了把功率与水量单位统一起来的简化处理。实际应用中,要根据上下水库的库容曲线和扬程数据,建立真实的功率-水量转换关系。

3.4 结果可视化与敏感性分析

调度结果的展示对判断模型正确性非常关键。我推荐至少画三张图:

第一张是功率平衡图。把风电出力、光伏出力、电池充放电、抽水蓄能充放电、购电功率和负荷曲线画在一张图里,可以直观看出每个时段的功率流向。

第二张是SOC曲线和水量曲线。这张图用来检查储能是否在合理范围内运行,有没有出现越界或者周期末没有回到初始值的情况。

第三张是成本构成堆叠图。把购电成本、弃风惩罚、弃光惩罚分别堆叠,可以直观看出成本结构的占比,从而判断优化效果。

敏感性分析方面,我习惯做两个维度的实验:第一个是改变电池容量和抽水蓄能容量,观察系统成本和新能消纳率的变化趋势;第二个是改变购电电价和弃风弃光惩罚系数,观察调度策略的变化。这类实验对项目报告和论文写作都有直接价值。

3.5 参数设定与原始数据来源说明

代码里用到的参数,有些来自公开资料,有些按典型工程经验暂定。举几个例子:

  • 风电、光伏装机容量设为50MW和30MW,这是中等规模新能源场站的常见配置;
  • 电池储能容量30MWh、功率10MW,充放电时间约3小时,接近电网侧储能电站的典型配置;
  • 废弃矿井抽水蓄能的水量上限60万m³,是基于小型废弃矿井改造项目的常见容量范围估算的。

注意:这只是演示代码的默认参数,不是任何实际工程项目的真实参数。实际应用时,必须用项目现场的真实数据替换,尤其是抽水蓄能的效率和水头参数,对结果影响非常大。

4. 常见问题与排查技巧实录

4.1 优化结果出现“边充边放”怎么办

这个问题在初版代码里几乎必现。表现是某时段电池充电功率和放电功率都不为零,优化结果“看起来很合理”,但实际上是在损耗能量。

排查方法很简单:打印每个时段的充放电功率,检查有没有同时为正的时段。如果存在,优先检查有没有加互斥约束。加互斥约束后这个问题基本消失。

不过要注意:加了二进制变量后,模型从纯线性规划变成了混合整数线性规划,求解时间会显著增加。对于24时段的小规模问题完全没问题,但如果扩展到8760小时全年调度,求解时间可能让人崩溃。这时候有两种方案:一是去掉二进制变量,用一个小惩罚项抑制边充边放——比如目标函数中加上“充电功率的正则项”,这个技巧在实践中效果不错,但严格来说不能100%保证消除;二是换用更高效的商业求解器gurobi,并开启MIP焦点参数,速度提升非常明显。

4.2 SOC曲线出现锯齿状波动

如果SOC曲线高频上下波动,常见原因有两个。一是充放电功率本身跟随负荷波动,这属于正常现象,不用处理。二是效率参数设置出错,比如充放电效率乘除方向搞反了——充电效率应该在充电侧乘,放电效率应该在放电侧除,方向搞反就会导致SOC异常波动甚至发散。

出现SOC曲线问题时,先做一个极简测试:只保留电池储能,关闭风电光伏和抽水蓄能,把负荷设成恒定值,看SOC是否平滑变化。如果还是锯齿状,基本可以确定是递推公式写错了。

4.3 单位不统一导致结果离谱

这是新手最容易踩的坑。功率单位是MW,电量单位是MWh,电价单位是元/kWh或元/MWh,不统一会导致目标函数数值差好几个数量级。比如电价按元/kWh计算,而功率电量按MW/MWh计算,那么电价应该按1000倍换算成元/MWh再参与计算。

我在代码里统一使用MW、MWh、元/MWh,并且让所有涉及价格的参数都明确标注单位。这样做的直接好处是:调试时可以手动验算某个时段的成本,能够快速定位问题。

4.4 求解器报“Infeasible”怎么处理

模型不可行的原因通常有三类:第一类是约束之间互相矛盾,比如SOC初值设置为0.2,同时又要求SOC最小值0.3,那初始时刻就已经违约了;第二类是功率平衡方程过于严格,没有给购电功率足够的取值范围;第三类是抽水蓄能的末水位约束与初始库容设置冲突。

排查时建议先用注释法,把所有约束逐个注释掉,每注释一个求解一次,很快就能锁定是哪条约束导致不可行。这个方法虽然笨,但对于50行以内的模型非常高效。

4.5 关于求解器的选择经验

我用过的求解器包括pulp自带的默认求解器、scipy.optimize.linprog、gurobi和cplex。简单概括:

  • 学习阶段用pulp+ 默认CBC求解器,免费且安装方便;
  • 正式研究且模型规模较大时,优先考虑gurobi,学术版免费,性能出色;
  • 如果模型是凸优化或非线性,可以考虑cvxpy配合开源求解器。

另外还想提一句:代码写完之后,先用小规模数据(比如把24时段改成8时段)跑通,确认逻辑正确后再加完整数据。这样调试效率最高,也最容易定位问题。

5. 扩展方向与代码的后续改进

这个项目做到当前程度,已经能回答“风光储如何互补调度”这个核心问题。但实际工程中还有几个方向值得继续深挖:

第一个方向是加入随机优化。风电光伏出力本质上是不确定的,确定性调度只是“给定预测值求最优解”。加入随机场景后,模型可以输出鲁棒性更强的调度策略。实现方式也比较直接:把单一场景扩展为多场景,目标函数改为期望成本最小化。新增的工作量主要是场景生成和场景缩减,场景缩减可以用K-means聚类或快速前向选择算法。

第二个方向是引入分时电价和需求响应。我现在的模型里购电电价是固定值。实际电网中,分时电价很常见,峰时电价可能是谷时的两倍以上。加入分时电价后,储能的调度策略会发生很大变化——可能白天充满、晚间高峰放出的“峰谷套利”会成为主要策略。需求响应侧,可以考虑让部分可转移负荷参与调度,模型会更加贴近实际。

第三个方向是模型预测控制。目前是“开环调度”,也就是先算好24时段的策略,然后按计划执行。实际运行中,风光出力预测会不断更新,储能实际SOC也会偏离计划值。用模型预测控制的滚动优化思路,每个时段基于最新预测重新计算未来24时段的策略,只执行当前时段的指令,控制效果会更稳。

第四个方向是考虑储能寿命衰减成本。电池储能每次充放电循环都会造成容量衰减,这个成本在优化模型里通常被忽略。更精细的模型可以加入循环老化成本函数,让优化器自动权衡“现在多充放带来的收益”和“未来容量衰减带来的损失”。这一块对电池储能经济性评估的影响很大,值得深入做。

这些扩展的代码基础,我在项目里已经预留了接口。决策变量、约束函数、目标函数都是独立模块,改一个模块不需要动其他模块,扩展起来比较顺手。

我在实际调试中体会最深的一点是:这个项目的价值不在于数学模型有多复杂,而在于把“物理约束、经济目标、时间耦合”这三件事揉在一起的建模能力。风光储互补调度看起来是优化问题,其实更是一个系统思维问题。模型跑通的瞬间很有成就感,但更值钱的是你对每条约束、每个参数背后物理意义的理解。做研究也好,做工程也罢,能解释清楚“为什么这么建模”的人,才是真正把这个方向吃透的人。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询