☰
风电光伏储能联合调度与混合储能优化建模实践
2026/10/2 3:31:36 网站建设 项目流程

1. 为什么要把风电、光伏和储能放到一起调度

做新能源调度的朋友应该都有同感:风电和光伏单独拿出来看,出力曲线都“不太听话”。风电靠天吃饭,来一阵风就满发,风停了出力能掉到个位数;光伏更直接,日出而作日落而息,阴天雨天直接躺平,但中午那三四个小时的峰值又猛得吓人。如果把这两个电源放在一起看,其实反而有点意思——白天光伏出力高、晚上风电往往更稳定,春夏光照好但风小,秋冬风大但光照弱,地理和时域上都有一定的天然互补空间。但光靠这种“自然互补”远远不够,电网要的是时时刻刻的功率平衡,不是一个大概率的统计规律。

这时候储能就成了关键的“缓冲垫”。但储能也不是笼统地上一套就行。电池响应快、能量密度高,适合做短时功率调节和削峰填谷;而抽水蓄能虽然响应慢一些,胜在容量大、寿命长、度电成本低,适合做跨时段的能量搬移。最妙的是,把目光放到废弃矿井上,把原本是负担的矿井空间改造成小型抽水蓄能电站,既解决了退役矿井的处置问题,又给系统增加了低成本的长时储能容量——这个思路我最早是在一个省级课题报告里看到的,后来自己做仿真时发现,建立包含这两类储能的联合调度模型,远比单一储能方案更贴近实际工程需求。

这篇内容适合正在做新能源功率预测、微电网调度、储能容量配置或者电力系统优化方向的同学。我会把整套研究的思路、建模方法、Python实现细节和调试过程都拆开讲,代码和参数都是可以直接拿去改的。哪怕你目前只做风电或者只做光伏,这套“多能互补+混合储能”的框架也有参考价值。

2. 先想清楚:这个调度问题的本质是什么

2.1 不是简单的“供需平衡”,而是多时间尺度的协调

很多初学者拿到这类题目,第一反应是“我做一小时内发电和负荷的平衡”,然后就开始写约束。但实际上,风电、光伏和储能的互补调度最核心的难点在于——不同电源和储能设备的时间尺度完全不同。

风电的波动周期从秒级到小时级都有,光伏基本上跟着太阳辐照度走,电池可以在毫秒级响应,但容量有限,抽水蓄能的启动需要几十秒到几分钟,一旦运行,调峰周期往往是小时级。如果把所有设备都放在同一个时间分辨率的模型里,要么计算量爆炸,要么模型失真。我在做这个项目时采用的是典型日法:用历史数据聚类出几种典型场景,每个场景以1小时为间隔,优化周期24小时,先解决“能量怎么分配”的问题,再在仿真层面叠加分钟级的功率波动,看看系统跟踪效果。分层的思路极大简化了问题,也让优化结果更具工程说服力。

2.2 经济性与可靠性,目标函数必须权衡

调度模型的目标函数一般有两种流派:一种是以系统运行成本最小为目标,另一种是以弃风弃光率最低为目标。实际做下来你会发现,单目标优化很容易走向极端。单纯追求经济性,可能会让储能频繁充放、寿命折损严重;单纯追求消纳,又可能不计成本地调用高成本的调节手段。

我最终采用的是多目标加权的方式:

  • 第一部分是弃风弃光惩罚成本,即未被消纳的新能源电量乘以惩罚系数;
  • 第二部分是储能充放电的等效损耗成本,把电池循环寿命折损折算成每MWh的惩罚费用;
  • 第三部分是向电网购电的成本(如果系统可以与外部电网交互)。

运行效果上,这个目标函数调出来的结果非常务实——系统会优先消纳新能源,在风光不足时调用储能放电,只有当储能容量或功率不够时,才会从电网买电,绝不会出现为了省电费而故意弃风弃光的“自欺欺人”式优化。

2.3 混合储能的分工逻辑

既然要同时建模电池和废弃矿井小型抽水蓄能,就得先想明白各自在调度策略里的定位。我的经验是,从“能量粒度”和“响应速度”两个维度去划分职责:

  • 电池储能:容量较小但响应快,适合做分钟级到小时级的功率平滑、爬坡支撑、短时填谷。在模型中我设置了较高的充放电功率上限,但同时约束了单次充放电深度,防止过度损耗。
  • 废弃矿井抽水蓄能:容量很大,但受限于上下水库容积和输水系统,启停切换有一定延时。在模型中表示为“能量型”设备,适合做日内的跨时段搬移——比如夜间风电大发时抽水蓄能,白天高峰时放水发电。

这套分工在结果里表现得非常清晰:电池的SOC曲线振荡频繁,但幅度不大;抽水蓄能的蓄水量曲线则是一个明显的日内大起大落。两条曲线叠加起来,系统整体的净负荷曲线被削得相当平整。

3. 风光出力建模,数据是基础,别上来就写优化

3.1 风电出力模型:从风速到功率的非线性映射

风电的出力本质上是一个从风速到功率的非线性映射。工程上常用分段函数近似:

  • 低于切入风速(通常3 m/s左右)时不发电;
  • 在切入风速和额定风速之间,出力随风速近似线性增长;
  • 达到额定风速以后,出力保持额定功率不变;
  • 超过切出风速(通常25 m/s)时为了保护风机,直接停机。

在实际Python建模中,我不建议直接用公式写死,因为不同风机的参数差别很大。更稳妥的做法是做成一个可配置的函数,参数放在配置文件里,随时可调。下面是一个简化版本的结构:

def wind_output(wind_speed, rated_power, cut_in=3.0, rated_wind=12.0, cut_out=25.0): if wind_speed < cut_in or wind_speed >= cut_out: return 0.0 if wind_speed >= rated_wind: return rated_power # 线性段简化处理 return rated_power * (wind_speed - cut_in) / (rated_wind - cut_in)

如果你手头有实际的风速时序数据(比如来自气象站或测风塔),最好做一下风速频率分布分析,看看数据质量。我踩过一个坑:某段历史数据里有连续十几个小时的风速完全相同,后来发现是传感器故障,数据采集器直接把重复值写入数据库了。这类脏数据如果不处理,做出来的调度结果毫无意义。

3.2 光伏出力模型:辐照度、温度都得考虑

光伏出力比风电要“规矩”一些,主要受两个因素影响:太阳辐照度和组件温度。标准的光伏出力计算公式是:

[ P_{pv} = P_{STC} \times \frac{G}{G_{STC}} \times [1 - \beta \times (T_{cell} - T_{STC})] ]

其中 ( G_{STC} ) 是标准辐照度1000 W/m²,( T_{STC} ) 是标准电池温度25°C,( \beta ) 是功率温度系数(通常在0.003~0.005 /°C之间)。电池板温度本身又和环境温度、辐照度有关,工程上常用经验公式估算:

[ T_{cell} = T_{amb} + \frac{G \times (NOCT - 20)}{800} ]

我在代码里直接把这两层嵌套实现出来了,同时加入了一个“是否限电”的标记位。在调度模型里,光伏的可用出力是上限,实际出力由优化器决定——这其实就是弃光行为的数学表达。

def pv_output(irradiance, ambient_temp, pv_capacity, noct=45, beta=0.004): cell_temp = ambient_temp + irradiance * (noct - 20) / 800 derate = 1 - beta * (cell_temp - 25) return pv_capacity * (irradiance / 1000) * max(derate, 0)

3.3 场景削减:别用一整年8760小时硬算

有人可能会想:既然我有全年数据,直接跑8760小时的优化不就行了吗?问题在于,有储能设备的优化模型本质上是时序耦合的,电池的当前状态会影响下一时刻的决策。一整年数据直接丢进优化器,变量数量会膨胀到几十万甚至百万级别,求解时间长得怀疑人生。

我的方案是先用K-means或者层次聚类对全年日数据进行聚类,得到4到6个典型场景,每个场景分配一个权重。典型场景能覆盖“大风季+高负荷”“夏季辐照强+负荷降”“冬季风光双枯”等典型状态。聚类后用每个场景分别优化,最后按权重加权求和评估全年指标。这样既保留了时序特征,又把计算量降了一个数量级。

4. 储能建模的几个关键约束,新手最容易在这翻车

4.1 电池储能模型:SOC边界与充放电约束

电池模型的数学表达很简单:SOC(荷电状态)按时间步长递推,每步更新量取决于充电功率、放电功率和效率:

[ SOC_{t+1} = SOC_t + \eta_c \cdot P_c \cdot \Delta t - P_d \cdot \Delta t / \eta_d ]

但仅仅写完这个递推公式远远不够。关键的约束有三个:

  • 功率上下限:充电功率和放电功率分别有上限,不能混用一个变量然后让它取负值,除非你用商业求解器支持的特殊变量类型,否则建议拆成两个非负变量,再加一个互斥约束。
  • SOC安全区间:不建议让电池从0充到100%,工程上通常设置在10%~90%甚至更窄。全充全放会急剧加速电池老化,这在调度模型里一定要体现。
  • 充放电状态互斥:同一时刻不能既充电又放电,这需要引入二进制变量。如果你的模型规模不大,直接用整数变量做状态标志就行。

我在代码里用的是一组连续变量加上一个二进制状态变量 ( u ):充电时 ( u=1 ),放电时 ( u=0 )。互斥约束写为:

# 充电功率 <= 最大充电功率 * u prob += charge_power[t] <= max_charge * u[t] # 放电功率 <= 最大放电功率 * (1 - u) prob += discharge_power[t] <= max_discharge * (1 - u[t])

这个逻辑看起来很简单,但很多初学者会写成 ( P_{charge} \cdot P_{discharge} = 0 ),这是非线性约束,求解器处理起来非常痛苦。用二进制变量把非线性转换成线性,既快又稳。

4.2 废弃矿井小型抽水蓄能:本质是能量存储

废弃矿井的抽水蓄能模型,物理结构上比电池复杂,但在数学模型里其实可以简化成“广义能量存储”——关注的是上下水库之间的水量与能量转换关系。核心参数包括:

  • 最大蓄水量(折算成储能容量,MWh)
  • 最大抽水功率(MW)
  • 最大发电功率(MW)
  • 抽水效率(通常0.75~0.85)
  • 发电效率(通常0.82~0.90)

它的递推关系与电池SOC很相似,但有个特殊点:抽水蓄能机组的发电和抽水工况切换有时间限制,实际操作中不能频繁切换。在模型里如果要严格模拟这个特性,需要加入“最小持续运行时间”约束,但这会引入额外的整数变量,小规模项目可以直接忽略,用启发式规则控制切换频率。

我做这个项目时采用的是将蓄水储能与电池储能分开建模、在目标函数里统一调用的方式。两种储能各有各的决策变量和约束,但共享同一个新能源出力源和负荷需求——这样互相不干扰,代码结构清晰,调试起来也方便定位问题。

4.3 互补调度的核心数学表达

整个系统的功率平衡约束可以写成:

[ P_{wind} + P_{pv} + P_{pump_dis} + P_{bat_dis} + P_{grid} = P_{load} + P_{pump_ch} + P_{bat_ch} ]

其中 ( P_{wind} ) 和 ( P_{pv} ) 是实际被消纳的风光出力,( P_{grid} ) 是系统从电网购电的功率(或者向电网卖电,符号相反),所有变量都带时间下标但这里省略了。这个等式是模型的“骨架”——任何可行解都必须满足它,优化器就是在满足它以及所有设备约束的前提下找最小成本方案。

我做仿真时还额外加了一条:风电、光伏实际出力不超过可用出力的95%(留5%旋转备用),这是为了模拟系统跟踪误差带来的损耗。加上这条约束后,优化结果明显更真实,弃风弃光率从0变成了一个合理的非零值,反而让人更放心。

5. Python实现全流程:从数据读入到结果可视化

5.1 环境准备与依赖库选择

这个项目我用的核心库是PuLP做线性规划求解,Pandas处理时序数据,Matplotlib画图。PuLP的优点是语法简洁、对新手友好,而且底层可以无缝切换到CBC、GLPK等开源求解器。如果问题规模特别大,可以换成Pyomo搭配商业求解器Gurobi,但说实话,典型日24小时规模的问题,CBC完全跑得动。

安装依赖的常用命令:

pip install pulp pandas matplotlib openpyxl

我自己习惯用Anaconda管理Python环境,Python 3.9+版本的兼容性都没问题。如果在导入pulp时遇到动态库报错,多半是系统缺少GLPK的链接库,重新安装glpk或者换用conda环境能直接解决。

5.2 数据预处理:日期索引与标准化

这一步很枯燥但也最重要。原始数据可能是Excel或CSV,时间戳格式五花八门,时区问题也够喝一壶的。我通常先把时间列统一解析成datetime格式,再设置为DataFrame的索引,然后重采样成1小时粒度:

import pandas as pd df_raw = pd.read_csv('wind_pv_load.csv', parse_dates=['time']) df_raw.set_index('time', inplace=True) df_hourly = df_raw.resample('1H').mean() # 聚合成小时级数据

数据清洗上,我一般检查三件事:是否有NaN(直接向前填充或者删掉)、是否有跳变(单个时间点变化超过阈值的标记为异常)、是否有重复时间戳。这些检查看似基础,但数据不洗干净,后面跑出来的优化结果再漂亮也是空中楼阁。

5.3 优化建模与求解:一整套可用代码框架

我整理了一个精简版的建模框架,结构清晰、可直接扩展:

import pulp def build_optimization_model(wind_avail, pv_avail, load, params): T = len(load) prob = pulp.LpProblem("Complementary_Scheduling", pulp.LpMinimize) # 决策变量 wind_use = pulp.LpVariable.dicts("wind_use", range(T), lowBound=0) pv_use = pulp.LpVariable.dicts("pv_use", range(T), lowBound=0) # 电池充放电 bat_ch = pulp.LpVariable.dicts("bat_ch", range(T), lowBound=0) bat_dis = pulp.LpVariable.dicts("bat_dis", range(T), lowBound=0) # 抽水蓄能充放电 pump_ch = pulp.LpVariable.dicts("pump_ch", range(T), lowBound=0) pump_dis = pulp.LpVariable.dicts("pump_dis", range(T), lowBound=0) # 电网交互(购电为正) grid_buy = pulp.LpVariable.dicts("grid_buy", range(T), lowBound=0) # 弃风和弃光 curt_wind = pulp.LpVariable.dicts("curt_wind", range(T), lowBound=0) curt_pv = pulp.LpVariable.dicts("curt_pv", range(T), lowBound=0) # 电池SOC soc = pulp.LpVariable.dicts("soc", range(T + 1), lowBound=0.1, upBound=0.9) pump_level = pulp.LpVariable.dicts("pump_level", range(T + 1), lowBound=0.1, upBound=1.0) # 目标函数:弃风弃光惩罚 + 购电成本 + 储能损耗 prob += ( pulp.lpSum(curt_wind[t] * params["curtail_penalty"] for t in range(T)) + pulp.lpSum(curt_pv[t] * params["curtail_penalty"] for t in range(T)) + pulp.lpSum(grid_buy[t] * params["grid_price"] for t in range(T)) + pulp.lpSum((bat_ch[t] + bat_dis[t]) * params["battery_loss_cost"] for t in range(T)) ) # 功率平衡约束 for t in range(T): prob += ( wind_use[t] + pv_use[t] + bat_dis[t] + pump_dis[t] + grid_buy[t] == load[t] + bat_ch[t] + pump_ch[t] ), f"power_balance_{t}" # 弃风弃光定义 for t in range(T): prob += wind_use[t] + curt_wind[t] == wind_avail[t], f"wind_curtail_{t}" prob += pv_use[t] + curt_pv[t] == pv_avail[t], f"pv_curtail_{t}" # 电池SOC递推与约束 eta_c, eta_d = params["bat_charge_eff"], params["bat_discharge_eff"] bat_capacity = params["bat_capacity"] for t in range(T): prob += soc[t + 1] == soc[t] + (bat_ch[t] * eta_c - bat_dis[t] / eta_d) / bat_capacity prob += soc[t + 1] == soc[t] + (pump_ch[t] * eta_pc - pump_dis[t] / eta_pd) / pump_capacity return prob

这段代码把系统的核心骨架都涵盖了,剩下的就是补边界条件(比如SOC初值与终值相等,这样调度方案才是可持续的)和约束参数。求解的话:

prob.solve(pulp.PULP_CBC_CMD(msg=True)) print(f"Status: {pulp.LpStatus[prob.status]}")

如果返回状态是Optimal,就可以放心地提取各变量结果画图;如果是Infeasible,多半是边界条件设置矛盾了,比如负荷值在全天任意时刻都高于风光加储能的最大出力,那无论再怎么调度也满足不了平衡约束。

5.4 结果可视化:几张图说明问题

Python画图我用的还是Matplotlib,重点看三张图:

  • 第一张:风光出力、负荷和各类储能充放电的时序堆叠图,直观看到系统的功率流向。
  • 第二张:电池SOC和抽水蓄能蓄水量的对比曲线,看两种储能如何“接力”运行。
  • 第三张:弃风弃光率柱状图,对比不同季节典型日的表现。

有一次我在画堆叠图时发现曲线之间有负值,查了一圈发现是电池放电变量和充电变量同时不为零,而二进制互斥约束忘了加到某个时段上。这类问题光看数据表很难发现,画成图一眼就能找到异常。

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

6.1 求解器报Infeasible,先查边界条件

如果模型报告不可行,90%的情况不是代码写错了,而是约束之间互相冲突。我最常遇到的几个场景:

  • 负荷曲线里有一些异常尖峰,导致在任何时刻可用电源都不足以覆盖负荷;
  • SOC终值约束设成了固定值0.5,但电池容量太小,无法在最后一个小时内把SOC拉回目标值;
  • 抽水蓄能的初始蓄水量和最终蓄水量要求相等,但一个典型日内可转移的能量不足以满足这个条件。

排查思路是“拆约束二分法”:先把储能相关约束全部注释掉,只跑功率平衡,确认基础模型可行;然后逐步加回电池约束、抽水蓄能约束,每加一组就run一次。这个做法虽然笨,但能精准定位是哪个模块导致不可行。

6.2 结果里出现同时充放电,加状态变量

这个问题我在电池和抽水蓄能上都遇到过。如果没有二进制变量互斥,求解器为了满足目标函数,可能出现“一边充电一边放电”的荒谬结果——因为充放电的效率损耗被计入了成本,理论上求解器应该避免这种情况,但在某些约束放松的情况下确实会发生。

解决办法就是前面代码里展示的二进制状态变量方案。需要注意的一点是,加入二进制变量后模型从LP变成MILP,求解速度会明显变慢。如果遇到大规模问题求解时间过长,可以尝试把时间粒度从1小时放宽到2小时,或者在目标函数里增加一个极大的“同时充放电惩罚系数”,用软约束替代硬约束,速度会快很多。

6.3 抽水蓄能频繁启停的优化陷阱

在处理纯抽水蓄能优化时,还有一个很容易被忽视的问题:优化结果可能会让抽水蓄能机组每小时都在“抽水—发电—再抽水”之间来回切换。数学上这是可行解,因为目标函数里没有启停成本项;工程上这完全是灾难——机组频繁切换工况,磨损和维护成本高到离谱。

我的处理办法是加一个简化的“最小运行持续时间”约束,或者干脆在目标函数中加入一个与状态变化次数成正比的小惩罚项。比如统计所有相邻时段运行状态是否改变,每次改变加一个固定成本。加了这个惩罚项之后,优化器会自然倾向于减少启停次数,结果里机组的运行模式就理性多了。

6.4 参数选择没有头绪怎么办

模型里的参数真的不少:惩罚系数、储能损耗系数、初始SOC、容量配置……我个人建议不要试图一次性把所有参数都调到“最优”,而是分阶段:

  • 先固定储能容量和效率等“硬参数”;
  • 调整curtail_penalty的值,观察弃风弃光率的变化曲线,找到一个拐点;
  • 再调整grid_price与储能损耗系数的比值,观察储能是“多用”还是“少用”。

每个参数调整后都记录下目标函数的值和几个关键指标,形成一张敏感度分析表。这个表格写进论文或者项目报告里,比纯放几张代码截图有说服力得多。

7. 这个项目后续还能怎么扩展

把一个基础的风光储互补调度模型搭起来之后,可扩展的方向很多。我想到至少三个可以明显提升模型价值的点:

  • 加入功率预测模块:当前模型假设未来24小时的出力已知,现实中需要预测。可以用LSTM或者XGBoost训练预测模型,让调度真正“滚动起来”。
  • 考虑储能容量优化:把储能容量作为决策变量加入模型,可以直接回答“配置多大电池、多大抽水蓄能最经济”这个问题,变成一个扩展规划类的课题。
  • 多目标Pareto优化:用NSGA-II之类的算法同时优化经济性和可靠性,而不是简单加权求和,可以画出Pareto前沿,供决策者选择不同风险的运营模式。

我个人在实际操作中的体会是:刚开始做这类模型时,总会忍不住把模型设计得很复杂——多场景、多目标、滚动优化全都要。但实际上,能把一个24小时、两组储能、三种电源的模型跑通,并且把每一种异常情况都解释清楚,已经是非常扎实的工作了。复杂模型的每一层扩展,都应该建立在基础模型结果可信的前提之上,不然叠床架屋的架构只会让问题更难排查。

最后再分享一个小技巧:模型求解结果出来后,别急着写总结。把SOC曲线、储能功率曲线和弃风弃光率做一个Excel输出,手工核对几个典型时段的功率平衡关系。每次这样核对,几乎都能发现一些新的细节——有一次我就发现光伏出力在中午时段的限电约束没有完全生效,导致弃光量被严重低估。这种从结果反推模型的习惯,比多跑十次仿真都管用。

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

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

立即咨询