配电网无功优化,是每个做配电网方向的人迟早会遇到的问题。IEEE33节点系统搭配二阶锥规划(SOCP),现在几乎成了配电网优化领域最经典的一套组合。我最初接触这个课题时,翻了十几篇文献,试了粒子群、遗传、内点法各种路子,最后还是老老实实回到了SOCP——不是因为别的,而是它能稳定给出全局最优解,建模过程也足够直观。这篇文章就把这套方案的原理、建模、Matlab实现和调试经验完整谈一遍,适合正在做配电网优化、写毕业论文,或是想从启发式算法转向凸优化做潮流/调度的同学参考。
1. 项目要解决什么问题:配电网无功优化的痛点
1.1 为什么配电网特别需要无功优化
配电网和输电网的运行特性差别很大。输电网线路的R/X比很小,无功主要通过线路电抗自然流动,电压水平相对平稳;配电网则不一样,线路电阻占比明显偏高,有功和无功在馈线上的耦合很强,一旦无功补偿不到位,末端电压就会像一个漏水的管子一样,越到后面越压不住。
我随便举一个实际现象:在IEEE33节点标准算例里,正常负荷下末端节点电压已经接近0.95 pu;如果负荷再往上乘1.2倍,18号、33号节点的电压能直接跌破0.93 pu。这种电压水平放在配电网里是不可接受的,轻则设备运行效率降低,重则导致末端用户设备欠压停机。而配电网里又不可能像输电网那样靠调整发电厂励磁来调压,可用的手段无非是无功补偿装置、变压器分接头、分布式电源无功支撑这几类。
所以“无功优化”的核心任务,就是在一个给定的配电网拓扑和负荷水平下,决定哪些节点投入多少无功功率,让系统电压回到合理区间,同时尽可能降低网络损耗。这个问题听起来简单,真正做起来却不轻松,因为节点电压、支路潮流、无功补偿量之间是强耦合的非线性关系。
1.2 为什么选二阶锥规划而不是别的算法
这个问题我在论坛上见过很多人问,接触这个方向之前我也有同样的疑惑。既然无功优化本质上是一个最优潮流问题,为什么主流论文都在用SOCP,而不是继续用传统内点法或者粒子群?
原因可以分三层说。第一,配电网无功优化模型本身是非凸的,传统内点法直接求解非凸非线性规划,只能用迭代逼近,理论上只能保证找到局部最优解,初始点选得不好甚至会振荡不收敛。第二,粒子群、遗传这类启发式算法虽然实现简单,但每一次适应度评估都要跑一次完整潮流,计算量很大,而且算法参数对结果影响极其敏感,同一个模型换一组参数结果就变了,说服力不足。第三,二阶锥规划通过一个巧妙的凸松弛技巧,把原本非凸的潮流约束转换成凸约束,于是整个问题变成凸优化问题。
凸优化意味着什么?意味着求出来的解就是全局最优解,有严格的数学理论兜底,不需要担心陷入局部极小值。而且SOCP的求解效率非常高,33节点的算例在普通笔记本上基本是秒级出结果。这一点在做多时段、多场景扩展时特别重要,因为规模一旦上来,算法速度就直接决定你能不能做下去。
我自己的体会是:如果你想快速验证一个配电网优化思路,或者论文需要严谨的最优性支撑,SOCP是当前性价比最高的选择。相比之下,启发式算法更适合做离散变量特别多、且对最优性要求不高的场合。
1.3 整套方案的执行路线
这个项目的完整实现路线大致分成五步:
- 把配电网参数整理成标幺值,包括支路阻抗、节点负荷、网络拓扑。
- 基于DistFlow支路潮流模型,建立节点功率平衡和电压降落方程。
- 对潮流方程中非凸的电流平方项做二阶锥松弛,得到凸可行域。
- 设置目标函数和运行约束,在Matlab中用YALMIP建模,交给SOCP求解器求解。
- 提取结果,对比优化前后的电压分布和网损,验证松弛的紧性。
后面的内容基本上就是按照这条路线逐步展开。我建议读者在动手敲代码之前,先花一点时间把第2部分的潮流模型看懂,否则后面代码里变量之间的关系会很容易搞混。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 潮流模型与二阶锥松弛原理
2.1 DistFlow支路潮流模型怎么建
配电网通常是辐射状结构,单电源从变电站母线往下供,没有环网。对这种拓扑,用DistFlow支路潮流模型最合适。它不像牛拉法那样需要维护完整的节点导纳矩阵雅可比,而是直接沿着馈线把功率和电压的关系写出来,物理意义一目了然。
考虑一条从节点i流向节点j的支路,其电阻为r_ij、电抗为x_ij,流过支路的有功功率为P_ij、无功功率为Q_ij。定义两个辅助变量:
- U_i = V_i²,表示节点电压幅值的平方;
- l_ij = I_ij²,表示支路电流幅值的平方。
DistFlow方程的基本形式是:
节点j的有功功率平衡:
[
\sum_{i \in parent(j)} (P_{ij} - r_{ij} l_{ij}) - \sum_{k \in child(j)} P_{jk} = P_{L,j}
]
节点j的无功功率平衡:
[
\sum_{i \in parent(j)} (Q_{ij} - x_{ij} l_{ij}) - \sum_{k \in child(j)} Q_{jk} = Q_{L,j} - q_{c,j}
]
电压降落方程:
[
U_j = U_i - 2(r_{ij} P_{ij} + x_{ij} Q_{ij}) + (r_{ij}^2 + x_{ij}^2) l_{ij}
]
最后一个关系式把电流平方和功率关联起来:
[
l_{ij} = \frac{P_{ij}^2 + Q_{ij}^2}{U_i}
]
前面几个方程都是线性等式,问题出在最后一个式子——它是二次等式,非凸。直接丢给求解器去解,得到的不一定是全局最优。
2.2 非凸项如何转成二阶锥约束
处理l_ij的等式约束,最经典的做法是先把它放宽成不等式:
[
l_{ij} \geq \frac{P_{ij}^2 + Q_{ij}^2}{U_i}
]
这时约束方向变成“电流平方不小于功率平方除以电压平方”,在数学上等价于:
[
l_{ij} U_i \geq P_{ij}^2 + Q_{ij}^2
]
这个不等式仍然不是标准二阶锥的形式。需要做一个等价变形。令U_i作为第一个变量,l_ij作为第二个变量,利用恒等式:
[
(l_{ij} + U_i)^2 - (l_{ij} - U_i)^2 = 4 l_{ij} U_i
]
于是原不等式可以写成:
[
(l_{ij} + U_i)^2 \geq (l_{ij} - U_i)^2 + 4 P_{ij}^2 + 4 Q_{ij}^2
]
两边开平方,就得到标准二阶锥约束:
[
\left| \begin{bmatrix} 2P_{ij} \ 2Q_{ij} \ l_{ij} - U_i \end{bmatrix} \right|2 \leq l + U_i
]
在YALMIP里,这一行可以直接写成norm([2*Pij(k); 2*Qij(k); lij(k)-U(fb(k))]) <= lij(k)+U(fb(k)),求解器会自动识别这是一个二阶锥约束。这里有个提醒:千万不要试图把锥约束展开成平方形式然后交给求解器,那样YALMIP很可能会把它当成一个非凸二次约束,整个问题性质就变了。
2.3 松弛为什么“敢”这么松:紧性分析
看到这里,你肯定会问:把等式放宽成不等式,解出来的东西还是原来那个问题的解吗?
这里的关键在于目标函数的方向。常规的无功优化目标是最小化网络损耗,也就是最小化:
[
\sum_{(i,j)} r_{ij} l_{ij}
]
这个目标函数对l_ij是单调递增的。换句话说,求解器在优化过程中天然倾向于把l_ij压到尽可能小的值。如果节点电压没有触碰到上界,那么l_ij被压到最小的时候,锥不等式恰好取到等号,也就是l_ij U_i = P_ij² + Q_ij²,松弛是紧的。这时SOCP的解就是原非凸问题的最优解。
不过在工程实践中,不能默认松弛一定紧。比如当某些节点电压越界被约束强行拉回,或者目标函数里加了奇怪的权重项时,松弛解可能出现偏差。所以每次求解完,我强烈建议检查一下锥残差:
[
\text{residual} = l_{ij} U_i - P_{ij}^2 - Q_{ij}^2
]
如果残差数量级在1e-6以下,说明松弛紧性没问题;如果残差明显偏大,就要回头检查目标函数或者约束边界了。后面第6部分我会具体说怎么排查。
3. IEEE33节点系统建模与参数处理
3.1 IEEE33节点系统长什么样
IEEE33节点系统是1989年提出的经典配电网算例,结构非常简单:12.66kV单电源辐射网,33个节点、32条支路,总负荷大约3715kW + 2300kvar。它的最大特点就是末端电压天然偏低,尤其是节点18、22、25、33这几个末梢,非常适合用来验证无功优化和调压策略。
在Matlab里获取这套数据,最省事的方法是直接用MATPOWER工具包里的case33bw函数,它把节点负荷、支路阻抗都封装好了。如果不想装MATPOWER,也可以从公开发表的IEEE33节点标准参数表中手动录入。两种方式我都用过,手动录入容易录错,尤其是支路阻抗表有32行,一个数字错了后面结果就全不对了。我建议优先用MATPOWER,等代码跑通了再手动替换数据也不迟。
这里有一个容易踩的坑:IEEE33的标准参数表通常以有名值给出,支路阻抗单位是欧姆,负荷单位是kW和kvar,而优化模型里最好用标幺值。建议基准容量取10MVA、基准电压取12.66kV,然后把所有参数折算到标幺值。不要为了省事直接用有名值建模,否则电压平方和功率的数值量级能差出好几个数量级,求解器数值稳定性会很差。
3.2 目标函数与补偿装置建模
本项目里的无功补偿装置,我建议先按连续可调容性无功源来建模,可以想象成SVG或者DSTATCOM这类电力电子设备。为什么强调连续?因为SOCP要求所有优化变量是连续的。如果非要用传统电容器组,那就得引入0-1整数变量,问题会升级成混合整数二阶锥规划(MISOCP),求解难度直接上升一个台阶。做毕业设计或者论文复现的话,用连续无功源基本够用了。
目标函数我习惯写成:
[
\min \sum_{(i,j)} r_{ij} l_{ij} + \lambda \sum_{j} q_{c,j}
]
第一项是系统网损,第二项是对无功补偿量的微小惩罚。为什么要加这个惩罚项?因为如果不加,目标函数只看网损,求解器往往会把所有可用无功容量全部用满,补偿量饱和,虽然网损也很低,但工程上不太合理。加一个很小的λ,比如0.001到0.01之间,就能让补偿量收敛在一个合理的水平,又不会明显影响网损优化效果。
补偿节点的选择,最直接的方法是先跑一次不带补偿的潮流,把电压低于0.97 pu的节点挑出来。在IEEE33里,节点18、22、25、33这四条支路末端基本都是电压最低点,把补偿装在这几个位置效果最明显。
3.3 约束条件完整清单
整个优化模型的约束条件可以整理成下面这个表:
| 约束类型 | 表达式 | 说明 |
|---|---|---|
| 电压幅值约束 | U_min ≤ U_j ≤ U_max | 标幺值下一般取0.95²到1.05² |
| 平衡节点约束 | U_1 = 1.0² | 变电站母线电压固定 |
| 支路功率约束 | 节点功率平衡方程 | 用DistFlow线性方程表达 |
| 二阶锥约束 | |2P, 2Q, l-U| ≤ l+U | 由电流与功率关系松弛而来 |
| 补偿容量约束 | 0 ≤ q_c ≤ q_c,max | 受装置容量限制 |
电压约束这块要注意上下限的平方关系。很多初学者容易写成0.95 <= U <= 1.05,实际上U代表的是电压幅值的平方,应该写成0.9025 <= U <= 1.1025。两种写法得到的可行域完全不同,后者才是正确的。
4. Matlab+YALMIP代码实现详解
4.1 环境准备与数据输入
先说明一下环境需求。我这里用的是Matlab R2021a以上版本,优化建模工具是YALMIP,求解器推荐Mosek。如果没有Mosek授权,用SeDuMi或者SDPT3也能跑,只是速度和稳定性会差一些。Gurobi也支持SOCP,效果也不错,不过需要额外配置。
安装顺序有讲究:先把Matlab装好,再把YALMIP解压到任意目录并添加到路径,最后安装求解器。装完以后在Matlab命令行运行yalmiptest,如果能正常返回,说明YALMIP和求解器都识别成功了。这一步别跳过,我见过不少人代码写得没问题,结果卡在求解器没被YALMIP识别上。
数据输入部分,假设你已经拿到了IEEE33的标准参数,整理成下面这样的向量:
matlab复制%% IEEE33节点数据输入(示例格式)
% 支路首端节点、末端节点
fb = [1; 2; 3; 4; 5];
tb = [2; 3; 4; 5; 6];
% 支路电阻、电抗(标幺值,基准容量10MVA,基准电压12.66kV)
r = [0.0092; 0.0493; 0.0366; 0.0381; 0.0819];
x = [0.0047; 0.0251; 0.0186; 0.0194; 0.0707];
上面只展示了前5条支路的格式,完整32条支路数据可以从标准参数表中获取,格式完全一致。这里我特别提醒:如果你用MATPOWER的case33bw自动提取数据,要注意负荷单位是MW/Mvar,而IEEE33标准算例里经常写成kW/kvar,不要忘记归一化到同一套基准值。
4.2 核心优化模型代码
下面这段代码是整套方案的核心模型部分,可以直接复制到脚本里运行。前提是你已经准备好fb、tb、r、x、Pload、Qload六个数据向量。
matlab复制%% 变量定义
nb = 33; % 节点数
nl = 32; % 支路数
U = sdpvar(nb, 1); % 节点电压幅值平方
Pij = sdpvar(nl, 1); % 支路首端有功
Qij = sdpvar(nl, 1); % 支路首端无功
lij = sdpvar(nl, 1); % 支路电流平方
qc = sdpvar(nb, 1); % 节点无功补偿量
%% 约束条件搭建
Constraints = [];
% 平衡节点电压
Constraints = [Constraints, U(1) == 1.0];
% 非平衡节点功率平衡约束
for j = 2:nb
in_p = 0; in_q = 0; out_p = 0; out_q = 0;
for k = 1:nl
if tb(k) == j
in_p = in_p + (Pij(k) - r(k) * lij(k));
in_q = in_q + (Qij(k) - x(k) * lij(k));
end
if fb(k) == j
out_p = out_p + Pij(k);
out_q = out_q + Qij(k);
end
end
Constraints = [Constraints, in_p - out_p == Pload(j)];
Constraints = [Constraints, in_q - out_q == Qload(j) - qc(j)];
end
% 电压降落方程
for k = 1:nl
i = fb(k); j = tb(k);
Constraints = [Constraints, ...
U(j) == U(i) - 2*(r(k)*Pij(k) + x(k)*Qij(k)) + (r(k)^2 + x(k)^2)*lij(k)];
end
% 二阶锥约束(核心)
for k = 1:nl
i = fb(k);
Constraints = [Constraints, ...
norm([2*Pij(k); 2*Qij(k); lij(k) - U(i)]) <= lij(k) + U(i)];
end
% 电压上下限约束
Umin = 0.95^2; Umax = 1.05^2;
Constraints = [Constraints, Umin <= U(2:end) <= Umax];
% 无功补偿容量约束
qc_max = zeros(nb, 1);
comp_nodes = [18; 22; 25; 33]; % 电压最低的末梢节点
qc_max(comp_nodes) = 0.05; % 每个节点最大0.05 pu,即0.5 Mvar
Constraints = [Constraints, 0 <= qc <= qc_max];
%% 目标函数
lambda = 0.001;
obj = sum(r .* lij) + lambda * sum(qc);
%% 求解
ops = sdpsettings('solver', 'mosek', 'verbose', 2);
sol = optimize(Constraints, obj, ops);
代码逻辑上要特别注意两点。第一,功率平衡约束里的方向我写得很明确,in_p表示从父节点流入该节点的有功,out_p表示该节点流向子节点的有功,它们之间的差值等于节点净负荷。第二,二阶锥约束里的索引是对齐支路首端节点的,也就是U(i)里的i = fb(k),写错位置会导致锥结构不对。
4.3 结果输出与可视化
求解完成后,通过value()函数取出各变量的数值,然后就可以画图对比优化前后的电压曲线了。
matlab复制%% 结果提取
U_opt = value(U);
Pij_opt = value(Pij);
Qij_opt = value(Qij);
lij_opt = value(lij);
qc_opt = value(qc);
loss_opt = sum(r .* lij_opt); % 标幺值网损
loss_kW = loss_opt * 10 * 1000; % 转换为kW
%% 电压分布曲线
figure;
plot(1:nb, sqrt(U_opt), 'b-o', 'LineWidth', 1.5);
xlabel('节点编号'); ylabel('电压幅值 (pu)');
grid on;
title('SOCP无功优化后的电压分布');
如果想对比优化前的基线数据,建议单独用前推回代潮流程序跑一组不接补偿装置的DistFlow,把电压和网损记录下来,再和SOCP结果画在同一张图里。这样结果看起来会很直观,论文里放这样的对比图也很有说服力。
5. 仿真结果分析与效果评估
5.1 电压改善效果对比
在IEEE33节点标准工况下,不装任何无功补偿时,系统末端电压是比较难看的。节点18、33这些末梢电压普遍在0.95 pu左右,部分重载场景下甚至更低。投入无功补偿之后,末端电压通常能提升到0.98 pu以上,如果补偿容量给得比较足,逼近1.0 pu也不是不可能。
这里要注意一个物理规律:无功补偿对电压的支撑是“就近有效,远山救不了近火”。你给末端节点补无功,影响最大的就是它自己和相邻节点;如果你把同样的无功容量放在变电站出口附近,对末端电压的改善效果会差很多。这就是为什么补偿位置的选择比补偿容量还重要。
我在实际操作中习惯画两张图,一张是优化前后的电压分布曲线,另一张是各节点补偿量柱状图。电压曲线能看出整体调压效果,补偿量柱状图能看出求解器把无功“分配”到了哪些位置,两者结合起来,方案合理性一目了然。
5.2 网损优化结果与补偿容量
网损是评价无功优化效果最直接的指标。IEEE33标准工况下,不补偿时的系统网损大约在202 kW左右,具体数值会因为基准功率、负荷数据来源略有差异。加入SOCP无功优化后,我跑出来的典型结果能降到160~175 kW之间,降损幅度大约15%~20%。
不过复现的时候不要过分纠结绝对数字,因为网损对补偿节点位置、补偿容量上限、目标函数里λ的取值都很敏感。比如你只选一个补偿节点和选四个补偿节点,结果差别就很大。下面是一个典型的补偿点数量对比:
| 补偿方案 | 网损/kW | 相对不补偿降幅 |
|---|---|---|
| 不补偿 | 202 | - |
| 1个补偿点 | 185 | 约8% |
| 2个补偿点 | 174 | 约14% |
| 4个补偿点 | 165 | 约18% |
可以明显看到,补偿点从1个增加到4个,网损下降幅度在递减,这就是边际效应。实际工程中不可能无限装补偿装置,所以应该结合网损收益和装置成本做一个平衡。SOCP模型能很快帮你试出不同补偿配置下的网损下限,这是它特别有价值的地方。
5.3 几个容易影响结果的细节
第一个细节是补偿容量上限。别把单点补偿上限设得太大,比如在末端节点直接给到0.1 pu(1 Mvar),求解器可能会让这个节点无功超发,出现电压过高的问题,而且这种解在工程上毫无意义。一般单点0.03~0.05 pu比较合理。
第二个细节是λ惩罚系数。如果你把λ设成0,理论上网损会最低,但补偿量往往饱和;如果你把λ设得太大,比如0.1,目标函数会过度抑制无功补偿,网损优化效果就被削弱了。我的经验是λ保持在0.001~0.01之间,输出的解既满足降损目标,补偿量也相对克制。
第三个细节是电压上限。如果不补偿的时候末端电压很低,加补偿之后电压会被拉起来,但如果某条支路本身压降很小,补偿略微一加电压就顶到1.05 pu上限,这时求解器会自动限制该节点的补偿量。这种情况不算bug,反而说明电压约束在工作。
6. 常见问题与实战排坑记录
6.1 环境和求解器问题
运行YALMIP建模时最常遇到的就是“求解器没找到”。典型报错是No solver found或者SeDuMi not installed。这种情况先运行yalmiptest看输出,如果列表里没有Mosek,说明YALMIP根本没找到你的求解器。解决办法是重新把求解器所在目录加入Matlab路径,重启Matlab再试一次。
还有一个容易忽略的问题是Mosek许可证。学术版许可证需要单独激活,如果许可证过期,YALMIP会报一堆看不懂的错误。建议先单独在命令行运行mosekopt测试一下,确保Mosek本身没有健康问题,再回到YALMIP建模型。
6.2 建模和数值问题
求解结果显示Infeasible problem是初学者最容易慌的错误。我遇到过的原因主要有三个:
- 补偿容量上限设得太小,同时电压下限约束又比较紧,可行域被挤没了。
- 功率平衡约束里的负荷符号方向搞反,导致系统功率不平衡。
- 电压约束写成了
0.95 <= U <= 1.05,而U是电压平方,可行域严重偏小。
排查思路很简单:先放宽电压约束和补偿容量约束,如果问题变成可行,说明是约束互相矛盾;如果仍然不可行,再检查功率平衡约束的符号。
还有一个数值层面的坑是求解器警告Numerical issues。这种问题在IEEE33这样的小系统里不常见,但在更大规模系统里经常出现。解决办法是把基准值统一、避免过大过小的数量级同时出现,同时适当降低求解器容差,比如sdpsettings('mosek_param_MSK_DPAR_INTPNT_CO_TOL_PFEAS', 1e-7)。
6.3 验证你的结果可信不可信
求解出结果之后,一定要做两步验证,否则论文和报告里放的结果很可能经不起推敲。
第一步是锥松弛紧性检查。按第2.3节的方法,逐条支路计算lij_opt .* U_opt(fb) - Pij_opt.^2 - Qij_opt.^2。如果残差接近零,说明松弛紧,SOCP解可信;如果残差很大,说明锥被“撑开了”,解需要重新审视。
第二步是用独立潮流程序验证。把SOCP求出的补偿量代入前推回代潮流程序,重新跑一次潮流,对比两次计算得到的节点电压和网损。如果SOCP解是可信的,两种方法算出来的电压曲线应该高度吻合。这一步骤花不了多少时间,但能极大增强结果的可靠性。我每次做完优化都会顺手做一次这个校验,习惯了之后根本不觉得麻烦。
关于目标函数选择的影响,再补充一句:如果你后续想扩展成考虑分布式电源的配电网优化,SOCP这套框架依然适用,把DG的有功无功出力作为决策变量加进去就行。模型结构不用推翻重来,这也是我喜欢用SOCP做配电网优化的原因之一——它给了你一个可以持续扩展的底座,而不是一次性的死代码。
最后再分享一个小技巧。我刚开始复现这类论文时,总是想一步到位把完整代码写出来,结果出了问题根本不知道是建模错还是数据错。后来我学乖了,先跑一组不带补偿的DistFlow,用前推回代法求出基准电压和网损,再用SOCP模型以零补偿容量作为输入去解,两个结果对比一致之后,才把补偿变量和补偿约束加进去。每一步增量都很小,问题能在第一时间暴露出来。如果你也正在被这个课题折腾,我强烈建议按这个节奏来,能少走很多弯路。
