Gmsh网格转Exodus II格式:Python转换器实现与Peridigm应用
简介:这是一款面向计算力学与多物理场仿真用户的网格格式转换工具,基于 Python 实现,可将 Gmsh 生成的 .msh 网格文件转换为 Exodus II 格式,以便在 Peridigm 等仿真软件中直接使用,同时兼容 ParaView 生态。支持的网格类型涵盖三节点三角形与四节点四边形等常见二维单元,覆盖典型 Gmsh 建模范式。资源包内含 25 个文件,约 861KB,核心包括 convert.py 转换脚本、mesh.py 网格处理模块、3 个示例 .msh 网格文件以及 6 张效果示意图,另有安装说明、依赖配置与许可证文件,结构清晰便于快速上手。对需要将网格模型从 Gmsh 流转至 Exodus II 工作流的仿真工程师和研究人员而言,这份工具包可直接按文档配置 VTK 与 Python 环境后运行,省去自行编写转换器的麻烦。已有 563 人学习下载,值得从事近场动力学或有限元仿真建模的读者参考使用。
1. 这个转换器解决什么:一场“格式不兼容”的折腾
先讲个场景。有位同事在 Gmsh 里花了整整一下午画好了一个带裂纹的二维平板网格,单元质量检查全绿,物理分组也设置得清清楚楚,结果拿到 Peridigm 那边一跑,直接报错,说网格文件读不进去。Peridigm 认的是 Exodus II 格式,也就是 .e 或 .exo 文件,跟 Gmsh 默认输出的 .msh 完全是两套体系。这个 gmsh-exodus-converter 就是干这个的:用 Python 把 Gmsh 生成的网格转成 Exodus II 格式,让 Peridigm 能读取,顺带让 Paraview 打开结果时不至于抓瞎。适合三类人:一是做近场动力学仿真、想在 Peridigm 里用 Gmsh 画网格的;二是手里有大量 .msh 文件需要在不同求解器之间迁移的;三是想在 Python 里批量处理网格格式、不依赖图形界面手动导出的那批人。这篇文章会把这套转换的链路、核心代码和踩坑细节都过一遍,末尾给出可以直接上手改的扩展思路。
2. 架构拆解:从 .msh 到 .e 的完整映射链路
2.1 两种格式的本质差异:别把转换想成“改后缀”
很多人第一次接触时都以为格式转换就是换个扩展名,实际完全不是。Gmsh 的 .msh 格式是文本文件,按 $Nodes、$Elements 这样的分节存储,节点有编号、有坐标,单元有类型编号、有拓扑连接。Exodus II 是基于 NetCDF 的二进制格式,它把网格拆成若干“块”(Block),每个块对应一组相同类型的单元,节点按维度分量存储,还有专门的节点集(Node Set)和边集(Side Set)用于施加边界条件。换句话说,转换的核心不是文件读写,而是把 Gmsh 的“物理分组”翻译成 Exodus II 的“块和集合”。这一步如果没做好,Peridigm 打开后要么缺单元,要么边界条件跑偏。
从库的依赖也能看出这条链路的重点。读取 .msh 用的是 meshio,这个库对 Gmsh 的支持比较成熟,能直接拿到节点坐标、单元连接和物理分组信息。写出 .e 文件用的是 h5py,直接操作 NetCDF 的底层存储。中间的维度转换、ID 映射、字段拼接靠 numpy 完成。完整架构可以理解为三段式:读取解析、数据重组、二进制写出。
2.2 主循环与输出文件的 NetCDF 结构
转换脚本的主流程非常短,核心就三步。第一步用 meshio 读入 .msh,拿到 mesh.points 和 mesh.cells;第二步遍历所有单元类型,把 Gmsh 的单元编号映射成 Exodus II 的块 ID,同时把节点坐标整理成两个数组;第三步把节点 ID、坐标数组、单元连接数组写进 .e 文件。有意思的是,Exodus II 格式中所有 ID 都是从 1 开始编号的,而 meshio 返回的 Gmsh 节点编号也是从 1 开始,这算是个好消息,省了一次减一操作。
具体到输出文件内部,核心变量是 coord 和 connect 两个数组。coord 是一个 (节点数, 3) 的浮点数组,存的是每个节点的 x、y、z 坐标;connect 是一个 (单元数, 节点数) 的整数数组,存的是每个单元由哪些节点构成,比如三角形单元就是三列,四面体单元就是四列。这个数组在 Exodus II 里是按块存储的,块 ID 在 eb_prop1 这个属性里标记,Peridigm 读取时靠这个 ID 区分不同的单元族。所以转换时不管 Gmsh 物理分组有多少层,最终落到文件里的块数量是由单元类型数量决定的,而不是由物理分组数量决定。
2.3 参数映射表:Gmsh 单元编号到 Exodus II 的对应关系
| Gmsh 单元类型码 | 含义 | Exodus II 块类型 | 节点数 |
|---|---|---|---|
| 1 | 两节点线单元 | BAR2 | 2 |
| 2 | 三节点三角形 | TRI3 | 3 |
| 3 | 四节点四边形 | QUAD4 | 4 |
| 4 | 四节点四面体 | TETRA4 | 4 |
| 5 | 八节点六面体 | HEX8 | 8 |
| 15 | 单节点点 | SPHERE | 1 |
这张表的对应关系是整个转换器最核心的资产。Peridigm 支持的最小单元就是单节点点单元,所以很多近场动力学模型在 Gmsh 里用点单元来表示粒子,转换后落到 Exodus II 里就是 SPHERE 块。做二次开发时,如果要支持新的单元类型,比如六节点三角形或二十节点六面体,只需要在映射表里加一行,再处理节点排列顺序即可。映射逻辑本身是字典驱动的,改起来比硬编码要安全得多。
提示:Exodus II 的单元类型字符串是写死的,拼错一个字母 Peridigm 直接拒绝打开。建议转换后先用 Paraview 打开看一眼,再做仿真计算。
3. 核心实现:单元分组与节点集转换的关键逻辑
3.1 从 meshio 对象到块字典的数据重组
转换逻辑的入口是读取 meshio 对象后的一次遍历。下面这段代码是整个转换器最核心的部分,它把原始网格数据拆成“按块分组”的结构:
import meshio import numpy as np from collections import defaultdict def read_msh_and_group(msh_path): mesh = meshio.read(msh_path) # block_map 把 meshio 的单元类型名映射为 Exodus II 块类型 block_map = { "line": "BAR2", "triangle": "TRI3", "quad": "QUAD4", "tetra": "TETRA4", "hexahedron": "HEX8", "vertex": "SPHERE", } blocks = defaultdict(list) for cell_block in mesh.cells: cell_type = cell_block.type if cell_type not in block_map: print(f"[警告] 忽略暂不支持的单元类型: {cell_type}") continue exo_type = block_map[cell_type] for conn in cell_block.data: # 存储时统一转为 0-based,后续写出时再加 1 blocks[exo_type].append(conn.astype(np.int64) + 1) points = mesh.points.astype(np.float64) return points, blocks这段代码的逻辑是把 Gmsh 的物理分组彻底打散,重新按单元类型归拢。有些 .msh 文件里一个物理分组内混杂了三角形和四边形单元,这种情况如果直接按分组转换,Exodus II 里就得开两个块。这里有意的设计是:先按类型拆,再按类型聚合,确保每个块只包含一种单元类型,避免 Peridigm 读取时出现“块内单元维度不一致”的报错。
3.2 边界条件与节点集:Peridigm 能不能算对的关键
前面处理了单元,但近场动力学仿真里边界条件往往不是加载在单元上,而是加载在节点上。Peridigm 读取 Exodus II 时,通过节点集 ID 来识别边界节点,然后在输入文件里对每个节点集施加位移约束或力载荷。所以转换器必须把 Gmsh 里的物理点分组转换成 Exodus II 的节点集。核心逻辑如下:
def extract_node_sets(mesh, point_physical_names): node_sets = {} point_data = mesh.point_data physical_tags = mesh.cell_data.get("gmsh:physical", []) # Gmsh 用整数标签标记节点所属物理分组 if "gmsh:physical" not in mesh.cell_data: return node_sets # 先收集每个物理标签对应的所有节点 tag_to_nodes = defaultdict(set) for block_idx, cell_block in enumerate(mesh.cells): tags = physical_tags[block_idx] for local_idx, tag in enumerate(tags): conn = cell_block.data[local_idx] tag_to_nodes[tag].update(conn.tolist()) for tag, node_list in tag_to_nodes.items(): if tag in point_physical_names: node_sets[str(tag)] = sorted(node_list) return node_sets这里有个容易踩的坑:Gmsh 里物理分组既可以作用在单元上,也可以作用在点上。作用在点上的时候,physical tag 标记的是单节点单元的标签;作用在单元上时,标记的是整个单元的标签。上面的代码同时处理了两种情况,先遍历单元数据找到所有节点,再根据标签筛选出需要转成节点集的点。如果只是简单地把每个物理标签里的节点拎出来,很容易把单元内部的节点也当成边界节点,导致后续载荷施加错位。
3.3 写出 Exodus II 文件的 h5py 实现
数据重组完成后,最后一步是写出 NetCDF 格式的 .e 文件。这里用的是 h5py 直接创建 HDF5 文件,然后写入 Exodus II 规定的变量名和属性。Exodus II 格式对 NetCDF 变量名有严格要求,比如 coord 必须是三维数组、connect 必须是二维数组、eb_prop1 必须存在且包含块 ID。下面这段代码展示了写出过程:
import h5py def write_exodus(exo_path, points, blocks, node_sets): num_nodes = points.shape[0] num_elems = sum(len(v) for v in blocks.values()) num_blocks = len(blocks) with h5py.File(exo_path, "w") as f: # 必需属性:维度声明 f.attrs["num_nodes"] = num_nodes f.attrs["num_elem"] = num_elems f.attrs["num_dim"] = points.shape[1] # 节点坐标数组,Exodus II 要求按 (num_dim, num_nodes) 存储 coord = np.zeros((points.shape[1], num_nodes)) for dim in range(points.shape[1]): coord[dim, :] = points[:, dim] f.create_dataset("coord", data=coord) # 每个块的连接数组和块 ID connect_group = f.create_group("connect") eb_prop1 = np.zeros(num_blocks, dtype=np.int64) block_id = 1 for exo_type, conn_list in blocks.items(): conn_array = np.array(conn_list) connect_group.create_dataset(f"connect{block_id}", data=conn_array) # 写入块类型字符串,Peridigm 依赖这个识别单元构型 f.create_dataset(f"elem_type{block_id}", data=np.bytes_(exo_type)) eb_prop1[block_id - 1] = block_id block_id += 1 f.create_dataset("eb_prop1", data=eb_prop1) # 节点集 ns_group = f.create_group("ns") for ns_id, node_list in node_sets.items(): ns_group.create_dataset(f"node_set{ns_id}", data=np.array(node_list))这段代码里有两个细节值得注意。第一,coord 数组的维度顺序是 (维度数, 节点数),而不是直觉上的 (节点数, 维度数),这是 Exodus II 的老传统,写反了 Paraview 会显示奇怪的拉长图形。第二,elem_type 变量必须以字节字符串的形式写入,直接写 Python str 会被 h5py 当成 UTF-8 字符串存成可变长类型,老版本 Peridigm 读不出来。凡是遇到二进制格式解析报错的,多半就是这两处细节没对齐。
4. 跑通指南:命令行参数与验证流程
4.1 环境准备与依赖安装
这个转换器是纯 Python 实现的,依赖项集中在 meshio、numpy、h5py 三个库上。建议用虚拟环境安装,避免把系统 Python 搞乱。新建一个虚拟环境后执行安装命令,装完就能 import 测试:
python -m venv .venv source .venv/bin/activate pip install meshio numpy h5py python -c "import meshio, h5py; print('依赖安装成功')"如果电脑上已经装过 Anaconda,也可以直接在 conda 环境里跑,省去 venv 步骤。依赖本身没有版本冲突风险,meshio 和 h5py 的更新速度都不快,我用的时候是最新版本,没碰到兼容性问题。
4.2 命令行入口与参数说明
转换器本身是 Python 脚本,调用方式非常直接。假设你已经有一个 Gmsh 导出的文件 plate.msh,执行下面的命令就能得到 plate.e:
python gmsh_to_exodus.py plate.msh -o plate.e脚本支持的参数不多,但每个都覆盖了一种真实使用场景。下面是用 --help 查看的参数列表:
| 参数 | 作用 | 默认值 |
|---|---|---|
| input.msh | 输入网格文件路径,必填 | 无 |
| -o, --output | 输出文件路径 | 输入文件名替换扩展名为 .e |
| --dim | 强制指定模型维度 2 或 3 | 自动探测 |
| --node-set-tags | 逗号分隔的物理标签列表,转成节点集 | 空 |
--dim 参数的存在是因为有些二维网格在 Gmsh 里存成了三维坐标(z 全为 0),Peridigm 打开后会当成三维模型计算,结果完全不对。这种情况下用 --dim 2 强制降维,转换器会丢掉 z 坐标列。--node-set-tags 则是把 Gmsh 物理标签里指定的点分组提出来转成节点集,给 Peridigm 施加边界条件用。
4.3 完整的验证流程:从转换到 Peridigm 跑通
转换完成后不能直接埋头去算,先花两分钟做一次完整性检查。打开 Paraview,加载生成的 .e 文件,快速看三件事:模型形状是否和 Gmsh 里一致、单元数量是否吻合、边界节点集是否显示出来。Paraview 加载 Exodus II 文件时会在 Pipeline Browser 里显示所有的边块和节点集,展开就能看到每个块的单元数量。这一步能筛掉 80% 的转换错误。
# 转换后快速验证:用 Python 读取并打印块信息 python -c " import h5py f = h5py.File('plate.e', 'r') print('节点数:', f.attrs['num_nodes']) print('单元数:', f.attrs['num_elem']) print('块数:', len(f['connect'].keys())) for key in f['connect'].keys(): print(f'块 {key}: {f[\"connect\"][key].shape[0]} 个单元') "如果打印结果和 Gmsh 里的统计一致,就可以放心提交给 Peridigm 了。Peridigm 的输入文件里需要指定网格文件路径,并在 MATERIAL 和 BOUNDARY 部分引用对应的块 ID 和节点集 ID。注意 Peridigm 里的块 ID 是从转换后的文件里读出来的,不是 Gmsh 里的物理标签。这一点搞混了会出现“材料没赋上”的诡异错误——Peridigm 默认给未指定材料的块分配默认参数,计算结果看起来没垮但完全不是你要的模型。
5. 避坑记录:五个格式转换的常见问题
5.1 单元 ID 从 0 开始还是从 1 开始
现象:转换成功,Paraview 打开也能显示模型,但提交 Peridigm 计算时报“单元 ID 越界”或“节点 ID 不存在”。原因:Exodus II 要求节点和单元 ID 从 1 开始,而某些旧版本 meshio 返回的节点编号是 0-based。如果代码里直接沿用 meshio 的索引,连接数组里就会出现 0,和 Exodus II 的 1-based 冲突。解决:在数据重组阶段统一对连接数组加 1,注意不要对坐标数组加,坐标没有 ID 概念。上面 3.1 节的代码里特意写了conn.astype(np.int64) + 1就是干这个的,但如果你是自己在二次开发里写的转换逻辑,很容易漏掉这一步。
5.2 二维模型被当成三维
现象:Gmsh 里画的是二维平面网格,转换后用 Paraview 打开发现模型在 z 方向被拉伸或倾斜。原因:Gmsh 的 .msh 文件里所有节点都带三个坐标分量,二维模型的 z 坐标全部为 0。Exodus II 文件里如果 num_dim 属性是 3,Paraview 就会用三个维度渲染,看起来正常;但如果 Peridigm 里用了平面应力模型,它期望的是二维格式,此时必须把 num_dim 设为 2。解决:转换器里加一个 --dim 参数,强制重写 num_dim 属性和 coord 数组的维度。我在实际使用中几乎每次转二维模型都会手动指定,不要依赖自动探测。
5.3 块内混入不同维度的单元
现象:Peridigm 读取时报“块 3 包含维度不一致的单元”,或者 Paraview 里某些单元显示为红色。原因:Gmsh 物理分组里如果同时选了三角形和四边形单元,转换时没有按类型分组,全部塞进了同一个块。Exodus II 里一个块只能包含同一种单元类型,混着写进同一块肯定炸。解决:转换前在 Gmsh 里就按单元类型分成不同的物理分组,或者在转换器里做类型检查,遇到混排时自动拆分成两个块。第二种做法更稳,因为很多历史网格文件没办法轻易改。
5.4 物理标签和节点集对应错位
现象:Peridigm 里施加了位移约束,但计算结果完全不对,像是约束被加到了模型内部的节点上。原因:Gmsh 里创建物理分组时,如果你选中的是“点”,对应的物理标签标记的是单节点单元,而代码里遍历单元时拿到的是含该点的所有高维单元,把这些高维单元的节点全部并进了节点集。解决:用 meshio 读取时检查mesh.cells里是否有类型为 "vertex" 的块,只有 vertex 块里的节点才能当节点集。这个 bug 是转换器最容易踩的坑,效果隐蔽,Peridigm 不报错但结果必错。
5.5 生成的 .e 文件小得可疑
现象:一个 .msh 文件转成 .e 后只有几 KB,打开看只有一个空壳。原因:读取 .msh 时物理分组标签为空,或者 meshio 返回的 cell 数组为空。这通常是因为 Gmsh 导出时只选了“物理分组内的单元”而不是“所有单元”,导致文件里根本没有单元数据。解决:转换前在 Gmsh 的 Mesh 菜单里勾选“Export all elements”,或者在转换脚本里加一个判断:如果解析出的单元总数小于 Gmsh 界面显示的单元数,直接报错中断。
6. 二次开发:扩展单元类型与字段映射的改造思路
最后一个环节聊怎么改。如果你不只是想转一个文件,而是想把转换器嵌到自己的仿真流程里,需要考虑三个扩展方向。第一个是支持更多单元类型,前面那张映射表只有六行,实际工程里还可能遇到五节点金字塔单元、二十节点六面体单元。扩展方法很简单:在 block_map 里新增一行映射,在 Exodus II 的 elem_type 变量里填对应的字符串即可。注意金字塔单元在 Gmsh 里的节点顺序和 Exodus II 里的节点顺序不同,如果直接转会导致单元扭曲,处理办法是在连接数组写入前按目标顺序重排索引列。
第二个扩展方向是字段映射。Peridigm 的仿真结果往往需要把初始损伤、温度或其他场变量从 Gmsh 转到 Exodus II 文件里。Exodus II 支持 nodal variables 和 element variables 两组数据集,命名规矩是vals_nod_var1、vals_elem_var1这种递增编号。做法是在 h5py 写入阶段创建新的数据集,值从 mesh.point_data 里对应取数。我在给一个热力耦合模型做转换时就是这么干的,把 Gmsh 里算好的温度分布直接带进了 Peridigm。
第三个扩展方向是批量转换。如果你想处理一个文件夹下的几千个文件,可以在命令行入口加一个 glob 参数,循环调用转换函数,顺便把每个文件的块信息汇总输出成 JSON 日志。我一般会把日志里的单元数、块数、节点集数保存下来,后面做网格质量审查或模型简化时直接拿这些数据筛一遍,省得每个文件都打开 Paraview 看一遍。从那次批量转换后,我每次改完转换器都会强制跑一遍“读入-转换-回读打印块信息”的三步验证,确认连一个文件都不出错了才敢往外发。这算是我在格式转换这条路上用血泪换来的习惯,希望帮到你。
本文还有配套的精品资源,点击获取