简介:gmsh-exodus-converter 是一款基于 Python 的网格格式转换工具,面向使用 Gmsh 生成网格并在 Peridigm 等仿真软件中开展研究的工程师与科研人员,解决 msh 文件向 Exodus II 格式转换的问题。压缩包共 25 个文件、约 861KB,核心包含转换脚本、安装配置与核心转换模块,另附测试用网格、示例效果图、说明文档及依赖配置文件,类型覆盖 Python 脚本、网格文件、图片和文档等。资源已有 563 人学习,适合需要快速掌握网格转换流程的入门到进阶用户。除可直接运行的转换命令与完整源码外,还提供了测试样例便于对照验证;说明文档特别给出 VTK 依赖环境及 Ubuntu 下 Python 绑定包的选择建议,可帮助规避分段错误等常见问题,节省环境配置时间。对于希望批量转换或定制转换逻辑的读者,模块化的代码结构和示例输出也提供了清晰的二次开发基础。
1. 为什么近场动力学前处理,卡在了 gmsh 和 Peridigm 的格式缝隙上
接过一个断裂模拟项目时,同事发来一份用 gmsh 画好的三维网格,模型切分得很漂亮,四面体也排布得均匀。结果 Peridigm 一读就报错,日志里只留下一句“找不到有效的单元块信息”。当时的问题很典型:gmsh 默认输出的 .msh 文件只描述几何与单元,而 Peridigm 这类近场动力学求解器需要的是 exodus II 格式里那一整套块、节点集和材料区域的组织关系。后来我把“gmsh-exodus-converter”这条路径理成一套固定流程,再也没在这步翻过车。
这个标题要解决的,就是把 gmsh 编写的网格完整转换为 exodus II 格式,让 Peridigm 能正确读取并用于近场动力学计算。适合正在做近场动力学、键型本构或断裂扩展模拟的从业者,尤其是那些已经用 gmsh 完成了几何建模、却在求解器前处理阶段被格式卡住的人。下面这套流程不依赖任何收费软件,纯命令行和 Python 脚本就能跑通。
2. 先从格式差异下手:exodus II 在存什么,gmsh 缺什么
2.1 exodus II 不是“一种网格”,是一套模型组织规范
很多人第一次接触 exodus II 时,以为它和.msh一样,只是换了个扩展名的网格文件。这个误解会让后面的每一步都走得别扭。exodus II 本质上是一个基于 NetCDF 或 HDF5 的数据模型,它在文件里组织的是“节点坐标数组、单元块、节点集、边集、结果变量”等多个相互关联的对象,而不是简单地把点和单元排成两张大表。
以 Peridigm 的读取逻辑来看,最重要的三个对象是节点坐标、单元块和节点集。节点坐标好理解,就是每个物质点的空间位置;单元块则承担了“材料区域”的职责——同一个单元块里的单元,在后续输入卡里会被指定为同一种材料、同一种本构模型;节点集则是边界条件的挂载点,位移约束、力边界都要通过节点集编号去引用。换句话说,exodus II 文件里如果没有把这些对象分清楚,Peridigm 就不知道该把材料参数赋给谁,也不知道该在哪些点上施加边界条件。
gmsh 的物理组其实也有类似的思路,但它在 .msh 文件里的表达方式完全不同。物理组只是给单元和几何实体打上的标签,没有像 exodus II 那样强制区分“单元块”和“节点集”。这就是问题的根源:gmsh 画得出来模型,但缺的是 exodus II 那套组织语义,而不是缺几何数据。
2.2 三条转换路径:现成导出、meshio、专用脚本
我见过不少人第一步会尝试 gmsh 直接另存为.e文件。gmsh 确实内置了 exodus 格式的导出选项,界面里或命令行里都能触发。但在我遇到的场景里,这种方式导出的文件常常只带走了单元块,节点集要么缺失、要么需要手工补一大圈。如果只是拿去看一眼几何,这个导出够用;真要喂给 Peridigm 跑算例,多半会在边界条件挂载那一步卡住。
第二条路是用 meshio 这类格式转换库做中转。meshio 能读 .msh,也能写 exodus 格式,几分钟就能得到一个.e文件。它的优势是快,缺点是它默认只处理节点和单元,对节点集的写入支持很有限。而 Peridigm 的边界条件恰恰依赖节点集,所以这条路径适合纯几何传递,不适合直接对接求解器。
所以我最后用的是第三条路:写一个专用转换脚本,用 meshio 读 .msh,用 pyexodus 类库写 exodus II 文件,在中间阶段手工编排单元块和节点集。这条路看起来工作量最大,但恰恰是唯一一条能把“材料区域、边界条件、单元块编号”三者完整带过去的路径。下表是三条路径的对比。
| 路径 | 能拿到什么 | 缺什么 | 适合场景 |
|---|---|---|---|
| gmsh 直接导出 .e | 节点、单元块 | 节点集不完整,块 id 与物理组对应不可控 | 快速浏览、几何验证 |
| meshio 转格式 | 节点、单元 | 节点集通常丢失,材料区域信息不保留 | 纯几何网格传递 |
| 专用转换脚本 | 完整单元块 + 节点集 + 可控制编号 | 需要自己维护脚本 | 真正对接 Peridigm 的算例 |
2.3 转换器的最小能力清单
在动手写脚本之前,先把转换器必须具备的能力列清楚。这里没有玄学,每一条都是 Peridigm 读取文件时的硬性要求。第一,能够从 .msh 中识别并提取所有三维体单元,比如四面体或六面体,把它们按物理组编号归入对应的单元块。第二,能够把 gmsh 中定义的物理面转换为节点集,因为 Peridigm 的边界条件是挂在节点上的。第三,能够统一处理索引编号,gmsh 的节点编号是 1 开始的,而 Python 数组是 0 开始的,这一步错位会让整个模型坐标乱掉。第四,能够过滤掉二阶单元,Peridigm 的近场动力学离散通常要求一阶单元。第五,写完后能读回自检,确认节点数、单元块数、节点集数与原始模型一致。
这几项能力听起来简单,每一条背后都有对应的坑。接下来的章节就按照这个清单,从 gmsh 端准备开始,一步步把转换器搭起来。
3. 网格端准备:物理组和单元阶数定生死
3.1 物理组编号规划:材料归材料,边界面归边界面
转换是否顺利,一半在 gmsh 建模阶段就已经决定了。我习惯在写.geo脚本时就约定一套物理组编号规则:材料区域用 1 到 99 编号,边界面用 100 到 199 编号,边界点用 200 以上编号。这样转换脚本拿到文件后,一看编号就知道哪个是材料区、哪个是节点集来源,不需要额外维护映射表。
下面是一个典型的.geo文件片段,两个材料体、两个边界面:
// model.geo 片段 SetFactory("OpenCASCADE"); // 两个材料体 Physical Volume(1) = {1}; Physical Volume(2) = {2}; // 边界面,编号 100+,用于转换后的节点集 Physical Surface(101) = {5}; Physical Surface(102) = {6}; // 网格参数 Mesh.ElementOrder = 1;关键在最后一行Mesh.ElementOrder = 1,它把单元阶数锁死为一阶。中间的Physical Volume编号是转换后单元块的 id,Physical Surface编号是转换后节点集的 id。用这套编号规则,转换脚本不需要任何额外配置文件,看到 100 以上的物理组编号就自动走节点集提取流程。
我在实际项目中吃过亏:早期直接把所有物理组混在一起,材料区用 1、2,边界面也用 1、2,结果转换出来的文件里冲突一堆,Peridigm 输入卡里都不知道该引哪个编号。从那以后,每次建模前都要确认编号段。
3.2 生成前的三个检查项
建模阶段还有一个容易踩坑的地方:gmsh 默认选项可能把二阶单元、混合维度的单元一起写进文件。Peridigm 对网格的维度一致性很敏感,如果文件里既有四面体又残留三角形面单元,转换脚本虽然能过滤,但过滤出来的块往往和你预期的数量对不上,排查起来很费时间。
我一般会在生成网格后、导出.msh前做三个检查。第一个检查单元阶数,gmsh 界面里在 Mesh 菜单确认单元阶数为 1,或者在.geo文件中用Mesh.ElementOrder = 1固定。第二个检查物理组是否齐全,打开参考信息窗口,确认每个材料体和边界面都有对应的物理组标签。第三个检查维度混合情况,用 gmsh 的统计信息看整个模型里是否混杂了面单元、线单元或点单元。
| 检查项 | 检查方式 | 后果 |
|---|---|---|
| 单元阶数 | Mesh 菜单下的单元阶数设置 | 二阶单元导致近场动力学邻居关系错乱 |
| 物理组完整性 | 参考信息窗口查看物理组标签 | 物理组缺失导致转换后单元块为空 |
| 维度混合 | 统计信息查看各维度单元数量 | 低维单元混入导致 Peridigm 读取出错 |
做完这三个检查再导出,转换脚本基本不会遇到意外数据。如果是从别人那里拿到现成的.msh文件,缺少物理组是最常见的翻车点。这类文件即便几何再漂亮,也建议先回 gmsh 里重新分配物理组,而不是硬着头皮转。
3.3 一阶单元与近场动力学的匹配关系
为什么要死磕一阶单元?这得从近场动力学的离散方式说起。Peridigm 把连续体离散成一系列物质点,每个物质点与周围一定半径内的邻居点建立“键”。键的本构关系基于参考构型中两点的初始距离和相对体积,而单元在这里承担的是给物质点提供参考体积和局部密度。一阶四面体和六面体正好满足这个需求,节点少、体积信息清晰。
二阶单元就不一样了。四面体二阶单元除了四个角节点,每条棱上多出一个边中节点,整体是 10 节点单元。多出来的这些物质点在邻域搜索时会出现位置重合或间距过近的情况,键的性质会被污染。虽然脚本里可以强行只取前四个角节点来降阶,但这样做的几何近似误差会直接影响近场半径内的邻居数量统计。更稳妥的做法是在 gmsh 里直接生成一阶网格,从源头避免这个问题。
我早期接过一个遗留网格,里面全是二阶单元,当时图省事做了降阶处理,结果近场动力学算出来的应力分布图明显不对称,排查了好几天才发现是降阶时破坏了单元体积的一致性。从那以后,二阶单元一律在 gmsh 端解决,不在转换环节做补救。
4. 转换器实现:读取、提取、写出、自检一条龙
4.1 读取 .msh 并解析物理组
转换脚本的第一步是把.msh文件读进来,并弄清楚单元类型、物理组标签的对应关系。我用 meshio 来读取,它的输出结构比较直观。下面是读取和初步探查的代码。
import meshio fname = "model.msh" mesh = meshio.read(fname) # 查看文件里有哪些单元类型,以及每个类型的数量 for block in mesh.cells: print(block.type, block.data.shape) # 查看 gmsh 物理组标签的结构 for label, data in mesh.cell_data_dict.items(): print(label, data)这段代码的关键是理解mesh.cells和mesh.cell_data_dict的对应关系。mesh.cells里每个 block 是同一类型单元的集合,比如全部四面体一个 block、全部三角形一个 block。cell_data_dict里的gmsh:physical字段保存了每个单元所属的物理组编号,维度信息与mesh.cells是一一对应的。运行这段代码后,你会看到类似tetra (12345, 4)的输出,意思是 12345 个四面体单元,每个单元 4 个节点。物理组编号能否成功提取,取决于建模阶段是否做了物理组分配;如果打印结果为空,回到 gmsh 里补上Physical Volume再导出。
参数说明方面,block.type是字符串,常见值包括tetra、hexahedron、triangle、quad、line。block.data是一个二维数组,行是单元序号,列是该单元包含的节点编号。这些编号是 0 索引的,对应mesh.points的行号,写入 exodus II 时需要加 1 转成 1 索引。
4.2 把物理面转成边界节点集
读取完数据后,要做两件核心的事:按物理组分割体单元、从边界面物理组收集节点集。下面这段代码把材料体单元按物理组编号分块,同时把 100 号以上的物理面单元收集成边界节点集。
import numpy as np def extract_blocks_and_nsets(mesh): blocks = {} nsets = {} phys = mesh.cell_data_dict.get("gmsh:physical") if phys is None: raise RuntimeError("没有物理组信息,请回 gmsh 检查 Physical Volume 定义") offset = 0 for block in mesh.cells: btype = block.type ndata = block.data.shape[0] tags = phys["gmsh:physical"][offset : offset + ndata].tolist() offset += ndata # 体单元按物理组编号分块 if btype in ("tetra", "hexahedron"): for i, tag in enumerate(tags): blocks.setdefault(tag, []).append(block.data[i]) # 边界单元(面或线)按物理组编号收集节点 if btype in ("triangle", "quad", "line"): for i, tag in enumerate(tags): if tag >= 100: nodes = block.data[i].tolist() nsets.setdefault(tag, set()).update(nodes) # 节点集排序,保证写入稳定 nsets = {k: sorted(v) for k, v in nsets.items()} return blocks, nsets blocks, nsets = extract_blocks_and_nsets(mesh) print("材料块编号:", sorted(blocks.keys())) print("边界节点集编号:", sorted(nsets.keys()))这段代码的核心是一个offset游标。因为 meshio 把物理组标签按单元顺序展平存放,展平顺序和mesh.cells里各 block 的顺序一致,所以必须用一个游标逐步切分。blocks字典的 key 是材料物理组编号,value 是单元连接关系列表;nsets字典的 key 是边界面物理组编号,value 是去重排序后的节点编号列表。
这里有一个细节值得注意:边界节点集是从面单元上收集节点,一个面上三个或四个节点会同时归属于多个面,所以必须用set()去重。如果不做这一步,同一个节点会在节点集里重复出现,Peridigm 施加边界条件时会给出重复约束的警告。
4.3 用 pyexodus 写出 .e 文件
数据提取完成后,下一步就是写 exodus II 文件。pyexodus 这个库封装了底层的 exodus API,能直接生成 Peridigm 可读的.e文件。下面是写入部分的代码结构。
import pyexodus # 创建文件 exo = pyexodus.exodus("out.e", mode="w", title="converted from gmsh", array_type="numpy") exo.set_dimension(3) exo.set_num_nodes(mesh.points.shape[0]) exo.set_num_elem_blocks(len(blocks)) # 写节点坐标 xs = mesh.points[:, 0].copy() ys = mesh.points[:, 1].copy() zs = mesh.points[:, 2].copy() exo.write_nodes(xs, ys, zs) # 写单元块 for btag, elems in blocks.items(): n_elems = len(elems) n_pe = elems[0].shape[0] exo.write_element_block(btag, "TETRA" if n_pe == 4 else "HEX", n_elems, n_pe) conn = np.concatenate([e + 1 for e in elems]) exo.write_element_connectivity(btag, conn) # 写节点集 for sid, nodes in nsets.items(): exo.write_node_set(sid, np.asarray(nodes) + 1) exo.close()写入逻辑里最关键的是三处参数。第一个是write_element_block的块编号,它必须与 gmsh 建模时定义的Physical Volume编号一致,Peridigm 输入卡里引用的就是这个编号。第二个是单元类型字符串,pyexodus 接受TETRA、HEX这类大写形式,四周体对应 4 节点、六面体对应 8 节点。第三个是conn + 1,meshio 读取的节点编号是 0 索引,exodus II 要求 1 索引,这里漏掉加 1 会让整个模型的拓扑连接直接错乱。
写节点集时同样要加 1。write_node_set的节点编号必须是文件内部的全局节点编号,也就是mesh.points的行号加 1,而不是 gmsh 原始编号。有些读者会拿 gmsh 界面里看到的节点编号直接写入,结果坐标对应错位,这个问题在后面的避坑章节里会展开说。
4.4 写完后先自检一遍
转换器写完.e文件后,不要急着直接丢给 Peridigm,先读回来自检一遍。最基础的自检是确认节点数、单元块数和节点集数量与源文件一致。
# 读取刚生成的文件做一致性检查 check = pyexodus.exodus("out.e", mode="r") print("节点数:", check.get_num_nodes()) print("块数量:", check.get_num_elem_blocks()) # 打印方法名以当前 pyexodus 版本为准,可用 dir(check) 查看自检这一步的作用是发现问题的最前置防线。节点数对不上,说明读取或过滤环节有逻辑漏洞;节点集数量为 0,说明边界面物理组没有被正确识别。每次转换后跑一遍这几个打印语句,能省掉后面调试 Peridigm 输入卡的大量时间。这时候再把文件喂给 Peridigm,通常能直接进入算例配置阶段。
5. 避坑记录:现象、原因、解决
5.1 单元全被丢弃,输出一个空块
现象:转换脚本运行完,.e文件里blocks字典是空的,日志里没有任何报错,但求解器加载时提示找不到单元块。
原因:gmsh 导出.msh时,如果模型中的体没有定义Physical Volume,gmsh 不会把体单元写入文件,或者把它标记成无物理组状态。meshio 读进来时,gmsh:physical字段缺失,脚本里的提取函数直接返回空。
解决:回 gmsh 重新检查.geo文件,确认每个材料体都有Physical Volume定义。记住 gmsh 的规则是:只有被物理组引用的实体才会进入输出文件。转换脚本里加一个保护性判断,当blocks为空时直接抛异常,而不是静默生成空文件,这样至少能第一时间暴露问题。
5.2 边界条件挂不上,求解器说找不到节点集
现象:Peridigm 输入卡里写了位移边界,指定了节点集编号,但求解器报错说找不到对应的节点集,或者边界条件没有生效。
原因:这种问题大多出在转换路径的源头。如果用 gmsh 直接导出.e,或者用 meshio 做纯格式转换,节点集很可能根本没有写进文件。exodus II 的节点集是独立对象,不是靠几何面自动生成的。
解决:改用专用转换脚本,并且在 gmsh 建模阶段把边界面的物理组编号规划到 100 以上。转换脚本里只要看到 100 以上的物理组编号,就自动走节点集提取逻辑。写完.e文件后,用上一节的自检代码确认节点集非空,再进 Peridigm。
5.3 块 id 对不上,材料区域被 gmsh 悄悄重排
现象:转换脚本输出的材料块编号和Physical Volume里写的编号不一致,Peridigm 输入卡里给块 1 指定的材料,实际作用到了块 2 上。
原因:gmsh 在建模过程中如果删除过实体、重新编号过实体,物理组编号在导出时可能被重排。特别是通过 GUI 操作建的模型,编号变化往往不可见。
解决:在.geo文件里显式写出每一个Physical Volume编号,避免依赖 GUI 自动分配。转换脚本启动时先打印sorted(blocks.keys()),人工核对一遍再往下走。这一步不花时间,但能避免 Peridigm 算完后发现材料参数对不上的大返工。
5.4 节点编号偏移一处,坐标全错位
现象:转换后的模型看起来节点位置全乱了,但单元数量又对得上,Peridigm 计算出来的位移分布完全不符合物理直觉。
原因:meshio 读取.msh时已经把节点重编号成 0 索引,某些转换脚本在后续处理中把 gmsh 原始编号和数组下标混用了。比如从面单元收集节点时用的是原始 gmsh 编号,写入节点集时又没有统一映射回数组下标,导致边界的实际位置和预期不符。
解决:从读取到写入,全程只用mesh.points的行号作为唯一节点标识。面单元里存的节点编号本来就是行号,不需要额外映射。写入 exodus II 时统一在这个行号上加 1,任何中间环节都不再引用 gmsh 原始编号。代码里最好写一个节点编号连续性的断言,发现编号超出范围立即中断。
5.5 单位没统一,近场半径算出的邻居数离谱
现象:Peridigm 能跑通,但输出结果里物质点的邻居数要么为 0,要么几万个,近场动力学计算效果完全不可用。
原因:gmsh 建模时的单位制与 Peridigm 输入卡中的单位制不一致。比如 gmsh 里用毫米,模型坐标范围是几百,而 Peridigm 的近场半径写成 0.001,按毫米单位算出来的半径内根本没有邻居节点。
解决:在转换脚本里增加一个坐标范围统计,输出mesh.points.min(axis=0)和mesh.points.max(axis=0),时刻确认模型量级。近场半径的取值要和模型单位保持同一量级,这是近场动力学模拟里最容易忽略、影响却最大的一步。把这个统计写进转换日志,每次转换都瞄一眼,基本不会踩这个坑。
6. 给转换器加三道闸门:让后续算例不再返工
6.1 闸门一:维度与单元类型检查
转换脚本稳定之后,可以再加三道自动闸门,把容易翻车的场景拦在源头。第一道闸门检查单元类型和阶数,确保读进来的只有一阶四面体或六面体。下面这段代码放在读取之后、提取之前执行。
def gate_dimensional(mesh): allowed = {"tetra": 4, "hexahedron": 8} for block in mesh.cells: npe = block.data.shape[1] if npe == 10 or npe == 20: raise ValueError(f"存在二阶单元 {block.type},请回 gmsh 重新生成一阶网格") if block.type not in allowed: print(f"忽略非体单元: {block.type}") print("维度与单元阶数检查通过")它的逻辑是:凡是节点数为 10 或 20 的单元,直接判定为二阶单元并中断转换;对于三角形、线这类非体单元,打印提示但继续执行。这样可以避免二阶单元混入后,Peridigm 在邻居搜索阶段出现莫名其妙的键异常。
6.2 闸门二:节点集完整性检查
第二道闸门验证节点集是否覆盖了输入卡里所有要引用的边界编号。如果 Peridigm 输入卡里打算使用节点集 101、102,转换结果里却缺了 102,这个问题应该在转换阶段就暴露,而不是等求解器报错。
def gate_nsets(nsets, required): missing = [rid for rid in required if rid not in nsets] if missing: raise KeyError(f"缺少边界节点集: {missing}") for rid, nodes in nsets.items(): if len(nodes) == 0: raise ValueError(f"节点集 {rid} 为空,请检查 Physical Surface 定义") print("节点集完整性检查通过")这段代码把需要的节点集编号作为参数传入,缺失时直接抛异常。同时检查节点集是否为空集,防止出现“编号在但一个节点都没收集到”的静默失败。
6.3 闸门三:编号连续性检查
第三道闸门检查节点编号是否连续,防范 0 索引和 1 索引混用带来的隐患。
def gate_indexing(points, blocks): max_nid = points.shape[0] for btag, elems in blocks.items(): for e in elems: if e.max() >= max_nid or e.min() < 0: raise ValueError(f"块 {btag} 存在越界节点编号") print("编号范围检查通过")points.shape[0]是文件里的节点总数,任何单元里的节点编号如果超过这个范围,说明索引映射出了问题。三道闸门全部通过后,才把.e文件交给 Peridigm,基本上能做到一次读入、直接开跑。我第一次搭转换器的时候,就是少了这些检查,拿着 gmsh 直接导出的.e文件强行喂给 Peridigm,被空块问题折腾了两天。现在每次新增算例,这三道闸门先跑一遍,省下的调试时间远超写闸门本身那几行代码的成本。你要是也卡在 gmsh 到 Peridigm 的格式转换上,这套流程可以直接照着搭,希望帮到你。
本文还有配套的精品资源,点击获取