做配电网规划的朋友,十有八九都会撞上分布式电源选址定容这道坎。我最近把一套基于多目标遗传算法的分布式电源选址定容代码从头到尾捋了一遍,也补了不少工程细节。先说实话:这类代码的难点不在遗传算法本身,而在把网架结构、潮流约束、多个优化目标拧成同一个可计算的数学问题。只要建模建模得清楚,算法拿来用就顺,模型搭得模棱两可,后面调参能调到你怀疑人生。这篇文章就把我的建模思路、NSGA-II实现细节、代码模块划分,以及调试中踩过的坑一次性讲透,适合正在写论文、做课程设计或者刚接触配电网规划项目的同学参考。
1. 这个项目到底在解决什么问题
1.1 分布式电源为什么不能想装哪就装哪
分布式电源,常见的就是屋顶光伏、小型风电、燃气轮机、储能这几种。它们接入配电网后,最直接的影响有两个:一是改变节点电压分布,二是改变支路潮流方向和大小。听起来都是好事,毕竟分布式电源能就近供电、减轻主网压力。问题在于,配电网网架设计是按传统单向潮流来规划的,DG接入的位置和容量一旦不合适,就会出现末端电压越上限、线路过载、继电保护误动这些麻烦。
我见过一个很典型的案例:在某条10千伏馈线的末端强行接入一个容量偏大的光伏电站,中午出力最大时,线路末端电压直接顶到1.08倍额定值,用户侧电压越限不说,并网逆变器频繁报故障脱网。这就是典型的“位置不合理”。反过来,如果只在变电站出口附近装DG,虽然对电压没什么坏影响,却根本发挥不了降低网损、提升线路供电能力的作用,投资回报率很低。
所以选址定容本质上是个“空间决策+容量决策”的组合优化问题。通俗点说,就像开连锁店:店开在哪、店面开多大,决定你能不能赚钱,也决定会不会影响周围路况。电网里每一个备选节点都有可能装DG,容量也从几十千瓦到几兆瓦不等,组合数量是指数级的,靠人工一个个试根本不现实。
1.2 单目标优化不够,多目标才有工程意义
很多人第一反应是把全部要求揉成一个公式:把电压偏离和网损的成本折算成钱,再和目标投资成本加权求和,做成单目标遗传算法。这样做不是不行,但有个很大的隐患:网损成本、电压惩罚系数、DG投资回报周期这些权重都靠拍脑袋定,权重不同得到的方案完全不一样,评审或导师问你“为什么取这个权重”,解释起来非常被动。
多目标遗传算法处理这个问题要优雅得多。它把“网损最小”“电压偏差最小”“综合成本最小”等目标并列保留,最后得到一整套帕累托前沿解,而不是唯一的答案。你可以看到网损和成本之间的博弈关系,比如再投100万可以多降多少网损、电压质量能改善多少,决策者完全可以结合工程预算来选择折中方案。这也是这类代码在论文和实际项目中特别受欢迎的根本原因。
这套代码的核心逻辑就是:用多目标遗传算法去搜索“在哪些节点装、各装多大容量”的方案集合,在满足潮流约束和节点电压约束的前提下,尽量压低网损、抬高电压质量、减少总投入。
2. 数学模型拆解:目标函数、约束和编码
2.1 目标函数怎么定才不飘
目标函数是整套代码的灵魂。我在这套代码里默认按最常见的三目标来做:
第一是网络损耗最小化。配电网线损通常用潮流计算结果里的支路有功损耗求和得到。数学上就是每条支路电流平方乘电阻再累加,公式写出来大概是Ploss = sum(I_ij^2 * R_ij),前提是潮流算出来以后我们手里已经有一组准确的支路电流。网损直接关系着经济运行水平,也是投资方最关心的指标之一。
第二是节点电压偏差最小化。电网人讲究电压质量,一般用所有节点和额定电压的偏差绝对值累加,也可以用平方和。公式类似Vdev = sum( abs(V_i - V_n) )或者sum( (V_i - V_n)^2 )。这个目标一旦加上,优化算法就会偏向在那些电压偏低、偏高的节点附近装DG,起到支撑作用,非常符合现场工程直觉。
第三是综合成本最小化。包括DG单位容量的投资费用、运行维护费用,有时候还叠加网损折价成本。我们在代码里可以简化为“安装容量越大,投资成本越高”,即Cost = sum(C_install * S_DG_i + C_om * E_i)。如果想更精细化,还可以把不同DG类型(光伏、风电、燃气轮机)区分成不同单价。
这三个目标之间天然存在矛盾。网损最小的方案可能要求把DG装在负荷最重的末端,而末端节点往往电压偏高,继续装DG就可能让电压越限;成本最低的方案则是什么都不装,但网损和电压目标明显变差。多目标算法存在的意义,就是把这些矛盾摊开给你看,而不是强行用权重掩盖掉。
2.2 约束条件是怎么落到代码里的
选址定容的约束条件大概有四类,缺一类代码就会跑出离谱的结果。
潮流等式约束必须满足,也就是每个节点的有功、无功必须平衡。这个约束不需要我们手动去写表达式,因为只要调用了潮流计算函数,算出来的结果自然满足潮流方程。但要注意,潮流必须收敛,不收敛的方案要直接判成不可行解。
节点电压约束是边界约束,比如配电网要求电压在0.93到1.07倍额定电压之间。代码里常见的做法有两种:硬约束直接丢弃,或者软约束用罚函数。我倾向于用罚函数,因为分布式电源接入后,系统原本可能就有电压越限的趋势,如果直接丢掉所有略微越限的方案,搜索空间会变得支离破碎。
DG渗透率和容量约束也很关键。很多接入规则要求分布式电源总容量不能超过配电网最大负荷的一定比例,或者单个接入点的容量有上限,比如不能超过该节点变压器容量的25%。这些约束如果不体现在代码里,算法很可能给你一个“整个馈线全装满了”的疯狂方案,看着目标很漂亮,实际根本过不了并网审批。
还有一种容易忽略的约束是DG接入数量的限制。现实中一条馈线不太可能在30多个节点都装DG,运维成本太高。代码里通常加一个“最多允许接入N个DG”的限制,这个参数直接决定了染色体里基因分布的稀疏程度。
2.3 染色体编码:选址和容量怎么表达
编码方式决定了搜索效率,也决定了交叉变异算子怎么写。我见过两种主流编码,各有各的坑。
第一种是二进制编码。把所有备选节点编号,每个节点用一个二进制位表示装或不装,再用另一段二进制位表示容量等级。这种编码简单直观,但缺点非常明显:节点很多时染色体会变得特别长,而且“容量等级”和实际容量之间是离散映射,优化精度受限。另一个麻烦是交叉和变异之后很容易产生非法的“节点重复安装”状态,需要大量修复操作。
第二种是整数+实数混合编码,也是我个人更推荐的做法。染色体分两段,第一段是安装节点的编号序列,比如[17, 25, 31, 4, 9],表示第1个DG装在节点17,第2个装在节点25;第二段是对应的容量序列,比如[0.6, 0.8, 0.5, 0.3, 0.2]兆瓦。这种编码做选择、交叉都很自然,只要保证节点编号互不重复,解码就很干净。
我在代码里默认使用第二种编码,并且在交叉变异后写了一个“去重”函数:如果某个节点被重复选择,就随机替换成另一个没被选中的节点。实际跑下来,这个修复操作比在目标函数里做大幅惩罚要稳定得多,因为惩罚会导致很多重复方案被保留,白白浪费计算量。
3. 多目标遗传算法的设计与实现要点
3.1 为什么选NSGA-II这种套路
多目标遗传算法家族里有很多选择:NSGA-II、NSGA-III、SPEA2、MOEA/D等等。对于中低压配电网选址定容这个规模,NSGA-II是最成熟、最容易实现的选项。它不依赖目标个数特别多的场景,两到三个目标时效果很好,代码资料也多,出了问题容易排查。
NSGA-II的核心思想可以概括成一句话:在每一代进化过程中,先把父代和子代合并成一个大小为2N的种群,然后对这2N个个体做“非支配排序”,也就是把所有互不支配的解挑出来排成第一层,再去掉它们再找第二层,以此类推。每一层里的解再按“拥挤度距离”排序,最后从排名靠前、拥挤度小的个体里选出N个组成下一代。
这个机制的高明之处在于:既保持了种群的多样性,又不至于让某一代的优秀个体直接消失。精英保留策略保证了最好的解不会因为交叉变异被破坏掉,非常适合工程优化这种对收敛性要求较高的场景。
3.2 非支配排序和拥挤度的工程实现
非支配排序的原理不复杂:如果解A的所有目标都不差于解B,并且至少有一个目标严格优于B,就说A支配B;反过来也一样;互相都有优势就说这两个解互不支配。扫描式实现虽然简单,但复杂度是O(MN^2),种群200个个体的三层目标问题倒也能忍,如果要跑500个个体、上千次迭代,性能就有点吃力了。
我在代码里用了经典的“快速非支配排序”,思路是先计算每个个体被哪些个体支配、支配了几个个体,然后从没有被任何个体支配的那批开始逐层剥离。这个算法写起来也不难,关键是数据结构要清晰,我一般用两个列表:dominated_solutions存“我被谁支配”,dominate_count存“我支配了几个人”。每一层扫描后更新计数,计数归零的个体进入下一层。
拥挤度距离的计算更直白:对每个目标,把同一非支配层里的个体按该目标值排序,首尾个体的拥挤度设为无穷大,中间个体的拥挤度取相邻两个个体在目标空间上的归一化距离之和。这个值越大,表示这个解在目标空间里越“孤僻”,保留它能维持多样性。注意每个目标都要先归一化,不然数值大的目标会支配拥挤度计算。
3.3 潮流计算:前推回代法怎么嵌进目标函数
选址定容的每一次目标评估都要算潮流。配电网通常是辐射状网络,用前推回代法比用牛顿-拉夫逊法简单得多,也快得多。基本步骤是:先给各个节点电压赋初始值(通常取额定电压),从末端节点开始,根据负荷功率和注入功率倒推每条支路电流,得到支路电流后再从首端向后推算各节点电压,反复迭代直到两次电压差小于容差。
前推回代法在代码里实现细节决定成败。第一,DG接入的节点,注入功率方向是“从节点流向网络”,所以前推时该节点的净功率应该是负荷减去DG出力;第二,要处理无功功率,光伏逆变器和风电通常以单位功率因数运行,也就是无功出力设为0,但这不代表节点无功平衡不用算,线路充电电容和负荷无功仍然要计算;第三,迭代收敛判据除了电压幅值差,最好也看一下有功功率的不平衡量。
这套代码里我默认使用IEEE 33节点配电网系统做测试,基准容量取10MVA,基准电压12.66kV。为什么用这个系统?因为它是配电网优化领域公认的测试算例,节点支路数据到处都能找到,对比文献时大家用的都是同一个网架,结果可复现性很强。很多论文里直接把测试系统的参数写死,运行代码前记得确认数据文件格式。我后面的讨论也围绕33节点系统展开,如果你的场景是50节点、69节点,思路完全一致,只需要替换拓扑和负荷数据。
4. 代码结构与关键模块实现
4.1 总体架构怎么组织才不乱
拿到一套代码,先别急着看遗传算法函数,我的习惯是先把目录结构捋清楚。这套代码我按功能分成五个模块:主入口脚本、初始化模块、目标评估模块、NSGA-II进化模块、结果输出模块。
主入口脚本做三件事:加载配电网参数、设置遗传算法参数、调用进化循环。通常运行完还会自动画三张图,Pareto前沿、最优网损对应方案的电压分布、目标收敛曲线。
初始化模块负责两件事:随机生成初始种群、把真实配电系统的节点数据格式化成算法可用的结构体或字典。这里有个很关键的细节:所有参与优化的量都要标幺化。用有名值计算容易造成数值病态,比如12.66千伏电压和0.1兆瓦网损放一起,数量级差太大,拥挤度计算会失衡。标幺化以后,电压接近1.0,功率、阻抗都在个位数级别,遗传算法内部比较起来公平得多。
我这里展示一段大概的初始化思路,用MATLAB伪代码写,换成Python numpy也完全一样:
pop_size = 100; % 种群规模 n_dg = 3; % 最多装3个DG n_node = 33; % 33节点系统 candidate_nodes = 2:33; % 节点1通常是电源,不装DG pop_nodes = zeros(pop_size, n_dg); pop_cap = zeros(pop_size, n_dg); for i = 1:pop_size pop_nodes(i,:) = datasample(candidate_nodes, n_dg, 'Replace', false); pop_cap(i,:) = 0.1 + 0.9 * rand(1, n_dg); % 0.1~1 MW end注意候选节点必须去掉平衡节点和联络开关节点,不然算法会把DG装在根本不允许接入的位置。这个坑在我第一次跑代码的时候就踩了,平衡节点装DG会导致潮流计算直接崩溃。
4.2 目标函数和约束罚函数的关键写法
目标函数是整个算法里最容易被写乱的部分。我的建议是把它拆成三层:第一层解码染色体,把节点编号和容量映射到网络参数上;第二层调用潮流计算,获取网损和各节点电压;第三层计算三个目标值并附加惩罚值。
解码这一步亲切地叫做“基因型到表现型转换”。代码里要严格检查,节点编号数组里不能有重复,容量数组不能出现负值。一旦发现染色体不合法,可以有两种处理:一是在解码阶段直接修复,二是返回一个极端大的目标值淘汰它。我在代码里选择先修复再计算,因为如果淘汰太多个体,种群多样性会很快崩掉。
潮流计算返回的结果要保存好,后面画电压分布图还要用。目标评估时顺手做一些中间结果的记录,这样可以避免整个进化过程结束后重新算一遍所有个体,省下不少时间。
约束罚函数部分,我习惯按节点电压越限程度罚,公式类似penalty = lambda * max(0, V_min - V_i) + lambda * max(0, V_i - V_max),其中lambda取一个较大的正数,比如1000。要注意的是,罚函数不要罚到完全掩盖目标本身的差异,否则算法会盲目追求不越限而忽略经济性。我一般让罚函数占目标量级的10%到30%,具体数值可以多次试。
潮流不收敛的个体也要处理。常见的做法是给所有目标赋一个很大的值,比如[1e6, 1e6, 1e6],相当于直接淘汰。我试过给不收敛个体零目标值,结果遗传算法把一堆坏解全当成完美解保留了下来,场面非常难看。
4.3 进化流程中那些容易写错的小地方
NSGA-II主循环的流程是:评估目标、非支配排序、选择父代、交叉变异产生子代、父子合并、再排序截断。听起来很顺,但有几个细节特别容易写错。
第一个是锦标赛选择。很多初学者为了省事直接随机挑两个个体比支配关系,但锦标赛的规模通常取2,也就是“二元锦标赛”。如果两个个体都在同一非支配层,光比支配关系就没法分高下了,这时候必须比拥挤度,拥挤度大的胜出。这两层判定缺一不可,不然选择压力不够,种群收敛慢。
第二个是交叉变异算子的写法。因为染色体是“节点编号+容量”的混合结构,我分别处理:容量部分用模拟二进制交叉(SBX)加多项式变异,节点编号部分用部分映射交叉来避免重复。没有现成算子的编程语言,手写部分映射交叉会有点绕,建议先画两个例子推演一遍再写函数。
第三个是交叉概率和变异概率不要拍脑袋。交叉率我一般设0.9左右,这样大部分个体都会参与基因交换;变异率如果按每个基因位算,取1除以染色体长度左右比较合适,太高会让算法退化成随机搜索。容量变量还要设一个步长上下界,防止变异幅度过大导致容量越界。
下面这段是去掉细节后NSGA-II的核心循环骨架,重点是体会它的流程:
for gen in range(max_gen): # 1. 评估目标 objectives = evaluate(pop_nodes, pop_cap, sys_data) # 2. 非支配排序 fronts = fast_non_dominated_sort(objectives) # 3. 计算拥挤度并建立下一代 next_pop_nodes, next_pop_cap = [], [] i = 0 while len(next_pop_nodes) + len(fronts[i]) <= pop_size: overcrowding_distance(objectives, fronts[i]) next_pop_nodes += fronts[i] i += 1 # 4. 从第i层按拥挤度补齐 # 5. 锦标赛选择 -> 交叉 -> 变异 -> 得到子代 # 6. 父子合并进入下一轮真正跑起来以后你会发现,最耗时的其实是第1步的目标评估和潮流计算,遗传算子本身反而是秒级的。所以优化重点要放在潮流函数的向量化上,不要循环内套循环,用矩阵运算一次算出所有支路电流。
5. 结果解读与常见问题排查
5.1 Pareto前沿图怎么看、怎么选方案
运行完毕,最直观的结果是三维Pareto前沿散点图。如果算法没问题,你看到的应该是一张漂亮的曲面分布点图,点与点之间没有明显的“谁完全支配谁”关系。如果所有点都聚集在一个很小的区域,说明早熟了;如果点乱得不成形状,说明约束或目标函数有bug。
选方案的手段有很多。最简单的是从Pareto前沿里找离理想点最近的解,所谓理想点就是每个目标分别取所有解中的最优值构成的虚拟点。计算每个解到理想点的欧氏距离,距离最小的就是折中解。这个方法虽然朴素,但非常直观,在论文里也好解释。
如果想更严谨,可以用模糊隶属度方法。对每个目标定义一个隶属度函数,把每个解在每个目标上的满意度转换成0到1之间的数,然后求和取最大。这种做法能体现决策偏好,比如你更看重投资少,就可以给成本目标更高的权重。我在代码里附带了折中解计算函数,不用额外改算法,直接跑完就能输出一个推荐方案。
我还强烈建议把折中解对应的节点电压分布画出来,和没装DG的初始电压曲线叠在一起对比。这张图在论文里比Pareto前沿还有说服力,它直接展示了优化的电压改善效果,评审一看就懂。
5.2 我踩过的几个典型坑
最大的一次教训是潮流计算里迭代容差写错了。前推回代法的收敛判据我一开始只判断节点电压增量,结果算出来的网损稳定在一个错误值上,目标函数曲线平滑得很,但解集里的网损数值比理论值大了百分之三十。排查了很久才发现,支路电流迭代和电压迭代需要同时判断收敛,只判电压在一些情况下会提前退出。
第二个坑是种群初始化没有考虑初始方案。完全随机生成的种群,很多个体把所有DG容量都堆到1兆瓦上限,导致初始Pareto前沿严重偏向成本大的区域。解决办法很简单:初始化时把容量分布设成从0到上限的均匀随机,而不是固定在0.1到1之间。这样初始解集覆盖广,收敛也快。
第三个坑是安装节点编号选择和导线型号耦合的问题。在配电网模型里,不同节点处线路的载流量和阻抗都不一样,容量上限如果统一设1兆瓦,某些支路可能已经过载了。我后来在约束里加了“支路电流不超过载流量”的硬约束,这才让优化结果在实际配电网中真正具备可行性。
第四个坑比较隐蔽:多目标遗传算法的性能指标,比如世代距离、超体积指标,很多人根本不算,只看Pareto前沿图就下结论。我建议在代码里补一个简单的世代距离统计,每一代记录当前Pareto前沿到参考前沿的平均距离,距离单调下降说明收敛正常。另一个是超体积指标,计算起来稍复杂,但对“解集分布是否均匀”非常敏感。
5.3 参数调整和结果稳定性建议
我常用的参数组合是这样的:种群100,迭代200,交叉率0.9,容量变量变异率0.1,节点编号变异率0.05,罚函数系数1000。这个组合在33节点系统上跑一次大概几十秒到几分钟,取决于你的潮流计算实现。如果电脑性能一般,可以先调成种群60、迭代100看趋势,再把参数加大跑最终结果。
多目标遗传算法的随机性很大,单次运行不足以支撑论文中的结论。建议同一组参数至少跑10次独立实验,统计10次里Pareto前沿的覆盖范围、折中解目标值的均值和标准差。代码里我预留了一个随机种子循环接口,每次运行固定不同的种子,方便复现和统计。没有这个稳定性统计,盲审专家很可能质疑你的结果。
还有一个工程经验:DG数量上限从2个改到5个,Pareto前沿会明显变好,但单个DG的容量会变小。如果你发现前沿分布特别稀疏,试着把DG数量上限调高1个再看;反之如果算法跑得很慢而且方案里很多DG容量只有0.05兆瓦,就说明上限设太高了。容量粒度也值得调,容量连续优化太慢的话,可以离散化成0.1、0.2、0.3兆瓦三档,问题规模小很多,工程上更接地气。
最后再分享一个实在的心得:代码能不能跑通只是第一步,能不能说服别人才是硬功夫。我每次拿到这类代码都会先盯着潮流模块看半小时,因为网上的仿真算例千奇百怪,节点编号从1开始还是从0开始、首端是电源还是末端是电源,都会让结果直接翻车。真正稳定的代码应该是把“网络拓扑参数”和“遗传算法逻辑”完全解耦,这样随换配电网算例,只要改一个数据文件就够了。如果你打算长期做配电网优化方向,建议从一开始就按这个标准来设计,后面会省下大量返工时间。