ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

IEEE 33节点配电网潮流计算:Python前推回代法实现与验证

IEEE 33节点配电网潮流计算:Python前推回代法实现与验证

简介:这是一套面向电力系统专业学生与科研人员的Matlab潮流计算程序包,针对IEEE 33节点与69节点典型配电网模型,完成loadflow稳态分析,可用于教学实验、算法对比以及配电网运行与规划研究。压缩包内共有5个文件,包含4个M脚本与1个PDF说明文档,整体体积约341KB。其中脚本分别承载33节点、69节点的潮流主程序与前推回代核心算法,PDF文档详细阐述计算原理、输入数据组织方式和结果输出含义。程序支持经典牛顿-拉夫森法和前推回代法两种求解思路,能计算节点电压幅值、相角、支路功率及网络损耗,便于对比不同算法在标准系统上的精度与收敛性;同时,通过修改负荷、拓扑等参数可快速推广到其他配电网算例,适合二次开发与算法验证。目前已有459人学习借鉴,对正在开展标准节点潮流计算、分布式电源接入影响分析或课程设计的读者来说,是一套可直接运行的参考代码,配有说明文档,能显著缩短建模与编程时间。

1. IEEE 33 节点潮流计算:为什么全网都在用这个 33 节点算例

同样是配电网潮流计算,33 节点和 69 节点两个算例跑出来的结果差一大截,问题常常不在算法,而在入口数据。IEEE 33 节点(也叫 33ieee、IEEE33)是配电网潮流计算里最常用的标准算例,很多人在做前推回代法验证、分布式光伏接入分析、网损计算时,都拿它当基线。Power_Flow_33_69_33节点 这类仓库名,核心就是 loadflow 实现。我觉得读这类代码不必从头啃,先把数据表、基准值和收敛条件对齐,剩下的循环很快就能跑通。这篇笔记写给两种人:刚把潮流计算教材合上、想找算例练手的新手,以及被 DG 接入搞到头疼、需要一个可信基线做对比的工程师。

2. 看懂 IEEE 33 节点模型:数据表结构、拓扑与两套潮流计算路线

2.1 支路表、负荷表和松弛节点:读数据前先搞懂这三张表

IEEE 33 节点系统的公开参数几乎都长一个样:33 个节点、32 条支路,根节点是松弛节点,其余节点按恒功率 PQ 负荷处理。基电压 12.66 kV,基准容量 10 MVA,这个基准对账非常重要,后面会反复用到。

拿到任何一份数据,第一件事不是写算法,而是把三样东西从原始表里抠出来:支路表、负荷表、松弛节点编号。支路表至少要包含起始节点、终止节点、电阻、电抗四个字段;负荷表通常按节点给有功功率和无功功率;松弛节点一般是一个独立母排,电压幅值固定为 1.0 p.u.。原始数据可能是 CSV、Excel,也可能是别人抄来抄去的文本,第一步都是统一成 Python 里的列表结构。

import numpy as np base_kV = 12.66 base_MVA = 10.0 Z_base = base_kV ** 2 / base_MVA # 160.3524 ohm # 支路表:from, to, R(ohm), X(ohm) # 根节点按 bus 0 处理,节点编号 0~32 branch_ohm = [ (0, 1, 0.0922, 0.0470), (1, 2, 0.4930, 0.2511), (2, 3, 0.3660, 0.1864), (3, 4, 0.3811, 0.1941), (4, 5, 0.8190, 0.7070), (5, 6, 0.1872, 0.6188), (6, 7, 1.7114, 1.2351), (7, 8, 1.1440, 0.7401), (8, 9, 0.1640, 0.1565), (9, 10, 0.6462, 0.2610), (10, 11, 0.2465, 0.1230), (11, 12, 0.2544, 0.1170), (12, 13, 1.2200, 0.6550), (13, 14, 0.8140, 0.5420), (14, 15, 0.1872, 0.6188), (15, 16, 1.1440, 0.7401), (16, 17, 0.7114, 0.2351), (1, 18, 0.1640, 0.1565), (18, 19, 1.5040, 1.3554), (19, 20, 0.4095, 0.4784), (20, 21, 0.7089, 0.9373), (2, 22, 0.4510, 0.3083), (22, 23, 0.8980, 0.7091), (23, 24, 0.8960, 0.7011), (5, 25, 0.2030, 0.1034), (25, 26, 0.2842, 0.1447), (26, 27, 1.0590, 0.9337), (27, 28, 0.8040, 0.7006), (28, 29, 0.5075, 0.2585), (29, 30, 0.9744, 0.9630), (30, 31, 0.3105, 0.3619), (31, 32, 0.3410, 0.5362), ]

这段代码做完之后,先别急着跑,对一下总数:branch_ohm应该有 32 条,起始节点最大到 31,终止节点最大到 32,根节点 0 只出现在起始侧。很多拿到 1~33 编号版本的人,会把根节点写成 1,但不影响算法,只要内部索引统一即可。我这里统一从 0 开始,原因很简单:Python 的列表和数组索引天然从 0 开始,少做一次减一,就少一个隐患。

负荷表里值得注意的节点是 24、25、30 号。24 和 25 的有功都是 420 kW,是整个系统里最重的两条馈线负荷,30 号的无功在不同文献里出现过 200 kVar 和 600 kVar 两种口径,这个版本差异直接决定最低电压点数值,后面验证结果时要小心。

2.2 前推回代法与牛顿-拉夫逊:配电网场景下我为什么先用前推回代

配电网潮流计算不是只有一种做法。教材里最先教的是牛顿-拉夫逊法,它的优势是收敛快、通用性强,能处理环网和多种节点类型,但代价是要构造雅可比矩阵,而且对初值敏感。IEEE 33 这种辐射状网络,R/X 比值普遍偏高,不是典型的输电网特性,直接用牛顿法调初值会让人很痛苦。

前推回代法(Forward/Backward Sweep)正好是为辐射状网络设计的。它不构造矩阵,只做两趟遍历:回代从末端往根节点走,累加各节点注入电流;前推从根节点往末端走,用支路电流算电压降落。每一轮迭代都在更新节点电压,直到最大电压偏差小于阈值。对 33 节点这种规模,10 到 20 次以内基本收敛,速度完全可以接受。

方法适用网络矩阵操作初值敏感度处理DG弱环
牛顿-拉夫逊输电网/网状网需要高需要改造
前推回代辐射状配电网不需要低弱环需拆分

选前推回代还有一个务实理由:后面做分布式光伏接入分析时,大多数场景只是把某个 PQ 负荷节点改成恒功率注入,前推回代的修改量最小。如果一开始就用牛顿法,每次改负荷模型都要连带改雅可比矩阵里的偏导数项,调试成本高不少。等到真正需要处理环网或 PV 节点时,再考虑用牛顿法或三相序分量法,那属于进阶范围,不是起步阶段该纠结的事。

3. 用 Python 从零实现 IEEE 33 潮流的最小可复现流程

3.1 数据定义:用有名值转标幺,防止单位翻车

很多开源代码直接给标幺值,看起来干净,但出了问题很难查。我习惯先在代码里保留有名值(欧姆、千瓦、千乏),再显式转换一次,这样任何人都能拿着原始参数核对。转换公式只有两组:阻抗标幺值等于有名值除以 Z_base,功率标幺值等于有名值除以基准功率。

n = 33 # 节点负荷:P(kW), Q(kVar),索引即节点号 load_kw = { 2: 100, 3: 90, 4: 120, 5: 60, 6: 60, 7: 200, 8: 200, 9: 60, 10: 60, 11: 45, 12: 60, 13: 60, 14: 120, 15: 60, 16: 60, 17: 60, 18: 90, 19: 90, 20: 90, 21: 90, 22: 90, 23: 90, 24: 420, 25: 420, 26: 60, 27: 60, 28: 60, 29: 120, 30: 200, 31: 150, 32: 210, } load_kvar = { 2: 60, 3: 40, 4: 80, 5: 30, 6: 20, 7: 100, 8: 100, 9: 20, 10: 20, 11: 30, 12: 35, 13: 35, 14: 80, 15: 10, 16: 20, 17: 20, 18: 40, 19: 40, 20: 40, 21: 40, 22: 40, 23: 50, 24: 200, 25: 200, 26: 25, 27: 25, 28: 20, 29: 70, 30: 600, 31: 70, 32: 100, } S_load = np.zeros(n, dtype=complex) for i in range(1, n): S_load[i] = (load_kw.get(i, 0) + 1j * load_kvar.get(i, 0)) / (base_MVA * 1000.0)

参数说明:base_MVA * 1000.0是把 10 MVA 换算成 kVA,这样和负荷表里的 kW/kVar 同单位。之所以用get(i, 0),是因为节点 1 没有负荷,有些版本连节点 0 都给了 0 负荷,写成字典后用 get 可以省去手动补零的麻烦。这段跑完,S_load[5]应该是0.006 + 0.003j,可以用这个值验算自己的数据有没有抄错。

3.2 拓扑排序与支路方向校正:实现前推回代的第一件事

前推回代法不关心节点编号长什么样,关心的是每条支路的方向能不能构成一棵从根节点出发的树。IEEE 33 的支路表天然满足“上游节点号小于下游节点号”,但这不是所有配电网算例都成立。为了以后直接换 69 节点数据,我会先把邻接表和父节点关系建出来,再做一次广度优先遍历。

children = [[] for _ in range(n)] parent = [-1] * n for src, dst, r, x in branch_ohm: children[src].append(dst) parent[dst] = src # 从根节点做广度优先,生成拓扑顺序 order = [0] for node in order: for c in children[node]: if c not in order: order.append(c) if len(order) != n: print("节点数不一致,支路方向或有向图检查失败") raise SystemExit

这里有个隐藏检查:if len(order) != n。如果支路表里把某条支路的首尾写反,比如写成(1, 0, ...),那么广度优先遍历时 0 号根节点会被当成子节点,order 的长度仍然可能是 33,但顺序会乱。更稳妥的检查是确认parent[0] == -1,并且每个非根节点只出现一次。我一般会在建完 parent 后加一行断言,宁可报错也不带着错误拓扑跑迭代。

3.3 迭代核心:回代算电流,前推算电压,一次循环搞定

主循环代码很短,但每一行都有讲究。回代阶段从 order 的末端倒着走,先把当前节点的负荷电流算出来,再把这个电流累加到父节点上;前推阶段从根节点往下走,用父节点电压减去支路阻抗乘支路电流,得到子节点电压。

Z = {} for src, dst, r, x in branch_ohm: Z[(src, dst)] = (r + 1j * x) / Z_base max_iter = 50 tol = 1e-6 V = np.ones(n, dtype=complex) V[0] = 1.0 + 0j for it in range(max_iter): V_old = V.copy() I_node = np.zeros(n, dtype=complex) # 回代:从末端到根,累加电流 for node in reversed(order): if node == 0: continue I_node[node] += np.conj(S_load[node] / V[node]) I_node[parent[node]] += I_node[node] # 前推:从根到末端,更新电压 for node in order[1:]: p = parent[node] V[node] = V[p] - Z[(p, node)] * I_node[node] diff = np.max(np.abs(V - V_old)) if diff < tol: break else: print("达到最大迭代次数,未收敛") # 结果输出 net_loss = np.sum(np.abs(I_node[node])**2 * Z[(parent[node], node)].real for node in order[1:]) print(f"迭代次数: {it+1}") print(f"全网有功网损: {net_loss * base_MVA * 1000:.2f} kW") print(f"最低电压节点: {np.argmin(np.abs(V))}, 幅值 {np.min(np.abs(V)):.4f} p.u.")

逻辑说明:np.conj(S_load[node] / V[node])是把复功率转换成注入电流,取共轭是因为功率的正方向约定为流入负荷。回代循环里I_node[parent[node]] += I_node[node]是关键,它把当前节点的电流累加到父节点,等循环走到父节点时,I_node 已经包含了整条下游馈线的电流,前推时直接用即可。

参数说明:max_iter = 50对 IEEE 33 绰绰有余,正常 15 次以内就收敛,但如果负荷数据填错或拓扑有环,也可能 50 次跑不满。tol = 1e-6是标幺值下的电压偏差阈值,换算成有名值大约是 12.66 kV 乘 1e-6,不到 0.013 V,精度足够了。再往下调没有意义,只会增加迭代次数,不会让结果更准。

3.4 三类出口检查:迭代次数、网损与节点电压幅值

跑完不是结束,得看一眼三个数:迭代次数、有功网损、最低电压节点。常见的收敛结果是迭代次数小于 20,有功网损接近 202.5 kW,最低电压出现在 18 号节点附近,幅值在 0.90 到 0.913 p.u. 之间。这三个数只要有一个对不上,数据表大概率有误,先别急着调算法。

迭代次数异常通常意味着拓扑方向错了,或者初值给得太离谱。网损偏大要检查是不是负荷单位填错,比如把 kW 填成了 MW,等于把系统加重了一千倍;网损偏小甚至接近零,要检查负荷表有没有真正生效,我在 5.1 节会展开说。

4. 结果合理性验证:把网损和最低电压与参考值对齐

4.1 参考值口径:总网损、最低电压与 0.9038 p.u. 的来历

网上能找到的 IEEE 33 节点结果大体分成两类。一类是经典参数版本,全网有功网损约 202.5 kW,最低电压约 0.9131 p.u.,出现在 18 号节点;另一类是某些论文修改了 30 号节点无功负荷后的口径,最低电压会降到 0.9038 p.u. 左右。这两个数值差别不大,但足够让第一次对账的人以为自己程序写错了。

我没有必要去纠结哪个才是“官方标准”,工程上更实用的是记住一组自己复现出来的基线值。只要你的负荷表和我上面给的版本一致,跑出来的网损在 202.5 kW 上下浮动不超过 5 kW,最低电压在 0.9038 到 0.9131 之间,程序就基本正确。如果偏离太多,再往下查。

4.2 对比验证清单:电压、网损、迭代次数三者一起看

验证时要同时对照三组数据,而不是只盯网损。单看网损对上了说明整体功率平衡没问题,但可能某条支路的电压都算得离谱,只是恰好相互抵消。我在调试时会打一张小表,把 0、8、18、24、32 这几个代表性节点的电压幅值列出来。

节点 0: 1.0000 p.u. 节点 8: 0.9842 p.u. # 典型中间馈线电压 节点 18: 0.9131 p.u. # 典型最低电压点 节点 24: 0.9742 p.u. # 重负荷支路末端 节点 32: 0.9675 p.u. # 第二条重负荷支路末端

这些数值不是精确到每一位的官方答案,而是用来快速筛查的区间。如果你的 18 号节点电压是 0.95,网损却接近 202 kW,说明算法里可能存在收敛判断问题,比如电压幅值算对了但相角没更新,或者前推回代顺序不完整。反过来,如果网损不对但电压全对,负荷表或功率基准几乎肯定有问题。

4.3 当结果明显偏高或偏低:从版本口径找差异

最常遇到的情况是:程序逻辑完全正确,但结果和某个参考值对不上。这时候先别怀疑代码,去查数据来源的版本口径。IEEE 33 节点在流传过程中,30 号节点的无功出现过 200 kVar 和 600 kVar 两个版本,24、25 号节点的负荷在一些简化版本里被合并过,节点编号从 1 开始还是从 0 开始也会影响支路表里 root 的位置。

我的习惯是先看数据表里的总负荷。把每个节点的 P 和 Q 分别加起来,如果总有功不是 3715 kW,总无功不是 2300 kVar,那这份参数和常见基线就对不上。如果总负荷能对上,再检查支路表条数,32 条少一条都会导致某个节点悬空,结果完全失真。

5. IEEE 33 潮流计算最容易翻车的 5 个坑:现象、原因与解决

5.1 算出来的电压全部等于 1.0:负荷表问题

现象:迭代一次就“收敛”,所有节点电压全是 1.0 p.u.,网损为零。原因:负荷表没有生效,常见于把负荷写进了load_kw字典但忘了调用get填数组,或者复功率在转标幺时除以了base_MVA * 1.0而不是base_MVA * 1000.0,导致所有负荷都变成原本的千分之一。解决:打印np.sum(S_load),确认标幺总负荷约等于0.3715 + 0.2300j,如果偏小一个数量级,就去找单位换算。

5.2 迭代次数直接拉满但没收敛:初值与阈值设置

现象:max_iter 设成 50,跑完没进 break,diff 一直停在某个值附近不动。原因:初值设置不当或者负荷太重。解决:把初值从全 0 改成全 1.0∠0°,同时检查最大迭代次数是否够用。IEEE 33 无补偿状态下通常 15 次内收敛,如果 diff 到第 30 次还在 1e-4 附近摆,多半是拓扑方向有问题,不是收敛阈值太严。

5.3 支路表方向写反:前推回代序列失效

现象:报错 parent[0] 不是 -1,或者电压出现从根节点往末端越来越高的反直觉结果。原因:原始表统一按“上游到下游”读的,但某几行写反了,比如把(1, 2)写成(2, 1)。解决:在代码里显式检查 parent[0],并且确认每个节点在 order 中只出现一次。我一般会加一段自动校正逻辑,当检测到parent[0] != -1时就翻转让 from 和 to。

5.4 有功网损是负的:功率方向约定不一致

现象:跑出来的 net_loss 是负数。原因:有人用sum(S_load[node])的方式算网损,或者在前推回代里把复功率符号写反了。解决:不要从节点功率相加减推网损,直接按支路电流平方乘电阻求和,公式是Σ |I_branch|^2 * R。写成代码只用一行 generator,结果永远是正的,不容易受方向约定干扰。

5.5 基准容量用 100 MVA:标幺值全乱

现象:程序逻辑没问题,电压幅值在 0.999 到 0.998 之间,几乎看不出压降。原因:基准容量取成了 100 MVA,而不是 10 MVA。Z_base 由基准电压平方除以基准容量决定,基准容量大了十倍,所有阻抗标幺值就小了十倍,压降自然不明显。解决:统一用 10 MVA,电压等级 12.66 kV,Z_base 固定为 160.3524 Ω。这两个数建议直接写死在配置区,不要放在函数参数里随手传。

6. 从 IEEE 33 升级到 IEEE 69 节点算例:换数据、排错与验证

6.1 扩充数据表:68 条支路的拓扑检查

把 33 换成 69 不是把数组长度改成 69 那么简单。IEEE 69 节点系统有 68 条支路,总负荷约 3.8019 MW + j 2.694 MVar,网损参考值约 225 kW,最低电压节点通常在 65 号附近。换数据时先按照 3.2 节的方式重建 children 和 order,然后检查len(order) == 69。69 节点支路表的编号不像 33 那么规整,某些分支的父节点编号比子节点大,所以不能再依赖编号顺序。

6.2 连通性检查与电压初值:换算例时的两个关键点

我一般会先跑一个连通性检查:从根节点做广度优先,统计能访问到的节点数量,如果访问不到全部 69 个节点,说明支路表有孤立节点,直接报错。第二步再检查负荷表,IEEE 69 的负荷字典比 33 长很多,容易漏项,建议打印np.sum(S_load)核对总负荷。电压初值仍然用 1.0∠0°,不需要为 69 节点做特殊处理,前推回代法对初值不敏感的优点在这里体现得最明显。

整个流程跑通后,对比参考值:网损 225 kW 左右,最低电压 0.909 p.u. 附近,迭代次数在 20 次以内。如果网损偏大,优先查 60 号之后的负荷数据,那一带的支路阻抗数值大,对网损的贡献也最明显。我从 33 跨到 69 之后最大的教训是:先验证拓扑,再谈收敛。换算例最容易翻车的不是算法,而是数据表抄错。希望这份从数据表到结果校验的路径,能帮你在 loadflow 上少走几步弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进