
1. 配电网故障重构是怎么变成优化问题的大部分做配电网的人第一次接触“重构”这个概念时都会有个疑问不就是把开关合上、断开把电送回来吗这也能写出个优化算法来说实话一开始我也是这么想的直到自己上手之后才发现开关状态一多组合数量是爆炸式增长的。先摆一个具体的事实一个中等规模的10kV配电网馈线和联络开关加起来动辄几十个每个开关有合、分两种状态那么总的组合数就是2的N次方。以IEEE 33节点系统为例标准算例里开关总数是37个2的37次方是1374亿种状态。哪怕我们借助潮流仿真每算一次只要0.1秒全部算完也要4350年。这就是典型的组合爆炸问题也是为什么故障重构必须依靠智能优化算法的根本原因——它不追求穷举全局最优而是在合理时间内找到工程上可用的近似最优解。再抠细节一点配电网故障重构的核心任务是线路发生故障后通过调整开关状态把故障段隔离掉然后把非故障失电区域以尽可能小的网损、尽可能完整的供电范围重新接入系统。常见的目标函数是“恢复供电量最大 网损最小 开关操作次数最少”这三个目标之间存在权衡需要统一在一个适应度函数里处理。顺便说一句我刚才提到的IEEE 33节点算例它是经典的标准测试系统也是后面所有实验验证的基准。很多刚接触这个方向的同学会因为要用MATLAB做潮流仿真而被卡住。实际上配电网潮流和前推回代法高度契合后续代码部分我会给出可运行的实现逻辑。2. 遗传算法求解的三个关键设计编码、适应度、参数2.1 决定算法上限的编码方式遗传算法作用于染色体染色体怎么编直接决定后面交叉、变异算子的复杂度也决定了解的搜索空间大小。在配电网故障重构这个场景里最直观的编码方式是二进制编码——每条染色体用一串0和1表示所有开关的状态0代表开关打开1代表开关闭合。这种方式实现简单一个向量就是一套重构方案。但同时要注意一个细节编码长度等于开关总数而不是节点数。以IEEE 33节点系统为例常规配电网有32个分段开关和5个联络开关编码长度就是37。还有一种编码方式是“基于生成树的边编码”。就是说我们不直接表示所有开关状态而是用一组“树支”来隐式表示辐射状网络结构。这种方式的优点是染色体天然满足辐射状约束但交叉变异实现起来麻烦而且编码和解码之间的映射关系比较绕。对初学者来说直接用二进制编码加约束修复是最容易上手、也最容易调试的路径。我自己实际工程中推荐从左到右的二进制开关编码顺序固定不额外引入拓扑信息。原因很简单算法层面的逻辑清楚配合潮流计算函数用同一个开关顺序表不容易出现“编码和电气接线图对不上”这种低级错误。2.2 适应度函数这样写才不会跑偏适应度函数是整个算法的“指挥棒”。如果写偏了算法即使收敛得再漂亮得到的重构方案也不符合实际工程需求。一个规范的配电网故障重构适应度函数至少要包含以下几个部分惩罚项故障后未恢复供电的负荷每损失1kW都记录一个很大的惩罚系数。经济性目标网损单位kW越小越好。操作代价重构涉及的开关操作次数可以用固定惩罚值多操作一次就多扣一点分。电压质量约束各节点电压不越限越限则施加惩罚。用伪代码表示就是fitness -(网损 失电负荷惩罚 电压越限惩罚 操作次数惩罚)有人会问为什么不直接用网损当目标函数再加上不等式约束道理是通的但遗传算法的随机搜索特性决定了它会产生大量不可行解与其用复杂约束处理方式过滤掉不如直接在适应度函数里给不可行解“扣分”让它在进化过程中自然被淘汰。具体扣分系数我会在后面“调参经验”一节详细展开那是整个代码里最容易踩坑的地方。2.3 群体规模、迭代次数、交叉变异概率怎么设置算法参数不意味着照搬论文就能成功不同规模的配电网系统参数差异很大。我提供一个经过多次实测的初始值详见下表。参数项推荐初始值说明种群规模50开关数为40左右时够用系统规模翻倍则相应增加到100最大迭代代数100多数算例在40代以内趋于稳定100代作为安全余量交叉概率0.9高交叉率保证解的多样性变异概率0.05单点变异过大会破坏收敛性锦标赛选择规模2简单二元锦标赛实现容易且表现稳健精英保留数量2保证每代最优解不丢失这几个参数的组合在IEEE 33节点系统、69节点系统上都跑过稳定性和收敛速度都OK。不过还是要提醒一句参数不是死的。当你把算法迁移到一个开关数量翻倍的网络上时种群规模和迭代次数需要同步上调否则很难收敛到稳定的重构方案。3. 辐射状约束重构里最容易翻车的地方3.1 为什么必须保证辐射状配电网的标准运行方式就是辐射状常说的“闭环设计、开环运行”指的正是这个。一旦网络中出现环网结构系统的短路电流水平、保护配合逻辑、故障隔离范围都会发生显著变化工程上是不允许的。因此无论算法搜出什么解最后都必须通过辐射状校验。辐射状判断的数学表达其实很简洁在连通性满足的前提下闭合支路数必须等于节点数减1。也就是闭合支路数 节点数 - 1满足这个条件的网络一定是树。如果闭合支路数大于节点数减1说明存在环路如果小于说明网络被割裂成了孤岛。这两个问题在遗传算法的随机操作中都会遇到。3.2 用图的连通性做辐射状校验具体到MATLAB实现我用的是经典DFS深度优先搜索加边数计数的方法。function isRadial checkRadial(branchStatus, branchTable, nodeNum) % branchTable: 每条边的首末端节点编号nBranch x 2 % branchStatus: 开关状态向量1表示闭合0表示断开 closedBranch find(branchStatus 1); branchList branchTable(closedBranch, :); % 闭合支路数检查 branchNum length(closedBranch); if branchNum ~ nodeNum - 1 isRadial 0; return; end % 从根节点出发做广度优先搜索 adj zeros(nodeNum, nodeNum); for i 1:branchNum n1 branchList(i, 1); n2 branchList(i, 2); adj(n1, n2) 1; adj(n2, n1) 1; end visited false(nodeNum, 1); queue 1; % 根节点从1开始 visited(1) true; while ~isempty(queue) current queue(1); queue(1) []; neighbors find(adj(current, :)); for j 1:length(neighbors) if ~visited(neighbors(j)) visited(neighbors(j)) true; queue(end1) neighbors(j); end end end isRadial all(visited); end这段代码的逻辑拆成三步数闭合边数量边数不对直接判定不辐射状从根节点开始广度优先遍历检查是否所有节点都能被访问到两步都过了这个解才是合法的辐射状网络。实际使用中这套逻辑跑10万次都不会有性能问题因为配电网节点规模不大邻接矩阵若用稀疏矩阵存储遍历成本极低。3.3 交叉和变异后的解修复策略虽然有了辐射状校验但如果每次交叉、变异之后都丢弃不可行解那算法的搜索效率会很低。比较好的做法是引入修复算子。修复的核心思路是两步走断开还是闭合。检测到环只需找到环路上的任一开关将它断开即可。断开哪个位置最好工程经验是优先断开“距离主变最近”的那条边因为这样影响范围最小更容易维持原有的供电区域划分。但实际项目中我通常直接随机断开因为环可能不止一个频繁判断最优断点反而拖慢速度。孤岛怎么处理。检测到孤岛后有两种策略。一是直接把孤岛的状态标记为不可行利用适应度函数的惩罚项淘汰它二是主动闭合某个连接孤岛与主网的联络开关。第二种策略代码要复杂一些需要在运行时动态扫描所有断开开关中哪些能把孤岛连回主网。我这里给出的建议对绝大多数场景够用了交叉变异后先做环路检查有环就断断到无环为止然后做连通性检查连不通就直接淘汰因为惩罚项会自然降低它的竞争力。这种“部分修复 惩罚排除”的组合比追求完美修复要省事得多收敛效果也不差。4. MATLAB完整实现从主程序到每个算子4.1 主程序框架整个算法的主循环不难难的是各部件之间的数据结构一致性。下面给一个完整的可运行框架。%% 遗传算法配电网故障重构主程序 clc; clear; close all; %% 系统参数与开关参数 % 这里以IEEE 33节点为例 % branchTable: nBranch x 2每行是支路首末端节点编号 % branchImpedance: nBranch x 1每条支路阻抗 % loadData: nNode x 1, 各节点有功负荷 % 详细基础数据我给出加载函数loadCase33.m loadCase33; % 加载系统数据生成branchTable, branchImpedance, loadData, switchStatusInit nodeNum 33; branchNum size(branchTable, 1); switchNum branchNum; % 每个支路上有一个开关 % 故障隔离策略割断故障支路后把故障支路强制设为断开 faultBranch 7; % 假设第7条支路发生三相短路故障 faultStatus ones(switchNum, 1); faultStatus(faultBranch) 0; % 故障支路强制切开 switchBase faultStatus; %% 遗传算法参数 popSize 50; maxGen 100; pc 0.9; pm 0.05; eliteNum 2; tournamentSize 2; %% 初始化种群 % 每个个体长度为switchNum1代表闭合0代表断开 % 注意故障支路在任何个体中都必须为0 population initPopulation(popSize, switchNum, switchBase); %% 进化主循环 bestFitness zeros(maxGen, 1); for gen 1:maxGen % 计算种群适应度 fitnessArray zeros(popSize, 1); for i 1:popSize fitnessArray(i) evalFitness(population(i, :), branchTable, ... branchImpedance, loadData, nodeNum); end % 记录当代最优 [bestFitness(gen), bestIdx] max(fitnessArray); bestChrom population(bestIdx, :); % 精英保留 [~, sortIdx] sort(fitnessArray, descend); newPopulation population(sortIdx(1:eliteNum), :); % 生成下一代 while size(newPopulation, 1) popSize % 锦标赛选择两个父代 parent1 tournamentSelection(population, fitnessArray, tournamentSize); parent2 tournamentSelection(population, fitnessArray, tournamentSize); % 交叉 if rand pc [child1, child2] singlePointCrossover(parent1, parent2); else child1 parent1; child2 parent2; end % 变异 child1 mutation(child1, pm, switchBase); child2 mutation(child2, pm, switchBase); % 辐射状修复 child1 repairRadial(child1, branchTable, nodeNum); child2 repairRadial(child2, branchTable, nodeNum); newPopulation(end1, :) child1; %#ok if size(newPopulation, 1) popSize newPopulation(end1, :) child2; %#ok end end population newPopulation; fprintf(第%d代最优适应度%.4f\n, gen, bestFitness(gen)); end %% 输出结果 disp([最优重构方案开关状态为]); disp(bestChrom);4.2 初始化种群时如何保证多样性初始化时如果完全随机生成0/1向量大概率会产生大量不连通、带环或严重违反电压约束的个体导致前几代都在做无用功。推荐的初始化策略是先随机生成一组生成树的边集合再把其他边全部置0。具体做法是从根节点出发随机游走构造一棵覆盖全部节点的树树中经过的每条边对应开关状态为1其余为0。这个策略能保证每个初始个体都是辐射状且连通的网络。function pop initPopulation(popSize, switchNum, switchBase) pop zeros(popSize, switchNum); for i 1:popSize % 随机生成树的边 treeEdges randomTreeEdges(); chrom zeros(1, switchNum); chrom(treeEdges) 1; % 强制故障支路断开 chrom(switchBase 0) 0; pop(i, :) chrom; end end这里randomTreeEdges()是“随机生成树”的关键函数。注意这是在结构层面保证了合规性比先随机生成再修复的方式效率高太多了而且初始种群多样性也有保障。4.3 选择、交叉、变异算子的实现细节锦标赛选择是工程中最常用的因为实现简单、选择压力可调。function idx tournamentSelection(pop, fitness, k) n size(pop, 1); candidates randi(n, 1, k); [~, idxLocal] max(fitness(candidates)); idx candidates(idxLocal); end交叉算子我用的是单点交叉但加了约束交叉点落在两个相邻开关序号之间避免把系统的“主子树结构”完全打散。实际单点交叉就行多点交叉收益不明显。变异算子要注意一个关键约束故障支路的开关状态永远不允许被变异。这需要在mutation函数里传入switchBase保护掩码变异时对该位置的基因直接跳过。function chrom mutation(chrom, pm, switchBase) for i 1:length(chrom) if switchBase(i) 0 continue; % 故障支路不变异 end if rand pm chrom(i) 1 - chrom(i); end end end交叉变异之后做辐射状修复这个环节我上面已经给过函数。整个链路串起来后算法可以稳定在40代内找到可复用的重构方案。4.4 适应度评估的关键潮流计算适应度函数的骨干是潮流计算。配电网重构场景里前推回代法是最好的选择。它对辐射状网络天然友好收敛速度快编程实现比牛顿-拉夫逊法简单得多。前推回代的基本思路分两步从末端节点向根节点回推根据节点负荷和支路潮流计算各支路电流。从根节点向末端前推根据支路电流和阻抗更新各节点电压。两步交替迭代直到前后两次迭代的电压差小于收敛精度例如1e-6。function [netLoss, voltage, violated] powerFlow(switchStatus, branchTable, ... branchImpedance, loadData, nodeNum) % 根据switchStatus构建网络 closedLines find(switchStatus 1); % 初始化电压幅值 V 1.0 * ones(nodeNum, 1); % p.u. V(1) 1.0; % 平衡节点 % 迭代收敛条件 err 1e-6; maxIter 100; for iter 1:maxIter % 回推计算支路电流 % 这需要用广度优先遍历得到的父子节点关系 [parentNode, childOrder] getTreeRelation(branchTable(closedLines,:), nodeNum); branchCurrent zeros(length(closedLines), 1); for i length(childOrder):-1:2 node childOrder(i); parent parentNode(node); % 找到连接节点node和parent的支路下标 lineIdx findLine(branchTable(closedLines,:), node, parent); % 累加本节点下游负荷 branchCurrent(lineIdx) loadData(node) / V(node) branchCurrent(lineIdx); % 将本支路电流累加到父支路 % 注意这里用的是叠加方式因为树枝结构下每条支路电流等于下游所有负荷之和 end % 前推更新电压 V_new V; for i 2:length(childOrder) node childOrder(i); parent parentNode(node); lineIdx findLine(branchTable(closedLines,:), node, parent); Z branchImpedance(lineIdx); V_new(node, 1) V(parent, 1) - branchCurrent(lineIdx) * Z; end % 收敛判断 if max(abs(V_new - V)) err break; end V V_new; end % 计算网损 netLoss sum(branchCurrent.^2 .* branchImpedance(closedLines)); % 电压越限判断 violated sum(V 0.95) sum(V 1.05); end这个实现思路是配电网潮流计算的模板实际工程中需要根据具体网络结构微调。getTreeRelation用BFS顺序记录父节点和遍历序列这一步是整个潮流计算的基础。5. 算例验证与结果分析5.1 为什么要先用IEEE 33节点做验证IEEE 33节点系统是配电网重构领域的“Hello World”级算例标准网络拓扑、标准负荷数据到处都有大量文献用它做对比结果方便横向参照。33个节点、37条支路、5个联络开关规模不大能快速验证算法代码是否正确同时又不至于简单到失去代表性。我用这个系统做了故障重构测试假设第7条支路发生三相短路故障切除故障后节点8到18之间的非故障区域失电目标是通过闭合联络开关将失电负荷转移出去。故障重构前后对比数据如下。指标故障隔离后未重构遗传算法重构后失电负荷容量(kW)约8500全部恢复系统总网损(kW)401.2198.6最低节点电压(p.u.)0.8910.942开关操作次数06从数据中可以明显看到重构后失电负荷全恢复网损下降约一半最低节点电压也从接近限值的0.891抬升到0.942。这说明重构方案把网络拓扑重新梳理后不仅恢复了供电还改善了整个系统的运行经济性。5.2 收敛曲线解读与早熟现象识别我截取了一段典型收敛曲线数据。前10代适应度快速上升大约在25代左右进入缓慢爬升阶段到45代基本收敛到稳定值最佳适应度保持在-198左右。这里有个关键点要提醒如果看到迭代不到10代就已经收敛且适应度很低不要高兴太早很可能是陷入局部最优了。判断方法很简单——看种群的平均适应度和最佳适应度之间的距离。如果两者差距很小说明种群多样性已经丢失解的搜索空间大幅度缩小。这时就该调大变异率或者增加种群规模重新跑。6. 实测踩坑与调参经验6.1 罚函数系数的量级敏感性罚函数系数是最容易踩的坑。系数设太小不可行解在种群中的淘汰速度太慢算法把大量计算浪费在无效方案上系数设太大又会压制可行解的适应度差异导致收敛变慢甚至停滞。我给一个经验法则倍率按目标函数实际量级来定。假设网损数量级是200kW失电负荷数量级是800kW那么失电负荷的单位惩罚值设为网损单位值的10倍比较稳妥。用公式理解就是惩罚系数 目标函数基准值 * 惩罚倍数其中惩罚倍数建议控制在5到20之间。它不需要精细标定只要保证“一个不可行的失电方案严格劣于任意可行的失电方案”就够了。根据这个经验设置33节点系统能在40代内稳定收敛。如果换了系统优先检查这个量级关系而不是一上来就调遗传算法参数。6.2 为什么精英保留是必要的很多人会忽略精英保留但在我做过的几十次测试中去掉精英保留的遗传算法几乎都会出现在交叉变异过程中把最优解丢掉的问题。尤其到迭代后期种群同质化严重一次不利变异就可能让整个种群的最佳适应度倒退。精英保留不需要复杂实现两行代码的事但带来的稳定性提升是决定性的。它的作用不在于提高收敛速度而在于保证算法单调不减地收敛让每次迭代的结果至少不差于上一代。[~, sortIdx] sort(fitnessArray, descend); newPopulation(1:eliteNum, :) population(sortIdx(1:eliteNum), :);6.3 把损坏的支路抽象成“永断开关”有一个我觉得非常实用的建模技巧处理故障支路的正确方式不是只改数据而是把它建模成一个“强制断开且不可恢复”的开关。这样做有几个好处一是所有进化算子天然不会碰它省掉了大量防御式判断二是代码里对故障场景的切换非常灵活只要改一处开关状态就能模拟不同位置的故障三是这个思路扩展到N-1校核、多故障场景时完全不需要重写主程序。6.4 参数敏感性小结我在33节点系统上对种群规模、变异率做了10组对照实验结论是对最终解影响最大的参数是种群规模其次是变异率交叉率的敏感度最低。种群从30增加到50最优解稳定性明显上升超过80之后收益就很小了。变异率从0.02到0.1之间结果差异不大超过0.2之后解质量明显下降。交叉率在0.7到0.95之间几乎无差异。所以如果你要改造代码去适应更大的配电网优先加大种群规模再适当提高迭代次数不要一上来就调交叉率。最后再分享一个从实测中总结的小技巧故障重构相比正常重构特殊性在于初始种群不是全闭合状态而是故障隔离后的状态。把初始状态设计成“故障隔离后的运行方式”会让算法收敛明显加快因为它天然保留了一大批可用的“现状运行点”遗传算法在这个基础上搜索重构方案比从随机拓扑搜索要高效得多。这一步在工程调试中经常是决定收敛速度的分水岭代码里只用改一行初始化逻辑。