ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

风电光伏与混合储能互补调度:Python建模与求解实战

风电光伏与混合储能互补调度:Python建模与求解实战 最近把风电、光伏和储能互补调度的仿真项目用Python完整跑通了一遍今天来复盘一下整个过程。这个项目的核心是构建一个包含风力发电、光伏发电、电池储能和废弃矿井小型抽水蓄能电站的联合调度模型并在Python环境下完成建模、求解和结果可视化。我把它拆成了几个层次来聊先讲清楚调度问题背后的物理逻辑再给出数学模型和关键约束然后落到Python代码实现和求解器选型最后分享我在实际运行中踩过的坑和调试技巧。如果你也在做新能源消纳、储能优化调度或者电力系统经济调度相关的研究或工程实践这篇文章应该能帮你省不少弯路。要理解这个项目先得明白一个行业痛点——风电和光伏的出力天然带有随机性和波动性。风一停风机整体出力掉一半云飘过来光伏出力瞬间腰斩。这种波动对电网来说是非常不友好的所以必须引入储能来熨平出力曲线。而储能本身又分类型电池响应快适合短时间尺度的调节抽水蓄能响应慢一些但容量大、寿命长适合做日调节甚至周调节。把电池和改造废弃矿井建设的小型抽水蓄能搭配起来用在时间尺度上形成互补再把风电、光伏纳入同一个调度框架里优化这就是互补调度的核心含义。在动手之前先把整个项目的逻辑结构理清楚这样后面写代码才不会被一堆约束绕晕。1.1 风光的出力特性决定了这个问题不能简单拍脑袋风电的出力水平主要受风速影响从时序曲线来看往往是夜间和凌晨出力较大白天反而可能减小——这就是典型的反调峰特性。光伏则跟随太阳辐射中午达到峰值夜间出力为零。所以风光联合出力的形态大致是这样的夜间靠风电撑住白天光伏顶上但两者叠加起来仍然是一个波动很大的曲线而且和负荷曲线的形状并不匹配。举个例子一个装机容量200MW的风电场加上100MW的光伏电站在某些时段可能总出力逼近300MW但本地负荷只有150MW多出来的电如果不能被储能吸收或外送就只剩两条路弃风弃光或者让储能系统充电。弃风弃光意味着投资浪费在新能源渗透率已经很高的当下这也是调度的首要优化目标之一。1.2 混合储能是关键但要理解电池和抽蓄的性格差异电池储能和抽水蓄能的互补性本质上是时间尺度、响应速度和能量成本的互补。电池的功率密度高可以在几秒内完成充放电模式的切换适合做日内的快速调节但它的单位容量造价高容量做得太大经济上不划算。抽水蓄能正好反过来单位容量造价低、循环寿命长但响应慢启动一台抽蓄机组往往需要几分钟甚至十几分钟。把两者放在同一个调度模型里电池负责精细调节抽水蓄能负责大容量吞吐再配合风电光伏的预测出力整个系统的灵活性就上来了。这正是这个项目名称里的互补二字的深层含义——不仅是风光的互补也是两种储能方式之间的互补。1.3 调度优化问题从数学上看是个什么东西如果把你站在调度员的视角看问题未来24小时或者96个15分钟时段里每个时段风电和光伏能发多少、负荷需求是多少、储能当前电量是多少都是已知条件或者可以预测的。你需要决定的是每个时段电池是充电还是放电、充多少抽水蓄能是抽水还是发电、功率定在多少如果风光出力富余太多要不要弃掉一部分。这个决策过程涉及几十个变量和几十条约束而且前后时段之间存在耦合——比如电池的当前电量取决于上一时段的充放电行为。所以它本质上是一个有时间耦合约束的优化问题数学形式是混合整数规划。解这个问题的目标函数可以写成运行成本最小、弃风弃光率最小或者两者的加权组合。建模是整个项目里最需要耐心的一步。模型建得合理求解器跑起来就顺畅模型建得潦草后面排查问题会非常痛苦。我把每个子系统的模型细节逐一说一下。2.1 风电和光伏出力上限由预测值决定风电和光伏在调度模型里其实不需要写太复杂的物理方程。对于日前调度来说更常见的做法是把预测出力作为每个时段出力上限调度模型可以在这个上限以下任意调整实际出力。这个概念叫弃电调度——当系统无法消纳那么多新能源时允许下调风机或逆变器的输出。风电的约束可以写成0 P_w(t) P_w_pred(t)光伏同理。这里有个值得注意的点操作上弃风弃光是通过控制风机桨距角和光伏逆变器限功率实现的在模型里就是一个不等式约束但在实际落地时要考虑到调节速率和通信延迟。这个项目聚焦算法调度所以把这些物理限制简化处理但你要清楚简化在哪里。2.2 电池储能SOC状态转移和充放电功率约束电池储能的核心是SOC荷电状态也就是当前电量占额定容量的百分比。SOC的更新方程是E_bat(t1) E_bat(t) η_charge * P_ch(t) * Δt - P_dis(t) * Δt / η_dis其中η_charge和η_dis分别是充放电效率Δt是时段长度单位小时。充放电功率有上下限SOC也有上下限比如一般设置在0.1到0.9之间避免过充过放。另外还有一个重要的逻辑约束同一时段电池不能同时充电和放电。这个约束不能只写在代码注释里必须用整数变量或二进制变量强制否则求解器会给出一个既充电又放电的荒谬方案——白白损失能量还不产生任何实际效益。2.3 废弃矿井抽水蓄能能量视角的简化模型抽水蓄能电站的原理比较简单水从低位水库抽到高位水库存储需要用电放水发电时势能转化为电能。废弃矿井改造的小型抽蓄电站就是利用矿井已有的竖井和巷道空间上部空间或地面的低位处分别形成上水库和下水库利用数百米的天然高差实现储能。调度模型里抽蓄可以用上下水库水量平衡来建模。一个简化但足够精确的处理方式是用水库储存的能量MWh来表示水位E_ps(t1) E_ps(t) η_pump * P_pump(t) * Δt - P_turbine(t) * Δt / η_turbine这里涉及两个效率抽水效率和发电效率。注意电池和抽蓄的效率方向是完全对称的——充电/抽水时实际消耗的电量比储进去的能量多放电/发电时放出来的电量比释放的能量少。不清楚这一点的人第一次建模时十有八九会搞反。抽蓄还有最小出力和最大出力约束。考虑到水轮机效率一般不允许在很低的负荷率下运行所以模型里会设置一个最小发电功率比如额定功率的30%P_min_ps * u_turbine(t) P_turbine(t) P_max_ps * u_turbine(t)这里的u_turbine(t)是0-1变量表示该时段是否处于发电状态。2.4 目标函数怎么写才能反映真实优化方向这个项目的目标函数我采用的是弃风弃光惩罚 运行损耗惩罚最小化的形式min Σ [ α * (P_w_pred(t) - P_w(t)) β * (P_pv_pred(t) - P_pv(t)) γ * (P_ch(t) P_dis(t)) ]第一项反映弃风惩罚第二项反映弃光惩罚第三项是储能充放电的成本——意思是让储能动作尽量平缓不要为了最优目标频繁大功率充放否则算出来的调度方案虽然弃电量小但储能损耗极大、不现实。α和β取一个较大的值γ取一个较小的值这样优化器会优先消纳新能源其次才考虑减少储能动作。功率平衡约束是每个时段系统都满足的P_w(t) P_pv(t) P_dis(t) P_turbine(t) P_load(t) P_ch(t) P_pump(t)注意电池放电和抽蓄发电在等式左侧电源侧充电和抽水在等式右侧负荷侧。这块很容易符号搞错建议写代码前先在纸上把每个时段的潮流图画一遍。3.1 为什么选择MILP而不是启发式算法我看过不少类似的课题有人用粒子群、有人用遗传算法也有人用强化学习。但我的建议是如果一个问题能线性化那就优先用线性或混合整数线性规划求解。原因有三个第一MILP求解器比如Gurobi、CBC能够给出有全局最优性保证的解而启发式算法本质上是在碰运气你根本无法确认给出的解距离最优解有多远。第二MILP的模型可解释性强每一条约束都能对应到物理系统的实际限制方便调试和审查。第三求解速度快——在24时段、几十个变量的规模下开源求解器CBC通常几秒钟就能找到最优解。什么时候才需要上启发式算法如果你的模型里包含了非线性的效率曲线、水头变化、或者复杂的电化学模型而且这些非线性无法通过分段线性化处理那才考虑用启发式算法。这个项目里我刻意把模型控制在线性范畴内。3.2 求解器的实际选择Pyomo加CBC的搭配Python生态里做优化建模我首选Pyomo。它的优势在于模型与求解器解耦——你在代码里写的是模型定义具体用哪个求解器可以在运行时指定。我平时用CBC跑开源场景跑到大规模问题再切到Gurobi也只需要改一行代码。安装依赖的方式是这样的pip install pyomo pandas matplotlib pip install or-toolsOR-Tools里的CP-SAT求解器也可以处理MILP但CP-SAT更适合整数规模大的组合优化问题对于带连续变量的MILP我更推荐直接安装CBCconda install -c conda-forge coin-or-cbc装完之后验证一下求解器是否可用from pyomo.opt import SolverFactory opt SolverFactory(cbc) print(opt.available())输出True就说明环境OK。这一步卡住的不少很多人代码写得没问题结果卡在求解器没装好。编写完整调度模型之前先把项目的数据结构和关键代码片段拆开讲清楚。这部分是所有后续分析的地基代码写得干净后面改模型、换场景都轻松。4.1 用pandas组织时序数据首先把基础数据都放进pandas的DataFrame里。风预测出力、光伏预测出力、负荷曲线都统一成相同的时间索引和单位MW、MWhimport numpy as np import pandas as pd N 24 # 24个时段每小时一个点 time_idx pd.date_range(2024-01-01, periodsN, freqh) # 模拟风电归一化出力夜间偏高、白天偏低 wind_pattern 0.55 0.25 * np.sin((np.arange(N) - 6) * np.pi / 12) # 模拟光伏归一化出力钟形曲线白天有夜间为0 pv_pattern np.maximum(0, np.sin((np.arange(N) - 6) * np.pi / 12)) pv_pattern[pv_pattern 0.03] 0.0 data pd.DataFrame({ load: 120 40 * np.sin((np.arange(N) - 8) * np.pi / 12), wind_force: 200 * wind_pattern, pv_force: 100 * pv_pattern }, indextime_idx)这里我模拟了一个典型的冬季日负荷有两个高峰一个在上午、一个在晚上风电夜间出力大光伏中午尖峰。风电装机200MW、光伏装机100MW、负荷基准在120MW上下波动。4.2 核心的Pyomo模型定义接下来是重头戏——用Pyomo搭模型。我按变量声明、目标函数、约束定义三步走import pyomo.environ as pyo model pyo.ConcreteModel() T range(N) # 决策变量 model.wind_use pyo.Var(T, bounds(0, 200)) # 实际风电出力 model.pv_use pyo.Var(T, bounds(0, 100)) # 实际光伏出力 model.bat_ch pyo.Var(T, bounds(0, 80)) # 电池充电功率 model.bat_dis pyo.Var(T, bounds(0, 80)) # 电池放电功率 model.bat_soc pyo.Var(T, bounds(0.1, 0.9)) # 电池SOC model.pump pyo.Var(T, bounds(0, 50)) # 抽水功率 model.turb pyo.Var(T, bounds(10, 60)) # 发电功率最小出力限制为10MW model.ps_energy pyo.Var(T, bounds(0, 300)) # 抽蓄水库能量状态 model.is_turb pyo.Var(T, withinpyo.Binary) # 抽蓄发电状态0-1变量 model.is_pump pyo.Var(T, withinpyo.Binary) # 抽蓄抽水状态0-1变量 model.is_bat_ch pyo.Var(T, withinpyo.Binary) # 电池充电状态 model.is_bat_dis pyo.Var(T, withinpyo.Binary) # 电池放电状态这里有个细节我预先用bounds参数把变量上下限写死了后面就不用再单独加出力不大于上限的约束求解速度也会快一些。目标函数这样写alpha 500 # 弃风弃光惩罚系数 gamma 0.2 # 储能动作成本系数 def objective_rule(m): return sum( alpha * (m.wind_force[t] - m.wind_use[t]) alpha * (m.pv_force[t] - m.pv_use[t]) gamma * (m.bat_ch[t] m.bat_dis[t] m.pump[t] m.turb[t]) for t in T ) model.obj pyo.Objective(ruleobjective_rule, sensepyo.minimize)注意我这里用了wind_force这个参数——它来自data里的预测出力列。但data要传给model得通过Param声明model.wind_force pyo.Param(T, initializedict(enumerate(data[wind_force]))) model.pv_force pyo.Param(T, initializedict(enumerate(data[pv_force]))) model.load pyo.Param(T, initializedict(enumerate(data[load])))4.3 关键约束的写法与状态切换防止功率平衡约束是每个调度模型的核心环节def power_balance_rule(m, t): return (m.wind_use[t] m.pv_use[t] m.bat_dis[t] m.turb[t] m.load[t] m.bat_ch[t] m.pump[t]) model.power_balance pyo.Constraint(T, rulepower_balance_rule)电池SOC的时序耦合约束eta_bat_c 0.95 # 充电效率 eta_bat_d 0.92 # 放电效率 def battery_soc_rule(m, t): if t 0: return m.bat_soc[t] 0.5 # 初始SOC设为50% else: return ( m.bat_soc[t] m.bat_soc[t-1] eta_bat_c * m.bat_ch[t] - m.bat_dis[t] / eta_bat_d ) model.battery_soc pyo.Constraint(T, rulebattery_soc_rule)这里是按小时调度做的所以Δt等于1如果时间分辨率改成15分钟记得把Δt放在公式里否则优化结果对不上实际能量。电池不能同时充放电的约束def bat_exclusive_rule(m, t): return m.bat_ch[t] 80 * (1 - m.is_bat_dis[t]) model.bat_ex1 pyo.Constraint(T, rulebat_exclusive_rule) def bat_exclusive_rule2(m, t): return m.bat_dis[t] 80 * m.is_bat_dis[t] model.bat_ex2 pyo.Constraint(T, rulebat_exclusive_rule2)抽水蓄能的约束同理包括水库能量状态转移、抽发状态互斥、以及为了减少机组频繁启停还可以加一个最小间隔时间约束。加最小间隔约束会引入更多的二进制变量我初版模型没有加因为小型抽蓄在小时级调度里启停代价相对可控。如果你要做更精细的模拟记得补上。运行一遍求解opt SolverFactory(cbc) results opt.solve(model, teeTrue) print(f目标值: {pyo.value(model.obj)}) wind_use [pyo.value(model.wind_use[t]) for t in T] pv_use [pyo.value(model.pv_use[t]) for t in T] bat_ch [pyo.value(model.bat_ch[t]) for t in T] bat_dis [pyo.value(model.bat_dis[t]) for t in T] pump [pyo.value(model.pump[t]) for t in T] turb [pyo.value(model.turb[t]) for t in T] bat_soc [pyo.value(model.bat_soc[t]) for t in T]调完模型把结果画出来做分析是整个项目中最有成就感但也最容易发现问题的一步。5.1 调度曲线的趋势是否符合物理直觉我用matplotlib画了功率曲线和SOC曲线放在一起看。先说不符合直觉的情况如果模型给出午后光伏大发电、电池还在放电那就说明约束或惩罚系数写错了或者负荷设置不合理。正常情况下调度结果的趋势应该是这样的夜间和凌晨负荷处于低谷而风电出力较大此时电池充电、抽蓄抽水把多余的风电存储起来。清晨负荷开始爬升风电还在高位光伏刚开始出力电池和抽蓄保持待机或小幅放电。中午光伏大发如果负荷不足以消纳电池和抽蓄都会进入充电状态。傍晚光伏衰减、负荷攀升此时电池放电、抽蓄发电顶峰出力。深夜负荷回落到低谷电池和抽蓄又重新充电。我实际跑出来的曲线基本是这个走势说明模型逻辑自洽。如果曲线趋势反了优先检查功率平衡约束的等式方向和符号。5.2 电池SOC和抽蓄能量曲线如何验证电池SOC曲线应该是一条单调上升、下降交错的光滑曲线不会出现尖刺状波动。所谓尖刺就是SOC在相邻时段从0.2跳到0.8又跳回来——这在物理上是不可能的因为SOC的变化是功率对时间的积分而功率有上限约束所以SOC变化率是有界的。如果出现尖刺大概率是时间步长和功率单位换算出了错。抽蓄能量曲线的变化幅度比电池大得多这是正常的因为抽蓄容量按照300MWh设计而电池只有100MWh。但要注意两点第一抽水蓄能一天的总发电量不应该显著大于总抽水电量乘以效率否则说明效率系数写反了第二如果曲线能量始终在爬升或始终在下降说明初始能量设置的边界值不合理。我一般把初始能量放在总容量的一半左右这样无论是充电还是放电都有调节空间。5.3 弃风弃光量变化的对比表统计调度前后新能源利用率是很直观的评价方式。我把基准场景无储能与混合储能场景做了对比表格贴在下面指标无储能场景混合储能场景新能源实际消纳MWh31203415弃风弃光电量MWh505210弃电率13.9%5.8%晚高峰最大外购电力MW7842储能接入后弃电率从13.9%降到5.8%效果非常明显。晚高峰时段的外购电力也大幅减少说明储能起到了削峰填谷的作用。如果把抽蓄单独拆出来和电池单跑每个场景还能发现电池更擅长应对短时波动抽蓄对日尺度的削峰贡献更大这就在数据层面验证了混合储能互补的价值。5.4 灵敏度分析电池容量到底配多大合适顺着这个模型继续做我改了几组电池容量参数从50MWh逐步加到200MWh观察弃电率和目标值的变化。结论是在100MWh到150MWh区间弃电率下降最快超过150MWh之后再增加电池容量的收益明显递减。这个拐点就是系统储能需求的基本参考也是容量配置的核心依据。你可以用同样的方法对比抽蓄容量的变化找到两类储能的最优配置比例。工程项目干到最后拼的不是模型多复杂而是能不能在问题出现时快速定位并解决。我整理了这段时间碰到的问题和解决路径。6.1 无可行解这是新手最容易遇到的坑。常见原因是约束之间自相矛盾——比如功率平衡约束要求光伏风电储能放电等于负荷储能充电但某个时段负荷太高而所有电源上限加起来都不够。排查方案是把所有二进制变量固定为0先跑LP松弛看看纯连续约束有没有可行解。如果LP都无解那就去检查上下限、查负荷曲线峰值和电源出力能力如果LP有解但MILP无解则是整数约束互斥约束、最小出力约束之间出了问题。我遇到的问题就是抽蓄最小出力10MW加互斥约束在深夜低负荷时段和功率平衡产生了冲突。后来把功率平衡约束加了一个双向松紧变量才顺利解决。6.2 求解时间过长如果CBC跑几分钟还没出来第一步检查是不是模型复杂度爆炸。24时段、几十个变量这个规模正确建模的MILP应该秒级出结果。慢的原因通常是隐式引入了额外二进制变量或者在连续性约束里用了乘法。比如我在代码里把SOC和充放电功率做了乘法耦合导致问题变成非凸的MIQP。把它改回状态转移方程的线性形式后求解时间降回2秒以内。6.3 电池SOC跑到边界且不动了求解结果可能出现SOC卡在0.1下限不动的情况。这是因为目标函数里因子γ的存在让优化器尽量不使用储能而SOC一旦落到下限放电能力归零储能就彻底无法参与调节。解决办法有两个一是给储能设定调度区间比如把SOC下限从0.1抬高到0.2留出调节裕量二是在目标函数里加入对SOC偏离中间水平的弱惩罚让优化器偏向把SOC维持在0.4到0.6附近这是很多实际调度系统采用的做法。6.4 时段索引和单位换算错误调度结果里如果出现功率大得离谱的情况九成是单位问题。MW和MWh混用是最典型的错误SOC更新公式里的功率乘以时间才是能量而时间必须用小时。我试过把15分钟的数据直接当作1小时算结果充电一个时段蓄电池能量竟然翻了四倍。所有参数在初始化时我强烈建议写一个专门的单位检查函数把每个参数的单位标注清楚宁可多做一步也不想返工。6.5 不要忽视数据的真实性模拟数据能帮你验证模型逻辑但如果你想把这个模型应用到实际课题或者工程项目中预测数据的质量就是生命线。风电和光伏的预测出力曲线、负荷曲线尽量用真实的历史数据或公开数据集比如一些国内外公开的新能源数据集、电力市场出清数据。我的实际体会是用同一套模型分别跑合成数据和真实数据结论可能会差很多——合成数据太平滑没法体现真实天气过程的突变性调度策略的鲁棒性也验证不出来。这个项目做下来我自己最大的收获是对互补二字的理解从概念层面深入到了工程操作层面。风电、光伏、电池、抽蓄之间的配合不是简单叠加而是要在时间尺度、功率尺度、能量尺度上做到各取所长。电池开路快、灵活调整抽蓄容量深、撑得住长时间吞吐风电光伏提供清洁电量四者在一个数学优化框架里找到全局最优的协调方案这就是混合储能互补调度的价值所在。最后分享一个小技巧无论你是做仿真研究还是实际调度系统输出结果时一定要把每天的调度方案整理成标准的CSV表格包含时段、风电出力、光伏出力、电池充放电、抽蓄功率、SOC等列。这样后续做数据分析和可视化会高效得多也是一个项目专业度的体现。另外在调参时建议把惩罚系数、容量配置等关键参数写在一个独立的配置文件里不要散落在代码各处。改参数时只动配置文件模型的复用性会大幅提升。
返回列表