简介:一份面向电力系统优化调度方向的毕业设计参考资源,源于《梯级水光互补系统最大化可消纳电量期望短期优化调度模型》论文,针对梯级水光互补短期调度问题,提供了可复现论文模型的完整源程序。压缩包共4个文件,含2个MATLAB脚本(分别用于主程序运行与结果展示)、1份代码说明PDF及1张结果示意图,整体仅2.15MB,结构紧凑、易于查阅。程序以可消纳电量期望最大化为目标,以机组为最小调度单位,精细建模电站、机组及电网约束,并通过分段线性逼近、0-1整数变量、发电水头离散等方法将非线性模型转换为混合整数线性规划,借助CPLEX求解;算例参考西南某流域4个梯级水电站15台机组与2个光伏群,可帮助读者理解不确定性建模与MILP求解思路。已有161人学习,适合正在开展水光互补、新能源消纳或优化调度相关课题的高年级本科生和研究生,通过代码说明与运行结果可快速掌握模型构建和求解流程。
1. 梯级水电调度做「期望最大化」先解决光伏消纳账怎么算
光伏出力不确定,最常见做法是把预测坐实、把误差压小,但预测再准也有爬坡和云团遮挡,真正能用的是梯级水电的库容和时滞。这个模型把目标直接设为「可消纳电量期望最大」,意思是调度方案不是追求某一条预测曲线下的最优,而是对一组光伏出力场景做加权平均后的整体最优。这里有个反直觉结论:未必每个场景都跑出最大消纳,但期望值最大时,梯级负荷的跨时段调配会变得异常灵活,水电的电网支撑和光伏互补双重角色都会被激活。适合正在做水光互补调度、毕设方向是新能源消纳或梯级优化,以及想把 MILP 建模落到 CPLEX 上的读者。下面按模型、线性化、求解、验证四个层面拆开讲。
2. 可消纳电量期望目标与机组级约束体系
2.1 目标函数为什么是「可消纳电量期望」并带场景概率
常规调度模型常用「发电量最大」或「运行成本最小」,但这个资源面向的是消纳口径,目标函数要回答的是:给定梯级水电和光伏的总送出能力,在光伏波动的情况下,系统最多能把多少电量真正送入电网。由于光伏出力是不确定的,消纳电量就是随机量,直接最大化随机量没有意义,因此用期望值作为目标,即对场景 s 的概率 p_s 和该场景下的可消纳电量 E_s 做加权求和:
max sum_s p_s * E_s其中 E_s 与该场景下水电出力、光伏出力、负荷需求和弃电惩罚项有关。常见做法是把弃电按惩罚系数放进目标,这样模型会主动规避光伏高发时段的送出阻塞。注意,场景概率 p_s 是外部输入,可以由历史光伏出力聚类或者拉丁超立方抽样生成,模型本身不负责生成场景。
2.2 电站约束、机组约束和电网约束都做了什么
原模型的最小调度单位是机组,而不是电站,这一点决定了建模粒度。电站级建模会把整个电站看成一个黑箱,忽略机组间的水头差异、振动区差异和启停成本,结果在低水头工况下容易给出不可行的机组组合。机组级建模则把每台机组的发电流量、出力上下限、振动区、最小开机时间和最小停机时间都显式表达出来,约束体系按三层组织:
| 约束层级 | 典型约束 | 表达方式 |
|---|---|---|
| 电站层 | 水量平衡、库容上下限、出库流量限制、坝前水位-库容关系 | 连续变量等式/不等式 |
| 机组层 | 出力上下限、发电流量上下限、振动区规避、启停逻辑、最小开停机时间 | 混合整数约束 |
| 电网层 | 梯级总送出功率上下限、断面潮流限制、光伏消纳上限 | 聚合功率不等式 |
水位-库容关系和水头-耗水率关系是非线性的,这部分留到第 3 章处理。电网层的约束不是简单把各电站出力相加,而是要考虑梯级电站之间的输电通道容量,以及光伏群接入点的注入功率限制。在西南流域的场景里,光伏群通常接入梯级中某个电站的升压站,因此电网约束要按接入点分别建,而不是全流域统一一个断面。
2.3 光伏不确定性:场景集合怎么进入约束
把光伏出力不确定引入模型,不是简单地在目标函数里对 p_s 加权,而是在每个场景下都要满足系统平衡约束和电网约束。也就是对每个场景 s,有:
P_hydro(s,t) + P_pv(s,t) + P_dis(s,t) = P_load(t) + P_curtail(s,t)其中 P_dis 表示水电弃水对应少发的功率,P_curtail 表示光伏弃电功率,这两项在目标里是被惩罚的对象。换句话说,模型在可行域内自动寻找「在哪些时段稍微少发水电、在哪些时段接受光伏弃电」的折中方案,而不是把每个场景的等式约束都严格卡死。这个处理非常重要:如果每个场景都要求等式严格成立且不允许弃电,那模型会为了极端光伏场景而大幅牺牲正常场景的消纳量,期望值反而下降。所以可消纳电量期望最大这个目标,实际上容忍了部分场景下的合理弃电,前提是它在所有场景的加权期望上不吃亏。
2.3.1 场景数量的选择逻辑
场景太少,期望值估计偏差大,优化结果在真实光伏波动下可能明显偏离预期;场景太多,MILP 规模成倍增长,主机求解时间可能从分钟级变成小时级。实际案例里 10 到 20 个场景是比较常用的区间,每个场景覆盖一天 96 个时段(15 分钟分辨率)或者 24 个时段(1 小时分辨率),取决于调度周期的时间颗粒度。
3. 线性化降维:分段逼近、0-1 变量与水头离散
3.1 水电出力函数的分段线性逼近
水电机组的出力并非发电流量和发电水头的线性函数,典型表达式为 P = 9.81 * η * Q * H,η 随工况变化,H 随水库水位变化,直接进 MILP 一定会破坏线性结构。工程上最常见的手法是分段线性逼近:把发电流量 Q 的可行区间切成若干段,每段内假设单位耗水率近似常数,用 SOS2(Special Ordered Sets of type 2)或者一组连续变量和 0-1 变量来表达分段选择逻辑。
构建逼近时,需要确定分段点和对应的出力-流量折线。对每台机组,给定最小技术出力对应的流量和满发流量,中间插入 3 到 5 个分段点,段数太少逼近误差大,段数太多会增加 SOS2 约束的数量,求解时间上升。生成约束的 MATLAB 代码逻辑如下:
% 对机组 i 在时段 t 建立分段线性出力-流量关系 % x: 发电流量, y: 出力, breakpoints: 单调递增的分段点 nSeg = length(breakpoints) - 1; lambda = sdpvar(1, nSeg); for k = 1:nSeg % 每段对应一个权重变量 lambda(k) Q_expr = Q_min + sum(lambda(k) * (Q_break(k+1) - Q_break(k))); P_expr = P_min + sum(lambda(k) * (P_break(k+1) - P_break(k))); end SOS2(lambda);这段代码的核心思想是把连续流量变量拆成多个分段权重,SOS2 约束保证最多只有相邻两个权重非零,从而把流量点和出力点同时限定在同一条折线段上。这里用 sdpvar 只是示意,实际接入 CPLEX 时需要把 lambda 声明成 IloNumVar 并显式添加 SOS2 约束。分段点的选取要考虑机组的运行特性:水头高时同样的流量对应更高的出力,因此分段点不能只设一组,最好按水头区间分别标定。
3.2 0-1 整数变量处理振动区和启停逻辑
机组振动区是水电机组调度里避不开的坑。振动区内运行会造成机组剧烈振动和空蚀,实际调度中要么避开,要么快速穿越。建模上,振动区约束是非凸的,必须用 0-1 变量把允许运行区间拆成互斥的子区间。假设机组 i 在时段 t 的出力为 P(i,t),振动区为 [P_low, P_high],那么需要引入二进制变量 b(i,t,1)、b(i,t,2):
P(i,t) >= P_min * b1 + P_high * b2 P(i,t) <= P_low * b1 + P_max * b2 b1 + b2 = z(i,t) % z(i,t) 为开机状态其中 b1 和 b2 不能同时为 1,这样出力要么落在振动区以下的低负荷区间,要么落在振动区以上的高负荷区间,不会出现跨振动区取值的连续解。这是典型的「大 M 法 + 0-1 变量」拆非凸可行域的做法。如果机组有多个振动区,就按同样的方式插多个二进制变量,公式不复杂,只是变量数量线性增加。
启停逻辑的约束则以最小开停机时间为主,常见表达是:
z(i,t) - z(i,t-1) <= z(i,tau) % 启动后至少运行 T_on 时段 z(i,t-1) - z(i,t) <= 1 - z(i,tau) % 停机后至少停 T_off 时段这类约束在 MILP 里是标准的,但要特别注意:如果目标里没有开停机成本项,模型会倾向于频繁启停来钻约束空子,因此目标函数中加入启停惩罚是很有必要的。
3.3 发电水头离散:把双线性项拆成选择逻辑
出力函数中流量 Q 和水头 H 以乘积形式出现,即双线性项,直接线性化会引入大量辅助变量。作者在水头维度做离散:把可能的水头范围分成若干个区间,每个区间对应一个代表水头值,再引入 0-1 变量表示机组当前处于哪个水头区间。具体做法是:
sum_k y(i,t,k) = z(i,t) % 开机时只能选一个水头区间 H_hat(i,t) = sum_k H_rep(k) * y(i,t,k) % 用区间代表水头近似实际水头 P(i,t) = sum_k (a(k) * Q(i,t) + b(k) * y(i,t,k)) % 各水头区间下出力-流量线性关系离散水头区间数量通常取 3 到 5 个。区间太粗,出力-流量关系失真,调度结果在真实水头下可能不可行;区间太细,y 变量数量增多,分支定界树变大,求解时间明显变长。这里有个工程技巧:先用水头变化平缓的日期序列做预分析,统计水头实际波动范围,把区间边界设在概率密度较高的位置,而不是均匀划分。
4. MATLAB 数据流与 CPLEX 求解实现
4.1 main.m 的调度流程与数据组织
main.m 负责把电站参数、机组参数、光伏场景和负荷曲线组装成可供求解器消费的形式。由于 CPLEX 的 Java API 在 MATLAB 里调用需要额外配置 javaclasspath,常见做法有两个:一是直接在 MATLAB 里用 cplex 工具箱函数构建模型,二是把数据写成 LP 格式或 MPS 格式,在 Java 程序里读取并求解,再把结果写回文本文件。后者的好处是模型构建和求解完全解耦,出问题时可以单独用 CPLEX Studio 打开 LP 文件排查。
main.m 的核心流程并不复杂:
% 按日调度周期(96时段)构建模型数据 spills = []; % 弃水序列 p_pv = load('pv_scenarios.mat'); % 光伏场景集 for t = 1:T for i = 1:Nunit % 机组出力变量 P(i,t),0-1启停变量 z(i,t) % 振动区约束按 3.2 节的大M法生成 end % 汇总梯级总出力约束:所有电站 + 光伏群的总送出功率限制 % 目标:sum(p_s * E_s) - 惩罚项数据组织的关键是按「时段 × 机组」的二维结构给所有变量编号。CPLEX 的索引从 0 开始,MATLAB 从 1 开始,转换时很容易错位,所以建议在 MATLAB 里预先分配好变量 id 矩阵,把变量编号映射关系存入结构体,后续无论写 LP 文件还是调 Java 接口都能直接引用。我在实际项目中会在构建约束矩阵的同时打印「变量名 → 列索引」的映射表到文本文件,求解结果异常时对照检查,比在黑盒里猜高效得多。
4.2 CPLEX 求解参数与终止条件设置
模型转成 MILP 后,求解质量很大程度上取决于参数配置。剪枝策略、可行解提升(polish)和 MIP gap 容忍度三项最重要。穿靴戴帽的顺序建议是:先用默认参数跑一次完整求解,观察 gap 下降曲线和节点数,然后针对性地收紧分支策略。
常用参数表如下:
| 参数 | 典型值 | 说明 |
|---|---|---|
| MIP gap tolerance | 0.01 或 0.005 | 1% 的 gap 在消纳电量评估中完全够用,强求 0.1% 会让求解时间翻数倍 |
| node limit / time limit | 3600 秒 | 大规模场景下必须限时,否则分支定界树可能失控 |
| mip emphasis | 3 | 隐藏可行解优先模式,适合快速拿到可用调度方案 |
| threads | 8 或物理核数的一半 | 线程数不是越多越好,超过 16 线程加速比明显下降 |
| start algorithm | barrier | 根节点松弛用 barrier 往往比单纯形更快,尤其模型含大量 SOS2 约束时 |
求解代码在 Java 端的样子大致如下:
IloCplex cplex = new IloCplex(); IloNumVar[] P = new IloNumVar[nUnit * T]; // 添加目标:sum_s p_s * E_s,E_s 由 P_pv(s,t) + P_hydro(s,t) 组成 cplex.addMaximize(objectiveExpr); cplex.setParam(IloCplex.Param.MIP.Tolerances.MIPGap, 0.01); cplex.setParam(IloCplex.Param.TimeLimit, 3600); cplex.setParam(IloCplex.Param.Threads, 8); if (cplex.solve()) { double[] result = cplex.getValues(P); // 写出 P(i,t) 和 z(i,t) 到 solution.csv }这里把目标函数、约束构建隐去,保留参数设置的结构,因为每条约束的构建代码会非常长。关键点是:IloCplex.solve() 返回布尔值只代表找到了可行解并达到 gap,不代表模型已经最优。所以拿到解之后,要额外读取 cplex.getMIPRelativeGap(),如果超过 5% 就要回头检查场景数量或者约束是否过紧。另外 CPLEX 的 warning 输出里出现「No solution found」时,优先检查变量上下界是否出现 Min > Max 的矛盾,这种问题通常来自水头离散区间和出力上下限的不一致。
4.3 show_result.m 的结果回读与可视化
show_result.m 主要负责读取求解结果文件,并绘制几类关键图:梯级各电站的出力过程线、库水位变化曲线、光伏消纳情况以及弃电功率曲线。读取结果的核心代码逻辑如下:
% 读取CPLEX或Java程序写出的 solution.csv % 文件格式: t,i,P_hydro,z,P_pv,P_curtail data = readmatrix('solution.csv'); t = data(:,1); P_hydro = data(:,3); P_curtail = data(:,6); % 绘制某电站总出力与光伏出力的互补关系 figure; plot(t, P_hydro, 'LineWidth', 1.5); hold on; plot(t, P_pv, 'LineWidth', 1.5); xlabel('时段'); ylabel('出力/MW'); legend('梯级水电出力', '光伏出力');可视化更重要的是验证约束是否被满足,而不只是看曲线的形状。我会额外绘制一个「弃电时段标注」的子图,把 P_curtail 非零的时段用散点标出,对比这些时段的光伏出力曲线,确认模型是否在光伏高发且负荷低谷时正确地选择了弃电,而不是随机弃电。另一个验证点:每个时段各电站出力之和必须落在梯级总送出功率约束范围内,这个检查写成断言,超过容差就报 warning。
5. 场景结果复盘:四站十五机怎么看出调度效果
案例系统是参考中国西南某流域的梯级水电站,4 个电站共 15 台机组,配 2 个光伏群,光伏总装机远大于梯级水电的日调节能力,所以调度方案中会看到明显的「光伏高发压低水电、光伏低谷水电顶上」的互补过程。拿到结果后,先看三个指标:总消纳电量期望值、弃电率、水电弃水量。如果弃电率集中在光伏高发的几个连续时段,说明模型在电网约束下做了正确的取舍;如果弃电时段零散分布在全天,那大概率是机组振动区约束或者最小开停机时间约束在捣鬼。
具体验证技巧上,我习惯做一个「反事实对比」:把光伏场景数量减半重新求解,对比两组结果的弃电率差异。如果场景从 20 减到 10 弃电率变化超过 2 个百分点,说明场景代表性不足,需要重新做场景缩减;如果变化很小,则确认场景数量已经收敛。这个技巧不用改模型,只改数据规模,非常适合快速诊断不确定性建模是否到位。另外检查水头离散区间的边界:如果某台机组长时间落在区间边界上,说明离散太粗,把区间边界调整到更贴近实际运行水头的位置,通常能显著改善解的稳定性和可执行性。
本文还有配套的精品资源,点击获取