☰
二阶锥松弛在配电网最优潮流中的MATLAB+YALMIP+CPLEX实现
2026/10/5 3:11:12 网站建设 项目流程

最近总有做配电网方向的师弟问我同一个问题:二阶锥松弛做最优潮流到底怎么落地。论文里满屏的SOCP、DistFlow、松弛紧性,真到了自己上手写程序,经常卡在IEEE33节点数据怎么组织、YALMIP里锥约束怎么写、CPLEX为什么一直报infeasible这些最基础的事情上。我干脆把自己常用的这套MATLAB + YALMIP + CPLEX算例从原理到代码完整整理了一遍,从DistFlow方程推导、二阶锥转换开始,到IEEE33节点数据准备、完整程序实现、结果校验和常见坑位排查,全程走一遍。这篇内容适合正在做配电网优化、分布式电源接入、微电网调度的研究生和工程师直接对照复现,看完你能真正理解SOCP每一步在干什么,而不是只会复制粘贴代码。

1. 为什么配电网最优潮流需要二阶锥松弛

1.1 传统交流潮流模型为什么在配电网里不好使

最优潮流(Optimal Power Flow,OPF)的本质,是在满足潮流方程、电压限值、设备容量等一系列约束的前提下,最小化某个目标函数,最常见的就是网络损耗。输电网里我们习惯用完整的交流潮流方程,包含节点电压相角、支路导纳、cos和sin三角函数,这些非线性项凑在一起,形成一个高度非凸的优化问题。

这种非凸问题用传统非线性规划方法(比如内点法)去求解,最头疼的就是初值敏感。初值给得好,能收敛到一个还不错的局部最优解;初值给得差,直接发散或者收敛到明显不合理的解。对配电网来说这个问题更严重,因为配电网有几个和输电网不一样的特点:一是R/X比值大,线路电阻大,有功和无功耦合严重,潮流方程的非线性更强;二是网络呈辐射状,节点多、分支多,但没有环网;三是低压配电网里负荷波动大,电压沿馈线下降明显,约束常常是紧的。

这意味着,我们需要一种能够把配电网潮流方程转化成凸约束的办法,让求解器能够稳定地找到全局最优解,而不是靠调初值碰运气。二阶锥松弛(Second-Order Cone Programming Relaxation)就是目前配电网领域应用最成熟、效果最稳定的方案之一。

1.2 二阶锥松弛如何把非凸约束变成凸约束

二阶锥松弛的核心思想并不复杂:把潮流方程中一个难以处理的非凸二次等式,放宽成一个凸的二次锥不等式。听起来抽象,我拆开说。

配电网最优潮流里,最麻烦的非凸项来源于线路电流和功率之间的关系。具体来说,一条支路上的电流平方 (l_{ij}),必须等于有功平方加无功平方再除以电压平方,也就是:

[ l_{ij} = \frac{P_{ij}^2 + Q_{ij}^2}{v_i} ]

这个等式,P和Q是变量,v也是变量,分母还带着变量,数学上是一个非凸等式约束。求解器面对这种约束,没办法保证全局最优。

SOCP的做法是:把等式“松弛”成不等式:

[ l_{ij} \ge \frac{P_{ij}^2 + Q_{ij}^2}{v_i} ]

然后再把这个不等式等价变形为标准二阶锥形式。为什么可以这样松弛?因为我们的目标函数是最小化网损,而网损正比于电流平方 (l_{ij}),目标函数会拼命把 (l_{ij}) 往下压。既然只允许 (l_{ij}) 大于等于右边这一项,最优解就会让它正好取到等号。这种情况下,松弛是“紧的”,松弛前后的最优解一样。

当然,这个“目标函数会把l压下去”的直觉并不永远成立。如果目标函数不是网损最小,而是购电成本最小、DG运行成本最小,或者约束条件过于宽松,也可能出现松弛不紧的情况。所以做SOCP算例,最后一定要回来检查松弛间隙,这一点我在第4部分会专门讲怎么做。

2. 算例构建:IEEE33节点系统与求解环境

2.1 为什么大家都在用IEEE33节点做算例

IEEE33节点系统是配电网领域最经典的测试算例,出自Baran和Wu在1989年发表的配网重构论文,后面几乎所有配电网研究——DG接入、无功优化、网络重构、储能调度——都会拿它作为基准系统。

这个系统的参数很有代表性:33个节点、32条支路,基准电压12.66kV,总负荷约3715kW加2300kVar,网络呈辐射状单馈线结构。最妙的是它的30号节点带了一个600kVar的重无功负荷,导致系统在无补偿状态下的末端电压明显偏低,大概在0.904p.u.左右。这意味着它在无任何调节手段时就已经接近甚至越过电压下限,非常适合用来测试电压调节类算法。

用IEEE33做SOCP入门还有一层好处:它的公开结果非常多。比如无DG、纯辐射状运行时的网损大约是202.7kW,这个数字你可以拿来验证自己写的程序是否正确。一个SOCP模型跑出来如果网损和这个数字差太远,那基本可以断定代码有问题,而不是算法有问题。

2.2 标幺化处理与基准值选择

很多初学者拿到IEEE33的原始数据就直接往约束里塞,结果CPLEX报出各种数值警告甚至infeasible。问题出在哪?原始数据里,支路电阻是0.0922Ω这种量级,负荷是100kW这种量级,导纳和功率之间差了好几个数量级。纯数值上看,这是典型的病态问题,求解器内部的容差设置很难同时照顾到所有约束。

解决办法是标幺化。针对IEEE33,通常取基准容量 (S_B = 10) MVA,基准电压 (V_B = 12.66) kV,那么基准阻抗:

[ Z_B = \frac{V_B^2}{S_B} = \frac{12.66^2}{10} \approx 16.03\ \Omega ]

所有支路阻抗除以16.03就得到标幺阻抗。负荷功率同理,1kW相当于 (1 / (10 \times 1000) = 0.0001) p.u.。电压直接用标幺电压,根节点设1.0p.u.。这样整个模型里的变量基本都在0.01到1这个区间,求解器处理起来非常舒服。

我见过太多人忽略这一步,直接用有名值建模,然后花大量时间在求解器参数上调来调去。坐标统一这件事,做优化建模永远排在第一位。

2.3 YALMIP与CPLEX的安装和验证步骤

YALMIP是一个运行在MATLAB里的免费建模工具箱,作用是把优化变量、约束、目标函数用很接近数学表达式的语法写出来,然后自动转换成底层求解器能识别的格式。CPLEX则是IBM的商业求解器,性能稳定,对二阶锥规划支持得非常好,高校一般能申请到学术版授权。

安装步骤其实很简单:

  1. 从官方渠道注册安装MATLAB,这里不展开,拿到授权后正常安装就行。
  2. 从YALMIP官网或GitHub仓库下载最新版YALMIP,解压后把整个文件夹添加到MATLAB路径中。
  3. 安装CPLEX,安装完成后找到CPLEX安装目录下的cplex/matlab文件夹,同样添加到MATLAB路径。
  4. 在MATLAB里运行yalmiptest,如果列表里能看到CPLEX的状态是可用,就说明环境配置成功。

这里有个容易踩的坑:MATLAB路径里如果同时存在多个求解器的接口,YALMIP默认会按自己的优先级选择求解器。所以当你明明装了CPLEX却一直显示在用别的求解器时,记得在调用代码里用sdpsettings('solver','cplex')显式指定。我下面给出的程序里就是这么处理的。

3. DistFlow方程到二阶锥约束:从原理到代码

3.1 DistFlow方程:配电网潮流计算的基本框架

配电网最优潮流里最常用的潮流模型是DistFlow,它专门针对辐射状网络设计,避开了完整交流潮流里那些复杂的三角函数。

对于一条从节点i流向节点j的支路,定义变量:

  • (P_{ij}):支路有功功率
  • (Q_{ij}):支路无功功率
  • (v_i = V_i^2):节点电压幅值平方
  • (l_{ij} = I_{ij}^2):支路电流幅值平方

DistFlow的三大方程如下:

有功平衡:

[ \sum_{k:(j,k)} P_{jk} = P_{ij} - r_{ij}l_{ij} - P_{Lj} + P_{Gj} ]

无功平衡:

[ \sum_{k:(j,k)} Q_{jk} = Q_{ij} - x_{ij}l_{ij} - Q_{Lj} + Q_{Gj} ]

电压降落方程:

[ v_i - v_j = 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}{v_i} ]

配电网的线路比较短,对地导纳很小,DistFlow忽略充电电容是合理的。这套方程组把一个复杂的交流潮流问题简化成了只包含实数变量的多项式方程,为后面的凸松弛打下了基础。

3.2 从非凸等式到二阶锥不等式:关键一步在哪里

上面方程组里,前三个方程都是线性约束,优化求解器很喜欢。真正麻烦的是最后一个电流定义式,它把 (P)、(Q)、(v)、(l) 四个变量用非凸的二次等式绑在一起。

SOCP的转换分两步。

第一步,把等式改成不等式:

[ l_{ij} \ge \frac{P_{ij}^2 + Q_{ij}^2}{v_i} ]

第二步,把这个不等式等价改写成标准二阶锥形式。具体来说,通过对两边进行代数变形,可以得到:

[ \left| \begin{bmatrix} 2P_{ij} \ 2Q_{ij} \ v_i - l_{ij} \end{bmatrix} \right|2 \le v_i + l{ij} ]

验证很简单,把两边平方展开:

左边 (4P^2 + 4Q^2 + (v-l)^2),右边 ((v+l)^2 = v^2 + 2vl + l^2)。两边约掉公共项,最终等价于 (P^2 + Q^2 \le vl),也就是 (l \ge (P^2+Q^2)/v)。

这个变换妙就妙在,二阶锥是一个凸集合。整个非凸最优潮流问题就变成了一个凸优化问题,CPLEX这类求解器可以保证收敛到全局最优解,不再依赖初值。

3.3 用YALMIP表示SOCP时需要注意的细节

在YALMIP里写二阶锥约束,有两种常见写法:

% 方式一:用cone函数 Constraints = [Constraints, cone([2*P(k); 2*Q(k); v(i)-l(k)], v(i)+l(k))]; % 方式二:用norm Constraints = [Constraints, norm([2*P(k); 2*Q(k); v(i)-l(k)]) <= v(i)+l(k)];

两种写法数学上等价,但我建议优先用cone。原因是cone能显式告诉求解器这是一个二阶锥约束,求解器可以走专门的SOCP算法;而norm写法虽然YALMIP也能识别,但在某些版本里有额外的转换开销,也更容易触发求解器内部的预处理问题。

还有一个细节:DistFlow里的支路方向不是随便定的。IEEE33的辐射状结构可以看作从根节点1开始向下游分支,我习惯按“起点靠近根节点、终点远离根节点”的方向组织支路数据。这样 (P_{ij}) 为正表示功率从根节点侧流向末端,功率平衡方程写起来不会乱。

4. 完整算例:MATLAB + YALMIP + CPLEX程序与结果分析

4.1 完整程序:数据准备、建模、求解、后处理

下面给出一个可以直接运行的完整程序。我在里面加了一个分布式电源场景,节点18和节点33各接一个容量400kW、功率因数0.9的DG,优化变量是DG的有功出力,目标是网损最小。这个设置比纯无DG潮流更有“最优潮流”的味道,能看出优化变量在起作用。

首先给出数据部分:

%% IEEE33节点配电网SOCP最优潮流 % 基于YALMIP + CPLEX实现,目标函数:网损最小 clear; clc; close all; %% 1. 基准值与数据输入 SB = 10; % 基准容量 MVA VB = 12.66; % 基准电压 kV ZB = VB^2 / SB; % 基准阻抗 Ohm % 支路数据: [起点 终点 R(ohm) X(ohm)] branch = [ 1 2 0.0922 0.0470 2 3 0.4930 0.2511 3 4 0.3660 0.1864 4 5 0.3811 0.1941 5 6 0.8190 0.7070 6 7 0.1872 0.6188 7 8 0.7114 0.2351 8 9 1.0300 0.7400 9 10 1.0440 0.7400 10 11 0.1966 0.0650 11 12 0.3744 0.1238 12 13 1.4680 1.1550 13 14 0.5416 0.7129 14 15 0.5910 0.5260 15 16 0.7463 0.5450 16 17 1.2890 1.7210 17 18 0.7320 0.5740 2 19 0.1640 0.1565 19 20 1.5042 1.3554 20 21 0.4095 0.4784 21 22 0.7089 0.9373 3 23 0.4512 0.3083 23 24 0.8980 0.7091 24 25 0.8960 0.7011 6 26 0.2030 0.1034 26 27 0.2842 0.1447 27 28 1.0590 0.9337 28 29 0.8042 0.7006 29 30 0.5075 0.2585 30 31 0.9744 0.9630 31 32 0.3105 0.3619 32 33 0.3410 0.5302 ]; % 负荷数据: [节点 有功(kW) 无功(kVar)],根节点1无负荷 loadData = [ 2 100 60 3 90 40 4 120 80 5 60 30 6 60 20 7 200 100 8 200 100 9 60 20 10 60 20 11 45 30 12 60 35 13 60 35 14 120 80 15 60 10 16 60 20 17 60 20 18 90 40 19 90 40 20 90 40 21 90 40 22 90 40 23 90 50 24 420 200 25 420 200 26 60 25 27 60 25 28 60 20 29 120 70 30 200 600 31 150 70 32 210 100 33 60 40 ]; nb = size(branch, 1); % 支路数 nn = 33; % 节点数 % 标幺化 r_pu = branch(:, 3) / ZB; x_pu = branch(:, 4) / ZB; PL = zeros(nn, 1); QL = zeros(nn, 1); for t = 1:size(loadData, 1) PL(loadData(t,1)) = loadData(t,2) / (SB * 1000); % kW -> p.u. QL(loadData(t,1)) = loadData(t,3) / (SB * 1000); % kVar -> p.u. end

接下来是建模和求解部分:

%% 2. 定义优化变量 P = sdpvar(nb, 1); % 支路有功 p.u. Q = sdpvar(nb, 1); % 支路无功 p.u. l = sdpvar(nb, 1); % 支路电流平方 p.u. v = sdpvar(nn, 1); % 节点电压平方 p.u. % DG变量,接在节点18和33 nodeDG = [18; 33]; Pdg = sdpvar(2, 1); % DG有功出力 p.u. pmax = 400 / (SB * 1000); % 400kW pf = 0.9; Qdg = Pdg * tan(acos(pf)); % 恒功率因数控制 %% 3. 约束条件 C = []; % 根节点电压 C = [C, v(1) == 1]; % DG出力限值 C = [C, 0 <= Pdg <= pmax]; % 电压上下限,配电网一般允许0.90~1.10 Vmin = 0.90; Vmax = 1.10; C = [C, Vmin^2 <= v <= Vmax^2]; % 支路电压降落与二阶锥约束 for k = 1:nb i = branch(k, 1); j = branch(k, 2); C = [C, v(i) - v(j) == 2*(r_pu(k)*P(k) + x_pu(k)*Q(k)) - (r_pu(k)^2 + x_pu(k)^2)*l(k)]; C = [C, cone([2*P(k); 2*Q(k); v(i)-l(k)], v(i)+l(k))]; end % 节点功率平衡 for t = 2:nn inflowP = 0; outflowP = 0; inflowQ = 0; outflowQ = 0; for k = 1:nb i = branch(k,1); j = branch(k,2); if j == t inflowP = inflowP + P(k) - r_pu(k)*l(k); inflowQ = inflowQ + Q(k) - x_pu(k)*l(k); end if i == t outflowP = outflowP + P(k); outflowQ = outflowQ + Q(k); end end gIdx = find(nodeDG == t); if isempty(gIdx) C = [C, inflowP - outflowP == PL(t)]; C = [C, inflowQ - outflowQ == QL(t)]; else C = [C, inflowP - outflowP == PL(t) - Pdg(gIdx)]; C = [C, inflowQ - outflowQ == QL(t) - Qdg(gIdx)]; end end %% 4. 目标函数:网损最小 Objective = sum(r_pu .* l); %% 5. 求解 ops = sdpsettings('solver', 'cplex', 'verbose', 2); sol = optimize(C, Objective, ops); if sol.problem ~= 0 error('求解失败:%s', sol.info); end %% 6. 结果后处理 loss_kW = value(Objective) * SB * 1000; V = sqrt(value(v)); [minV, minIdx] = min(V); fprintf('网损:%.2f kW\n', loss_kW); fprintf('最低电压:%.4f p.u.(节点%d)\n', minV, minIdx); % 根节点注入有功 pinj_pu = 0; for k = 1:nb if branch(k, 1) == 1 pinj_pu = pinj_pu + value(P(k)); end end fprintf('根节点注入有功:%.2f kW\n', pinj_pu * SB * 1000); % 功率平衡校验 Pdg_opt = value(Pdg); balance_err = (pinj_pu + sum(Pdg_opt) - sum(PL) - value(Objective)) * SB * 1000; fprintf('功率平衡校验误差:%.4f kW\n', balance_err); % 松弛间隙校验 max_gap = 0; for k = 1:nb i = branch(k, 1); gap = value(l(k)) - (value(P(k))^2 + value(Q(k))^2) / value(v(i)); if gap > max_gap max_gap = gap; end end fprintf('最大对偶间隙:%.2e\n', max_gap); % 电压剖面 figure; plot(1:nn, V, 'o-', 'LineWidth', 1.5); grid on; xlabel('节点编号'); ylabel('电压幅值 (p.u.)'); title('IEEE33节点优化后电压剖面');

把这个脚本按顺序保存成一份MATLAB文件,配置好YALMIP和CPLEX路径后直接运行即可。代码里的注释已经比较详细了,下面说一下关键部分的设计意图。

4.2 结果怎么判断:网损、电压剖面与松弛间隙

程序跑完后,你会看到终端输出几组关键数据。判断程序是否正确,我建议按下面顺序来。

第一步看求解器返回状态。sol.problem == 0表示求解成功,其他值都代表有问题,具体意思可以用sol.info查看。

第二步看网损。如果你把DG变量删掉,只跑纯无DG的SOCP潮流,网损应该是大约202.7kW,最低电压大约0.904p.u.,最低电压节点在节点18附近。这两个数字是IEEE33系统的公开标准结果,能对上,说明你的模型和数据没有问题。

第三步看功率平衡校验。程序里我做了全网功率平衡检查:根节点注入有功加上DG有功总出力,应该等于总负荷加上全网损耗。误差在kW量级的千分之一以下都算正常。这一步能一次性排查掉大部分建模错误。

第四步看对偶间隙。SOCP松弛是否精确,就看每个支路上计算出的 (l_{ij}) 与 (\frac{P_{ij}^2 + Q_{ij}^2}{v_i}) 的差距。最大间隙在10的负5次方以下,说明松弛紧,结果可信。如果间隙很大,说明松弛不紧,那就要回头审视目标函数和约束条件了。

加上DG优化后,你会看到两个现象:一是网损比无DG时下降,二是系统最低电压被抬升。DG的无功出力和有功出力是绑定的,功率因数0.9意味着它同时向系统注入无功,这对电压支撑是有帮助的。不过注入无功过多也可能导致某些节点电压逼近上限,所以DG出力不一定就会冲到400kW的满发上限,这正是最优潮流的魅力——所有变量都在约束边界上自动寻找最优点。

4.3 如何把这段代码快速扩展到自己的课题

这套程序最大的价值在于扩展性。我自己写配电网SOCP相关程序,都是在这个框架上改的。

想加储能,就在对应节点加一个新的变量 (P_s),把储能荷电状态(SOC)随时间变化的约束叠加上去,目标函数里再加上充放电惩罚项。想加OLTC有载调压变压器,就把变压器支路的电压比作为一个新变量,配合变比范围和调节代价约束。想研究三相不平衡配电网,那要把单相DistFlow换成三相DistFlow,变量从标量变成3x1向量,但SOCP的整体框架不变。

换求解器也很简单。如果你手上有Gurobi或者Mosek的授权,把solver参数从'cplex'改成'gurobi'或'mosek'就行,YALMIP会自动做语法转换。这意味着你不需要因为换求解器而重写模型。

5. 常见问题与排查技巧实录

5.1 求解器报错类问题速查

我把自己和周围人跑这些算例时最常遇到的报错整理了一下,直接看表:

报错或现象可能原因解决办法
No suitable solverYALMIP路径里没有找到CPLEX/Gurobi检查求解器是否加入MATLAB路径,用yalmiptest验证
License ErrorCPLEX授权未配置或到期检查授权环境变量,重新激活学术版授权
Infeasible problem电压约束设得太紧,或模型约束写错先放宽电压上下限,再逐条排查约束
Numerical trouble/NaN变量量级不统一,没做标幺化全部转成标幺值,检查基准容量和基准电压
求解很慢二阶锥约束写得太多或太碎用cone函数而非norm,关闭verbose看耗时
报错cone无法处理CPLEX版本过旧升级CPLEX到12.10以上版本

这里面最坑的就是Infeasible。很多初学者一看到不可行就慌了,其实排查思路很清晰:先把所有约束全部注释掉,只留根节点电压约束和最松的设备容量约束,然后一条条加回去。哪一步加上去之后问题变成不可行,哪一步就是出错的地方。用二分法排,通常十分钟内能定位。

5.2 结果异常与模型不可行的几类典型案例

第一类,网损数值对不上。IEEE33无DG标准网损是202.7kW左右,你要是算出个500kW或者20kW,先别怀疑算法,查数据。重点检查支路阻抗有没有写反、负荷单位是不是kW写成了MW、节点编号有没有对错位。我甚至见过有人把33条负荷数据粘成32条,末尾的负荷整体前移,结果当然全错。

第二类,电压下限设置不当导致不可行。IEEE33在无DG时本身末端电压就只有0.904p.u.,你要是把(V_{min})设成0.95,求解器直接给你报infeasible。这不是代码问题,是系统本身在重负荷下达不到这么高的电压下限。这时要么放宽下限到0.90,要么加DG、无功补偿装置把电压抬起来后再收紧约束。

第三类,DG功率因数设置导致约束冲突。恒功率因数控制下,DG的有功和无功是线性绑定关系。如果DG接入点的电压上限比较紧,而这个节点负荷又很小,DG发功率时就会把电压推高,导致模型无解。实践中要么把DG容量调小,要么改成无功可调的DG模型,让DG既能发无功也能吸无功。

5.3 松弛不精确怎么办

最后说一个进阶问题:对偶间隙不收敛到零。前面说过,SOCP松弛紧不紧取决于目标函数是否在推动 (l) 往下压。当目标函数不是网损最小,而是DG运行成本最小、或者某些节点注入功率有直接经济成本时,最优解处的锥约束就有可能不紧。

处理办法有三个思路。第一个思路最简单:在目标函数里加一个很小的网损惩罚项,比如 (\epsilon \sum r_{ij}l_{ij}),(\epsilon)取1e-4量级,既不影响原目标的主次,又能把松弛“拉紧”。第二个思路是求解后检查哪些支路的对偶间隙超标,然后对这几条支路单独加补偿。第三个思路是如果间隙始终大,说明这个问题的目标函数本质上不鼓励松弛紧,可以考虑更高精度的松弛,比如SDP松弛,或者用凸凹过程(CCP)做迭代修复。

从我的经验来看,配电网SOCP模型中90%以上的场景,松弛都是紧的,真正需要修复的情况很少。但检查这一步绝不能省,因为这是你的模型结果可信度的最后一道保险。

我自己做这套算例踩过最深的坑,是单位不统一导致的一小时无意义排错。当时负荷数据里混用了kW和MW,CPLEX怎么都给出一个离谱的网损结果。后来把所有量全部转成标幺值,问题瞬间就干净了。所以每次拿到一个新的系统数据,我第一件事永远是确认基准容量、基准电压,然后才动手写约束。如果你准备拿这套程序去改自己的DG选址、储能调度课题,建议先把无DG的版本跑通、跑出202.7kW这个标准结果,再往上加设备。一次只加一个变量,出问题的时候也容易定位是哪一步引入的。这个习惯,能帮你省下大量调试时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询