ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

风光氢储协同调度实战:多时间尺度优化与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), domainpyo.NonNegativeReals) model.Pel pyo.Var(T, bounds(0, 60), domainpyo.NonNegativeReals) model.Pch pyo.Var(T, bounds(0, 30), domainpyo.NonNegativeReals) model.Pdis pyo.Var(T, bounds(0, 30), domainpyo.NonNegativeReals) # 二进制变量电解槽启停状态1 表示运行 model.Z pyo.Var(T, domainpyo.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, rulepower_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, rulesoc_rule) # 电解槽最小运行功率约束运行时功率不小于 15 kW def min_load_rule(model, t): return model.Pel[t] 15 * model.Z[t] model.min_load pyo.Constraint(T, rulemin_load_rule) # 目标函数购电费用 启停惩罚 功率变化惩罚 model.cost pyo.Objective( exprsum((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[t1] - model.Pel[t])**2 for t in range(23)), sensepyo.minimize ) # 用 CBC 求解器求解 solver pyo.SolverFactory(cbc) results solver.solve(model, teeFalse) 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_soc0.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, rulebalance_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, rulesoc_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(rulempc_objective, sensepyo.minimize) solver pyo.SolverFactory(cbc) solver.solve(model, teeFalse) 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), domainpyo.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, ruledr_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次那就不能用。这一步也很有价值——它让你在写论文或做方案汇报时能拿出“模型结果设备可行性”双重证据。我现在的习惯是每次改完参数都会保留一组固定的历史场景做回归测试。这个场景集里包含一个典型晴天、一个典型阴天和一个极端大风天保证每次修改不会让某个场景变差。如果你带着这个思路去复现这套代码调整权重系数、替换真实数据时心里会更有底。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进