做过多微电网项目的人都有同感:大多数时候瓶颈不在“运行”而在“规划”。哪里架联络线、哪些微电网之间互联、要不要走冗余回路,这些拓扑设计问题一旦规模上来,组合数量是几何级数膨胀。传统的人工推演加启发式规则在小规模场景还能凑合,微电网数量到几十个、候选支路上百条之后,可以用“上亿种可能”来形容,没有一套能在解空间里高效搜索的算法,工程上基本很难往下走。
我之前在项目里把这一整块处理成了很经典的框架:基于约束差分进化算法的多微电网拓扑设计,个体用矩阵形式编码,整个寻优过程直接在Matlab里完成。这套方案的优势在于不要求目标函数连续、不要求约束可微,只要能写出适应度函数就能跑,天然适配拓扑设计这种“既有离散决策变量、又有非线性运行约束”的复杂问题。这篇文章把设计思路、算法原理、代码实现和踩坑过程完整过一遍,适合正在做微电网规划、配电网网架优化、能量管理系统里线路拓扑优化相关工作的同学参考。
1. 为什么把多微电网拓扑设计变成“矩阵优化”问题
1.1 从“单微电网”到“多微电网”:模型的维度跃迁
单个微电网的拓扑结构通常是给定的,比如一个交流母线带多个集中负荷、储能和光伏,规划时只需要往里配逆变器容量、储能功率。一旦扩成多微电网,问题性质就变了:多个微电网之间可以互联,A微网的冗余电量到底是自己储能、还是通过联络线送给B微网,B微网缺电时选择就近接入还是绕路,这些接线关系本质上变成了“一张网”的设计。我见过不少项目初期用人工排线、按负荷就近原则一个个试,节点数超过十几个以后方案数量直接爆炸,人工很难评价哪一个结构更优。
这里说的拓扑设计,一般包括三个层面:第一是决定哪些候选线路需要建设,也就是网络的连通结构;第二是决定线路的容量和型号选择,这通常和拓扑决策耦合在一起;第三是在拓扑确定之后校验电网的安全运行和功率分配。三层耦合起来,离散变量和连续变量混在一起,标准的凸优化工具并不能直接吃下。这也是我倾向于用群体智能算法做上层搜索器的原因:它不关心问题是不是凸的,团队只要把评价函数写好,算法就能在巨大的解空间里自动寻优。而矩阵表达在其中的角色,就是把拓扑的离散结构转化成算法在连续空间里可以操作的编码。
一条候选支路就是“选”或“不选”的0/1决策,全部支路拼起来就是邻接矩阵。这个思路和图的邻接矩阵定义完全一致,也符合配电网计算里常用的节点关联矩阵。用矩阵承载个体,开发者能直接调用Matlab极其成熟的矩阵运算,向量化评估比逐条支路循环快出几个数量级。这个点很多论文一笔带过,实际工程中差别非常大:同样是500个个体评估一次,用for循环可能要跑好几分钟,改成矩阵批量计算后几十秒就能出结果,直接影响一天能做多少组参数试验。
1.2 邻接矩阵:一种干净自然的编码方式
具体到编码,我常用的约定是这样的:设微电网节点数为N,候选联络支路数为M,定义决策向量x的长度为M,x(m)∈[0,1]表示第m条候选支路被选入最终拓扑的程度。与此同时把x还原成N×N的邻接矩阵A,A(i,j)=A(j,i)=1代表节点i和j之间有实际连接,其余位置为0。用“支路列表映射到矩阵”的方式,比直接用N²长度的矩阵更省内存,也更容易处理候选线路集合的工程约束。差分进化在连续空间里搜索,x(m)不会天然等于0或1,所以评估时要用阈值做二值化,再做一次对称化和去自环处理。
二值化这个操作在DE里其实是双刃剑。一刀切会产生大量“接近0.49的候选项被淘汰、接近0.51的被保留”的边界效应,导致搜索随机性偏大。我的处理方式是:在DE变异交叉阶段保留连续实数值,只在适应度函数内部投影成离散拓扑,投影之后不回写。这样既让DE的搜索空间连续光滑,又能保证最终解天然满足拓扑的离散性。这个细节看着不起眼,却直接决定了算法找得到找不到更好的解。不少复现论文失败的同行,根因就在这里——他们把决策变量完全整数化,群体搜索的“进化动力”反而被削弱了。
矩阵化的另一个好处是很多图论性质可以直接用现成函数检查。比如判断是否满足辐射状,我一般先检查支路数是否等于N-1,再用图的连通分量确认全图连通。这两条合起来就是辐射状网络的充要条件。写成代码就是graph(A)、conncomp(G)这类内置API,十几行就能完成,根本不用自己写深度优先搜索。
1.3 约束怎么进模型:等式、不等式与辐射状网络
接下来是约束部分。多微电网拓扑设计常见约束包括:节点功率平衡(等式约束)、支路容量上下限(不等式约束)、节点电压允许范围(不等式约束)、供电可靠性指标(不低于某个阈值),以及配电线路常见的辐射状拓扑约束。辐射状约束属于组合约束,在线性规划里其实不太好写,而群体算法里直接用图性质判断反而很简单:要求支路数=N-1且全图连通。它在我这套代码里以惩罚形式进入总违反量。
关于目标函数,我通常同时考虑投资成本和运行成本。投资侧包括新建联络线的年化建设成本、设备维护成本;运行侧可以用简化网损公式近似。具体实现时,如果节点规模不大,可以直接接Matpower做潮流计算;如果只想验证算法逻辑,就用线性化的网损近似。后面的代码先用贴近工程实际的简化模型展示算法骨架,也方便后续替换成正式潮流接口。
2. 约束差分进化算法与矩阵编码的配合逻辑
2.1 差分进化为什么能用来搜索拓扑
差分进化是比较老牌的群体优化算法,核心思想就是拿种群里的差分向量指导变异。它参数少、思路直观、对目标函数没有连续性要求,在电力系统这种处处非凸的领域,生存能力比梯度法强得多。标准DE有三步:变异产生新候选解,交叉混合候选解与父代,选择时保留更优个体。典型变异公式写成v = x_r1 + F×(x_r2 - x_r3),其中F是缩放因子,控制差分向量的扰动量。F太大搜索激进,F太小容易原地打转,工程上常取0.5左右,后面我会给一组参考参数。
把DE放到矩阵编码上,并不需要特别改动变异公式,只要记住操作的是向量化后的决策向量就行。每个个体是一行长度为M的向量,M等于候选支路数量。变异得到的v是实值向量,交叉时按二项式规则逐个维度抽签,抽中概率受交叉率CR控制。CR越大,子代继承变异向量的维度越多。这套流程和普通DE一模一样,区别只在评估之前要把向量还原成矩阵。所以“矩阵优化”字面上指的是个体的本质结构是矩阵,数学上它仍然是连续空间上的向量搜索。
我遇到过一种理解偏差,以为“矩阵优化”是要把DE全部改写为矩阵式的运算。其实不需要。真正要注意的是向量还原为矩阵时的维度对齐,以及二值化后的对称性修复。只要处理好这几个映射关系,DE照常运行,完全不用改算子。
2.2 约束处理:罚函数不够用,直接上可行性法则
群体算法处理约束有几种流派。最省事的罚函数法是把违反约束的量乘以很大权重加进目标函数,实现容易,但罚权重需要反复试,设置不好非可行解就会在种群早期占优势,或者反过来让搜索过于保守。我更推荐Deb的可行性法则,它在选择环节直接引入偏好:
- 两个个体都可行时,选目标函数值更小的;
- 个体A可行、个体B不可行,无条件保留A;
- 两个个体都不可行,保留总约束违反量更小的。
这个规则最吸引人的地方是没有罚权重参数,不会因为参数设置不当导致搜索失衡,在“可行域占比不高”的多微电网问题上尤其管用。我做过一个20节点、候选支路40条的算例,可行拓扑在所有状态组合里的占比经常不到3%。这时候只靠罚函数,前几十代种群会被大量不可行个体塞满,收敛速度肉眼可见地下降。换用可行性法则后,算法每一代都往约束违反量小的方向走去,即使暂时找不到全局可行解,也能先找到接近可行域的路径。
当然也要平衡一点:可行性法则太强调约束,在可行域极小的问题上有时候会损失目标函数的多样性。可以引入温和的松弛策略,比如前5%迭代代允许“少量违反但目标更优”的个体进入种群,再逐步收紧。这个在调参部分会详细讲,初版代码先用纯可行性法则就好。
2.3 从连续值到离散二值:精度修复与“投影”技巧
代码细节上,把连续向量还原成拓扑矩阵需要一套稳定流程,我拆成四步:
- 从向量x映射到矩阵A,A(i,j)=x(idx(i,j));
- 做对称化处理A=(A+A')/2;
- 对角线清零,排除节点自环;
- 按阈值把连续值转成0/1,然后进入图论约束检查。
这个流程每次评估前都要执行,所以尽量不要用三重循环,要用索引映射。我在代码里用了一张预计算的索引表,一次算好位置关系,之后每次直接用,几千次评估下来能省下相当可观的时间。还有一个坑要留意:DE变异后x可能跑到[0,1]区间外面,我用截断法直接clamp到边界,实测比“反射”或“随机重新初始化”更稳定,尤其在约束比例高的问题上。
阈值选0.5不是绝对真理。如果最终解数量总是不满N-1条支路,适当下调到0.45会提高边的存活率;如果冗余回路太多,可以上调到0.55。这个阈值本身也可以做成自适应参数,但初版不建议,变量太多反而不好对比。
3. Matlab工程实现:从建模到跑通主程序
3.1 程序整体结构怎么搭
我习惯按“数据准备—种群初始化—DE主循环—结果后处理”四段式组织工程目录,每段一个函数文件,后续调参数、换场景都方便。完整目录结构如下:
de_topology/ run_main.m % 主程序入口 initNetwork.m % 生成案例数据:节点坐标、负荷、候选支路 initPopulation.m % 初始化种群个体 fitnessEvaluation.m % 适应度与约束违反量计算 constrainDE.m % 约束差分进化主循环 plotResults.m % 收敛曲线和拓扑可视化主程序入口并不复杂,设置参数、调用函数、出图。所有参数集中在结构体里,方便批量扫参。结构大致是这样的:
%% 主程序入口 clear; clc; rng(2025); para.N = 15; % 微电网节点数 para.NE = floor(1.5 * para.N); % 候选支路数量 para.NP = 40; % 种群规模 para.F = 0.5; % 变异缩放因子 para.CR = 0.9; % 交叉率 para.maxGen = 200; % 最大迭代代数 para.alpha = 1e3; % 目标函数中投资成本权重 load('networkCase.mat'); % 场景数据,含节点坐标、负荷、候选支路 [X, fval, violation, history] = constrainDE(para, net); plotResults(net, X, fval, history);3.2 初始化与矩阵索引表
第一步不是急着随机生成个体,而是先把“向量维度和矩阵位置”的映射关系算好。以候选支路列表的形式建一个edgeSet,第m条候选支路连接(u_m, v_m),那么向量第m维就对应矩阵的(u_m, v_m)位置。初始化代码比较简单:
function X = initPopulation(NP, M) X = rand(NP, M); % 每个个体都是M维的0~1向量 end初始个体均匀随机生成,候选支路被选中的期望概率是50%。如果你对初始结构有一些工程经验,可以把随机分布中心压低到0.3附近,让初代拓扑更稀疏,减少后续调整成本;但初版用均匀分布就足够。
3.3 适应度函数:把向量还原成拓扑并计算成本
适应度函数里先做连续到离散投影,再用简化网损公式估算运行成本。我在简化模型里假设线路阻抗已知、节点注入功率已知,网损用I²R近似,这个近似在验证算法骨架阶段完全够用。真实工程可以在这个位置替换成Matpower潮流。
function [f, conV] = fitnessEvaluation(x, net) n = net.N; % 步骤1:向量 -> 邻接矩阵 A = zeros(n, n); for m = 1:size(net.edges, 1) i = net.edges(m, 1); j = net.edges(m, 2); A(i,j) = x(m); A(j,i) = x(m); end A = (A + A') / 2; A(A > 0.5) = 1; A(A <= 0.5) = 0; A(eye(n) == 1) = 0; % 步骤2:辐射状拓扑检查 conV = 0; edgeNum = sum(A(:)) / 2; G = graph(A); comps = conncomp(G); if edgeNum ~= n - 1 || max(comps) ~= 1 conV = conV + abs(edgeNum - (n - 1)) * 5; conV = conV + (max(comps) - 1) * 10; end % 步骤3:简化网损与投资成本 load_ratio = net.load; % 每个节点的负载系数 loss = 0; for m = 1:size(net.edges, 1) i = net.edges(m, 1); j = net.edges(m, 2); if A(i,j) == 1 loss = loss + net.r(m) * (abs(load_ratio(i) - load_ratio(j)))^2; end end invest = edgeNum * net.lineCost; f = net.beta * loss + invest; end这一段有几处可以再优化。循环遍历候选支路一般不超过几百条,影响很小;如果到上万条支路,建议把net.edges整理成稀疏邻接索引,用矩阵运算一次算完。另外电压差这里用了近似公式,真要做严谨慎核就要换潮流模型,我在工程落地部分会再强调。
3.4 约束差分进化主循环的实现
主循环是算法心脏。我按标准的变异—交叉—选择三步走,但在选择步骤里嵌入了可行性法则。核心代码大概长这样:
function [bestX, bestF, history] = constrainDE(para, net) NP = para.NP; M = size(net.edges, 1); X = initPopulation(NP, M); f = zeros(NP,1); g = zeros(NP,1); for i = 1:NP [f(i), g(i)] = fitnessEvaluation(X(i,:), net); end history = zeros(para.maxGen, 1); for gen = 1:para.maxGen for i = 1:NP % 变异:随机取三个互不相同的个体 r = randperm(NP, 3); while any(r == i) r = randperm(NP, 3); end v = X(r(1),:) + para.F * (X(r(2),:) - X(r(3),:)); % 交叉:二项式交叉 jrand = randi(M); u = zeros(1, M); for d = 1:M if rand() <= para.CR || d == jrand u(d) = v(d); else u(d) = X(i,d); end end % 边界处理 u = min(max(u, 0), 1); % 评估 [fu, gu] = fitnessEvaluation(u, net); % 可行性法则选择 if (g(i) == 0) && (gu == 0) if fu < f(i) X(i,:) = u; f(i) = fu; g(i) = gu; end elseif (g(i) > 0) && (gu == 0) X(i,:) = u; f(i) = fu; g(i) = gu; elseif (g(i) > 0) && (gu > 0) if gu < g(i) X(i,:) = u; f(i) = fu; g(i) = gu; end end end [bestF, idx] = min(f); history(gen) = bestF; end bestX = X(idx,:); end这段代码有几个细节容易被忽略。第一个是randperm之后要排除当前个体,处理不好会让变异包含自身,模态多样性下降。第二个是二项式交叉里jrand的作用,它保证子代至少有一个维度来自变异向量,否则交叉后可能和父代一模一样,算法原地踏步。第三个是可行性法则的判断顺序,我坚持把“都可行”“A可行B不可行”“A不可行B不可行但违反量更小”三个分支分开写,不会用一句“gu<g(i)”一刀切,避免可行个体被不可行个体挤掉。
主循环的速度瓶颈通常在适应度函数上。想加速可以把适应度函数改成批量版本,一次性评估整个种群,但代码可读性会下降。我的建议是先跑通单个体版本,确认逻辑没有毛病,再考虑性能优化。
3.5 参数设定与结果后处理
参数方面,我给出一个偏保守的参考区间,适合15到30节点规模的多微电网案例:
| 参数 | 含义 | 参考范围 | 我的常见取法 |
|---|---|---|---|
| NP | 种群规模 | 20 ~ 80 | 40 |
| F | 变异缩放因子 | 0.3 ~ 0.8 | 0.5 |
| CR | 交叉率 | 0.6 ~ 1.0 | 0.9 |
| maxGen | 最大迭代代数 | 100 ~ 500 | 200 |
| 阈值 | 二值化阈值 | 0.4 ~ 0.6 | 0.5 |
后处理阶段我会画两条东西:一条是收敛曲线,用来判断是否收敛稳定;另一条是最终拓扑图,用graph画出来,能直观看到哪些联络线被选中。Matlab画拓扑网络很简单,一行plot(graph(A), 'XData', net.x, 'YData', net.y)就能完成。如果没有节点坐标,可以生成spring layout,它会自然呈现漂亮的簇状图,和实际多微电网的聚集形态很接近。收敛曲线我建议画成半对数坐标,DE前期下降快、后期变化小,线性坐标下后期几乎看不出差别。
4. 调参、踩坑与工程落地
4.1 收敛慢、早熟:用参数和初始化自救
复现过不少差分进化改进版本之后,我发现很多“算法失效”的案例并不是算法本身不行,而是参数和初值设置有问题。多微电网拓扑场景最容易出现两个症状:一是前期种群始终找不到可行解,个体全被罚函数拉在原地;二是收敛曲线前50代猛降,之后完全不动,明显早熟。
针对第一个症状,我建议检查约束是否过于苛刻。比如“支路数必须等于N-1”这种等式约束,如果初始随机个体几乎都离它很远,DE要花很多代才能靠近。一个实用技巧是先用松弛版问题跑一遍,只优化目标不怎么加约束,得到一个大致合理的拓扑雏形,再把这个雏形作为初代部分个体,剩下个体随机生成。这种“热启动”能让收敛速度提升30%以上。
针对第二个症状,可以把F设置成随代数调节的自适应模式,前期用大F保证搜索,后期用小F精调。我常用F随代数线性从0.8降到0.3,实测比固定0.5平均下降5%左右的最终目标值。CR则反过来,前期低后期高,用来在后期保留更多交叉多样性。这些都是经验规律,换算例后还是建议用控制变量法做一组参数敏感性试验,不要盲从。
4.2 大规模矩阵运算的效率优化
当节点数涨到50以上、候选支路数百条时,适应度函数里的循环结构会变得非常拖慢。我做过一组对比:15节点跑一代大约0.3秒,50节点沿用单个体循环评估,一代可能要5秒以上,200代就是十几分钟起步,这还没算调试时反复运行的次数。大规模场景下必须做两个优化:一是对候选支路索引做预计算,把edges映射到稀疏矩阵的位置索引;二是用向量化计算替代适应度里的for循环。
比如网损计算可以写成向量形式:
A = zeros(n,n); idx = sub2ind([n n], net.edges(:,1), net.edges(:,2)); A(idx) = x; A = (A + A')/2; A = double(A > 0.5); A(1:n+1:end) = 0; lossMatrix = abs(net.Pload * ones(1,n) - ones(n,1) * net.Pload.'); loss = sum(sum(lossMatrix .* A .* net.Zmat));这样把逐条支路的for循环换成一次矩阵点乘,速度能提升一个数量级。实际工程中,我还建议先清洗掉明显没有工程意义的候选支路,例如两端距离超过线路最大允许长度的,直接从edgeSet里删掉。这不仅加快速度,还能缩小搜索空间、提高解质量。
4.3 常见报错与排查速查表
调试中常遇到的坑我整理成了一张速查表,有类似症状直接按表排查,能省很多时间:
| 症状 | 可能原因 | 排查与修复 |
|---|---|---|
| 所有个体约束违反量一直很大 | 二值化阈值或边界处理出错 | 检查A矩阵是否对称、对角线是否清零,打印中间状态 |
| 收敛曲线水平不动 | 变异向量等于当前个体 | 检查r是否排除了i,检查CR是否过小导致交叉失效 |
| 结果拓扑大量环网 | 辐射状约束没进入违反量 | 确认edgeNum==n-1和连通分量数量两个条件都写了 |
| 结果总缺支路或多余支路 | 阈值与DE搜索不匹配 | 做阈值敏感性扫描0.4~0.6 |
| 报错Input must be non-negative | sub2ind索引出现非法位置 | 检查候选支路端点编号是否越界或重复 |
| 大规模运行内存不足 | 矩阵重复创建 | 预分配所有数组,避免循环内创建大型矩阵 |
4.4 从算法验证到工程落地的几点体会
最后聊聊工程落地。很多论文里的约束差分进化算法在标准算例上效果很好,但拿到真实多微电网工程会遇到更多麻烦。第一,实测负荷是时序变化的,单一负荷场景优化出来的拓扑在高峰时段可能不适用,这时建议用多个典型日的负荷曲线做多场景加权评估,不要只取单点数据。第二,实际线路投资不是简单线性函数,和地形、走廊宽度、施工条件都有关,这些参数不准,算法再强也只是空中楼阁。我通常请造价工程师把典型线路造价折算成年化系数,再进入优化,这件事比重构算法重要得多。
另外,拓扑设计结果通常要交给详细仿真做校核,差分进化得到的是“粗优方案”,最终还要用严格潮流、稳定性、N-1校验复核。这一点我在项目里吃过亏:算法导出的拓扑在简化模型里很好,但放到正式潮流环境里,个别节点电压接近下限。后来我在目标函数里加了电压偏移惩罚项,同时规定算法输出必须再跑一遍精确潮流,不通过就淘汰,方案工程可用性提升非常明显。
还有一个经验是把中间结果“可视化”做扎实。算法跑完只给一行最优解是没法说服评审和电网侧的。我习惯把每代的目标值、违反量、支路选择率都记录下来,画成子图,配合最终拓扑图一起输出。支路选择率曲线尤其有价值,它显示某些候选联络线几乎每代都被选中,说明这些线路是拓扑骨架;另一些只在少数代出现,说明它们是边际选项。这种可视化视角能帮工程师快速定位关键线路,也方便向非算法背景的同事解释优化结果。
最后给一个实用建议:真正常用的是把差分进化作为上层生成器,下层接入潮流工具做校核,而不是把算法和潮流耦合得过紧。也就是说,差分进化负责搜索“较优拓扑候选集”,潮流工具负责验证候选集里的每个拓扑是否满足运行约束。这种分层结构让算法部分保持轻量,拟合潮流部分可以随时升级成更精确的模型,扩展性非常好。我在这套框架上做过升级,把单目标改成了多目标Pareto搜索,把静态场景换成了多场景加权,改动成本都不高,核心的矩阵编码和约束差分进化逻辑一直稳定复用。