我做CFD和结构仿真二次开发这些年,最深的感受就是:几何建模往往不是瓶颈,网格控制才是真正让项目延期的地方。建模软件里拖个点、拉个面谁都会,可一旦要求“这块加密、那里保持粗网格、过渡还得平滑”,GUI里的鼠标操作就开始力不从心了。Gmsh作为开源网格划分软件里普及度最高的一个,它的二次开发接口恰好把这个问题解决得比较彻底——用Python脚本把网格生成与控制的每一个环节都固化下来,跑一遍程序,网格就按你的规则长出来了。
这一篇是Gmsh二次开发教程系列的第4篇,前几篇已经带着大家把环境搭建、几何建模这些基础打完了。今天我们集中聊网格生成与控制,也就是把几何变成可计算网格的这段关键流程。不管你是做流体、结构还是电磁仿真,只要你的目标是用脚本化方式控制网格尺寸、算法、结构化方式甚至单元阶次,这篇文章都能给你一套能直接照着做的方案。
1. 为什么网格控制是Gmsh二次开发里最值得花时间啃的部分
1.1 脚本化网格控制的本质:把“经验”变成可复用资产
先聊一个很现实的问题:你在Gmsh GUI里手动加密一个局部区域、调一个算法参数,可能只要几分钟。但这段操作经验留在哪里?留在你的脑子里。下个月换个人来做同样的事,他又得重新摸索一遍;下个季度你换了一套几何尺寸,又得重新调整。项目越大,这种不可复现的操作就越危险。
所以二次开发的核心不是“用代码代替鼠标”,而是把网格控制策略变成一种可复现、可评审、可追溯的资产。Gmsh提供了一套完整的API,Python、C++、C、Julia都能调用。写一个网格控制脚本之后,参数变了改参数,几何变了改几何,网格规则基本不用动。我在实际项目里最常用的模式是:把网格生成封装成函数,输入几何tag和尺寸参数,输出msh文件。这样后面接优化算法、批量跑工况,都是水到渠成的事。
1.2 Gmsh网格生成与控制的完整流水线
很多人一上来就对着Gmsh的API文档硬啃,其实没必要。Gmsh的网格控制逻辑是一条清晰的流水线:几何准备 → 尺寸定义 → 算法选择 → 结构化/重组合处理 → 生成 → 优化 → 输出。理解这条流水线,比你记住100个API函数都有用。
尺寸定义是决定网格分布的第一步,也是最容易被忽略的一步。Gmsh支持三种尺寸控制手段:直接设置在点/线/面上的特征长度、全局尺寸上下限、以及基于背景场的空间变化尺寸。算法选择决定节点和单元如何填充这个区域,Gmsh内置了多种算法,各自有不同的优缺点。结构化处理则是另一种思路,通过Transfinite和Recombine这样强行让网格按规则排列,适用于需要四边形或六面体单元的场合。最后是生成和优化,Gmsh会在生成过程中自动检查单元质量,也可以通过平滑和优化选项进一步改善。
搞清楚这条流水线,再看Gmsh的Python API就会觉得很清晰,无非就是流水线上每个环节对应几个函数而已。下面我们就逐个环节拆开讲。
2. 网格生成与控制的三个核心维度:尺寸、算法与结构化
2.1 特征长度、全局尺寸与曲率细化:先搞懂尺寸怎么来的
先说一个很多新手栽过的跟头:Gmsh里的“尺寸”(size)到底代表什么?它不是一个严格的单元边长,而是该位置的特征长度,你可以近似理解成网格的目标尺度。实际生成的单元边长通常会在一定程度内浮动,所以不要指望每个单元边长都严格等于你设定的数值。
代码里最直接的尺寸控制方式是设置点的特征长度。在gmsh.model.geo.addPoint里传入第四个参数,或者在同步之后用gmsh.model.mesh.setSize统一设置。以带孔板为例:
import gmsh gmsh.initialize() gmsh.model.add("plate_hole") # 几何:100x100的板,中心圆孔半径20 p1 = gmsh.model.geo.addPoint(0, 0, 0) p2 = gmsh.model.geo.addPoint(100, 0, 0) p3 = gmsh.model.geo.addPoint(100, 100, 0) p4 = gmsh.model.geo.addPoint(0, 100, 0) l1 = gmsh.model.geo.addLine(p1, p2) l2 = gmsh.model.geo.addLine(p2, p3) l3 = gmsh.model.geo.addLine(p3, p4) l4 = gmsh.model.geo.addLine(p4, p1) outer = gmsh.model.geo.addCurveLoop([l1, l2, l3, l4]) pc = gmsh.model.geo.addPoint(50, 50, 0) pc1 = gmsh.model.geo.addPoint(70, 50, 0) pc2 = gmsh.model.geo.addPoint(50, 70, 0) pc3 = gmsh.model.geo.addPoint(30, 50, 0) pc4 = gmsh.model.geo.addPoint(50, 30, 0) a1 = gmsh.model.geo.addCircleArc(pc1, pc, pc2) a2 = gmsh.model.geo.addCircleArc(pc2, pc, pc3) a3 = gmsh.model.geo.addCircleArc(pc3, pc, pc4) a4 = gmsh.model.geo.addCircleArc(pc4, pc, pc1) inner = gmsh.model.geo.addCurveLoop([a1, a2, a3, a4]) surf = gmsh.model.geo.addPlaneSurface([outer, inner]) gmsh.model.geo.synchronize() # 孔边加密到1mm,外边粗放到8mm gmsh.model.mesh.setSize([(1, a1), (1, a2), (1, a3), (1, a4)], 1.0) gmsh.model.mesh.setSize([(1, l1), (1, l2), (1, l3), (1, l4)], 8.0) gmsh.model.mesh.generate(2) gmsh.write("plate_hole.msh") gmsh.finalize()这段代码的几何是手工搭建的带孔板,外轮廓是四条直线,内孔用四段90度圆弧拼成。为什么要拼成四段圆弧而不是直接用一条闭合圆线?因为后续做Field局部加密、做结构化网格时,四段弧线各自的tag是明确的,操作起来更灵活。如果直接用一条圆弧表示整圆,边界识别和循环处理都会麻烦很多。
再说全局尺寸上下限Mesh.MeshSizeMin和Mesh.MeshSizeMax。这两个选项是兜底用的,确保网格尺寸不会突破你设定的范围。比如你把孔边尺寸设成1mm,但忘了设MeshSizeMin,Gmsh有可能因为其他机制把某些单元生成得特别小,导致计算量失控。反过来,如果你在某个角落漏设了尺寸,上限又能防止那个区域生成吓人的粗网格。我自己的习惯是:只要脚本里有尺寸控制,就一定同时设置这两个值,不然排查问题的时候容易怀疑人生。
还有一个容易忽略的选项是Mesh.MeshSizeFromCurvature,它的作用是让网格根据曲线曲率自动加密。这个值表示一个完整的2π周期内至少划分多少个单元,设成10就表示一个圆周大约被分成10段,设成20就分得更细。默认值是10,如果你希望孔边更圆润,可以把它调高到20甚至30。不过要注意,这个选项是全局生效的,调得太高会让所有弯曲边界都加密,计算量会明显上升。
2.2 背景场(Field):局部加密的完整解法
直接设置特征长度只能控制点的邻域,没法表达“越是靠近某条线越密”这种空间渐变。想要真正灵活地控制尺寸分布,必须用Gmsh的尺寸场(Field)机制。尺寸场可以理解成一个“背景网格”,Gmsh在生成网格时会查询每个位置对应的尺寸值,如果设置了背景场,它的优先级会高于直接设置在几何实体上的特征长度。
我第一次用Field的时候犯过一个错误:加了一堆Field设置,结果网格尺寸纹丝不动,后来才发现是忘了调用setAsBackgroundMesh。这个函数的作用是把某个Field激活为当前背景场,不调用它,前面所有Field配置都是白写的。
最常用的组合是Distance场加Threshold场。Distance计算到指定曲线的距离,Threshold把这个距离映射成网格尺寸。还是用上面的带孔板,假设孔边要1mm,远处要8mm,距离孔边2mm以内保持1mm,25mm以外变成8mm,中间线性过渡:
dist = gmsh.model.mesh.field.add("Distance") gmsh.model.mesh.field.setNumbers(dist, "CurvesList", [a1, a2, a3, a4]) gmsh.model.mesh.field.setNumber(dist, "Sampling", 200) thr = gmsh.model.mesh.field.add("Threshold") gmsh.model.mesh.field.setNumber(thr, "InField", dist) gmsh.model.mesh.field.setNumber(thr, "SizeMin", 1.0) gmsh.model.mesh.field.setNumber(thr, "SizeMax", 8.0) gmsh.model.mesh.field.setNumber(thr, "DistMin", 2.0) gmsh.model.mesh.field.setNumber(thr, "DistMax", 25.0) gmsh.model.mesh.field.setAsBackgroundMesh(thr)这里的关键参数是DistMin和DistMax。DistMin表示尺寸从多少距离开始增大,DistMax表示尺寸增大到什么距离后不再变化。注意要把Mesh.MeshSizeExtendFromBoundary设成0,否则Gmsh会把边界上的尺寸向外扩展,和Field的设置互相干扰。这就是很多人“Field设了没效果”的另一个原因。
如果你需要的尺寸变化是纯数学公式,可以用MathEval,它支持任意数学表达式。比如我想让板左下角(25,25)附近加密,远处变粗,公式可以写成:
f = gmsh.model.mesh.field.add("MathEval") gmsh.model.mesh.field.setString(f, "F", "8 - 7 * exp(-((x-25)^2 + (y-25)^2)/100)") gmsh.model.mesh.field.setAsBackgroundMesh(f)这个表达式里x和y是坐标变量,Gmsh会自动代入。MathEval适合规则化、可解析的场合,而Distance加Threshold适合工程上常见的“到某条边、某个点越近越细”的需求。实际项目中,我经常把多个Field通过Min或Max组合起来,实现“多个加密源同时起作用”的效果。
2.3 网格算法怎么选:MeshAdapt、Delaunay、Frontal与HXT
尺寸定完,接下来是网格生成算法。Gmsh的2D网格算法主要有四种:MeshAdapt(算法编号1)、Delaunay(2)、Frontal(5)、BAMG(6)。3D算法则是Delaunay(1)、New(2)、Frontal(4)、MMG3D(6)、HXT(7)。不同算法生成的网格形态、质量、耗时差别很大。
如果网格拓扑复杂、曲面曲率很大,MeshAdapt通常能给出不错的适应性网格,速度也比较快。Delaunay是经典的Delaunay三角化算法,稳健性好,常规二维问题基本都能处理。Frontal则是前沿推进法,生成的三角形质量普遍较好,方向性也更强一些,适合对网格质量要求较高的场景。BAMG是基于各向异性自适应算法的,适合需要控制单元拉伸比的场合。
我的选择经验是这样的:二维问题,默认无脑用Delaunay基本不会出大问题,但如果你发现某个局部区域三角形特别差,改成Frontal往往能有明显改善。三维问题,如果模型规模在百万网格以上,优先选择HXT,它是并行四面体算法,速度优势非常明显;如果网格质量优先,可以考虑New或Frontal。需要特别注意的是,算法选择对结果的影响远不如尺寸场和几何处理来得大,所以不要把时间浪费在反复切换算法上,先保证尺寸场和几何拓扑正确再说。
三种算法生成的网格对比,可以通过一段简单的循环脚本实现:
for algo in [2, 5, 6]: gmsh.model.mesh.clear() gmsh.option.setNumber("Mesh.Algorithm", algo) gmsh.model.mesh.generate(2) types, elements, nodes = gmsh.model.mesh.getElements(2) count = {t: len(e) for t, e in zip(types, elements)} print("算法编号", algo, "单元统计", count)这里用gmsh.model.mesh.clear()清掉上一次的网格,但几何数据不会丢。getElements拿到当前维度的单元类型和数量,类型2表示三角形、3表示四边形。通过这个统计,你能直观看到不同算法对同一个几何生成了多少单元、有没有生成四边形。
2.4 结构化和四边形/六面体网格:Transfinite、Recombine与几何切分
自由网格虽然适应性强,但CFD边界层、结构接触分析这些场景往往需要四边形或六面体网格。Gmsh里实现结构化网格的主要武器是Transfinite和Recombine。
先理解Transfinite的原理:它要求几何区域必须满足严格的拓扑条件。二维面必须是四条边的拓扑结构,三维体必须是六个面的拓扑结构。矩形满足这个条件,圆柱面也满足,但带孔板这样的单一面就不满足,因为它的边界是八条线。如果在不满足条件的面上调用setTransfiniteSurface,Gmsh会直接报错。
正确的做法是先对几何切块,把复杂形状分解成若干个四边形拓扑的子区域,然后对每个子区域分别做Transfinite。这也是为什么我说几何分解是结构化网格的核心能力。举个例子,一个简单的矩形区域要生成规则四边形网格,代码是这样的:
gmsh.model.add("rect_quads") p1 = gmsh.model.geo.addPoint(0, 0, 0) p2 = gmsh.model.geo.addPoint(20, 0, 0) p3 = gmsh.model.geo.addPoint(20, 10, 0) p4 = gmsh.model.geo.addPoint(0, 10, 0) l1 = gmsh.model.geo.addLine(p1, p2) l2 = gmsh.model.geo.addLine(p2, p3) l3 = gmsh.model.geo.addLine(p3, p4) l4 = gmsh.model.geo.addLine(p4, p1) loop = gmsh.model.geo.addCurveLoop([l1, l2, l3, l4]) s = gmsh.model.geo.addPlaneSurface([loop]) gmsh.model.geo.synchronize() # 对边节点数必须一致:两条长边都分21段,两条短边都分11段 gmsh.model.mesh.setTransfiniteCurve(l1, 21) gmsh.model.mesh.setTransfiniteCurve(l3, 21) gmsh.model.mesh.setTransfiniteCurve(l2, 11) gmsh.model.mesh.setTransfiniteCurve(l4, 11) gmsh.model.mesh.setTransfiniteSurface(s) gmsh.model.mesh.setRecombine(2, s) gmsh.model.mesh.generate(2)这段代码里,setTransfiniteCurve给四条边设定节点数,setTransfiniteSurface把这个面标记为结构化映射面,setRecombine让Gmsh把三角形对合并成四边形。三条组合在一起,最终得到的就是规则的四边形网格。
Recombine还有一个角度参数,默认是45度,表示两个三角形合并成四边形时,如果四边形内角偏离90度超过这个角度就放弃合并。所以如果你发现某些区域四边形很少、三角形很多,可以适当把角度调大一点,但代价是四边形质量下降。我一般建议保持默认,宁可局部留三角形,也不要用畸形四边形去影响求解精度。
对于三维六面体网格,思路完全一致:先把体切分成六面体拓扑的子块,然后对十二条边做setTransfiniteCurve,对六个面做setTransfiniteSurface,最后对体做setTransfiniteVolume并配合setRecombine(3, vol)。这套流程对规则几何非常可靠,但对复杂模型来说,几何切分本身就是一门手艺,我在初学阶段经常在切分上花的时间远超网格生成本身。
2.5 网格阶次:一阶、二阶与“先低后高”的二次开发技巧
网格阶次是很多人容易忽略的控制维度。Gmsh默认生成一阶网格,也就是线性单元。二阶网格每个单元增加了中间节点,能更好地表达弯曲边界和应力梯度,但代价是节点数成倍增加,而且二阶单元对畸变更敏感。
在二次开发里,我经常用到一个“先低后高”的技巧:先用一阶网格做几何调试和尺寸验证,确认无误后再转换成二阶网格。因为一阶网格生成快、问题少,适合快速迭代。二阶转换用gmsh.model.mesh.setOrder(2)就能完成,配合前面的尺寸场和算法设置,整个过程是自动化的。
高阶网格在实际工程中最常见的坑是负Jacobian。原因通常是几何本身的曲率太大、局部网格太粗、或者尺寸过渡太剧烈。如果遇到这类问题,不要盲目调单元阶次,先回头看看局部尺寸是否该加密。一个实用的检查顺序是:关掉二阶(回到一阶),确认一阶网格有没有负体积;如果有,说明几何或尺寸有问题;如果没有,说明是二阶转换阶段引入的问题,需要加密该区域尺寸或者调整算法。
3. 实操案例:带孔板从自由网格到受控网格
3.1 案例目标与几何建模
理论说了一堆,现在来一个完整的实操案例。我们的目标很明确:100×100的方形薄板,中央有半径20的圆孔,孔边网格目标尺寸1mm,远离孔的区域尺寸8mm,中间平滑过渡。同时我们希望比较几种算法在同一个几何上的表现,感受一下不同算法的差异。
几何建模沿用前文那段手工搭建带孔板的代码。为什么要用手工搭建而不是直接用OCC的布尔运算?因为手工搭建的每条线段、每段圆弧tag都是确定的,后面做Field、做结构化处理时引用起来非常清楚。用布尔运算的话,tag是内核自动生成的,不查一下很难确定哪条线对应孔边。
3.2 三种尺寸控制手段的落地代码
建模完成后,同步几何,然后一口气配置三种尺寸控制手段。首先是全局上下限:
gmsh.option.setNumber("Mesh.MeshSizeMin", 1.0) gmsh.option.setNumber("Mesh.MeshSizeMax", 8.0)然后是特征长度,把孔边四段圆弧的尺寸设为1,外边界四条直线设为8:
gmsh.model.mesh.setSize([(1, a1), (1, a2), (1, a3), (1, a4)], 1.0) gmsh.model.mesh.setSize([(1, l1), (1, l2), (1, l3), (1, l4)], 8.0)接着用Distance加Threshold做空间渐变。这里用四段圆弧作为Distance的输入曲线,这样孔边就是加密源,距离孔边越远网格越粗:
dist = gmsh.model.mesh.field.add("Distance") gmsh.model.mesh.field.setNumbers(dist, "CurvesList", [a1, a2, a3, a4]) gmsh.model.mesh.field.setNumber(dist, "Sampling", 200) thr = gmsh.model.mesh.field.add("Threshold") gmsh.model.mesh.field.setNumber(thr, "InField", dist) gmsh.model.mesh.field.setNumber(thr, "SizeMin", 1.0) gmsh.model.mesh.field.setNumber(thr, "SizeMax", 8.0) gmsh.model.mesh.field.setNumber(thr, "DistMin", 2.0) gmsh.model.mesh.field.setNumber(thr, "DistMax", 25.0) gmsh.model.mesh.field.setAsBackgroundMesh(thr) gmsh.option.setNumber("Mesh.MeshSizeExtendFromBoundary", 0)生成网格后,打开GUI查看,你会看到孔边一圈很细,向外逐渐变粗,到板边缘基本稳定在8mm左右。这个分布比单纯在点和线上设置尺寸要自然得多,没有明显的尺寸突变。
3.3 算法切换与网格统计对比
同一个几何,同一个尺寸场,切换不同算法会得到不同的网格。我们来跑一个对比:
for algo in [2, 5, 6]: gmsh.model.mesh.clear() gmsh.option.setNumber("Mesh.Algorithm", algo) gmsh.model.mesh.generate(2) types, elements, nodes = gmsh.model.mesh.getElements(2) count = {t: len(e) for t, e in zip(types, elements)} print("算法", algo, "单元统计", count)在我本机上,Delaunay生成的三角形数量最多,单元分布均匀;Frontal生成的三角形数量略少,但在孔边等曲率变化区域单元方向性更好;BAMG对这个几何也表现不错,但统计数量会有一定波动。你跑出来的具体数字可能和我不一样,这是因为Gmsh版本、操作系统、甚至编译器都会对结果有细微影响,这不重要,重要的是通过这个循环,你能建立起对不同算法特征的感觉。
换算法不需要重建几何,clear清网格之后重新generate即可。这个特性在自动化流程里非常实用——比如你想对同一个模型用不同算法生成多套网格,然后导入求解器对比网格无关性验证,几行代码就搞定了。
3.4 Transfinite+Recombine生成全四边形网格
带孔板这个几何要生成完全规整的四边形网格,需要先切块。直接对一个带孔面调用setTransfiniteSurface肯定会失败,因为它的边界是8条线,不满足四边形拓扑要求。实际项目中,通常会用OCC几何内核把板切开,比如沿圆孔中心做水平和垂直两条分割线,把板分成四个区域,再对每个区域配合径向辐条切出四边形子域。这个切块过程本身就是几何建模的核心技巧,这里就不再展开整套切分代码了,但思路是明确的:结构网格的本质是几何分解。
为了让你看到标准做法,我用一个矩形区域来做完整演示,就是2.4节那段代码。跑完以后你会得到一张全四边形的规整网格,单元数和节点数都可以通过setTransfiniteCurve里的分段数精确控制。这个可控性是自由网格不具备的。
如果你要把这个思路推广到带孔板,记住两点:第一,切出来的每一个子域都必须是四边拓扑;第二,对相邻子域的公共边,节点数必须保持一致,否则跨区域的网格节点对不上,求解器会报错。这两个坑我踩过不止一次,每次都是从GUI里盯着网格看了半天才发现是几何切分不够细。
3.5 导出网格与后续使用建议
网格生成完毕,接下来要导出。gmsh.write("plate_hole.msh")会按当前Gmsh版本默认的MSH4格式保存。如果下游求解器只支持MSH2,可以这样处理:
gmsh.option.setNumber("Mesh.MshFileVersion", 2.2) gmsh.write("plate_hole.msh")另外,如果只想导出物理组对应的区域,而不是全部几何,需要先把面加入物理组,然后把Mesh.SaveAll设为0。这个设置我在项目里经常用,因为仿真模型往往包含辅助几何,比如计算域外的延伸段、网格过渡区等,这些区域不参与求解,导出时应排除掉。
网格质量的检查也别忘了。在GUI里打开msh文件,菜单Tools → Statistics能看到单元数量、节点数量、最小单元质量等统计信息。脚本层面可以通过getElements拿单元数据,自己算内角或Jacobian。对于大模型,我一般习惯在生成后立即打印单元统计,如果发现三角形数量异常、或者某些区域密度明显不对,趁早修改尺寸场,别等到导入求解器才发现问题。
4. 常见问题速查:网格失控的典型场景与排查实录
4.1 尺寸设了却不生效
这个问题大概是Gmsh二次开发里问得最多的。具体现象是:你在代码里设置了孔边尺寸1mm,生成后发现孔边网格还是粗得要命。常见原因有三个。第一,Field加了但没调用setAsBackgroundMesh,导致它根本没生效。第二,设置了Field,但Mesh.MeshSizeExtendFromBoundary还是1,边界特征长度向外扩展,盖过了Field的设置。第三,你在synchronize()之前调用了setSize,几何还没同步,设置被后续操作覆盖了。
排查顺序建议这样:先把所有Field相关代码注释掉,只留setSize,看尺寸能不能生效;能生效说明问题出在Field配置上,不能生效说明是同步或选项问题。然后再逐个打开Field,每打开一个就重新生成一次,很快就能定位是哪一个环节出了问题。
4.2 Transfinite/Recombine失败的判断依据
setTransfiniteSurface报错的时候,Gmsh的提示信息往往只说“cannot be transfinite”之类的泛泛之词,真正的排查要靠自己。第一步检查面的边界线数量,用gmsh.model.getBoundary([(2, surf)])列出所有边界曲线,如果不是四条,那这个面肯定没法直接用Transfinite。第二步检查对边的节点数,两条相对边必须设置相同数量的节点,这个很容易漏。第三步检查角点顺序,setTransfiniteSurface支持传入cornerTags参数,如果默认排序不对,手动指定角点能解决问题。
Recombine失败的表现不太一样,它不报错,只会留下很多三角形。这时候首先要确认Mesh.RecombinationAlgorithm是不是你期望的值,再检查单元尺寸是否太小——如果网格太密,三角形对合并不一定成功。还有一种常见情况是几何里存在很尖的角,合并后的四边形畸变严重,Gmsh按角度限制自动放弃了合并。
4.3 负体积、畸变网格的处理
生成网格时报Negative Jacobian或者Negative Volume,这是最让人头疼的问题之一。出现负体积的根本原因,通常是网格单元的某个节点跑到了其他节点对面,导致单元方向反转。常见诱因有三个:局部尺寸变化太剧烈、几何本身存在微小的自相交、二阶单元转换阶段引入的方向问题。
处理策略按优先级排序:第一步,检查几何,把自相交、重叠面修掉;第二步,加密局部的尺寸过渡,比如把DistMax调得更大,让尺寸渐变更平缓;第三步,打开优化开关,Mesh.Optimize设为1,Mesh.Smoothing适当调大,比如从默认值调到5;第四步,换算法,Frontal生成的单元方向性更强,某些情况下能直接解决问题。
我有一个习惯:每次生成网格后,都会用gmsh.option.setNumber("General.Verbosity", 3)把日志级别调高,Gmsh会在终端打印出所有警告信息,负体积通常会在生成阶段第一时间暴露出来,不用等求解器报错才发现。
4.4 大规模网格的性能优化
模型一大,网格生成本身可能就需要很长时间。我在一个百万级三维模型上遇到过脚本跑了一个小时还没结束的情况,后来发现是默认算法太慢。针对大模型,优先推荐HXT算法,它是并行的,多个核心同时工作,速度优势非常明显。另外记得开启多线程:
gmsh.option.setNumber("Mesh.MaxNumThreads", 8)这个选项默认可能就是多线程,但显式设置会更保险。还有一个容易被忽略的点:尽量不要用全局小尺寸去加密局部区域,全局尺寸一旦设小,整个模型的所有区域都会跟着加密。正确做法是用MeshSizeMin/Max限制全局范围,用Field对局部加密。我见过有人为了加密一个边角,把全局Max从8改到0.5,结果整个模型多了几百万个单元,纯粹是浪费资源。
网格规模过大还有一个间接后果:msh文件体积膨胀。MSH4格式本身比较紧凑,但如果你生成的是可读性强的ASCII格式,文件可能达到几百MB。对大模型,建议直接用二进制格式导出,也就是在write时指定fmt参数或者修改Mesh.MshFileVersion相关的输出选项,速度和解压后的大小都有明显改善。
最后再说一个我自己的习惯:凡是需要反复调整的网格控制逻辑,一定写成一个独立的函数,输入是几何tag和尺寸参数,输出是msh文件。在带孔板这个案例里,尺寸场写完以后,后续换孔半径、换目标尺寸,只要改三个数就能重新出网格。这种把网格策略固化成代码的做法,才算真正吃透了Gmsh二次开发。下一篇我打算接着聊网格到求解器的数据衔接,就是物理组、边界条件怎么通过API写进msh文件,到时候咱们继续。