
简介这份资源面向电力系统仿真、潮流计算学习者与科研人员提供标准IEEE 39节点系统的MATLAB建模数据包可直接用于MATPOWER或Power System Toolbox开展交流潮流、直流潮流、灵敏度分析及稳定性与故障仿真等任务也适合高校教学演示与课程设计使用。压缩包共6个文件约11KB以case39.m为核心建模脚本另含case39.py、requirements.txt、.inscode、.gitignore及来源说明文本分别承担MATLAB建模、Python调用、依赖声明与工程配置等用途。数据已按IEEE标准定义39个节点的母线类型、电压幅值与相角34条支路的线路阻抗与导纳10台发电机的出力与无功限值以及各负荷的有功无功功率加载即可运行无需格式转换或参数补全。目前已有55人学习下载适合需要快速搭建标准测试系统、验证算法或开展教学实验的读者参考使用。1. 从 case39.m 说起为什么电力系统仿真总绕不开这 39 个节点如果你做过电力系统潮流计算、暂态稳定分析或者新能源并网仿真大概率在某个深夜被一组数据折磨过——节点数太少算出来的结果没有代表性节点数太多动辄上千节点跑一次仿真要等半天调参调到怀疑人生。IEEE 39 节点系统恰好卡在一个微妙的平衡点上它足够复杂能反映真实电网的拓扑特征和动态行为又足够精简在一台普通笔记本上几秒钟就能跑完一次潮流。而case39.m这个文件就是把这套系统从论文里的接线图变成 MATLAB 可执行数据的钥匙。很多新手第一次拿到case39.m时打开一看全是数字矩阵完全不知道从哪下手。也有人直接runpf(case39)跑通了就以为万事大吉结果后面做优化、做稳定分析时发现数据维度对不上、发电机参数缺失、节点编号混乱。这篇笔记就是要把这套数据包从“能跑”推到“能用”——讲清楚它里面到底存了什么、怎么在 MATLAB 里正确加载和修改、潮流计算时哪些参数必须核对、以及那些我踩过的坑。2. case39.m 数据包解剖矩阵结构、字段含义与版本差异2.1 baseMVA 与 bus 矩阵节点数据的九个列到底怎么读打开case39.m第一眼看到的是baseMVA 100这是整个系统的基准功率单位是兆伏安。所有标幺值都基于这个数换算如果你后续要接入实际物理量第一步就是确认这个基准值和你手头其他数据是否一致。常见翻车场景是从别处拷来一组发电机容量数据单位是 MW直接往gen矩阵里填忘了除以baseMVA结果潮流直接发散。bus矩阵是 39 行 13 列不同版本可能列数略有差异每一行对应一个节点。前几列的含义必须记牢列号字段含义常见取值1BUS_I节点编号1~392BUS_TYPE节点类型1PQ, 2PV, 3平衡节点3PD有功负荷 (MW)正数4QD无功负荷 (MVar)正数5GS并联电导 (MW)通常为 06BS并联电纳 (MVar)正负均有7BUS_AREA区域编号18VM电压幅值初值 (p.u.)1.0 左右9VA电压相角初值 (度)010BASE_KV基准电压 (kV)345/230 等后面几列在不同版本里可能是ZONE、VMAX、VMIN做潮流计算时前 10 列是核心。我一般会在加载后先跑一遍bus(:,2)的统计确认平衡节点只有一个、PV 节点数量与发电机数量匹配。如果 PV 节点数对不上后面runpf会直接报“没有平衡节点”或者“PV 节点电压不可控”。2.2 gen、branch 与 gencost发电机、线路和成本数据的联动关系gen矩阵的列数通常是 21 列但真正影响潮流结果的只有前 10 列左右。关键字段包括所在节点号、有功出力、无功出力、最大/最小有功、最大/最小无功、电压设定值、是否在线。这里有一个血泪经验gen矩阵里发电机的节点编号必须能在bus矩阵里找到对应行而且该节点的BUS_TYPE必须是 2 或 3。如果某个发电机挂在一个BUS_TYPE1的节点上潮流程序会直接忽略它你算出来的发电出力总和永远对不上负荷。branch矩阵描述线路和变压器核心列是首末节点、电阻、电抗、电纳、变比、相移。39 节点系统里有几条变压器支路变比不是 1.0如果你手动改数据时把变比列填成 0程序会报除零错误。gencost是给最优潮流用的做普通潮流计算时用不到但如果你要跑runopf这个矩阵的格式必须符合 MATPOWER 的约定2 列是多项式系数个数后面跟系数最后是上下限。2.3 不同来源的 case39.m 差异列数、节点编号与发电机模型网上流传的case39.m至少有三种常见变体。第一种是 MATPOWER 自带的case39.m结构最规范bus矩阵 13 列gen矩阵 21 列。第二种是从某仿真软件导出的版本节点编号从 0 开始直接扔进 MATPOWER 会报“节点编号必须从 1 开始”。第三种是某些教材配套的简化版把发电机内电抗和暂态参数也塞进了gen矩阵列数变成 30 多列但前 10 列的含义和标准版一致。我一般会写一个校验脚本加载后先检查三件事节点编号是否连续且从 1 开始、平衡节点是否唯一、所有发电机节点是否都是 PV 或平衡节点。这三条过了再往下做潮流计算基本不会出结构性问题。% 加载 case39 并做基础校验 mpc case39; % 或 loadcase(case39.m) bus mpc.bus; gen mpc.gen; % 检查节点编号连续性 assert(isequal(bus(:,1), (1:size(bus,1))), 节点编号不连续或未从1开始); % 检查平衡节点唯一性 assert(sum(bus(:,2) 3) 1, 平衡节点数量不为1); % 检查发电机节点类型 gen_bus gen(:,1); for i 1:length(gen_bus) idx find(bus(:,1) gen_bus(i)); assert(bus(idx,2) 2 || bus(idx,2) 3, ... [发电机所在节点 num2str(gen_bus(i)) 不是PV或平衡节点]); end disp(基础校验通过);这段代码的逻辑很直白先确认节点编号是 1 到 39 的连续整数再确认只有一个平衡节点最后逐个核对发电机挂载点。参数说明方面bus(:,2)是节点类型列gen(:,1)是发电机节点号列。如果你拿到的数据包节点编号从 0 开始可以在加载后统一加 1但记得branch矩阵里的首末节点号也要同步加 1否则线路会连到不存在的节点上。3. 用 MATPOWER 跑通潮流从 runpf 到结果解读的完整链路3.1 最小可复现命令三行代码跑出电压和网损如果你已经装好了 MATPOWER跑通 39 节点潮流只需要三行mpc case39; % 加载数据包 results runpf(mpc); % 执行交流潮流计算 printpf(results); % 打印格式化结果runpf默认使用牛顿-拉夫逊法收敛精度是 1e-8最大迭代次数 10。对于 39 节点系统通常 3 到 4 次迭代就能收敛。printpf会输出每个节点的电压幅值、相角、注入功率以及每条线路的潮流和网损。如果你只想看关键结果可以用results.bus(:,8)取电压幅值results.bus(:,9)取相角results.branch(:,14)和results.branch(:,15)分别取线路首末端有功。这里有一个容易忽略的点runpf返回的results结构体里bus矩阵的电压列已经被更新为计算值但gen矩阵的无功出力列也会被更新。如果你要反复调用runpf做参数扫描每次都要用原始case39重新加载否则上一次计算的无功出力会作为下一次的初值虽然通常不影响收敛但在重载情况下可能导致迭代次数增加。3.2 收敛失败时先查这五个参数平衡节点、PV电压与无功限值潮流不收敛是新手最常遇到的翻车场景。报错信息通常是“Newton method failed to converge in 10 iterations”。我一般按以下顺序排查第一确认平衡节点存在且唯一。如果bus矩阵里没有BUS_TYPE3的行或者有两行都是 3直接报错。第二检查 PV 节点的电压设定值。gen矩阵里有一列是发电机电压设定值通常在 1.0 到 1.05 之间。如果某个 PV 节点的设定值偏离合理范围太远比如 0.8 或 1.2潮流可能震荡。第三检查无功限值。gen矩阵里有最大无功和最小无功两列。如果计算过程中 PV 节点需要的无功超出限值MATPOWER 会把这个节点转为 PQ 节点如果转换后系统无功不足就会不收敛。我一般会先把无功限值放宽到 ±999确认收敛后再逐步收紧。第四检查线路电抗。branch矩阵里如果某条线路的电抗为 0雅可比矩阵会奇异。39 节点系统里没有零电抗线路但如果你手动改过数据务必确认。第五检查负荷水平。如果所有负荷同时乘以 2.0系统可能接近电压崩溃点不收敛是正常的。做重载研究时我一般从 1.0 倍负荷开始每次增加 0.05找到临界点。% 排查不收敛逐步放宽无功限值并观察迭代次数 mpc case39; mpc.gen(:,4) -999; % 最小无功 mpc.gen(:,5) 999; % 最大无功 results runpf(mpc); if results.success fprintf(放宽无功限值后收敛迭代次数%d\n, results.iterations); else fprintf(仍不收敛检查平衡节点和线路参数\n); end这段代码把发电机的无功上下限临时设为 ±999相当于取消了无功约束。如果这样能收敛说明原数据里的无功限值偏紧需要根据实际发电机容量调整。results.iterations告诉你迭代了多少次如果超过 8 次即使收敛也说明系统接近临界状态后续做稳定分析时要小心。3.3 结果后处理节点电压热力图与线路负载率排序跑通潮流只是第一步把结果整理成能看懂的图表才是日常。我常用的两个后处理一是节点电压分布图二是线路负载率排序表。节点电压用results.bus(:,8)取配合results.bus(:,1)的节点编号画柱状图。39 个节点里电压最低的通常是负荷中心节点电压最高的通常是发电机节点。如果最低电压低于 0.95 p.u.就要考虑无功补偿了。线路负载率用线路首端有功除以线路容量。branch矩阵里有一列是长期允许容量results.branch(:,14)是首端有功。负载率超过 80% 的线路要重点关注超过 100% 说明过载。% 节点电压与线路负载率后处理 V results.bus(:,8); [V_sorted, idx] sort(V); fprintf(电压最低的5个节点\n); for i 1:5 fprintf( 节点 %d: %.4f p.u.\n, results.bus(idx(i),1), V_sorted(i)); end % 线路负载率 flow abs(results.branch(:,14)); rate results.branch(:,6); % 容量列不同版本可能不同 loading flow ./ rate * 100; [load_sorted, idx2] sort(loading, descend); fprintf(负载率最高的5条线路\n); for i 1:5 fprintf( 线路 %d-%d: %.1f%%\n, ... results.branch(idx2(i),1), results.branch(idx2(i),2), load_sorted(i)); end参数说明results.branch(:,6)是线路容量列在标准 MATPOWER 格式里是 RATE_A。如果你用的数据包这一列全是 0说明没有填容量数据负载率计算会得到无穷大需要手动补充或跳过这一步。我一般会先检查sum(results.branch(:,6))是否大于 0再决定要不要算负载率。4. 改数据、加设备、做扩展在 case39 上做二次开发的避坑指南4.1 新增节点和线路时最容易犯的三个错误在 39 节点系统上做扩展比如接入一个风电场或者新增一条线路最容易犯的三个错误是节点编号冲突、矩阵维度不匹配、基准电压不一致。节点编号冲突很好理解你新增一个节点编号写了 40但bus矩阵里原来有 39 行直接bus(40,:) ...会报索引越界。正确做法是先扩展矩阵行数再赋值。更隐蔽的问题是branch矩阵里新增线路的首末节点号如果写了 40 但bus矩阵还没扩展runpf会报“节点不存在”。矩阵维度不匹配是另一个高频翻车点。bus矩阵有 13 列你新增一行时只填了 10 列MATPOWER 在解析时会自动补零但补零的位置如果落在BUS_TYPE列就会变成 PQ 节点可能导致平衡节点丢失。我一般会写一个辅助函数确保新增行的列数和原矩阵一致。基准电压不一致更隐蔽39 节点系统里有 345 kV 和 230 kV 两个电压等级通过变压器连接。如果你新增的节点基准电压填了 110 kV但通过一条线路直接连到 345 kV 节点上潮流计算时标幺值换算会出错结果完全不可信。% 安全扩展 bus 和 branch 矩阵 mpc case39; nbus size(mpc.bus, 1); ncol_bus size(mpc.bus, 2); % 新增节点 40PQ 节点负荷 100 MW基准电压 230 kV new_bus zeros(1, ncol_bus); new_bus(1) 40; % 节点编号 new_bus(2) 1; % PQ 节点 new_bus(3) 100; % 有功负荷 MW new_bus(4) 30; % 无功负荷 MVar new_bus(8) 1.0; % 电压初值 new_bus(10) 230; % 基准电压 kV mpc.bus [mpc.bus; new_bus]; % 新增线路从节点 40 到节点 20阻抗 0.01j0.1 ncol_br size(mpc.branch, 2); new_br zeros(1, ncol_br); new_br(1) 40; % 首端节点 new_br(2) 20; % 末端节点 new_br(3) 0.01; % 电阻 new_br(4) 0.1; % 电抗 new_br(5) 0.02; % 电纳 mpc.branch [mpc.branch; new_br]; % 校验维度 assert(size(mpc.bus,2) ncol_bus, bus矩阵列数不一致); assert(size(mpc.branch,2) ncol_br, branch矩阵列数不一致); disp(扩展完成维度校验通过);这段代码的关键在于先读取原矩阵的列数用zeros创建新行再逐列赋值最后用assert校验维度。参数方面new_bus(10)是基准电压必须和所连节点的电压等级一致否则变压器变比换算会出错。新增线路的电阻电抗用标幺值基准值由baseMVA和节点基准电压共同决定。4.2 把新能源机组接入 case39PV 节点转换与无功控制模式在 39 节点系统上做新能源接入仿真常见做法是把某个同步发电机替换成风电场或光伏电站。这里有一个关键选择新能源机组是作为 PV 节点还是 PQ 节点如果新能源机组具备无功电压控制能力可以保持 PV 节点但需要修改gen矩阵里的无功限值因为电力电子变流器的无功调节范围通常比同步发电机窄。如果新能源机组只发有功、不调无功就要把对应节点从 PV 转为 PQ同时把gen矩阵里该发电机的无功出力设为 0并在bus矩阵里增加相应的无功负荷来模拟变流器吸收的无功。我一般会保留原同步发电机作为调相机把新能源机组作为负的负荷接入。这样既不影响原系统的电压支撑又能模拟新能源的功率注入。具体做法是在bus矩阵的负荷列减去新能源出力或者在gen矩阵里新增一行把节点类型设为 PQ有功设为负值。4.3 批量潮流扫描用 for 循环做负荷增长与 N-1 校验做规划或稳定研究时经常需要批量跑潮流。比如负荷从 80% 增长到 120%步长 5%观察电压最低点和线路负载率的变化。或者做 N-1 校验逐条断开线路看系统是否还能收敛。批量扫描的代码框架很简单但有两个细节要注意每次循环都要重新加载原始数据避免上一次的修改累积断开线路时不要真的删除branch矩阵的行而是把该线路的阻抗设为一个极大值或者把首末节点改成一个不存在的节点号让潮流程序自动忽略它。% 负荷增长扫描80% 到 120%步长 5% load_scales 0.8:0.05:1.2; Vmin zeros(size(load_scales)); for k 1:length(load_scales) mpc case39; % 每次重新加载 mpc.bus(:,3) mpc.bus(:,3) * load_scales(k); % 有功负荷 mpc.bus(:,4) mpc.bus(:,4) * load_scales(k); % 无功负荷 results runpf(mpc); if results.success Vmin(k) min(results.bus(:,8)); else Vmin(k) NaN; end end plot(load_scales, Vmin, -o); xlabel(负荷倍数); ylabel(最低电压 (p.u.)); grid on;这段代码的逻辑是每次循环重新加载case39按比例缩放所有节点的有功和无功负荷跑潮流记录最低电压。如果某次不收敛Vmin记为 NaN画图时会断开。参数方面load_scales的范围和步长可以根据需要调整我一般从 0.8 开始因为 39 节点系统在 1.2 倍负荷附近可能接近电压稳定极限。5. 避坑与排查case39 潮流计算中最容易翻车的五个场景5.1 报错“Undefined function case39”路径与命名冲突现象MATLAB 命令窗口输入case39后报错提示函数未定义。原因通常有两个一是case39.m文件不在当前工作目录或 MATLAB 搜索路径下二是文件被重命名过比如叫case39_data.m但函数名还是case39MATLAB 要求文件名和函数名一致。解决方法是把文件放到当前目录或者用addpath添加路径并确认文件名和函数名完全一致。5.2 潮流结果里发电机无功出力为负无功限值设置反了现象跑完潮流后某些发电机的无功出力是负数而且数值很大。原因通常是gen矩阵里最大无功和最小无功两列填反了或者最小无功填了一个正数。MATPOWER 在迭代过程中会检查无功限值如果限值不合理PV 节点会频繁在 PV 和 PQ 之间切换导致结果异常。解决方法是确认gen(:,4)是最小无功通常为负gen(:,5)是最大无功通常为正且gen(:,4) gen(:,5)。5.3 节点电压全部为 1.0潮流根本没跑或者结果没更新现象printpf输出的电压全是 1.0相角全是 0。原因可能是你直接打印了原始case39的bus矩阵而不是runpf返回的results.bus。原始数据里的电压列只是初值通常设为 1.0。解决方法是确认你打印的是results.bus(:,8)和results.bus(:,9)而不是mpc.bus(:,8)。5.4 线路潮流方向与预期相反首末节点定义搞反了现象某条线路的有功潮流方向和你手算的相反。原因通常是branch矩阵里首末节点号填反了或者你在后处理时取了末端有功而不是首端有功。MATPOWER 约定branch(:,1)是首端branch(:,2)是末端results.branch(:,14)是从首端流向末端的有功。如果方向反了把首末节点对调即可但要注意变压器变比的方向也会跟着变。5.5 修改数据后不收敛忘了同步更新 gencost 或 bus 矩阵现象在gen矩阵里新增了一台发电机潮流不收敛。原因可能是新增的发电机节点在bus矩阵里还是 PQ 节点或者gencost矩阵没有同步扩展导致runopf报维度错误。解决方法是每次修改gen后检查对应节点的BUS_TYPE是否已改为 2并确认gencost的行数和gen一致。如果只跑runpfgencost不影响但养成同步更新的习惯能避免后续切换程序时出错。6. 从 case39 到自定义系统一套可复用的数据校验与潮流脚本6.1 把校验逻辑封装成函数check_case 的五个断言在 case39 上跑通之后下一步通常是把这套流程迁移到自定义系统上。我一般会把第 2 章里的校验逻辑封装成一个check_case函数输入是一个 MATPOWER 格式的结构体输出是校验通过或报错信息。五个核心断言节点编号连续且从 1 开始、平衡节点唯一、发电机节点类型正确、线路首末节点存在、基准电压非零。function check_case(mpc) % 校验 MATPOWER 格式数据包的完整性 bus mpc.bus; gen mpc.gen; branch mpc.branch; assert(isequal(bus(:,1), (1:size(bus,1))), 节点编号不连续); assert(sum(bus(:,2) 3) 1, 平衡节点数量不为1); for i 1:size(gen,1) idx find(bus(:,1) gen(i,1)); assert(~isempty(idx), [发电机节点 num2str(gen(i,1)) 不存在]); assert(bus(idx,2) 2 || bus(idx,2) 3, ... [发电机节点 num2str(gen(i,1)) 类型错误]); end for i 1:size(branch,1) assert(any(bus(:,1) branch(i,1)), 线路首端节点不存在); assert(any(bus(:,1) branch(i,2)), 线路末端节点不存在); end assert(all(bus(:,10) 0), 存在基准电压为零的节点); disp(check_case 全部通过); end这个函数的参数就是标准的 MATPOWER 结构体bus、gen、branch三个矩阵。断言顺序按重要性排列节点编号和平衡节点是结构性的必须最先检查。发电机节点类型和线路节点存在性是关联性的放在中间。基准电压是数值性的放在最后。如果你拿到的数据包列数不是标准的 13 列需要先调整列索引再调用这个函数。6.2 用 case39 做算法验证以直流潮流与交流潮流对比为例case39 的另一个价值是作为算法验证的基准。比如你写了一个直流潮流程序想验证它和交流潮流的偏差就可以在 case39 上跑对比。直流潮流忽略无功和电压幅值只算相角和有功通常用于快速估算。在 39 节点系统上直流潮流的有功结果和交流潮流的有功结果偏差一般在 5% 以内但电压幅值信息完全丢失。我一般会写一个对比脚本同时跑rundcpf和runpf然后比较线路有功的差异。如果某条线路的偏差超过 10%说明该线路的电阻电抗比偏大直流潮流的假设不成立。这个技巧在判断自定义系统能不能用直流潮流做快速扫描时特别有用。6.3 一个习惯每次改数据前先备份原始 case39最后说一个我坚持了很多年的习惯每次在 case39 上做修改之前先把原始数据另存一份命名成case39_orig.m然后在副本上改。MATLAB 的case39函数每次调用都会返回一份新的结构体理论上不会污染原始数据但如果你不小心把mpc.bus的某列改了又用save覆盖了原文件后悔药是没地方买的。我一般会在脚本开头写一行mpc case39;然后在所有修改之前加一句mpc_orig mpc;这样随时可以回退。对于批量扫描每次循环重新加载case39是最稳妥的做法虽然多花几毫秒但避免了状态累积导致的玄学问题。这套流程从 39 节点迁移到几百节点的自定义系统上同样适用核心就是先校验、再备份、后修改、勤对比。希望帮到你。本文还有配套的精品资源点击获取