
去年做某地区电网N-1安全分析时遇到一个让我印象很深的场景系统通过了全部N-1校核但调度员依然担心——因为历史上那几次大停电没有一个是由单一元件故障直接掀翻的而是两三条支路在几分钟内相继跳闸形成连锁。事后反推很容易故障报告上明明白白写着哪几条线路先后过载、谁先跳谁后跳。但事前想提前锁定“到底是哪几个初始故障组合最能引发连锁”就没那么轻松了。这个问题的本质是组合搜索在成百上千个元件里找出那些高风险的多重故障集合。我用“随机化学”算法把这件事做成了一套Matlab工具专门用来识别引发连锁故障的多重故障集合。所谓随机化学核心是借鉴化学反应中分子碰撞、断裂、重组、合成的随机演化过程把故障集合当成“分子”在解空间里碰撞筛选最终锁定高风险组合。这套方法比全枚举快得多也比普通随机采样更有方向性。本文把这套方法完整拆开从问题建模、算法原理、Matlab代码实现到IEEE 39节点算例验证最后是工程落地时我踩过的一些坑希望对做电力系统安全分析、可靠性评估的朋友有参考价值。1. 连锁故障里的“N-k”难题为什么多重故障集合难找1.1 单重故障的“安全幻觉”与连锁故障的真实链条电网规划运行里最常用的是N-1准则任意单一元件退出运行后系统仍能稳定运行。这个准则用了很多年简单、可执行、有明确标准但它的假设是“一次只坏一个”。实际连锁故障从来不是这样发展的。典型的连锁链条是初始支路故障跳闸→潮流转移到周边并行线路→某条线路负载率超限→保护动作切除该线路→潮流再次转移→更多线路过载→最终导致大面积失负荷甚至电压崩溃。这个过程里单条线路跳闸本身并不可怕可怕的是它把潮流逼到了另一条本就接近极限的线路上。两条线路负载率都在85%、平时看起来非常安全但任意一条跳闸后另一条直接冲到170%显然扛不住。这就是N-1校验全部通过、系统却在N-2情况下崩溃的典型场景。而且连锁故障还存在“时滞”和“隐蔽性”。初始故障发生后往往是几分钟甚至几十秒之后才出现第二个跳闸从数据上看好像两个故障互不相关实际上第二跳就是第一跳的潮流转移结果。调度员很难在事前判断哪些线路对之间存在这种“耦合关系”更不用说三跳、四跳的组合。1.2 把问题形式化从故障空间到风险评估函数要识别引发连锁故障的多重故障集合首先得把它变成可计算的问题。我这里的做法是定义一个二元向量 x长度为支路数 n每个元素取0或1表示该支路是否在初始时刻发生故障。风险函数 f(x) 的输入是初始故障集合 x输出是对应连锁过程的严重度指标可以是失负荷总量、切负荷比例也可以是综合风险分值。整个搜索目标就是找max f(x)也就是在所有可能的故障集合中找到那个让后果最严重的组合。这里的 f(x) 不是一个显式公式而是嵌套了一层连锁模拟程序给定初始开断集合需要逐步计算潮流、判断过载、切除支路、再计算直到连锁停止或系统崩溃。这个问题的难点在于x 是离散变量空间大小随支路数指数增长f(x) 是非线性、非凸、带离散事件的黑箱没法求导也没法用传统凸优化方法。工程上最朴素的办法是枚举但枚举在N-4以上的组合里根本不现实所以必须引入随机搜索算法。1.3 组合爆炸的规模账本拿我常用的IEEE 39节点系统来说该系统有39个节点、10台发电机支路数46条。假设我们关心的是2到6条支路同时初始故障的情况用组合数公式 C(n,k) n! / (k!(n-k)!)规模如下初始故障数 k组合数 C(46,k)如果单次评估耗时0.5秒总耗时21035约0.5秒约9分钟315180约7590秒约2.1小时4163185约81592秒约22.7小时51370754约68.5万秒约7.9天69370480约468.5万秒约54天这还只是46条支路的系统。实际省级电网支路数动辄上千N-4以上的组合数已经是天文数字。全枚举在计算上是死路这也是为什么必须引入启发式搜索。但随机算法有个问题搜索空间太大时容易迷失方向所以需要一种既能保证“多样性探索”又能“局部精修”的机制。这也是我选择化学反应启发思路的原因——它天然的碰撞、合成、分解算子正好覆盖了粗搜和精搜两个尺度。2. 随机化学算法把故障集合搜索看作分子反应过程2.1 化学反应机制为什么适合组合搜索化学反应中分子在容器里随机碰撞能量高的分子分解成小分子能量低的小分子合成大分子系统整体朝着低能稳定状态演化。这个过程和组合优化搜索有很强的类比性不同的“分子状态”就是不同的候选解“势能”就是目标函数值“碰撞和重组”就是邻域搜索和信息交换。我最初也想直接用遗传算法GA但对比下来有一个明显差异GA的交叉变异比较“机械”子代继承了父代基因后就固定了而化学反应框架里的分解和合成是受“能量守恒”约束的只有能量上“可接受”的反应才被保留搜索过程更符合物理直觉在组合爆炸空间里不容易早熟。当然标题里的“随机化学”并不是某一篇论文的固定名词我这里的做法是把CROChemical Reaction Optimization的核心算子拿过来再针对连锁故障场景做随机化改造增加随机重启、按风险加权生成新分子、限制最大故障数等。本质上是一类受化学反应启发的随机元启发式算法适合处理这类离散组合搜索问题。2.2 分子、能量与反应容器的映射关系用一张表把算法术语和电力系统故障搜索术语对应起来化学反应概念算法/问题含义分子一个候选故障集合0/1向量分子结构该集合包含哪些支路势能PE该故障集合导致的风险严重度 f(x)动能KE搜索扰动的“力度”决定下一次碰撞改变几位反应容器所有可能的故障集合空间能量守恒接受规则新的分子状态需满足能量条件才被保留温度/激活能控制算法从“广泛探索”过渡到“局部精修”的退火因子这里有个关键点在最小化问题里势能越低越好但在连锁故障场景我定义的是风险值通常希望找到“后果最严重”的集合所以我在实现里把 f(x) 取负值作为势能。这样算法在演化中自然会趋向风险更高的故障集合。细节会在代码部分说明。2.3 四类反应算子的搜索行为解读化学反应优化算法框架下最常见的是四类算子第一类单分子无效碰撞IWC。分子和容器壁碰撞只改变分子结构中少量位置。对应到故障搜索上就是随机翻转当前故障集合中的一到两个bit在现有解附近做局部搜索。这个算子负责“精修”让算法逐步逼近局部最优。第二类分解反应DEC。一个分子能量太高对应风险高但结构不佳的解会分解成两个分子各自探索不同的子空间。在故障搜索中就是从一个高风险故障集合出发拆成两个有一定重叠的子集扩大搜索覆盖面避免所有分子扎堆在一个局部区域。第三类分子间无效碰撞IIC。两个分子交换部分结构产生两个新分子。类似遗传算法的交叉操作让两个“优秀故障集合”之间共享信息有可能组合出更严重的新集合。第四类合成反应SYN。两个分子碰撞后合并成一个分子通常是保留势能更低风险更高的部分结构再融合。这个算子负责收敛保证精英信息不丢失。这四类算子在主循环里按一定概率随机触发分子群就在“分散探索—信息交换—局部精修—精英收敛”之间动态平衡。实测下来对连锁故障的N-k搜索效果比单纯模拟退火和遗传算法都稳。3. Matlab实现细节从分子群初始化到反应主循环3.1 整体流程与数据结构设计整个Matlab程序分三层顶层是主循环中间是四个反应算子底层是连锁故障评估函数。数据结构我用结构体数组保存每个分子字段如下mol struct(... structure, [], ... % 1xn逻辑向量1表示该支路初始故障 PE, [], ... % 势能 -风险值 KE, [], ... % 动能 当前扰动步长 numHit, [0] ... % 连续未更新计数用于判断停滞 );主循环伪代码大概是function best stochasticChemistrySearch(caseData, params) % 初始化分子群 molecules initMolecules(params.numMol, caseData.nBranch, params.kmax); evaluateAll(molecules, caseData); % 计算初始PE for iter 1:params.maxIter for i 1:length(molecules) r rand(1, 2); if molecules(i).KE params.alpha % 动能大倾向于单分子反应 if r(1) params.pDec [molA, molB] decomposition(molecules(i)); evaluateMol(molA, caseData); evaluateMol(molB, caseData); accept acceptByEnergy(molecules(i), molA, molB, params); if accept, 更新分子群; end else molNew onWallCollision(molecules(i), params.nBits); evaluateMol(molNew, caseData); if acceptByEnergy(molecules(i), molNew, params) 更新; end end else % 动能小倾向双分子反应 j randi([1, length(molecules)]); if r(2) params.pSyn [molNew] synthesis(molecules(i), molecules(j)); 评估并尝试替换; else [molA, molB] interCollision(molecules(i), molecules(j)); 评估并尝试替换; end end end % 温度衰减降低动能门限 params.alpha params.alpha * params.delta; 记录全局最优; end end实际工程中我不会在每次迭代后立即更新所有分子而是先收集一批候选分子统一评估后再按能量条件接受这样能配合parfor做并行加速。3.2 连锁故障评估子函数吃性能的大头评估函数是最耗时的部分因为每个候选集合都要完整模拟一遍连锁过程。为了能跑得动我第一版用直流潮流近似大幅压缩求解时间。基本逻辑是function [pe, detail] evaluateFaultSet(x, caseData) status ~x; % statustrue表示支路投入运行 loadLoss 0; cascadeStep 0; for s 1:caseData.maxCascade cascadeStep cascadeStep 1; % 1. 直流潮流求解得到节点相角 theta solveDCPowerFlow(status, caseData); % 2. 计算各支路有功潮流 flow computeBranchFlow(theta, status, caseData); % 3. 判断过载超过额定容量1.1倍视为过载 overloaded (abs(flow) ./ caseData.rate) 1.1; % 4. 如果没有过载支路连锁停止 if ~any(overloaded) break; end % 5. 切除过载支路 status(overloaded) 0; % 6. 切除后计算失负荷量 loadLoss loadLoss shedLoadByIsland(status, caseData); end % 连续达到最大级数视为系统崩溃风险设为全负荷损失 if cascadeStep caseData.maxCascade pe -caseData.totalLoad; else pe -min(loadLoss, caseData.totalLoad); end detail.steps cascadeStep; detail.loadLoss -pe; end这里有个非常重要的细节求解器函数 solveDCPowerFlow 里不能只解线性方程组必须做孤岛检测。当某些支路被切除后系统可能分裂成孤岛导纳矩阵变成奇异矩阵直接求解会报错。我的做法是先用图遍历找出连通分量对每个孤岛单独求解孤岛内有发电机但无法外送的部分就按失负荷处理。提示评估函数里的过载阈值、最大连锁级数、是否考虑低频减载都会直接影响搜索结果。这些参数要和实际系统保护定值对齐否则算出来的“高风险故障集合”可能在实际中根本不成立。3.3 碰撞、分解、合成、置换算子的代码实现单分子碰撞的算子很简单随机翻转少量bitfunction molNew onWallCollision(mol, nBits) molNew mol; n length(mol.structure); idx randperm(n, min(nBits, n)); molNew.structure(idx) ~molNew.structure(idx); molNew.KE mol.KE * 0.9; % 碰撞消耗动能 molNew.numHit mol.numHit 1; end分解算子负责产生多样性。我的实现逻辑是把原分子的故障位分给两个子分子各保留一部分再随机补一些新故障位保证子分子既继承父本信息又不完全相同function [molA, molB] decomposition(mol, kmax) s mol.structure; n length(s); idxOn find(s); idxOff find(~s); half max(1, round(length(idxOn)/2)); % 分子A随机选取原故障位中的一半 selA idxOn(randperm(length(idxOn), min(half, length(idxOn)))); molA.structure zeros(1, n); molA.structure(selA) 1; % 分子B随机选取原故障位中的另一半允许重叠 selB idxOn(randperm(length(idxOn), min(half, length(idxOn)))); molB.structure zeros(1, n); molB.structure(selB) 1; % 各自随机补1个新故障位增加探索性 if sum(molA.structure) kmax addA idxOff(randi(length(idxOff))); molA.structure(addA) 1; end if sum(molB.structure) kmax addB idxOff(randi(length(idxOff))); molB.structure(addB) 1; end end合成算子负责收敛把两个分子的故障位取并集但如果并集超过设定的最大故障数kmax就随机删掉一些function molNew synthesis(molA, molB, kmax) molNew.structure molA.structure | molB.structure; if sum(molNew.structure) kmax idxOn find(molNew.structure); drop randperm(length(idxOn), sum(molNew.structure) - kmax); molNew.structure(idxOn(drop)) 0; end molNew.KE 0; molNew.numHit 0; end分子间碰撞算子就是交换两个分子部分位置的故障状态function [molA2, molB2] interCollision(molA, molB, nSwap) n length(molA.structure); idx randperm(n, min(nSwap, n)); molA2 molA; molB2 molB; tmp molA2.structure(idx); molA2.structure(idx) molB2.structure(idx); molB2.structure(idx) tmp; end3.4 主循环、参数表与收敛判断我实际使用的参数组合如下参数含义取值numMol分子数30maxIter最大迭代次数300kmax单个故障集合最大故障数5nBits单分子碰撞翻转位数2pDec分解概率0.15pSyn合成概率0.20alpha初始动能门限0.5delta温度衰减系数0.995maxCascade连锁模拟最大步数10收敛判断不能只看最优值有没有变。我习惯同时记录最优故障集合连续未更新的代数如果连续50代没有更新直接终止。另外因为算法本身是随机的单次运行的结果不一定可靠我的做法是固定随机种子跑若干次取出现频率最高的高风险集合作为最终候选集。后面算例部分有详细说明。4. IEEE 39节点算例算法验证与参数敏感性分析4.1 测试系统和故障集合设置我用标准IEEE 39节点系统做验证该系统的支路数据、发电机参数都可以在Matpower里直接调用。故障集合大小限定在2到5之间过载阈值设为额定容量的1.1倍最大连锁模拟步数设为10。基准功率100 MVA负荷按标准算例取值。在做算例之前我先随机抽了1000个故障集合做预评估确定了几个“已知高风险区域”——比如某些关键的联络线组合只要其中一条跳闸另一条必定过载跳闸形成双回线全停。这些预评估结果可以作为验证算法的“标尺”如果算法能找到这些已知高风险集合说明搜索方向是对的如果还能发现预评估里没发现的组合那才是真正的增量。4.2 与随机采样、遗传算法的对比结果为了说明随机化学算法不是花架子我把它和两种基准方法放在同样的评估函数下对比一是纯随机采样抽取同样数量的候选集合二是标准遗传算法种群大小相同、最大迭代相同。三项指标找到的最优风险值、达到该风险值所需评估次数、运行20次的稳定性。方法最优风险值pu失负荷平均评估次数20次运行方差随机采样0.3191000.004遗传算法0.5790000.002随机化学算法0.6687000.001随机采样的问题很明显它也能偶然碰到一些不错的组合但缺乏导向性找最优解全靠运气。遗传算法比随机采样好很多但在组合爆炸空间里容易陷入局部最优尤其当多个高风险故障集合分布在解空间不同角落时种群会过早收敛到其中一个角落。随机化学算法因为分解算子能把分子不断推向新区域合成算子又保留精英信息整体表现得最均衡。需要说明的是这里的风险值不是绝对值和负荷模型、过载阈值、保护动作逻辑密切相关。我做对比时保证三个方法用同一个评估函数所以横向比较是公平的。4.3 参数调优的实测经验调参数的时候有几个经验值得分享。分子数不要盲目加大。分子数从10提高到30效果提升明显从30提高到60提升有限但总评估次数翻倍。在评估函数比较贵的情况下我倾向于30个分子跑300代而不是60个分子跑150代因为后者的多样性提升被评估次数稀释了。kmax这个参数非常关键。它限制了单个故障集合里最多包含几条故障支路。如果kmax5算法最优解里出现1条支路的风险值一般远低于多条支路组合但kmax太大会让搜索空间急剧膨胀。我做了一组对比kmax4时找到的最优组合全部是4条支路kmax6时最优组合稳定在4到5条支路但计算时间多了30%以上。所以kmax设成5是一个好平衡点。nBits翻转位数也值得调。只翻转1位时局部搜索精细但容易长时间停留在同一个集合附近翻转2位时搜索效率最高翻3位以上则接近随机跳跃劣化明显。如果研究的是特别大的系统可以考虑让nBits随迭代次数自适应变化前期大、后期小。5. 工程落地时最容易被坑的五个细节5.1 评估函数太贵用代理模型还是并行计算连锁故障评估函数是整个流程的瓶颈。直流潮流单次求解虽然只有几毫秒但一个故障集合要迭代多轮潮流乘以几百万候选集合后计算量相当可观。我第一版跑了9201个候选集合用了近两小时后来把评估函数里的潮流求解改成稀疏矩阵预分解速度提升了一个数量级。如果系统更大就要考虑并行计算。Matlab里可以直接用parfor把分子群评估并行化代码改动很小。但要注意parfor里每个worker的随机数流是独立的如果不手动设置随机种子并行跑出来的结果可能不稳定。我的做法是在每个worker里用RandStream设置独立的子流保证结果可复现。5.2 多重故障集合的“对称性”陷阱连锁故障分析里存在一种容易被忽略的对称性如果一个故障集合里包含两条并联线路那么互换这两条线的位置风险值完全一样。算法搜索时会把大量计算浪费在等价的集合上。比如支路3和支路7的组合与支路7和支路3的组合是同一种情况但因为编码不同算法会重复访问。解决方法是维护一个“已访问集合”的哈希表。每次评估新的候选集合前先对故障位做排序后生成键值如果键值已存在直接跳过评估。我实测这个去重策略让有效计算量提升了大约25%。5.3 随机算法结果不可复现怎么办元启发式算法的通病是结果不可复现这在工程评审时非常致命。评审问“你这个高风险集合怎么来的”你不能回答“跑出来的但换个种子可能不一样”。我的做法是固定一个母种子然后用rng(seed)初始化全局随机数流。算法主循环里所有随机操作都从这个流取数。这样只要参数不变跑出来的结果完全一致。并行场景里需要对每个worker分别设置RandStream否则每次运行的worker分配顺序都可能不同结果必然波动。注意固定随机种子只能保证“实验可复现”不能掩盖算法本身的随机性。正式报告里应该同时给出多次运行的结果分布而不是只给一次的最优值。5.4 单一严重度指标可能误导搜索最初我只用失负荷量作为风险值结果算法找到的“最优”故障集合全部集中在某个重负荷区域的几条线路上。这些组合确实切了很多负荷但如果加入过载持续时间、电压越限等因素排序会发生变化。工程上更稳妥的做法是构造综合风险函数风险 失负荷量 × 概率权重 过载严重度积分。其中概率权重可以用历史统计数据近似过载严重度用每次连锁模拟中过载支路的越限比例累积求和。这样一来算法搜索到的不再是“后果最重但概率极低”的极端组合而是“后果和可能性兼有”的现实威胁。5.5 和现有N-k安全校验流程的衔接最后一点经验随机化学算法不应该是孤立工具最好嵌入到已有的安全分析流程里。我的做法是分三层第一层用随机化学算法生成top-K候选故障集合第二层对候选集做更精确的交流潮流连锁模拟第三层才把排序结果交给调度预案编制。这样既有启发式搜索的效率又有精确评估的可信度也方便和现有的N-1、N-2校核结果对比。我现在每次做连锁故障分析时已经习惯先跑一遍随机化学工具把候选集数量和计算时间控制下来然后集中算力去精校那些真正有威胁的组合。个人体会是这类问题真正难的不是写代码而是把故障模型、保护动作逻辑和算法框架结合得足够紧密否则算出来再“漂亮”的结果放到现场也无法落地。如果后面有机会我打算把天气影响因子和检修计划也加进去让候选故障集合随着运行方式变化动态更新那就是另一个值得展开的话题了。