尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

风电光伏储能联合调度与混合储能优化建模实践

风电光伏储能联合调度与混合储能优化建模实践 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_in3.0, rated_wind12.0, cut_out25.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, noct45, beta0.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_{t1} SOC_t \eta_c \cdot P_c \cdot \Delta t - P_d \cdot \Delta t / \eta_d ]但仅仅写完这个递推公式远远不够。关键的约束有三个功率上下限充电功率和放电功率分别有上限不能混用一个变量然后让它取负值除非你用商业求解器支持的特殊变量类型否则建议拆成两个非负变量再加一个互斥约束。SOC安全区间不建议让电池从0充到100%工程上通常设置在10%~90%甚至更窄。全充全放会急剧加速电池老化这在调度模型里一定要体现。充放电状态互斥同一时刻不能既充电又放电这需要引入二进制变量。如果你的模型规模不大直接用整数变量做状态标志就行。我在代码里用的是一组连续变量加上一个二进制状态变量 ( u )充电时 ( u1 )放电时 ( u0 )。互斥约束写为# 充电功率 最大充电功率 * 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, inplaceTrue) 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), lowBound0) pv_use pulp.LpVariable.dicts(pv_use, range(T), lowBound0) # 电池充放电 bat_ch pulp.LpVariable.dicts(bat_ch, range(T), lowBound0) bat_dis pulp.LpVariable.dicts(bat_dis, range(T), lowBound0) # 抽水蓄能充放电 pump_ch pulp.LpVariable.dicts(pump_ch, range(T), lowBound0) pump_dis pulp.LpVariable.dicts(pump_dis, range(T), lowBound0) # 电网交互购电为正 grid_buy pulp.LpVariable.dicts(grid_buy, range(T), lowBound0) # 弃风和弃光 curt_wind pulp.LpVariable.dicts(curt_wind, range(T), lowBound0) curt_pv pulp.LpVariable.dicts(curt_pv, range(T), lowBound0) # 电池SOC soc pulp.LpVariable.dicts(soc, range(T 1), lowBound0.1, upBound0.9) pump_level pulp.LpVariable.dicts(pump_level, range(T 1), lowBound0.1, upBound1.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] ), fpower_balance_{t} # 弃风弃光定义 for t in range(T): prob wind_use[t] curt_wind[t] wind_avail[t], fwind_curtail_{t} prob pv_use[t] curt_pv[t] pv_avail[t], fpv_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(msgTrue)) print(fStatus: {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输出手工核对几个典型时段的功率平衡关系。每次这样核对几乎都能发现一些新的细节——有一次我就发现光伏出力在中午时段的限电约束没有完全生效导致弃光量被严重低估。这种从结果反推模型的习惯比多跑十次仿真都管用。
返回列表