1. 双目标规划:综合能源系统设计里绕不开的“既要又要”
做综合能源系统规划的同学应该都有同感:项目一开始最头疼的往往不是设备选型,也不是潮流计算,而是怎么跟业主解释——“最省钱”和“最环保”这两个目标,很多时候没法同时满足。你说全上光伏加储能,碳排放确实降下来了,但初始投资高得吓人;你说全用燃气轮机加电锅炉顶着,经济性挺好,可排放指标又过不了审。这种矛盾不是计算精度的问题,而是问题本身就是一个双目标优化问题。
我最早接触这个方向是在做某园区级综合能源系统预可研的时候。当时业主要求同时提交两个方案:一个是最小投资运行成本的方案,一个是最小碳排放的方案,最后领导层再开会拍板。我一开始偷懒,把两个目标加权成单目标去跑优化,结果被评审专家一句话问住了:“你这个权重凭什么取0.5和0.5?取0.3和0.7结论是不是就变了?”确实,权重一变,最优方案就变,但你又拿不出足够说服力的理由去解释这个权重。
后来我换成了NSGA-II跑一遍双目标优化,直接把一整条Pareto前沿摆出来,让决策者看到“多花多少钱能换多少减排量”,事情瞬间就清楚了。这篇文章就围绕这套方法展开,从问题建模、算法原理、代码实现到调参避坑,把完整流程摊开讲一遍。适合正在做能源系统规划、微电网容量配置,或者刚接触多目标进化算法、想在工程案例里试一试NSGA-II的读者。项目代码和思路可以迁移到任何“成本+排放”“成本+可靠性”这类双目标规划场景里,不需要改太多东西。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 优化问题建模:把“省钱”和“减排”翻译成数学语言
2.1 系统里都有什么设备,变量选什么
在动手写算法之前,得先把综合能源系统的物理模型搭起来。一套典型的园区级综合能源系统,通常包含这样几类设备:光伏机组(PV)、风力发电机组(WT)、燃气轮机(CHP,热电联产)、燃气锅炉(GB)、电锅炉(EB)、电储能(ESS)、热储能(TSS),再加上从上级电网购电的交互关口。
规划问题的本质是回答两个问题:第一,每种设备装多大容量;第二,在典型日内这些设备怎么运行。所以决策变量天然分成两层:容量变量和运行变量。容量变量包括光伏装机容量、风机装机容量、燃气轮机容量、储能额定功率和容量等;运行变量包括各个典型日逐时段的机组出力、储能充放电功率、购电功率等。
设备一多,变量维度就会膨胀。我用三个典型日(冬、夏、过渡季)来代表全年运行工况,每个典型日24个时段,每个时段都要给每台设备定义一个出力变量。这样算下来,决策变量的数量基本在200到400之间。对于NSGA-II这类进化算法来说,这个维度规模属于“舒适区”,不需要什么特殊处理,但编码方式安排得当的话,后面会省很多麻烦。
2.2 两个目标函数:成本函数与碳排放函数
第一个目标函数是综合经济成本,我习惯把它拆成三块:
- 设备投资成本的等年值,把初投资按照设备寿命和折现率折算到每一年;
- 年运行维护成本,按装机容量的比例估算,光伏和储能取1%到2%,燃气轮机取3%到5%;
- 年运行费用,主要是购电费用和燃气费用,购电按分时电价算,燃气按气价和机组效率算。
折算公式不复杂,年值系数长为 r(1+r)^n / ((1+r)^n - 1),r取6%到8%,n是设备寿命。比如储能电池寿命按10年算,光伏按25年算,注意不同设备寿命不同,要分开折算再求和。
第二个目标函数是碳排放总量,包括外购电力的等效碳排放和燃气燃烧的直接碳排放。外购电力的碳排放因子取区域电网的平均值,大概在0.5到0.8 kg CO₂/kWh之间,看你项目所在地;天然气碳排放因子相对固定,按单位热值算大约0.2 kg CO₂/kWh热量。这里有一个常见的坑:如果你把生物质、绿氢这些零碳燃料也加进来,碳因子就要单独定义,不能一刀切。
2.3 约束条件别漏项,否则结果没法用
约束条件决定了优化结果能不能真正落地。我常用的约束包括这几类:
一类是能量平衡约束,电力母线、热力母线在每一个时段都要平衡,发电加购电加储能放电等于电负荷加电锅炉用电加储能充电;燃气轮机余热加燃气锅炉产热加电锅炉产热等于热负荷,这个必须逐时段严格满足。
另一类是设备出力上下限约束,以及爬坡约束。燃气轮机尤其要注意爬坡率,否则优化结果里会出现上一小时满发、下一小时停机的“抖振”工况,现场根本执行不了。
还有一类是储能约束,包括充放电功率限制、SOC上下限和周期始末能量一致约束。储能在规划层面的建模比较粗糙,一般把一天作为一个周期,要求结束时刻SOC回到初始值,不然优化结果会“作弊”——把电存到第二天用,成本算起来当然好看,但工程上不现实。
处理约束的方式我建议先用罚函数法把约束放进适应度里,实现快,跑起来也稳。等到模型复杂、约束变硬之后,再考虑Deb的约束支配法则也不迟。NSGA-II本身不原生处理约束,工程上多数是修改支配关系来满足。但第一次实现,罚函数足够用。
3. NSGA-II为什么适合干这个活,以及它是怎么工作的
3.1 多目标加权的痛,和Pareto解的必要性
前面提到,把两个目标加权成一个目标,问题就变成了单目标优化。这个方法在工程里大量使用,主要原因是简单——跑一次就出一个解,决策者拿着这个直接做方案。
但它有两个硬伤。第一,权重怎么取没有标准,你取经济权重0.6和取0.7,最优方案可能差异很大;第二,如果Pareto前沿不是凸的,加权法会漏掉非凸部分的解,而这个区域往往正是决策者感兴趣的“性价比拐点”所在。第三,它一次只能给一个点,想要多个方案对比,就得反复调权重反复跑。
NSGA-II这类多目标进化算法就不一样。它一次运行能输出一整条分布均匀的Pareto前沿——在这条前沿上,任何一个方案都不能在不恶化另一个目标的前提下继续改进。工程上可以理解为“多花一百块成本能减多少碳、再多花一百块还能减多少”的一系列方案包。决策者在这个方案包里选,而不是在“算法替他选好”的一个点里选。
这就是NSGA-II在双目标规划中最大的价值:它不替你做权衡,而是把所有可能的权衡结果摊开给你看。
3.2 非支配排序、拥挤度、精英保留:三个关键机制拆开讲
NSGA-II的核心思想可以概括为三个机制:快速非支配排序、拥挤度距离计算、精英保留策略。
快速非支配排序做的事情是给种群分层。假设种群里有100个个体,两两比较目标向量,如果一个个体在两个目标上都优于另一个体,那它就是支配对方的。遍历完整个种群后,找出那些不被任何其他个体支配的,放进第一层Pareto前沿,然后去掉它们,在剩余个体里继续找第二层、第三层。排序的复杂度是O(MN²),M是目标个数,N是种群规模,目标只有两个时跑起来非常快。
拥挤度距离是NSGA-II用来维持多样性的关键。同一层里的个体,如果彼此离得太近,说明解在目标空间里扎堆,覆盖不充分。拥挤度距离就是计算每个个体在目标空间中两个相邻个体之间的距离——距离越大越稀疏,越值得保留。这样做的效果是让解集均匀铺满整个前沿,而不是挤在某个角落里。
精英保留策略听上去玄,实际就是“下一代种群优先从排序靠前的层里取,同一层里优先取拥挤度大的”。这样一来,优秀的父代个体有机会直接进入下一代,不会因为交叉变异被冲掉,算法收敛稳定性大幅提升。
3.3 遗传算子怎么选:SBX交叉与多项式变异
NSGA-II最基础的版本用的是模拟二进制交叉和多项式变异。SBX交叉算子的特点是:两个父代交叉后产生的子代会集中在父代附近,但又保留一定的离散步长。它的核心参数是分布指数ηc,一般取15到20。ηc越大,子代越接近父代,局部搜索能力强但探索能力弱;ηc越小,子代离父代越远,全局搜索强但收敛变慢。
变异算子用多项式变异,参数ηm一般取20到100,变异概率通常取决策变量维数的倒数,也就是每个变量平均每一代有大概一次变异机会。
我个人的经验是,不要迷信NSGA-II论文里给的默认参数。像IES规划这类问题,决策变量之间有隐式耦合,比如光伏装多了储能出力方式就会变,如果变异概率太高,后代很容易跑飞;太低又容易陷在局部。我在调试中最后固定的是:种群规模100,迭代200代,SBX交叉概率0.9,ηc=20,变异概率1/n,ηm=50。这套参数在这个问题上表现相当稳定,你可以拿去做基准再微调。
4. 实操完整流程:从典型日选取到Pareto前沿输出
4.1 典型日数据怎么处理
综合能源系统规划不可能全年8760小时逐时建模,那样决策变量直接上万,NSGA-II跑不动。实务标准做法是聚类选出几个典型日,每个典型日赋予权重,代表这一个月或者一个季节的运行特征。
我自己常用k-means聚类,把全年逐时负荷和风光出力数据聚成3类,再按每类的天数加权。比如聚类结果是冬季类占120天、夏季类占90天、过渡季类占155天,那目标函数里运行费用就按这权重累加。
聚类之后,每个典型日要有24小时的负荷曲线、光伏出力和风电出力归一化曲线。光伏归一化曲线要根据项目的纬度和辐照数据生成,不能随便拍脑袋。如果项目现场没有实测数据,可以用NASA或新能源发电数据平台查典型气象年的逐时辐照,再折算成光伏出力曲线。
4.2 目标函数和约束怎么写进代码
写代码时我建议把问题结构拆清楚,别把所有逻辑塞进一个庞大函数里。核心就是定义四个部分:输入参数、决策变量解码、目标函数计算、约束校验。
决策变量我采用实数编码,排成一段向量。前半段放容量变量,比如:
- 光伏容量(kW),范围由屋顶面积或用地限制决定;
- 风电容量(kW);
- 燃气轮机容量(kW);
- 电锅炉容量(kW);
- 储能额定功率和额定容量(kW/kWh)。
后半段放运行变量,即每个典型日每个时段的设备出力,比如燃气轮机出力、储能充放电功率、购电功率等。运行变量的范围根据容量变量动态变化,解码时要注意边界。
目标函数的计算顺序是先解码容量,再算设备年化投资和年维护费用;然后把运行变量和容量变量代入典型日逐时功率平衡方程,算出购电量和燃气消耗量,最后汇总成年购电费用、燃气费用和碳排放量。
注意:一旦某个时段能量不平衡,罚函数要起作用,但不是简单给一个无限大的数。我建议把不平衡量归一化后乘以一个较大的惩罚系数,再加到两个目标上。这样能让算法知道“往哪个方向改进能减少违规”,比直接判死更有利于搜索。
4.3 主程序框架:NSGA-II跑一圈的完整结构
下面给出一个简化版本的程序框架,用Python伪代码形式展示核心逻辑。实际工程中可以用pymoo或者geatpy库的NSGA-II实现,但理解底层流程能帮你更好调参。
python复制import numpy as np
from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.core.problem 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 IESPlanningProblem(Problem):
def __init__(self, data):
# n_var 是决策变量维度,包含容量变量和运行变量
# xl, xu 是每个变量的上下界
super().__init__(n_var=data['n_var'],
n_obj=2,
xl=data['x_lower'],
xu=data['x_upper'])
def _evaluate(self, X, out, *args, **kwargs):
# X是种群矩阵,每一行是一个个体
f1 = np.zeros(X.shape[0]) # 经济成本
f2 = np.zeros(X.shape[0]) # 碳排放
feas = np.zeros(X.shape[0]) # 罚函数相关的可行性度量
for i in range(X.shape[0]):
cost, emission, violation = calc_ies_objective(X[i])
# 如果有约束违反,通过罚函数叠加到目标中
penalty = 1e5 * violation
f1[i] = cost + penalty
f2[i] = emission + penalty
feas[i] = 1.0 if violation < 1e-6 else 0.0
out["F"] = np.column_stack([f1, f2])
out["G"] = -feas # 用于pymoo的约束处理,负值表示可行
algorithm = NSGA2(
pop_size=100,
sampling=FloatRandomSampling(),
crossover=SBX(prob=0.9, eta=20),
mutation=PM(prob=1.0, eta=50),
eliminate_duplicates=True
)
res = minimize(IESPlanningProblem(data),
algorithm,
('n_gen', 200),
seed=42,
verbose=True)
这里有个地方要特别说明:我在calc_ies_objective函数内部会把决策变量向量解码成容量参数和逐时出力,再做功率平衡的逐时计算。实际工程项目中,这个函数是整个程序最核心、最容易出错、也是计算量最大的地方。建议用numpy向量化计算整年的8760小时,每代评估时间是毫秒级的,但如果写成纯Python循环遍历24个时段再套三层循环,计算量会爆炸。
4.4 跑出来的结果怎么读,Pareto前沿图怎么看
程序运行结束后,res.F就是最终的Pareto前沿目标值集合,res.X则是对应的决策变量。先把res.F画成散点图,横轴是年综合成本(万元或亿元),纵轴是年碳排放量(吨)。
好的结果应该呈现一条向左下凸的、近似光滑的曲线或者带状点集,从左到右碳排放逐渐降低、成本逐渐升高。如果跑出来的点是乱糟糟一团,没有任何递减趋势,大概率是约束罚函数没调好或者种群没收敛,先不要急着分析方案,回去查模型。
前沿确定之后,可以看到几个有代表性的关键点。左端点表示几乎不减排的纯经济最优方案;右端点表示不计成本的最大减排方案;“膝盖点”则是曲率最大处——曲率最大的地方往往是性价比最高的转换点。再往右边走,每多投一块钱能换到的减排量会显著下降。膝盖点的计算不难,取前沿上前后两点的斜率变化最大处即可。
5. 调试过程中最容易翻车的几个问题
5.1 种群数量和迭代代数不是越大越好
NSGA-II看起来像一个黑盒优化器,很多人上来直接把种群设置成500,迭代跑1000代,觉得“跑得越多越接近全局最优”。实际不是这样。种群数量越大,每代评估时间线性增长,而收益在超过一定规模后迅速递减。对于200到400维决策变量的问题,我的经验是种群100就够用,最多200。
迭代代数看收敛曲线。怎么判断收敛?把每代种群的超体积指标(Hypervolume,简称HV)打印出来。HV衡量的是前沿解集在目标空间覆盖的体积,HV值在某一段区间不再明显增长时,说明算法已经收敛了。如果跑200代还没稳定,适当加到300代,但没必要无限加。
值得注意的是,IES规划目标函数里包含逐时运行模拟,代数和种群数量乘起来,就是总运行模拟次数。100个个体跑200代,等于做了2万次全年逐时运行模拟,每次还要遍历不同典型日。如果解算速度已经到秒级,请优先优化calc_ies_objective内部的向量化程度,而不是减少种群规模。
5.2 约束违反惩罚系数太大会让目标失去意义
罚函数法的惩罚系数是双目标规划里最阴险的一个坑。惩罚系数太小,约束形同虚设,能量不平衡的方案会堂而皇之地混进Pareto前沿,导致前沿看起来“特别优秀”,实际不可行;惩罚系数太大,目标函数里罚函数项会淹没真实的成本和碳排放数值,两个目标的区分度下降,算法退化成在“找可行解”而不是在“找最优解”。
我以前吃过一次亏:有一个案例惩罚系数取了10的8次方,结果Pareto前沿上的点在成本维度的差异全部被罚函数抹平,最后画出来的前沿几乎是一条竖线,任何分析都做不了。
处理办法是这样:先单独跑一次经济单目标优化,看看不加约束时的最优成本大概是多少量级,把惩罚系数设成这个量的100到1000倍。这样既能保证可行解优先,又不至于完全淹没目标函数本身的梯度信息。还有一种更稳的方式:改用Deb的约束支配法,即两个解比较时,可行解优先于不可行解,都可行时按Pareto支配比较,都不可行时比较违规量大小。这个方法不引入额外参数,工程上更干净。
5.3 结果里出现储能疯狂循环充放怎么办
这是一个典型的运行约束缺失导致的假解。如果储能没有设置单日循环次数上限或者效率惩罚,优化器会利用储能“低买高卖”——比如在电价低谷充满、在电价尖峰放光,一天循环好几次。从数学上看,这确实降低了购电成本,而且减少了弃风弃光,目标值很好看。但真实储能电池的循环寿命和充放电效率不允许这么干,这种方案根本没法落地安装。
解决方式是在约束里加入储能日循环次数限制,或者把储能充放电效率建模得更精确,不能只是简单定义一个0.9的充放电效率,要考虑变流器损耗和电池内阻损耗在低功率时占比更高这个特点。更进一步的建模是引入老化成本——深度充放电一次折算多少寿命损耗,直接把老化成本加入目标函数。不过这个属于进阶玩法,第一次做规划不建议加太细,否则模型复杂度会迅速失控。
5.4 Pareto前沿分布不均:解都挤在膝盖点附近,两端稀疏
用NSGA-II跑双目标,常见的现象是所有点都挤在膝盖点附近,两端几乎没有点。这说明多样性保持机制没起作用,或者两个目标之间相关性太强,导致两端的“极端解”在拥挤度比较中不受欢迎。
解决方法之一是检查种群规模是不是太小,另一种方法是增加交叉变异强度。如果解仍然挤成团,可以检查一下是不是决策变量边界设置过窄。比如如果储能容量上限给的太小,那“最大减排方案”就只能依赖光伏和风电装机,前缀变化受限,自然就会出现某一段目标空间无法达到的情况。
还有一种战术性做法:使用参考点方向的分解思想,比如把目标空间划分成几个扇区,分别在不同扇区里强制保留个体,这样能保证前沿覆盖度。但这个是更进阶的MOEA/D的思路,NSGA-II跑不匀的时候先用前面几个方法排查,基本能解决90%的问题。
6. 从双目标迈向更复杂场景的一些扩展想法
双目标规划只是多目标优化在IES中的起点。在实际工作中,我后来遇到了不少需要三目标甚至四目标的情况,最典型的是在“成本、碳排放”之外增加“年供能可靠性”或者“可再生能源渗透率”作为第三个目标。NSGA-II本身支持三维Pareto前沿输出,只是3D目标下的前沿可视化困难,可读性没有二维那么直观。
如果要做三个目标,建议考虑:第一,HV指标依然可以判断收敛性,但前沿可视化要借助平行坐标图,或者投影到二维平面加颜色映射;第二,求得的Pareto面会从一条曲线变成一个曲面,决策者的选择难度大大增加,工程落地时一般要配一个多属性决策方法,比如TOPSIS或者层次分析法,做二次筛选。
代码层面,NSGA-II换三目标几乎不需要改动,只需要把n_obj从2改成3,再补一个目标函数即可。但要注意,目标维数升高,非支配排序的支配关系会变“松弛”——两个目标时一个个体支配另一个体较容易判断,三个目标时很多个体互相都支配不了对方,帕累托前沿上第一层的个体数量会暴增,导致选择压力下降。这是所有基于Pareto支配关系方法的通病。
应对方案有两类,一是增大种群规模,二是采用指标-based算法(如IBEA)或者分解类算法(如MOEA/D)。我给一个务实建议:先试NSGA-II,如果发现前沿层数很多但解集分布不够好,或者运行时间难以接受,再换MOEA/D。实际上很多论文里“NSGA-II不好用”的结论,都出在没调好参数或目标函数尺度差异过大这两个地方。我建议在做任何目标函数聚合之前,先把两个目标的取值范围归一化到同一个量级——比如用设备投资上限和排放上限做基准去归一化。这个操作对NSGA-II这类基于距离的方法至关重要。
再往深一层说,IES规划问题往往还带有整数变量,比如设备选型台数、储能是否配置等。标准NSGA-II天然支持混合编码,你只要把编码中一部分变量定义为整数,再在交叉变异操作中做取整处理即可。pymoo和geatpy都有混合编码的例子,可以直接查文档借用,不需要自己实现。
7. 最后想说:算法重要,但建模功力才决定结果上限
文章到这里,NSGA-II在综合能源系统双目标规划中的应用流程已经完整过了一遍。从一个工程实践者的角度,我想说几句掏心窝的话:NSGA-II只是一个高性能的工具,真正花时间的其实是前期的系统建模和目标约束的定义。算法跑得快不快、前沿画得匀不匀,这些是技术层面的问题,花几天时间总能调好。但模型本身是不是准确描述了项目的实际边界条件,运行约束是不是符合设备真实性能,这决定了对业主而言这个优化结果是否有参考价值。
另外提一个很多新手容易忽略的细节:NSGA-II跑完之后,从Pareto前沿上选中的所谓“最优折中方案”,建议再单独放到精确的仿真模型里复核一遍,特别是要看储能SOC曲线是否在允许区间内、燃气轮机的爬坡约束是否真的满足。为什么?因为多目标进化算法在搜索过程中为了效率,常常会做一定的约束松弛,目标函数里的罚函数设计可能达不到全局严格可行,复核一遍能避免你在汇报时被现场工程师质疑。
如果说还有什么实用的建议,那就是做这类项目,一定要把决策变量的上下界约束写得贴合实际。风速资源差的地方给风机容量上限设2000千瓦,屋顶面积不够的地方给光伏设5000千瓦,即使算法算出来“最优解”,也完全不可执行。规划问题的最优解不是数学游戏的结果,而是所有边界条件约束下的工程可行解。把这个理念想明白,NSGA-II对你来说就不只是一个算法,而是一套能把复杂工程问题清晰呈现出来的解决方案框架。
