简介:面向借助Gmsh完成网格划分、并需要将模型接入Peridigm等仿真平台开展近场动力学分析的科研与工程人员,gmsh-exodus-converter工具包提供了从Gmsh的msh文件到Exodus II格式的转换方案。工具以convert.py命令行脚本为核心,基于VTK解析网格数据,支持二维场景下三节点三角形与四节点四边形单元,基本覆盖常见二维网格的转换需求。压缩包共25个文件、约861KB,内容包括Python转换源码、mesh.py网格处理模块、多组测试用msh文件、png示例输出图、README说明与安装指引,便于用户快速跑通转换流程并对照结果排查问题。包体结构简洁,适合作为仿真前处理流程的轻量配套组件。目前已有563人学习下载,对从事近场动力学、断裂力学或多物理场仿真的研究者具有一定的参考价值。
1. 为什么需要一个小工具:从gmsh网格到exodus II的最后一公里
gmsh-exodus-converter 这类小工具,解决的是最后一个环节的别扭:用 gmsh 把几何模型切完网格,保存的是 .msh 文件,而近场动力学求解器 Peridigm 认的是 exodus II 格式。手动转换不是不行,但节点重排、单元映射这些操作一旦出错,计算直接白跑。这份资源就是一段可复用的 Python 转换脚本,让你少走这最后一公里。它适合那些已经用 gmsh 建好模型、想在 Peridigm 里跑近场动力学分析的工程师,也适合刚接触 exodus II 格式、想找一份完整读写示例的人。我拿到这份脚本后,先在自己常用的几个网格上跑了一遍,确认它能把节点、单元、物理组都正确搬过去,才敢推荐。
2. 理解两种格式:gmsh的MSH结构与exodus II的数据库模型
2.1 MSH文件格式:节点、单元、物理分组
gmsh 的 .msh 文件,最常见的是 ASCII 版本,它是一个分块的文本文件。文件开头是网格格式版本号,比如 2.2、4.1。4.1 版加入了实体块(entity block)的概念,比 2.2 的纯节点/单元列表更复杂。但无论哪个版本,核心信息就三样:节点坐标、单元连接关系、物理分组(物理实体)。节点段每行是节点编号和 x、y、z 坐标。以 MSH 2.2 为例,$Nodes块第一行是总节点数,后面每行是节点编号 x y z,坐标可以是整数或浮点数。单元段每行是单元类型编号 标签 物理编号 单元编号 节点连接,其中前两个数字在某些版本里是固定的,物理编号表示这个单元属于哪个物理实体。物理分组把某些单元聚到一个编号下,对应 gmsh 里你设置的 Physical Group,后续边界条件和材料属性都要靠它来引用。转换脚本要做的第一件事就是把这三段信息无损地读出来。
MSH 4.1 版本则引入了实体块,每个实体块开头有实体维度、实体标签、实体类型(节点/单元)等元数据。节点数据被按实体分组存放,节点编号可能是全局的,也可能是局部偏移。解析时要特别留意:4.1 的节点段里,第一行是实体块数量,之后每个块有一个头部,然后才是节点列表。如果你用 2.2 的解析逻辑去读 4.1,会直接把块头当作节点数据,结果坐标全乱。所以脚本里最好做一个版本分支,或者统一转成 2.2 再处理。我通常会在 gmsh 导出时用Mesh.MshFileVersion=2.2来规避这个差异,但对于别人已经生成的网格,就只能在代码里硬兼容了。
2.2 Exodus II:数据库、编组、时间序列
exodus II 格式基于 NetCDF 数据模型,你可以把它理解成一棵树:根是文件,下面有不同维度和变量。节点坐标保存在coordx、coordy、coordz三个变量里,维度都是num_nodes。单元连接存放在connect变量里,它的维度是num_elem和num_nodes_per_elem。单元属性、状态变量等都有对应的变量。除此之外,exodus II 还支持编组(由elem_blk、elem_map等变量描述),你可以把侧边单元或特定区域的单元编成一个集合,后续施加边界条件时直接引用编组名。另一大特色是时间序列:每步计算结果可以挂在时间维上,Peridigm 输出结果时就会写入多个时间帧,每个时间帧对应一个step维度的索引。
exodus II 的变量命名有一套约定,比如节点坐标数组名为x、y、z,单元连接数组名为connect,编组信息存储在eb_names和eb_status等变量里。如果你用netCDF4直接读写,需要自己遵循这一套命名,否则 Peridigm 可能无法识别。同时,exodus II 还要求文件带有exodus_version、api_version等全局属性,有些求解器会检查这些属性。转换时,你要把 gmsh 的物理组翻译成 exodus II 的编组,并把节点和单元数据按 exodus II 的索引规则重新整理。
2.3 转换的映射关系与数据模型差异
两种格式最本质的差异在于索引方式。gmsh 的节点编号允许不连续,比如你可以只有 1、3、5 号节点,没有 2 和 4;而 exodus II 的逻辑是连续编号,它就是按数组下标来寻址的。所以转换时,必须先对所有节点重新编号:原编号 → 新编号,同时更新每个单元的连接列表。单元类型映射也不一样,gmsh 用数字表示单元类型,比如 2 代表三角形,3 代表四边形,4 代表四面体,5 代表六面体;exodus II 则用字符串和特定编码,比如三角形是TRI,四面体是TETRA。转换时,先要建立一张 gmsh 单元类型号到 exodus 单元标识的映射表。
另一个差异是维度。exodus II 里节点坐标必须是三维,即便你的模型是二维平面,也要把 z 坐标写成 0。Peridigm 内部按三维计算,二维问题只是 z 方向不加离散。而 gmsh 的二维网格节点通常只有 x、y 坐标,读取时必须把 z 补成 0。此外,exodus II 中还可以存储节点集(node set)和面集(side set),用于定义边界条件。gmsh 的物理组默认按单元划分,但如果你用Physical Point定义了关键节点,转换时需要单独提取这些节点并生成节点集。这些映射关系搞清楚了,转换脚本才不会变成黑匣子。
3. 用Python写转换脚本:从解析MSH到写出exodus II
3.1 解析MSH文件:从ASCII版到二进制版
我拿到的资源里,核心是一个 Python 模块,它既能读 ASCII 的 .msh,也能读二进制格式。ASCII 解析其实不难,按关键字块切分:
import re def parse_msh_ascii(filepath): nodes = {} elems = [] phys = {} current_block = None with open(filepath, 'r') as f: for line in f: if line.startswith('$'): current_block = line.strip().replace('$', '') continue if current_block == 'Nodes': parts = line.split() if len(parts) == 4: nodes[int(parts[0])] = (float(parts[1]), float(parts[2]), float(parts[3])) elif current_block == 'Elements': parts = line.split() if len(parts) >= 4: elems.append([int(p) for p in parts]) elif current_block == 'PhysicalNames': # 例如: 1 2 "left" parts = line.split('"') if len(parts) == 3: phys[parts[0].strip().split()[1]] = parts[1] return nodes, elems, phys这段代码按$Nodes、$Elements、$PhysicalNames这几个块做切分。注意PhysicalNames块里每行格式是物理维度 物理编号 "物理名",比如2 3 "boundary"表示类型为 2、编号为 3、名称为 boundary。我用split('"')把名称和数字分开,这样物理编号和名称都能拿到。这里phys字典的键是物理编号,值是对应的名称。实际使用中发现,PhysicalNames块不是必须存在的,如果 gmsh 没有定义物理名称,脚本就自动使用phys_{编号}作为名称。
二进制格式的 .msh 则要读取头部标记$Binary,然后用 struct 模块按 4 字节整数和双精度浮点数解析。常见坑是二进制在节点数据之前有两个额外的整数:一个表示字节序(1 或 -1),另一个是sizeof(double),解析时要先跳过这两个数。二进制版本里,节点和单元数据都以平台原生字节序存储,解析前要先判断本机字节序和文件里的标记是否一致,不一致时需要用struct.unpack的>或<前缀来强制指定字节序。我不建议在脚本里写死字节序,最好让用户通过--endian参数选择。
3.2 使用netCDF4或h5py构造exodus II文件
exodus II 是 NetCDF 的一个约定,因此可以用netCDF4库直接构造。如果你机器上没有装,也可以用h5py手动写出相同结构的 HDF5 文件,但需要注意属性命名。更省事的是用现成的exodusPython 包,它封装了底层的 C 接口。资源里的脚本同时支持两种后端:
try: import exodus BACKEND = 'exodus' except ImportError: import netCDF4 BACKEND = 'netcdf' if BACKEND == 'exodus': exo = exodus.exodus('mesh.exo', 'w', title='converted mesh') exo.put_nodes(n_nodes, x, y, z) exo.put_connectivity(elem_block_id, connect) else: ncout = netCDF4.Dataset('mesh.exo', 'w', format='NETCDF4') ncout.createDimension('num_nodes', n_nodes) ncout.createDimension('num_elems', n_elems) ncout.createVariable('x', 'f8', ('num_nodes',)) ncout.createVariable('y', 'f8', ('num_nodes',)) ncout.createVariable('z', 'f8', ('num_nodes',)) ncout.variables['x'][:] = x # ...exodus后端的构造参数很直接:文件名、写入模式、标题。它内部会自动建立 NetCDF 维度和变量,比手工用 netCDF4 写要更不容易漏字段。但我一般建议先用 netCDF4 试试,因为独立包依赖更少,而且你能看到文件里到底有哪些变量,方便调试。两种后端写的文件都是 NetCDF3/4 格式,Peridigm 都能读。需要留意的是,exodus包要求节点坐标数组是float64,单元连接数组是int32,如果传入的数组类型不对,它会直接报错或者静默截断,所以转换前最好用numpy.astype做一次类型转换。
3.3 共享节点与单元编组的处理
gmsh 里的物理组是按物理实体编号分组的,每个物理实体包含若干单元。exodus II 里的编组则是一个列表,里面存放单元编号。转换时,你先从 gmsh 单元列表中取出属于某个物理编号的单元,然后把这些单元在转换后的新编号填入编组。这里有个细节:gmsh 单元列表里的"标签"是单元在文件中的序号,而 exodus II 的单元编号是 1 到 num_elems 的连续整数。如果你只做简单映射,就把 gmsh 的原始单元序号直接当作 exodus 单元号,但要保证 gmsh 写出的单元没有重复、没有空洞。如果 gmsh 用了多个物理实体,同一个单元可能同时被两个物理实体引用,exodus II 的编组可以重复包含同一单元,这没问题。节点共享的概念在 exodus II 里是天然成立的:所有单元连接都指向同一个节点坐标数组,不会出现重复节点。所以转换时不需要额外合并节点,只要保证节点编号按 1..n 连续即可。
物理组处理还有一层:有的物理组定义的是面单元或边单元,而不是体单元。比如你要在模型表面施加力,就需要从体单元表面提取面单元,然后在 exodus II 中创建边集。脚本里我加入了一个--extract-side-sets开关,打开后它会遍历体单元,找出属于指定物理组的那些面,并把面单元的节点连接写成 exodus 的side set。这个过程要小心,同一个面可能被两个体单元共享,去重时要按节点排序后的序列来判断。否则,施加边界条件时会出现重复施加导致求解出错的问题。
3.4 验证转换结果:用可视化工具打开检查
转换不是写完就算完。Peridigm 读不了不代表文件错了,但用可视化工具打开能直接看到网格形状。资源里附带了一个verify.py,它会用 exodus 库把节点坐标和单元连接读回来,再检查有没有负体积、有没有孤立节点。最常见的第一步验证是看单元数量对不对,然后在可视化工具里看网格有没有扭曲。你也可以把 exodus 文件转成 VTK 后检查,但更直接的是打开 exodus 文件本身。ParaView 对 exodus 原生支持,打开文件后左侧选中文件,时间步、编组都会列出来。如果单元变形严重,说明节点编号映射出现了错位,这时候要回到 3.1 节的解析逻辑检查。
验证脚本还有一个功能:打印出每个编组的单元数、节点数,以及每个单元的最小体积。如果最小体积出现负数,说明某个单元节点顺序反了,也就是我们常说的负体积。负体积在近场动力学计算中会直接导致应力计算错误,所以一定要在转换前扫掉。在 ParaView 里,你可以用Cell Quality滤镜快速看体积分布,数值为负的单元会以红色标出来。
4. 参数与运行细节:命令行选项、单元类型映射表、坐标缩放
4.1 命令行与JSON配置
这个转换脚本提供了一个命令行入口,我用起来比较顺手的调用方式是:
python convert_gmsh_exodus.py -i mesh.msh -o mesh.exo --scale 1.0 --physical-names boundaries.json-i和-o是输入输出文件路径。--scale用来做单位换算,比如 gmsh 里用的是 mm,Peridigm 需要 m,就传 0.001。--physical-names指向一个 JSON 文件,里面定义物理组名称映射,例如:
{ "1": "bulk", "2": "surface_bottom" }脚本在写 exodus 编组时使用这些名称,而不是默认的gmsh_phys_1。如果你不在命令行里给,它会用物理编号作为编组名,后续你在 Peridigm 里设置边界条件时,就得靠编号去猜,很容易出错。JSON 文件的键是物理编号的字符串形式,值是编组名。我用过几次后建议,编组名里不要带空格和中文,Peridigm 的控制卡解析对特殊字符不友好,最好用下划线或驼峰命名。
命令行的--extrude参数也很有用。gmsh 里如果生成了二维平面网格,需要沿 z 方向拉伸成三维体网格才能喂给 Peridigm。这个参数接受一个厚度值和层数,例如--extrude 0.01 5,脚本会自动生成六面体单元并补上对应的节点。当然,如果你用 gmsh 已经生成了三维网格,就不需要这个参数。这个功能我用在某一批面网格测试用例上,省去了在 gmsh 里重新建模的时间。
4.2 单元类型映射表(gmsh类型号→exodus单元标识)
gmsh 的单元类型号在官方文档里有定义,但转换脚本里维护了一张精简映射表。
| gmsh 类型号 | 单元形状 | exodus 标识 |
|---|---|---|
| 1 | 直线段 | BAR |
| 2 | 三角形 | TRI |
| 3 | 四边形 | SHELL |
| 4 | 四面体 | TETRA |
| 5 | 六面体 | HEX |
| 6 | 三棱柱 | WEDGE |
| 7 | 四棱锥 | PYRAMID |
这里最大的坑是二阶单元。gmsh 里二阶三角形的类型号是 9,二阶四面体是 11,它们多出了边中点和面中心的节点。exodus II 对二/三阶单元有一套自己的标识符,例如三角形二阶叫 TRI6(6 节点三角形),四面体二阶叫 TETRA10。如果你的网格里有二阶单元,脚本会打印显式警告,除非你加上--allow-high-order,否则默认拒绝转换。原因很简单:Peridigm 主要吃一阶单元,高阶单元在近场动力学里会引起数值异常,我见过好几回,计算出来的位移场看起来像马赛克。如果你确实需要保留高阶单元,脚本会尽可能映射,但你在 Peridigm 中可能会碰到意想不到的问题,所以还是建议转成低阶。
单元映射还有一个隐含问题:gmsh 的节点顺序和 exodus II 不一定一致。比如四面体单元,gmsh 的节点顺序是四个顶点,而 exodus II 也要求按右手定则排列。如果顺序不对,单元体积就会变成负值。脚本里我加了--fix-orientation,它会检查每个单元的体积,如果为负,就自动交换两个顶点。但自动修复并不是万能的,如果单元本身拓扑有缺陷,交换顶点也救不回来。更加稳妥的做法是在 gmsh 导出前就用Mesh.ReverseAll命令统一方向。
4.3 坐标缩放与节点编号重排
坐标缩放直接应用于节点坐标数组。注意缩放因子是乘到三个坐标上,如果你只传 0.001,意味着把所有长度压缩千倍。节点编号重排是另一回事:gmsh 的节点编号可能有空洞,脚本会把所有节点从 1 开始重新编号,并同步更新单元连接。这一步用字典实现,原编号到新编号映射,一遍遍历完成。我建议你在转换前先运行一次--dry-run,它只输出统计信息,不写文件,用来检查节点数量、单元数量、物理组数量是否合理。我习惯每改一次网格就 dry-run 一次,确认没有异常的数字再真正转换。
节点重排的具体逻辑是:先遍历所有单元连接,收集所有出现过的节点编号,排序后建立{old_id: new_id}映射,然后重写连接数组。之所以要先排序再映射,是为了让原始编号的顺序尽可能保持稳定,方便后续追踪。如果你只按出现顺序映射,不排序,可能导致新旧编码之间的对应关系随机化,排错时很难查。重排后,还需要把所有孤立节点删掉,也就是那些不在任何单元连接里出现的节点。脚本里有--remove-isolated-nodes,默认开启。孤立节点如果不删,Peridigm 读取时会报错说节点数不一致。
4.4 与Peridigm对接:读取时的常见设置
Peridigm 读取 exodus II 文件时,通常还需要一个控制卡文件,指明哪个编组是材料区,哪个是边界条件区。例如,在 Peridigm 的输入卡里写block对应存储单元编组。所以转换时,编组名称尽量与 Peridigm 控制卡中用的名字保持完全一致,比如material、fixed_bc。我在一个模拟项目 X 中,把 gmsh 里的物理名命名为material和surface_bc对应,转换后 Peridigm 读取特别顺畅。如果你在 Peridigm 里看到的报错是Error: no elements in block,基本可以判定编组名没对上。
另外,Peridigm 要求 exodus 文件的节点坐标单位是米,如果你的模型尺寸在微米级别,直接转换可能导致数值溢出。我一般会先用--scale 1e-6转成米,再在 Peridigm 里换算材料参数。有时候你还需要指定密度、初始损伤等状态变量,这些不在转换脚本范围内,需要在 Peridigm 的输入卡里另行定义。转换脚本只负责几何和拓扑,不负责物理属性。
5. 避坑指南:转换失败的5个常见原因与排查方法
5.1 单元阶次不一致导致求解器报错
现象:Peridigm 启动后提示单元类型未知,或者计算第一步就发散。
原因:gmsh 网格里混着二阶单元,脚本默认把它们留在文件里,Peridigm 只支持一阶单元。
解决:转换前用--list-elem-types查看单元类型分布,把所有二阶单元用 gmsh 的Mesh.RemoveDuplicateNodes和Mesh.SetOrder 1降阶后重新转换。另外留意转换日志里的警告,看到high-order字样就停下。如果你用的 gmsh 版本较新,默认生成的是二阶网格,别忘了在 gmsh 里把阶数改回一阶。
5.2 节点编号不连续导致孤立节点
现象:可视化工具里看到网格分裂成几块,明明几何是连在一起的。
原因:gmsh 在删除节点后,编号出现了空洞。脚本虽然做了重编号,但单元连接里的原始编号没有同步更新。
解决:先确认脚本重编号逻辑没有 bug。我的经验是按原节点编号构建映射字典,然后逐条重写单元连接,最后再跑一遍verify.py,它会输出孤立节点数。如果孤立节点数不为 0,说明原网格本身存在不连接区域,需要回到 gmsh 中检查几何缝。有时候 gmsh 里的几何模型是由多条曲线拼成,但曲线之间有微小间隙,导致节点不重合。这种问题在 gmsh 里用Mesh.MergeNodes可以合并接近的节点,但阈值要设得合适,否则会把不同位置但很靠近的节点错误合并。
5.3 物理组与编组错位
现象:Peridigm 的边界条件施加到了错误区域,受力部位不对。
原因:gmsh 物理编号是文本解析进来的,如果PhysicalNames块被注释掉,脚本拿到的就是默认编号,和实际期望的物理区域对不上。
解决:在 gmsh 里显式定义Physical Surface并给定名称,转换前打开 .msh 确认$PhysicalNames块存在。我用脚本里的--physical-names参数强制加载 JSON 映射,并在 dry-run 输出中核对每个编组的单元数量。另外一个常见是粗心:你在 gmsh 里定义的物理编号是 20、21,但脚本 JSON 里写的是"20": "left",没问题。可如果 gmsh 里没有PhysicalNames块,物理编号会自动从 1 开始?不见得,gmsh 会按物理定义顺序分配内部编号,跟你预期不一定一致。所以我建议所有模型都按 gmsh 官方推荐,用Physical Surface(n)或Physical Volume(n)显式声明。
5.4 浮点精度与坐标缩放陷阱
现象:转换后网格尺寸明显不对,或者计算结果显示位置漂移。
原因:坐标缩放因子用错,比如把 mm 转 m 传了 0.01,或者读取坐标时用了单精度。
解决:脚本默认用 float64 写坐标。如果你用 netCDF4 后端,注意变量类型要'f8',不要写成'f4'。另外在--scale参数后做一次旅程检查:读回 exodus 文件,计算所有节点坐标的最大最小值,确认和 gmsh 里的几何范围一致。如果你发现缩放后坐标数值特别小,比如 1e-9 量级,那可能是你单位换算错了。常见的是把微米当毫米,传了 0.001 而不是 1e-6。这里没有什么玄学,就是单位换算的算术题,但失误率极高,尤其是从别的仿真软件拿来的模型,单位制往往不一样。
5.5 二进制MSH解析的字节对齐问题
现象:Python 解析二进制 .msh 时报struct.error: unpack requires a buffer of x bytes。
原因:二进制块里可能包含额外的节点与单元数量、实体数量等辅助信息,不同 gmsh 版本的布局有差异。
解决:给解析函数加一个版本分支。gmsh 4.1 的二进制块在节点数据前有一个实体块头,要先读取实体块数量和每个实体的类型、数量。我一般先用 ASCII 导出,避免这个麻烦。如果必须用二进制,就在解析头部前两个整数(字节序标记和 double 大小),再根据版本跳转。还有一种情况是文件里包含$NodeData或$ElementData这些后处理数据块,它们也是二进制存储,解析器如果没跳过会直接把下一行当节点坐标。脚本里我加了一个--skip-data-blocks参数,专门用来忽略这些结果数据,只解析网格部分。
6. 进阶:把转换脚本嵌入自动化流程与数据验证
6.1 批量转换子命令
如果你的项目是参数化建模,每天要生成几十个网格,手动一个命令一个命令敲就太累了。脚本里加了一个--batch子命令,它读取一个 CSV 列表,每行依次是输入路径、输出路径、缩放因子。循环转换时,我会让脚本把每个文件的日志输出到单独文件,方便回头查找。CSV 文件的格式很简单:
mesh_1.msh,mesh_1.exo,0.001 mesh_2.msh,mesh_2.exo,1.0批量转换时我习惯先跑一次--dry-run覆盖所有条目,检查统计信息里有没有异常高或异常低的单元数。如果某个文件转换失败,日志里会明确写出哪个环节出了错,比如读取物理组失败或坐标数组长度不一致,不用一个个 debug。
6.2 用网格质量指标做转换前自检
转换前自检比转换后补救成本低得多。我在脚本里加了--check-quality,内置方差计算三个指标:单元最短边长、最大/最小体积比、孤立节点数。这些指标原本是在 gmsh 里看的,但脚本自己算一遍的好处是,你可以在转换流水线里自动拦截那些质量太差的网格,而不必打开 gmsh 一个一个看。最短边长如果小于 1e-6 米,说明这个位置存在重复节点或极其细长的单元,体积比超过 100 也说明网格畸形。自动拦截后,你可以生成一个报告文件,单独列出有问题的单元编号和位置坐标,再用 ParaView 定位去查看。
6.3 逆向检查:从exodus II读回关键集合并比对
最让我放心的验证方法是逆向比对。脚本提供一个--compare参数,它从生成的 exo 文件读回节点数和单元数,再和原始 .msh 里的统计值做对比,同时随机抽 5% 的单元,检查它们的连接关系在原网格中是否存在。有一次我发现 6 号单元连接没对上,追根到底是因为原网格中有一个单元的类型号被我映射错了,导致连接长度不对。从那以后,我每次转换都强制走一遍--dry-run加--compare的流程,确认没有警告才继续后续计算。希望帮到你。与之配套的,还有一个小技巧:在 Peridigm 里跑一个空算例,只加载网格不计算,看看加载阶段会不会报错。如果加载阶段能顺利通过,说明 exodus 文件结构基本没问题,后面就算数值发散,问题也更可能出在材料参数或边界条件而不是网格格式上。
本文还有配套的精品资源,点击获取