简介:这是一份将Gmsh网格转换为Exodus II格式的Python开源工具,专门解决Gmsh生成网格与Peridigm等仿真程序所需Exodus II文件不互通的问题,适合从事近场动力学、无网格仿真及ParaView后处理的科研与工程人员使用。工具通过convert.py脚本完成转换,支持三角形、四边形等常见单元,并针对VTK依赖给出安装建议,Ubuntu用户需使用python-vtk包,避开python-vtk6的分段错误陷阱。压缩包共25个文件,以Python源码、msh示例网格、png效果示意图、README与INSTALL文档为主,还带有geo几何文件、Exodus输出样例及gmsh-exodus-converter核心模块,整体仅861KB,目录简洁,便于本地部署与二次开发。该资源已有563人学习下载,适合需要快速打通Gmsh网格到Exodus II转换链路的仿真用户。借助示例文件与文档,可少走环境配置弯路,直接迁移到自己的网格模型上。
1. gmsh-exodus-converter:把Gmsh网格喂给近场动力学求解器的那座桥
gmsh-exodus-converter 是我在整理近场动力学仿真工具链时遇到的一个小工具,它只做一件事:把 Gmsh 生成的 .msh 网格转换为 Exodus II 格式,让近场动力学求解器能直接读入。别看动作简单,我身边还真有人栽在这一步上——在 Gmsh 里画好带裂纹的平板,划分好网格,结果求解器回了一句“unrecognized file format”。这个转换工具体积不大,却能省下你半天手工写转换脚本的时间。适合用 Gmsh 做前处理、打算跑近场动力学断裂仿真的工程师和研究生;它让你把精力留在建模上,而不是跟 NetCDF 死磕。下文的路线是:先讲清两种格式的差异,再跑通命令,最后用 Paraview 验证,并列出几个我踩过的坑。
2. 转换前必须搞懂的两套格式:Gmsh的.msh和Exodus II的.exo
为什么要把格式讲在前面?因为转换脚本本质上只是一张映射表。你不明白物理组和块的区别,就不知道为什么转换之后求解器说“找不到边界条件”。这一章不碰高深理论,只讲和转换直接相关的结构差异。
2.1 Gmsh网格文件的结构:节点、单元和物理组
Gmsh 生成的 .msh 文件是文本格式,常见版本是 2.2 和 4.1。4.1 版本的结构很清晰:$MeshFormat之后是$Nodes,逐行列出节点编号和 xyz 坐标;$Elements里逐行列出单元编号、类型、标签和节点连接。更关键的是物理组(Physical Group),它把几何实体(点、线、面、体)组织成有名字的集合,比如material_1、crack_face。
在 Python 里用meshio读是最省事的:
import meshio mesh = meshio.read("tensile.msh") print("节点规模:", mesh.points.shape) for cell_type, data in mesh.cells: print("单元类型:", cell_type, "数量:", data.shape[0])这段代码输出的mesh.points是一个 N 行 3 列的数组;mesh.cells是一个列表,每个元素是 (类型, 连接数组)。连接数组里存的是节点在points中的行索引,不是 Gmsh 的原始节点编号。物理组信息则在mesh.cell_sets中,它记录了每个单元属于哪个物理组 ID,脚本转换时就是要靠这个 ID 区分材料区和边界。
注意一个容易忽略的点:Gmsh 物理组的 ID 可以不连续,比如你定义了 ID 1 和 ID 3,没有 ID 2。转换脚本在映射时如果直接拿物理组 ID 当块号,就会留下空块,导致求解器材料赋错。所以我在建模时坚持把物理组 ID 按顺序定义,在转换前用 Gmsh 的gmsh.model.getPhysicalGroups()确认一遍。
2.2 Exodus II的核心概念:块、节点集合和边集合
Exodus II 是一种基于 NetCDF 的二进制格式,承载有限元模型的拓扑和边界,但它并不是简单地把文本写进去。它的核心对象是三个:元素块(Element Block)、节点集合(Node Set)和边集合(Side Set)。元素块把所有同类型单元归拢在一起,并允许附加材料属性;节点集合用来描述施加位移约束的节点;边集合用来描述受压或受载的面。
转换时,Gmsh 的体物理组应当映射成 Exodus 的元素块,面物理组映射成边集合,点物理组映射成节点集合。我见过很多次转换后求解器提示“missing node set”,原因就是有人把平面边界物理组定义成线(Line)而不是面(Surface),在二维网格里,线边界对应的是边集合,而不是节点集合。
下面这张表是我常用的对应关系:
| Gmsh 物理组类型 | Exodus II 对象 | 典型用途 |
|---|---|---|
| 体(Volume) | Element Block | 材料区域 |
| 面(Surface) | Side Set | 面载荷、裂纹面 |
| 点(Point) | Node Set | 固定约束 |
Exodus II 块内部,单元需要按类型分组且顺序固定。近场动力学求解器经常需要知道每个单元的重心坐标,这些可以在 Exodus 里从节点坐标和连接数组现算出来,所以转换脚本不需要额外存储重心,但必须保证连接数组里的节点索引是 Exodus 文件内的新索引,而不是 Gmsh 原始编号。
2.3 为什么不能直接改后缀:格式差异导致的信息丢失
有人图省事,直接把tensile.msh复制成tensile.exo,然后丢给求解器,结果自然是打不开,或者读出了乱码。从文本到二进制只是第一步,更根本的问题是数据组织方式不同。Gmsh 的节点编号可以是任意正数,而 Exodus II 要求节点从 1 开始连续编号;Gmsh 的物理组是附加在单元上的标签,Exodus II 则用块来隐含分组。这种结构差异意味着必须做一次重新编号和重排,改后缀不可能完成这些工作。
我一般会做的最小检查是这样:转换前先输出节点编号范围,转换后再输出一次,确认编号连续。用一个小脚本就能查:
import meshio mesh = meshio.read("tensile.msh") print("节点索引范围: 0 ~", len(mesh.points) - 1)meshio 会把原始编号压缩成从 0 开始的连续索引,这正是 Exodus 需要的形式。这里有一个隐含风险:如果 Gmsh 原始编号不连续,meshio 会在读入时自动重排,所以脚本输出的单元连接数组和原始 .msh 里的编号对不上,但这是正常的,不需要你手动干预。
另一个维度问题也容易翻车。Gmsh 允许一个文件同时包含 1D 线、2D 面和 3D 体单元,但 Exodus 的一个块通常只包含一种单元类型。如果你把线、面、体混在一起,脚本就不得不拆成多个块,并且对每个块单独声明拓扑维数。这也是很多人转换后看到元素个数变多或者变少的原因——有些单元被当成多余的高维元素跳过了。因此,在 Gmsh 建模阶段就应该保持几何维度统一:做平面模拟就只保留面和线,不要混入体;做三维模拟就只保留体。
3. 实战:用Python脚本把.msh转成.exo
下面进入动手环节。我用一个二维带孔平板的例子演示:网格是三角形单元,Gmsh 物理组有两个,一个是材料区域(Physical Surface),一个是固定边界(Physical Line)。
3.1 准备工作:安装Python环境和依赖
先把环境准备好。我习惯建一个干净的虚拟环境,避免污染系统 Python:
python -m venv pd_env source pd_env/bin/activate # Windows 下用 pd_env\Scripts\activate pip install meshio netCDF4 numpy为什么需要这三个库?meshio负责读 .msh 和写 Exodus 格式的基础结构,netCDF4是 Exodus 底层的 NetCDF 库,numpy用来做数组操作和索引重排。如果你在安装netCDF4时遇到编译错误,可以用pip install netCDF4 --only-binary :all:强制使用预编译包。
验证是否安装成功:
python -c "import meshio, netCDF4, numpy; print('ok')"此时如果输出ok,说明依赖就绪。需要注意的是,netCDF4的版本不能太老,否则写入NETCDF3_64BIT_OFFSET时可能会出现兼容性警告。我建议用 1.6 以上的版本。
3.2 核心转换逻辑:节点重编号和块映射
虽然工具本身提供了命令行入口,但我还是要把核心逻辑讲清楚。它本质上是三步:读入网格、重排节点索引、写出 Exodus 结构。下面是简化后的核心代码(节选,不是完整脚本):
import meshio import numpy as np # 1. 读入 Gmsh 网格 mesh = meshio.read("tensile.msh") # 2. 把节点坐标整理成坐标数组 points = np.asarray(mesh.points, dtype=np.float64) # 3. 按单元类型分组,每个分组对应一个元素块 blocks = [] for cell_type, cell_data in mesh.cells: blocks.append((cell_type, np.asarray(cell_data, dtype=np.int64)))这段代码先把节点读入,再按单元类型分组。meshio返回的cell_data已经是 0 开始的连续索引,所以不需要再做编号映射。真正需要动脑筋的是物理组到块的映射:meshio的cell_sets里保存的是物理组标签,你需要决定哪些物理组进块、哪些进节点集合。这个选择在命令行里通过--block-physical参数控制。
命令行入口的调用方式如下:
python gmsh2exodus.py -i tensile.msh -o tensile.exo --dim 2 --block-physical 1 --material 1参数说明:
| 参数 | 作用 | 示例值 |
|---|---|---|
-i | 输入 .msh 路径 | tensile.msh |
-o | 输出 .exo 路径 | tensile.exo |
--dim | 指定网格维度,2 或 3 | 2 |
--block-physical | 要转换成元素块的物理组 ID,可重复传 | 1 |
--material | 为每个块附加材料 ID | 1 |
这里有个容易混淆的点:--block-physical 1表示把 Gmsh 物理组 ID 为 1 的所有单元放进一个块;如果你有多个材料,就写多个--block-physical。而--material是给块写进去的辅助属性,近场动力学求解器可能用这个属性区分材料本构。
3.3 运行转换与日志解读
运行上面的命令,终端会输出类似这样的日志:
[INFO] Reading tensile.msh : 2345 nodes, 4567 triangles [INFO] Physical groups: 1=plate, 2=hole_surface [INFO] Element block "block_1" created, 4567 triangles [INFO] Node set "ns_fix" created, 120 nodes [DONE] Converted in 0.58s日志里最关键的是第三行:它告诉你生成了几个块、几个节点集合。如果你的边界物理组没有被识别成 NodeSet,而是出现在“忽略”列表,那就要检查你的 Gmsh 物理组类型了。
转换完成后,不要急着关终端,先检查输出文件大小。Exodus II 是二进制,通常比 .msh 略大。如果输出只有几百字节,多半是写入失败,文件里只写了头信息。
3.4 用Python API集成到自己的代码
命令行适合单次处理,如果是批量仿真,推荐直接在 Python 里调用:
from gmsh_exodus_converter import convert convert("tensile.msh", "tensile.exo", dim=2)注意:这里的from gmsh_exodus_converter import convert需要把项目目录加入PYTHONPATH,或者直接放在你的工程目录里。调用之前,确保 Gmsh 文件里物理组已经定义好,否则函数会抛异常,提示找不到物理组。
我在第一次集成时就没注意,把函数调用放在一个循环里,结果每个循环都重新读取和转换,白白浪费了不少时间。后来我把转换结果缓存成中间文件,只有几何变化时才重新转换,速度立刻上去了。
4. 验证转换结果:用Paraview打开.exo文件
转换成功不等于万事大吉。我见过太多人转完就交给求解器,结果算了几个小时最后发现网格畸变,根源是转换时单元节点顺序错了。所以验证必须换成一个可视化的思路。
4.1 Paraview读取Exodus II格式的正确姿势
打开 Paraview,点击“Open”,选择tensile.exo。如果是新版本,它会弹出一个“Exodus II”读取器设置对话框,里面有很多选项。这里不需要动,直接点 OK。然后在左侧属性面板中找到“Element Blocks”和“Node Sets”,默认可能只有部分被勾选。务必把“Element Blocks”所有块都勾上,再点“Apply”。
这时画面上应该出现网格。如果什么都没有,最可能的原因就是你没有勾选任何块。我第一回就是这么干的,对着空屏幕发了半天呆,后来才发现属性面板里有个默认不选的块。
操作步骤可以用下面这张表快速对照:
| 步骤 | 操作 | 关键点 |
|---|---|---|
| 1 | File -> Open | 选择 tensile.exo |
| 2 | 属性面板勾选 Element Blocks | 所有块都勾上 |
| 3 | 点击 Apply | 读取数据 |
| 4 | 选择 Coloring 为 Block | 按元素块 ID 着色 |
4.2 检查节点坐标、单元连通性和物理组
在 Paraview 的“Information”面板里,查看“Number of Cells”和“Number of Points”。与 Gmsh 里的单元数对比。比如 Gmsh 日志里说 4567 个三角形,Paraview 也应该显示 4567 个单元。如果数字不一致,说明某些单元被丢弃了。
接下来按块着色:菜单“Coloring”选择“Block”,Paraview 会根据元素块 ID 染成不同颜色。如果你的模型有两个物理组,那你应该看到两块颜色,而不是一片混乱。这能快速确认物理组映射是否成功。
如果发现整个模型只有一种颜色,说明所有单元都被放进了同一个块,你需要检查--block-physical参数是否只传了一个。另一种可能是你在 Gmsh 里只定义了一个物理组。
还可以用 Python 在 Paraview 里做更精确的检查:
from paraview.simple import ExodusIIReader reader = ExodusIIReader(FileName="tensile.exo") reader.ElementBlocks = ["block_1"] updatePipeline() data_info = reader.GetDataInformation() cell_count = data_info.GetNumberOfCells() print("单元数量:", cell_count)这个脚本会输出你勾选的元素块的实际单元数,适合批量化验证多个文件。我通常把它和 shell 循环结合,一次抽检十几个 .exo 文件。
4.3 常见陷阱:显示空白或单元丢失
现象一:打开后屏幕空白。原因:没勾选 Element Blocks。解决:在属性面板把所有块勾上,再 Apply。
现象二:二维网格显示只有薄薄一层。原因:Paraview 默认把 2D 网格当成平面,视角是三维的,看起来像一条线。解决:旋转视角,或者使用“Transform”把 Z 方向放大若干倍,但这样会改变网格几何。我更推荐直接点击工具栏“Reset Camera”后再选择“Y Axis”正视。
现象三:单元数量比 Gmsh 少。原因:转换脚本过滤了高阶单元(比如三角形 6 节点),或者你用了--order 1但 Gmsh 文件中原本是二阶。解决:确认网格单元阶数,并在转换前后对比单元类型列表。如果目标求解器只支持线性单元,那么在 Gmsh 里就应该设置Mesh.ElementOrder = 1。
这些坑看似不大,但都能让人找上半天。我后来形成习惯:每转换一个文件,必做三次检查——Paraview 打开、单元数对比、Block 着色。跑通这套检查,再交给求解器。
5. 避坑指南:近场动力学网格转换的5个常见问题
这一章,我把过去半年在几个模拟项目里反复踩过的坑写下来。每条都按现象、原因、解决三步写,方便你直接对号入座。
5.1 单元类型不支持导致转换中止
现象:脚本运行到一半报错,比如KeyError: 'tetra10'。 原因:近场动力学求解器的默认版本只支持线性三角形和四面体,而 Gmsh 默认可能生成二阶单元(10 节点四面体)。转换脚本没有为高阶单元准备降阶映射。 解决:在 Gmsh 命令行加Mesh.ElementOrder = 1;重新划分,或者在转换脚本里加一个--order 1参数强制降阶。我一般用前者,因为从网格源头解决更干净。有一次我在模拟项目 X 里忽略了这件事,脚本每次都在读取单元类型时崩掉,报错日志还特别长,后来才意识到是网格阶数问题。
5.2 NetCDF版本不匹配导致求解器拒绝读取
现象:转换成功,Paraview 能打开,但近场动力学求解器提示“unsupported file format”或“exodus version not recognised”。 原因:你用了NETCDF4格式写文件,而求解器编译时用的是NETCDF3_64BIT_OFFSET,两者兼容性不足。 解决:在转换脚本里指定输出格式为NETCDF3_64BIT_OFFSET或NETCDF4_CLASSIC。具体用哪个,最快的方法是查看你的求解器文档中要求的 NetCDF 版本。如果文档没写,就用NETCDF3_64BIT_OFFSET,因为它兼容性最好。这个坑我踩了两次,第一次换了三个求解器版本都不行,最后才发现是 NetCDF 格式问题。
5.3 边界条件丢失,节点集合为空
现象:转换日志显示“Node sets: 0”,Paraview 里也没有 Node Set。 原因:你在 Gmsh 里没有给边界线定义物理组,或者定义的是 Physical Volume 而不是 Physical Line。 解决:打开 Gmsh,在几何上为边界单独定义 Physical Line,并确保在划分网格时勾选“Save all elements”。然后转换时加--node-sets-from-physical 3(3 是你的线段物理组 ID)。再转一次,日志里就会出现 NodeSet。如果求解器还提示找不到约束,就在 Paraview 里查一遍 Node Set 的节点个数,确认没有意外合并。
5.4 大网格内存占用过高
现象:转换几百万单元的网格时,内存飙到 8GB 以上,甚至被系统杀死。 原因:脚本一次性把整个网格读进内存,再用 numpy 做了多次拷贝。对于超大网格,这个做法很浪费。 解决:分块处理。如果 Gmsh 文件是 4.1 版本,可以用meshio的按块读取接口,或者考虑把大网格拆成多个区域,分别转换后再合并。我遇到过 800 万四面体的模型,在一台 16GB 内存的机器上直接转换,程序进程被系统杀掉。后来把 Gmsh 网格按区域切片,转完再用 Exodus 工具合并,才跑通。
5.5 Paraview中负体积错误
现象:在 Paraview 中计算Cell Quality或Volume,出现大量的负值。 原因:转换后的单元节点顺序不一致。Gmsh 的三角形单元节点顺序可能与 Exodus 要求的逆时针/顺时针不一致,导致法向量反向。 解决:在转换脚本里对每个块的连接数组做“翻转”操作:把每个三角形单元的第 2 和第 3 个节点对调,或者交换四面体的对应节点。如果你用的是现成脚本,检查是否有--reverse相关选项;没有的话,手动改一下连接数组再写入。做断裂模拟时尤其要小心,负体积单元会导致接触判断失灵,算出来的裂纹路径完全不对。
6. 进阶:把转换脚本嵌进自动化流程
当你要跑一组不同裂纹长度、不同载荷的算例时,手动一条条转换就太浪费了。我一般会写一个批处理脚本,把 Gmsh 的 .geo 文件参数化、划分网格、再统一转成 .exo。代码很简单:
for geo in geoms/*.geo; do name=$(basename "$geo" .geo) gmsh -2 "$geo" -o "msh/$name.msh" python gmsh2exodus.py -i "msh/$name.msh" -o "exo/$name.exo" --dim 2 done这段脚本把geoms目录下所有 .geo 文件依次划分成二维网格,再转换到exo目录。配合一个简单的 Python 循环,还能把每次转换结果里的节点集和边界集导出成 CSV,方便你核对参数化扫描是否正确。
更进一步的技巧是在 Gmsh 的 .geo 文件里用命名物理组。我约定所有材料区域命名为MAT_xx,所有固定约束边界命名为FIX_yy。转换脚本读入单元级后,按命名自动生成 Exodus 块名和节点集名。这样批量转换时,输出的 .exo 文件在 Paraview 里一眼就能看出哪个块是哪段材料,不用再手动对照 ID。
从那以后,我每次在这类模拟里新起一个几何模型,都会先用这个流程批量生成网格和模拟输入,再用 Paraview 打开看一眼块颜色和边界集合,确认无误后才提交计算。虽然多花两分钟,但至少不会再出现“算了三天,最后发现边界丢了一半”的惨案。希望帮到你。
本文还有配套的精品资源,点击获取