ARTICLE DETAIL

资讯详情

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

梯级水光互补系统最大化可消纳电量期望短期优化调度模型复现与Python实现

梯级水光互补系统最大化可消纳电量期望短期优化调度模型复现与Python实现 最近手头接了一个EI论文复现的项目题目是“梯级水光互补系统最大化可消纳电量期望短期优化调度模型”要求用Python完整实现。这几年电力系统方向的课题组里这类复现需求越来越多论文归论文从公式变成能跑出调度方案的代码中间隔着大量细节。这个模型的核心逻辑一句话就能说清楚在光伏出力不确定的前提下通过调节梯级水电站各时段的发电计划让水光联合系统在电网消纳能力受限时尽可能多地把电量送进电网。目标函数是“可消纳电量的期望”核心约束是梯级水电的时序耦合、库容边界和光伏出力的随机波动。我复现完跑通后觉得这套东西非常适合三类人参考一是做电力系统优化调度的研究生刚接触随机优化和场景法二是准备复现EI/SCI论文但不知道怎么下手的人三是实际做水风光联合调度方案设计的工程师。全文我会先从模型逻辑讲起再落到数学公式和线性化处理然后给出可运行的Python代码框架最后整理我在复现过程中踩过的坑和排查思路。1. 模型核心思路拆解互补调度为什么能成立1.1 梯级水电与光伏的天然互补性光伏出力的典型特征是“跟着太阳走”白天午间出力达到峰值夜间出力为零而且受云层影响随时可能剧烈波动。这种出力曲线对电网来说非常不友好尤其是当光伏装机占比越来越高午间的功率倒挂和傍晚的快速下跌会给调度带来很大压力。梯级水电则是另一番特性。多个水电站沿着同一条河流上下游串联布置上游电站发完电的水会汇入下游水库继续发电形成时间上的接力。水电机组从零出力到满出力只需要几十秒到几分钟调节速度快、启停灵活是公认的优质调峰电源。这两者组合在一起就形成了一种天然的互补关系光伏出力旺盛的时候水电可以把发电功率压下来把水蓄在水库里等光伏掉下去或者电网需要更多电力的时候水电再集中放水发电。我在检查调度结果时经常看到这样的曲线光伏午间峰值正好对应水电的低谷傍晚光伏归零后水电爬坡顶上去。这种“光伏顶白天水电顶夜间”的节奏就是互补调度价值的直观体现。1.2 “可消纳电量”到底是什么意思很多初学者会把目标理解成“最大化发电量”这其实不够准确。电网侧的消纳能力是有限的或者是因为外送通道容量受限或者是因为负荷需求不够高总之上网电量存在一个上限。光伏发出来的电如果送不出去只能弃掉水电站发出来的电如果超过了消纳能力同样需要弃水或者压低出力。所以在数学上可消纳电量通常写成可达上网电量 min(水电出力 光伏出力, 电网消纳能力上限)这个min非常关键。如果只盯着“发电量最大化”模型可能为了多发电而拼命放水但实际并网的电量并没有增加多出来的部分全部被浪费掉了。以“可消纳电量”为目标模型自然会把水火联合出力压到消纳上限以内并且在低于上限时尽量多发电。这其实是把电网侧、电源侧的物理约束统一折算进了优化目标里思路非常干净。1.3 为什么是“期望值”而不是确定性场景光伏出力是不确定的但日前调度必须在实际光伏出力还没发生之前就把水电站的发电计划制定好。这就面临一个很现实的问题我只知道光伏出力的概率分布或者未来可能出现的若干种出力曲线但不知道明天到底会出哪种情况。“最大化可消纳电量期望”就是对这个问题的一个自然回答让水电计划对各种可能的光伏场景都“比较不差”把这些场景下的可消纳电量按概率加权求和作为目标。期望值模型在随机规划里属于风险中性决策它不追求极端情况下的最优也不刻意规避最差情况而是在平均意义上做到最优。在做短期调度时这个取向是比较符合工程实际的——调度机构每天都要做计划追求的正是长期运行中总消纳电量最多。2. 数学模型构建目标函数、约束与线性化处理2.1 目标函数的转换技巧辅助变量替代min把“期望最大化”落到可计算的数学表达式需要一个关键处理。可消纳电量里有个min运算如果直接把min放进目标函数绝大多数求解器是没法处理的。标准做法是引入辅助变量Z把min展开成一组线性不等式约束。目标函数写成max ∑_{s1}^{S} π_s ∑_{t1}^{T} Z_{s,t}约束加两条Z_{s,t} ≤ ∑_{n1}^{N} P_{h,n,t} P_{pv,s,t} Z_{s,t} ≤ P_{grid,t} Z_{s,t} ≥ 0这里S是光伏场景数量π_s是第s个场景的概率T是调度时段数通常取24N是梯级水电站数量。P_h是水电出力决策变量P_pv是场景s下第t时段的光伏出力参数P_grid是电网可消纳上限。由于目标是在最大化Z所以Z会自动取到min那一项的值不需要显式写min。还有一个容易被忽略的点水电计划P_h只能依赖场景概率信息不能针对某个具体场景单独调整这在随机规划里叫非预期约束。也就是说同一个水电出力序列要“扛住”所有光伏场景。代码里自然就满足这一点因为P_h不带有场景下标。2.2 梯级水电的核心约束群从水量平衡到出力特性梯级水电建模的关键在于把上下游电站的水量联系表达清楚。每个电站在每个时段都要满足水量平衡关系V_{n,t1} V_{n,t} (I_{n,t} Q_{n-1,t} Spill_{n-1,t} - Q_{n,t} - Spill_{n,t}) × ΔtV是库容水量I是区间入流Q是发电流量Spill是弃水流量。Δt是每个时段的时长。这个式子的意思是水库里的水除了区间自然来水之外上游电站放出来的水量发电流量加弃水流量也会汇入本库成为入库水量的一部分。短期调度里如果河道水流传播时间不长可以忽略延迟如果论文里提到到达时间还需要在Q_{n-1,t}的时间下标上减去一定时段数。除了水量平衡还需要一组不等式约束库容上下限V_min ≤ V_{n,t} ≤ V_max水库不能放空也不能超蓄发电流量上下限Q_min ≤ Q_{n,t} ≤ Q_max受机组过流能力限制弃水流量非负Spill_{n,t} ≥ 0末库容约束V_{n,T} ≥ V_{n,end}保证调度周期末水位不低于要求水电出力与流量的关系用简化公式表示P_{h,n,t} 9.81 × η_n × H_n × Q_{n,t} / 1000H是水头η是发电效率9.81是重力加速度除以1000把kW转成MW。如果假设水头恒定这个公式就是一条过原点的线性直线非常好处理。但更接近实际的模型会把H作为库容的函数这样P就是Q和V的乘积项变成非线性。短期调度中常见的处理是分段线性化把出力-流量曲线拆成几段折线用Gurobi的addGenConstrPWL接口直接建模或者手工引入分段线性辅助变量。复现时如果论文没明确给出公式先按线性模型跑通再加非线性细节这是比较稳妥的路径。还要加爬坡约束限制水电机组相邻时段出力的变化幅度-Ramp_n ≤ P_{h,n,t} - P_{h,n,t-1} ≤ Ramp_n这个约束是我在复现中特别注重的没有它求解器会给出非常“激进”的调度策略前一小时停机后一小时满发。现实中机组根本反应不过来。2.3 光伏随机场景的生成与概率赋值期望值模型里最耗时间准备的就是光伏场景。复现时如果论文没有给出场景数据通用的做法是拿历史光伏出力做聚类提取典型场景。我这次用K-means从一年的日光伏曲线里聚出了10个典型场景每个场景的概率直接按该簇天数占总天数的比例赋值。具体思路是这样把365条日光伏曲线每条24个点作为样本聚类成10个簇每个簇的中心曲线就是“典型场景”。这种方法的好处是场景之间差异明显概率也自动归一化。比随机抽样更稳定因为聚类中心天然剔除了个别极端异常日。场景数目的选择是个平衡问题。太少光伏波动特征刻画不足太多模型规模成倍增大求解时间跟着暴涨。我实测下来短期调度用10到20个场景就能把期望目标算得比较稳再往上加场景数对结果精度的影响很小求解时间却可能翻好几倍。2.4 参数与量纲复现前必须理清的细节论文复现最容易翻车的地方不是模型结构而是量纲不统一。我在第一次跑通模型之前花了大半天把单位全部校准了一遍。核心换算关系是1 m³/s的流量持续1小时对应水量是3600 m³。如果库容单位是万m³那就乘以0.36即1 m³/s在1小时内等于0.36万m³。下表是我复现时整理的主要参数分类和单位约定参数类别符号单位说明库容V万m³上下限、初值、末值发电流量Qm³/s机组过流能力弃水流量Spillm³/s非负变量区间入流Im³/s外部来水水电出力P_hMW由流量和水头决定光伏出力P_pvMW场景参数消纳上限P_gridMW电网侧约束时段长度Δth短期调度取1h水电出力公式里还有一个细节9.81 × η × H × Q算出来单位是kW如果要转成MW还要除以1000。有的论文把9.81换成8.81甚至直接给一个综合出力系数k这时候直接用k乘以流量即可但一定要搞清k的物理含义和单位否则目标函数数级会差得很离谱。3. Python实现从公式到可运行的调度代码3.1 环境准备与求解器选型这个模型本质是大规模线性规划变量数量主要取决于场景数、时段数和电站数的乘积。用我这次的算例来说3个电站、24个时段、10个场景辅助变量和约束加起来大约一千多个线性规划求解器秒级就能解决。但如果论文里加了机组启停的0-1变量问题就变成混合整数线性规划对求解器的要求高不少。求解器我选的是Gurobi原因很直接第一学术许可免费高校课题组基本人手一个第二它对线性规划、混合整数规划的处理性能是同类求解器里最强的梯队第三Python接口gurobipy写起来非常顺手支持直接添加分段线性约束。如果你用的是Gurobi 10.x版本直接运行pip install gurobipy安装完成后首次建模还会校验许可证学术版需要在官网申请注册码在本地运行grbgetkey激活。没有Gurobi的话用开源的CBC、HiGHS也能求解线性规划但处理分段线性和大规模MIP时性能会差一些。除了求解器还需要几个常见的Python库numpy处理数组运算pandas读数据表matplotlib画调度曲线scikit-learn做K-means场景聚类。一次性安装pip install numpy pandas matplotlib scikit-learn gurobipy3.2 数据准备光伏场景生成与预处理先准备基础数据包括电站参数数组、区间入流、光伏历史出力曲线。这里我给出场景生成的核心代码import numpy as np import pandas as pd from sklearn.cluster import KMeans # pv_data: shape (天数, 24)取值已经按光伏装机容量归一化到[0,1] kmeans KMeans(n_clusters10, random_state42) kmeans.fit(pv_data) labels kmeans.labels_ scenarios [] probs [] for i in range(10): cluster_data pv_data[labels i] scenarios.append(cluster_data.mean(axis0)) # 典型日曲线 probs.append(len(cluster_data) / len(pv_data)) scenarios np.array(scenarios) # shape (10, 24) probs np.array(probs) # shape (10,)聚类完成后把归一化的场景曲线乘上光伏装机容量就得到实际的光伏出力场景P_pv_actual。我建议把场景曲线也画出来看一眼如果发现某些“畸形”场景比如午后突然掉到零多半是数据清洗的问题别直接喂给模型否则求解出来的调度策略会莫名其妙。3.3 Gurobi建模核心代码逐段解读建模部分我直接贴核心代码再逐段说明这样比只讲理论好理解得多。import gurobipy as gp from gurobipy import GRB T 24 N 3 S len(scenarios) dt 1.0 # 电站参数示例值实际按论文或工程数据修改 V_min np.array([50, 40, 60]) V_max np.array([800, 600, 900]) V_init np.array([400, 300, 450]) V_end np.array([400, 300, 450]) Q_min np.array([0, 0, 0]) Q_max np.array([120, 100, 130]) Q_k np.array([0.8, 0.75, 0.9]) # 出力系数P_h Q_k * Q单位MW/(m³/s) P_max np.array([100, 80, 120]) Ramp np.array([30, 25, 35]) P_pv_actual scenarios * 600 # 假设光伏装机600MW P_grid_limit np.full(T, 300.0) P_grid_limit[8:18] 350.0 model gp.Model(hydro_pv_schedule) # 变量库容、发电流量、弃水、水电出力、消纳电量 V {} Q {} Sp {} P_h {} Z {} for n in range(N): for t in range(T 1): V[n, t] model.addVar(lbV_min[n], ubV_max[n], namefV_{n}_{t}) for t in range(T): Q[n, t] model.addVar(lbQ_min[n], ubQ_max[n], namefQ_{n}_{t}) Sp[n, t] model.addVar(lb0, namefSp_{n}_{t}) P_h[n, t] model.addVar(lb0, ubP_max[n], namefP_{n}_{t}) for s in range(S): for t in range(T): Z[s, t] model.addVar(lb0, namefZ_{s}_{t}) # 目标最大化可消纳电量期望 model.setObjective( gp.quicksum(probs[s] * Z[s, t] for s in range(S) for t in range(T)), GRB.MAXIMIZE ) # 约束1消纳电量辅助约束 for s in range(S): for t in range(T): model.addConstr( Z[s, t] gp.quicksum(P_h[n, t] for n in range(N)) P_pv_actual[s, t], namefconsume_{s}_{t} ) model.addConstr( Z[s, t] P_grid_limit[t], namefgrid_{s}_{t} ) # 约束2水量平衡 for n in range(N): for t in range(T): inflow I[n, t] # I: 区间入流参数, shape(N, T) if n 0: inflow Q[n - 1, t] Sp[n - 1, t] # 上游出流汇入 model.addConstr( V[n, t 1] V[n, t] (inflow - Q[n, t] - Sp[n, t]) * 0.36 * dt, namefwater_{n}_{t} ) # 约束3水库初末库容 for n in range(N): model.addConstr(V[n, 0] V_init[n], namefinit_{n}) model.addConstr(V[n, T] V_end[n], namefend_{n}) # 约束4出力特性线性简化 for n in range(N): for t in range(T): model.addConstr(P_h[n, t] Q_k[n] * Q[n, t], namefpower_{n}_{t}) # 约束5爬坡约束 for n in range(N): for t in range(1, T): model.addConstr(P_h[n, t] - P_h[n, t - 1] Ramp[n], nameframp_up_{n}_{t}) model.addConstr(P_h[n, t - 1] - P_h[n, t] Ramp[n], nameframp_down_{n}_{t}) # 求解 model.optimize()这段代码有几点值得注意。水量平衡里0.36的换算系数是核心很多复现跑崩就是在这一行前后单位没对上。出力特性我用了最简单的一次线性关系P_h Q_k × Q实际论文里可能是包含水头的非线性关系如果需要更精确可以用model.addGenConstrPWL把Q映射到P_h的分段折线约束上。目标函数里的Z变量有一个巧妙的双重约束一方面受水电出力加光伏出力的上限限制另一方面受消纳上限限制由于目标在最大化Z它自然就收敛到两者中的较小值。这个技巧在很多优化建模里都用得到比直接写min表达式灵活得多。3.4 求解与结果提取调用model.optimize()之后先检查求解状态if model.Status GRB.OPTIMAL: print(目标值期望可消纳电量:, model.ObjVal) elif model.Status GRB.INFEASIBLE: print(模型不可行)结果提取我习惯把它们装进numpy数组方便后续分析和画图P_h_total np.array([sum(P_h[n, t].X for n in range(N)) for t in range(T)]) P_pv_avg np.mean(P_pv_actual, axis0) Z_avg np.array([sum(probs[s] * Z[s, t].X for s in range(S)) for t in range(T)]) V_level np.array([[V[n, t].X for t in range(T 1)] for n in range(N)])画图用matplotlib我一般画三张图第一张是水电总出力、光伏平均出力、消纳上限和可消纳电量的时序曲线第二张是各水电站的库容变化第三张是弃水流量。这三张图基本能把一次调度方案的全部信息展示出来。import matplotlib.pyplot as plt hours np.arange(1, T 1) plt.figure(figsize(10, 5)) plt.plot(hours, P_h_total, labelHydro total, markero) plt.plot(hours, P_pv_avg, labelPV average, markers) plt.plot(hours, P_grid_limit, labelGrid limit, linestyle--) plt.plot(hours, Z_avg, labelConsumed energy, linestyle-., marker^) plt.xlabel(Hour) plt.ylabel(MW) plt.legend() plt.grid(True) plt.show()看到图之后先做一个人工合理性检查光伏出力峰值时段水电出力是否压低了傍晚光伏下跌后水电是否及时顶上任何时刻总出力有没有超过消纳上限。这些如果都符合预期基本可以判定模型逻辑没有问题。4. 复现过程中的常见问题与排查实录4.1 模型不可行先约束再数据逐层剥离运行报“Model is infeasible”是我第一次跑通前遇到最多的错误。Gurobi提供一个非常好用的诊断工具model.computeIIS()它能在不可行模型里找到一组最小不可行约束子集再用model.write(model.ilp)把冲突的约束导出看。但根据我的经验绝大多数不可行原因都可以人工排查掉比依赖工具更快。先检查水量平衡的单位V的初始值、上下限与水量平衡式里的0.36系数是否匹配。比如V_init设定为400万m³V_max只有800但入流按m³/s输入后忘记乘以0.36假设某时段入流50 m³/s换算后每时段才增加18万m³问题不大反过来如果没除对一个时段增加1800万m³直接超出库容上限不可行就是必然的。再检查末库容约束。V_end设得太高而调度周期内来水不足是不可能在期末把水位蓄到目标值的。我建议先把末库容约束放宽成V[n,T] V_min[n]跑通后再逐步收紧看看系统在什么边界条件下开始不可行这样能快速定位瓶颈。光伏场景和消纳上限的搭配也容易出问题。如果某个场景光伏出力极大消纳上限又很低水电可能被迫压到零出力但某些电站有最小出力的要求这时候约束就冲突了。处理方式要么降低最小出力要么放宽消纳上限做测试。4.2 求解缓慢场景数与线性化之间的平衡我最初把场景数设成50个加上分段线性约束模型求解时间一度超过十分钟MIP gap还在5%左右徘徊。后来场景压缩到10个线性化断点只取了5个关键点求解时间降到几秒目标函数值只变化了不到3%。对于纯线性规划没有0-1变量Gurobi求解一千多个变量和约束几乎是瞬时的。真正拖慢求解的是两类东西一是整数变量二是不够光滑的约束表达。水电调度如果不考虑机组组合通常不需要整数变量但一旦加了“弃水只能在整个水库放空后才发生”之类的逻辑就必须引入0-1变量模型复杂度立刻上一个台阶。我给的实操建议是第一阶段先用无整数的LP模型把调度策略算出来确认结果合理之后再加精细化约束。同时给求解器设一个合理的容差model.Params.MIPGap 0.02 model.Params.TimeLimit 300MIPGap设为2%意味着求解器在证明优化结果与最优值的差距小于2%时停止这是工程上非常实用的折中。4.3 调度结果异常水位震荡与出力跳变跑出可行解之后不等于万事大吉我见过好几次目标函数值很漂亮但调度曲线完全不能用的结果。最典型的问题就是水库水位剧烈震荡相邻时段库容忽高忽低或者水电出力从0直接跳到满发完全不考虑实际机组的调节能力。这种问题十有八九是缺少爬坡约束或末水位约束导致的。求解器会利用目标函数里每一度电都“值钱”的特点在约束允许的范围内把水挤到最有利的时段发电哪怕这个操作在物理上不现实。我自己的排查顺序是先检查有没有爬坡约束再看看末库容是否被正确地限制住了最后看水量平衡式里有没有把发电流量和弃水流量算重复。另一个容易被忽视的问题是弃水流量和发电流量同时为正。在梯级电站里如果上游来水太大电站可能一边发电一边弃水这在物理上是可能的。但很多论文的简化模型不允许同时发生因为机组可以全开的情况下没必要既发电又弃水除非发电流量已经到上限。要处理这个问题可以给弃水流量加一个逻辑约束或者干脆在目标函数里加一项很小的弃水惩罚系数迫使求解器优先选择发电而不是弃水。我复现时给弃水加了一个0.01的惩罚权重效果立竿见影。4.4 Gurobi安装与许可证的坑gurobipy虽然能直接pip安装但要真正调用求解器必须有一个有效的许可证。高校师生可以申请学术版免费且功能完整。有几个地方容易卡壳许可证过期之后代码不会报错提示而是直接输出“License expired”然后终止需要重新下载许可证文件在服务器上安装时许可证文件默认位置在/opt/gurobi/如果权限不够可以放到用户目录并用环境变量GRB_LICENSE_FILE指定路径pip install gurobipy的版本必须与grbgetkey拿到的许可证版本匹配别装了11.x的gurobipy却用10.x的license如果只是跑线性规划也可以用pip install highspy换用HiGHS求解器语法类似但不支持分段线性约束接口需要手动做线性化。5. 复现经验总结与后续扩展方向5.1 复现EI论文的几个通用步骤这次复现让我对“读论文→写代码→对结果”这件事有了更深的体会。我的流程基本固定成四步。第一步从论文里提取“优化三要素”决策变量是什么、目标函数是什么、约束条件有哪些。我会做一个Excel表把符号、含义、数学表达式、单位全部列出来这一步看起来费时间实际上能省掉后面大量返工。第二步先写小规模测试再写完整模型。我习惯用2个水电站、6个时段、3个场景做代码框架验证数据全部拍脑袋设定重点确认模型能求出最优解、约束都正确、结果符合物理直觉。小规模测试通过后再把数据替换成论文算例问题定位会快很多。第三步参数缺失时做合理假设并在报告中明确标注。论文受篇幅限制很多参数不会完整给出复现时只能合理假设。我的原则是影响约束松紧程度的参数库容上下限、爬坡速率直接决定结果合理性需要谨慎输出系数、效率系数误差对结果方向影响较小的用经验值即可。第四步对比论文结果时不要只看目标值。目标值的对比容易被参数误差干扰调度曲线的形状、各场站出力规律、库容变化趋势才是更可靠的验证依据。如果曲线的形状和论文一致基本可以认定模型复现成功了。5.2 模型可以继续延伸的方向完成基础版本之后这个模型有很大的扩展空间。最直接的是把电网消纳上限从固定曲线改成可优化的负荷曲线引入需求响应让负荷侧也参与调节或者把光伏出力的随机性从场景法升级为分布鲁棒优化用模糊集刻画真实分布与历史分布的偏差提高决策对最坏情况的免疫力。如果研究梯级水电还可以把长期调度和短期调度嵌套起来长期模型给出各水库的蓄水策略短期模型在蓄水策略的约束下做精细化的水光互补优化。这种耦合模型更贴近实际运行方式但求解复杂度会大幅上升。市场环境下目标函数也可以从“最大化消纳电量”改成“最大化售电收益”这就需要引入分时电价并根据光伏、水电出力特性优化各时段的上网申报策略。我在复现这个模型的过程中踩过不少坑最深刻的体会是优化模型的瓶颈往往不在数学公式本身而在于数据口径的统一和约束细节的取舍。单位换算错一位结果就会完全跑偏缺一个爬坡约束调度方案就变成空中楼阁。另外提醒一个实操细节每次跑完模型记得把目标值、场景数、关键约束是否启用的记录保存下来论文修改模型设置后负面对比时这些记录能让你快速定位是哪一处改动影响了结果。希望这篇文章能给正在做类似复现的同学一些参考少走几步弯路。
返回列表