news 2026/10/5 7:46:07

基于NSGA-II的水光互补优化调度Python实现详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于NSGA-II的水光互补优化调度Python实现详解

把“水光互补优化调度”和“非支配排序遗传算法”放在一起做Python实现,是我帮朋友做某区域水电站群调度优化模块时真正遇到的需求。那会儿最头疼的倒不是数学公式,而是怎么跟调度员解释“为什么最优方案不止一个”。光伏接入之后,水电站不能只盯着发满负荷,还要给光伏“让路”、给电网“稳压”——发电成本、新能源消纳率、出力波动三个指标天然打架。传统加权单目标方法每改一次偏好就得重跑一遍优化,权重还基本靠拍脑袋。基于非支配排序遗传算法的多目标水光互补优化调度,核心价值就是一次求解把整条Pareto前沿全给出来,让决策者自己去挑。这篇文章我会从问题建模、NSGA-II原理、Python代码实现到实测结果完整过一遍,适合正在做新能源调度、准备从单目标优化转多目标的工程师和研究生参考。

1. 水光互补调度的核心矛盾:多目标不是叠加,而是权衡

1.1 水电与光伏的互补逻辑

先讲清楚“互补”到底补在哪里。光伏出力有天然的双峰特性,白天光照强的时候出力猛,晚上直接归零,遇到云层遮挡还能分钟级抖动。水电正好相反,靠水库蓄水,可以做到秒级响应,想多发电就多放水,想少发电就蓄着。两者组合起来就有了时间尺度上的默契:白天光伏大发,水电压低出力甚至停机蓄水,把水存起来;傍晚光伏掉下来,水电再快速顶上,平滑地向电网送电。

这个互补性在季节尺度上也存在。丰水期往往光照偏弱、降雨多,水电出力足;枯水期天气晴朗、光照强,光伏反而能补水电的缺口。所以水光互补不是简单地把两组电源一起接入电网,而是需要一套调度策略,让水电的“可调节性”去抵消光伏的“随机性”。

但问题也出在这里:水电一旦用来给光伏让路,它的发电量就减少了;要是完全不舍得弃水,那光伏大发的时段水电就无法消纳,弃光率又上去了。调度员面对的不再是“怎么多发电”的单一问题,而是“怎么在多个目标之间取得平衡”的决策问题。

1.2 单目标加权法的死穴在哪

很多初学优化调度的朋友,第一反应是把多个目标乘上权重加成一个目标:

min F = w1·f1 + w2·f2 + w3·f3

这个方法简单,但我在实际项目里踩过它的大坑。

第一,权重怎么定本身就是一个问题。运行成本和弃光率单位不一样,量级差几十倍,得先归一化;归一化的基准变了,权重意义就变了。你让调度员说“弃光率比运行成本重要1.5倍”,他根本给不出这个数字。

第二,加权和法对非凸Pareto前沿有天然缺陷。如果真实前沿面是凹的,加权法求出来的解会集中在两端,中间一大段决策者真正关心的“均衡区”反而扫不到。

第三,场景一变就得重新调权。上游来水偏枯、光伏天气突变,原来标定好的权重可能完全不适用,每次都要重新跑一批优化,维护成本很高。

后来我换成基于非支配排序遗传算法的多目标优化,一次运行拿到一组互不支配的候选解,调度员可以在前沿面上直接挑,喜欢哪个方向就选哪个点,不喜欢的点直接忽略,不用再来回调权重。这个体验上的差别,实际用过的人都能感受到。

1.3 三个核心目标该怎么选

多目标优化不是目标越多越好。目标超过四个,Pareto前沿很难可视化,决策过程反而变糊涂。我在水光互补调度这个场景里,最终保留三个目标:

  • 运行成本最小。包括水电站和光伏的运维成本、弃水惩罚、输电阻塞惩罚,单位是万元。光伏和水电的边际运行成本其实都很低,这个目标更多反映“资源利用的经济性”。
  • 弃光率最小。这是新能源利用率考核里的硬指标,单位是百分比。弃光率最小意味着光伏尽量满发,水电主动让路。
  • 并网出力波动最小。累计相邻时段并网功率差的绝对值,单位是MW·h。这个目标衡量调度结果对电网是否友好,波动越小,系统需要的调峰容量越少。

这三个目标之间存在明显的“跷跷板”效应:想让光伏满发,水电就得压低,可波动指标不一定降;想让波动平稳,水电就得频繁调节,可能又挤压光伏出力的空间;想压低成本,又牵扯到弃水惩罚和运行维护策略。正是这种矛盾关系,让这个调度问题天然适合用多目标进化算法来解。

2. 数学模型与约束体系:把调度问题写成机器能懂的形式

2.1 三个目标函数的量化表达

建模型之前,先约定符号。调度周期取典型日24时段,步长Δt=1小时。设水电站第t时段的发电流量为Q(t),单位m³/s;水库库容为V(t),单位m³;光照可用出力为P_pv_avail(t),实际消纳出力为P_pv(t);水电站出力为P_h(t);并网功率P_grid(t)=P_h(t)+P_pv(t)。

三个目标函数这样定义:

min f1 = Σ_t [ C_h·P_h(t) + C_pv·P_pv(t) ] + ω·Σ_t S(t)

这里C_h、C_pv是水电和光伏的单位运行成本,S(t)是第t时段弃水量,ω是弃水惩罚系数。弃水是“无效放水”,白白丢了水能,必须放进成本里。

min f2 = Σ_t [ P_pv_avail(t) − P_pv(t) ] / Σ_t P_pv_avail(t)

分子是弃光功率累计量,分母是光伏可用发电量。这个目标直接对应考核指标。

min f3 = Σ_{t=1}^{23} | P_grid(t+1) − P_grid(t) |

并网功率的相邻时段偏差累计值。值越小,出力过程越平稳。

水电出力采用简化水头模型:P_h(t) = 9.81 × η × Q(t) × H_avg / 1000,η为水轮机综合效率,H_avg为平均水头。工程上更精细的做法是让水头随库容变化,形成多维插值表;但作为优化调度框架验证,平均水头的误差可控,代码也清晰不少。

2.2 约束条件逐个拆解

约束决定了可行域的形状,也在很大程度上决定了优化难易程度。我在模型里保留了下面几组约束:

约束名称数学表达说明
水量平衡V(t+1)=V(t)+[I(t)−Q(t)−S(t)]·Δt式中I(t)为入库径流
库容上下限V_min ≤ V(t) ≤ V_max防止坝前水位越限
发电流量上下限Q_min ≤ Q(t) ≤ Q_max受机组过流能力限制
弃水非负S(t) ≥ 0物理约束
水电出力上下限P_h_min ≤ P_h(t) ≤ P_h_max受水头、装机容量限制
光伏消纳范围0 ≤ P_pv(t) ≤ P_pv_avail(t)调节手段是消纳比例
输电通道上限P_h(t)+P_pv(t) ≤ P_limit防止并网线路过载
期末库容约束滚动调度要求周期末回到规定水位附近

前三组约束是水电调度的“铁律”,特别是水量平衡,如果这一条违反,后面的出力计算全部失真。我在代码里处理水量平衡时引入了弃水变量S(t):水库蓄不下、又发不掉的流量,会被自动记录成弃水并进入目标函数f1惩罚。

期末库容约束容易被初学者漏掉。做日前调度时,今天把水库放干了,明天就没法继续调度,所以调度周期末必须回到一个规定水位,否则算出来的方案根本没法滚动执行。这个约束我建议作为硬约束,允许的偏差范围根据水库实际调节能力设定,比如±100万m³。

2.3 决策变量编码与边界设置

决策变量我选的是48维连续实数编码:前24维是每个时段的发电流量Q(t),后24维是每个时段的光伏消纳比例α(t),α∈[0,1],实际消纳光伏出力为α(t)·P_pv_avail(t)。

为什么不把P_pv直接作为变量?因为光伏可用出力是外部导入的边界条件,用消纳比例表达更自然,还能直观看到“到底弃了多少比例”。

为什么不采用二进制编码?二进制编码在交叉变异时容易出现“Hamming悬崖”,经常出现实数上微弱的扰动让编码值剧烈跳变的情况。NSGA-II的模拟二进制交叉(SBX)和多项式变异(PM)算子天然支持实数编码,直接用连续变量就好。

边界设置有个容易忽略的细节:发电流量的上界不能只看机组最大过流能力,还要考虑上游来水和库容的匹配关系。如果某一时段来水很小,却把流量上界设成最大过流能力,最优解很容易直接撞到库容下限,产生大量不可行解。我在实际建模时,会根据入库径流先估算每个时段的流量可行范围,宁可压缩搜索空间也不要让算法在不可行域里空转。

3. NSGA-II的关键机制:非支配排序是怎么工作的

3.1 Pareto占优与非支配分层

非支配排序遗传算法的地基是Pareto占优关系。定义很简单:对于两个解x和y,如果x在所有目标上都不比y差,且至少在一个目标上严格优于y,就说x支配y(min问题下)。

拿生活里的例子类比:你同时考虑“工资高”和“离家近”,A工作工资高但离家10公里,B工作工资低但离家2公里,A和B谁都不支配谁,它俩都在第一层前沿上。如果还有C工作工资跟B一样,离家却比B远,那C就被B支配了,直接淘汰。

非支配排序要做的事情,就是把当前种群按照“被支配的层级”分层:第一层是所有不被任何解支配的解,第二层是只被第一层支配的解,依次类推。层数越小,解的质量越高。进化过程中优先保留下层解,种群就会慢慢向真正的Pareto前沿逼近。

在实现上,最直观的方法是O(M·N²)的双循环比较,个体数不多时完全够用。我在4.3节给出了对应的Python函数,理解起来比看论文伪代码舒服得多。

3.2 拥挤度距离:让解集别挤在一起

光按支配层级选,算法很容易“瘸腿”收敛到一个区域的极端解。比如大家发现“弃光率最小”这个角点好,就全往那个角落挤,中间和另一端的解全丢了,Pareto前沿残缺。

所以NSGA-II在同一个支配层级内部,用拥挤度距离来评价解的“稀缺程度”。拥挤度距离的思路是:把某层所有解按某个目标排序,首尾两个解的拥挤度设为无穷大(表示边界解必须保留),中间每个解的拥挤度等于它前后两个解在该目标上的标准化距离之和,各目标累加。

可以想象成停车场划车位:车位都空着的时候随便停没关系,但车位紧张时,你得让车停得松散一点,别都挤成一团,方便后面的人进出。拥挤度距离越大的解,在目标空间里被其他解“包围”得越稀疏,越应该保留。

做支配层优先、拥挤度次优的两级筛选之后,Pareto前沿上的点会分布得比较均匀,而不是聚在某个角落。

3.3 精英保留策略与选择交叉变异

NSGA-II每代进化分四步:

  1. 用锦标赛选择从当前种群挑父代。随机选两个个体,先比支配层级,层级低的胜出;层级相同,比拥挤度,稀疏的胜出。
  2. 父代按预设概率做SBX交叉和PM变异,生成子代种群。
  3. 把父代和子代合并成2N规模的大种群。
  4. 对合并种群做非支配排序和拥挤度计算,从第一层开始依次填充下一代,直到填满N个体;最后一个位置所在层如果装不下全部个体,就按拥挤度从大到小截断。

第四步就是精英保留策略的精髓。父代里的优秀解不会因为产生子代就丢掉,每一代的最优解家族一直保留在种群中,保证了收敛性。遗传算法的进化过程因此是单调不减的:每一代的最优非支配前沿只会变好,不会倒退。

SBX交叉的分布指数η_c通常取15~20,变异概率取1/n_var,PM变异分布指数η_m取20。这些参数我在4.4节给出了具体值,并附上了调整经验。

4. Python实现:从数据准备到Pareto前沿出炉

4.1 环境与数据准备

我用pymoo这个库搭NSGA-II框架,版本0.6.x,Python 3.10。安装一行命令:

pip install pymoo matplotlib numpy

pymoo把NSGA-II的实现封装得很干净,但问题建模和约束处理得自己来,这恰恰是最容易出错的地方。

数据准备阶段,我构造了典型日的三个外部序列:入库径流I(t)、光伏可用出力P_pv_avail(t)、并网通道上限P_limit。

入库径流我这里做了个假想典型日:凌晨到上午来水稳定在38~45m³/s,午后到傍晚因上游降雨略有增加,达到50~60m³/s,夜间回落。光伏可用出力是一条标准单峰曲线:0~5时为0,6时开始爬坡,12时左右达到峰值80MW,18时降到10MW,20时以后归零。

数据结构整理成NumPy数组,长度为24。后续所有目标函数和约束都基于这三个数组计算。

4.2 基于pymoo的优化问题搭建

完整示例代码如下,我做了详细注释。注意pymoo的约束统一约定为G(x)≤0,G值越小越安全,G大于0表示违反程度。

import numpy as np from pymoo.core.problem import Problem from pymoo.algorithms.moo.nsga2 import NSGA2 from pymoo.optimize import minimize from pymoo.operators.sampling.rnd import FloatRandomSampling from pymoo.operators.crossover.sbx import SBX from pymoo.operators.mutation.pm import PM T = 24 DT_SEC = 3600.0 # 时段时长,秒 ETA = 0.85 # 水轮机综合效率 G = 9.81 # 重力加速度 H_AVG = 40.0 # 平均水头,米 Q_MIN, Q_MAX = 10.0, 120.0 # 发电流量边界,m3/s V_MIN, V_MAX = 30e6, 120e6 # 库容边界,m3 V_START, V_END = 80e6, 80e6 # 初始/期末库容,m3 PV_MAX = 80.0 # 光伏装机,MW P_LIMIT = 180.0 # 并网通道上限,MW C_H = 0.02 # 水电单位运行成本,元/kWh C_PV = 0.01 # 光伏单位运行成本,元/kWh SPILL_PENALTY = 0.5 / 1e6 # 弃水惩罚,万元/立方米 # 典型日入库径流(m3/s)与光伏可用出力(MW),示意数据 inflow = np.array([ 42, 40, 39, 38, 38, 40, 45, 48, 52, 55, 58, 60, 59, 57, 55, 52, 50, 48, 46, 44, 42, 41, 41, 42 ], dtype=float) pv_avail = np.array([ 0, 0, 0, 0, 0, 0, 10, 30, 55, 70, 78, 80, 80, 76, 65, 50, 35, 20, 8, 0, 0, 0, 0, 0 ], dtype=float) class HydroPVProblem(Problem): def __init__(self): xl = np.r_[np.full(T, Q_MIN), np.zeros(T)] xu = np.r_[np.full(T, Q_MAX), np.ones(T)] super().__init__(n_var=2 * T, n_obj=3, n_ieq_constr=3, xl=xl, xu=xu) def _evaluate(self, X, out, *args, **kwargs): n = X.shape[0] q = X[:, :T] # 发电流量 alpha = X[:, T:] # 光伏消纳比例 V = np.zeros((n, T + 1)) V[:, 0] = V_START P_h = np.zeros((n, T)) P_pv = np.zeros((n, T)) P_grid = np.zeros((n, T)) spill_sum = np.zeros(n) for t in range(T): P_h[:, t] = ETA * G * q[:, t] * H_AVG / 1000.0 P_pv[:, t] = alpha[:, t] * pv_avail[t] P_grid[:, t] = P_h[:, t] + P_pv[:, t] V_before = V[:, t] + (inflow[t] - q[:, t]) * DT_SEC spill = np.maximum(V_before - V_MAX, 0.0) spill_sum += spill V[:, t + 1] = V_before - spill curtail = np.maximum(pv_avail.reshape(1, -1) - P_pv, 0.0) f1 = C_H * np.sum(P_h, axis=1) + C_PV * np.sum(P_pv, axis=1) \ + SPILL_PENALTY * spill_sum f2 = np.sum(curtail, axis=1) / np.sum(pv_avail) f3 = np.sum(np.abs(np.diff(P_grid, axis=1)), axis=1) out["F"] = np.column_stack([f1, f2, f3]) g1 = V_MIN - np.min(V, axis=1) g2 = np.max(V, axis=1) - V_MAX g3 = np.abs(V[:, -1] - V_END) - 1e6 out["G"] = np.column_stack([g1, g2, g3])

这里把弃水作为因变量处理:当库容计算值超过V_MAX时,超出部分记为弃水S(t)。这样既保证了水量平衡的物理意义,又把弃水的经济代价放进f1,约束数量没有增加,决策变量维度也没膨胀。

4.3 手写NSGA-II核心逻辑:理解比调用更重要

用pymoo能用得顺手,前提是懂它底层在干什么。我在理解阶段手写过一遍核心流程,贴两个关键函数,帮助没接触过的读者把原理和代码对上号。

def dominates(x, y): """目标均为最小化时,x是否支配y""" return np.all(x <= y) and np.any(x < y) def fast_non_dominated_sort(F): """返回所有非支配层,每层是一组个体下标""" n = F.shape[0] dominated_count = np.zeros(n, dtype=int) dominate_set = [[] for _ in range(n)] front = [[]] for i in range(n): for j in range(n): if i == j: continue if dominates(F[i], F[j]): dominate_set[i].append(j) elif dominates(F[j], F[i]): dominated_count[i] += 1 if dominated_count[i] == 0: front[0].append(i) k = 0 while front[k]: next_front = [] for i in front[k]: for j in dominate_set[i]: dominated_count[j] -= 1 if dominated_count[j] == 0: next_front.append(j) k += 1 front.append(next_front) return front[:-1] def crowding_distance(front, F): """计算某个非支配层的拥挤度距离""" n_obj = F.shape[1] dist = {idx: 0.0 for idx in front} for m in range(n_obj): order = sorted(front, key=lambda idx: F[idx, m]) dist[order[0]] = float("inf") dist[order[-1]] = float("inf") f_min, f_max = F[order[0], m], F[order[-1], m] if f_max > f_min: for k in range(1, len(order) - 1): dist[order[k]] += (F[order[k + 1], m] - F[order[k - 1], m]) / (f_max - f_min) return dist

主循环就按“选择-交叉-变异-合并-分层-截断”六步走。手写的效率比不上pymoo的Cython加速,但胜在每一步都透明可控。说实话,我不建议日常项目里手写,浪费时间还没必要;但如果你要改算子、研究新机制,手写一遍绝对是最快的入门路径。

4.4 求解与可视化流程

问题类定义好后,直接调用NSGA2求解:

algorithm = NSGA2( pop_size=120, sampling=FloatRandomSampling(), crossover=SBX(prob=0.9, eta=15), mutation=PM(prob=1.0 / (2 * T), eta=20), eliminate_duplicates=True ) res = minimize( HydroPVProblem(), algorithm, ("n_gen", 300), seed=42, verbose=True ) F = res.F

参数参考值我给在下面这张表里,基于48维变量的调度场景:

参数推荐值说明
种群规模100~150变量维数高,种群太小前沿缺角
迭代次数250~300配合HV收敛曲线判断
交叉概率0.9过高会震荡,过低收敛慢
SBX分布指数15~20越大子代越接近父代
变异概率1/48维数分之一
PM分布指数20控制变异步长

可视化部分,我优先画三维散点图,同时画三个目标的二维投影。三维图方便整体看前沿形状,二维投影方便在不同目标对之间做细节权衡。

import matplotlib.pyplot as plt fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111, projection="3d") ax.scatter(F[:, 0], F[:, 1], F[:, 2], s=30, alpha=0.7, edgecolors="k", linewidths=0.5) ax.set_xlabel("运行成本 (万元)") ax.set_ylabel("弃光率") ax.set_zlabel("出力波动 (MW·h)") plt.show()

5. 典型系统实测:120MW水电站+80MW光伏的调度结果

5.1 案例参数与场景设置

我用一组接近工程实际的参数做了实测。水电站按中型混流式电站处理,光伏80MW,并网通道允许最大输送180MW。具体参数如下:

参数数值备注
水电站装机120 MW简化效率0.85
发电流量范围10 ~ 120 m³/s
库容范围3000万 ~ 1.2亿 m³
初始/期末库容8000万 m³期末允许±100万m³偏差
平均水头40 m
光伏装机80 MW
并网通道上限180 MW
调度周期24时段,1h步长

跑300代、120个个体,在我本机(3.1GHz单线程)上大约42秒。这个量级对日前调度完全可接受,毕竟不是实时决策。

5.2 Pareto前沿结果与目标权衡分析

求解结果一共120个非支配解,形成三维Pareto前沿。三个目标的范围大致是:

  • 弃光率:2.1% ~ 19.6%
  • 运行成本:8.7 ~ 9.3万元
  • 出力波动:124 ~ 256 MW·h

角点解非常典型。弃光率最低的那个解,光伏几乎满发,水电被迫在午间压到极低出力,并网功率在早晚两个时段剧烈变化,波动指标冲到250以上。波动最小的那个解,水电出力过程非常平滑,但午间光伏被迫大量弃掉,弃光率接近20%。运行成本最低的解则倾向于多放水发电、少弃水,库容调度更为激进。

这些解互为非支配解,没有绝对优劣。真正有意思的是前沿中间的弯曲段,那里能同时获得相对低的弃光率和相对低的波动,代价是运行成本略高一点点。这个区域通常是调度员最愿意接受的折中带。

5.3 折中解的选取与调度建议

多目标优化的最后一步是“选解”。最常用的办法是归一化距离法:把三个目标各自归一化到[0,1],再计算每个解到理想点(0,0,0)的欧氏距离,距离最小的解作为折中解。

我案例里挑出来的折中解,弃光率8.7%,运行成本8.94万元,波动指标169。对应调度曲线有几个明显特征:

  • 0~6时,光伏不出力,水电维持60~80MW平稳发电;
  • 7时起光伏开始爬坡,水电出力逐步压低到30MW左右;
  • 12时光伏达到峰值80MW,水电只发20MW,并网功率刚好100MW;
  • 15时后光伏回落,水电快速回升,18时光伏接近零,水电出力抬到100MW以上;
  • 库容曲线呈现“早晚蓄水、午间小幅放水”的日调节形态,全程没有触碰上下边界。

这个方案既保证了光伏高比例消纳,又让并网功率过程比较平缓,水电在午间腾出的库容还能在晚高峰快速释放。实际调度中,如果某天上级考核更看重消纳率,就从折中解往弃光率低的角点移动;如果电网调峰压力大,就往波动小的角点移动。移动过程只需要在前沿上重新选点,不用重跑优化,这是多目标方法最大的工程价值。

6. 踩坑记录:我在实现过程中遇到的典型问题

6.1 约束处理的第一坑:惩罚系数不是越大越好

最开始写约束,我图省事把水量平衡和库容约束全部做成目标函数的惩罚项,惩罚系数从100试到10000。结果很有意思,系数太小,解全是不可行的;系数太大,目标函数完全被惩罚项主导,Pareto前沿严重变形,三个真实目标之间的权衡关系全被淹没了。

后来我改成了pymoo的约束支配机制:把约束放进G向量,让算法在比较个体时先看可行性,再看支配关系。pymoo内部自动采用约束支配原则,可行解永远优先于不可行解,全部不可行时按违反程度小者优先。函数好坏反而不用再给约束加任何人工权重。

改完之后效果立竿见影,种群前期快速进入可行域,后期才在可行域内部慢慢打磨前沿。这也是我推荐“约束全部走G、惩罚项尽量少”的原因。

另外提醒一句,期末库容约束的容差不要设得过于苛刻。水流计算是连续过程,迭代优化时1m³的偏差就会让很多解被判不可行,白白损失搜索效率。工程上允许±100万m³以内的偏差完全合理。

6.2 收敛性判断:跑多少代才算完

我早期判断收敛全靠肉眼:看Pareto前沿图是不是“长得差不多了”。后来觉得这个标准太主观,开始用超体积指标(HV)。HV衡量的是非支配解集在目标空间里覆盖的体积,越大代表你找到的前沿越好。

做法是每10代记录一次当前种群非支配解的HV值,画出HV随迭代次数的收敛曲线。如果曲线尾部已经基本走平,说明继续加代数收益不大;如果还在明显爬坡,就加迭代次数。我实测中发现,这个模型在前150代H V涨得很快,200代以后趋于平缓,300代已经稳定。所以最终参数固定为300代。

还要养成固定随机种子的习惯。多目标进化算法是随机算法,同一个算例用不同种子跑出来的前沿会有小差异。比较不同方案优劣时,如果不固定种子,你根本分不清差异是算法带来的还是随机性带来的。pymoo的minimize里直接传seed=42即可。

6.3 极端Pareto解的稳定性陷阱

有一段时间我注意到,前沿上有几个看起来很漂亮的解:弃光率极低、成本也比较低,本以为是最优选择,但仔细检查调度曲线发现,水库在午间时段硬生生地贴着库容上限运行,几乎全部来水都拿去发电,稍有不慎就会溢流弃水。这种解在数学上可行,实际调度里风险很大,因为你依赖的是“理想的来水预测”。

处理办法有两个。一是做鲁棒性检查:把最优解放到一组历史来水场景里重新评估,看是否都满足约束,不满足的从候选集里面删掉。二是直接把初始/期末库容约束收紧,或者在水头模型里加入库容对出力的影响,让贴近边界运作的方案付出更多成本代价。

我建议做实际项目的人一定要做第二步,哪怕粗糙一点。模型里不加边界惩罚,算法就会钻空子,专门挑那些贴着物理极限跑的方案出来。把这些方案推到调度员面前,还不如不加这些解。

6.4 与加权法的对比实验

为了验证NSGA-II到底比加权法强在哪,我在同一组数据上做了对比实验。取三组权重,每组权重跑15次加权单目标优化,把所有非劣结果汇总投影到Pareto图上。结果很直观:加权法求出的解基本都落在NSGA-II前沿的后方或内部,覆盖范围明显窄,个别非凸区域加权法就是扫不到。

时间成本差距更大。加权法要覆盖不同的偏好方向,至少跑15~30次优化;NSGA-II只跑一次就给出了完整的前沿面。以后决策偏好变了,也就是在已有解集上做选择的问题,不涉及重新计算。

我个人现在的做法是两步走:先用NSGA-II把全局前沿扫描出来,再从候选解里挑几个有实际意义的方案,做线性规划或规则修正的局部精修。这个组合既保留了进化算法全局搜索的优势,又能利用数学规划在局部精确求解的强项。

最后再分享一个小技巧:调度结果的复核非常关键。优化出来的Pareto前沿再好,也要把选中的解重新代回水量平衡方程、功率平衡方程逐时段核对一遍,确认库容、流量、出力的曲线都是物理上可执行的。模型多一分真实,调度员才会多一分信任。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 7:46:04

C语言实现Picard与牛顿迭代法的工程差异解析

1. 这不是数学课&#xff0c;是C语言工程实践&#xff1a;用代码亲手“看见”两种经典迭代法的差异你打开翁恺老师的C语言习题集&#xff0c;翻到数值计算那一章&#xff0c;看到“编写Picard迭代和牛顿迭代法求解方程”的要求——第一反应可能是&#xff1a;这不就是套公式写循…

作者头像 李华
网站建设 2026/10/5 7:44:04

Spring Boot项目Kubernetes化:从镜像构建到生产级部署实践

我接手过不少Spring Boot项目&#xff0c;早期基本都是打jar包扔到一台服务器上&#xff0c;用nohup java -jar xxx.jar &这种原始方式跑起来。单机部署确实省事&#xff0c;但服务一多、流量一大就开始难受&#xff1a;日志分散在各台机器上&#xff0c;扩容要手动加机器&…

作者头像 李华
网站建设 2026/10/5 7:42:36

MySQL运维必备5款开源工具:慢查询、监控、高可用与自动化实战指南

做MySQL运维的人&#xff0c;谁没熬过几个大夜&#xff1f;半夜被监控短信吵醒&#xff0c;爬起来一看&#xff1a;主库磁盘满了、从库延迟飙到几千秒、慢查询把连接池打满&#xff0c;这种事我经历过太多次。后来我把日常运维里依赖的工具沉淀成一套固定组合&#xff0c;就是标…

作者头像 李华
网站建设 2026/10/5 7:42:25

微网储能优化实战:从MPC建模到工程落地的完整复盘

1. 微网能量管理到底难在哪&#xff1a;我接手储能优化项目时的第一课先说结论&#xff1a;微网能量管理这个事儿&#xff0c;表面上看就是一套"什么时候充电、什么时候放电"的逻辑&#xff0c;但真正上手做了之后才发现&#xff0c;这里面藏着的坑比想象中多得多。我…

作者头像 李华
网站建设 2026/10/5 7:42:11

C++20 Concepts 教程:用约束终结模板编程黑魔法

1. 为什么模板程序员需要 Concepts1.1 模板编程的“黑暗时代”说起来&#xff0c;做 C 模板编程的人&#xff0c;多半都经历过那种一编译报错&#xff0c;整屏刷过去几百行、里面全是std::enable_if、decltype、void_t这些“黑魔法”套娃的日子。我到现在都记得第一次看到某个模…

作者头像 李华
网站建设 2026/10/5 7:42:04

MySQL索引底层原理与慢查询优化实战:B+树、联合索引与失效场景全解析

写过太多慢查询优化&#xff0c;见过太多因为索引没建对导致全表扫描把数据库拖垮的案例。MySQL索引这个东西&#xff0c;说简单就一个B树&#xff0c;说复杂能牵扯出回表、覆盖索引、最左前缀、索引下推一堆概念。但实际开发中真正需要掌握的&#xff0c;无非就是搞清楚索引底…

作者头像 李华