配电网重构这个话题,我前前后后折腾了近两个月,从单时段的33节点算例,到多时段的24小时负荷曲线,最后总算用CPLEX把一个二阶锥模型在Matlab里跑通了。过程中踩了不少坑,也把很多论文里语焉不详的地方挨个验证了一遍。这篇文章不打算泛泛聊理论,而是把整个“CPLEX+Matlab二阶锥配电网重构”的思路、建模过程、代码骨架和坑位一次性说透,希望对正在做配电网方向毕设或项目的人有帮助。
先说清楚这篇内容适合谁:已经了解电力系统潮流计算、想用精确算法实现配电网重构的硕士生、博士生,或者工程上需要做网损优化、DG接入方案评估的工程师。如果你还没听说过DistFlow方程,也没关系,下面会尽量用大白话解释建模过程,但默认你至少会用Matlab写基本脚本。
1. 为什么用二阶锥模型和CPLEX来解重构问题
1.1 配电网重构到底在重构什么
配电网重构,本质上是在一个“闭环设计、开环运行”的配电网里,通过改变分段开关和联络开关的开合状态,改变整个网络的拓扑结构,从而在保证辐射状运行、电压不越限、线路不过载的前提下,找到损耗最小或者恢复供电效果最好的那个拓扑。
这里有一个很容易被忽略的物理背景:配电网里的开关数量很有限,一个33节点系统通常只有5个联络开关加32个分段开关,但把所有可能的开关组合都枚举出来,就是2的37次方量级,大约一千多亿种组合。这还没算上潮流方程在整个网络里的耦合。所以这本质上是一个大规模组合优化问题,而且潮流部分是非线性的。
传统做法无非是遗传算法、粒子群这类启发式算法。说实话,这类算法在拓扑搜索上确实能出结果,但它有两个毛病:第一,每次迭代都要重新算潮流,33节点还好,节点数一多就慢得离谱;第二,算法本身没有最优性保证,你跑十次可能得到十个不同的“最优解”,论文里写“收敛到最优”其实是模糊的说法。所以这两年学术界和工程界都转向了基于精确求解器的建模方法,其中最有代表性的就是二阶锥规划(SOCP)加整数变量的组合,也就是MISOCP。
1.2 从启发式到精确式:二阶锥松弛的引入
配电网的潮流方程和输电网差别很大。输电网是高压环形网络,潮流方程里电压相角占主导;配电网是低压辐射状网络,线路电阻和电抗接近甚至电阻更大,所以潮流计算通常采用前推回代法,或者基于DistFlow方程。
DistFlow方程本身是精确的,但它最后会引入一个非凸约束:支路电流的平方等于支路有功平方加无功平方除以节点电压平方。这个约束让整个模型变成非凸的混合整数非线性规划(MINLP),商业求解器没法直接高效求解。二阶锥松弛的关键一步,是把这个非凸等式约束松弛成一个凸的二阶锥不等式。
这一步在数学上看着简单,工程上意义非常大:非凸问题没有可靠的全局最优算法,凸问题则有成熟的内点法;再加上0-1开关变量是整数,整个问题就变成一个混合整数二阶锥规划(MISOCP),CPLEX这类商业求解器在给定精度范围内可以保证找到全局最优解。这也是为什么近年文献里“DistFlow+二阶锥松弛”几乎成了配电网规划的标配。
1.3 为什么选择CPLEX而不是GA或Gurobi
有人会问,GA不是更简单吗?为什么非要用CPLEX?我的理由是分层的。GA解决的是开关组合搜索,它本身不懂潮流约束,你必须在GA框架外面套潮流计算,每一代个体都要调一次前推回代,相当于把优化和潮流解耦了。而CPLEX面对的MISOCP模型是同时考虑拓扑和潮流的,所有约束放在一个框架里求解,数学性质清晰,而且有MIP gap可以衡量解的质量。这一点在写论文时非常关键,因为审稿人经常会问“你的解离全局最优有多远”,你直接给出gap就能回答。
至于为什么是CPLEX而不是Gurobi,说实话两个都能解MISOCP。CPLEX在学术界有大量免费license,IBM Academic Initiative对学生很友好,而且Matlab路径配置更常见。Gurobi现在也支持二阶锥,但CPLEX在论文中的引用习惯更成熟,很多已有代码都是基于CPLEX写的,遇到问题更容易找到参考。本文里的代码用YALMIP建模,后端直接指向CPLEX,如果你装了Gurobi,改一行求解器参数也能跑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数学模型:把重构问题翻译成MISOCP
2.1 DistFlow方程和二阶锥松弛
先约定一下符号。对一个配电网,把所有支路编号为b=1,2,...,B,每条支路连接父节点i和子节点j,电阻为r_b,电抗为x_b。变量包括:节点注入有功P_j、无功Q_j,支路有功P_b、无功Q_b,节点电压幅值平方v_i,支路电流幅值平方l_b。
DistFlow方程长这样:
code复制P_j = sum(P_b) + p_j + r_b * l_b
Q_j = sum(Q_b) + q_j + x_b * l_b
v_j = v_i - 2(r_b*P_b + x_b*Q_b) + (r_b^2 + x_b^2) * l_b
l_b = (P_b^2 + Q_b^2) / v_i
前三个是潮流守恒和电压降落方程,最后一个就是非凸的来源。把它改写成二阶锥形式:
code复制|| [2*P_b, 2*Q_b, v_i - l_b] || <= v_i + l_b
这个约束等价于 v_i * l_b >= P_b^2 + Q_b^2。当网络是辐射状且目标函数是网损最小化时,这个松弛在大多数情况下是紧的,也就是说松弛前后解一致。这个“紧性”保证了我们求出来的结果是物理可实现的,而不是某个无意义的数学解。不过要注意,是否紧与网络参数和负荷水平有关,实际计算后建议做一次检查,后面会专门讲。
2.2 辐射状约束:配电网的“天然铁律”
配电网运行的前提是辐射状,也就是拓扑必须是一棵树。这个约束在模型里怎么表达?如果写成节点-支路关联矩阵的秩等于N-1,那是非凸的,对求解器非常不友好。工程上常见的做法是入度约束加连通性约束的组合。
用0-1变量z_b表示支路开关是否闭合,闭合为1,断开为0。入度约束写成:
code复制对每个非根节点j:sum(z_b) = 1,其中b是所有以j为子节点的支路
这个约束的意思很明显:每个节点只能有一个父节点,从父节点那边接受电能。但只有这个约束不够,因为它可能形成孤立的环。要保证整棵树连通,需要再加一个虚拟潮流约束:设根节点(变电站出口)向整个网络注入N-1单位的虚拟流量,每个节点消耗1单位虚拟流量,每条支路的虚拟流量上限设为一个大数,这样就能强制所有节点都从根节点连过来。这个处理方式是经过验证比较稳健的,比单纯用生成树约束好写得多。
多时段重构中,每个时段都要满足同样的辐射状约束,这就导致z_b,t在每个时刻都是一棵独立的树,开关动作约束会把相邻时刻的树串起来。
2.3 开关变量与Big-M约束耦合
在潮流方程里,如果某条支路的开关是断开的,那这条支路上的有功和无功必须强制为0。这里需要引入Big-M约束:
code复制-M * z_b <= P_b <= M * z_b
-M * z_b <= Q_b <= M * z_b
M的取值不能乱来。理论上只要大于网络中最大可能传输功率就行,但M太大会引发数值病态,导致求解器精度下降。我的经验是以系统容量为基准,设成总负荷的2到3倍。比如33节点基准容量10MVA,总负荷约3.7MW,M取10左右就够用了。这里如果M过大,CPLEX在分支定界过程中会出现严重的数值误差,表现为解不满足潮流约束或者目标函数不稳定。
另外,电压和电流的状态约束也要跟上:v_i保持在0.95到1.05的标幺值范围,l_b不超过线路热极限对应的电流平方值。
3. 单时段重构:先从33节点最小验证开始
3.1 数据准备与参数约定
单时段重构是所有后续工作的地基。算例选用IEEE 33节点系统,这是配电网重构论文里最经典的测试系统,数据在IEEE和很多开源文档里都能找到。
在Matlab里搭建数据时,建议把支路参数、节点负荷、初始开关状态都定义成独立数组,方便后面循环建模。关键参数包括:支路编号、父节点、子节点、电阻、电抗、节点有功负荷、无功负荷、初始拓扑开关状态。基准容量取10MVA,基准电压12.66kV,这样求出来的电压标幺值和网损标幺值可以直接跟文献对比。
准备工作完成后,我会先用一套手写的潮流代码验证数据本身没问题。因为很多坑其实在原始数据里就已经埋下了,比如节点编号和对母线类型定义不一致。这一步花二十分钟,能省后面调试时间的一天。
3.2 用YALMIP搭建单时段MISOCP模型
Matlab里直接写CPLEX回调建模比较繁琐,我推荐用YALMIP做中间层。YALMIP的语法风格很接近自然语言,能直接声明二阶锥约束,再交给CPLEX求解。核心代码结构是这样:
matlab复制% 声明变量
z = binvar(nbranch, 1); % 开关状态
P = sdpvar(nbranch, 1); % 支路有功
Q = sdpvar(nbranch, 1); % 支路无功
l = sdpvar(nbranch, 1); % 电流平方
v = sdpvar(nnode, 1); % 电压平方
Constraints = [];
for k = 1:nbranch
i = FromNode(k); j = ToNode(k);
% DistFlow 电压方程
Constraints = [Constraints, ...
v(j) == v(i) - 2*(r(k)*P(k) + x(k)*Q(k)) + (r(k)^2+x(k)^2)*l(k)];
% 二阶锥松弛
Constraints = [Constraints, ...
cone([2*P(k); 2*Q(k); v(i)-l(k)], v(i)+l(k))];
% Big-M 开关关联
Constraints = [Constraints, ...
-M*z(k) <= P(k) <= M*z(k), ...
-M*z(k) <= Q(k) <= M*z(k), ...
0 <= l(k) <= M*z(k)];
end
% 辐射状约束
for j = 2:nnode
idx = find(ToNode == j);
Constraints = [Constraints, sum(z(idx)) == 1];
end
% 此处还需加虚拟潮流连通性约束
% 目标函数
Objective = sum(r .* l);
ops = sdpsettings('solver', 'cplex', 'verbose', 2);
optimize(Constraints, Objective, ops);
有一个地方特别容易搞错:YALMIP的cone(x, y)定义是||x|| <= y,而且y必须是标量或和x第一维匹配。我见过不少人在这个函数上栽跟头,报错说维度不匹配,其实是因为x写成行向量了。建议统一用列向量形式。
电压约束也别漏了:0.95^2 <= v <= 1.05^2,如果漏掉,求解器可能给出一个电压越限但网损很小的结果,那在工程上是不可接受的。
3.3 结果校验:网损、电压、开关动作
单时段模型跑通后,必须和公开文献结果对一轮。IEEE 33节点标准算例的初始网损大约是202.5kW,重构后最优网损大约在139.5kW附近(不同文献的变压器出口节点处理略有差异,数值会有波动)。如果你的结果跟这个差太多,先检查数据,再检查约束。
电压方面,重构后的最低节点电压应该比初始状态高。因为重构的目标之一是改善电压分布,DGs接入后电压抬升会更明显。还有一个值得关注的指标是开关动作量。单时段重构虽然没有动作次数约束,但结果里应该能看到明显的“换路”:原来闭合的联络开关断开,对应的分段开关闭合,网络环被打开。如果结果里开关状态变化很少甚至没变化,大概率是辐射状约束写错了,把所有开关都锁在了初始状态里。
我习惯在求解后做一层“潮流反验”:把求到的开关状态作为给定拓扑,用前推回代法重新算一遍潮流,对比目标函数值是否跟CPLEX输出一致。这个环节能暴露二阶锥松弛不紧的问题。如果两者不一致,说明松弛有间隙,需要回到模型层面去查。
4. 多时段重构:把时间维拉进来
4.1 多时段模型多出哪些内容
单时段跑通后,多时段的思路就是在时间轴上复制模型。每个时段t都有一个独立的开关状态z_t、支路功率P_t和电压状态v_t,各时段之间通过开关动作次数约束串联起来。除此之外,负荷和DG出力也会随时间变化,不再是固定值。
这里有一个容易忽略的大坑:负荷是逐时变化的,但同一个时段内负荷又假设为恒定值。也就是说,24小时曲线被离散成了24个等值时段,每个时段对应一个潮流场景。这种处理方式在工程上已经足够,但如果负荷波动剧烈,建议把时段细分到15分钟或30分钟,代价是求解规模翻倍。
多时段的目标函数也变了:既要让所有时段的总网损最小,又要控制开关动作次数。最常用的做法是加权多目标:
code复制min sum_t ( sum_b r_b * l_b,t * dt ) + lambda * sum_t sum_b y_b,t
其中y_b,t是开关动作的辅助变量,lambda是罚系数,需要根据网损和开关动作的数量级来调节,通常取网损费用的百分之几到几倍。
4.2 开关动作次数约束的线性化
多时段重构里最核心的新约束是限制相邻时段开关状态的变化次数。实际配电网中,频繁操作开关会严重影响设备寿命,还会引起供电中断风险,所以这个约束不是可选项,而是硬约束。
直接写绝对值约束没法用在线性规划里,需要线性化。引入辅助变量y_b,t,要求:
matlab复制Constraints = [Constraints, ...
y(b,t) >= z(b,t) - z(b,t-1), ...
y(b,t) >= z(b,t-1) - z(b,t)];
然后加上总和约束:
matlab复制Constraints = [Constraints, sum(sum(y(:,2:T))) <= MaxAction];
这样y_b,t就是开关状态变化的指示变量,值为1表示这一时段该开关动作了。MaxAction的取值要结合工程经验,一般单次重构最多允许5到10次开关操作,如果设成1或2,求解器会很难找到可行解,甚至直接报infesible。建议先从20开始试,逐步收紧到目标值,观察网损变化曲线。
4.3 多时段模型维度膨胀与加速求解
多时段的规模比单时段大一个数量级。以33节点24小时为例,0-1变量数量是开关数乘以时段数:37*24=888个,再加上对应的辅助变量,YALMIP生成的模型文件动辄几万行约束。CPLEX默认的求解参数在这种规模下虽然能收敛,但可能会跑几十分钟甚至更久。
我做了几个有效的加速策略。第一,给CPLEX设置MIP gap容差,sdpsettings('cplex.mip.tolerances.mipgap', 0.01),把默认的1e-4放宽到1%,目标函数损失极其微小,但求解时间可以缩短一个量级。第二,提供初始可行解,用单时段重构的结果作为第一个时段的初始拓扑,后续时段从上一个时段的解做热启动,这种MIP start能让CPLEX在分支定界一开始就有一个较好的上界,大幅剪枝。第三,如果模型里每个时段都设独立的电压范围,可以用场景缩减,比如把24小时合并成典型日加尖峰日和低谷日,三个场景做代表,效果也很接近。
5. 实操中的坑与排查记录
5.1 CPLEX求解器配置和License问题
Matlab里调CPLEX最常见的问题就是路径配置失败。CPLEX的Matlab接口不在默认的path里,需要手动添加。以Windows为例,是C:\Program Files\IBM\ILOG\CPLEX_Studio_XXX\cplex\matlab\x64_win64,注意不同版本路径里的版本号不一样。添加完后,在Matlab里执行which cplexqcp,如果返回路径就说明成功了。
还有一个坑是License文件的指向。学术版的CPLEX在安装时就会配好License,但如果你把它放在非默认路径,就需要在系统环境变量里设置ILOG_LICENSE_FILE指向license文件。很多人明明装了CPLEX却报License错误,基本都是这个原因。
5.2 二阶锥松弛不紧怎么办
二阶锥松弛虽然理论上有紧性保证,但实际计算中经常遇到不紧的情况,尤其是多时段模型里,某个时段的电流平方明显大于实际物理值。一个简单的检查方法是:把求到的l_b和用P_b、Q_b、v_i算出来的值对比,即l_b_calc = (P_b^2+Q_b^2)/v_i,如果差很多,说明该支路的SOC约束没有被激活。
原因通常是目标函数没有充分激励松弛紧化。比如只有网损作为目标时,网损项包含r*l,会推动l变小,但如果某个支路上有功为负,可能出现补偿效应。解决办法有三个:一是对每个支路加一个小的惩罚项 epsilon * sum(l_b),epsilon取1e-4到1e-6;二是检查Big-M约束里l_b的上限,如果上限过松,也会导致松弛间隙;三是用线性化目标函数替代二次目标,不过这个在二阶锥框架下不太常用。
5.3 多时段求解过慢的系统性调试方法
如果24小时的模型跑了半小时还没结果,不要急着调高gap。我的调试顺序是:先跑4个小时段,确认模型正确、结果合理;再扩到8小时段,观察求解时间增长比例;最后才上24小时。如果8小时段就卡住了,问题多半在辐射状约束或Big-M上,而不是CPLEX本身。
另一个经验是:诊断不可行问题。CPLEX会返回行名和变量名,YALMIP默认的变量名是匿名的,这时候可以用constraints命令查看具体约束,或者给关键约束取名字,Constraints = [Constraints, '辐射状约束': sum(z(idx))==1]。但更快的办法是分阶段松弛:先把辐射状约束全部删掉,看是否能得出一个“含环但满足潮流”的结果;再逐步加回约束,每一步都跑一次,就能定位是哪一类约束导致不可行。
6. 小结与个人经验
这个方法整个跑下来,我最深的体会是:单时段模型是试金石,多时段模型是照妖镜。单时段能帮你快速验证建模思路和数据有没有问题,多时段才会真正暴露性能瓶颈和约束关系上的疏漏。所以不管你的项目最终目标是不是多时段,都建议先把单时段做到跟文献数值一致再往下走。
最后分享一个实用小技巧:结果输出时不要只打印网损数值,把每个时段的开关状态变化用矩阵形式记录下来,一列是一个时段,一行是一条支路,这样找开关动作最频繁的支路、分析时段关联性都非常直观。另外,如果后面要扩展DG接入或储能,只需要在节点注入功率里加入对应的时变项即可,模型主体基本不用大改,这个框架的扩展性还是很强的。
