☰
冷热电联供系统多目标优化:NSGA-II代码实战与Pareto前沿分析
2026/10/11 22:58:39 网站建设 项目流程

简介:基于多目标算法的冷热电联供型综合能源系统MATLAB代码与操作视频,面向能源、电气及自动化等相关专业的本硕博学生与教研人员,可作为综合能源系统多目标优化课题的入门模板与实践参考。包内共4个文件,包括两个MATLAB脚本(主程序与适应度计算)、一份txt格式说明文档以及一段avi操作录像,整体压缩后仅160KB。内容围绕燃气成本、向电网购电/售电费用和碳排放费用三类优化目标建模求解,运行环境建议采用MATLAB 2021a及以上版本,主入口为Runme.m,且要求当前文件夹处于工程所在路径;配套操作录像完整演示了从环境准备到启动运行的流程,可有效避免子函数误运行与路径设置失误。已有1697人浏览学习。借助这套代码与录像可快速上手冷热电联供系统的多目标优化方法,理解模型构建、目标函数权衡与求解输出;也可根据研究需要调整目标权重或约束参数,开展不同工况下的对比分析与参数优化。

1. 冷热电联供综合能源系统为什么要靠多目标算法来编排

一个园区能源站要同时应付电、热、冷三种负荷,有余热回收的燃气轮机,有吸收式制冷,有电制冷、储能和屋顶光伏。传统值班方式靠经验或者只盯着运行成本压减,结果经常是成本低了碳排放反而居高不下,或者为了“环保达标”把设备频繁启停搞得寿命缩水。多目标算法的价值,就是一次算出几十、几百个互相冲突的最优解,而不是替你拍一个“所谓最优”的单点答案。这个标题指向一套可运行的冷热电联供综合能源系统优化代码,适合正在做综合能源调度、研究生课题或者设计院规划方案的从业者:看懂模型、会改参数、能复现结果。这类问题的难点从来不是套一个 NSGA-II 库,而是怎么把“冷热电联供”真实场景翻译成目标函数和约束,再让多目标算法在有限时间内收敛得干净。

2. 搭建冷热电联供的数学框架:三个目标与三张平衡表

2.1 设备链路与调度变量的对应关系

冷热电联供系统的典型链路是:天然气进入燃气轮机发电,发电后的高温烟气进入余热锅炉产生蒸汽,蒸汽再驱动吸收式制冷机组制取冷水,同时也能通过换热器供热。电网作为后备电源,电制冷机负责补冷,储能电池收纳多余的电。如果屋顶还有光伏,那整个系统的输入变量就更多了。模型的第一步,是把要调的设备出力全部揉进一个决策向量x里。

设备输入能量输出能量调节变量
燃气轮机天然气电 + 高温烟气发电出力 P_gt(t)
余热锅炉 / 换热器高温烟气热供热功率 H_hx(t)
吸收式制冷机蒸汽/热水冷制冷功率 C_abs(t)
电制冷机电冷制冷功率 C_ec(t)
储能电池电电充/放功率 E_bat(t)
电网交互电电购电功率 P_grid(t)

常见做法是把一天 24 小时的这些变量全部平铺成一维数组,长度是固定的,方便交给遗传算法交叉变异。我一般会定义变量顺序为:燃气轮机出力、购电量、吸收式制冷出力、电制冷出力、储能出力。其中E_bat(t)大于 0 表示放电,小于 0 表示充电。下面的伪代码定义了这个向量结构:

# 变量顺序定义 import numpy as np HOURS = 24 x = np.zeros(5 * HOURS) # [P_gt, P_grid, C_abs, C_ec, E_bat] 逐小时拼接 # 切片索引,后续所有函数都靠这个索引解包 P_gt = x[0:HOURS] P_grid = x[HOURS:2*HOURS] C_abs = x[2*HOURS:3*HOURS] C_ec = x[3*HOURS:4*HOURS] E_bat = x[4*HOURS:5*HOURS]

这段代码里的核心是“用固定切片代替变量名数组”。多目标算法每一次调用目标函数,接收到的就是一个一维向量,模型层必须把这个向量还原成物理上可理解的各设备出力。切片的顺序只要前后一致,算法本身并不关心你内部怎么排,但如果你后面加了一台电锅炉,务必在目标函数、约束函数和初始化函数三处同步修改切片索引,这是最常见的低级翻车点。

2.2 经济、排放、能效:目标函数怎么写才不打架

冷热电联供里最常见的两个优化目标是运行成本和碳排放量,有时还会加一个综合能源利用率。运行成本的构成是:燃气费用加购电费用,如果有余电上网还可以扣掉售电收益,但多数项目里光伏和燃气轮机发的电都优先自用,所以我这里先按纯购电模型写。

成本目标f1:

f1 = sum( gas_price * gas_consumption(t) + grid_price(t) * P_grid(t) )

碳排放目标f2:

f2 = sum( emission_factor_gas * gas_consumption(t) + emission_factor_grid * P_grid(t) )

这里需要解释清楚为什么两个目标会冲突:低谷电价时段,电网购电便宜,但电网电的碳排放因子通常高于天然气;高峰电价时段,燃气轮机发满虽然省电费,却会让天然气消耗和排放在同一时段集中。两个目标天然此消彼长,不存在一个解能让两项同时达到全局最小,所以必须交给多目标优化去生成一组 Pareto 前沿解。

代码里,目标函数不能写成一句话返回标量,而要返回一个向量,给 NSGA-II 做非支配排序用:

def evaluate_objectives(x, data): """ 输入完整调度向量x,返回目标数组 [cost, emission, energy_eff] data 里至少要有: data['load_e'], data['load_h'], data['load_c'] data['price_grid'] # 24小时分时电价 data['gas_price'] data['lambda_grid'] # 电网碳排放因子,kgCO2/kWh data['lambda_gas'] # 天然气碳排放因子 """ HOURS = data['HOURS'] P_gt = x[0:HOURS] P_grid = x[HOURS:2*HOURS] C_abs = x[2*HOURS:3*HOURS] C_ec = x[3*HOURS:4*HOURS] E_bat = x[4*HOURS:5*HOURS] # 燃气轮机耗气量:用发热量折算,单位 m³/h # 燃气轮机发电效率 eta_gt 取 0.35,天然气热值 LHV 取 9.7 kWh/m³ gas_load = P_gt / (data['eta_gt'] * data['LHV_gas']) # 余热供热+吸收式制冷补燃的耗气量,这里简化为与热/冷产出线性相关 gas_heat = data['alpha_h'] * (data['load_h'] - data['eta_hx'] * gas_load * data['eta_recover']) gas_heat = np.clip(gas_heat, 0, None) gas_total = gas_load + gas_heat cost = np.sum(gas_total * data['gas_price']) + np.sum(P_grid * data['price_grid']) emission = np.sum(gas_total * data['lambda_gas']) + np.sum(P_grid * data['lambda_grid']) return np.array([cost, emission])

这段代码有两个容易被忽略的设计:燃气轮机的耗气量不是固定比例,需要按发电效率eta_gt反推;而余热锅炉的补燃只在余热不够供热需求时才发生,np.clip(gas_heat, 0, None)保证了补燃量不会出现负数。如果你抄代码时漏掉这个 clip,目标函数在余热富余时段会出现“负耗气量”,算法会相当兴奋地往那个方向搜索,最后给出的解根本没法落地。

2.3 电、热、冷三张平衡表与出力区间约束

约束条件是冷热电联供模型的骨架。第一类是等式约束,即每一时刻冷热电必须供需平衡。电平衡:

P_gt(t) + P_grid(t) + P_pv(t) + E_bat(t) == load_e(t) + C_ec(t) / COP_ec

热平衡:

H_hx(t) + H_gb(t) == load_h(t)

冷平衡:

C_abs(t) + C_ec(t) == load_c(t)

第二类是不等式约束,比如燃气轮机出力在 30% 到 100% 额定功率之间,储能电池不能同时充放电,储能容量在 0.1 到 0.9 倍额定容量之间。代码实现时不要把这些约束分得太散,统一写成返回违反量的函数,方便后续调试。

def evaluate_constraints(x, data): HOURS = data['HOURS'] P_gt = x[0:HOURS] P_grid = x[HOURS:2*HOURS] C_abs = x[2*HOURS:3*HOURS] C_ec = x[3*HOURS:4*HOURS] E_bat = x[4*HOURS:5*HOURS] violations = [] # 1. 燃气轮机出力上下限 P_gt_min = data['P_gt_max'] * 0.3 violations.append(np.sum(np.maximum(P_gt_min - P_gt, 0))) violations.append(np.sum(np.maximum(P_gt - data['P_gt_max'], 0))) # 2. 网格购电上限 violations.append(np.sum(np.maximum(P_grid - data['P_grid_max'], 0))) # 3. 电平衡:忽略配网网损 balance_e = P_gt + P_grid + data['P_pv'] + E_bat - data['load_e'] - C_ec / data['COP_ec'] violations.append(np.sum(np.abs(balance_e))) # 4. 冷平衡 balance_c = C_abs + C_ec - data['load_c'] violations.append(np.sum(np.abs(balance_c))) # 5. 储能容量约束:简单线性化,认为储能SOC由E_bat积分决定 soc = np.cumsum(E_bat) / data['E_capacity'] soc_min = np.zeros_like(soc) + 0.1 soc_max = np.zeros_like(soc) + 0.9 violations.append(np.sum(np.maximum(soc_min - soc, 0))) violations.append(np.sum(np.maximum(soc - soc_max, 0))) return np.sum(violations)

约束代码的要点是“所有违反量都返回非负数,量纲统一成功率或能量”。如果你的燃气轮机出力违反 20 kW,购电上限违反 5 kW,这两个数直接相加没有物理意义,但作为惩罚项引导搜索方向是够用的。真正要小心的是储能 SOC 约束:np.cumsum(E_bat)假设初始 SOC 为 0,如果你的样例代码里初始 SOC 是 0.5,那么这行代码会让算法把 SOC 整体抬高,最终得到一个“伪平衡”的解。操作视频里通常会强调初始化 SOC 取值,我建议把 SOC(0) 作为外部参数写进data,而不是藏在 cumsum 里。

3. 多目标算法选型与代码工程结构:从NSGA-II到MOPSO

3.1 为什么 NSGA-II 成了这一类问题的事实标准

做冷热电联供优化,你搜到的示例代码八成都是 NSGA-II,紧接着是多目标粒子群 MOPSO。原因很现实:CCHP 调度问题不是单峰函数,非凸、非线性、有大量离散状态(比如储能充电/放电切换),传统的加权求和法在一个权重下只能得到一个点,跑十组权重得到的解分布还非常不均匀。NSGA-II 用非支配排序加拥挤度距离,一次种群迭代就能把 Pareto 前沿推出来,而且实现代码相对短,容易改造成自己的模型。

MOPSO 在连续变量问题上收敛速度更快,但冷热电联供里面有不少逻辑判断,比如“吸收式制冷在余热不足时切换补燃”,这种条件分支会让粒子群的速度更新变得混乱。我的习惯是:第一版先用 NSGA-II 跑通模型,确认目标函数和约束没有逻辑错误,再在这个骨架上对比 MOPSO。不要一上来就上一套复杂混合算法,模型错了,再怎么优化也是自欺欺人。

3.2 一套能复现的代码结构:数据、模型、算法、画图四个模块

标题带“代码操作视频”,观众最反感的是视频里从一个完整工程里随便抽几个函数讲,文件依赖关系一团乱麻。我一般会按四个目录组织代码,复制到本地就能跑:

cchp_multiobjective/ ├── data/ │ ├── load.csv # 24h 电/热/冷负荷,三列 │ └── price.csv # 24h 分时电价,一列 ├── models/ │ ├── cchp_model.py # 目标函数和约束条件 │ └── device_params.py # 所有设备的效率、容量、排放因子 ├── solvers/ │ ├── nsga2.py # NSGA-II 主体 │ └── mopso.py # 可选的多目标粒子群 ├── results/ │ └── pareto_solutions.csv # 输出Pareto解集 └── run_case.py # 主入口,一键运行

这个结构遵循一个原则:算法层不感知设备参数,模型层不感知优化算法。cchp_model.py只负责“给定 x,返回目标和约束违反量”,nsga2.py只负责“给定种群,返回新一代种群”。改燃气轮机效率时只动device_params.py,换算法时只动solver目录,不会牵一发动全身。

主入口run_case.py只需要二十行左右:

import numpy as np from models.device_params import get_default_data from models.cchp_model import evaluate_objectives, evaluate_constraints from solvers.nsga2 import run_nsga2 data = get_default_data() pop_size = 100 max_gen = 200 final_pop, final_obj = run_nsga2( pop_size=pop_size, max_gen=max_gen, n_vars=5 * data['HOURS'], data=data, evaluate_objectives=evaluate_objectives, evaluate_constraints=evaluate_constraints ) np.savetxt("results/pareto_solutions.csv", final_pop, delimiter=",") np.savetxt("results/pareto_objectives.csv", final_obj, delimiter=",") print("Pareto solutions saved:", final_pop.shape)

这段代码的参数含义非常直白:pop_size=100表示每一代保留 100 条调度方案,max_gen=200表示进化 200 代,n_vars是决策变量总数。对于 24 小时的模型,5 个设备变量乘以 24 刚好是 120 个变量。如果算力紧张,甚至可以砍到pop_size=60, max_gen=100,先验证代码有没有 bug,最后再跑正式大种群。

3.3 NSGA-II 在冷热电场景里的三个必调参数

很多示例代码给出的 NSGA-II 交叉概率、变异概率是拍脑袋写的,丢到 CCHP 问题上往往前 50 代就收敛到某个甜点,Pareto 前沿只稀稀拉拉几个点。以下是我在这个场景里的经验数值:

参数常见默认值CCHP 建议值原因
种群规模5080-120变量数超过100,种群太小覆盖不住完整可行域
交叉概率0.90.85-0.95保持搜索强度,但不要过于剧烈破坏已可行个体
变异概率1/n_vars0.05-0.15CCHP变量区间差异大,固定变异率容易让储能变量越界
惩罚系数1100-1000约束违反量单位是kW,目标函数单位是元,相差太大

最后一行惩罚系数是最容易被忽略的。假设一个平衡约束违反 200 kW,惩罚系数为 1,那这个解在目标函数里只被扣了 200 元,而一个优秀解的运行成本可能比差解少几千元,算法会倾向于保留“违反约束但成本更低”的劣质解。我一般会把约束违反量乘上一个data['penalty_scale'],并设为 100 到 1000 之间,让不可行解在非支配排序里基本没有生存空间。这个数值要在跑出第一代时观察一下目标量级,再回来调节。

4. 跑通示例代码的完整步骤:数据、初始化、求解、画图

4.1 准备输入数据:负荷曲线、分时电价和光伏出力

示例代码的“操作视频”一般会先展示数据集,但视频里一晃而过的 CSV 格式你自己拼的时候很容易漏列。最稳妥的做法是准备三个文本:load.csv保存电、热、冷三条负荷曲线,price.csv保存 24 点分时电价,pv.csv保存光伏预测出力。

数据读取统一放在主脚本里:

import pandas as pd def load_case_data(load_file="data/load.csv", price_file="data/price.csv", pv_file="data/pv.csv"): load_df = pd.read_csv(load_file) # 列名: load_e, load_h, load_c price_df = pd.read_csv(price_file) # 列名: price_grid pv_df = pd.read_csv(pv_file) # 列名: pv_power data = dict() data['load_e'] = load_df['load_e'].values data['load_h'] = load_df['load_h'].values data['load_c'] = load_df['load_c'].values data['price_grid'] = price_df['price_grid'].values data['P_pv'] = pv_df['pv_power'].values data['HOURS'] = 24 return data

很多人初跑代码时报“ValueError: operands could not be broadcast together”,原因基本是 CSV 里多了一行表头或者某一列长度是 23。视频里不会当面告诉你 CSV 要 24 行整数点,我的排查习惯是先print(data['load_e'].shape),确认所有数组长度都是 24,再继续往后面调。

4.2 初始化和群约束:防止第一步就把约束拍死

NSGA-II 第一步是随机生成初始种群。如果纯随机生成,五个变量互不关联,电平衡约束几乎不可能满足,第一代全是不可行解。这样非支配排序会失去梯度,算法变成乱搜。我常用的补救是先做随机初始化,再做一个简单的“启发式修正”:根据电平衡反推P_grid,从而让每一时刻的电平衡约束自动满足,剩下的变量还是随机。

def initialize_population(pop_size, data): pop = np.zeros((pop_size, 5 * data['HOURS'])) for i in range(pop_size): # 随机生成燃气轮机、制冷和储能出力 P_gt = np.random.uniform(0.3, 1.0, data['HOURS']) * data['P_gt_max'] C_abs = np.random.uniform(0.2, 1.0, data['HOURS']) * data['C_abs_max'] C_ec = np.random.uniform(0.2, 1.0, data['HOURS']) * data['C_ec_max'] E_bat = np.random.uniform(-0.5, 0.5, data['HOURS']) * data['E_capacity'] / 4.0 # P_grid 由电平衡反解,保证等式约束天然满足 P_grid = (data['load_e'] + C_ec / data['COP_ec'] - P_gt - data['P_pv'] - E_bat) P_grid = np.clip(P_grid, 0, data['P_grid_max']) # 如果不clip之后电平衡被破坏,重新计算一次E_bat E_bat = data['load_e'] + C_ec / data['COP_ec'] - P_gt - data['P_pv'] - P_grid pop[i, 0:24] = P_gt pop[i, 24:48] = P_grid pop[i, 48:72] = C_abs pop[i, 72:96] = C_ec pop[i, 96:120] = E_bat return pop

这个初始化函数的核心逻辑是“利用等式约束消去一个变量”。随机生成燃气轮机、制冷、储能后,购电功率不再随机,而是作为差额补齐。这样种群从一开始就站在可行域边界附近,算法后续只需要优化其他不等式约束。这里有个细节:如果P_grid被clip限幅,电平衡会被破坏,所以我又用反解出的E_bat兜底,总有一端是真实满足等式约束的。

4.3 迭代与结果输出:把解保存下来而不是只听终端

NSGA-II 主循环的示例代码网上很多,但很多示例跑完之后只打印一个目标值,不保存种群,你没法后续做决策。我建议在每一代结束都追加保存当前代的最优解集:

def save_pareto(pop, obj, gen): # 按非支配排序第一层取出Pareto前沿 from solvers.nsga2 import fast_non_dominated_sort fronts = fast_non_dominated_sort(obj) pareto_idx = fronts[0] save_pop = pop[pareto_idx] save_obj = obj[pareto_idx] np.savetxt(f"results/pareto_gen_{gen}.csv", save_pop, delimiter=",") np.savetxt(f"results/pareto_obj_gen_{gen}.csv", save_obj, delimiter=",")

保存频率不用每一代都做,我习惯每 10 代保存一次,最后再单独存第 200 代。如果你的磁盘和内存有限,这个函数可以改为只在代数尾号为 0 或最末代时调用。视频里经常能看到一条略过优化过程、直接展示结果曲线的画面,实际上代码运行时间主要全花在这里,保存中间代对分析收敛性非常有价值。

4.4 画 Pareto 前沿和调度图:判断你的解是否健康

光盯着终端里的数值看不出问题,必须把 Pareto 前沿画出来。冷热电联供一般是二维或三维目标,画个二维散点图就够了:

import matplotlib.pyplot as plt obj = np.loadtxt("results/pareto_obj_gen_200.csv", delimiter=",") cost_all = obj[:, 0] emis_all = obj[:, 1] plt.figure(figsize=(8, 6)) plt.scatter(cost_all, emis_all, c='steelblue', alpha=0.7, edgecolor='k') plt.xlabel("总运行成本 / 元") plt.ylabel("总碳排放量 / kg") plt.title("Pareto 前沿:成本与碳排放的取舍") plt.grid(alpha=0.3) plt.show()

这个散点图如果呈现一条从左上到右下的光滑弧线,说明算法收敛正常;如果点非常离散、四周乱成一片,大概率是约束惩罚系数太小,或者是目标函数里出现了异常值。画调度图时可以把某个中间解拆回各设备出力,用 stackplot 展示燃气轮机、购电、储能各自承担了多少电负荷。这一步是为了说服自己“解可执行”,而不是只有数字好看。

5. 避坑与排查:冷热电联供多目标代码的五个典型血泪经验

5.1 第一代全是不可行解,Pareto 前沿空荡荡

现象:程序跑完,fast_non_dominated_sort返回的第一层只有个位数,甚至整个种群的目标数组里出现inf。

原因:随机初始化没有利用等式约束消元,电平衡违反量动辄几千千瓦,惩罚系数又只有 1,目标函数差和约束违反量不在一个量级,算法认为所有个体都一样差,失去了选择压力。

解决:把初始化改成上一节里“由负荷反推 P_grid”的做法,同时把惩罚系数调到 100 以上。跑前可以先单独调用evaluate_constraints打印一下初始种群的违反总量,如果平均违反量还在百千瓦级别,就说明初始化修得不彻底。

5.2 目标函数出现负成本,算法疯狂在错误方向搜索

现象:Pareto 前沿左下角出现成本为负的点,且数量还不少,看起来“无限省钱”。

原因:代码里某项余热回收的热量被错误地乘以负系数,比如gas_heat = data['alpha_h'] * (data['load_h'] - data['eta_hx'] * gas_load * data['eta_recover'])中gas_load单位是 m³/h,eta_hx * gas_load却是 kWh 需要的数值,量纲不匹配,导致高温烟气余热量计算远大于实际值,补燃量被 clip 成 0,燃气耗量只算发电部分,相当于能源白送。

解决:所有物理量先统一单位再代入公式。建议在device_params.py里定义一个单位转换常量,比如KWH_PER_M3 = 9.7,每个公式里都显式乘上转换系数,不要靠人肉心算。写出目标函数后,先把每个设备设为满发状态,手算一次成本,跟程序输出对比。

5.3 储能约束写进惩罚项但永远不满足

现象:SOC 曲线在结果图上贴着边界一直跑,或者干脆 SOC 在任何时刻都大于 0.9。

原因:储能 SOC 约束用了cumsum,但初始 SOC 不是 0,而代码里从 0 开始累加,实际运行完的系统 SOC 比设计值偏高或偏低,算法为了补偿这个偏差,干脆把整条储能出力曲线往一个方向偏移。

解决:将初始 SOC 作为data['soc_init']传入,SOC 计算改成soc = soc_init + np.cumsum(E_bat) / E_capacity。另外要检查 SOC 约束的单位,SOC 是无量纲比例,而其他约束是 kW,惩罚总量纲不统一,可以把 SOC 违反量除以 10,缩小与其他约束的权重差距。

5.4 交叉变异后变量越界,目标函数报错中断

现象:程序在交叉变异后报错,比如“exp overflow”或“invalid value encountered in multiply”。

原因:NSGA-II 的模拟二进制交叉(SBX)和多项式变异可能产生超出变量上下界的值,如果目标函数里直接对越界的功率做运算,会导致负荷平衡被破坏,出现负数开方或零除。

解决:在cchp_model.py的第一行就对x做 clip,规定每个变量的物理边界:

def repair_variables(x, data): x_repair = x.copy() HOURS = data['HOURS'] x_repair[0:HOURS] = np.clip(x[0:HOURS], 0.3 * data['P_gt_max'], data['P_gt_max']) x_repair[HOURS:2*HOURS] = np.clip(x[HOURS:2*HOURS], 0, data['P_grid_max']) x_repair[2*HOURS:3*HOURS] = np.clip(x[2*HOURS:3*HOURS], 0, data['C_abs_max']) x_repair[3*HOURS:4*HOURS] = np.clip(x[3*HOURS:4*HOURS], 0, data['C_ec_max']) x_repair[4*HOURS:5*HOURS] = np.clip(x[4*HOURS:5*HOURS], -data['E_bat_max'], data['E_bat_max']) return x_repair

修复函数要在evaluate_objectives的一开始调用,而不是在遗传算子之后单独调用。因为种群里的所有个体都应该以“修复后”的状态参与适应度评价,不然新旧个体不在同一个可行域标准下比较,非支配排序的公平性就没了。

5.5 换了一台电脑,结果完全跑不出来

现象:视频里 MATLAB 或 Python 代码在自己电脑上各种报错,从缺包到文件路径乱掉。

原因:操作视频通常直接在某个 IDE 里点运行,文件的相对路径依赖当前工作目录。你新建的项目目录如果名称带空格,或者把load.csv放在别的位置,pd.read_csv("data/load.csv")就会报文件不存在。

解决:在主脚本顶部显式切换到项目根目录:

import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) os.chdir(BASE_DIR)

这样路径只和脚本文件所在目录有关,跟 IDE 的工作目录无关。另外,建议创建一个requirements.txt,把numpy、pandas、matplotlib固定好大版本,视频里如果用的是pandas 1.x,你装个pandas 2.x在读取某个 CSV 时可能行为有差异,多花十分钟锁定版本能省掉一晚上的玄学问题。

6. 在多个 Pareto 解里做终选:熵权-TOPSIS 后处理与调度落地

多目标算法算出来的是一堆非劣解,但调度员每天只能执行一套方案。后处理选择是操作视频里很少详细展开、却是实际落地最关键的一步。我的习惯是从 Pareto 解集里筛掉不可行或者过于极端的解,再用熵权-TOPSIS 选出折中解,既不让成本爆表,也不让排放过高。

先解包所有解的目标值,然后对每个目标做熵权计算。这里贴一段能直接替换到你自己代码里的精简版:

def entropy_topsis(obj_matrix): """ obj_matrix: shape (n_solutions, n_objectives) 返回每个方案的相对贴近度,越大越优 """ n = obj_matrix.shape[0] # 归一化,成本型和排放型目标越低越好 norm = (obj_matrix.max(axis=0) - obj_matrix) / (obj_matrix.max(axis=0) - obj_matrix.min(axis=0) + 1e-9) # 计算熵权 p = norm / (norm.sum(axis=0) + 1e-9) entropy = -np.sum(p * np.log(p + 1e-9), axis=0) / np.log(n) weights = (1 - entropy) / np.sum(1 - entropy) # 计算正负理想解距离 weighted_norm = norm * weights ideal_best = weighted_norm.max(axis=0) ideal_worst = weighted_norm.min(axis=0) dist_best = np.sqrt(((weighted_norm - ideal_best) ** 2).sum(axis=1)) dist_worst = np.sqrt(((weighted_norm - ideal_worst) ** 2).sum(axis=1)) closeness = dist_worst / (dist_best + dist_worst + 1e-9) return closeness

这段代码中,熵权会自动给“区分度大”的目标更高权重。比如碳排放目标在所有解之间差异很小,成本差异很大,那成本会被赋予更高权重,最终选出的方案更接近成本最优而不是排放最优。你要是想体现“双碳”偏好,可以直接手动把碳排放权重拉高,替换掉熵权输出。

拿到最优折中解后,再把它解包成设备出力曲线,转成实际调度表。我最后总会做一次人工核验:打开调度表,看燃气轮机有没有频繁启停、储能有没有在某小时同时充放电、夜间低谷电价时段购电比例有没有明显上升。这一关过了,这套方案才敢交给运行值班人员。做这个方向久了会发现,多目标算法的代码本身只是个体力和技巧活,最花功夫的是让模型贴合真实设备边界,以及学会在几十个 Pareto 解里做负责任的取舍。希望帮到你,也祝你复现时一次跑通。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询