拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

风光氢储协同调度实战:多时间尺度优化与Pyomo建模

风光氢储协同调度实战:多时间尺度优化与Pyomo建模

简介:面向新能源制氢系统优化调度研究者与工程师,这份PDF完整复现了多时间尺度优化调度策略,覆盖孤岛与并网两种典型场景。内容从风机、光伏发电模型到锂电池储能、电解槽集群控制均有可运行Python代码与逐段解释,并针对孤岛场景日前阶段的改进雷达图模型优化(负荷失电率、失氢率、经济效益及弃风弃光量)、日内阶段的模型预测控制(MPC)功率跟踪,以及并网场景下电-氢综合需求响应模型(价格型、替代型、激励型)给出实现细节,可帮助读者快速理解并复现论文算法。资源包仅含1个PDF文档,大小792KB,体量精简但代码密度高,涵盖系统建模、优化求解、参数整定与实际案例对比分析,适合有一定Python与优化调度基础的研究人员按需查阅。目前已有127人学习下载,可作为构建或验证自身调度策略的实用参考基准。

1. 风光氢储协同调度:为什么“一句话需求”背后全是时间尺度问题

早上8点到晚上8点的风电出力可能翻三倍,光伏在午后暴涨又在傍晚骤降,电解槽却不可能跟着每一秒的功率剧烈启停。正是这种“电源波动快、制氢设备响应慢”的矛盾,让【新能源制氢系统】的多时间尺度优化调度从一句口号变成了真正要写代码解决的工程问题。风光氢储协同调度的本质,是把风电、光伏、电解槽、储氢罐、燃料电池当成一个整体来调度,在日前、日内、实时三个时间尺度上分别做决策,再把决策层层下传。需求响应在这个系统里不是简单的“少用电”,而是把电解槽的功率曲线当成可平移、可调整的柔性资源,配合电网信号改变制氢节奏。

这套方案适合谁?一类是做园区级风光制氢示范项目的人,需要把调度策略落到能跑的代码上;另一类是研究微电网或综合能源系统、想快速搭出一版可复现优化模型的学生和工程师。读完这篇,你能得到一套用 Python 和 Pyomo 实现的两阶段优化框架,以及那些模型里看不出来、跑现场才会撞上的边界问题。

2. 建立优化模型:目标函数、约束与两套时间基准

2.1 目标函数怎么写:制氢成本、售氢收益与弃电三者最小化

多时间尺度优化调度首先要回答一个经济学问题:在满足制氢产量的前提下,系统每一分钟该让谁发电、让谁充电、让谁停机。目标函数需要同时覆盖三类成本:从电网购电的费用、制氢设备的运行维护成本、以及弃风弃光带来的惩罚成本。如果园区还有售氢收入,可以把售氢收益作为负成本放进目标函数,让模型在“多产氢多赚钱”和“多用电多花钱”之间自己权衡。

一个常见的做法是把目标函数写成日前的总成本最小化,时间步长取1小时,决策变量包括各机组的出力、储氢罐的充放氢功率、电解槽的启停状态。这里有一个很容易犯的错:用单一时间尺度去描述电解槽的爬坡约束,结果模型每小时都在大幅调整功率,实际设备根本跟不上。因此目标函数里必须加入对电解槽功率变化的惩罚项,让调度结果尽量平滑。

2.2 约束条件里真正难的是耦合约束

风光氢储协同调度的约束可以分成三类:电源出力约束、储能状态约束、以及电解槽运行约束。电源出力约束最直接,风电和光伏的出力上限由预测曲线给定,下限为0;储能状态约束要求储氢罐的容量在上下限之间,且充放氢不能同时进行;电解槽约束则要描述最小运行功率、爬坡速率和启停次数上限。

真正难处理的是耦合约束:电解槽的功率来自风电、光伏和储氢系统共同供给,而储氢罐的另一端又连着燃料电池,燃料电池发电后可以反哺负荷或电解槽。这个循环结构让模型变成了带二进制变量的混合整数线性规划(MILP),求解时间随设备数量增长很快。如果你在复现时发现求解器卡住,优先检查耦合约束里是否写了多余的互斥条件,而不是急着换求解器。

电解槽还有一个区别于电池的特性:它有一个最低运行功率门槛,比如额定功率的20%。低于这个值电解槽必须停机,停机后再启动又有时间和成本代价。这类“非连续运行区间”必须用二进制变量建模,否则线性规划会把电解槽功率降到0到20%之间的某个值,得到一个现场根本无法执行的调度方案。

2.3 日前/日内/实时三层之间到底传什么

三层调度各自承担不同的职责。日前调度以1小时为步长,根据风光出力预测和次日分时电价,决定每小时的购电功率、电解槽启停和储氢罐的充放计划;日内调度以15分钟为步长,用更新的预测数据修正日前计划;实时控制以秒级或分钟级执行,负责跟踪日内指令并处理突发偏差。

层与层之间传递的不是整个计划表,而是几个关键参数:日前传给日内的是电解槽的启停状态和储氢罐的SOC目标带,日内传给实时的是当前时刻的功率指令和可调范围。这样做的好处是,下一层只需要在一个较小的决策空间里做微调,不需要重算全局。工程上这个设计很容易被忽略,有人直接把日前的完整计划拿来跟踪,结果光伏一波动,整个系统就开始频繁调整,储氢罐SOC被反复顶到上限。

3. 用 Python 和 Pyomo 实现两阶段调度:可复现的代码骨架

3.1 数据准备:造一套能复现的风光与负荷场景

复现一个多时间尺度调度模型,不需要真实的历史数据也能验证逻辑。我一般会先用随机生成的方式造出24小时的风电、光伏和电价曲线,再跑通调度模型,最后再把真实数据替换进去。这样能保证你看到的每一步结果都是确定的,排错时不会因为数据波动而怀疑模型本身。

import numpy as np import pandas as pd # 设置随机种子,保证每次运行结果一致 np.random.seed(42) hours = 24 # 风电出力基线 + 随机波动,模拟白天风电较弱、夜间较强 wind_base = 40 + 20 * np.sin(np.linspace(0, 2 * np.pi, hours)) wind = wind_base + np.random.normal(0, 3, hours) wind = np.clip(wind, 0, 80) # 光伏出力:只在 6:00-18:00 之间有出力,午间达到峰值 pv = np.zeros(hours) for h in range(6, 18): pv[h] = 50 * np.sin(np.pi * (h - 6) / 12) pv += np.random.normal(0, 2, hours) pv = np.clip(pv, 0, 50) # 分时电价:峰、平、谷三段 price = np.array([0.5] * hours) price[8:12] = 1.2 price[18:21] = 1.4 price[0:6] = 0.3 # 生成 DataFrame,方便后续建模时索引 data = pd.DataFrame({ 'wind': wind, 'pv': pv, 'price': price }) print(data.head())

这里把风电出力基线设在40千瓦左右,光伏峰值50千瓦,加上随机噪声模拟预测误差。电价曲线的峰谷时段是典型的三段式设置,你可以根据自己的项目调整。注意随机噪声用的是正态分布,这在模拟预测误差时比较合理,但实际历史数据往往存在偏态,后面接入真实数据时要做归一化处理。

3.2 日前调度:MILP 求解主体代码

日前调度的核心是在给定预测曲线的前提下,求解一组最优决策。下面用 Pyomo 建立混合整数线性规划模型,决策变量包括购电功率、电解槽功率、储氢罐充放氢功率等。为了控制篇幅,这里只展示主体约束,完整约束可按项目需要补齐。

import pyomo.environ as pyo # 创建具体模型实例 model = pyo.ConcreteModel() T = range(24) # 决策变量:购电功率、电解槽功率、储氢罐充放氢功率 model.Pbuy = pyo.Var(T, bounds=(0, 100), domain=pyo.NonNegativeReals) model.Pel = pyo.Var(T, bounds=(0, 60), domain=pyo.NonNegativeReals) model.Pch = pyo.Var(T, bounds=(0, 30), domain=pyo.NonNegativeReals) model.Pdis = pyo.Var(T, bounds=(0, 30), domain=pyo.NonNegativeReals) # 二进制变量:电解槽启停状态,1 表示运行 model.Z = pyo.Var(T, domain=pyo.Binary) # 储氢罐 SOC,范围 10% 到 90% model.soc = pyo.Var(T, bounds=(0.1, 0.9)) # 方程:功率平衡,风光 + 购电 = 电解槽 + 电网外送(简化场景不设外送) def power_balance_rule(model, t): return data.loc[t, 'wind'] + data.loc[t, 'pv'] + model.Pbuy[t] == model.Pel[t] + model.Pch[t] model.power_balance = pyo.Constraint(T, rule=power_balance_rule) # 储氢罐 SOC 递推关系:罐体容量 100 kWh,效率 0.9 def soc_rule(model, t): if t == 0: return model.soc[t] == 0.5 else: return model.soc[t] == model.soc[t-1] + (model.Pch[t-1] * 0.9 - model.Pdis[t-1] / 0.9) / 100 model.soc_cons = pyo.Constraint(T, rule=soc_rule) # 电解槽最小运行功率约束:运行时功率不小于 15 kW def min_load_rule(model, t): return model.Pel[t] >= 15 * model.Z[t] model.min_load = pyo.Constraint(T, rule=min_load_rule) # 目标函数:购电费用 + 启停惩罚 + 功率变化惩罚 model.cost = pyo.Objective( expr=sum((data.loc[t, 'price'] * model.Pbuy[t]) for t in T) + sum(5 * model.Z[t] for t in T) + sum(0.1 * (model.Pel[t+1] - model.Pel[t])**2 for t in range(23)), sense=pyo.minimize ) # 用 CBC 求解器求解 solver = pyo.SolverFactory('cbc') results = solver.solve(model, tee=False) print('求解状态:', results.solver.termination_condition)

功率平衡约束里的关键是购电功率变量,它给系统留了一个弹性出口,避免风小光弱时无解。SOC 递推里除以100,是因为储氢罐容量为100千瓦时,这样 SOC 的变化量落在0到1之间,数值稳定性更好。电解槽最小运行约束用的是“大M写法”的简化版:当 Z 为0时功率下限为0,当 Z 为1时下限为15千瓦。如果你在求解时遇到数值警告,优先检查 SOC 变量的取值范围,权重差异过大时求解器容易误判整型变量。

3.3 日内跟踪:把日前计划当参考的 MPC 主体代码

日内调度我习惯用模型预测控制(MPC)思路做滚动优化,每次只预测未来4个时间点(1小时),求解后只执行第一个指令,然后滚动到下一个时刻。这样计算量小,而且能不断用最新数据修正偏差。

def mpc_step(current_soc, forecast_wind, forecast_pv, forecast_price, prev_pel, weight_soc=0.5): model = pyo.ConcreteModel() Horizon = range(4) model.Pbuy = pyo.Var(Horizon, bounds=(0, 100)) model.Pel = pyo.Var(Horizon, bounds=(0, 60)) model.Pch = pyo.Var(Horizon, bounds=(0, 30)) model.Pdis = pyo.Var(Horizon, bounds=(0, 30)) model.soc = pyo.Var(Horizon, bounds=(0.1, 0.9)) def balance_rule(model, k): return forecast_wind[k] + forecast_pv[k] + model.Pbuy[k] == model.Pel[k] + model.Pch[k] model.balance = pyo.Constraint(Horizon, rule=balance_rule) def soc_track_rule(model, k): if k == 0: return model.soc[k] == current_soc + (model.Pch[k] * 0.9 - model.Pdis[k] / 0.9) / 100 else: return model.soc[k] == model.soc[k-1] + (model.Pch[k-1] * 0.9 - model.Pdis[k-1] / 0.9) / 100 model.soc_track = pyo.Constraint(Horizon, rule=soc_track_rule) # 目标是尽量贴近日前计划,同时保持 SOC 在目标范围内 def mpc_objective(model): return sum((model.Pel[k] - prev_pel[k])**2 for k in Horizon) + weight_soc * sum((model.soc[k] - 0.5)**2 for k in Horizon) model.obj = pyo.Objective(rule=mpc_objective, sense=pyo.minimize) solver = pyo.SolverFactory('cbc') solver.solve(model, tee=False) return model.Pel[0].value, model.soc[0].value

这段代码的精髓在于目标函数不直接最小化运行成本,而是最小化对日前计划的偏离。这样做的原因是,日内阶段电价已经锁定,过度追求经济性会导致功率频繁波动,不利于电解槽健康。weight_soc 参数控制 SOC 回归目标带的力度,我一般取0.5,如果储氢罐容量较小可以加大到2,但过大时会牺牲跟踪精度。

4. 需求响应信号从哪接入、参数怎么定

4.1 需求响应在这里不是“削负荷”,而是“平移电解槽功率”

传统需求响应是电力用户根据电网信号削减用电负荷,而制氢系统有个特殊优势:电解槽本身就是一种可平移负荷。氢气可以存储,所以今天少产的那部分氢,明天可以多产补回来,只要储氢罐有剩余容量。这意味着需求响应的目标不是限制用电总量,而是优化用电的时序分布。

当一个削峰信号到达系统时,调度策略要做的是把电解槽的功率从峰时段平移到谷时段,同时保证24小时内的总制氢量不下降。这要求在目标函数里增加一个“响应量约束”:在指定的响应时段内,电解槽功率相对基准曲线减少的量必须大于等于申报值。如果你的代码里已经建好了电解槽功率变量,接入这个约束只需额外加一组不等式。

4.2 接入方式:信号处理与参数设置

实际项目中需求响应信号一般有三种接入方式:从电网调度系统通过通信协议直接下发、从电力交易中心的分时电价间接体现、或者由园区能量管理系统在本地生成。前两种方式只需要把信号值解析成约束边界;第三种方式则要做预测,例如根据天气预测判断明天上午是否有弃风风险,提前生成响应需求。

代码实现时我会把需求响应信号统一处理成一个长度为24的数组dr_signal,取值0或1。1表示该时段需要削减负荷,0表示正常调度。然后在模型中增加一个响应量变量,用它表示削减的功率:

model.DR_cut = pyo.Var(T, bounds=(0, 30), domain=pyo.NonNegativeReals) # 当 dr_signal[t] = 1 时,强制削减至少 20 kW def dr_rule(model, t): if dr_signal[t] == 1: return model.DR_cut[t] >= 20 else: return model.DR_cut[t] == 0 model.dr_cons = pyo.Constraint(T, rule=dr_rule) # 把削减量放进功率平衡约束 def balance_rule_with_dr(model, t): return data.loc[t, 'wind'] + data.loc[t, 'pv'] + model.Pbuy[t] == model.Pel[t] + model.Pch[t] + model.DR_cut[t]

这里把 DR_cut 当作一个“电负荷”处理,本质上是告诉模型:有一部分电被响应掉了,不能用来制氢或储氢。这样模型会自动调整购电计划和储氢罐充放策略来满足削减要求。参数20千瓦是根据你的系统规模定的,一般取电解槽额定功率的30%~40%比较合适,太小没实际意义,太大会导致无解。

4.3 响应费用怎么定:不要让模型为了响应而响应

需求响应如果完全没有经济收益,模型肯定不愿意削减负荷。所以在目标函数中要加入响应补偿项,让模型在“获得补偿”和“损失制氢量”之间做权衡。补偿价格设置一般参考当地需求响应补偿标准,常见做法是峰时电价的2~3倍。

响应还有一个现实约束:一天内参与响应的次数上限。电解槽频繁启停不仅磨损设备,还会产生额外的维护成本。这个约束用代码表达很简单,只需统计一天内 DR_cut 从0变到正数的次数,限制小于等于2次。如果忽略这个约束,模型可能在每个峰时段都削减一点功率去拿补偿,虽然从数学上看收益最大,但现场设备受不了。

5. 避坑指南:从实际项目中踩出来的5个问题

5.1 现象:MILP 求解时间长到无法做日内滚动

第一次跑日内滚动优化时,我把日前模型的全部约束复制过来,只改了预测数据,结果每个滚动窗口要解40秒,根本没法做15分钟级的跟踪。

原因是模型里带了二进制变量,MILP 的求解时间随整数变量数量指数增长。解决方法是把日内模型做成纯线性规划:电解槽的最小运行功率约束在日内阶段可以放宽,因为日前已经把启停状态定下来了,日内只需要跟踪功率,不重新决策启停。这样去掉二进制变量后,单次求解时间降到0.2秒以内。

5.2 现象:储氢罐 SOC 越界,但约束里明明写了限值

排查时发现约束soc >= 0.1存在,但运行结果中 SOC 还是到了0.08。原因是 SOC 的递推公式里充放氢效率不对称,充入时乘以0.9,放出时除以0.9,这两个操作叠加之后,实际可使用容量小于标称值。

解决方法是把 SOC 上下限按实际效率折算,而不是按几何容量折算。比如标称100千瓦时,效率0.9,实际可用下限对应的氢量是100 * 0.1 / 0.9,约11.1千瓦时。把约束边界放宽到0.09,同时增加一个安全余量,可以避免数值求解时因为浮点误差触碰边界。

5.3 现象:需求响应信号频繁跳变,电解槽功率跟着震荡

项目联调时,电网下发的削峰信号每5分钟变化一次,导致模型每次滚动优化都在调整电解槽功率。现场人员反馈电解槽温度波动变大,产氢效率下降。

根因是需求响应信号没有做死区处理。解决方法是加一个信号滤波模块:在一个调度周期内,如果信号变化量小于设定阈值,则维持原状态。阈值一般取系统总负荷的5%,太小滤不干净,太大又响应不及时。

5.4 现象:目标函数里量纲不一致,惩罚项失去作用

代码里购电成本是几千元量级,启停惩罚是5元量级,功率变化惩罚是0.1元乘功率差的平方,三个数量差两三个数量级。结果求解器把精力全放在优化购电成本上,启停和功率变化约束形同虚设。

解决方法是先跑一次不带惩罚项的求解,看看各成本项的数量级,再把惩罚系数设置成对应成本项的5%~10%。我用这个办法调过几次模型后,基本能在一个调度周期内得到变化平滑、启停合理的方案。

5.5 现象:接入真实数据后模型频繁无解

模拟数据跑得好好的,换成真实风光数据就报infeasible。检查发现真实数据里包含极端天气片段,例如连续阴雨导致光伏出力接近0,同时风电也处于低出力段,系统无法满足功率平衡。

解决方法是给功率平衡约束增加松弛变量。松弛变量代表未满足的负荷量,在目标函数中给予一个很大的惩罚系数。这样模型即使遇到极端场景也能给出“尽量满足”的可行解,而不至于崩溃。惩罚系数要远大于正常购电成本,通常取最贵电价的10倍。

6. 验证方法:用历史出力反推目标曲线,证明你的调度策略有效

证明多时间尺度调度策略有效,不能只看目标函数值下降了,还要看三个指标:储氢罐SOC是否稳定在安全区间内、电解槽功率波动是否明显降低、需求响应时段的实际削减量是否达到申报值。我的做法是把历史数据按时序分两段,前7天做滚动调度模拟,后3天做结果比对。

具体做法是先跑一版“纯日前调度”作为基准,再跑一版“日前+日内”的两阶段调度,对比两组结果的电解槽功率标准差。如果两阶段调度的标准差比基准降低超过20%,说明日内修正策略起了作用。再看需求响应时段前后的SOC曲线,如果响应结束后SOC很快回到目标带,说明系统有足够的恢复能力。

还有一个容易忽略的验证步骤:把调度结果回放到设备仿真模型里,检查电解槽在每一个15分钟窗口里的启停次数。很多优化模型在数学上最优,但设备厂商给的数据手册里写着“一天最多启动3次”,你的方案一天启动8次,那就不能用。这一步也很有价值——它让你在写论文或做方案汇报时,能拿出“模型结果+设备可行性”双重证据。

我现在的习惯是每次改完参数,都会保留一组固定的历史场景做回归测试。这个场景集里包含一个典型晴天、一个典型阴天和一个极端大风天,保证每次修改不会让某个场景变差。如果你带着这个思路去复现这套代码,调整权重系数、替换真实数据时,心里会更有底。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表