ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

水光互补多目标优化调度:NSGA-II的Python实现与Pareto前沿

水光互补多目标优化调度:NSGA-II的Python实现与Pareto前沿 水光互补优化调度这六个字拆开来看水、光、优化、调度每个词单独拎出来都是电力系统里的老话题但把它们揉到一起变成一个多目标优化问题再配上NSGA-II这个算法很多人就卡壳了。先别急着搜代码我建议先把“这到底是个什么问题、为什么要这么解”想清楚否则跑完一遍结果都不知该怎么评价。这里说的水光互补简单讲就是水电和光伏共同承担某条负荷曲线或并网任务。光伏出力看天吃饭中午猛、傍晚归零波动大还不可控水电呢水轮机响应快、启停灵活但受制于来水和库容。两者的天然属性互补问题也随之而来光出力大的时候水是不是该少发水少发会不会导致弃水光出力突然掉下去水能不能立刻顶上来这些矛盾交织在一起就变成了典型的多目标优化调度问题。我这次用Python实现了一套基于非支配排序遗传算法NSGA-II的多目标水光互补优化调度方案目标函数同时考虑发电收益最大化、出力波动最小化、弃光弃水最小化并用罚函数处理水库水量平衡、出力上下限、库容等约束最终输出Pareto前沿解集供调度人员按偏好挑选。整个过程跑下来算法代码本身不复杂真正费劲的是约束建模和数据组织。这篇文章就把我的建模思路、代码结构、参数调试经验和踩过的坑一次说清楚适合有一定Python基础、正在做电力系统优化调度方向的学生和工程师参考。1. 问题拆解水光互补到底在优化什么1.1 为什么“互补”本身就是一个优化问题很多人以为水光互补就是把两个电源的出力简单叠加其实没那么简单。水电和光伏的出力特性差别很大时间是耦合的。水电机组的调节能力强但它的“能量来源”——水库来水——是有限的今天把水多用了明天可能不够用光伏则是即发即用没法存储除非配储能那是另一套模型。所以在调度周期内水电的出力序列必须和光伏出力序列配合着排这才叫互补。从电网角度看期望的是电站群总出力曲线尽量平稳、尽量跟得上负荷需求同时收益尽量高。水多了导致弃水是损失光多了导致弃光也是损失这些损失之间也存在权衡。把这些目标并列摆出来就成了多目标优化没有唯一的“最优解”只有一组互相冲突的折中解也就是Pareto前沿。我当初拿到这个题目时第一反应也是“直接用单目标加权和不就完了吗”但真做起来发现有两个问题一是权重怎么定收益和波动之间的权重比例没有先验依据拍脑袋定的权重结果很难让决策者信服二是目标量纲不同收益是万元波动是MW的平方归一化之后再加权解的质量对权重极其敏感。换成NSGA-II之后一次运行就能得到一整条Pareto前沿决策者后处理时再挑这个思路在调度场景里更合理。1.2 调度模型的三要素决策变量、目标函数、约束条件先说决策变量。常见做法是两种一种以各时段水电机组出力为决策变量另一种以各时段水库发电流量为决策变量。我选的是后者因为发电流量直接连着水量平衡方程物理意义清楚后面算库容变化时不容易出错。设调度周期为T个时段比如一天24个小时或者96个15分钟时段决策变量就是每个时段的发电流量q_t光伏出力则由外部预测数据给定在模型中作为已知参数。再列目标函数。我这边设置了三个目标分别是发电收益最大水电、光伏上网电价不同总收益为两者收益之和出力波动最小用相邻时段总出力的平方差之和衡量值越小越好弃光弃水最小光伏实际可发而没发的量、水库可发而没发的量要尽量少我简化成“剩余可用水量/弃光电量”的综合惩罚项。约束条件就比较硬了水量平衡约束库容等于上一时段库容加上游来水减去发电流量和弃水、库容上下限约束、发电流量上下限约束、水电机组出力上下限约束、光伏出力上限约束还有调度周期末库容尽量不低于某个水位这叫末水位控制实际调度里很看重这个。1.3 为什么选NSGA-II而不是遗传算法或粒子群既然是多目标最朴素的想法是把多个目标线性加权成单目标然后用标准遗传算法或粒子群去跑。这种方式不是不行但前面说了权重很难定而且一次运行只能得到一个解想看不同偏好下的调度策略就得换权重重跑计算量大不说解集之间的连续性也不容易保证。NSGA-II的核心竞争力在于一次运行同时求解多个目标并维护一组分布均匀的非劣解。它通过非支配排序把种群分层通过拥挤度距离保证Pareto前沿上的解尽量分散再结合精英保留策略防止优秀解被丢掉。对小规模调度问题决策变量几十到上百个来说NSGA-II收敛快、实现代码相对简单是学术界和工程界用得最多的多目标进化算法没有之一。2. 非支配排序与拥挤度NSGA-II的两个核心机制2.1 非支配排序在做什么要理解非支配排序先理解什么叫“支配”。举两个调度方案A和B如果A在发电收益上比B高在出力波动上比B小在弃光弃水上也比B小那B就完全没有存在的必要A支配B。但如果A收益高但波动也大B收益低但波动平稳两者各有胜负谁也压不倒谁就叫互不支配都该保留下来。非支配排序就是把整个种群按照“被支配程度”分层第一层是当前种群中所有不被任何其他个体支配的个体它们构成Pareto前沿第二层是去掉第一层后剩下的个体中再次筛选出的非支配个体以此类推。层数越靠前个体越优。这个机制的意义在于它不再用人为设的权重来比较个体好坏而是让目标之间的比较回归到“客观优势”上。我在代码实现里用的是快速非支配排序复杂度O(MN²)M是目标个数这里M3N是种群规模对小规模种群完全够用。2.2 拥挤度距离是怎么算的、有什么用光有层级还不够。假设第一层有50个个体它们都在Pareto前沿上但可能都挤在一片区域里另一边空空荡荡。这时候就需要第二个指标——拥挤度距离——来区分优劣。拥挤度距离的计算思路很直白对某个目标维先把前沿上的个体按该目标值排序然后计算每个个体前后相邻两个个体的目标值之差把三个目标维的差累加起来就是该个体的拥挤度距离。直观理解就是这个个体周围“有多少空间”空间越大越值得保留因为能维持解的多样性。这一排序加一距离构成了NSGA-II的选择机制两个个体比较时优先看非支配层级层级靠前的胜出如果层级相同拥挤度距离大的胜出。这样一来种群会在向前沿推进的同时尽量铺满整个前沿不会团成一个点。2.3 遗传操作在调度问题里的实现细节NSGA-II的遗传操作包括锦标赛选择、模拟二进制交叉SBX和多边形变异Polynomial Mutation。选择这一步没什么好说的按上面说的规则两两比较赢者进入交配池。交叉这一步值得展开讲调度问题的决策变量是一串实数值各时段发电流量用二进制编码会丢失数值间的连续性所以必须用实值编码的SBX交叉。SBX交叉的核心是生成一个分布因子β子代在两个父代附近按一定概率展开既保留父代信息又引入变化。多项式变异同理它是在原解附近做小范围扰动扰动的幅度由分布指数η控制η越大扰动越小我实测下来η设20效果比较平衡。需要说明的是交叉和变异产生的子代发电流量必须保证数值合法比如落在0到最大发电流量之间。我是在遗传操作之后再统一做一次边界约束的修正而不是在遗传操作内部处理这样代码结构更清晰。3. Python实现的代码结构与关键函数3.1 整体代码流程我的代码遵循NSGA-II的标准框架主要分为初始化、评估、非支配排序、拥挤度计算、选择、交叉变异、环境选择这几个模块。主循环的大致逻辑是def nsga2_main(pop_size, max_iter, crossover_prob, mutation_prob, data): # 1. 种群初始化 population initialize_population(pop_size, data) # 2. 初始评估 for ind in population: evaluate(ind, data) # 3. 初始非支配排序和拥挤度计算 fast_non_dominated_sort(population) crowding_distance(population) for gen in range(max_iter): # 4. 锦标赛选择生成交配池 mating_pool tournament_selection(population, pop_size) # 5. 交叉和变异产生子代 offspring create_offspring(mating_pool, crossover_prob, mutation_prob, data) for ind in offspring: evaluate(ind, data) # 6. 父子合并环境选择 combined population offspring fast_non_dominated_sort(combined) crowding_distance(combined) # 7. 按层级拥挤度挑选前pop_size个个体 population environmental_selection(combined, pop_size) return population这个框架可以套用到很多多目标优化问题上改改评估函数和约束处理就能复用到其他调度场景。3.2 数据结构与编码方式每个个体我用一个简单类来装class Individual: def __init__(self, decision_vars): self.x decision_vars # 决策变量各时段发电流量 self.objectives None # 目标函数值三维数组 self.constraints None # 约束违反量用来算罚函数 self.rank None # 非支配层级 self.crowding_dist 0.0 # 拥挤度距离决策变量初始化时我采用“先随机生成一组发电流量再调整到满足水量平衡”的思路。具体做法是对每个时段随机生成0到q_max之间的小时流量然后从第一个时段开始按水量平衡递推水库库容如果某个时段的库容越界就把该时段的流量往可行方向修正。这个过程不能保证百分之百满足所有约束但能保证大部分个体一开始不会太离谱给后续进化一个好的起点。3.3 目标函数评估把调度场景变成可计算的数学式子评估函数是整段代码最需要小心的地方因为目标函数的计算顺序和物理过程必须对应。我的计算逻辑是这样的阶段一由决策变量q_t递推水库库容和弃水量。水量平衡方程是V_t V_{t-1} I_t - q_t - sp_t其中V_t是t时段末库容I_t是时段来水sp_t是弃水流量。如果V_t超过库容上限多出来的水量记为弃水sp_t如果V_t低于死库容说明流量给大了记为约束违反。阶段二根据q_t和当前库容查水头-出力关系计算水电机组出力P_hydro_t。这里我用了简化公式P_hydro_t 9.81η·q_t·H_effη是机组综合效率H_eff是有效水头实际项目中水头通常和库容相关我这里为简化假设水头近似恒定。光电出力P_pv_t直接读预测数据。阶段三计算三个目标函数# 目标1总收益最大化转化为最小化负收益 f1 -(sum_price_hydro * sum(P_hydro) price_pv * sum(P_pv)) # 目标2总出力波动最小化 f2 sum((P_total[t1] - P_total[t])**2 for t in range(T-1)) # 目标3弃光弃水综合惩罚 末水位偏差惩罚 f3 weight_waste * (phosphate_waste water_waste) weight_refill * refill_deviation3.4 约束处理我用的是“罚函数可行性修正”双保险约束处理是我这次踩坑最多的地方。用进化算法处理约束有几种主流思路罚函数法、修复法、约束支配法。我最终选的是罚函数为主、边界修复为辅。罚函数的核心是把约束违反量加到目标函数上。但要注意直接加会让三个目标之间的权衡关系变形。我用的做法不是简单地把违反量加到某个目标上而是把总违反量作为第四维“目标”参与非支配排序比较在比较支配关系时如果两个个体违反了同一约束违反量更小的去支配更大的如果一个个体违反约束、一个个体不违反那么不违反的支配违反的。这个思路其实是约束支配法比单纯加权罚函数更稳健也不会出现罚函数系数需要反复调的尴尬。边界修复则针对发电流量这类简单边界约束交叉变异之后直接把超出[q_min, q_max]的流量裁剪回边界。库存水量平衡的修复办法是如果某时段库容越界把该时段的流量往减少越界的方向调整然后再重新递推一遍库容重复修正直到满足或达到最大修正次数。3.5 快速非支配排序的实现细节排序算法的实现逻辑不复杂但有个细节值得注意在比较两个个体时必须对每个目标分别判断“是否更优”或“是否相等”。浮点数比较时要用容差否则由于计算误差可能把本应互不支配的个体误判成支配关系。我采用的是1e-6的容差判断两个目标值相等。def dominates(ind_a, ind_b, n_obj, tol1e-6): better_any False for i in range(n_obj): if ind_a.objectives[i] ind_b.objectives[i] - tol: better_any True elif ind_a.objectives[i] ind_b.objectives[i] tol: return False return better_any排序过程用经典的两层循环对每个个体统计它支配多少个个体、被多少个个体支配然后从“不被任何个体支配”的那批开始逐层剥离。复杂度O(MN²)N100时跑一次排序很快但注意这个操作每个进化代都要跑两次一次在父代选择前用一次在父子合并后环境选择用所以代码实现时别写出多余的计算比如重复统计支配关系否则迭代次数一上去整体速度会明显变慢。4. 仿真算例与Pareto前沿结果分析4.1 算例参数设定我用的算例规模设定如下24个调度时段一天每小时一个时段水电站最大发电流量80 m³/s库容上限1200万m³死库容200万m³综合效率0.85有效水头30米上游来水采用一天内先多后少的典型径流曲线。光伏装机容量50 MW预测出力设成典型晴天曲线早上6点开始爬升中午到峰值晚上8点降到零。电价方面水电上网电价0.25元/kWh光伏上网电价0.45元/kWh这个差价设置会让算法倾向于优先消纳光伏。NSGA-II参数种群规模100迭代300代交叉概率0.9变异概率1/决策变量个数。这里有个经验变异概率不要图省事固定设个0.1决策变量是24维变异概率设1/24≈0.04左右更合理意思是每个个体平均变异一个基因位点左右太大会变成随机搜索太小又缺乏探索能力。4.2 收敛性分析怎么看算法是否跑稳了多目标优化没有单一适应度函数可以画收敛曲线我自己常用的办法有三种。第一是看Pareto前沿是否随代数增加而整体向坐标原点三个目标都最小化的视角下方向移动第二是看每一代前沿解集里个体数量的变化第三是计算相邻代之间解集的超体积指标超体积开始稳定增长、后期平缓说明收敛得差不多。实际跑下来前100代前沿推进明显目标1收益优化方向提升很快这是水电机组和光伏出力配合从无序到有序的过程100代到200代前沿变化趋缓200代之后基本稳定只剩局部的小幅扰动。还有一个很容易被忽略的细节多目标进化算法每次运行结果都有随机性。我习惯用固定随机种子跑多次比如10次取目标值的中位数或者最优的一次作为最终结果。不然你拿其中一次运行结果去写报告复现的时候数值对不上会非常尴尬。4.3 Pareto前沿解读与调度策略分析最终输出的Pareto前沿用时可以先画一个三维散点图三个轴分别为收益负值取反、波动、弃光弃水惩罚。前沿形状通常是一个曲率明显的弧面弧面上的每个点都代表一种调度策略。我从前沿里挑两个极端解来看一个是最大收益解水电机组出力安排尽量避开光伏出力大的时段把水留到电价高光伏不出力的晚高峰发收益最高但总出力的波动相应较大另一个是最小波动解水电机组和光伏总出力尽量平稳水电作为补偿电源出力的思路很明显但代价是部分时段弃光或者低效运行收益略低。中间区域的解就是我说的“互补”策略光伏出力爬坡时段水电出力缓慢下调光伏出力骤降时段水电提前蓄力、及时顶上。这个结论对实际调度是有参考价值的调度员根据当天的负荷压力、电网调峰需求和来水情况在前沿上选一个合适的工作点而不是指望算法给出唯一的“最优答案”。这也正是NSGA-II这类方法最有价值的地方。5. 实操中的坑与排查经验5.1 约束处理不当导致解全部不可行我最早一版代码罚函数系数设得太大导致算法前期把所有注意力都放在降低约束违反量上目标函数本身反而没有优化Pareto前沿解的质量很差。后来换成约束支配法这个现象立刻消失。如果你不想改选择机制也可以用罚函数但记得让罚函数量级和目标函数量级尽量匹配比如约束违反量乘以一个和收益量级相近的系数这个系数需要在调试时观察违反量的量级再定。5.2 种群规模和迭代次数的取舍有朋友问我种群越大迭代越多是不是效果越好。理论上说是但实际有边界效应。我测过种群50、100、200三组50代时前沿明显比较乱100代已经很稳定200代提升有限但计算时间翻倍。对于24维决策变量这个规模种群100、迭代200就是一个很经济的配置。决策变量更多比如96时段的话建议种群增加到150到200迭代400以上同时考虑用并行评估加快速度。5.3 早熟收敛与多样性丢失多目标算法跑到后期前沿可能被压缩到一小块区域这是多种因素叠加的结果。我遇到最多的是变异概率过低导致探索能力不足其次是锦标赛选择的竞争强度太大锦标赛规模设得过大导致种群迅速收敛。我的做法是锦标赛规模取2不要贪心变异概率按决策变量维数的倒数来设。如果还是容易早熟可以在选择环节增加一点点随机性比如有5%的概率跳过拥挤度比较直接接受两个个体中的任意一个这个“破坏性”策略能有效保住多样性。5.4 目标函数量纲悬殊的处理收益的量级可能是几万波动量级是几千平方弃光弃水电量是几百。如果直接比较波动和弃光的目标几乎不参与支配判断。这不是算法的问题是目标归一化的问题。我的做法是先用单目标优化跑一次各目标的大致范围再按这个范围做归一化。还有一种思路是目标值不减均值不缩放的“原始值比较”但那样支配关系会失真务必做归一化处理。水光互补调度还能往哪走如果你在这套代码基础想继续扩展我个人觉得有三个方向比较值得动手。一个是把时间尺度细化到15分钟决策变量从24维变成96维这时算法参数、计算效率都要重新调整更接近实际AGC调度的场景。另一个是引入来水不确定性用场景法或鲁棒优化描述来水预测误差看看调度方案的稳健性会有什么变化。再有就是加储能把储能充放电也作为决策变量水光储三者联合调度是当前工程应用里更常见的形态。说到底NSGA-II这套框架只是个工具真正的价值在于你对调度问题的理解深度约束建得准不准、目标设得合不合理直接决定算法跑出来能不能用。把这个基本盘打扎实了无论换什么算法、加什么约束其实都是水到渠成的事。
返回列表