ARTICLE DETAIL

资讯详情

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

基于NSGA-II的多目标水光互补优化调度实战

基于NSGA-II的多目标水光互补优化调度实战 水光互补优化调度这个词圈外人听着像教科书概念但真正在电力系统调度、新能源规划、抽蓄电站运行这些方向摸过模型的人都会明白它有多现实。光伏出力中午猛、傍晚归零来阵云就剧烈波动水电虽然调节能力好可又背着来水、库容、生态流量的硬约束两者放在一个时间尺度里协同调度本质上就是多目标博弈。我这次整理的项目就是用非支配排序遗传算法NSGA-II求解一个多目标水光互补优化调度问题全程Python代码实现最终输出一条Pareto前沿让调度人员根据当下电网的运行偏好自己去挑方案。这篇文章把模型设计、算法原理、完整代码和踩坑记录都放出来适合正在做水光互补、梯级水电站调度、新能源消纳相关课题的研究生和工程师参考。1. 水光互补优化调度本质是一道“多目标博弈”题1.1 光伏、水电各有脾气互补才有戏搞新能源调度的人常说一句话光伏电站好建难的是让它听话。光伏出力取决于太阳辐射白天有、晚上没晴天多、阴天少一片云飘过来就能让出力在几分钟内掉一半。这种波动对电网来说非常不友好尤其是在光伏渗透率高的区域午间大发时段容易弃光傍晚出力骤降时段又需要火电或水电快速顶上。水电刚好是另一副脾气。有水库的水电站可以通过调节发电流量来改变出力从几十兆瓦到满发也就是操作闸门的事响应速度虽然比不上火电的AGC但在日内调节这个尺度上已经足够灵活。不过水电也并不是随心所欲来水是老天爷给的水库库容有上下限下游还有生态流量和供水要求汛期甚至要防洪预泄。这意味着水电的调节能力不是无限的它是一笔“有时间属性的资源”用早了后面就没得用用晚了光伏正大发又派不上用场。水光互补这个思路之所以成立正是因为两者在时间分布上存在天然的错位。光伏夜间出力为零水电可以承担基荷光伏午间大发水电可以压低出力、把水蓄起来留到晚上光伏剧烈波动的时候水电快速调节去平抑。理想状态下的互补效果是让水光联合出力曲线变得平滑同时把清洁能源的利用率顶到最高。这个互补的过程在数学上就是一个优化调度问题给定光伏预测出力和来水预测安排未来若干个时段里水电站的发电流量或出力让整个系统在满足各类约束的前提下达到某些目标最优。1.2 调度目标不可能只有一个如果一个优化问题只有一个目标那是幸福的。比如“给定来水求发电量最大”这种问题用动态规划或者线性规划就能解得很漂亮。但真实的水光互补调度从来没有只关心一件事。从电站运营方角度看当然希望整个调度周期内的总发电量最大多发一度是一度这直接关系到经济效益。从电网调度角度看联合出力最好平稳不要忽高忽低否则调频压力全甩给火电甚至可能被迫拉闸。从新能源消纳角度看弃光率是硬指标光伏发出来的电尽量全额上网尤其这几年“双碳”背景下弃光率直接跟考核挂钩。问题来了这三个目标在很多工况下是冲突的。想发更多电水电可能需要在光伏大发时也满发但此时系统总出力可能超过输电通道极限或者负荷需求反过来又得弃光。想让出力平稳就得让水电承担大量基荷但这样到了晚间光伏为零时水库可能已经没水了联合出力照样撑不住。想在光伏大发时段全额消纳水电就得大幅压减出力甚至停机发电量目标必然受损。这种“按下葫芦浮起瓢”的局面正是多目标优化要处理的典型场景。不存在一个方案同时让三个目标都达到最优只存在一组互有取舍的折中方案这个集合就是Pareto最优解集。1.3 数学上怎么看这个问题把前面这些思路落到数学表达上水光互补优化调度可以概括成决策变量水电站各时段的出力或发电流量通常取调度周期内的离散时段常见的是24时段每小时一个点或96时段每15分钟一个点。目标函数比如总发电量最大、弃光率最小、出力波动最小目标函数一般至少两个以上。等式约束水量平衡方程即水库时段末库容等于时段初库容加上来水减去发电流量和弃水。不等式约束库容上下限、发电流量上下限、水电站出力上下限、光伏实际消纳出力不超过预测出力等等。求解这样一个模型目标就是找到一组决策变量使其满足所有约束同时让各个目标函数在Pareto意义下达到最优。传统方法里可以用加权法把多目标压成单目标但权重怎么定是个大坑——不同目标量纲不同权重拍脑袋定完算出来的往往只是某一端的极端解。所以我最终选择用NSGA-II这类多目标进化算法直接把整个Pareto前沿搜出来方案取舍交给调度人员做。2. 非支配排序遗传算法为什么多目标求解绕不开NSGA-II2.1 单目标求解 vs 多目标求解单目标优化大家很熟比如“给定成本参数求运行成本最小的调度方案”这时候目标函数是一个标量收敛方向唯一。梯度下降、粒子群、遗传算法都能干区别只是效率和精度。多目标优化就不一样了。目标函数变成了向量比如[发电量, 弃光率, 波动方差]向量与向量之间怎么比较大小直接没法比。假设方案A发电量1000MWh、弃光率5%方案B发电量980MWh、弃光率3%A发电量高但弃光也高B发电量低但弃光更低。你没法说A比B好还是B比A好只能说二者互不支配。Pareto支配的定义是如果方案X在所有目标上都不比方案Y差并且至少在一个目标上严格优于方案Y那么X支配Y。如果X和Y互相都不支配对方那它们就同属于Pareto前沿。多目标优化的目标就是找到尽可能接近真实前沿且分布均匀的一组非支配解。这一点跟水光互补调度是天然契合的。调度员不需要一个“唯一答案”他需要看到一组不同偏向的方案偏向发电量的方案长什么样偏向平稳的方案长什么样偏向消纳的方案长什么样然后在电网实际情况里挑一个当下最合适的。2.2 NSGA-II的三大核心机制NSGA-II全称Non-dominated Sorting Genetic Algorithm II是Kalyanmoy Deb在2000年提出的。它能成为多目标进化算法里最经典的算法靠的是三个核心机制。第一个是快速非支配排序。每一代种群生成后算法对种群里的个体两两比较支配关系把不被任何个体支配的那一批个体划为第一层级Rank 1然后把它们从种群中剔除再在剩余个体中找不被支配的划为第二层级以此类推。层级越靠前说明这个解在种群中的“综合质量”越高优先进入下一代。第二个是拥挤度距离。光按层级选还不够如果同一层级的个体扎堆在一个小区域种群的多样性就差了容易收敛到局部前沿。拥挤度距离的计算方法是对同一层级的个体按每个目标上的取值排序计算相邻个体在每个目标上的归一化距离之和。这个值越大说明这个个体周围越空旷值得保留越小说明它周围挤满了邻居同类方案太多了优先级往后放。第三个是精英保留策略。每一代进化算法先把父代种群和子代种群合并在这个2N规模的集合上做非支配排序和拥挤度计算然后按层级从优到劣依次填充新的N个个体。精英保留意味着父代里优秀的个体不会因为交叉变异被轻易丢掉算法收敛性有保证。除了这三个机制NSGA-II的遗传算子依然沿用了经典遗传算法二元锦标赛选择、模拟二进制交叉SBX、多项式变异。选择过程先随机抽两个个体比较层级层级低者胜出层级相同比较拥挤度拥挤度大的胜出。交叉算子让两个父代个体交换信息产生两个子代变异算子以较小概率扰动个体基因避免种群早熟。2.3 与其他多目标算法的取舍NSGA-II不是唯一的多目标进化算法也说不上所有场景里都最强但它非常适合水光互补调度这类问题。MOEA/D基于分解的多目标进化算法思路是把多目标分解成多个单目标子问题用相邻子问题之间的信息共享来搜索。它的收敛速度在某些问题上确实快但需要分配权重向量权重分布对结果影响很大写代码的时候要额外小心。SPEA2用强度Pareto来评价个体环境选择时用聚类方法保证多样性在小规模问题上效果很细腻但计算复杂度偏高种群一大跑起来明显比NSGA-II慢。NSGA-II的优势在于参数直观无非就是种群规模、代数、交叉率、变异率、代码结构清晰、两三目标的问题上效果稳定。水光互补调度典型的决策变量是几十维时段数目标数是2到3个约束以线性居多正好落在NSGA-II的舒适区。3. 优化模型构建目标函数与约束不能拍脑袋3.1 决策变量与编码方式设计在我这个项目里调度周期取24小时每个时段1小时所以决策变量就是水电站在这24个时段里的出力值写成向量形式是 P_hydro [P_h1, P_h2, ..., P_h24]取值范围在电站最小技术出力到装机容量之间。可能有朋友会问为什么不直接把水库各时段的库容作为决策变量我的经验是库容和出力之间存在非线性关系包含水头变化的影响编码和解码都要多绕一层而且库容约束做个最小最大限制还好但牵涉到流量、弃水、水头损失时非常绕。直接以出力为决策变量约束表达清晰交叉变异操作的对象也直观唯一要处理的坑是水量平衡校验——出力定了之后对应的发电流量要能被水量平衡方程接受如果库容越界就惩罚。这里还有一个细节染色体编码用实数编码而不是二进制编码。二进制编码在离散优化问题里很常见但24维连续决策变量用二进制编码染色体长度会爆炸而且相邻数值之间的汉明距离可能很大变异一次跳变太猛搜索效率低下。实数编码配合SBX交叉和多项式变异每一维基因就是一个小数物理意义就是某时段的出力MW写代码、看结果都直观。3.2 三个典型目标函数设计我这个模型选了三个目标分别对应运营方、电网、政策考核三个视角目标一调度期内系统总发电量最大。联合出力 P_total(t) P_hydro(t) P_pv_use(t)其中 P_pv_use(t) 是光伏实际被系统消纳的出力它不超过光伏预测出力 P_pv_pred(t)。目标函数就是 sum(P_hydro(t) P_pv_use(t))时间从1到24。这个值越大越好用NSGA-II是求最小化所以取负号。目标二弃光率最小。弃光量是 sum(P_pv_pred(t) - P_pv_use(t))把它除以总预测光伏电量就是弃光率。弃光率做目标比弃光量做目标更公平不然光伏装机规模不同没法横向对比。这个目标天然跟目标一冲突多发光伏电就少发水电但总发电量可能因为水电让路而下降。目标三联合出力波动最小。这里用相邻时段出力差的平方和来衡量也就是 sum((P_total(t1) - P_total(t))^2)。这个值越小联合出力越平稳对电网调频越友好。它同样跟目标一冲突——想发电量最大水电往往会在高电价或者光伏低谷时段满发出力曲线自然波动大。三个目标的量纲完全不同一个是MWh一个是百分比一个是MW平方。NSGA-II的比较只关心“谁大谁小”不关心绝对数值所以不需要做归一化。这点跟加权法不一样也是我倾向于用多目标算法的重要原因。3.3 核心约束与惩罚处理约束条件是这个模型里最容易被初学者忽视的部分但它恰恰决定了算出来的方案能不能实际用。水量平衡约束是最核心的等式约束V(t1) V(t) Inflow(t) * delta_t - Q_turbine(t) * delta_t - Q_spill(t) * delta_t。其中 V 是库容Inflow 是来水Q_turbine 是发电流量Q_spill 是弃水流量delta_t 是时段长度1小时。发电流量跟出力之间的关系近似为 P_hydro 9.81 * eta * Q_turbine * H式中 eta 是机组效率H 是水头。为了简化很多初版模型把水头当成常数但这时候要知道这只是近似真实项目里水头随库容变化最好做成查表或者插值。不等式约束包括库容约束V_min V(t) V_max任何时候不能突破。出力约束P_hydro_min P_hydro(t) P_hydro_max。发电流量上下限Q_min Q_turbine(t) Q_max。弃水流量非负Q_spill(t) 0。光伏消纳约束0 P_pv_use(t) P_pv_pred(t)。约束处理我采用罚函数法。每个约束违反量乘以一个惩罚系数加到三个目标函数值上。比如库容越界就把越界量乘一个大系数加进所有目标里这样这个解虽然参与竞争但会因为目标值被拖得很差而被自然淘汰。惩罚系数的大小非常关键。系数太小大量不可行解混进种群前沿形状会被污染系数太大任何一点轻微越界都被重罚种群很容易过早收敛到可行区域的边缘丢失一些原本靠近最优的折中解。我的做法是把惩罚系数设置为目标量级的2到3倍然后逐步增大前几代允许种群稍微探索后面代强制净化。4. Python代码实现从数据准备到NSGA-II完整跑通4.1 环境准备与项目结构这个项目用纯Python实现不需要商业求解器也不需要深度学习框架只要保证Python版本在3.8以上装好numpy和matplotlib就能跑。如果你懒得自己写NSGA-II可以额外装一个pymoo库几行代码就能搭出来但为了搞清楚内部原理我建议至少手写一遍核心逻辑。项目文件我建议拆成四个模块data.py生成或读入模拟数据包括光伏出力、来水、电站参数。model.py定义目标函数和约束计算对应之前说的数学模型。nsga2.py实现非支配排序、拥挤度、遗传算子等算法核心。main.py主程序负责初始化种群、进化循环、输出结果。这样拆的好处是改参数不用动算法代码回头拿真实电站数据替换data.py就行。4.2 模拟算例24时段水光数据生成为了让你拿到代码就能跑我用一个简化的模拟算例一个库容调节能力有限的中型水电站配一个50MW的光伏电站。光伏预测出力用一个半波正弦曲线模拟早上6点开始爬升中午12点达到峰值晚上8点归零中间叠加一点随机扰动模拟云层影响。import numpy as np import matplotlib.pyplot as plt np.random.seed(42) T 24 # 时段数每小时一个点 # 光伏预测出力50MW光伏峰值在中午 t_arr np.arange(T) pv_pred 50 * np.maximum(np.sin((t_arr - 6) / 12 * np.pi), 0) pv_pred np.random.normal(0, 1.5, T) # 模拟预测误差 pv_pred np.clip(pv_pred, 0, 50) # 水电站参数 P_hydro_max 60 # MW P_hydro_min 5 # MW eta 0.88 # 机组效率 H 30 # 平均水头简化取常数 g 9.81 # 重力加速度 V_max 300 # 万m3库容上限 V_min 80 # 库容下限 V0 180 # 初始库容 # 来水单位 m3/s假设在5~20之间波动 inflow np.array([8, 7, 6, 6, 7, 8, 9, 12, 15, 16, 17, 18, 19, 18, 16, 15, 13, 11, 10, 9, 8, 8, 7, 6])这段代码生成的光伏曲线和来水序列虽然粗糙但已经足够让算法跑出有意义的Pareto前沿。实际项目中这里的pv_pred应该替换成光伏电站的短期预测数据inflow替换成水文预报数据电站参数改成实际机组参数。4.3 NSGA-II核心代码实现接下来是重头戏NSGA-II的核心函数。我按之前说的三大机制逐一实现。先看个体类的定义和种群初始化class Individual: def __init__(self, decision_vars): self.x decision_vars # 决策变量长度T self.objectives None # 目标值长度3 self.rank None # 非支配层级 self.crowding 0 # 拥挤度距离 self.constraint_violation 0.0 # 约束违反总量 def initialize_population(pop_size, T): population [] for _ in range(pop_size): # 在可行出力范围内随机初始化 x np.random.uniform(P_hydro_min, P_hydro_max, T) population.append(Individual(x)) return population然后是非支配排序。这里需要对整个种群做两两比较。经典实现的复杂度是O(M*N^2)M是目标数N是种群规模。种群规模几十到几百时完全够用不要一上来就写复杂的快速排序优化保持代码可读性更重要def dominates(a, b): 判断a是否支配b所有目标都且至少一个严格 better_or_equal np.all(a b) strictly_better np.any(a b) return better_or_equal and strictly_better def fast_non_dominated_sort(population): n len(population) dominate_set [[] for _ in range(n)] dominated_count [0] * n fronts [[]] for i in range(n): for j in range(n): if i j: continue if dominates(population[i].objectives, population[j].objectives): dominate_set[i].append(j) elif dominates(population[j].objectives, population[i].objectives): dominated_count[i] 1 if dominated_count[i] 0: fronts[0].append(i) population[i].rank 1 k 0 while fronts[k]: next_front [] for i in fronts[k]: for j in dominate_set[i]: dominated_count[j] - 1 if dominated_count[j] 0: next_front.append(j) population[j].rank k 2 k 1 fronts.append(next_front) return fronts[:-1]拥挤度距离计算按每个目标排序后更新def crowding_distance(population, front): m len(population[0].objectives) for ind in front: population[ind].crowding 0 for obj_idx in range(m): front_sorted sorted(front, keylambda idx: population[idx].objectives[obj_idx]) population[front_sorted[0]].crowding float(inf) population[front_sorted[-1]].crowding float(inf) for i in range(1, len(front_sorted) - 1): delta (population[front_sorted[i1]].objectives[obj_idx] - population[front_sorted[i-1]].objectives[obj_idx]) population[front_sorted[i]].crowding delta锦标赛选择、SBX交叉和多项式变异这部分我直接给出简化的常用写法。SBX交叉对实数编码的问题效果很关键它的思想是让子代在父代附近按照一定分布生成分布指数eta_c默认取20分布指数越大子代越靠近父代def tournament_select(population, k2): candidates np.random.choice(len(population), k, replaceFalse) best candidates[0] for idx in candidates[1:]: if population[idx].rank population[best].rank: best idx elif population[idx].rank population[best].rank: if population[idx].crowding population[best].crowding: best idx return population[best] def sbx_crossover(p1, p2, eta_c20): x1, x2 p1.x.copy(), p2.x.copy() T len(x1) child1, child2 np.zeros(T), np.zeros(T) for i in range(T): if np.random.rand() 0.5: if abs(x1[i] - x2[i]) 1e-10: child1[i] x1[i] child2[i] x2[i] else: u np.random.rand() if u 0.5: beta (2 * u) ** (1 / (eta_c 1)) else: beta (1 / (2 * (1 - u))) ** (1 / (eta_c 1)) child1[i] 0.5 * ((x1[i] x2[i]) - beta * (x2[i] - x1[i])) child2[i] 0.5 * ((x1[i] x2[i]) beta * (x2[i] - x1[i])) else: child1[i] x1[i] child2[i] x2[i] child1 np.clip(child1, P_hydro_min, P_hydro_max) child2 np.clip(child2, P_hydro_min, P_hydro_max) return Individual(child1), Individual(child2) def polynomial_mutation(ind, eta_m20, prob0.1): x ind.x.copy() T len(x) for i in range(T): if np.random.rand() prob: u np.random.rand() if u 0.5: delta (2 * u) ** (1 / (eta_m 1)) - 1 else: delta 1 - (2 * (1 - u)) ** (1 / (eta_m 1)) x[i] delta * (P_hydro_max - P_hydro_min) x np.clip(x, P_hydro_min, P_hydro_max) return Individual(x)主循环按精英保留策略执行核心逻辑是每代先生成子代把父代子代合并成2N规模做非支配排序和拥挤度计算再按层级从好到差取前N个def main(): POP_SIZE 100 GENERATIONS 200 MUTATION_PROB 1.0 / T CROSSOVER_PROB 0.9 population initialize_population(POP_SIZE, T) for gen in range(GENERATIONS): # 评估目标 for ind in population: ind.objectives evaluate_objectives(ind.x) offspring [] while len(offspring) POP_SIZE: parent1 tournament_select(population) parent2 tournament_select(population) if np.random.rand() CROSSOVER_PROB: child1, child2 sbx_crossover(parent1, parent2) else: child1, child2 Individual(parent1.x.copy()), Individual(parent2.x.copy()) child1 polynomial_mutation(child1, probMUTATION_PROB) child2 polynomial_mutation(child2, probMUTATION_PROB) offspring.extend([child1, child2]) # 精英保留 combined population offspring[:POP_SIZE] for ind in combined: ind.objectives evaluate_objectives(ind.x) fronts fast_non_dominated_sort(combined) new_population [] for front in fronts: if len(new_population) len(front) POP_SIZE: crowding_distance(combined, front) for idx in front: new_population.append(combined[idx]) else: crowding_distance(combined, front) front_sorted sorted(front, keylambda idx: combined[idx].crowding, reverseTrue) need POP_SIZE - len(new_population) for idx in front_sorted[:need]: new_population.append(combined[idx]) break population new_population # 最后一轮评估并返回前沿 for ind in population: ind.objectives evaluate_objectives(ind.x) fronts fast_non_dominated_sort(population) return [population[i] for i in fronts[0]]这段代码就是完整的NSGA-II骨架。你可能注意到evaluate_objectives函数我还没给出它需要根据模型部分计算三个目标值和约束违反量在实际代码里可以放在model.py中。4.4 结果可视化与Pareto前沿挑选算法跑完把Pareto前沿上的个体画成散点图横轴是总发电量纵轴是弃光率点的颜色表示出力波动指标。你会看到一条明显的折中曲线发电量高的方案往往弃光率也高波动也大想压低波动发电量就上不去。用matplotlib画图的代码很简单front run_nsga2() front_array np.array([ind.objectives for ind in front]) plt.figure(figsize(8, 6)) plt.scatter(-front_array[:, 0], front_array[:, 1], cfront_array[:, 2], cmapviridis) plt.xlabel(总发电量/MWh) plt.ylabel(弃光率) plt.colorbar(label出力波动) plt.show()从Pareto前沿里挑最终方案不要指望算法替你决定。我的习惯是先把前沿解按加权评分排序权重由调度需求决定如果当天系统调频压力大把波动目标权重给高如果光伏消纳任务紧迫把弃光率权重给高。更严谨一点可以让调度员选择参考点用TOPSIS或者投影法选出距离参考方向最近的点。这个后处理环节虽然不属于NSGA-II本身但实际项目里必不可少。4.5 想省事直接用pymoo如果你不想手写NSGA-IIpymoo库是现成的。它把非支配排序、拥挤度、交叉变异都封装好了模型只要继承Problem类并实现_evaluate方法即可。核心代码如下from pymoo.algorithms.moo.nsga2 import NSGA2 from pymoo.problems import Problem from pymoo.optimize import minimize from pymoo.operators.crossover.sbx import SBX from pymoo.operators.mutation.pm import PM from pymoo.operators.sampling.rnd import FloatRandomSampling class WaterSolarScheduling(Problem): def __init__(self): super().__init__(n_var24, n_obj3, xl5, xu60) def _evaluate(self, X, out, *args, **kwargs): f1, f2, f3 [], [], [] for x in X: objectives evaluate_objectives(x) f1.append(objectives[0]) f2.append(objectives[1]) f3.append(objectives[2]) out[F] np.column_stack([f1, f2, f3]) algorithm NSGA2( pop_size100, samplingFloatRandomSampling(), crossoverSBX(prob0.9, eta20), mutationPM(prob0.05, eta20), eliminate_duplicatesTrue ) res minimize(WaterSolarScheduling(), algorithm, (n_gen, 200), seed42)pymoo版本运行速度快代码量少适合项目交付时用手写版适合教学和修改底层逻辑。我个人的建议是先用pymoo跑通流程再用自己的代码逐行对照理解两套都掌握。4.6 参数设置与调优经验NSGA-II参数虽少但每改一个都影响结果。我常用的初始参数见下表参数推荐值说明种群规模POP_SIZE100决策变量24维时100足够变量越多越要加大进化代数GENERATIONS200观察前沿是否还在变如果还在变就加上去交叉概率0.9常规推荐太小搜索能力弱变异概率1/T约等于每代平均每个个体变异一个基因SBX分布指数eta_c20越大子代越接近父代多项式变异指数eta_m20同上锦标赛选择k2常规值k越大选择压力越大调参的经验是前沿覆盖度不好就加大种群前沿过于集中就加大变异概率收敛太慢就加大交叉概率。不要把参数一次改太多一次调一个变量看Pareto前沿的分布变化不然出了问题根本不知道是谁的锅。5. 实操中踩过的坑与排查技巧5.1 Python环境与依赖问题这个项目对Python版本其实不挑3.8以上都行但依赖库之间的版本冲突确实容易坑人。最常见的一个问题是numpy和matplotlib版本不匹配安装新版本numpy之后旧版matplotlib直接报错建议直接用pip install numpy matplotlib --upgrade成套更新。另一个常见问题是matplotlib画图中文乱码。默认字体不支持中文图上的“弃光率”三个字会变成方框。解决方法是在代码开头加两行plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False如果你用的是Linux服务器SimHei字体多半不存在可以换成WenQuanYi Zen Hei或者干脆把标签改成英文省事。5.2 算法不收敛或早熟的几个原因症状一Pareto前沿只分布在很小一片区域而且多代没有明显变化。先检查是不是罚函数系数太大导致种群被死死压在极其苛刻的可行区域边界。症状二前沿分布很散但没有层次看起来像一团乱麻。大概率是目标函数评估出错了比如弃光率计算公式里分母用了预测总量但分子忘了乘以时段长度或者水量平衡检查里时间单位不一致。最容易犯的隐蔽错误是输出前沿时只取了最后一层的个体而前面的代数里可能已经出现了质量非常好的个体但由于精英保留排序的问题被刷掉了。排查方法很简单在每一代记录Rank 1个体的目标值看它是否单调稳定如果中间发生过倒退多半是精英保留的逻辑写错了。5.3 约束处理不当导致大量不可行解我在初版代码里吃过一次亏种群初始化时直接随机生成出力向量很多个体在上游时段把水量用光后续时段虽然出力很小但库容已经跌破下限被罚函数重罚之后整个种群被“不可行个体”占据大半前沿根本形不成。解决办法有两个方向。一是初始化时先做一次启发式可行化按来水顺序逐时段生成出力保证库容不越界。这样做初始种群可行率高算法能快速进入有意义的搜索二是罚函数系数采用自适应策略前1/3代用小系数放探索后面增大系数收紧可行域。后一种方法更具通用性但代码复杂度高一些。5.4 从Pareto前沿到实际调度方案前端算法跑完真正落地时还会有一次选择。Pareto前沿上几十个点有的方案发电量大但是弃光率看着扎眼有的方案弃光率几乎为零但出力波动大得离谱调度员得根据当前电网状态做决策。我建议准备一个简单的多准则决策脚本让使用者输入权重向量发电量权重、弃光权重、波动权重算每个前沿解的加权得分按得分排序后输出前几个方案。如果跟调度员开会就把前沿散点图投影成两个维度打成彩色大图每个点上标注关键指标肉眼选完再用脚本核对。这套流程我用了很多次比丢一堆数据让调度员自己翻要高效得多。5.5 常见问题速查表问题现象可能原因解决办法matplotilb画图报错numpy版本新旧冲突成套升级依赖中文标签乱码缺少中文字体设置中文字体或用英文标签Pareto前沿不铺开种群太小或变异太小加大种群和变异概率前沿突变、层次混乱目标函数里有bug单测目标函数单独画出水量平衡曲线全部是极端解交叉概率过高降低到0.7左右跑得很慢非支配排序O(N^2)开销减小种群或用pymoo加速最终方案无法落地模型漏了输电约束补上联络线功率上下限约束6. 最后说几点个人体会水光互补优化调度这个方向模型和算法都不算新但把两者组合好、真正做到能落地其实挺考验细节。目标函数设计多花一点心思比调算法参数有用得多约束处理做扎实比盲目堆迭代次数更解决问题。我做这个项目最大的体会是不要一上来就追求复杂的梯级水库或者96时段高精度模型先在一个24时段、单库、单电站的小算例上把NSGA-II的每条逻辑跑顺再逐步往真实场景加约束。等代码框架稳定了换数据就只是改参数的事而你不会再被“为什么跑出来全是不可行解”这种问题困住。另外把结果用可交互的方式呈现会给合作方留下极其深刻的印象。我写了一个简单的Streamlit页面调度员可以拖拽权重滑块看Pareto前沿上的推荐方案怎么变一张图就能讲清楚多目标取舍的直观含义。这个后续扩展的思路强烈推荐给大家对项目验收和论文讨论都很有帮助。
返回列表