ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

梯级水光互补系统期望最大化调度模型复现与Python实现

梯级水光互补系统期望最大化调度模型复现与Python实现 梯级水光互补系统这个方向最折磨人的往往不是公式本身而是把一个写在纸面上的“期望最大化”目标变成一个真正能跑、能调、能解释的调度模型。这篇复现我前后折腾了两周踩了不少坑把核心思路、建模取舍、Python实现细节和验证方法完整梳理一遍给同样在啃EI论文复现的朋友一份可以直接抄作业的路线图。先说清楚这个模型在解决什么问题。一条流域上有两座梯级水电站上游水库调节能力较强下游电站偏径流式附近再配一座大型光伏电站水电光伏打捆后通过一条容量有限的输电通道送出。晴天且光伏大发时水电如果满发通道容量不够只能被迫弃水或弃光阴天或晚上光伏出力为零又得靠水电顶上。调度员要在日前决定未来24小时甚至96个时段里每座水电站发多少电、蓄多少水、弃多少水光伏能消纳多少。难点在于明天光伏到底发多少是不确定的任何基于单一预测值的确定性计划都是在“押宝”。这篇EI论文的核心思路就是把光伏出力的不确定性用一组概率场景表示然后让目标函数变成“期望可消纳电量最大化”本质上是在所有可能的光照情形下找到一套平均表现最优的调度方案。这篇文章适合正在复现同类优化调度论文的研究生、做水光互补项目的工程师以及想让自己的调度模型从“确定性”进阶到“不确定性优化”的人。我会按建模主线的顺序来讲先拆解目标函数为什么这么构造再展开梯级水电约束里最容易出错的滞时和线性化问题接着讲光伏场景集怎么进模型最后给出完整的Python实现框架和验证手段。1. 为什么“最大化可消纳电量期望”值得单独复现一遍1.1 短期调度问题的现实场景先把问题边界固定下来。这里说的“短期”指日前计划到日内滚动优化这个时间尺度一般以15分钟或1小时为一个时段优化周期24小时或96个时段。控制对象是梯级水电站群和一座光伏电站它们共享同一条外送通道或同一个负荷区域。通道容量是硬约束。通道能送多少电决定了水电和光伏加在一起最多能消纳多少。当来水充裕、光伏又大发时系统的发电潜力大于通道能力必然有一部分清洁电量要废弃。调度的艺术就是在不同时段之间分配有限的“通道资源”是把水存起来等光伏落下去再发还是现在就发满把光伏压掉一部分。这本质上是一个跨时段的资源分配问题而光伏预测又给这个问题加了一层不确定性。1.2 为什么不是“消纳量最大”而是“期望最大”如果光伏出力是已知的问题就是一个标准的确定性线性规划目标直接写成总消纳电量最大即可。但光伏预测不可能完全准确尤其提前24小时看明天的云量误差可能达到20%以上。此时若只用一个预测均值做优化晴天实际出力高于预测时通道可能被低估阴天实际出力低于预测时水电计划又可能过于激进导致弃水。期望最大化则是给每种可能的光照情形一个概率权重选一个在概率意义上最优的计划。打个比方你出门前看天气预报说有60%概率下雨、40%概率晴天带伞的“期望收益”取决于两种天气下淋雨和拿伞的不便程度加权。光伏调度就是这个逻辑只是把“淋雨”换成了“弃水弃光”把“概率”换成了场景权重。1.3 这篇模型和普通水电调度模型的差别在哪常规水电短期优化调度目标通常是发电量最大或蓄能最大不确定性主要来自入库径流。这篇模型把光伏不确定性作为核心水电作为调节资源来“补位”两者通过共享的通道约束耦合在一起。所以复现时必须同时处理好三件事梯级水电的水力联系、光伏场景的随机性表达、以及水电与光伏之间的竞争关系。缺了任何一块模型就失去了“互补”的意义。2. 建模第一步期望目标与决策变量怎么挂钩2.1 目标函数的数学形态假设一共有 T 个时段I 座梯级水电站S 个光伏场景场景 s 的概率为 p_s。模型的目标函数可以写为maximize J Σ_{s1}^{S} p_s · Σ_{t1}^{T} [ Σ_{i1}^{I} P_h(i,t) P_pv_abs(s,t) ]其中 P_h(i,t) 是第 i 座水电站在 t 时段的出力P_pv_abs(s,t) 是场景 s 下 t 时段光伏被实际消纳的功率。这里有个关键细节值得注意水电出力不带场景下标光伏消纳带场景下标。这个设计不是随意的。水电是日前计划决策在光伏实际出力还没揭晓之前就要确定下来而光伏消纳属于“事后调整”可以在每个场景下根据实际出力决定是否弃光。所以水电决策对所有场景是公共的光伏消纳则随场景不同而变化。这种“公共决策场景相关调整”的结构正好对应实际调度中“日前计划日内修正”的两阶段逻辑。2.2 期望与弃电惩罚的等价转换最大化可消纳电量等价于最小化期望弃电量但这个等价关系只有在总发电潜力是外生给定的前提下才成立。光伏场景出力是外生参数水电来水量也是外生参数那么系统总可用电量固定消纳得越多就意味着弃得越少。工程上我更喜欢在目标函数里把弃电惩罚项显式写出来maximize J Σ_s p_s Σ_t [ Σ_i P_h(i,t) P_pv_abs(s,t) — C_w · Qspill_coeff(i,s,t) — C_v · P_pv_spill(s,t) ]这里 Qspill 是弃水流量P_pv_spill 是弃光功率C_w、C_v 是惩罚系数。加惩罚项的好处是可以调节“保水”和“保光”的优先级比如来水丰沛时弃光惩罚加重枯水期弃水惩罚加重。复现论文时先看原文目标函数是纯消纳量最大还是带惩罚再决定用哪种形式。2.3 决策变量与参数表建模前先把所有量纲和符号固定下来可以避免后面写代码时混乱。建议用下表做一张建模速查表符号含义单位类型P_h(i,t)水电站 i 在时段 t 的出力MW决策变量Q_out(i,t)水电站 i 的发电流量m³/s决策变量Q_spill(i,t)水电站 i 的弃水流量m³/s决策变量V(i,t)水电站 i 在时段 t 末的库容m³决策变量P_pv_abs(s,t)场景 s 下时段 t 的光伏消纳功率MW决策变量P_pv_fore(s,t)场景 s 下时段 t 的光伏预测出力MW参数P_avail(t)时段 t 的通道可用容量MW参数I_in(i,t)水电站 i 的区间入流m³/s参数τ(i)从上游电站到下游电站的水流滞时时段参数决策变量里最容易被忽略的是 Q_spill。很多人写水量平衡时只写“来水发电流量库容变化”把弃水漏了导致系统没有弃水通道通道约束一紧就无解。这个问题我第5章还会详细讲。3. 梯级水电四组约束库容、流量、出力与水流滞时3.1 水量平衡方程梯级耦合的核心每一座水电站的库容变化由入流、出流和弃水决定。单站水量平衡是V(i,t) V(i,t-1) [ I_in(i,t) Q_in_up(i,t) — Q_out(i,t) — Q_spill(i,t) ] · Δt梯级耦合体现在 Q_in_up 上。对于梯级电站上游电站的出库流量经过 τ 个时段的滞时之后才会到达下游电站。数学上写成Q_in_up(i,t) Q_out(i-1, t — τ) Q_spill(i-1, t — τ)这个滞时项是最容易写反的地方。我之前第一次建模就把下标写成了 tτ结果上游电站还没放水下游库容就先涨了调度计划完全失真。滞时的方向一定是“过去的上游出库决定现在的下游入库”。Δt 的单位也要统一。如果流量单位是 m³/s库容单位是 m³一个时段的流量转换成水量要乘以 3600 秒小时级时段或 900 秒15分钟级时段。转换系数漏掉是最低级的错误却非常常见。3.2 库容、发电流量和出力限制每组物理边界都是约束缺一个模型就可能算出“超自然”的调度结果库容上下限V_min(i) ≤ V(i,t) ≤ V_max(i)对应水库死水位和正常蓄水位。发电流量上下限Q_out_min(i) ≤ Q_out(i,t) ≤ Q_out_max(i)取决于水轮机过流能力。弃水流量非负Q_spill(i,t) ≥ 0。水电出力上下限P_h_min(i) ≤ P_h(i,t) ≤ P_h_max(i)对应最小技术出力和装机容量。复制代码时这些都是直线约束直接用线性库可以搞定。真正需要动脑的是出力与流量、水头之间的非线性关系见 3.3。3.3 出力函数与水头处理先线性后精确水电站出力公式是 P η · ρ · g · H · Q其中 H 是发电水头由上下游水位差决定而上下游水位又都跟库容有关。于是 P 与 Q、V 之间存在非线性关系直接丢给线性求解器会报错。复现时有两条路。第一条路是固定水头近似假设调度期内水库水位变化不大水头 H 近似取额定值那么 P 与 Q 就是线性关系 P k · Q。对短期调度来说如果上游水库容量足够大、日水位变幅较小这个近似的精度在可接受范围内。建议先跑通固定水头版本再去升级精确模型。第二条路是分段线性化。把水头 H 关于库容 V 的函数以及出力 P 关于 H 和 Q 的乘积项做 McCormick 松弛或 SOS2 分段线性逼近转换成混合整数线性规划。这条路精度高但模型规模大求解时间成倍增加。我的建议是复现初期先做固定水头等模型和求解器都稳定了再对照原文看看它是否用了分段线性化按需加回来。3.4 梯级约束汇总把上面四组关系汇总成一张表建模时逐条对照不容易漏约束类型数学表达线性/非线性水量平衡V(i,t) V(i,t-1) Δt·(入流 上游滞后出库 — 出库 — 弃水)线性库容边界V_min ≤ V ≤ V_max线性流量边界Q_min ≤ Q_out ≤ Q_max线性出力边界P_min ≤ P_h ≤ P_max线性出力-流量关系P_h ηρgH·Q固定水头时线性近似水流滞时上游出库经 τ 时段后进入下游线性注意下标我有一个实操习惯把水量平衡方程单独拿出来在代码里做一个“净流入校验函数”算完一组结果后用它检查每个时段每个电站的库容变化是否严格等于入流减出流。误差超过 1e-6 就说明约束拼错了这是最快定位问题的方法。4. 光伏场景集与弃电惩罚随机性进入模型的正确姿势4.1 光伏预测误差怎么变成场景光伏出力的不确定性通常用“预测值 误差扰动”来描述。常见的做法是假设误差服从正态分布或 Beta 分布在此基础上用蒙特卡洛抽样或拉丁超立方抽样生成大量光伏出力样本再用场景削减算法筛出少量有代表性的场景。具体操作是先拿到一条光伏预测出力曲线 P_pv_pred(t)然后对每个时段施加扰动因子 ε(s,t)得到场景出力 P_pv_fore(s,t) max(0, P_pv_pred(t) ε(s,t))或者用乘性扰动 P_pv_fore(s,t) P_pv_pred(t) · (1 ε(s,t))。扰动项的方差可以按时段设置中午光伏大发时绝对误差大早晚误差小。场景削减这一步很多人图省事直接跳过实际上非常关键。原始抽样可能有几百上千个场景全部放进模型变量数量爆炸商用求解器也扛不住。用同步回代消除法或 K-means 聚类把场景缩减到 5 到 10 个每个场景重新分配概率就能在精度和计算量之间取得平衡。我实测下来7 个左右场景基本能逼近几百个场景的期望值结果。4.2 场景概率在目标函数里的正确位置场景概率 p_s 必须出现在目标函数的加权项里而且每个场景的目标贡献都要乘以对应概率所有 p_s 之和等于 1。这里有个隐蔽的坑如果每个场景的概率是通过削减算法得到的有些场景可能只剩 0.03 之类的权重调试时看着目标函数值“变小了”不要慌那只是期望化之后量纲变了。另外如果只期望光伏消纳部分而忘记期望水电部分会导致目标函数在概率加权上的不一致。正确做法是先写出完整的 J(s) Σ_t [ Σ_i P_h(i,t) P_pv_abs(s,t) ]再整体乘以 p_s 求和。水电出力虽然是公共决策但在求期望时它同样出现在每个场景里相当于它的系数是 Σ_s p_s 1所以最终水电部分不会被概率稀释。这一点在写代码时很容易无意识弄错。4.3 通道约束与弃电逻辑通道约束表达为Σ_i P_h(i,t) P_pv_abs(s,t) ≤ P_avail(t)这个约束对每个场景 s 都要成立。注意 P_h(i,t) 是公共变量不随场景变化所以它的可行性必须对所有场景同时保证。如果某个场景下光伏预测出力很高而水电计划已定的情况下压不掉那就只能靠弃光来实现约束也就是让 P_pv_abs(s,t) 小于 P_pv_fore(s,t)。所以在每个场景下还需要满足0 ≤ P_pv_abs(s,t) ≤ P_pv_fore(s,t)P_pv_abs 与预测上限之间的差值就是该场景下的弃光功率。是不是需要显式定义弃光变量取决于你目标函数里要不要给它加惩罚。如果目标函数里只有消纳电量最大化那 P_pv_abs 会自动尽量取大弃光变量可加可不加一旦要调弃光惩罚系数就必须把弃光变量显式写出来。5. Python完整求解流程数据准备、模型构建与求解器调优5.1 数据准备没数据怎么复现EI论文复现第一步卡住的往往是数据。真实电站的径流序列、水库参数、光伏出力数据属于生产数据通常拿不到。但复现的目的是验证模型逻辑不是复刻真实电站的精度所以完全可以用公开的模拟数据或合成数据。我的做法是先按典型参数构造一个两库一光的小系统。上游水库总库容 2 亿 m³死库容 5000 万 m³发电流量上限 200 m³/s下游水库库容 5000 万 m³发电流量上限 250 m³/s光伏装机 300 MW通道容量取 400 MW。入库径流用一条带季节趋势的正弦曲线加随机扰动生成光伏预测出力用一条典型的“中间高早晚低”曲线生成再按 4.1 节的方法生成场景。数据准备脚本可以单独放在一个模块里输出一个字典或 CSV 文件。建议所有参数集中管理不要散落在模型代码里否则后面调参数会很痛苦。5.2 模型构建代码骨架我用 PuLP 来演示核心建模思路因为它开源、语法清晰、对教学友好。完整代码框架如下import pulp as lp import numpy as np # 参数设置 T 24 # 时段数 I 2 # 梯级水电站数量 S 7 # 光伏场景数 dt 3600 # 秒/时段 p_s np.array([0.2, 0.15, 0.15, 0.15, 0.15, 0.1, 0.1]) # 场景概率需和为1 P_avail np.linspace(350, 400, T) # 通道可用容量 # 水电站参数示意 Q_out_max [200, 250] # m3/s Q_out_min [0, 0] V_max [2e8, 5e7] # m3 V_min [5e7, 1e7] V_init [1.2e8, 3e7] k_head [8.5, 7.2] # 固定水头下的出力系数 MW/(m3/s) tau [0, 2] # 水流滞时下游电站滞后2个时段 I_in np.random.uniform(80, 120, size(I, T)) # 区间入流示意 P_pv_fore np.random.uniform(0, 250, size(S, T)) # 光伏场景出力 # 构建模型 model lp.LpProblem(Expected_Max_Absorption, lp.LpMaximize) # 决策变量 P_h {} Q_out {} Q_spill {} V {} P_pv_abs {} for i in range(I): for t in range(T): P_h[i, t] lp.LpVariable(fP_h_{i}_{t}, lowBound0) Q_out[i, t] lp.LpVariable(fQ_out_{i}_{t}, lowBoundQ_out_min[i], upBoundQ_out_max[i]) Q_spill[i, t] lp.LpVariable(fQ_spill_{i}_{t}, lowBound0) V[i, t] lp.LpVariable(fV_{i}_{t}, lowBoundV_min[i], upBoundV_max[i]) for s in range(S): for t in range(T): P_pv_abs[s, t] lp.LpVariable(fP_pv_abs_{s}_{t}, lowBound0, upBoundP_pv_fore[s, t]) # 目标函数最大化期望可消纳电量 objective 0 for s in range(S): for t in range(T): scenario_value sum(P_h[i, t] for i in range(I)) P_pv_abs[s, t] objective p_s[s] * scenario_value * dt / 3600 # 换算成电量 MWh model objective # 约束水量平衡与梯级耦合 for i in range(I): for t in range(T): inflow I_in[i, t] if i 0 and t - tau[i] 0: inflow Q_out[i-1, t - tau[i]] Q_spill[i-1, t - tau[i]] if t 0: model V[i, t] V_init[i] (inflow - Q_out[i, t] - Q_spill[i, t]) * dt else: model V[i, t] V[i, t-1] (inflow - Q_out[i, t] - Q_spill[i, t]) * dt # 约束水电出力与流量关系固定水头线性化 for i in range(I): for t in range(T): model P_h[i, t] k_head[i] * Q_out[i, t] model P_h[i, t] 300 # 装机容量约束 # 约束通道容量 for s in range(S): for t in range(T): model sum(P_h[i, t] for i in range(I)) P_pv_abs[s, t] P_avail[t] # 求解 solver lp.PULP_CBC_CMD(msg1, timeLimit60, gapRel1e-4) model.solve(solver) print(Status:, lp.LpStatus[model.status]) print(Objective (MWh):, lp.value(model.objective))这段代码是一个可以跑通的最小骨架把目标函数、水量平衡、梯级滞时、通道约束全部包含了。注意目标函数里我按 dt/3600 把功率换算成了电量因为可消纳“电量”的单位是 MWh不是 MW。5.3 求解器选型从小规模演示到工程级PuLP 默认自带的 CBC 求解器处理上百万变量的模型不太行但对复现这类小规模问题足够。如果你的场景数多、分段线性化后引入了整数变量建议换成 Gurobi 或 CPLEX 这类商用求解器。在 Python 里只需要把 solver 换成对应接口模型定义部分基本不用动。求解性能的瓶颈主要来自场景数。S 个场景会把通道约束和光伏消纳约束复制 S 倍变量数量差不多也乘以 S。我实测过一个 96 时段、2 座水电站、20 个场景的模型CBC 要跑几分钟缩减到 7 个场景后几十秒内就能收敛。因此性能优化的第一手段永远是场景削减其次是设置合理的求解时间上限和 MIP gap而不是盲目加机器。5.4 结果导出与可视化算完之后一定要把关键结果落盘方便后续分析。我的习惯是把每个时段的 P_h、P_pv_abs、弃光量、库容轨迹、弃水量整理成 DataFrame再导出 CSV。可视化用 Matplotlib 画三张图第一张是水电光伏消纳堆叠图看互补关系第二张是库容轨迹图看调度有没有违反边界第三张是弃电功率曲线看弃电集中在什么时段。这三张图能快速暴露建模问题。比如如果你发现弃电集中发生在光伏大发时段而水电出力仍然很高那就是目标函数里弃光惩罚权重太低水电不肯让路如果库容曲线在某几个时段贴着上限走说明通道约束太紧或水电出力上界设小了。6. 复现中的关键坑与验证方法从结果反推建模漏洞6.1 坑一水流滞时方向写反这个我在 3.1 提过一次但值得单独强调。很多初版模型的写法是“下游电站 t 时段的入库等于上游电站 tτ 时段的出库”这等于让下游提前预支了未来才到的水水量平衡从根上就错了。检查方法很简单把上游出库序列设成一个脉冲看下游库容响应是不是在 τ 个时段之后才开始变化。如果立即变化滞时写反了。6.2 坑二目标函数里的期望被重复计算还有一种隐蔽的逻辑错误是把场景概率同时乘到了公共决策和场景决策上导致目标函数量纲混乱。比如有人写 objective p_s * sum(P_h) 之后再写 objective p_s * sum(P_pv_abs)看起来没问题但 P_h 部分是公共变量它本来就会被所有场景计算一次等价系数是 Σp_s 1现在额外乘了 p_s相当于把水电电量按概率打折了。检查方法是把 S 设为 1、p_s 设为 1目标值应该等于确定性模型的总消纳电量。如果不等于说明加权写错了。6.3 坑三固定水头近似在部分时段严重失真固定水头版本在库容变化剧烈时会有明显偏差。上游水库如果从高水位降到低水位实际出力比线性模型算出的要低可能导致结果过于乐观。验证方法是把固定水头模型的 P_h 反代回真实水头公式算一个“实际出力”然后对比模型出力。两者偏差超过 5% 时就得考虑升级成水位-库容曲线分段线性化。6.4 模型验证不能只跑出一组好看的数字就完事复现论文时我习惯按下面这个清单做验证约束残差检查水量平衡每天的累计误差应接近 0通道约束所有场景下应严格满足。与确定性模型对比把光伏场景数设为 1 且取期望预测值跑一个确定性版本比较它的目标值和期望模型的目标值。期望模型的目标值理论上不高于“完美预测”的确定性上界也不低于最保守模型的取值。灵敏度测试把光伏预测误差方差调大期望可消纳电量应当下降或不变如果反而上升说明模型在“利用”不确定性套利通常是有问题的。边界测试通道容量无穷大时目标值应等于总可用电量通道容量为 0 时目标值应为 0。两个极端场景能快速检验模型的物理一致性。验证项目操作方式预期结果水量平衡残差逐时段计算入流减出流减库容变化接近 0确定性对照单场景预测均值模型 vs 期望模型期望模型值介于保守与完美预测之间边界测试通道容量取 0 和无穷大目标值为 0 和可用电量上界滞时测试上游脉冲出库观察下游响应响应滞后 τ 个时段6.5 常见问题排查表现象可能原因排查思路模型无解通道容量过紧且无弃电变量检查 Q_spill 和 P_pv_abs 是否配了上界目标值异常大电量换算系数错误核对 dt/3600 是否漏乘或重复乘库容长期贴着下界水电出力下限设置过高调低 P_h_min 或检查出力系数光伏消纳总是等于预测值通道约束没生效或场景概率异常打印各场景下通道松弛量求解时间过长场景数过多或整数变量过多场景削减调整 SOS2 分段数7. 代码模块复盘与可以继续深挖的方向7.1 模块划分的经验我的复现代码一般拆成四个模块data.py 负责参数与场景生成model.py 负责定义变量和约束solve.py 负责调用求解器和保存结果analyze.py 负责指标计算和绘图。这样划分的好处是想换一组数据或换一个求解器时不需要动模型定义想改目标函数时也不需要碰数据生成。写模型文件时建议把目标函数、水量平衡、通道约束分别封装成函数。后期扩展成滚动优化时只需要在主循环里反复调用这些函数每次滚动更新初值不必复制粘贴整个模型。7.2 扩展方向一两阶段随机规划加非预期约束本文骨架里水电决策对所有场景是公共的本质是“日前计划固定”。如果想更精细地模拟日内滚动修正可以引入两阶段随机规划第一阶段决定水电出力第二阶段根据光伏场景调整弃光量和弃水量并通过非预期约束保证第一阶段决策不依赖具体场景。这个方向与当前骨架的差异主要在场景树的构建上值得深入。7.3 扩展方向二鲁棒优化替代期望优化期望最大化关心的是平均表现不保证极端场景下的可消纳电量。如果实际调度更关注“最坏情况下的底线”可以把光伏出力集合改成盒式不确定集或椭球不确定集目标改成最大化最坏场景下的可消纳电量。鲁棒模型会牺牲一定的平均消纳量换来对极端天气的抵抗能力。两种模型跑一次对比你会对“风险偏好”在调度模型里的体现有非常直观的感受。7.4 扩展方向三考虑碳约束或市场电价如果系统运行在电力市场环境下目标函数可以选择把“期望消纳电量”替换成“期望收益”并叠加碳价或绿证收益。梯级水电的蓄水决策会从“物理上能消纳多少”变成“经济上值不值得在这个时段发”模型的性质就完全不一样了。这个方向对已经有电力市场背景的读者会更有吸引力。复现这篇模型的过程中我最深的体会是不确定性优化模型的难点从来不在求解器而在“哪些决策先定、哪些决策后定”这个问题上。结构设对模型才有物理意义结构设错再精致的算法也只是花哨的错误。希望这份路线图能帮你少走我之前走过的弯路。
RELATED READING

延伸阅读

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