最开始接触Gmsh的API,是因为一个网格要切几十组参数:板长、孔径、倒角半径各变一遍,GUI里点十几次,晚上基本不用下班了。后来把几何建模、网格参数全部搬进Python脚本,一组参数一跑,输出文件命名好,几十分钟的重复劳动变成几十秒。这篇内容从动机讲起,带着你走完一条直接从Gmsh Python API入门到能自己写网格生成脚本的完整路径,中间会穿插我实际踩过的坑和后来才想明白的原理。适合正在学有限元分析、需要做网格剖分、或者做仿真研究期间需要批量生成算例的人。文章会覆盖环境安装、核心对象逻辑、完整建模案例、网格加密手段、与求解器的衔接,以及一份高频问题排查清单。
1. 为什么要用脚本控制网格生成?先想清楚动机
很多人第一次打开Gmsh,第一反应是“这不就是个画网格的软件嘛”,在界面里点几个按钮就能生成。可是真到了要批量出图、参数扫描、或者把前处理流程嵌入仿真流水线的时候,GUI操作就完全撑不住了。
1.1 GUI能满足大部分需求,但脚本才是真正的生产力
Gmsh的图形界面能做不少事:交互式建模、查看网格、手动定义边界、导出文件。如果你只做一两个模型,GUI完全够用。可一旦模型变多,GUI的问题就暴露出来了:每跑一组新参数都要重新点一遍菜单,操作步骤无法被记录,别人也拿不到你“做了什么”的完整过程。脚本化之后,几何尺寸、孔位、加密参数全部变成变量,循环里改数字就行。而且脚本本身就是文档,几个月以后再回头看看代码,每一步逻辑都清清楚楚,比翻截图和笔记好使太多。
这个API不是有些人以为的“给脚本留个后门”,它是Gmsh的主力使用方式。官方示例、自动化测试、参数化建模场景几乎全部跑在脚本上。界面里的每个按钮,背后都对应一个API调用。学会API就等于拿到了Gmsh的完整操作权限,界面能做的它能做,界面做不到的批量、循环、条件分支它也能做。
1.2 学习Gmsh API之前你需要具备的基础
这里帮你把基础条件理一理,避免一上来就被某个细节卡住。
- 懂一点点Python:能写变量、列表、循环、函数就够了。Gmsh的Python API本质上是一层绑定,底层是C++实现,你不需要了解C++,但至少要能读懂返回值里嵌套的元组和列表。
- 懂一点点网格概念:明白节点、单元、维数、边界、物理组这几个词大致是什么意思。不用精通,后面用到的时候我会再解释。
- 不需要会CAD软件:Gmsh自带的几何建模能力足够应付大多数前处理场景。复杂外部模型可以先通过STEP格式导入,再用occ内核做处理。
- 环境建议:Python 3.8以上,Gmsh 4.11以上的版本。如果你用5.x,API整体变化不大,但个别函数签名有调整,遇到报错先查一下当前版本的文档。
坐标、尺寸、单位这些概念需要多想一步:Gmsh不强制单位,你画的模型尺寸是5,它就当5处理,不会管你是毫米、米还是英寸。做仿真的时候必须自己保证几何尺寸和材料参数、求解器设置的单位一致,否则一个数量级错误就够你排查一整天的。
2. Gmsh Python API的启动方式与核心对象
学习任何API,第一件事不是背函数名,而是搞清楚它的组织方式。Gmsh的Python API把功能分成几个模块:gmsh.model负责几何和网格,gmsh.option负责全局选项,gmsh.view负责后处理视图,gmsh.write负责输出。其中你最常打交道的是gmsh.model下面的geo和occ这两个子模块。
2.1 安装和初始化,几步就能跑起来
安装很简单,用pip或者conda都可以:
pip install gmsh这个包会下载预编译的二进制,不需要自己编译,也不需要额外的依赖。装完之后,一个最小脚本长这样:
import gmsh gmsh.initialize() gmsh.model.add("demo") print(gmsh.model.getEntities()) gmsh.finalize()initialize()负责初始化底层对象,finalize()负责释放资源。脚本结束前一定要调用finalize(),尤其是需要反复运行脚本做调试的时候,不释放资源有时会留下残留状态,影响下一次运行。
gmsh.model.add("demo")是给当前模型起个名字。一个进程里其实可以创建多个模型,用gmsh.model.setCurrent()切换。初学者先只创建一个模型就够了,不要贪多。
2.2 两个建模内核:geo和occ,究竟怎么选
这是新手最懵的地方。Gmsh提供了两套几何构建接口,都在gmsh.model下面:
| 内核 | 全称 | 特点 | 适用场景 |
|---|---|---|---|
geo | Gmsh原生几何内核 | 轻量、直观、底层 | 点线面简单建模,结构化网格布线 |
occ | OpenCASCADE内核 | 支持布尔运算、倒角、拉伸旋转、STEP导入 | 复杂几何、CAD互操作、布尔差集 |
简单说,能用occ就用occ。因为真实工程模型几乎都要做孔、倒角、切槽这类布尔操作,geo内核做这些非常吃力。occ内核的建模思路更像真正的CAD软件:先画实体,再做布尔运算,最后掏空、分割。
需要强调一点,这两个内核都有addPoint、addLine这类基础函数,但它们是不同的实现,实体编号也是分开管理的。你不能在geo里建一个矩形,又跑到occ里做布尔操作。混用会导致实体tag混乱,后面排查起来相当痛苦。
2.3 必须理解的三件事:同步、物理组、编号规则
第一件是synchronize()。这个概念一开始让我踩过不少坑:在geo或occ里添加了点线面,但是网格操作找不到它们,报错说实体不存在。原因在于,几何内核里的实体和gmsh.model里的顶层实体不是自动同步的。你调用了addRectangle,它只是被记录在几何内核的临时数据库里,必须调用:
gmsh.model.occ.synchronize()这种同步操作才会把临时几何推送到顶层模型。之后getEntities、mesh相关函数才能看到这些实体。
第二件是物理组。物理组相当于给一批几何实体起一个有意义的名字,比如把某几条边界线统一命名为“固定端”,把这个面命名为“流体域”。后续写出的.msh文件里会包含这些分组信息,求解器才能在你指定的边界上施加边界条件。建立物理组用:
gmsh.model.addPhysicalGroup(2, [surface_tag], tag=101, name="plate")第一个参数是维度,2表示面,1表示曲线,3表示体。第二个参数是实体tag列表。这里的tag是你的名字,不是Gmsh自动分配的编号。
第三件是实体tag的获取方式。Gmsh里每个实体都有一个整数tag,但不是连续的,新建一个矩形可能返回tag 1,做个布尔之后返回的面可能是tag 2,也可能一下子跳到tag 7。所以千万不要在代码里写死tag数字,一定要用API的返回值去传递:
rect = gmsh.model.occ.addRectangle(0, 0, 0, 5, 3) # 后续用到矩形,用 rect 这个变量,而不是直接写 22.4 尺寸、坐标系和单位一次说清楚
Gmsh使用右手坐标系,点坐标统一用(x, y, z)三个浮点数表示,即使你在二维平面上建模,z坐标也要给0。这点和很多CAD软件的习惯一致。
关于单位,前面提过Gmsh本身不感知单位。网格尺寸参数,比如MeshSizeMin,给的是绝对数值。如果你画了一个10毫米宽的板,加密尺寸设成0.1,那意味着100个单元跨过这个宽度;如果同一个模型你当成10米来处理,0.1就代表网格远小于期望尺寸,单元数量会爆炸。做仿真前一定要先统一脑子里的单位:几何用毫米,材料参数就得对应毫米制,求解器设置也得匹配。Gmsh只能保证几何正确,不能保证物理单位正确,这个责任在建模者自己。
3. 一个完整案例:带圆孔的板网格生成
直接看代码比空讲概念有用得多。我选一个非常经典的结构:一个矩形板,中间掏掉一个圆孔。这个模型虽然简单,但包含了建模、布尔运算、局部加密、物理组、导出这些完整的环节,是很好的练手案例。
3.1 用occ内核快速建模,避开geo的布尔短板
先定义几个参数,方便后面改尺寸:
import gmsh gmsh.initialize() gmsh.model.add("plate_with_hole") # 几何参数 L = 10.0 # 板长 W = 5.0 # 板宽 R = 1.0 # 圆孔半径 # 建立矩形板 rect = gmsh.model.occ.addRectangle(0, 0, 0, L, W) # 建立圆盘,再用布尔减把圆孔从板里掏掉 disk = gmsh.model.occ.addDisk(L / 2, W / 2, 0, R, R) # 布尔差 cut = gmsh.model.occ.cut([(2, rect)], [(2, disk)])这里cut返回的outdimtags是一个列表,元素是(维度, tag)。我们要把面取出来:
gmsh.model.occ.synchronize() surfaces = [dimtag for dimtag in cut[0] if dimtag[0] == 2] surface = surfaces[0][1]为什么不直接取cut[0][0]?因为布尔操作可能返回多个实体,虽然本例只有一个面,但养成按维度筛选的习惯,以后处理复杂模型不会出错。
给这个带孔板定义一个物理组,方便后面做边界条件:
gmsh.model.addPhysicalGroup(2, [surface], tag=101, name="plate")3.2 设置网格尺寸并生成网格,关键参数怎么看
网格尺寸设置有几个层次。最简单的是全局最小/最大尺寸:
gmsh.option.setNumber("Mesh.MeshSizeMin", 0.05) gmsh.option.setNumber("Mesh.MeshSizeMax", 0.5)MeshSizeMin控制最小边长,MeshSizeMax控制最大边长。如果你希望整个模型都均匀加密,可以把这两个值设得非常接近;如果希望自动过渡,就让它们拉开差距。Gmsh会根据几何曲率自动在两者之间调整。
但全局尺寸还不够。以这个带孔板为例,应力会集中在孔边,孔附近的网格应该明显比远处的细。建一个基于坐标的尺寸场,用MathEval直接写一个函数来控制:
field = gmsh.model.mesh.field.add("MathEval", 1) gmsh.model.mesh.field.setString( field, "F", "0.05 + 0.15 * Sqrt((x-5)*(x-5) + (y-2.5)*(y-2.5))" ) gmsh.model.mesh.field.setAsBackgroundMesh(field)这个函数的意思很好懂:离圆心越近,尺寸越小;离圆心越远,尺寸逐渐增大。在圆心位置,尺寸是0.05;在远角位置,尺寸大概是0.05加0.15乘以距离,大约0.7。这样孔边自动加密,远处自动变粗,过渡也很自然。
背景网格字段的优先级高于MeshSizeMin/Max,但实际值仍会被全局Min/Max限制住。所以最好让字段值的范围落在Min/Max区间内,别让尺度差异过于夸张,否则网格过渡会很难看。
生成网格:
gmsh.model.mesh.generate(2)这里的2表示生成二维网格。如果你的模型是三维实体,应该写3。网格生成之后,可以用实体类型的统计函数看看到底生成多少单元:
print(gmsh.model.mesh.getStatistics())3.3 输出msh文件,并在GUI里检查结果
生成网格后,写文件就一行:
gmsh.write("plate_with_hole.msh")这里还要强调物理组的作用:如果没有建物理组,msh文件里只有节点和单元数据,但没有任何边界定义。求解器拿过去之后,没法区分“哪条边是固定端”,后续处理会非常麻烦。建了物理组之后,msh文件里会出现物理名称和对应的实体编号,大多数求解器都能直接读取。
想弹出一个图形窗口查看网格质量,加上:
gmsh.fltk.run()fltk.run()会阻塞进程,直到你关闭图形窗口。在服务器的无显示环境里不要调用这行,否则会报错。批量运行时,通常把这一行注释掉,只保留write()。
最后别忘了:
gmsh.finalize()完整脚本串起来,就是几十行Python代码。换一组尺寸参数,重新运行一遍,几秒钟就能得到一个新的网格文件,这就是脚本化的意义。
4. 网格质量与尺寸控制:从全局到局部的能力边界
会生成网格只是第一步,能不能控制网格质量才是真正拉开差距的地方。网格尺寸控制有多个层级,从全局到局部,从简单到复杂,实际工程中往往组合使用。
4.1 全局尺寸与局部点线面的优先级关系
Gmsh的尺寸控制可以作用在不同对象上:
gmsh.option.setNumber("Mesh.MeshSizeMin", ...):全局下限。gmsh.option.setNumber("Mesh.MeshSizeMax", ...):全局上限。gmsh.model.mesh.setSize([(维度, tag)], size):对指定实体局部设置尺寸。- 背景网格字段:基于空间位置的连续尺寸分布。
优先级上,实体级的setSize高于全局选项,背景网格字段又高于实体级。你在一个曲线上设了0.02,但全局Max是0.01,最终曲线尺寸会被0.01压下去。避免出现“我明明设了0.02却变成了0.01”这种看起来莫名其妙的状况,先检查全局配置。
一个更隐蔽的问题是:局部加密不应该让加密区域和非加密区域的尺度差异过于悬殊,否则从密到疏的过渡会非常生硬,网格单元的长宽比会变得很差。好的做法是让最大最小尺寸比值控制在10倍以内,除非你有非常充分的理由必须拉开间距,否则宁可把加密区域扩大一些,换一个更平滑的过渡。
4.2 局部加密:从setSize到背景网格字段
如果只需要给某几条边加密,setSize是最直接的方式:
gmsh.model.mesh.setSize([(1, curve_tag)], 0.05)这里的曲线tag就是你建模时得到的曲线编号。这条边会以0.05的目标尺寸生成网格,其他区域保持全局尺寸。这种方式简单明了,适合边界层、关键线、固定约束附近这类场景。
如果要让加密效果按空间距离连续变化,就需要背景网格字段了。除了前面用到的MathEval,常用的是Distance配合Threshold的组合:
# Distance 字段计算到指定曲线的距离 field1 = gmsh.model.mesh.field.add("Distance") gmsh.model.mesh.field.setNumbers(field1, "CurvesList", [hole_curve_tag]) # Threshold 字段把距离映射成网格尺寸 field2 = gmsh.model.mesh.field.add("Threshold") gmsh.model.mesh.field.setNumbers(field2, "InField", [field1]) gmsh.model.mesh.field.setNumber(field2, "SizeMin", 0.05) gmsh.model.mesh.field.setNumber(field2, "SizeMax", 0.5) gmsh.model.mesh.field.setNumber(field2, "DistMin", 0.2) gmsh.model.mesh.field.setNumber(field2, "DistMax", 1.0) gmsh.model.mesh.field.setAsBackgroundMesh(field2)逻辑是:先计算每个点到孔边界曲线的距离,再用Threshold定义“距离小于0.2时网格尺寸0.05,距离大于1.0时网格尺寸0.5,中间线性过渡”。这个组合比MathEval更通用,因为它不依赖你手动写坐标函数,适合模型复杂、加密目标又很明确的场景。
需要注意,距离字段依赖几何实体。如果之前的布尔操作已经改动了几何,曲线tag可能已经变化,一定要在getEntities或者返回值里重新获取最新的tag,再传给字段配置。
4.3 结构化网格:Transfinite与Recombine的正确打开方式
有些求解算法,特别是流体计算里的某些格式,希望网格尽可能结构化和规整。Gmsh里的Transfinite和Recombine就是做这个事的。
Transfinite需要你先在几何上布好线:给组成面的四条边界线设置等分点,然后再对面应用setTransfiniteSurface。最常见的错误是直接对任意面调用setTransfiniteSurface,结果发现生成的网格完全不受控制。原因很好理解:结构化网格要求面的拓扑结构相对简单,通常是四边域,如果你试图在五边形或者带孔的面上一键生成结构化网格,Gmsh做不到,也不会给你一个“完美”的结果。
更合理的做法是:把复杂几何先切成一个个四边形区域,然后再在每个区域上用Transfinite。这个工作叫“几何分解”,是前处理的核心手艺之一。网格能不能做好,很多时候不取决于网格算法,而取决于几何被你切成了什么形状。
Recombine则把三角形网格重组成四边形。开启全模型的四边形重组:
gmsh.option.setNumber("Mesh.RecombineAll", 1)重组后的网格质量波动比较大,建议设置Mesh.RecombineOptimizeTopology之类的选项,或者先小范围测试。不要图省事直接全模型一键四边化,结果很容易出现质量很差的单元。
5. 接口进阶:物理组、元素导出与求解器衔接
网格文件写出来只是中间产物。很多时候你需要把节点坐标、单元连接关系直接读进自己的程序,或者把物理组信息连同网格一起交给下游求解器。这一章把这些链路讲清楚。
5.1 合理组织物理组:比你想像的更重要
物理组的重要性,在真正跑仿真的时候才会完全体现。以一个热应力分析为例:你要给板的四条外边界分别定义不同的热边界条件,给孔壁定义对流换热系数,给整个板定义材料属性。如果你只是建了一个面,没有分边界,求解器根本不知道每条边的角色。
所以建物理组的时候,习惯性地把“结构”分组和“物理含义”分组分开:
# 几何分组 gmsh.model.addPhysicalGroup(1, [left_edge], tag=201, name="left") gmsh.model.addPhysicalGroup(1, [right_edge], tag=202, name="right") gmsh.model.addPhysicalGroup(1, [hole_edge], tag=203, name="hole") # 物理分组 gmsh.model.addPhysicalGroup(0, [corner_point], tag=301, name="fixed_point")这里维度1对应曲线,维度0对应点。物理组的tag可以自由指定,也可以用name字段直接命名。命名比tag数字更直观,输出到msh后,很多求解器会直接显示这个名字。
需要提醒的是,物理组别建得太多太碎,不然输出文件的物理区段会变得特别长,下游解析也麻烦。我的经验是:只给实际需要设置边界条件的地方建组,用不到的几何分组就别加。
5.2 不进GUI,直接拿到节点与单元数据
Gmsh的API允许你绕过写文件,直接把网格数据拿进来:
# 获取所有节点 node_tags, node_coords, _ = gmsh.model.mesh.getNodes() # 获取二维单元 elem_types, elem_tags, node_tags_per_elem = gmsh.model.mesh.getElements(2)node_tags是节点编号列表,node_coords是所有节点的坐标平铺数组,每三个数对应一个节点。有了这两个数组,再配合单元类型和每个单元的节点编号,你就能组装出自己的有限元网格结构。
拿到手之后用NumPy重新组织一下,逻辑会清晰很多:
import numpy as np coords = np.array(node_coords).reshape((-1, 3)) # node_tags_per_elem 每个单元是一个变长列表,依单元类型而定这样做的优势很明显:网格数据不需要落到磁盘再读一遍,减少IO开销,也避免了解析不同msh版本的麻烦。尤其在你写自己的求解器、做网格自适应迭代、或者把网格喂给深度学习模型做前处理时,这种方式非常顺手。
5.3 让输出文件被更多求解器接受
Gmsh默认写出的msh格式版本可能比你下游工具读的高,导致对方报“不支持的文件版本”。最常用的兼容操作是指定msh文件版本,并关闭二进制输出:
gmsh.option.setNumber("Mesh.MshFileVersion", 2.2) gmsh.option.setNumber("Mesh.Binary", 0)很多老牌求解器和自编程序只认ASCII格式的msh 2.2,这样设置之后基本不会出问题。如果你确定下游支持msh 4.1,也可以保持默认新版本,文件小,解析速度快,但新版本的格式改动比较大,解析器不更新就白搭。
另外一个容易被忽略的选项是:
gmsh.option.setNumber("Mesh.SaveElementTag", 0)这个选项控制写入文件时是否带单元编号。有些下游程序读到带tag的单元列表反而会误解,关掉会让文件更干净。
6. 高频踩坑与排查速查
这部分是我的亲身经历整理。每次在群里看到有人问Gmsh API报错,十次有八次是下面这几个原因。
6.1 最经典的报错:找不到实体或空集合
表现:同步之后,调用getEntities()列表为空,或者网格生成时报“No elements”。最常见的原因是忘了同步。occ里的几何对象和model层不打通,网格器看不到任何实体。解决办法很简单:调用gmsh.model.occ.synchronize(),或者使用geo内核就调用gmsh.model.geo.synchronize()。
另一个常见原因是:你在布尔操作之后没有重新获取面tag,还在用之前的tag。比如矩形板被圆孔cut之后,原先的矩形面可能已经改变,新面出现在cut[0]里。解决方法是把cut[0]里筛选出来的新面tag保存下来并重新使用,不要用旧的变量。
6.2 geo与occ混用带来的问题
我见过如下操作:先用gmsh.model.geo.addPoint建点,再用gmsh.model.occ.addRectangle画矩形,最后用gmsh.model.geo.addLine连线,结果报错说实体维度不一致或找不到曲线。这不是Gmsh坏了,是因为两个内核是两套独立的实体库,它们之间的实体不能相互引用。
建议从一开始就选定一个内核。需要布尔、倒角、复杂建模,全部用occ;只是简单平面结构且需要精确控制布线,全部用geo。别混着写。混着写出来的脚本,往往调试时间远超重写时间。
6.3 网格畸形的常见解法
表现:网格生成成功后,某些单元长宽比极大,或者局部区域畸形。先按顺序排查:
- 几何本身是否有狭长面或小于网格尺寸的微小特征。如果孔半径比网格尺寸还小,网格必然畸形。
- 全局尺寸是否设置得太严格。
MeshSizeMin和MeshSizeMax差距过大会导致Gmsh为了满足极小值而制造大量单元,网格总体数量失控。 - 尺寸字段的值是否平滑。突然从0.02跳到0.5,过渡区会有严重的单元形状退化。把
DistMin和DistMax拉大,过渡会更缓。 - 算法是否适配。对于含孔洞模型,可以尝试换算法:
gmsh.option.setNumber("Mesh.Algorithm", 6)常见的算法中有几档适合二维曲面网格,如果默认算法在某些几何上表现差,换一个算法往往立竿见影。
6.4 常见问题速查表
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
| GUI里看不到几何体 | 没有调用synchronize() | 调用对应内核的同步方法 |
| 网格生成了但缺少边界 | 没有创建物理组 | 给边界面/曲线加addPhysicalGroup |
| 局部加密不生效 | 全局Min/Max限制或字段被覆盖 | 检查MeshSizeMin/Max并调整字段值 |
| 写出的msh下游读不了 | msh版本或二进制格式不兼容 | 设置Mesh.MshFileVersion和Mesh.Binary |
| geo和occ实体互相找不到 | 两套内核混用 | 统一使用一个内核 |
| 单元数量暴增 | 全局最大尺寸和最小尺寸差距过大 | 调整尺寸边界,使用平滑过渡字段 |
| 布尔操作后tag失效 | 实体被重建 | 从布尔返回值重新获取实体tag |
服务器上fltk.run()报错 | 无图形环境 | 注释掉GUI调用,仅保留写文件 |
排查的时候,最有用的一个命令是打印当前模型的所有实体:
print(gmsh.model.getEntities()) print(gmsh.model.getBoundary([(2, surface)], combined=True))把实体列表和tag打出来,对照代码一步步检查,比自己闷头猜效率高得多。
7. 一些建议:把这套API用到自己的工作中去
最后分享一点个人体会。我学Gmsh API最有效的方法,不是先刷一遍官方文档,而是把文档里的示例代码拿下来跑一遍,然后随手改动几个参数,看输出有什么变化。把“改参数→看结果”这个循环重复几十次,对API的理解会比死记硬背深刻得多。
另一个特别推荐的做法是,把自己常用的前处理流程封装成函数。比如“带圆孔的矩形板”“带倒角的支架”“多层结构体”这类模板,各写一个可复用的Python模块,几何参数全部走函数入参。后续接到新任务,不再是每次从零建模,而是复制模板、改参数、生成网格。这个积累过程,会让你越来越觉得当初花时间学API是值得的。
如果遇到函数名和参数记不清,别硬记,直接在Python里调用:
help(gmsh.model.occ.addRectangle)所有API的docstring都嵌在包里,查询效率比翻网页高很多。
网格生成这件事,做到一定程度你会发现,难点往往不在软件操作,而在你对几何结构、物理问题、网格特性之间关系的理解。Gmsh的API给了你一个可以反复试验的高效工具,工具本身的逻辑并不复杂,真正需要打磨的是你在实际项目里不断积累的判断力。把这篇文章里的案例跑熟,再带上你自己的几何问题去折腾几次,很快就能上手了。