Plot3d格式完全指南:从文件结构到Python解析

发布时间:2026/10/4 6:13:47
Plot3d格式完全指南:从文件结构到Python解析 你要是翻过 CFD 项目里的原始数据文件大概率见过后缀叫.xyz、.x、.grid或者.p3d的文件。用文本编辑器一开满屏数字第一行往往只有三个整数后面跟着一长串坐标序列。懂行的人知道这是 Plot3d 网格数据格式不懂的人看着数字干瞪眼。这篇文章就把 Plot3d 格式一次讲透文件结构、坐标顺序、二进制细节、解析代码、常见坑点全都覆盖。不管是学生做课程设计还是工程师接外部网格只要拿到这类文件你都能快速上手处理。适合所有在 CFD、网格生成、后处理工具链里跟数据文件打过交道的人。1. Plot3d 格式到底是什么为什么值得专门讲一次1.1 一个老格式的“简历”Plot3d 是上世纪七八十年代由 NASA Ames 研究中心推动的一套结构化网格存储约定。当年的 CFD 计算远没有今天这么“图形化”求解器和后处理程序之间靠的就是纯文本或者二进制数据交换。Plot3d 在这套体系里扮演了一个中间人的角色它不管几何怎么生成也不管边界条件怎么设置只负责把结构化网格的节点坐标按顺序写清楚。它的核心思路非常朴素结构化网格的每个节点都可以用(i, j, k)三个整数索引定位所以只要事先声明三个方向的节点数ni、nj、nk再按固定顺序把所有节点的x、y、z坐标写出来就能完整还原一块网格。因为没有单元连接表、没有边界类型标记、没有注释字段整个文件可以做得极其精简。正是这种精简让它活到了今天。在开源求解器、网格生成器、后处理脚本之间来回倒腾数据时Plot3d 往往是成本最低的交换格式。很多经典算例比如各种翼型、叶栅、外流场算例的原始网格和流场数据也都是用它存的。说它是 CFD 圈里的“乐高积木”一点不夸张简单、通用、谁都能拼。1.2 网格文件与结果文件的分工Plot3d 格式可以粗略分成两大类一类存几何叫网格文件常见后缀有.xyz、.x、.grid、.g另一类存流场叫结果文件或者 Q 文件常见后缀有.q、.fun、.vortex、.rhoe。网格文件只放坐标。按块组织单块文件开头写三个整数ni nj nk多块文件开头写一个整数nblocks然后写每个块的维度。接着按块依次写坐标序列。就这么简单。结果文件则是在网格文件的维度信息基础上再叠加每个节点上的物理量。比如二维无粘流的守恒变量一般是四个量密度、x 方向动量、y 方向动量、能量三维加一个 z 方向动量变成五个量。文件在维度声明之后会再写一行参数说明每个点有几个量、有没有附加标志然后把所有节点的物理量按和坐标一样的顺序排列。理解这个分工很重要。很多新手把网格文件和结果文件当成两种完全不同的东西其实它们“骨架”完全一致只是结果文件在坐标之外多了一组标量/矢量场。后面解析时完全可以共用一套维度读取逻辑。1.3 适用场景谁还在用 Plot3d你可能想问现在有 CGNS、VTK、HDF5 这些功能更强的格式为什么还专门研究 Plot3d我的看法是它在特定场景下依然很能打。轻量传输场景里Plot3d 的体积比其他自描述格式小得多因为它没有冗余的字典信息。脚本处理场景里纯文本 Plot3d 用一个几行的 Python 脚本就能解析不需要引入大型库。数据交换场景里很多老牌工具Pointwise、Gridgen、CFL3D、OVERFLOW 等原生支持 Plot3d从外部拿到的网格经常就是这个格式。如果你要做网格质量检查、格式转换、算例对比不会读 Plot3d 会很被动。当然它也有明显短板不携带边界条件、不携带坐标系信息、对非结构网格无能为力、注释支持极差。所以它的定位很清楚——结构化网格数据交换的“最小公分母”而不是万能格式。2. 文件结构拆解从一行数字读懂数据布局2.1 ASCII 单块网格的完整读写流程先看一个最简单的 ASCII 单块二维网格例子。假设网格在 x 方向有 3 个点y 方向有 2 个点z 方向没有变化可以写成3 2 1 0.0 0.5 1.0 0.0 0.5 1.0 0.0 0.0 0.0 1.0 1.0 1.0第一行的3 2 1分别对应ni、nj、nk。接着是 x 坐标序列一共3 * 2 * 1 6个值0.0 0.5 1.0对应 j0 那一行上 i0,1,2 三个点的 x 坐标0.0 0.5 1.0对应 j1 那一行。然后写 y 坐标序列同样 6 个值两行分别是 y0 和 y1 平面上的坐标。关键点在于三个嵌套循环的顺序。Plot3d 的坐标写入相当于for k in range(nk): for j in range(nj): for i in range(ni): write x[i, j, k] for k in range(nk): for j in range(nj): for i in range(ni): write y[i, j, k] for k in range(nk): for j in range(nj): for i in range(ni): write z[i, j, k]也就是说最内层是i变化最快然后是j最外层是k。这和 Fortran 的数组列主序完全一致。你在读文件时拿到坐标数据后要想还原成三维数组用 NumPy 的reshape((ni, nj, nk), orderF)就能一步到位。我见过不少人在这里栽跟头用 C 序orderC去 reshape结果画出来的网格扭曲得完全没法看。三维网格和二维的逻辑一样只是nk大于 1坐标序列长度是ni * nj * nk写完x写y写完y写z。整个单块网格文件短则几 KB长则几百 MB本质上就是“维度声明 三大段坐标”这个结构。2.2 多块文件块数量与维度声明的两种写法真实工程里的网格很少是单块的。翼身组合体、叶轮机械通道、带外挂物的飞行器通常会被切分成多个结构化块块与块之间共享分界面。多块 Plot3d 文件就是在单块的基础上把多个块的数据按顺序拼接起来。多块文件的标准写法是第一行写一个整数nblocks接下来给出一共3 * nblocks个维度整数再往后才是每个块的坐标数据。举例来说2 3 2 1 4 3 1 0.0 0.5 1.0 0.0 0.5 1.0 0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.5 1.0 1.5 0.0 0.5 1.0 1.5 ...这里第一块是3 2 1第二块是4 3 1。后面按块号依次写每一块的坐标序列块与块之间没有额外的分隔标记读的时候必须靠每块的点数去切分数据。实际使用中你会碰到两种变体。一种是把所有块的维度写在同一行也叫“一张维度表”风格另一种是每个块维度单独占一行可能还混有注释或空行。写解析器时最稳妥的策略不是死磕行结构而是把整个文本文件先拆成 token 流去掉空行和注释再用索引顺序切分。这样无论维度怎么换行都能处理。还有一点容易引起混乱一些工具保存“单块”Plot3d 时并不写nblocks1而是直接写ni nj nk三个整数。所以判断文件是不是多块不能只看第一行是不是 1。更可靠的判断方法是统计前几行的整数个数如果第一个整数后面跟的两个整数共同组成三个一组那就当单块处理如果第一行孤立一个整数后面跟着3 * nblocks个维度那才是真正的多块描述。许多成熟解析器的自动识别逻辑都是按这个思路做的。2.3 结果文件Q 文件的数据布局结果文件的作用是把流场数据映射到网格节点上。一个典型的二维 Q 文件长这样3 2 1 4 0 1.10 0.20 0.30 2.50 1.20 0.25 0.32 2.60 ...第一行依旧是维度声明。第二行的含义在不同求解器里有差异但常见的约定是第一个整数表示每个节点的物理量个数nq第二个整数留作附加标志比如是否包含湍流变量、是否是无量纲量没有附加信息就写 0。接下来的数据按节点顺序排列每个节点写nq个值。以三维可压缩流为例如果是守恒变量一般是rho、rho*u、rho*v、rho*w、e如果是原始变量可能是p、u、v、w、T。到底按哪种顺序只能以生成文件的程序手册为准格式本身不做强制规定。按我的经验解析 Q 文件最大的坑不是维度而是“每个点有几个量”。三维无粘默认 5 个量三维粘性可能加湍流变量变成 7 个、8 个甚至更多。你在读文件之前最好先去确认这个信息不要靠猜。否则数据错位之后画出来的云图会非常奇怪密度和温度混在一起颜色分布毫无物理规律。2.4 二进制版本Fortran 记录格式与字节序问题文本文件好懂但体量大、解析慢所以大网格常用二进制 Plot3d。二进制不像 ASCII 那样一行一行写数据而是按 Fortran 的“记录record”机制存储。每条记录前后都有一个 4 字节整数标记这条记录的长度。读数据时你会看到这样的结构一个 4 字节记录头值通常是 4表示接下来 4 字节是一个整数4 字节的nblocks值一个 4 字节记录尾值也是 4一个 4 字节记录头值等于4 * 3 * nblocks所有块的维度整数数组对应的 4 字节记录尾进入坐标数据记录每条记录的头尾长度标记可能是12 * ni * nj * nk、16 * ni * nj * nk这种大数字。坐标数据在一条记录内还是分多条记录不同工具实现不一样。有的把 x、y、z 合在一条大记录里有的每个方向单独一条记录。更麻烦的是还有字节序问题早期 UNIX 工作站多是大端big-endian现代 PC 几乎都是小端little-endian。读出来全是无规律巨数时第一反应就应该是字节序不对把f4换成f4或者反过来再试一次。二进制版的 Q 文件也是同样的记录机制。网格和流场在一个文件里的情况偶尔也能见到一般前半部分是几何坐标后半部分紧跟物理量中间没有明显的分段标记。这种文件解析时尤其要小心一旦维度算错后面所有数据全部错位。3. 实操用 Python 读写 Plot3d 网格的完整方案3.1 用纯 Python 实现一个健壮的 ASCII 读取器先给一个可以直接用的 ASCII 多块网格读取器。它不做花哨的内存优化但结构清晰适合自己改造成项目中的工具函数。import numpy as np def read_plot3d_ascii(filename): tokens [] with open(filename, r) as f: for line in f: line line.split(#)[0] # 去掉注释如果生成工具加了注释 tokens.extend(line.split()) idx 0 first int(tokens[idx]) if first 1 and len(tokens) 4 and tokens[1].replace(., ).isdigit() is False: # 多块写法第一行是块数后面是维度表 nblocks first idx 1 dims [] for _ in range(nblocks): ni int(tokens[idx]); nj int(tokens[idx1]); nk int(tokens[idx2]) idx 3 dims.append((ni, nj, nk)) else: nblocks 1 idx 0 ni int(tokens[idx]); nj int(tokens[idx1]); nk int(tokens[idx2]) idx 3 dims [(ni, nj, nk)] blocks [] for (ni, nj, nk) in dims: npts ni * nj * nk x np.array(tokens[idx:idxnpts], dtypefloat) idx npts y np.array(tokens[idx:idxnpts], dtypefloat) idx npts has_z (nk 1) if has_z: z np.array(tokens[idx:idxnpts], dtypefloat) idx npts else: z np.zeros_like(x) # 还原成三维数组i 方向在内存中变化最快 x x.reshape((ni, nj, nk), orderF) y y.reshape((ni, nj, nk), orderF) z z.reshape((ni, nj, nk), orderF) blocks.append((x, y, z)) return blocks这个函数有几个地方值得说明。第一我用line.split(#)[0]先去掉注释。很多软件虽然不支持 Plot3d 注释但用户手动改文件时会加#标记这一步能避免踩坑。第二判断单块还是多块时只根据 token 流内容判断如果第一行只有一个整数而且它不是1个块时正好配合后面的维度表如果第一行就是三个整数则直接按单块解析。第三坐标数据转换成三维数组时用了orderF这是为了让数组索引顺序和写入顺序一致。这样后续访问x[i, j, k]时内存顺序就对应 Plot3d 的数据流不容易混淆。3.2 二进制读取器的实现思路与记录校验二进制读起来稍微复杂一点。核心是处理 Fortran 记录。我一般这样实现一个读取记录的辅助函数import struct import numpy as np def read_record(f, byteorder, dtype): head f.read(4) if len(head) 4: return None (n,) struct.unpack(byteorder i, head) data np.frombuffer(f.read(n), dtypenp.dtype(byteorder dtype)) tail f.read(4) (m,) struct.unpack(byteorder i, tail) if m ! n: raise ValueError(Fortran record length mismatch) return data调用时先尝试一种字节序。比如默认假设小端数据类型是单精度浮点f4with open(filepath, rb) as f: nblocks int(read_record(f, , i4)[0]) dims read_record(f, , i4).reshape((-1, 3)) blocks [] for ni, nj, nk in dims: npts int(ni * nj * nk) coords read_record(f, , f4) x coords[0:npts].reshape((ni, nj, nk), orderF) y coords[npts:2*npts].reshape((ni, nj, nk), orderF) z coords[2*npts:3*npts].reshape((ni, nj, nk), orderF) blocks.append((x, y, z))这里有个工程细节有些二进制文件里一个块的 x、y、z 不是合在一条记录里而是分成三条记录。如果合并读取失败也就是read_record返回的记录长度不等于3 * npts * 4你就可以改成分别读三条再拼接。我的做法是先检测一次记录头如果长度对不上就按“每方向一条记录”重新读。还有一点很多老式文件的坐标不是单精度而是双精度f8。判断方法很简单读一条记录看长度如果长度按f4解释时得到的点数和维度算出的ni*nj*nk差一个量级再按f8试试。这属于经验活但写解析器时把这两个分支都处理掉基本就够用了。3.3 写出网格ASCII 与二进制如何选写网格比读网格容易因为布局完全由你控制。如果是给其他工程师看的调试用网格我推荐写 ASCII方便别人直接用文本编辑器检查。输出的格式可以这样组织2 3 2 1 4 3 1 0.0 0.5 1.0 0.0 0.5 1.0 0.0 0.0 0.0 1.0 1.0 1.0 ...用 Python 写的时候可以这样with open(out.xyz, w) as f: f.write(f{nblocks}\n) for ni, nj, nk in dims: f.write(f{ni} {nj} {nk}\n) for block in blocks: x, y, z block xyz np.stack([x, y, z], axis0) for comp in range(3): data xyz[comp].flatten(orderF) f.write( .join(f{v:.8f} for v in data) \n)对很大的网格ASCII 写的文件会非常占空间。一个千万级节点的网格光坐标文件就轻松超过 500 MB写起来慢后续读取也慢。这种时候优先写二进制好处是文件小、读写快、精度可控。二进制的写法和读取对称先写 nblocks 记录再写维度记录然后每个块写坐标记录。写二进制时最容易犯的错误是不转换字节序。如果你用 numpy 直接写float32数组默认是机器本机字节序在普通 PC 上是小端如果对方工具是按大端读取的就会得到一堆乱码。稳妥做法是先调用numpy.ndarray.astype把 dtype 改成带明确字节序的格式比如f4或f4再写入文件。这个习惯能省掉大量排查时间。3.4 坐标顺序核对i、j、k 方向必须一致有人可能会问我用reshape((ni, nj, nk), orderF)读出来的数组索引[0, 0, 0]是文件的哪个点答案是文件里第一个坐标点。这个点到底位于几何的哪个角则取决于生成网格时的节点编号规则。很多 CF D 代码要求i方向沿某个物理边界比如翼型弦向j方向沿展向k方向沿法向。当你从外部拿到网格时第一件事就是确定代码或者网格生成工具约定的方向和你后续处理时是否一致。一个常见做法是先把 x、y、z 的最大值和最小值所在的索引打出来和预期几何特征对照。for bi, (x, y, z) in enumerate(blocks): xmin_idx np.unravel_index(np.argmin(x), x.shape) xmax_idx np.unravel_index(np.argmax(x), x.shape) print(block index, xmin_idx, xmax_idx)如果最小 x 点出现在(0, 0, 0)而你的几何预期那个角落应该是最小 x说明顺序没问题。如果完全对不上比如最小 x 跑到了(ni-1, 0, 0)那要么是文件的维度声明颠倒要么坐标序列写反了。这种索引层面的检查比肉眼看云图快得多建议每次处理新文件都跑一遍。还有一个很容易被忽视的点有些工具保存 2D 网格时nk要么不写要么写 1但坐标流里没有 z 坐标。你在读取时如果按“有 z 坐标”的分支去取数会把后面的数据吞掉。所以我写代码时总是先看nk是否大于 1再决定要不要读取第三段坐标而不是简单根据维度个数判断。4. 常见问题与排查技巧实录4.1 进入排查前的第一步确认文件到底是什么拿到一个后缀不明的文件先别急着写解析脚本。我习惯先做三件事。第一用十六进制编辑器或xxd打开文件头部看是纯文本还是二进制。如果文件头有\x00这种不可打印字符基本可以确定是二进制。第二如果是文本用文本编辑器或者head -20看前五行数字判断是否具备 Plot3d 特征有维度声明、后面跟着坐标序列。第三看看文件大小是否能整除可能的记录长度。这一步能快速排除一类文件那些其实是 CGNS、VTK 或者 NaN 网格的文件只是后缀起成了.xyz。我见过有人拿着一份 VTK 结构化网格文件硬套 Plot3d 解析逻辑结果当然是一团糟。遇到这种情况先搞清楚对方给你的到底是什么格式再谈解析。4.2 二进制读出来全是乱码/负百万级数据这是我最常被问到一个问题用numpy.fromfile直接读二进制 Plot3d数据全是-1.7e38这种值。原因基本就两个。第一个是没处理 Fortran 记录头尾。直接fromfile会把每条记录前面的长度整数当成浮点数据导致整个序列错位。解决办法是先解析记录长度或者跳过每一条记录的 4 字节头和 4 字节尾。第二个是字节序反了。如果你发现读出的数据数量是对的但值完全没有物理意义而且里面经常出现超大正数或负数那就把字节序反过来试一次。在 numpy 里最简单的方式是读取时直接指定 dtype比如np.dtype(f4)如果换成了f4数据就正常说明文件就是另一个字节序。排查时可以先用一个最小例子构造一个 2x2x1 的网格文件分别用 ASCII 和二进制保存然后用自写解析器去读。如果这个都过不了问题通常出在记录结构而不是后面的维度处理。4.3 多块拼接与重叠边界检查多块 Plot3d 文件解析成功后下一步要关注的是块与块之间的关系。由于文件里没有连接信息块之间的共享分界面只能靠几何位置来匹配。简单检查方法是把每块的最小和最大坐标范围打印出来两两取交集如果某个方向的区间有重合那很可能就是共享边界。更进一步的检查是比对重合面上的节点坐标。先按容差把两块中距离小于某个阈值的点找出来看看数量是否符合预期。如果两块网格在同一个物理面上一方有 51 个点、另一方有 51 个点那么匹配点数量应该是 2601 或接近这个数。如果数量差很多说明两块网格的拓扑不对齐拼接处会有缝隙后续计算可能出问题。这里我建议写一个简单的表面点最近邻匹配脚本用scipy.spatial.cKDTree或者粗暴的双循环判断小网格都能做。Plot3d 本身不管连接关系所以这些检查必须由使用者自己做。4.4 可视化看网格是否有畸变排查几何问题最直观的手段就是可视化。对于二维某个截面把网格线画出来看是否有交叉、拉断、负体积。最简单的脚本是这样import matplotlib.pyplot as plt x, y, z blocks[0] k 0 fig, ax plt.subplots() for j in range(y.shape[1]): ax.plot(x[:, j, k], y[:, j, k], b-, lw0.4) for i in range(x.shape[0]): ax.plot(x[i, :, k], y[i, :, k], r-, lw0.4) ax.set_aspect(equal) plt.show()画出来之后看着网格线交叉、折叠基本就是维度顺序颠倒或者坐标流解析错误。如果只是局部网格比较密或比较疏那是正常现象但如果某几条线突然穿过别的线那就有问题了。三维网格可以用pyvista直接显示表面网格。把(ni, nj, nk)的三维数组塞给StructuredGrid再save成 VTK 文件或者直接plot()。这样能快速定位严重的负体积和畸变区域。4.5 常见问题速查表为了方便参考这里整理一张速查表覆盖我实际工作中遇到的典型问题。症状可能原因处理方式第一行只有一个整数后续解析错位多块文件维度表没按多块处理判断 nblocks按维度表切分坐标序列长度对不上维度声明写错或读取时把 z 坐标漏读了核对 ni、nj、nk 与坐标段长度读出的坐标全是超大数字节序不对切换f4和f4再试解析失败找不到记录尾长度文件不是 Fortran 记录或按每方向一记录读取时错位用十六进制编辑器查看文件头确认记录结构某一块坐标严重偏移多块时块序和预期不一致打印每块坐标范围按几何位置重新排序网格线交叉维度声明与坐标流顺序颠倒用reshape(orderF)并核对方向结果文件的物理量范围离谱nq 参数搞错或物理量顺序与预期不同查生成程序文档确认 nq 和变量顺序5. 从 Plot3d 到其他格式工具链衔接建议5.1 用 Matplotlib 做网格质量初检前面已经写了画网格线的代码这里再补充一个判断负体积的思路。对结构化六面体网格你可以对每个单元计算 Jacobian 行列式。如果某个单元的 Jacobian 小于等于零说明单元翻转或者退化是网格质量事故。理论公式不复杂对每个六面体单元取 8 个顶点把单元切分成若干个四面体分别计算体积只要所有四面体体积为正单元基本就是好的。更简单的做法是利用单元三个方向边向量计算混合积。如果所有单元都正常混合积符号一致。# 简化版对三维结构化网格计算每一层的单元面积方向 # 这只是初筛更严格的需要逐单元计算 Jacobian x, y, z blocks[0] ni, nj, nk x.shape detJ np.zeros((ni-1, nj-1, nk-1)) for i in range(ni-1): for j in range(nj-1): for k in range(nk-1): # 取沿 i、j、k 三个方向的边向量 ei np.array([x[i1,j,k]-x[i,j,k], y[i1,j,k]-y[i,j,k], z[i1,j,k]-z[i,j,k]]) ej np.array([x[i,j1,k]-x[i,j,k], y[i,j1,k]-y[i,j,k], z[i,j1,k]-z[i,j,k]]) ek np.array([x[i,j,k1]-x[i,j,k], y[i,j,k1]-y[i,j,k], z[i,j,k1]-z[i,j,k]]) detJ[i,j,k] np.dot(ei, np.cross(ej, ek))如果detJ出现负值把对应编号打印出来定位到坐标数组中再去原始文件里检查那几行数据。这个方法虽然简单但用来拦截“坐标解析错位”这种系统性错误非常有效。5.2 转成 VTK/CGNS 的快速通道Plot3d 网格在结构化格式里属于比较“原始”的存在很多后处理和可视化工具更习惯用 VTK 或者 CGNS。转换的思路很简单把坐标数组按块填充到目标数据结构里。用pyvista写 VTK 只需要几行import pyvista as pv for x, y, z in blocks: grid pv.StructuredGrid(x, y, z) grid.save(block.vtk)这里传进去的x、y、z必须是三维数组维度顺序是(ni, nj, nk)正好和我们前面用orderF还原出来的结果一致。如果目标工具按 C 序索引也只需要转置轴即可比如.transpose(2, 1, 0)本质是把 k 变成最内层。如果是做 CFD 前处理CGNS 是更合适的方向因为它能存储边界条件、网格连接等额外信息。转换时需要注意 CGNS 的 Zone 也是按结构化块组织的每个 Zone 需要声明维度和坐标。用cgnspy或者pycgns这类库填入数据时每条记录对应 Plot3d 的一个块顺序不变即可。我个人的建议是在没有特别需求时先用 VTK 做可视化检查确认几何没问题后再转入下一步。直接拿解析后的数据去做数值计算如果前期坐标就有问题后面所有结果都不可信。转格式本身不是难点难的是保证坐标顺序和块序在转换过程中不被搞乱。最后再分享一个我工作上的习惯凡是新接触一套网格数据我一定先打印出每个块的最小和最大坐标与文档里的几何范围对照再做一次可视化初检最后才真正进入网格转换或者流场读取。这个流程帮我校正了不少低级错误也节省过很多和同事来回对文件的沟通成本。Plot3d 格式虽然朴素它的规律性很强把数据布局吃透之后你会发现不管换什么工具链处理起来都相当顺手。