ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于NSGA-II的水光互补多目标优化调度Python实现与工程实践

基于NSGA-II的水光互补多目标优化调度Python实现与工程实践 水光互补优化调度这活儿我前前后后折腾了大半年。最开始用线性规划后来换成智能优化算法最后稳定跑在非支配排序遗传算法NSGA-II这套框架上。今天就把这套基于Python的完整实现思路、代码逻辑和踩坑记录整理出来给同样在做新能源调度、微电网优化或者水库调度方向的朋友做个参考。这个话题本身横跨电力系统、水资源管理和运筹优化三个领域核心要解决的问题其实就一句话在有光伏出力和水电出力的系统里怎么安排水电机组每个时段的发电计划让经济性和运行稳定性同时尽可能好。而“同时尽可能好”这六个字就是多目标优化的精髓所在也是单目标算法搞不定的地方。这套代码我从头到尾用Python实现没有依赖pymoo这类现成库非支配排序、拥挤度距离、选择交叉变异全是手写的。倒不是说pymoo不好而是调度问题里约束条件太个性化了自己掌控整个算法流程后面加约束、改编码方式、调目标函数都会灵活很多。1. 问题建模水光互补多目标调度到底在优化什么1.1 为什么水电和光伏要放在一起调度光伏出力的特点不用多说白天有太阳就猛发晚上归零来一片云出力就剧烈波动。这种特性对电网是非常不友好的尤其是光伏渗透率高的地区系统调峰压力巨大搞不好就出现弃光或者频率越限。水电的优势在于调节速度快、机组启停灵活日调节能力强的水电站完全可以根据光伏的波动来反向调整自己的出力光伏大发的时候水电少发光伏出力掉下去的时候水电迅速顶上。但问题来了水电频繁调节是有代价的。机组工况频繁切换会加剧磨损水头变化影响发电效率而且水电如果一味迁就光伏自己可能没法在电价高的时段多发电。反过来如果水电只顾自己经济效益在光伏出力高峰期也满载发电系统出力曲线就会剧烈波动对电网运行极不友好。所以水光互补调度天然就是一个存在目标冲突的多目标优化问题。这个项目处理的场景是一条馈线上有一个日调节水电站和一个光伏电站共同向负荷供电同时允许系统从大电网购电或者向大电网售电。调度周期取24小时时间尺度取1小时决策变量就是水电站24个时段的出力计划。1.2 两个核心目标函数的定义我实际建模时用了两个主要目标一个管经济一个管稳定。第一个目标是最大化系统综合经济效益。考虑三个方面水电和光伏的上网售电收益、向电网购电的成本以及光伏弃电的惩罚。数学表达为f1 -[ Σ(P_h[t] * price[t] P_pv_used[t] * price[t]) - P_buy[t] * buy_price[t] - λ * P_curtail[t] ]用负号是因为遗传算法习惯统一做最小化处理。这里P_h[t]是水电第t时段出力P_pv_used[t]是第t时段实际消纳的光伏出力P_buy[t]是从电网购电功率P_curtail[t]是弃光功率λ是弃光惩罚系数。第二个目标是最小化系统净出力波动。净出力指水电出力加上实际消纳的光伏出力再减去负荷之后跟电网交换的功率波动。简化处理为最小化水电与光伏联合出力序列的相邻时段差平方和f2 Σ (ΔP[t])²其中 ΔP[t] (P_h[t] P_pv_used[t]) - (P_h[t-1] P_pv_used[t-1])为什么要最小化这个波动因为联合出力波动越小电网侧的备用容量需求就越低系统运行的稳定性和可靠性越高调度员也越省心。这两个目标之间的冲突很明显想多发水电赚钱往往跟光伏出力高峰撞车导致净出力曲线剧烈抖动想让出力曲线平稳水电就得频繁削峰填谷牺牲发电收益。这正是非支配排序遗传算法发挥价值的地方。1.3 约束条件怎么处理约束条件是这个项目里最需要花心思的地方。我建模时考虑了五类约束水电站出力上下限约束P_h_min ≤ P_h[t] ≤ P_h_max这个由机组特性和水头决定。光伏出力约束0 ≤ P_pv_used[t] ≤ P_pv_forecast[t]实际消纳不能超过预测值。电量平衡约束P_h[t] P_pv_used[t] P_buy[t] P_load[t] P_sell[t]每个时段发用平衡P_sell[t]是上网售电功率。日水量约束这是水电调度最关键的约束24小时总发电用水量不能超过水库给定的日调度水量等价于一个日电量约束Σ P_h[t] * Δt ≤ E_h_max这个E_h_max根据水库可用水量、平均水头和机组效率换算得到。比如水库日可用水量是500万方平均水头30米综合效率0.85那么E_h_max 5000000 * 30 * 9.81 * 0.85 / 3600 ≈ 34.7万kWh。这个换算过程是水电调度的基础常识但初学者经常漏掉。爬坡约束水电机组相邻时段出力变化不能超过机组爬坡速率限制。这个约束在实际运行中很重要直接限制了两个目标函数能探索的空间。约束处理我试过罚函数法也试过修复法后面在实操环节细说各自的坑。这里先说结论对于日电量这类全局性约束用动态罚函数效果比较好对爬坡约束这种局部约束用修复策略更靠谱。2. 算法引擎为什么选NSGA-II而不是加权求和2.1 多目标优化和单目标优化的本质差异很多人一开始想到的是把两个目标加权成一个目标f w1 * f1 w2 * f2然后用粒子群或者遗传算法去优化。这个思路本身没错但有个致命问题权重怎么定实际工程中经济效益和出力稳定性之间的权衡关系往往非线性不同来水年份、不同光伏渗透率下最优权重完全不一样。而且加权法一次只能求出一个解想得到多个权衡方案就得反复调权重、反复跑优化效率很低。多目标优化不一样的地方在于它一次性求出一组帕累托最优解集。所谓帕累托最优通俗讲就是在这个解上你不可能在不牺牲至少一个目标的前提下让另一个目标变得更好。整个解集里的每个解都是“不互相支配”的它们各自代表了不同的权衡偏好。2.2 NSGA-II的三个关键机制NSGA-II能在多目标优化领域火了二十年靠的是三个设计精妙的机制。第一个机制是快速非支配排序。这一层决定了种群中个体的优先级。把所有个体按支配关系分层第一层是不被任何个体支配的解第二层是去掉第一层后不被剩余个体支配的解依此类推。这个操作的核心意义在于进化过程中优先保留层级靠前的解让种群整体朝着帕累托前沿方向推进。第二个机制是拥挤度距离。同一层里怎么比较两个解的好坏看谁周围的解更稀疏。拥挤度距离本质上衡量的是某个解在目标空间里的“邻居密度”周围越空旷说明这个区域探索得越不充分越应该保留它来维持解的多样性。这就避免了所有解堆在前沿上某一段其它区域空白的情况。第三个机制是精英保留策略。父代和子代合并成一个2N规模的种群做完非支配排序后按层级依次放入下一代放满N为止。这意味着表现最好的个体永远不会丢失算法理论上可以证明其收敛性。我实际测试过三个机制的协同效果把精英保留去掉之后收敛速度明显变慢前沿的极端区域经常丢失把拥挤度距离换成随机选择解集很快就聚集到一两个区域多样性惨不忍睹。2.3 和其他多目标算法的对比我也对比过MOPSO多目标粒子群和MOEA/D基于分解的多目标进化算法。MOPSO的实现简单收敛快但对外部存档的管理和多样性维持比较麻烦容易出现粒子聚集在局部前沿的情况。MOEA/D把多目标问题分解成多个单目标子问题协同进化理论上性能很强但对权重向量的设置和邻域大小的选择比较敏感参数调试成本高。NSGA-II在这三者中表现最均衡参数鲁棒性好默认参数下就能得到像样的结果对工程应用来说这是巨大的优势。3. Python实现从数据结构到核心算子全拆解3.1 整体代码架构整个项目代码我拆成了六个模块各自职责清晰数据准备模块负责读入光伏预测出力、负荷预测曲线、分时电价、水库参数。模型模块定义目标函数计算和约束检查。编码模块负责个体在决策变量空间和目标空间的转换。算法模块实现NSGA-II的主循环包括初始化、选择、交叉、变异、环境选择。可视化模块负责绘制帕累托前沿、出力曲线、收敛曲线。主程序模块串联整个流程输出最终调度方案。决策变量编码方式我选的是实数编码直接用水电站24个时段的出力值序列作为个体维度24。没有用二进制编码因为实数编码在这个问题里更自然精度高而且后续做约束修复时直接操作数值很方便。3.2 目标函数计算的核心代码逻辑目标函数计算是整个程序的核心每次调用都要遍历24个时段。我贴一下简化版的伪代码逻辑完整代码在我的GitHub仓库里def evaluate(individual, data): P_h individual # 水电24时段出力 # 计算净出力序列 P_net [] P_curtail [] for t in range(24): # 光伏消纳先满足负荷再考虑弃光 P_pv_used min(data[P_pv][t], max(0, data[P_load][t] - P_h[t])) # 如果水电光伏超出负荷超出部分上网或弃掉 surplus P_h[t] data[P_pv][t] - data[P_load][t] if surplus 0: P_curtail.append(max(0, data[P_pv][t] - P_pv_used)) else: P_curtail.append(0) P_net.append(P_h[t] P_pv_used) # 目标1最大化经济收益取负 f1 0 for t in range(24): f1 - P_h[t] * data[price][t] \ data[P_pv_used][t] * data[price_spot][t] - \ data[P_buy][t] * data[buy_price][t] - \ lambda_ * P_curtail[t] # 目标2最小化出力波动 f2 0 for t in range(1, 24): f2 (P_net[t] - P_net[t-1]) ** 2 return f1, f2注意一个细节光伏消纳的计算逻辑是“水电优先发电光伏填剩余空间”。这是符合工程惯例的因为水电是可调电源光伏是不可控电源系统会优先保证光伏消纳。3.3 非支配排序的Python实现快速非支配排序是NSGA-II的灵魂这里给出一个简洁的Python实现。只要理解两个集合的概念就很简单S_p是被p支配的个体集合n_p是支配p的个体数量。def fast_non_dominated_sort(fitness_values): pop_size len(fitness_values) S [[] for _ in range(pop_size)] n [0] * pop_size rank [0] * pop_size fronts [[]] for p in range(pop_size): for q in range(pop_size): if p q: continue # 判断p是否支配q if dominates(fitness_values[p], fitness_values[q]): S[p].append(q) elif dominates(fitness_values[q], fitness_values[p]): n[p] 1 if n[p] 0: rank[p] 0 fronts[0].append(p) i 0 while fronts[i]: Q [] for p in fronts[i]: for q in S[p]: n[q] - 1 if n[q] 0: rank[q] i 1 Q.append(q) i 1 fronts.append(Q) return fronts[:-1]dominates函数的判断逻辑也简单如果个体A的所有目标都不差于B且至少有一个目标严格优于B就认为A支配B。注意这里是按最小化处理目标函数值。3.4 拥挤度距离计算与遗传算子拥挤度距离的计算逻辑是对每个目标排序边界个体的拥挤度设为无穷大中间个体的拥挤度是相邻个体目标值差归一化后的累加。核心代码可以在注释里给出思路def crowding_distance(fitness_values, front): dist [0] * len(front) m len(fitness_values[0]) # 目标个数 for obj in range(m): # 按第obj个目标排序 sorted_front sorted(front, keylambda x: fitness_values[x][obj]) dist[sorted_front[0]] float(inf) dist[sorted_front[-1]] float(inf) for i in range(1, len(sorted_front)-1): if fitness_values[sorted_front[-1]][obj] ! fitness_values[sorted_front[0]][obj]: dist[sorted_front[i]] (fitness_values[sorted_front[i1]][obj] - fitness_values[sorted_front[i-1]][obj]) / \ (fitness_values[sorted_front[-1]][obj] - fitness_values[sorted_front[0]][obj]) return dist遗传算子方面我用的是模拟二进制交叉SBX和多项式变异。这两个算子是实数编码进化算法里最经典的选择。SBX的特点是交叉后的子代在父代附近保持一定的局部搜索能力。多项式变异的η值我取20SBX的η值取15交叉概率0.9变异概率用1/24也就是1除以决策变量维度保证每个个体平均有一个变量发生变异。选择操作用了二元锦标赛从种群中随机挑两个个体先比非支配层级层级小的胜出层级相同比拥挤度距离距离大的胜出。这个策略非常简洁实测效果好。4. 实例演算完整跑一遍水光互补调度4.1 算例数据与参数设置我用一个典型的日调节水电站做测试。水电站装机容量100MW最小技术出力20MW爬坡速率限制30MW/h。光伏电站装机容量80MW用的是夏季典型日的预测出力曲线大致是早晨6点开始爬升中午12点到14点达到峰值约60-70MW下午18点后逐渐降为零。负荷曲线用某工业园区夏季典型日负荷峰值120MW出现在下午15点低谷40MW出现在凌晨3点。分时电价按照峰谷平三段设置峰段10点到15点、18点到21点电价0.85元/kWh平段7点到10点、15点到18点、21点到23点电价0.55元/kWh谷段其余时段电价0.30元/kWh。购电价在售电价基础上上浮20%。日电量约束E_h_max按35万kWh设置。种群规模100进化代数500这是经过多次测试后比较稳定的配置。4.2 求解结果与帕累托前沿分析跑完500代之后得到的帕累托前沿分布相当不错。我截取三个典型解做对比分析。第一个是经济最优解位于前沿的左端目标函数f1最小。这个解的特点是水电出力尽量往电价高的时段集中中午光伏出力高峰时段水电出力压到较低水平以避免出力波动惩罚其实不对经济最优解并不关心波动它只管收益所以水电会在峰段满载发电净出力曲线起伏很大波动指标很差但收益是最高的。第二个是平稳最优解位于前沿的右端目标函数f2最小。这个解的水电出力曲线完全是光伏出力曲线的镜像光伏大发的时候水电压低出力光伏落下去水电立刻顶上净出力保持一条近乎水平的直线。代价是水电经常在低电价时段发电、高电价时段少发经济效益明显受损。第三个是折中解位于前沿的中部。水电出力曲线介于前两者之间峰段适度多发平段跟随光伏波动做一定调节弃光率控制在5%以内波动指标比经济最优解改善了约45%而经济效益只损失了约12%。这个解在实际工程中往往最受调度员欢迎。帕累托前沿的可视化图是二维散点图横轴是经济目标纵轴是波动目标前沿呈一条向右下倾斜的曲线形状非常典型。前沿的均匀性实测不错没有明显的聚集或空洞区域这说明拥挤度距离选择机制发挥正常。4.3 折中解怎么选拿到了帕累托前沿之后工程上还面临一个问题这么多解到底用哪个我推荐用基于模糊隶属度函数的方法。对每个解在每个目标上计算隶属度值域为0到11表示该目标满意度最高。公式是很经典的那种梯形隶属度函数。然后对每个解把所有目标的隶属度加权求和得分最高的就是折中解。权重可以根据调度偏好设定比如电网运行更看重稳定性就把波动目标的权重设高一些。另外也可以用TOPSIS方法先确定理想点和负理想点然后计算每个解到两个参考点的距离距离理想点越近同时距离负理想点越远排序越靠前。两种方法算出来的折中解通常很接近可以交叉验证。4.4 与单目标优化结果的对比为了体现多目标优化的价值我用同样的数据分别跑了纯经济效益单目标优化和纯波动单目标优化然后对比三组结果。单目标经济效益优化算出来的收益是最高的但出力波动指标比折中解差了将近两倍弃光率倒是跟折中解差不多。单目标波动优化算出来的出力曲线最平稳但收益比折中解低了接近两成。也就是说折中解在只牺牲极小经济收益的情况下换来了显著的稳定性提升这种权衡单目标方法完全不给你选择的机会。还有一个细节值得注意单目标优化每次只能给一个方案如果第二天光伏预测修正了或者来水情况变了就得重新调权重重新算。而多目标优化一次给出的整条帕累托前沿相当于把不确定性的应对预案都准备好了实际运行中切换方案非常方便。5. 常见问题与排查技巧实录5.1 约束处理的两大流派实测对比我前面提到约束处理试过罚函数和修复策略两种方案。这里的经验很值得说细一点。第一种是静态罚函数把违约束量乘以一个大惩罚系数加到目标函数上。问题在于惩罚系数太小约束形同虚设太大目标函数数值量级被惩罚项主导非支配排序基本被约束信息淹没。我试了从100到1e6的一系列惩罚系数很难找到一个稳健的值。第二种是动态罚函数惩罚系数随进化代数逐渐增大。前期允许一定的约束违反保证搜索空间足够大后期强制种群收敛到可行域内。这个方案效果明显更好但引入了额外参数。第三种是对个体进行约束修复针对爬坡约束和日电量约束先检查违反量然后对决策变量进行定向调整。比如爬坡约束违反时直接对相邻时段的出力做线性插值修正日电量约束违反时按比例缩放所有时段出力。修复策略在局部约束上效果极好算法收敛速度快了很多。最终我的方案是组合拳爬坡约束和出力上下限约束用修复策略日电量约束用动态罚函数。这个组合实践效果最好你也可以参考这个思路针对自己的约束特点做适配。5.2 帕累托前沿分布不均衡的排查思路如果你跑出来的帕累托前沿所有点挤在中间一小段两头延伸不出去先别急着改代码按这个顺序排查。第一步检查种群初始化。用完全随机初始化种群在目标空间里往往聚集在某个区域。改进做法是混合初始化一部分个体偏向经济最优方向一部分偏向平稳最优方向剩余的随机生成。这样初始种群就覆盖了目标空间的大部分区域为最终前沿的完整性打下基础。第二步检查拥挤度距离实现。很多人把归一化分母写错导致某个目标量级远大于另一个时拥挤度计算被大数值目标主导多样性维持形同虚设。正确做法是每个目标单独归一化保证公平。第三步检查交叉变异算子的分布指数。η_c太大会导致子代几乎等于父代探索能力不足η_m太大会导致扰动过大破坏优秀个体的结构。实测SBX的η_c在10到20之间多项式变异的η_m在15到25之间是比较合适的范围。5.3 性能优化的几个实战技巧调度问题的种群评估涉及大量矩阵运算性能优化很关键。我的实践里有几个提速经验很有效。第一目标函数计算向量化。把24个时段的计算写成numpy数组运算避免Python for循环。我测试过向量化后评估速度提升了大约8倍。第二预计算常数矩阵。分时电价、负荷曲线、光伏预测、爬坡约束矩阵这些在进化过程中不变的数据在初始化时预计算好不要每代重复计算。第三并行评估。Python里可以用multiprocessing模块做种群并行动态评估用8核跑500代种群100的算例耗时可以从十几分钟压到三分钟以内。另外提醒一个容易忽略的点把非支配排序的频率降低。如果资源紧张可以每隔5代做一次完整排序中间几代只做简单的锦标赛选择可以省不少时间。不过一般不需要走到这一步。5.4 调度结果可信度的验证方法智能优化算法求出的解工程上必须经过验证才能用。我的做法是三层验证。第一层把NSGA-II求出的折中解跟线性规划求出的单目标最优解对比。如果线性规划求出的经济最优值比NSGA-II差很多说明算法没收敛到位增加代数重跑。第二层把调度方案代入水电仿真程序检查水位变化、发电流量、弃水流量等物理量是否符合水库运行规则。第三层改变光伏预测出力序列看算法给出的调度方案是否合理响应。预测偏差大的时段调度方案应该有相应的调整余量。还有一个很实用的技巧每次跑完保存完整log包括每代的前沿指标、约束违反程度、耗时等。这样跑完可以复盘哪一代收敛加速哪个参数组合效果好都有据可查。5.5 从论文算法到工程落地的距离如果你看过不少NSGA-II论文会发现大多数论文的算例都很干净数据也理想化。但真实工程里光伏预测出力是带误差的水头是随时变化的机组检修会改变可用机组数甚至分时电价政策都可能调整。我的建议是在做完确定性优化之后一定要做一个灵敏度分析光伏预测偏差±10%、±20%时最优调度方案的收益和稳定性变化多少。如果变化很大说明方案对预测误差敏感运行时需要配合滚动修正。我自己最后落地时用的是两阶段策略日前用NSGA-II求帕累托前沿生成调度预案日内根据实际光伏出力偏差做滚动修正修正阶段用简单的模型预测控制就够。这样既保留多目标优化的全局视野又能应对实际运行中的不确定性。最后再分享一个小技巧。如果你要在实际调度系统中部署这套算法前端展示的帕累托前沿散点图交互功能很关键让调度员能直接点击前沿上的点查看对应的调度方案曲线。我调试的时候就是靠这个交互图发现了一个约束写错的bug前沿某一段的出力曲线明显不符合物理规律。解决这个bug之后解集质量立刻上了一个台阶。优化算法做出来是一回事能让人愿意用、用得上又是另一回事。
返回列表