赶了很多次 Matlab 多目标优化的活儿之后,我越来越觉得,"基于NSGA-Ⅲ优化算法的梯级水电和火电机组联合多目标调度研究"这类题目,真正难的不是把 NSGA-Ⅲ 的代码跑通,而是把电力系统的物理逻辑塞进进化算法的框架里。这篇东西我打算按自己平时搭模型的实际顺序来讲:先讲清楚为什么水火必须联合调;再拆 NSGA-Ⅲ 的核心机制;然后是数学模型怎么列;最后是 Matlab 代码实现、仿真实验和调参踩坑。目标是让刚接触这个方向的同学除了能跑代码,还能知道每个模块在干什么、为什么这么干。
1. 水火联调的系统级动机:为什么少一个电源都不行
1.1 两类电源的互补性并不是一句空话
单看火电,机组出力可以比较精确地控制,惰性小、可用率高,但燃煤机组有一个天然的毛病:负荷变化时要烧更多的煤去维持锅炉和汽轮机的热力状态,而且低负荷运行的煤耗率和排放强度会明显上升。单看水电,响应速度快、启停灵活,枯期时来水不足,出力上不去;汛期来水丰沛时,又可能因为库容有限被迫弃水,白白浪费水能。
梯级水电更特殊,它不是一个独立的电源,而是一条链条:上游电站的发电流量和弃水会直接变成下游电站的入库流量。这意味着上游怎么调度,直接决定了下游的水头条件和可发功率。把火电加进来以后,系统在负荷低谷时可以让水电多蓄水少发电,把火电压低到最低技术出力附近;负荷高峰时再让水电放水发电,火电则承担腰荷和峰荷。这种配合既能减少火电在低负荷区间的高煤耗工况,又能尽量把水留在高价值时段使用。
1.2 多目标之间的矛盾是天然存在的
你可能会问,为什么不用一个单目标函数直接算最优?因为实际调度的目标根本不是一个。理论上讲,我们可以列出三个典型目标:
- 运行成本最小:主要来自火电的燃煤成本,水电机组的运行维护费占比很小,可以忽略。
- 碳排放最小:火电多发电,排放就高;水电顶上去,排放就降。但这可能迫使水电加大放水,导致后续时段水力不足。
- 弃水或生态流量偏差最小:汛期合理弃水是安全需要,但弃水太多就是浪费;同时下游生态基流又不能被破坏。
这三个目标不一定同时取到最优。比如成本最小的方案可能让火电在某个时段压到很低的出力,系统需要水电紧急补荷,如果此时上游来水不巧偏枯,水电站只能放掉部分死库容附近的水,成本没省多少,生态下泄却超标了。多目标优化的价值,就是把这些矛盾目标对应的可行解全部找出来,让决策者根据当时的工况去选一个折中方案。
1.3 单目标加权法为什么在这个问题上不够用
很多入门教程会把三个目标用权重加起来变成一个目标,比如 f = w1f1 + w2f2 + w3*f3。这个方法不是不能用,但有两个实际问题:第一,权重怎么定?不同调度部门对经济性、环保性和生态性的偏好不同,权重往往需要反复试;第二,一次加权只能得到一个解,想要一条完整的 Pareto 前沿,需要反复设置权重多次运行。而且当目标函数和约束组成的搜索空间是非凸的,加权法即使换很多权重也找不出中间凹陷部分的解,但进化算法一跑就是一群解,可以比较完整地逼近整条前沿。这也是 NSGA-Ⅲ 这类多目标进化算法在电力调度研究里受欢迎的根本原因。
2. NSGA-Ⅲ区别于NSGA-Ⅱ的关键:参考点不是神秘概念
2.1 为什么从NSGA-Ⅱ换到NSGA-Ⅲ
用 NSGA-Ⅱ 处理两个目标的调度问题,效果通常不错。它的多样性保持靠的是拥挤距离:同一非支配层里,优先保留周围个体稀疏的解。但目标维度上升到 3 个甚至更多的时候,拥挤距离的几何意义就开始失效,非支配个体的数量也会急剧膨胀,选择压力不够,最后得到的解集在空间里挤成一团,边界解丢失。NSGA-Ⅲ 保留了非支配排序的思路,但把多样性保持机制换成了参考点引导的小生境选择。它不直接看个体之间的拥挤程度,而是看个体关联到哪个参考点、边上还有没有空位,这样对目标空间的覆盖更均匀,在多目标问题上的表现也更稳定。
2.2 参考点是怎么生成的:Das-Dennis方法
参考点位于一个标准化的超平面上。拿三维目标来说,假设每个目标方向被划分成 p 份,那么参考点数量 H 满足组合数:
H = C(M + p - 1, M - 1)
M 是目标个数。M=3、p=12 时,H=C(14,2)=91,也就是说在三角形网格上有 91 个均匀分布的参考点。p 越大参考点越密,种群中围绕每个参考点的解数量就越多。实际做调度的时候,目标数通常就是 3 个,p 取 10 到 15 都可以;如果加了启停变量想单独做一个目标,M=4 以上时,建议采用两层参考点的方式,否则点数会膨胀得非常快。
2.3 归一化与关联操作:个体如何找到归属
进化算法每迭代到环境选择阶段,需要先把当前合并后的种群全部个体映射到参考点坐标系里。具体分两步:
第一是归一化。先找到当前种群中每个目标的最小值构成理想点,再确定每个目标最差值的估计构成 nadir 点,用理想点和截距把目标空间压缩到 [0,1] 区间。为什么要归一化?因为三个目标的量纲完全不同,成本单位是元、排放是吨、弃水是体积流量,不归一化的话,数值大的目标会主导关联距离计算,参考点机制就失效了。
第二是关联。每个参考点在归一化空间中与原点连成一条射线,称为参考线。计算每个个体到每条参考线的垂直距离,找出距离最短的那条参考线,这个个体就算关联到了对应参考点。可以这么理解:参考点把目标空间分成很多个"格子",每个个体找到离自己最近的网格线,挂靠上去。
2.4 小生境选择:多样性是这样保住的
合并后的种群先做非支配排序,得到一层层前沿 F1、F2、F3……从 F1 开始把整个前沿选入下一代,直到某一层再全部放进来就超过种群规模 N。这时,对最后一个前沿 Fl 做归一化和关联,再按下面的规则挑选:
- 统计每个参考点已经被选中的个体数,即小生境计数。
- 如果某个参考点还没有选中任何个体,优先从 Fl 中选一个到该参考线最近的个体进来,补上这个空位。
- 如果所有参考点都至少有一个关联个体了,再选择计数最少、且 Fl 中有候选者的参考点,同样从距离最近开始取。
这个操作的效果,是让算法在最后一个非支配层上刻意照顾那些还没人占的参考点,而不是挤在一小片区域里堆叠。这也正是 NSGA-Ⅲ 在高维目标下仍然能保持分布均匀性的原因。
3. 调度模型的约束清单:目标函数与四个约束家族的完整拆解
3.1 目标函数设计的三种典型写法
实际项目中我习惯先做"日调度",把时间维切成 24 个小时段,然后写两个或三个目标。最常用的三个目标如下:
成本目标:
min f1 = Σ_{t=1}^{24} Σ_{i=1}^{NG} (a_i * P_f_i_t^2 + b_i * P_f_i_t + c_i)
其中 P_f_i_t 是第 i 台火电机组在 t 时段的出力,a、b、c 是煤耗特性系数。火电成本通常按二次函数拟合,水电运维成本很小,可以略去或加一个与发电流量成比例的小项。
排放目标:
min f2 = Σ_t Σ_i ( α_i * P_f_i_t^2 + β_i * P_f_i_t + γ_i )
排放特征同样用二次函数或分段线性拟合,系数不一定和煤耗系数成比例,因为不同燃烧工况下单位煤耗对应的排放量并不恒定。
生态或弃水目标:
min f3 = Σ_j Σ_t ( Q_out_j_t - Q_eco_j_t )²
或者写成弃水量最小。我自己更常用生态流量偏差,因为它把"必须保证最小下泄"这个硬性要求直接映射成了优化目标,调度结果更接近环保部门的要求。
3.2 火电侧的约束:不只是上下限
火电机组最基本的约束是出力上下限 P_f_min ≤ P_f_i_t ≤ P_f_max。只加这个约束,模型就像玩具,因为真实机组还有爬坡约束,单位时间内出力变化不能超过规定速率:
| P_f_i_t - P_f_i_{t-1} | ≤ ΔP_ramp_i
如果考虑机组启停,还要加入最小运行时间和最小停机时间,以及机组启停状态的耦合方程。日调度场景中,若研究重点是算法本身,可以先把启停简化掉,只做连续变量;但如果要讨论"机组组合"层次的问题,那就必须加入二进制变量,问题变成混合整数多目标优化,NSGA-Ⅲ 的交叉变异策略也要相应调整。
3.3 梯级水电侧的约束:水量平衡是核心
梯级水电的物理约束比火电复杂得多,核心是水量平衡方程:
V_j_{t+1} = V_j_t + Δt * ( I_j_t + q_{j-1}t + s{j-1}_t - q_j_t - s_j_t )
其中 V 是库容,I 是天然入库流量,q 是发电流量,s 是弃水流量。上游电站放出的水经过一定延迟就会变成下游电站的入库,这就是梯级水力联系。如果忽略水流时滞,模型比较简单;要更精确就得用滞后时间参数。
其他约束包括:库容上下限 V_min ≤ V ≤ V_max,发电流量上下限 q_min ≤ q ≤ q_max,弃水流量非负且不超过闸门能力。水电机组出力不是简单设定值,它由水头和发电流量共同决定:
P_h_j_t = 9.81 * η_j * q_j_t * H_j_t
水头 H 又等于上游平均水位减下游尾水位,这两个水位需要查库容-水位曲线和泄流-尾水位曲线。Matlab 实现里通常用表格插值。
3.4 系统级耦合约束与决策变量的确定
把所有电源放在一个系统里看,最硬的约束是功率平衡:
Σ P_f_i_t + Σ P_h_j_t = P_L_t
P_L_t 是系统负荷曲线。如果完全不考虑网损,这就是个等式约束。大多数入门模型都忽略网损,当作 0 处理,但要注意在做可行性判断时,这个等式约束必须单独盯紧。
旋转备用约束给系统留裕量:
Σ P_f_max + Σ P_h_max ≥ P_L_t + R_t
决策变量集合可以这样理解:火电侧是每台机组每时段的出力,水电侧是每座电站每时段的发电流量和弃水流量。如果每座电站有多个机组,一种常见简化是先用厂内经济分配把电站总出力分摊到各机组,这样决策变量维度可以大幅下降;否则决策变量为 (NG + 2*NJ) * 24,NJ 是梯级电站数量,维度过高对算法收敛很不友好。
4. Matlab代码实现:编码、水力递推与NSGA-Ⅲ核心流程
4.1 编码方式与初始化策略
我推荐的编码方式是用实数编码,一个染色体就是一个一维行向量,按"火电出力在前、水电发电流量在中、弃水流量在后"的顺序排列。例如 6 台火电、2 级水电站的 24 时段调度问题,决策变量总数是 (6 + 2 + 2) * 24 = 240 维。虽然这个规模对进化算法来说还能接受,但要注意:交叉和变异算子操作的基本单位是基因位,维度过高时,单点变异效果会被稀释,建议用均匀交叉和多项式变异配合。
初始化时,所有决策变量在对应边界内均匀随机产生。水电的发电流量可以直接在 [q_min, q_max] 区间随机,但这样初始种群中大多数个体到计算出力时会出现库容越界。一个实用的做法是在初始化阶段对发电流量做一次"预调度":从初始库容开始递推,如果某时段库容将低于下限,就适当降低该时段的发电流量。这样的话,初始种群不至于一上来就全部不可行。
4.2 水头与时序递推的计算顺序
这是水力部分最容易被忽视的细节。每天从初始库容 V_start 开始,已知各时段发电流量和弃水后,水量平衡可以逐时段推进,得到每个时段的末库容,再用时段平均库容查水位曲线得到上游水位,用总出库流量 q+s 查尾水位曲线得到下游水位,于是水头 H 可以算出来,再带入 P = 9.81 * η * q * H 得到该时段水电出力。
但出力反过来又影响下一时段调度约束,这里有一个隐式的耦合关系:水头取决于发电流量,发电流量又取决于给定出力。这在小时间尺度计算时会带来偏差。我的处理方法是做三到五轮迭代:先假设当前时段水头等于上一时段,算出出力,修正发电流量,重新查表更新水头,直到前后两次水头差小于某个小阈值。实测下来,水电占比不高时两三轮就够,水电占比高时建议做五次迭代。
4.3 约束处理:惩罚函数、修复策略和可行性规则
NSGA-Ⅲ 本身不直接处理约束,需要自己设计约束处理机制。最常用的是外点罚函数法。把功率平衡偏差和末库容偏差作为罚项叠加到每个目标函数上:
f' = f + λ * (g_eq)^2
λ 的取值非常关键。我采用自适应策略:迭代前期 λ 取 100 到 500,让搜索有足够自由度探索全局;迭代后期把 λ 线性提升到 5000 到 10000,逼着算法收敛到可行域附近。如果 λ 全程都很小,最后选出来的解可能全是功率不平衡的废解;如果全程都很大,早期选择压力太强,算法很容易掉进局部陷阱。
另外,对末库容这种"末端必须落回目标值"的等式约束,纯惩罚效果一般。我建议配合修复:在最后几个时段,检查末库容偏差,如果低于目标,就把最后时段的发电流量适当调小;如果高于目标,就调大发电流量,并重新核算出力。修复完再计算目标函数,这样解的可行性会好很多。
4.4 NSGA-Ⅲ环境选择在Matlab中的落点
核心的 NSGA-Ⅲ 流程可以浓缩成下面这段逻辑:
function nextPop = NSGA3Selection(pop2N, refPoints, N) % pop2N是父代子代合并后的种群,尺寸为2N F = NonDominatedSorting(pop2N); % 非支配排序 nextPop = []; layer = 1; while size(nextPop,1) + size(F{layer},1) <= N nextPop = [nextPop; F{layer}]; layer = layer + 1; end % 处理最后一个不能全进的前沿 lastLayer = F{layer}; [idealPoint, nadirPoint] = EstimateIdealNadir([nextPop; lastLayer]); normLast = Normalize(lastLayer, idealPoint, nadirPoint); [refCount, associated] = AssociateRefLines(normLast, refPoints); remain = N - size(nextPop,1); nextPop = [nextPop; NicheSelection(lastLayer, normLast, refPoints, refCount, associated, remain)]; end上面只是一个框架,真正实现要注意几个细节:
- 归一化时的截距计算要用极端点,极端点选取需要解线性规划,Matlab 里可以用 linprog 求,也可以简化成目标最大值组合。
- 关联操作是两层循环:对每个个体遍历所有参考线求距离,种群规模 200、参考点 100 时,单次关联成本不高,但跑 500 代也要注意向量化。我建议用矩阵运算一次算全部个体到所有参考线的距离。
- 非支配排序可以用程序包自带的函数,但要求排序结果分层完整、层内个体无丢失,否则环境选择会出问题。
4.5 代码结构与调试顺序建议
把整个项目分成几个文件,可以大幅降低调试难度:
main_nsgaiii.m:设置参数、读取数据、循环迭代、输出结果initialize_pop.m:种群初始化calc_hydro.m:梯级水电时序递推,输入发电流量和弃水,输出各时段出力、库容、水头calc_objective.m:计算三个目标函数值并附加惩罚项nsgaiii_selection.m:环境选择sbx_mutation.m:SBX 交叉与多项式变异
调试顺序上,先单独跑calc_hydro.m,给定一个恒定出流,检查库容曲线和水头曲线是否合理;再跑单目标最小化,确认目标函数方向没写反;然后跑两个目标,最后换成三个目标。先把物理模型搞对,再谈算法效果,这一点是项目能不能按期交付的分水岭。
5. 仿真实验的设计与结果判读:别急着画Pareto图
5.1 测试算例怎么设计才算自洽
我经常看到有同学随便找一组火电参数和水库数据就开跑,结果 Pareto 前沿凹凸不平甚至出现大量不可行解。其实算例设计不需要追求真实,但必须内部自洽。以一个 6 台火电、2 级梯级水电站的系统为例,至少需要准备这些数据:
- 24 小时系统负荷曲线,包含早峰和晚峰两个典型波峰,峰谷差控制在系统可调能力的范围内。
- 火电参数表,包括每台机组的煤耗系数、排放系数、出力上下限、爬坡速率。为了让曲线有层次感,机组参数要有差异。
- 梯级水电参数表,包括每座电站的初始库容、末库容目标值、库容上下限、发电流量范围、出力系数、库容-水位曲线和尾水位-泄流关系。来水过程可以设定一个简单的日过程,上游来水略大于下游来水,体现梯级错峰。
调试阶段建议把负荷和来水都设成相对平滑的曲线,等算法跑通后再调成更苛刻的形状,否则很难判断结果变化是算法问题还是数据问题。
5.2 评价指标怎么选:HV、IGD、Spread
只画一张三维散点图,说服力是不够的。多目标优化算法论文里常用的三个指标,建议写成 Matlab 函数一并统计:
- 超体积指标 HV:以参考点为顶角计算解集覆盖的超体积,越大越好。参考点可以选略差于 nadir 点的向量,例如 [1.2f1_max, 1.2f2_max, 1.2*f3_max]。
- 反转世代距离 IGD:需要一条参考前沿,可以从多次运行得到的最大规模 Pareto 解集中选取近似解作为参考。IGD 越小解集越接近真实前沿。
- 分布度 Spread:评价解集在边界上的覆盖程度和内部均匀性,通常越小越好。
每次独立运行记录这三个指标的均值和标准差,至少跑 20 次。只跑一次得到的结果,方差大到完全不能说明问题。
5.3 Pareto前沿怎么读
如果实验设计合理,三维 Pareto 前沿应该近似一片曲面。看这个曲面能直接得到几个判断:前沿两端的极限解分别对应"纯经济最优"和"纯环保最优",中间区域代表折中解。举例来说,当一个折中解里水电在负荷高峰期多发、在低谷期少发时,对应的是梯级调蓄与火电爬坡的协同;如果发现某个解弃水量特别大而生态目标值却不差,那说明生态目标函数设计得有问题,弃水的惩罚没体现全。
在读图时,我会把 Pareto 解集投影到"成本-排放"平面和"排放-生态"平面各画一张二维散点图,比三维图更容易发现异常分布。如果两个投影图上的点出现了明显的带状断裂,那通常说明某个约束在起作用,比如火电爬坡限制了机组在相邻时段跳跃,导致解空间被切成多个不连通区域。
5.4 收敛性与稳定性分析
收敛性不能只看迭代曲线。用运行 500 代的 HV 值画逐代变化曲线,观察是否在 200 代之后基本平稳;如果 HV 还在持续波动,说明种群或参考点数量不足。稳定性则看 20 次运行的 HV 箱线图,箱体宽度大就说明算法对初始种群过于敏感,应该尝试增大种群规模或改进初始化策略。
对比实验也要在同一算例、同一评价指标、同一最大函数评估次数下进行。与 NSGA-Ⅱ 对比时,固定迭代代数是不公平的,因为 NSGA-Ⅲ 每次迭代的环境选择计算量更大;正确做法是固定 FES(函数评估次数),比如都计算 50000 次目标函数再停止。如果别的算法跑得快,就让它的种群更大、代数更多,这样才能看出算法本身的差距。
6. 参数调优记录:算法跑崩与结果漂移的常见诱因
6.1 种群规模和参考点数量必须配套
这是初学者最容易踩的坑。NSGA-Ⅲ 的参考点数量 H 由目标维数和划分份数 p 决定,如果种群规模 N 和 H 差距过大,比如 N=80 而 H=91,那么必然有参考点没有关联个体或关联极少,多样性保持机制就被打折扣。更糟糕的是,N 不是 H 的整数倍时,"补空位"逻辑会不稳定。
我的默认选择是:三维目标、p=12 时 H=91,种群取 N=182 或 273,也就是 H 的整数倍。如果算力紧张,至少也要让 N 接近 H。也可以反过来,通过枚举 p 找到最接近 N 的 H 值,比如 N=100 时选 p=10、H=66 就不如选 p=12、H=91 更匹配。设置完成后,建议在环境选择代码里输出一个统计:每个参考点关联个体数量的直方图,快速检查覆盖情况。
6.2 惩罚系数怎么定才能兼顾探索和收敛
惩罚系数 λ 太小,等式约束怎么都满足不了;λ 太大,目标函数值域被罚项淹没,算法又在可行域边缘绕圈。我推荐的调法是分两步:第一步,只看不可行个体的罚项量级,把初始 λ 设置成让它和目标函数数值处于同一个数量级;第二步,迭代中每 50 代把 λ 提高一个量级,直到末段达到目标函数的 10 倍以上。这个策略下,前期种群还能探索到"略微越界但目标很好"的区域,后期则被强力拉回可行域。
还要留意,环境选择时所有个体都用最终一代的 λ 计算目标值,而不是每一代记录的目标值直接参与最终比较。否则后期 λ 变大后,前期看似优秀但严重越界的个体会残留.
6.3 末库容这个等式约束,别一味依赖罚函数
梯级水电调度通常要求调度周期结束时库容回到目标值附近,这个约束如果只靠罚函数,算法要花大量代才能把末端流量修到云云。更聪明的做法是把它当成"可修复约束"。我在每个个体计算完 24 个时段的水库水位后,检查末库容偏差:偏差为正,就把最后一个时段的发电流量和弃水按比例加大;偏差为负,则反向调整。然后从最后一个时段反推一遍库容,确保末库容落在误差范围内。这个修复过程放在目标函数计算之前,可以显著提高可行解率。
修复时要注意:改变最后时段的出库流量,会同时影响该时段水头和出力,进而影响功率平衡。因此修复完末库容后,罚函数里的功率不平衡项会稍微变大,这个问题可以通过把"末库容校验-流量微调-水力重算"的步骤迭代两三次来解决。
6.4 交叉变异参数和随机种子对结果的影响
SBX 交叉中的分布指数 eta_c 一般取 15 到 20,多项式变异中的 eta_m 取 20 左右,变异概率取 1/决策变量数。这几个参数在大多数调度问题里都能用,不建议一上来就大改。真正影响巨大的是初始随机种子。同一个算例,换一个种子,Pareto 前沿可能形状相近,但具体点的分布差异明显。所以做对比实验时,不同算法要用同一组随机种子,或者至少用的是同一个随机数流的前几次调用结果。
我在代码里固定了rng(固定值, 'twister'),并把每个种子的结果单独保存到文件夹中,后面分析时按种子对齐。这样如果某次结果异常,可以确认究竟是随机种子导致的还是算法缺陷导致的。
7. 这个课题还能往哪走:我的扩展建议
梯级水电和火电联合调度做完基础版本之后,往上扩展的方向其实很多。如果是为了发论文或者做一个更有工程味道的项目,我建议从三个方向考虑。
方向一是引入机组组合,把火电启停状态作为二进制变量加入决策。这时候 NSGA-Ⅲ 的实数编码就不够用了,需要改成混合编码或者采用两层优化:外层用 NSGA-Ⅲ 搜机组状态组合,内层用线性规划或二次规划解负荷分配。这个结构我做过一轮,效果不错,但计算量会大一到两个数量级。
方向二是把新能源出力不确定性装进模型。目前的光伏和风电预测误差很难用一个确定值覆盖,可以设置多个典型场景,把"期望成本"和"最差场景下的成本"作为两个新目标,形成四目标甚至五目标优化问题。这时候 NSGA-Ⅲ 的参考点数量要按两层网格生成,种群规模也要相应提到 300 以上。
方向三是扩展时间尺度,从日调度变成周调度或月调度。时间维度拉长后,水力时序递推的步长和水头计算策略都要调整,否则累计误差会在后半段放大,特别是梯级电站数量多时,连续十几天的水量平衡可能完全走样。
最后分享一点我自己的实操体会:这类研究项目的成功,70% 的功夫在模型和数据上,剩下 30% 才是算法和代码。NSGA-Ⅲ 只是一个搜索工具,它本身不会替你判断约束写没写对、水力递推稳不稳定。所以最实用的建议是,动手写 Matlab 代码之前,先把问题用手算一个小算例推一遍,比如只取两小时两个电站,把水量平衡和目标函数的量级全部手算核对过,再放进算法里大规模跑。这个准备工作看似慢,实际省下的调试时间远超它的投入。