简介:面向主动配电网运行优化研究者与工程师的MATLAB代码包,基于混合整数二阶锥规划(MISOCP)与YALMIP工具箱,针对主动配电网最优潮流问题提供完整建模与求解实现。代码涵盖分布式电源、储能系统、电动汽车及无功补偿装置等主要元件的出力特性分析与可调潜力建模,可模拟“源—网—荷—储”多时间尺度协同优化,在保障配电网安全稳定运行的前提下,兼顾经济效益最优与可再生能源最大化消纳,同时有效缩减潮流峰谷差,实现“源、荷、储”协同运行。压缩包内共3个文件,以两个.m主程序文件为核心,辅以IEEE33节点配电网结构示意图,整体大小仅139KB,轻量易读,便于直接运行、测试并扩展为不同调度策略;两个主程序可对照拓扑图理解配置与收敛结果,便于科研复现。目前已有449人学习,可作为主动配电网多源协同优化、MISOCP最优潮流方向的参考实现,尤其适合需要快速搭建YALMIP求解框架、进行论文复现或课题设计的硕博生与工程研究人员。
1. MISOCP主动配电网最优潮流:为什么值得下载这套matlab代码
先说结论:主动配电网的最优潮流,难点不在“潮流”,而在“离散”。光伏出力波动、负荷随机性、OLTC分接头和电容器组投切——这些决策变量里有连续量也有整数量,目标函数是网损或电压偏差。直接拿连续优化器去跑,要么卡在局部最优,要么整段求解过程不收敛,看fmincon的输出像看黑匣子一样玄学。这套基于matlab和yalmip的开源代码,把问题建成混合整数二阶锥规划(MISOCP)模型,交给Cplex/Gurobi这类商用求解器,一次求解就能拿到全局最优解,而且自带IEEE 33节点算例,跑通后改参数就能用到自己的网架上。适合配电网方向的研究生、做分布式电源接入评估的工程师,以及刚接触最优潮流想找一套可复现代码的人。
2. 问题建模:把非凸潮流方程变成可求解的SOCP,再混合整数化
2.1 DistFlow潮流方程与二阶锥松弛:非凸项在哪
配电网最优潮流的第一道坎是潮流方程本身。完整的交流潮流方程里,节点注入功率与节点电压、相角之间是二次关系,非凸,NP-hard。对辐射状配电网,业内更常用DistFlow(分支潮流)方程来描述。以支路ij为例,三个核心等式:
- 有功平衡:(P_{ij} - I_{ij}^2 r_{ij} = \sum P_{jk} + P_{j}^{load} - P_{j}^{gen})
- 无功平衡:(Q_{ij} - I_{ij}^2 x_{ij} = \sum Q_{jk} + Q_{j}^{load} - Q_{j}^{gen})
- 电压降落:(V_j^2 = V_i^2 - 2(r_{ij}P_{ij} + x_{ij}Q_{ij}) + (r_{ij}^2 + x_{ij}^2) I_{ij}^2)
非凸项藏在最后一个等式里:(I_{ij}^2 = (P_{ij}^2 + Q_{ij}^2)/V_i^2),这是个二次等式约束。二阶锥松弛的常见做法是引入变量替换:令 (v_i = V_i^2),(l_{ij} = I_{ij}^2),然后用不等式代替等式:
[ | \begin{bmatrix} 2P_{ij} \ 2Q_{ij} \ v_i - l_{ij} \end{bmatrix} |2 \leq v_i + l{ij} ]
这个约束是凸的二阶锥约束。从等式变成不等式,物理含义是“允许线路电流虚高”,但在辐射状配电网的模型里,只要目标函数是网损最小或电压偏差最小,最优解处该不等式通常取等号,这就是文献里常说的“松弛紧”。把原问题从非凸变成二阶锥规划(SOCP),求解器能找到全局最优解。
我一般不建议直接跳过推导去写代码。理解这一步的关键是:松弛不是近似,而是把可行域“凸化”了,真正松不松,后面要靠对偶间隙去验证。这一点在第6章会展开讲。
2.2 整数变量从哪来:OLTC分接头、电容器组与DG投切
光有SOCP还不够。主动配电网与被动配电网的本质区别,在于“主动”二字——调度端可以主动调节有载调压变压器(OLTC)的分接头档位、投切电容器组、调整分布式电源出力、控制储能充放电。这些动作里,OLTC分接头是离散整数变量,电容器组投切是整数或0-1变量,DG越限切除也是0-1决策。
这就让模型从SOCP升级为MISOCP:连续变量描述潮流和DG出力,整数变量描述档位和投切。为什么要用MISOCP而不是把整数变量松弛成连续变量?举个例子:OLTC分接头调一档是0.025倍电压,你松弛成连续变量,优化器会给你一个0.013倍的“中间档位”,现场设备执行不了。整数的语义不能丢。
2.3 目标函数与权重:网损优先还是电压优先
目标函数决定了整个优化方向,也直接影响二阶锥松弛的紧度。常见的目标函数有三类:
- 网损最小:(\min \sum_{(i,j)\in E} l_{ij} r_{ij})
- 电压偏差最小:(\min \sum_{i} (v_i - v_{ref})^2)
- 运行成本最小:考虑购电成本、DG出力和储能损耗,各项乘以价格系数
实际项目里更多是加权组合。权重怎么定?我一般先跑一版纯网损最小,看电压分布是否合格,再跑一版纯电压偏差最小,看网损涨多少,最后根据电网调度考核指标折中。代码里用一组权重向量alpha控制,改起来很直接。
% 目标函数:网损 + 电压偏差加权,alpha为权重向量 Cost = alpha(1) * sum(l .* r) + alpha(2) * sum((v - v_ref).^2); F = [F, Cost]; % F为优化目标句柄这段代码里l .* r是支路电流平方乘以支路电阻,逐支路累加得到网络总有功损耗;v - v_ref是各节点电压平方与基准电压平方的偏差。实际使用时,如果重电压质量而轻经济性,就把alpha(2)调大,比如从0.5调到1.5,网损项的权重相应调小。权重没有绝对标准,以调度侧的实际考核指标为准。
3. YALMIP建模与求解:从变量声明到Cplex调用的完整流程
3.1 环境准备:matlab、YALMIP与支持MISOCP的求解器
在动手改代码之前,先把环境搭对。这套代码依赖三个部分:matlab本体、YALMIP建模工具箱、以及一个支持混合整数二阶锥规划的求解器。
YALMIP是Lofberg开发的建模工具,它不负责求解,只负责把数学模型翻译成求解器能识别的问题。求解MISOCP需要商用求解器支持二阶锥和整数,目前主流的三个选择是:
| 求解器 | 最低版本要求 | MISOCP支持 | 许可证方式 |
|---|---|---|---|
| Gurobi | 9.0+ | 完整支持 | 学术免费/商用收费 |
| Cplex | 12.8+ | 完整支持 | 学术免费/商用收费 |
| Mosek | 9.0+ | 完整支持 | 学术免费/商用收费 |
我个人优先用Gurobi,求解速度快,YALMIP接口最稳。安装完求解器后,在matlab里配置一次:
addpath(genpath('D:\yalmip')); % YALMIP路径,按实际安装位置改 addpath(genpath('D:\gurobi')); % 求解器路径 savepath; % 保存路径,下次启动自动加载 yalmiptest; % 测试YALMIP是否识别到求解器yalmiptest会列出每个求解器的可用状态,看到Gurobi: available这行就说明环境没问题。千万别跳过这一步直接跑算例,很多莫名其妙的报错都源于求解器没被YALMIP识别到。
3.2 变量与约束的YALMIP写法
YALMIP建模的核心套路是:先声明变量类型,再逐条写约束,最后定义目标函数并调用optimize。这套代码里的变量声明分三类:
% 连续变量:支路有功、无功、网侧注入 P_branch = sdpvar(n_branch, 1); % 每条支路的有功潮流 Q_branch = sdpvar(n_branch, 1); % 每条支路的无功潮流 v_node = sdpvar(n_node, 1); % 每个节点电压幅值的平方 l_branch = sdpvar(n_branch, 1); % 每条支路电流幅值的平方 % 整数变量:OLTC分接头档位、电容器组投切组数 tap = intvar(1, 1); % OLTC分接头档位,例如-8到8 cap_on = binvar(n_cap, 1); % 电容器投切状态,0或1sdpvar声明连续变量,intvar声明整数变量,binvar声明0-1变量。类型选错会直接导致模型不可解或求解时间爆炸。
核心约束按前面DistFlow方程逐条写。支路潮流守恒部分:
Constraints = []; for k = 1:n_branch i = branch_from(k); % 支路首端节点 j = branch_to(k); % 支路末端节点 % 有功守恒:本支路流出 = 下游支路之和 + 节点负荷 - DG注入 Constraints = [Constraints, ... P_branch(k) - l_branch(k)*branch_r(k) == ... sum(P_branch(downstream{k})) + P_load(j) - P_dg(j)]; % 无功守恒 Constraints = [Constraints, ... Q_branch(k) - l_branch(k)*branch_x(k) == ... sum(Q_branch(downstream{k})) + Q_load(j) - Q_dg(j)]; endbranch_from和branch_to是支路端点数组,downstream{k}记录支路k下游的所有支路编号,需要在建模前根据网络拓扑算好。这个循环写法比矩阵化写法慢一点,但可读性高,改网络时不容易遗漏。
电压降落方程和二阶锥约束:
for k = 1:n_branch i = branch_from(k); j = branch_to(k); % 电压降落方程 Constraints = [Constraints, ... v_node(j) == v_node(i) - 2*(branch_r(k)*P_branch(k) + ... branch_x(k)*Q_branch(k)) + (branch_r(k)^2 + branch_x(k)^2)*l_branch(k)]; % 二阶锥松弛:||2P, 2Q, v_i - l|| <= v_i + l Constraints = [Constraints, ... cone([2*P_branch(k); 2*Q_branch(k); v_node(i) - l_branch(k)], ... v_node(i) + l_branch(k))]; endcone(x, y)是YALMIP定义二阶锥的专用函数,表示约束(|x|_2 \leq y)。常见的翻车点是把cone的顺序写反,第二个参数必须是标量,如果写成cone(A, B)且B是二维向量,YALMIP会报维度错误,这类错误看英文提示往往看不明白。
节点电压上下限、DG出力上下限、OLTC档位取值范围:
% 根节点电压固定为1.0p.u. Constraints = [Constraints, v_node(1) == 1.0]; % 其他节点电压上下限:0.95p.u. ~ 1.05p.u. Constraints = [Constraints, 0.95^2 <= v_node(2:end) <= 1.05^2]; % DG出力上下限 Constraints = [Constraints, P_dg_min <= P_dg <= P_dg_max]; % OLTC档位范围 tap_min = -8; tap_max = 8; Constraints = [Constraints, tap_min <= tap <= tap_max];3.3 求解器配置与结果提取
模型建完,设置求解器参数这一步决定了求解效率。YALMIP通过sdpsettings控制求解器的行为:
options = sdpsettings('solver', 'gurobi', ... % 指定求解器 'verbose', 2, ... % 输出日志级别,2为详细 'showprogress', 1, ... % 显示建模进度 'gurobi.mipgap', 1e-4, ... % 整数规划收敛间隙 'gurobi.timelimit', 300); % 求解时间上限,单位秒 result = optimize(Constraints, alpha(1)*sum(l_branch.*branch_r) + ... alpha(2)*sum((v_node - v_ref).^2), options);optimize的第二个参数是目标函数,第三个参数是配置项。求解完成后检查result.problem是否等于0,等于0表示求解成功:
if result.problem ~= 0 disp('求解失败,错误代码: ' + string(result.problem)); else P_opt = value(P_branch); V_opt = sqrt(value(v_node)); tap_opt = value(tap); loss = value(sum(l_branch .* branch_r)); endvalue()函数把YALMIP变量从求解器结果里取回来。实际项目里,我习惯在取结果后先做一遍物理合理性检查:电压是否在0.95-1.05之间,支路功率是否超过线路容量,OLTC档位是否是整数。求解器说“成功”不代表结果物理可用,这一步复查不要省。
4. IEEE 33节点算例复现:参数、结果与改造成
4.1 算例数据从哪来:线路参数、负荷和DG接入点
代码包里自带的算例是IEEE 33节点配电网,这是配电网优化研究的标准测试系统:基准电压12.66kV,基准功率1MVA,系统总负荷有功3715kW、无功2300kVar,33条节点、32条分段支路,加上首端一个联络开关支路(编号33,默认断开,形成辐射状)。
线路参数是标准值,单位用的是标幺值体系。需要留意的是YALMIP模型里所有量都用标幺值,而负荷数据原始单位是kW/kVar,进模型前要除以基准功率:
baseMVA = 1; % 基准功率 1 MVA baseKV = 12.66; P_load_pu = P_load_kW / (baseMVA * 1000); % kW转标幺值 Q_load_pu = Q_load_kVar / (baseMVA * 1000);这个单位换算是很多人第一次跑翻车的地方。原始数据里3715kW看着不大,忘记除以1000,优化器会把负荷当成3715标幺值来算,相当于实际功率3.7GW,结果自然是不可行。
DG接入点的选择直接决定优化效果。代码包里默认把光伏接在配电网末端电压薄弱区域,比如节点18、22、25、32,单点接入容量200kW。选择末端的原因很实际:辐射状配电网末端电压最低,DG接入后可以有效支撑电压,优化效果也最明显。
4.2 复现结果解读:网损、电压和求解时间
跑通算例后,重点关注三个量:网损、电压最低点、求解时间。典型结果如下表:
| 指标 | 优化前 | MISOCP优化后 | 变化 |
|---|---|---|---|
| 网络有功损耗(kW) | 202.6 | 151.8 | 下降25.1% |
| 节点最低电压(p.u.) | 0.968 | 0.983 | 提升1.5% |
| 电压偏差总和 | 0.063 | 0.027 | 下降57% |
| 求解时间(s) | — | 约15 | — |
网损下降25%左右是33节点系统在中等DG渗透率下的典型水平,不算夸张,但足以说明OLTC和电容器调节的意义。最低电压从0.968抬升到0.983,意味着末端用户电压从“合格但偏低”变成“运行在健康区间”。
求解时间15秒左右,和求解器的配置强相关。用Gurobi默认参数跑出来可能30秒,设置mipgap=1e-4会快一些,但解的精度略微牺牲;设置timelimit=300可以保证在超时前给出一个可行解,而不是让程序无限跑下去。
复现过程碰到的第一个坑往往是:目标函数里sum(l .* r)用的是全部支路电流平方乘电阻,但联络开关支路在初始状态下是断开的,其潮流被强制为0。代码里会用一组0-1变量表示联络开关状态,断开支路的潮流要显式限定为0,否则优化器会“偷偷”合上联络开关,把辐射状结构变成环网,结果再漂亮也不符合实际运行要求。
4.3 改造指南:换拓扑、加储能、调权重
拿到代码后的第一件事不是跑通,而是改成自己关心的场景。最常见三种改造:
换拓扑:33节点换成69节点或实际馈线,核心只需要改四类数据——支路首端节点数组branch_from、末端节点数组branch_to、支路电阻branch_r、支路电抗branch_x。注意节点编号必须从1开始连续,如果实际网架有缺号,先做一次节点重编号,否则关联矩阵维度会报错。
加储能:储能建模比DG多一个状态变量——荷电状态SOC。需要在代码里加两组约束,充电时SOC上升、放电时SOC下降:
% 储能状态转移:SOC(k+1) = SOC(k) - P_bess*dt/E_cap SOC_next = SOC_now - P_bess * dt / E_cap; Constraints = [Constraints, SOC_min <= SOC_next <= SOC_max]; Constraints = [Constraints, -P_ch_max <= P_bess <= P_dis_max];dt是调度时段间隔(小时),E_cap是储能额定容量,P_ch_max和P_dis_max分别是充放电功率上限。储能SOC约束会让模型多一组状态关联约束,原本的单时段优化方阵变多时段优化,计算量会上去,这时第三节里的mipgap参数就派上用场了。
调权重:把目标函数权重改成运行成本最小化,需要在代码里加入购电电价、DG补贴电价和储能损耗系数。这部分改动不涉及模型结构,只改目标函数定义那一行。
5. 避坑与排查:MISOCP求解中的现象、原因与解决
5.1 一启动就报Infeasible problem
现象:optimize返回problem=1,求解器日志提示Infeasible problem,模型完全不可行。
原因:90%的情况是单位换算错误。负荷数据用kW直接传入标幺值模型,或者基准功率写错,导致节点注入功率远超线路物理极限。另外,如果电压上下限设定过紧(比如0.99-1.01p.u.),末端负荷又重、DG出力又受限,确实没有可行解存在。
解决:先做可行性排查。第一步检查负荷和DG出力是否除以baseMVA*1000转成标幺值;第二步把电压约束放宽到0.9-1.1p.u.,如果模型变得可行,说明原边界太紧;第三步用check(Constraints)查看YALMIP报告的约束残差,定位是哪个约束不可行。我从这以后每次建模都会先跑一遍纯潮流可行性验证,再叠加优化目标。
5.2 求解时间从几秒涨到几小时
现象:同样的算例,加了储能或增加OLTC档位数后,求解时间从15秒暴增至800秒以上,甚至一直停在MIP gap无法收敛。
原因:MISOCP的复杂度随整数变量数量指数级增长。OLTC档位如果是整数变量,取值范围-8到8共17档,加上每台电容器一个0-1变量,10个电容器就是10个二进制变量,分支定界的节点数会迅速膨胀。另一个原因是求解器参数没设置——默认的mipgap是1e-4,对规划类问题要求过高,实际工程里1e-2精度完全够用。
解决:分三步降复杂度。第一步把gurobi.mipgap从1e-4放宽到1e-2,求解时间通常能缩短80%,同时保住工程精度;第二步给OLTC分接头设合理的档位步长,把17档变成5档,虽然精度略降但模型规模大幅缩小;第三步对已知的连续强相关变量用热启动,先跑一版松弛SOCP,把结果作为MISOCP的初始解,减少分支定界初期探索量。
5.3 松弛不紧,得到“假的全局最优”
现象:求解器报告Status: Optimal,但检查结果发现支路电流平方l_branch明显虚高,电压分布不自然,网损数值异常低,对偶变量数值很大。
原因:二阶锥松弛不是天然紧的。当目标函数以经济性为主、电压约束又不紧时,优化器可能利用松弛的空隙,让(l_{ij})虚高以降低网损项,得到一个物理上不可实现的“最优解”。这是MISOCP建模最隐蔽的坑,比不可行问题更难排查。
解决:验证松弛紧性的标准做法是检查原等式是否在最优解处恢复,即判断(l_{ij} \cdot v_i - (P_{ij}^2 + Q_{ij}^2))的值,理论上该残差应为0。如果残差大于容忍阈值,需要往目标函数里加惩罚项,或者在原二阶锥约束之外追加割平面约束收紧可行域。代码里我一般会写一段紧性检查脚本,求解后自动输出最大残差,数值大于1e-3就报警,提醒你检查目标函数权重是否压得过于极端。
5.4 YALMIP版本与matlab版本兼容性引发的诡异报错
现象:代码在自己机器上跑正常,换到同事的R2023a环境就报Could not evaluate validity of the constraint或者solver not found,错误信息指向不明。
原因:YALMIP不同版本对约束内部表达式的处理方式有差异。老版本YALMIP(2019年之前)对cone函数的输入要求更宽松,新版本严格检查维度,导致旧代码直接移植到新环境时解析失败。另一个常见情况是matlab升级后系统环境变量里没有同步求解器的路径,YALMIP找不到求解器。
解决:代码移植后先跑yalmiptest,确认所有求解器状态正常。如果报约束解析错误,用forward指令调试具体是哪一行约束出问题。经验是把YALMIP升级到2020年之后的版本,并且参考代码里version注释来锁定依赖版本。我就吃过一次亏,旧代码在R2020b上跑得好好的,换到R2023b后cone语法报错,最后是逐条核对YALMIP官方changelog才定位到解法变了。
6. 进阶验证:松弛紧性检查、对比实验与参数扫描
6.1 用对偶变量和原问题双重验证紧性
MISOCP的求解结果不能直接信,紧性验证是必要动作。方案分两步走。第一步看对偶变量——求解器输出的对偶间隙为零,说明原问题和对偶问题之间没有缝隙,这是一个必要条件而非充分条件。第二步做原问题回代验证:
% 紧性检查:求l_ij*v_i - (P_ij^2+Q_ij^2)的残差 residual = max(abs(value(l_branch) .* value(v_node(branch_from)) ... - (value(P_branch).^2 + value(Q_branch).^2))); if residual > 1e-3 warning('二阶锥松弛不紧,最大残差 %.4f', residual); else disp('松弛紧,结果可信'); end残差超过1e-3就说明优化器利用了松弛缝隙。我在处理真实馈线项目时,会在目标函数里加入一个带小权重系数的(l_{ij})惩罚项来引导求解器恢复紧性,权重取原网损系数的5%就够。
6.2 与非线性最优潮流结果对比
另一种验证方式是拿同一算例,用fmincon跑一遍非线性规划版最优潮流做交叉验证。注意fmincon只能处理连续变量,OLTC档位要预先固定为MISOCP求出的结果,两者对比才有意义。如果MISOCP求出的网损比fmincon的连续解还低,那几乎可以断定松弛不紧,因为连续模型的理论最优值不会比离散模型更差。我用matlab内置的fmincon加solver='sqp'做对比,两三行代码就能跑出来,结论放在报告里很有说服力。
6.3 参数扫描脚本与热启动技巧
最后提一个能直接提升研究效率的用法——参数扫描。学术论文里最常见的就是“DG渗透率-网损”曲线或“OLTC档位-电压分布”曲线。我把目标函数权重、负荷水平和DG渗透率抽成三个向量,用一个双层循环批量求解:
for pen = 0.1:0.1:0.6 % DG渗透率从10%到60% for w = 0.5:0.5:2.5 % 电压偏差权重 alpha = [1, w]; set_dg_penetration(pen); % 更新算例中的DG容量 options = sdpsettings('solver','gurobi', 'gurobi.mipgap', 1e-2); result = optimize(Constraints, alpha(1)*loss + alpha(2)*v_dev, options); loss_table(pen*10, w*2) = value(loss); v_table(pen*10, w*2) = min(value(v_node)); end end扫描后的结果用surf画三维曲面,一条曲线变成一张图,论文和报告的可视化素材都有了。扫描过程中有一个值得养成的习惯:每跑完一组参数,把result的原始信息连同mipgap、求解时间一起写入日志文件,这样复现结果时能准确回答“当时用的什么求解参数”,而不是靠记忆。从那以后,我每次跑MISOCP都会强制走一遍紧性验证、记录求解器版本和参数,再开始信任结果。这套流程看着繁琐,但能省下后续为结果可信度反复返工的时间。希望帮到你。
本文还有配套的精品资源,点击获取