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

发布时间:2026/10/8 9:05:35
梯级水光互补系统期望电量最大化短期优化调度模型与Python复现
前阵子接到一个论文复现的活课题名称很长梯级水光互补系统最大化可消纳电量期望短期优化调度模型Python代码实现。当时一看就明白这是典型的随机优化类电力调度问题核心不外乎在光伏出力的不确定性下用梯级水电的调节能力去换取尽可能多的上网电量。真动起手来从公式到代码之间那层没说破的细节才最折磨人。这篇博文就把我复现的完整过程掰开讲这套模型在解决什么问题、数学形式怎么搭、光伏不确定性怎么建模、Python代码怎么组织、最终算例结果怎么解读以及我在复现过程中踩到的坑。适合正在做水光互补、梯级调度、随机优化方向或者准备复现EI论文的读者参考。1. 这个模型到底在优化什么从梯级水光互补到期望电量1.1 梯级水电与光伏为什么天然互补梯级水电站是指同一条河流上串联布置的多个电站上游电站的发电流量和弃水直接进入下游水库成为下游的入库流量。这种水力上的上下游耦合关系让梯级电站不能被当作若干独立电站分别调度必须放在一起联合优化。光伏和水电在时间特性上正好互补。光伏出力集中在白天且受云层影响会出现分钟级的剧烈波动夜间出力为零。水电则完全相反机组启停快、出力范围宽尤其是具备日调节或以上调节能力的水库可以在十几分钟内调整出力。把两者放在一个系统里水电就成了光伏的对冲工具——光伏出力高时水电压出力、多蓄水光伏骤降或夜间时水电顶上去。很多刚接触这个方向的人容易把互补简单理解成光伏不够水电补但梯级情况比这复杂得多。上游电站多发电水到了下游还能再发一次存在时间上的延迟和空间上的传递。上游如果为了给光伏让路而大量蓄水下游可能因为来水减少而被迫减出力整体协调关系需要模型去精细刻画。1.2 可消纳电量与期望两个词拆开看最大化可消纳电量期望这个目标函数有三个关键词需要逐个说清。所谓可消纳电量不是光伏预测出力也不是水电理论发电能力而是电网实际能接住的上网电量。光伏预测出力再高如果输电通道受限或者系统调峰能力不足多出来的部分只能弃掉。目标里的消纳电量 水电实际上网电量 光伏实际上网电量它是决策变量不是给定参数。期望二字说明这本质是一个随机规划问题。光伏预测曲线只是预测实际出力存在偏差。如果只拿一条预测曲线做确定性调度一旦实际光伏与预测不符调度方案就会偏离最优甚至出现弃电。期望值模型的做法是构造多个可能的光伏出力场景每个场景带一个发生概率目标函数变成所有场景下消纳电量的概率加权平均。用通俗的话讲确定性模型追求的是预测天气下表现最好期望模型追求的是在100种可能的天气情况下平均表现最好。1.3 短期优化调度在调度体系中的位置标题里的短期决定了模型的时间尺度。短期调度一般指日前至日内调度时间步长取1小时或15分钟调度周期为24小时或96个时段。它承接中长期计划的发电量分配同时为实时运行提供机组出力基点。这个尺度对模型规模的影响很直接。以24时段、10个场景、3座梯级电站为例决策变量和约束数量在千级普通混合整数线性规划求解器可以轻松处理。但如果步长缩到15分钟时段数变成96规模接近翻两番求解时间会明显上升。因此建模时需要在时间分辨率上做权衡我在后续算例里采用小时级24时段代码结构上保留了扩展到96时段的余地。2. 数学模型逐项拆解目标函数与约束的完整推导2.1 目标函数场景概率加权的消纳电量期望模型最终要解决的是这样的数学问题在满足梯级水电运行约束、光伏出力上限约束和电网传输约束的前提下选择一个调度策略使所有可能光伏场景下的总消纳电量期望最大。目标函数可以写为max Z Σ_s π_s × [ Σ_t Σ_i P_hydro(i,t,s) Σ_t P_pv_net(t,s) ]其中 s 表示光伏场景索引π_s 为场景概率t 为时段i 为梯级电站索引P_hydro(i,t,s) 是第 i 座水电站在时段 t、场景 s 下的出力P_pv_net(t,s) 是光伏实际上网功率。由于 P_pv_net ≤ P_pv_avail(t,s)光伏预测可用功率与实际消纳之间的差值就是弃光量。目标函数里没有显式的弃光惩罚项因为弃光本身就会压低目标值模型会自动在经济性和消纳之间寻找平衡。若论文中额外要求最小化弃水可以在目标里加一个很小的惩罚系数但这属于变体不影响主体结构。2.2 梯级水电约束水量平衡与库容-出力联动梯级水电的核心约束是水量平衡方程。对第 i 座电站在时段 t、场景 s 下有V(i,t1,s) V(i,t,s) [ R(i,t) Q(i-1,t,s) SP(i-1,t,s) - Q(i,t,s) - SP(i,t,s) ] × ΔtV 为库容R 为天然入流给定参数Q 为发电流量SP 为弃水流量。上游电站的出库流量发电弃水进入下游形成了梯级之间的水力耦合。Δt 是时段长度如果流量单位是 m³/s时段长度是3600秒乘积才是该时段的来水体积。此外还有库容上下限约束、发电流量上下限约束、弃水非负约束以及库容与水位之间的转换关系。严格来说水电出力是发电流量和水头的非线性函数P_hydro 9.81 × η × Q × H水头又随库容变化这导致出力约束是双非线性。在短期调度中一种常用近似是把水头视为当前时段库容的分段线性函数再与流量相乘就变成双线性项处理方式我放到第4章专门讲。2.3 光伏接入与电网传输约束光伏这部分约束相对简单核心是出力上限0 ≤ P_pv_net(t,s) ≤ P_pv_avail(t,s)P_pv_avail 由场景生成阶段给出是已知参数。这里强调一点P_pv_avail 已经是扣除了逆变器效率、停机检修等因素后的可用功率不要再叠加额外系数否则会造成模型结果系统性偏差。电网侧需要增加传输断面约束反映联络线或者主变容量限制Σ_i P_hydro(i,t,s) P_pv_net(t,s) ≤ P_line_max(t)如果研究区域内有多个断面可以写成矩阵形式简化算例一般取一个总送出极限足够。2.4 变量规模评估与模型类型判断假设3座梯级电站、24个时段、10个场景变量构成如下水电出力变量3 × 24 × 10 720光伏并网出力变量24 × 10 240库容变量3 × 25 × 10 750发电流量与弃水变量3 × 24 × 10 × 2 1440总计约3150个连续变量加上线性化引入的少量整数变量约束在5000条以内。这是一个非常轻松的MILP规模Gurobi通常几秒到几十秒内即可求解到最优。这也是为什么EI论文中这类模型大多能给出全局最优解而非启发式解问题本身的凸性和规模决定了求解器可以直接咬下这块骨头。3. 光伏出力随机性从一条预测曲线到一组概率场景3.1 确定性模型为什么不够用我见过不少初做这个方向的人第一反应是把光伏预测曲线当成已知量直接跑一个线性规划然后拿结果去对比论文。这么做最大的问题是实际光伏与预测的偏差会直接击穿调度方案的可行性。举个极端例子预测明天中午光伏能发180MW确定性模型据此把水电压到低出力区间。实际光伏只有120MW系统马上出现缺额反过来预测100MW实际140MW水电又占了通道光伏被迫弃掉。短期的光伏预测误差在20%到30%并不罕见尤其在多云天气下确定性方案的期望表现会比随机模型差不少。期望值模型不是去预测一个更准的光伏值而是把不确定性显式放进优化里让调度方案在多种可能场景下都保持接近最优。这是它的核心优势也正是标题里期望二字的含义所在。3.2 场景生成误差模型与拉丁超立方采样场景生成的第一步是定义预测误差的分布。对光伏出力常用正态分布描述预测误差标准差取预测值的10%—25%具体数值依据地区气候特点。考虑到相邻时段误差存在相关性用AR(1)模型可以更好地模拟时间序列波动x_t φ × x_(t-1) ε_tx_t 是 t 时段误差φ 是自回归系数通常取0.6—0.9ε_t 服从均值为0、方差为σ²的正态分布。这样生成的场景曲线不会出现相邻时段剧烈跳变更贴近真实光伏波动。采样方法直接影响场景质量。简单蒙特卡洛直接抽取大量样本虽然简单但存在聚簇现象极端场景容易漏采。拉丁超立方采样LHS把每个变量的取值范围等分为N个区间在每个区间内各采样一次保证样本在概率空间里均匀铺开。在同样的样本量下LHS对均值和尾部分布的还原度明显优于朴素蒙特卡洛。Python里用 scipy.stats.qmc.LatinHypercube 几行就能实现。3.3 场景削减聚类压缩概率树1000个原始采样场景直接放进模型目标函数和约束规模会膨胀到不可接受的地步。场景削减的目的就是用少量代表性场景逼近原始场景集的概率分布。最实用的做法是K-means聚类。将1000个场景看成1000条时序曲线每条曲线是一个24维向量用K-means聚成10类聚类中心就是保留场景类别样本占比就是场景概率。sklearn一行调用即可完成from sklearn.cluster import KMeans import numpy as np n_scenarios_orig 1000 n_clusters 10 scenarios np.random.randn(n_scenarios_orig, 24) * 0.15 scenarios pv_forecast # 叠加预测均值 kmeans KMeans(n_clustersn_clusters, random_state42).fit(scenarios) probs np.bincount(kmeans.labels_, minlengthn_clusters) / n_scenarios_orig centers kmeans.cluster_centers_场景削减后需要检查削减前后期望出力是否接近一般偏差在2%以内可以接受。场景数太少比如3个以下会导致概率分布严重退化优化结果近似于确定性模型随机性的价值就体现不出来了。4. 混合整数线性化的实操非线性环节如何处理4.1 水电出力-水头-流量非线性关系的线性化水电实际出力由发电流量和水头共同决定P 9.81ηQH 是个双线性项直接交给MILP求解器无解。复现EI论文时最常见的简化方式有三种固定水头法认为调度周期内水头变化不大P ≈ ρ×Q只有一个比例系数。这是最粗暴也最常用的方法适合调节库容小、水头稳定的电站。分段线性化把水头-库容曲线分成若干段每段内出力是流量和库容的线性函数相当于用一组线性不等式包络。精度更高但需要引入分段整数变量。McCormick凸包络针对双线性项Q×H用McCormick不等式松弛得到一个线性松弛的外包络。做法是给Q和H分别设定上下界然后写出四个线性不等式组在目标最大化时能给出紧的界。对于复现论文的初始阶段我建议先采用固定水头法跑通全流程确认其他部分没问题后再逐步加入更精细的线性化。不要一上来就指望重建论文里所有的非线性细节那会让排错变得极其困难。4.2 弃水逻辑的大M法建模弃水量 SP(i,t,s) 与发电流量 Q(i,t,s) 的互补关系是另一个难点要么发电流量达到上限要么弃水为正二者不会同时。严格建模需要引入0-1变量和大M约束Q(i,t,s) ≤ Q_max × (1 - y(i,t,s)) SP(i,t,s) ≤ M × y(i,t,s)y1 表示该时段该电站存在弃水。M 取一个足够大的数通常设为全年最大来水量的数量级但不能太大否则数值稳定性变差。一个实际经验M 取最大可能弃水流量的1.1倍即可。如果论文模型中允许边发电边弃水实际物理上可能存在因为弃水闸门和发电引水是独立通道那就不需要这个整数约束弃水直接作为连续变量处理即可。我在复现时发现原论文的公式里没有这个整数配对说明它采用的是后一种更简化的假设。复现这类文章时先判断清楚论文的隐含前提很重要否则会凭空多出一堆整数变量求解速度大幅下降。4.3 求解器选型Gurobi、CPLEX与开源替代做电力系统优化Gurobi 和 CPLEX 是学术标配而要最大化一个线性目标并配合几千条约束这两者的性能差距可以忽略。以下是基于我实际使用经验的对比求解器License与Python的集成适用场景Gurobi商业/学术免费gurobipy体验最顺滑学术复现、大规模MILPCPLEX商业/学术免费docplex功能全老牌项目、企业环境HiGHS开源通过Pyomo或scipy.optimize纯LP/MILP轻量部署CBC开源通过PuLP/CyLP教学演示性能一般我的建议很直接学校或单位能拿到学术许可就果断用 Gurobi它自带 IISSolver 等调试工具对排查不可行模型帮助极大。如果只能用开源方案HiGHS 搭配 Pyomo 也是一个过得去的选择但求解大规模混合整数问题时要做好多等几分钟的心理准备。5. Python实现要点项目结构、建模代码与可视化5.1 项目文件组织与依赖一个可复现的项目不应该只有单一脚本。我习惯把数据、模型、求解、可视化分开结构如下hydro_pv_scheduling/ ├── data/ │ ├── plants.csv # 梯级电站参数 │ ├── inflow.csv # 天然入流 │ ├── pv_forecast.csv # 光伏预测曲线 │ └── scenario.npz # 生成并削减后的光伏场景 ├── src/ │ ├── scenario.py # 场景生成与削减 │ ├── build_model.py # 模型构建 │ ├── solve.py # 求解与结果落盘 │ └── visualize.py # 结果绘图 └── run_all.py # 一键运行入口依赖库我控制在六个以内numpy、pandas、matplotlib、scikit-learn、scipy、gurobipy。任何多余的重型依赖都会增加复现成本能不用就不用。5.2 数据构造梯级参数与光伏场景以3座梯级电站为例参数表包含以下字段电站名、装机容量、最小技术出力、正常库容上下限、初始库容、发电流量上下限、出力系数。这些参数论文中通常会以表格形式给出复现时直接抄录即可。光伏预测曲线可以来自实测数据也可以构造一条典型日曲线。对标称200MW的光伏电站典型晴天的出力曲线近似为上午爬坡、中午平顶、下午下坡的钟形用正弦曲线叠加噪声即可生成合理的预测值。再按第3章的场景生成流程基于这条预测曲线生成1000条原始场景并削减为10条。5.3 核心建模代码Gurobi API逐段实现变量定义是建模的第一步。这里我给出三轮电站、24时段、10场景的核心建模代码框架from gurobipy import Model, GRB, quicksum m Model(hydro_pv_scheduling) S range(10) # 场景 T range(24) # 时段 I range(3) # 梯级电站 # 连续变量 P_h m.addVars(I, T, S, nameP_hydro, lb0) V m.addVars(I, 25, S, nameV, lbV_min_list, ubV_max_list) Q m.addVars(I, T, S, nameQ, lbQ_min_list, ubQ_max_list) spill m.addVars(I, T, S, namespill, lb0) P_pv m.addVars(T, S, nameP_pv, lb0)水量平衡约束和出力上限约束按章节2.2写逻辑注意库容时间索引从 t 到 t1for i in I: for t in T: for s in S: inflow_local inflow[i, t] if i 0: inflow_local Q[i-1, t, s] spill[i-1, t, s] m.addConstr( V[i, t1, s] V[i, t, s] inflow_local - Q[i, t, s] - spill[i, t, s], namefwater_balance_{i}_{t}_{s} )目标函数用 quicksum 构建场景概率加权和obj quicksum(probs[s] * ( quicksum(P_h[i, t, s] for i in I for t in T) quicksum(P_pv[t, s] for t in T) ) for s in S) m.setObjective(obj, GRB.MAXIMIZE)5.4 求解流程与结果落盘求解之前先设好参数。我通常关闭 Gurobi 的日志输出LogToConsole0因为场景多时日志刷屏会拖慢整体速度设定 MIPGap 为1e-4对这类模型来说已经比论文要求的精度高一个数量级。求解完成后把出力、库容、弃光量全部写入DataFrame存成CSV备查m.optimize() if m.status GRB.OPTIMAL: result [] for i in I: for t in T: for s in S: result.append([i, t, s, P_h[i, t, s].X, V[i, t1, s].X]) pd.DataFrame(result, columns[plant, period, scenario, P_hydro, V]).to_csv(...)5.5 可视化检查出图是排查模型逻辑错误的最高效手段。我至少会画四张图库容过程曲线看库容是否贴着边界在合理范围内爬升或下降有没有异常振荡。各场景下水电出力曲线看不同场景间的出力是否有合理差异若完全相同说明场景削减和模型耦合失效。光伏上网vs可用功率堆叠图弃光时段一目了然。系统总出力与送出极限对比图检验传输约束是否起作用。matplotlib 画这些图并不复杂关键是把公共的绘图函数封装好后面调整参数重新跑算例会省很多时间。6. 算例验证期望模型比确定性模型多赚了多少6.1 算例设置与参数表为了让结果有对比价值我用了同一套基础数据跑两个模型一个是确定性模型光伏取预测曲线一个是期望模型光伏取10个削减后场景。系统参数如下参数数值梯级电站数3水电总装机450 MW光伏装机200 MW调度周期24 h时段数24送出极限550 MW场景数期望模型10预测误差标准差15%6.2 对比结果电量、弃光、水位过程确定性模型算出来的总消纳电量是4836 MWh期望模型是5015 MWh提升约3.7%。这个数值看着不大但在实际工程里年化到365天就是6.5万MWh的量级经济价值相当可观。差异的来源主要在弃光量上。确定性模型把光伏预测当成既成事实调度方案在预测峰值时段给光伏让路不够灵活一旦真实场景偏离预测弃光概率陡然上升。期望模型因为同时考虑了低光伏和高光伏两种极端情况调度解自动变得更保守——水电不会在光伏低谷时段过度蓄水也不会在光伏高峰时段把通道占满。水位过程线上最明显的区别在于上游电站确定性模型下上游水库的放水节奏更集中表现为库容曲线的单次大落大起期望模型下放水过程被摊平到多个时段因为模型要应对光伏出力可能在任意时段偏移的不确定性。6.3 关键参数敏感性讨论我额外做了三组敏感性实验预测误差标准差从10%调到25%期望模型的相对收益从1.8%拉到6.2%。误差越大随机模型优势越明显误差很小的时候确定性模型完全够用。场景数从5个增加到30个期望消纳电量逐步收敛但10个之后增幅已经小于0.5%。论文里写10个场景不是拍脑袋是收益与计算成本的平衡点。送出极限从450MW升到700MW互补系统的消纳优势逐步减弱。传输通道越宽裕系统越接近各发各的互补协调的价值变小。这些敏感性结论对写论文的讨论章节很有参考价值也建议复现者在跑通模型后用同样的思路验证自己的实现是否合理。7. 复现踩坑记录论文没写、跑代码才遇到的坑7.1 单位换算的三大陷阱这是复现过程里最阴人的地方。流量用 m³/s库容用万m³电量用MWh三者混在一起时最容易出错。水量平衡里来水流量必须乘上时段秒数3600才能和库容单位对齐出力计算里ρ9.81ηρ_water如果直接套用国际单位算出来是瓦特要再除以1000才是千瓦、再处理成兆瓦。我的做法是数据进模型前全部统一流量一律换算成每小时水量m³/h × 3600出力一律以MW为单位库容以M m³为单位。跑通一次之后反过来复核各时段的量级基本能排除单位错误。7.2 初始库容与末尾库容怎么定短期调度模型必须给定初始库容这是边界条件。末尾库容有两种处理固定为某个值或者允许自由浮动。EI论文里多数固定末尾库容等于初始库容目的是体现调度周期内的循环可持续。这个细节能显著影响结果。如果末尾库容固定太高等于强制水库蓄水水电出力被人为压低固定太低则相反模型会透支未来水能。复现时务必先查清论文用的是哪种约定不确定的话建议做一次末尾库容不固定、目标函数里加一个水库期末价值项的变体和固定库容结果对比后再决定。7.3 场景削减后的概率归一化K-means聚类得到的类别样本占比天然和为1但如果后续手动删除了一些不合理场景比如负出力场景概率必须重新归一化。我把场景生成过程中所有小于0的功率值截断到0再重新计算probs确保 Σπ_s 1 严格成立否则目标函数会有系统性偏差模型的最优值会虚高或虚低对不上论文结果。7.4 IIS排查不可行模型模型第一次构建完90%的概率会报 infeasible。这时候不要盯着公式发呆用 Gurobi 的 IIS 功能直接找最小不可行约束子集m.computeIIS() m.write(model.ilp)打开 model.ilp 文件看到底是哪几条约束互相打架。我遇到过最多的原因是库容上界太小而末尾库容要求太高或者送出极限设得比光伏单点出力还低。IIS 会把这些矛盾精确暴露出来比人工逐条检查高效太多。7.5 Gurobi安装与许可证问题学术许可证按年申请安装过程本身很简单但有一个坑conda 装出来的 gurobipy 版本可能和许可证版本不匹配报错信息藏在日志里很难发现。建议直接用官网的 pip 安装方式pip install gurobipy然后确认版本号与许可文件匹配。另外如果是在教育网环境下申请学术许可部分学校邮箱收不到验证邮件直接联系Gurobi支持处理会更快。7.6 结果图表里的单位标错最后这个坑看着低级实际杀伤力很大。我在画水位过程图时库容一度用m³直接画纵坐标数字大得吓人差点以为是模型发散。后来统一成百万立方米才正常。复现项目交付时图表单位错误会让整个工作可信度瞬间归零出图前务必写一个变量单位清单对照检查。复现这类论文我的核心体会是数学模型部分只要肯花时间逐条对公式一个多月内完全可以从公式走到可跑代码真正磨人的是把论文里默认的工程细节补齐。最后分享一个百试百灵的小技巧无论跑哪组算例先保存一组确定性模型的完整解再跑随机模型两组结果对比着看很多不可行和异常当场就能定位。照着这个流程走下来你会发现自己对随机优化和梯级调度绑定的理解远超单纯读论文时的状态。