ARTICLE DETAIL

资讯详情

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

梯级水光互补系统可消纳电量期望最大化建模与Python实现

梯级水光互补系统可消纳电量期望最大化建模与Python实现 如果你正在复现一篇关于梯级水光互补系统的EI论文看到“最大化可消纳电量期望”这个目标函数时很可能跟我第一次遇到它的感觉一样公式能读懂但不知道为什么要这样建模更不知道怎样落到Python代码里。这篇博文就把这个模型从物理背景、数学化建模、代码实现到排障经验完整讲一遍内容围绕梯级水光互补系统短期优化调度模型展开重点讲清楚“可消纳电量期望最大化”到底在优化什么、梯级水电的时序耦合怎么建模以及用Python实现时的关键细节。适合正在做电力系统优化调度研究的研究生、马上要复现论文算法的工程师以及所有想把EI论文从公式变成可运行代码的人。1. 这个模型到底在解决什么问题1.1 梯级水光互补系统为什么值得做如果你在西南地区做过水电调度一定会对“弃水”不陌生。丰水期上游来水一大水库装不下下游又用不完只能眼睁睁把水放掉而光伏电站集中的地区白天出力猛冲晚上归零如果只靠火电平抑调节成本很高。把同一流域的多个水电站按从上游到下游的顺序串联起来再与周边光伏电站绑定成一个系统就是所谓的梯级水光互补系统。水电有水库这个天然储能池可以通过调节发电流量来平抑光伏的波动光伏则在白天替水电省下水量让水电可以把水留到晚上或高峰时段再发。但事情没那么简单。梯级电站之间不是孤立的上游发电流量经过一段时间后会变成下游的入流这叫水流滞时各水库有库容上限、最小下泄流量、出力上限光伏出力还有随机性。如果只按确定性预测来排计划实际运行时往往会出现很大的偏差预测晴天结果来了云光伏出力掉一半水电没来得及顶上去系统外送功率就跌破协议值反过来预测阴天结果大晴天光伏超发水电又压不下来只能弃光。所以短期优化调度的核心就是在这些约束下找一个让系统既能满足运行要求、又能尽量接住光伏电量的水电发电计划。这正是“最大化可消纳电量期望”出现的背景。1.2 “最大化可消纳电量期望”到底在优化什么先拆概念。“可消纳电量”指的是电网实际接纳的光伏电量。光伏预测出力不等于可消纳量因为受通道容量、水电最小出力、负荷需求等限制可能有一部分光伏出力发不出来这部分就是弃光电量。所以可消纳电量 光伏出力 - 弃光电量。再想“期望”。光伏短期出力是随机变量哪怕用数值天气预报也只能给出概率分布或一组典型场景。调度计划是提前定的但光伏实际出力到运行当天才知道。所以没法保证某个确定的“可消纳电量”只能保证它的期望尽量大。换句话说目标函数写的是在多个可能的光伏出力场景下把每个场景的可消纳电量按概率加权求和然后让这个加权和最大。这么设计的好处很直观它直接对着弃光问题开刀。传统调度目标可能是“最小化发电成本”或“最大化总发电量”但这些目标未必会让水电主动给光伏让路。用可消纳电量最大化做目标水电的本质角色变成了“给光伏腾空间”只要系统约束允许水电就压低出力把外送通道容量让给光伏光伏发不了的时候再让水电顶上。这个互补逻辑正是模型的核心价值。2. 模型数学化从物理系统到优化问题2.1 梯级水电的时序耦合约束怎么建梯级水电建模最核心的是水量平衡方程。对第 i 个水库、第 t 个时段V(i,t) V(i,t-1) ( Inflow(i,t) Q_up(i-1, t-tau(i)) - Q(i,t) - S(i,t) ) * Delta_t其中 V 是时段末库容Q 是发电流量S 是弃水流量Inflow 是区间的天然入流Q_up 是上一级电站在该时段之前的发电流量tau(i) 是水流从上一级到本级的滞时。这个方程把整个梯级串成了一条时间上的流水线上游放水不是立即到下游而是要经过一段延迟所以调度计划里必须把这种延迟关系写准。除了水量平衡还有一组运行约束库容上下限保证防洪和供水安全发电流量上下限对应水轮机的运行范围出力上下限。出力与流量的关系在水头变化不大时可以简化成线性函数P_hydro(i,t) eta(i) * Q(i,t)。如果水头变化明显就要引入“水库水位-库容曲线”、“尾水位-下泄流量曲线”出力和流量、水头都成非线性关系这时候模型就从线性规划变成了非线性规划求解难度会大很多。我复现时建议的第一步是先把线性版本跑通。很多EI论文为了让模型可解都会给出线性化的处理方式比如把出力-流量曲线分段线性化。先复现线性版再对比论文里是否有非线性项会友好很多。2.2 光伏出力不确定性的场景化表达光伏出力的随机性要在目标函数里体现最常规的做法是场景法。把光伏出力的可能取值抽成 S 个典型场景每个场景有一个概率权重 w(s)。场景可以从历史出力数据里直接取也可以用蒙特卡洛抽样以预测出力为均值加上一定方差的扰动再截断到 [0, 光伏装机] 区间。想更精细一点可以用Beta分布或用历史预测误差的经验分布把相关性也考虑进去。但场景数不能贪多。假设每增加一个场景系统功率平衡约束就多一组变量规模线性增长求解时间很快会失控。一般复现时先取 5 到 20 个场景就够看趋势了。如果场景太多可以用K-means聚类或同步回代消除法做场景削减把 1000 个原始场景砍到 10 个同时保留概率特征。场景削减这个步骤在一些论文里不显眼但实际操作非常影响结果。削减得太狠期望值会偏乐观保留太多求解器卡死。我个人的经验是先在 5 个场景上调通模型再逐步增加观察目标值变化是否收敛。2.3 目标函数与约束条件的完整形式把上面的东西攒起来一个线性化的随机期望模型可以写成这样。决策变量包括各水电站逐时段发电流量 Q(i,t)、弃水 S(i,t)、库容 V(i,t)以及各场景下的光伏消纳量 P_pv(s,t) 和系统外送功率 P_line(s,t)。目标函数max sum_{s} w(s) * sum_{t} P_pv(s,t)约束条件包括水量平衡方程上一节的等式约束库容上下限、流量上下限、弃水非负水电出力定义式 P_hydro(i,t) eta(i) * Q(i,t)系统功率平衡 P_line(s,t) sum_i P_hydro(i,t) P_pv(s,t)外送通道约束 P_min P_line(s,t) P_max光伏消纳量的范围 0 P_pv(s,t) P_pv_forecast(s,t)。这个形式的模型是线性规划理论上用单纯形或内点法就能求解。关键点在于功率平衡约束把水电和光伏“绑定”在了同一条外送通道上所以水电压低出力的行为会被目标函数直接驱动。如果外送通道没有上限那光伏消纳量一定等于预测值目标就退化成确定性最大化模型也就没有调度意义了。这也是很多新手复现时觉得模型“缺约束”的原因——必须先给系统套上一个出力上限。3. Python代码实现从数据准备到求解出图3.1 环境准备与数据预处理Python侧建议用 numpy 处理数组pandas 管数据matplotlib 画图求解器推荐用 pulp它语句化建模风格清晰默认自带 CBC 求解器对线性规划足够。如果之后要上大规模场景或非线性约束再换 Gurobi 或 OR-Tools。准备数据时先统一单位。流量常用 m3/s库容常用万 m3时间步长如果是 1 小时水量平衡的增益系数就是 3600 秒换算成小时再除以 10000 才变成万 m3。这个单位换算出错结果会偏离好几个数量级而且是查不出来的“隐形错误”。另外梯级电站的数据最好整理成 DataFrame每一行是一个电站、每个时段一行包含天然入流、初始库容、库容上下限、出力系数、水流滞时。光伏用每个场景的逐时出力序列。先别急着上真实数据手工造一个 24 时段的小例子把所有数值都放到能够手算验证的量级跑通再换真实数据。3.2 核心代码模型搭建的关键框架下面给出一段简化的核心骨架演示三个梯级电站、5 个光伏场景、24 时段怎么搭。代码里把外送通道约束放在最显眼的位置因为它决定了目标函数是否有意义。import numpy as np import pulp T 24 N 3 # 梯级电站数量 S 5 # 光伏场景数 dt 1.0 # 时段长度单位小时 # 参数设置单位统一后 eta np.array([8.5, 8.5, 8.5]) # 出力系数 MW/(m3/s) Qmin np.array([10.0, 10.0, 10.0]) # 最小发电流量 Qmax np.array([80.0, 80.0, 80.0]) # 最大发电流量 Vmin np.array([300.0, 200.0, 150.0]) # 最小库容 Vmax np.array([1200.0, 900.0, 600.0]) V0 np.array([800.0, 600.0, 400.0]) # 初始库容 Inflow np.random.rand(N, T) * 30.0 tau np.array([0, 1, 1]) # 水流滞时 Pmin_line 50.0 Pmax_line 300.0 pv_scene np.random.rand(S, T) * 200.0 # 每个场景的预测出力 prob pulp.LpProblem(Cascade_Hydro_PV, pulp.LpMaximize) v pulp.LpVariable.dicts(V, ((i, t) for i in range(N) for t in range(T))) q pulp.LpVariable.dicts(Q, ((i, t) for i in range(N) for t in range(T)), lowBound0) s pulp.LpVariable.dicts(S, ((i, t) for i in range(N) for t in range(T)), lowBound0) pv_use pulp.LpVariable.dicts(PV, ((sc, t) for sc in range(S) for t in range(T)), lowBound0) line pulp.LpVariable.dicts(Line, ((sc, t) for sc in range(S) for t in range(T)), lowBound0) # 目标最大化可消纳电量期望 prob pulp.lpSum(pv_use[sc, t] for sc in range(S) for t in range(T)) / S # 水量平衡与库容上下限 for i in range(N): for t in range(T): up_q 0 if i 0 and t - tau[i] 0: up_q q[i - 1, t - tau[i]] if t 0: prob v[i, 0] V0[i] (Inflow[i, 0] up_q - q[i, 0] - s[i, 0]) * 3600 / 10000 else: prob v[i, t] v[i, t - 1] (Inflow[i, t] up_q - q[i, t] - s[i, t]) * 3600 / 10000 prob v[i, t] Vmin[i] prob v[i, t] Vmax[i] prob q[i, t] Qmin[i] prob q[i, t] Qmax[i] # 功率平衡与外送通道约束每个场景都满足 for sc in range(S): for t in range(T): prob line[sc, t] pulp.lpSum(eta[i] * q[i, t] for i in range(N)) pv_use[sc, t] prob line[sc, t] Pmin_line prob line[sc, t] Pmax_line prob pv_use[sc, t] pv_scene[sc, t] prob.solve() print(目标值:, pulp.value(prob.objective))这段代码里 q 和 v 不随场景索引pv_use 和 line 随场景索引相当于两阶段结构水电计划在光伏场景实现前定下来光伏消纳和外送功率等到每个场景里再调整。目标函数里只有 pv_use 的期望所以模型会为了多消纳光伏而腾出通道空间。如果原论文用的是单阶段场景法把所有变量都加上场景索引也可以只是求解结果会更乐观这一点在复现时要特别注意。注意水量平衡公式里的 3600/10000 是单位换算。如果后续把时段长度改成 15 分钟这里要同步改成 900/10000否则库容会明显算错。3.3 求解器选择与结果输出pulp 默认用 CBC 求解器适合几十个变量的小模型。上面的简化模型大概有 3*(242424)2524 456 个变量CBC 秒解。如果场景涨到 100 个、电站涨到 10 个变量会到上万量级这时建议换 Gurobi 或 CPLEX求解速度能差出一个数量级。求解完成后可以用模版把结果写进 DataFrame然后核验几件事每个时段的总出力是否在外送通道上下限内逐场景的弃光量是否都大于等于零以及库容过程是否在上下限内。我习惯把每个场景的目标值和约束违约情况打印出来而不是只看总目标否则某个场景约束被悄悄放松了都不知道。3.4 可视化调度过程曲线与消纳统计调度结果如果只停留在数值表格很难一眼看出问题。我一般画三张图第一张是某个典型场景下光伏预测、光伏实际消纳、弃光量三条曲线叠在一张图第二张是各水电站出力和系统总外送功率的堆叠曲线第三张是库容变化曲线重点看是否撞到上下限。matplotlib 画图的细节不多但有两点值得提醒第一x 轴时段标签最好写成“00:00-01:00”这种可读形式方便和真实调度日志对照第二弃光曲线建议用正数表示不要让可消纳量和预测值两条线交叉否则图一乱汇报时很难解释。输出图片建议加上 300dpi 参数论文插图可以直接用。4. 复现过程中的常见问题与排障经验4.1 光伏场景生成不合理结果异常场景生成是最容易埋雷的地方。我见过最典型的错误是场景抽样没有截断出现负出力或者超过装机容量导致“可消纳电量”算出来比预测还高目标值虚胖。正确的做法是抽样后做 clip把场景限制在 [0, 装机] 区间。另外场景之间应当保留一定的时序相关性如果每个时段独立抽样生成的光伏曲线会像白噪声一样抖动和实际光伏的缓慢爬坡特征完全不符最终调度计划也会失真。场景削减不要用得太猛。比如用 K-means 削减到 3 个场景虽然求解快但极端高出力场景可能被消掉期望值会整体偏低甚至出现“模型建议水电满发”这种反直觉计划。建议保留一个高光伏场景和一个低光伏场景这样模型才会真正学会在两种天气之间做取舍。4.2 模型无解或求解极慢怎么办模型无解十有八九是单位或约束打架造成的。检查顺序是这样的先看库容方程初始库容加上入流是否在 Vmin、Vmax 范围内如果初始库容就超上限后面所有约束都会炸再看流量上下限和库容上限是否兼容有时候 Qmin 太大导致水库在几个时段内被迫放空低于 Vmin最后看通道容量如果 Pmax_line 小于最小水电出力加最低光伏消纳需求那功率平衡约束无解。求解极慢的案例一般是场景数太多或者部分约束非线性。先把场景数降到 5 试一次如果求解时间还不可接受就检查是不是误用了整数变量或者把乘法写进了约束。线性规划通常秒解遇到非线性项要主动做分段线性化。现象大概率原因优先检查项模型无解初始库容不在限内或约束互相矛盾先检查第一个时段的水量平衡求解时间异常长场景数过多或存在非线性项看变量类型和约束里是否有变量相乘目标值为零pv_use 被约束为0检查外送通道下限是否超过水电最小出力结果波动大场景削减太狠概率权重失真增加或调整场景数4.3 结果不满足论文预期的排查思路复现最折磨人的是“代码没报错但结果和论文对不上”。我的排查经验是按层拆先用单场景、确定性光伏预测跑一遍看水电计划是否符合物理直觉再把目标函数换成“最大化总电量”做对照实验确认梯级水电部分正确最后才引入多场景期望目标。这样出问题能定位到是场景处理、目标函数还是约束条件的问题。另一个常见原因是初始条件没对齐原论文。EI论文经常在算例设置里写“初始库容取正常蓄水位”或“取死水位”这两个设定结果差异极大。如果复现结果和论文差得远先核对初始库容和边界条件这一条比调任何参数都值得优先查。4.4 不可不知的细节数据单位、时段粒度、边界条件数据单位问题我反复踩过。流量单位不是 m3/s 就是 m3/h库容单位有 m3、万 m3、亿 m3 三种出力系数更是随工程不同差别巨大。建议所有数据进入模型前先统一成一套单位并写一个函数做校验库容变化量对应的时间段流量乘上时段时长再除以单位换算系数应该和库容步长在同一数量级能对上才继续。时段粒度同样影响模型解释。24 点短期调度是最常见的但有些论文用 96 点15 分钟一个时段。水流滞时如果是 1 小时在 24 点模型里就是 1 个时段在 96 点里就是 4 个时段代码里 tau 必须按时段粒度换算。边界条件里首末时段的库容是否强制相等周期调度也要在模型里明确声明很多复现结果跑偏就是因为少了“末库容回到初始值”这条约束。5. 模型扩展方向5.1 从期望值到两阶段随机优化上面简化模型里q 和 v 不随场景索引其实已经是两阶段结构。更严格的做法是继续拉大两个阶段的差距第一阶段决定各水电站的发电流量、库容轨迹第二阶段针对每个光伏场景做光伏消纳和外送功率的调整。这个结构在 pulp 模型里实现方式并不复杂关键是 q、v 不随场景索引而 pv_use、line 随场景索引这样目标函数仍然是期望值但决策顺序更贴近实际。5.2 加入电价与市场化因素如果你的研究方向更接市场可以在目标里把可消纳电量折算成收益比如光伏上网电价高于水电那目标函数就从“最大消纳电量”自然过渡到“最大期望收益”这时水电给光伏让路的动机就变成了经济效益。再加上分时电价、现货市场出清等约束模型会复杂不少但整体框架是同一套。5.3 考虑长期调度与短期调度的衔接短期模型通常只关心一天内但水库初始库容从哪来由月尺度或年尺度的中长期调度决定。把长期调度给出的末库容作为短期模型的边界条件再把短期模型的弃水统计反馈给长期模型做修正会形成一个双时间尺度闭环这也是当前流域风光水互补优化里比较热门的方向。我个人的体会是这个模型真正跑通之后最大的惊喜在于把光伏消纳问题转化成水电的腾挪问题物理直觉一下子就有了。不过我也要诚实地说我最初复现时在水流滞时上卡了整整两天最后发现是单位小时和秒的换算乘错了系数。所以我的建议是先手工算一个小规模案例把每一行约束的数值都手验过再上真实数据。这套框架跑通后改场景生成方式、换目标函数都很快后面的路会越走越顺。
返回列表