
这两年做电力系统优化调度方向的仿真感受最深的一件事是很多同学拿到一份“梯级水电和火电联合调度”题目第一反应是想办法把一堆目标压成一个单目标然后扔给求解器。这种做法不能说错但在实际科研和工程预研里发电量、耗水量、污染物排放这些指标往往是互相打架的强行加权平均只会掩盖真实的调度权衡。我后来改用NSGA-Ⅲ多目标进化算法做梯级水电和火电机组联合调度在Matlab里搭了一套完整代码才算是真正把“跑出结果”变成了“能讲清楚为什么是这个结果”。这篇就把我踩过的坑、模型怎么建、NSGA-Ⅲ为什么选它、代码按什么结构写以及结果怎么分析一次性讲清楚。1. 为什么梯级水电和火电机组要放在一起做多目标调度1.1 水电火电的互补性自然来水与可控调节之间的博弈梯级水电的特点是“有水才有电”来水的随机性、季节性和上下游水力联系让每一级电站的出力不是独立的。火电机组则相反出力可控、响应相对稳定但代价是燃料成本和污染物排放。单独调度水电汛期可能弃水枯期可能出力不足单独调度火电碳排放和煤耗又压不下来。把两者放在联合调度的框架里本质上是让水电的“随机能量”和火电的“调节能量”互相补偿。在模型里梯级水电之间还有一重约束上一级电站的出库流量会直接决定下一级电站的入库流量。这个延迟和水头影响是很多初写代码的人最容易忽略的。1.2 单目标优化为什么不够用如果只把目标设为“总发电量最大”计算机会把所有能发的电都发出来完全不考虑耗水和排放。如果只把目标设为“耗水量最小”梯级电站会把水尽量留到高水头时放火电就得大量补上缺口成本和排放随之飙升。这些目标之间的冲突不是靠调整权重就能很好解决的因为权重本身怎么定就是个难题。多目标调度的意义在于它一次性算出一组帕累托最优解在这组解里任何单独一个目标的改进都必然以至少另一个目标的劣化为代价。调度人员可以根据当天的来水预报、电网负荷需求、环保约束在帕累托前沿上选择偏发电、偏节水还是偏减排的方案。1.3 为什么多目标进化算法成了主流选择调度模型里到处都是非线性约束水头-库容曲线、机组出力限制区、火电爬坡速率。传统数学规划方法处理这种问题需要很多简化而进化算法只需要把目标函数和约束写清楚剩下的交给种群迭代。NSGA系列是应用最广的一类到NSGA-Ⅲ这一代重点解决了高维目标下种群多样性丢失的问题。梯级水库加火电目标通常三到四个用NSGA-Ⅲ正好能发挥它的优势。2. 先建模目标函数和约束条件怎么落到数学上2.1 三个目标发电量最大、耗水量最小、污染排放最小我在自己的代码里做了三个目标这也是这个领域最常用的组合。第一个目标是梯级水电总发电量最大同时火电也要满足负荷缺口所以干脆直接设为“系统总发电收益最大”折算成总发电量来处理更直观。第二个目标是梯级水电总耗水量最小最小化各站的发电流量总和。第三个目标是火电污染物排放量最小这里按火电出力的二次函数近似排放量。写成数学形式大概是这样F1 -sum(P_hydro(t) P_thermal(t))最大化发电量就等价于最小化负值。F2 sum(Q_discharge(i, t))各梯级电站的发电流量之和。F3 sum(a(i) * P_thermal(i, t)^2 b(i) * P_thermal(i, t) c(i))火电排放。这三个目标里F1和F2通常是矛盾的多发水电一方面增加发电量另一方面也会消耗更多水量。F1和F3在负荷固定的情况下也是矛盾的水电发得少火电就要补出力排放自然上升。2.2 约束条件清单库容、出力、水量平衡一个都不能漏约束条件决定了模型是否可信。我在代码里处理了这么几类水量平衡约束水库库容变化 入库流量 - 发电流量 - 弃水流量。这个约束是逐时段、逐电站写的。库容上下限约束每个梯级电站都有防洪限制和死库容限制不能超出。发电流量约束受机组最大过流能力和最小技术出力的双重限制。电站出力约束水电站出力由水头、发电流量和机组效率共同决定火电出力有上下限。火电爬坡约束火电从一个时段到下一个时段的出力变化不能超过最大爬坡速率。系统负荷平衡约束所有电站出力之和需要等于给定负荷需求。这些约束里最麻烦的是水电站出力与水头、流量之间的非线性关系。工程上通常用查表或者拟合函数来表现我为了加快Matlab运行速度事先把所有电量计算做成向量查表避免在循环里反复插值。2.3 决策变量编码与模型-算法接口NSGA-Ⅲ本身不关心你的变量是连续还是整数它只负责对编码后的个体做交叉和变异。我采用实数编码每个个体代表一个调度方案。调度时段的个数乘上电站个数再加上火电的时段出力就得到了每个个体的维数。举例来说24个时段、3座梯级水电站、1座火电厂个体就是一条长度为 24*(31) 96 的实数向量。前 72 个分量是各水电站的发电流量后 24 个分量是火电在各时段的出力。水电发电流量经过水量平衡递推能得到库容轨迹再经过约束修正得到完整调度方案。接口设计上我把目标函数封装成 function [F, violation] evaluate(individual, data)返回目标值和约束违反量。算法只调用这个接口内部怎么算水量平衡、怎么查水头-出力关系完全不影响主循环。这样调试起来方便换算例也不需要动算法核心代码。3. NSGA-Ⅲ凭什么比NSGA-Ⅱ更适合高维目标调度3.1 从非支配排序到基于参考点的选择机制NSGA-Ⅱ和NSGA-Ⅲ的第一步都是非支配排序把种群按帕累托支配关系分层第一层是最优的非支配解集第二层是被第一层支配的解里再次非支配的集合以此类推。这一步把“谁比谁好”变成“你在哪一层”是选择压力最基础的来源。分完层之后两层算法的分岔点出现了。NSGA-Ⅱ用拥挤度距离来保证多样性也就是优先保留那些在目标空间里分布稀疏的解。这个机制在二维、三维目标下效果不错但目标维数继续增加时拥挤度距离很容易被高维空间里大量非支配解稀释掉种群会迅速挤在一起失去多样性的意义。NSGA-Ⅲ不再靠拥挤度而是预先在目标空间里生成一批均匀分布的参考点然后让每个个体找到距离自己最近的参考点属于同一个参考点的个体互相竞争。这样就相当于把高维空间切成了一个个小区域每个小区域都要有代表解多样性自然保住了。3.2 归一化、关联和nichingNSGA-Ⅲ的精髓NSGA-Ⅲ每轮迭代都要做三件关键事。第一件事是目标归一化。不同目标的量纲差得非常大发电量可能是几万兆瓦时耗水量可能是几亿立方米排放量又可能只有几千吨。不归一化的话参考点坐标就毫无意义。归一化通常用种群当前的最优理想点和最差点构造截距把每个目标压缩到零到一的区间。第二件事是关联操作。归一化之后每个参考点对应一条从原点出发的参考线个体计算到每条参考线的垂直距离选最小距离的那个参考点作为归属。距离越小说明这个个体越接近该方向的“最优”。第三件事是小生境计数和保留。如果某个参考点已经有代表解新加入的个体会被限制防止一个方向挤太多。如果一个参考点完全没人占它反而会被特殊照顾从距离最近的层里挑一个解填进去。这个“照顾空区域”的机制就是NSGA-Ⅲ在四目标、五目标问题里仍然能拉开前沿的关键。3.3 遗传算子与约束可行性法则调度问题的决策变量往往有强约束比如发电流量不能为负、库容不能越界。直接用模拟二进制交叉和多项式变异生成的子代一定会有大量越界分量。我试过把所有越界分量直接修正到边界这样虽然保证变量合法但容易导致种群多样性下降尤其是变异算子刚把某个分量推出去立刻又被打回边界探索能力就白白浪费了。最后我采用的方法是交叉后做边界截断变异后随机重初始化一小部分分量。也就是说一个子代个体里如果某一个分量越界要么被拉回边界要么以一定概率重新在上下限内随机赋值。这个方法保留了遗传算法自身的随机探索能力也不破坏约束边界。约束处理方面我用了Deb的可行性法则两个个体比较时可行的优先于不可行的两个都可行时按非支配关系比较两个都不可行时约束违反量小的优先。代码里给每个个体维护了一个总约束违反量 violation所有等式和不等式约束都换算成违反量求和。4. Matlab代码实现框架、核心函数与参数配置4.1 代码整体结构与运行流程我的Matlab工程目录分得很清楚main.m、problem_data.m、nsga3.m、evaluate.m、crossover_mutation.m、result_plot.m。main.m负责读取数据、初始化种群、调用主循环、输出结果。problem_data.m定义所有电站参数和负荷曲线。nsga3.m是算法主循环每一步进化后调用选择过程。evaluate.m是模型核心输入一个体输出目标和约束。主流程大致是加载算例数据初始化 N 个个体。计算每个个体的目标值和约束违反量。进入进化循环父代通过锦标赛选择产生交配池。对交配池做交叉和变异生成子代种群。合并父代和子代得到规模为 2N 的群体。对这 2N 个个体做非支配排序。从排序结果里逐层加入下一代直到填满 N 个。最后一层不能全放时用参考点小生境选择决定放哪些。达到最大进化代数后输出第一层级的所有解作为帕累托前沿。这个流程是一个标准NSGA-Ⅲ骨架我自己在写的时候也参考了Deb公开的版本但把数据结构简化成了Matlab struct更适合调度问题快速二次开发。4.2 核心函数拆解初始化、交叉变异、环境选择初始化的关键不是random函数而是“怎么让初始种群合理分散”。纯随机生成的调度方案大概率连负荷平衡都满足不了约束违反量普遍偏大进化起来前几十代都在修复可行性。我用了启发式初始化把负荷平均分给火电和水电水电内部再按时段来水比例分配发电流量在这个基准上叠加小扰动。实测下来种群初始可行性肉眼可见地提高收敛速度也快了不少。交叉变异部分的核心函数大概是这样的function [child1, child2] sbx_crossover(parent1, parent2, lb, ub, eta_c, prob_cross) child1 parent1; child2 parent2; if rand prob_cross for j 1:length(parent1) if rand 0.5 u rand; if u 0.5 beta (2*u)^(1/(eta_c1)); else beta (1/(2*(1-u)))^(1/(eta_c1)); end child1(j) 0.5*((1beta)*parent1(j) (1-beta)*parent2(j)); child2(j) 0.5*((1-beta)*parent1(j) (1beta)*parent2(j)); child1(j) min(max(child1(j), lb(j)), ub(j)); child2(j) min(max(child2(j), lb(j)), ub(j)); end end end end多项式变异类似只是作用在单个个体上。真正花时间的是环境选择需要在合并种群上先做非支配排序再调用参考点生成、归一化、关联、小生境筛选。参考点生成用的是Das-Dennis方法。我用的目标数是3因此将每个维度划成13份参考点数量是组合数C(133-1, 2)105也就是C(15, 2)105。这个数量通常够用了。4.3 约束惩罚与不可行解的过滤代码里如果一个解严重违反水量平衡约束直接进入可行解梯队之前就可能被排序挤出。但这个“挤出”的力度要控制好否则算法会过早放弃探索极端但可能存在修复空间的区域。我把总约束违反量做了归一化除以所有个体中最大违反量然后按可行性法则参与排序。这样不可行度高的解在后期会被淘汰但在前期还能参与交叉产生新信息。在最终输出帕累托前沿时我只会保留完全可行的解。对违反量小于 1e-6 的解认为约束满足。这里要提醒一下不同约束的尺度差别很大库容约束的违反量可能是万方级别的负荷平衡的违反量可能是兆瓦级别的如果直接求和量级大的约束会完全淹没量级小的约束。所以每个等式约束要除以一个基准值把违反量压缩到同一量级。这个我在evaluate函数里提前处理好了。5. 算例分析与结果可视化5.1 测试环境与数据准备我用了一个简化但保留物理特征的算例三座梯级水电站一座火电厂调度周期24小时步长1小时。来水数据取的是枯水期典型日负荷曲线取的是早晚两个峰。火电参数参考典型燃煤机组最大出力600MW最小技术出力150MW爬坡速率每小时60MW排放系数用手册常用值。Matlab版本我用的是R2023a不需要额外工具箱只用到了基础函数和统计工具箱里的随机数生成函数。整个算例种群规模设为200进化代数500。一次完整运行在普通台式机上的耗时大约两分钟如果目标函数里没有查表插值而是逐时段循环时间会翻好几倍。5.2 帕累托前沿的形态分析跑完之后我习惯先把帕累托前沿投影到二维平面上看。三目标情况下可以分别看发电量-耗水量、发电量-排放量、耗水量-排放量三张投影图。发电量-耗水量投影通常是一条单调上升的曲线多发水电必然多耗水这个关系非常直观。有意思的是第三张投影耗水量和排放量之间不是简单的线性关系。来水集中在梯级上游某座电站时下游电站的流量衔接会影响整体耗水效率有些解耗水量更少但排放也更低这说明找到了一个更优的水火互补节奏。这种信息只有多目标计算才能暴露出来单目标优化基本看不到。5.3 收敛性与多样性评估我对算法是否收敛的判断不是只看最终帕累托前沿是否漂亮而是看进化过程有没有继续改进。三目标调度问题没有真实最优前沿可以对照我用超体积指标来评估每50代计算一次当前非支配解集相对于参考点的超体积观察曲线是否趋于平稳。多样性方面最简单有效的方法是统计最终前沿上解的“均匀程度”计算每个解在目标空间里的最近邻距离画成直方图。NSGA-Ⅲ跑出来的结果直方图往往比NSGA-Ⅱ更集中在一个窄区间这说明解面铺得更匀。我在做算法对比时把NSGA-Ⅲ和NSGA-Ⅱ各自独立运行10次比较超体积的均值和标准差。NSGA-Ⅲ整体超体积更高离散度也更小四目标以上的差距还会更明显。6. 调参经验与常见坑6.1 种群规模、进化代数、参考点数量怎么搭配这些参数看着给一个默认值就能跑但实际调起来有讲究。参考点数量如果超过种群规模会出现很多参考点没有分配个体的情况小生境保留机制基本失效。一般建议种群规模是参考点数量的1.5到2倍。三目标参考点选105个时种群规模至少150到210我用200比较顺手。进化代数取决于问题复杂度。如果约束不怎么紧200代就能出形约束紧的时候前200代大部分个体还在慢慢靠近可行域真正拉开前沿要到350代以后。保守起见我建议先用300代试跑看超体积曲线有没有平台期没平台就加代。交叉分布指数建议设在10到20之间变异分布指数设在20左右。变异概率建议设成1/变量维数这是个经验公式我实测下来比固定某个数值更稳。6.2 几个让我浪费时间的问题第一个坑是Matlab版本差异导致的随机数生成器行为不同。旧版本默认用twister新版本改成更严格的生成器同一套种子跑出来的结果不一样。这不影响算法本身但如果你要把某篇文章的结果复现出来一定要记录Matlab版本和随机数流设置。第二个坑是向量化不彻底导致的性能灾难。我最初在evaluate函数里对72个决策变量逐维计算24个时段内层再套一层循环一次种群评估接近一秒500代跑下来快半小时。改成矩阵运算和预计算查表后单次评估降到几十毫秒。调度仿真这种代码性能不是锦上添花而是能不能迭代调参的关键。第三个坑是约束违反量的量纲问题。这个前面提过但真的值得再强调一遍。我第一次跑出来的帕累托前沿全是“可行”的但实际水电库容轨迹在低谷时段有明显顶到边界的现象只是违反量和其他尺度大得多的约束混在一起被稀释到看不出异常。后来给每个约束单独设计了违反量系数问题才暴露出来。6.3 从仿真到工程落地的一些思考这套代码作为学术研究和预研工具算能打的了。但真要往实际调度系统上转还有几个方向值得继续改。一是引入更细的水头修正梯级电站在日调节内水头变化很明显固定水头模型会高估发电量。二是把机组尺度的组合启停加进决策变量这会显著增加问题维度NSGA-Ⅲ的高维优势更能发挥出来。三是把预测不确定性和风险约束加进去比如来水偏差和负荷预测误差这样解出来的前沿才更贴合调度员的真实决策空间。就我个人经验来说先跑通一个三层梯级加单火电的三目标算例理解NSGA-Ⅲ的代码逻辑和帕累托前沿的读法比直接上十多个电站的巨型模型要靠谱得多。模型复杂度可以后面逐步加算法框架和代码架构一定要一开始就立稳。你在自己的Matlab工程里照这个思路搭大概率能少走几个月弯路。