Gmsh Meshing Basics¶
How geometry becomes a mesh, what Gmsh assumes by default, and how the output is structured.
BRep–Mesh Duality¶
The BRep defines continuous geometry. The mesh is a discrete approximation classified onto that geometry. This classification is the fundamental link — every mesh entity (node, element) knows which BRep entity it belongs to.
GEOMETRY (continuous) MESH (discrete)
─────────────────── ─────────────────
BRep entities Nodes + Elements
identified by (dim, tag) classified onto entities
Classification means:
- A node on a curve belongs to that curve entity, not to the surfaces on either side of it.
- A node at the intersection of three curves belongs to the point entity at that junction.
- An element on a surface belongs to that surface entity.
This is how mesh conformity works across entities. Two surfaces sharing an edge don't each get their own nodes along that edge — the nodes belong to the edge entity (dim 1) and are shared by both surface meshes.
Surface A (2, 1) Surface B (2, 2)
┌───────────┬───────────┐
│ · · · │ · · · │
│ · · · ─┤─ · · · │
│ · · · │ · · · │
└───────────┴───────────┘
│
Curve (1, 5)
Nodes here belong to (1, 5)
Shared by both surface meshes
This is also why getNodes(dim, tag) works — you're asking for nodes classified on a specific BRep entity. And why getNodes(dim=2, tag=3) returns only the interior nodes of surface 3, not the boundary nodes (those belong to the bounding curves). To include boundary nodes, pass includeBoundary=True.
Dual query — nodes and elements on the same entity¶
The classified mesh can be queried through both getNodes and getElements at any (dim, tag). Both paths access the same mesh, from different angles:
| Query | Returns |
|---|---|
getNodes(dim, tag) |
Node tags + coordinates classified on entity |
getElements(dim, tag) |
Element types + IDs + connectivity on entity |
This works uniformly across all dimensions:
# Dim 0 — a point entity
getNodes(0, pt1) # → the node tag + coordinates at that point
getElements(0, pt1) # → point element (type 15) wrapping that same node
# Dim 1 — a curve entity
getNodes(1, crv5) # → nodes along the curve
getElements(1, crv5) # → line elements (type 1) connecting those nodes
# Dim 2 — a surface entity
getNodes(2, srf3) # → interior nodes on the surface
getElements(2, srf3) # → tri/quad elements with connectivity referencing those nodes
# Dim 3 — a volume entity
getNodes(3, vol1) # → interior nodes in the volume
getElements(3, vol1) # → tet/hex elements filling that volume
Nodes give you positions. Elements give you connectivity. Both are classified on the same BRep entity and are always consistent.
[!note]
getNodes(dim, tag)returns only nodes classified directly on that entity — not the boundary.getElements(dim, tag)returns elements on that entity whose connectivity references boundary nodes. So elements on a surface connect to nodes on the bounding curves, but those curve nodes won't appear ingetNodes(2, tag)unless you passincludeBoundary=True.
# Interior nodes only — classified directly on surface 3
interior_tags, interior_coords, _ = gmsh.model.mesh.getNodes(2, 3)
# All nodes — including those on bounding curves and corner points
all_tags, all_coords, _ = gmsh.model.mesh.getNodes(2, 3, includeBoundary=True)
Meshing Per Dimension¶
Gmsh meshes dimension by dimension, bottom-up. Lower dimensions must be meshed before higher dimensions, because the higher-dimensional mesh depends on the lower-dimensional discretization as its boundary.
Calling generate(3) triggers the full chain. Calling generate(2) meshes only curves and surfaces. You can also mesh one dimension at a time for finer control.
Dim 0 — Points¶
Points get point elements (Gmsh type code 15, alias point1). This is a single node wrapped in an element.
Why does this exist? It's not about meshing — a point has no extent to discretize. Point elements exist so that physical groups at dim 0 can contain mesh elements, not just bare nodes. This matters for:
- Concentrated loads applied at a point
- Point springs or dashpots
- Concentrated masses
- Pinned/fixed boundary conditions at specific nodes
Without point elements, a physical group at dim 0 would have no elements to query via getElements(). The point element is Gmsh's way of making the dim 0 → mesh → solver pipeline uniform across all dimensions.
# A physical group at dim 0 works like any other
gmsh.model.addPhysicalGroup(0, [pt1, pt2], name="BC_Pinned")
# After meshing, you can get the point elements
elemTypes, elemTags, elemNodeTags = gmsh.model.mesh.getElements(0, pt1)
# elemTypes = [15] ← point element
# elemNodeTags = [[node_tag]] ← the single node at that point
Dim 1 — Curves¶
Curves are discretized into line elements (type 1 line2 for linear, type 8 line3 for quadratic). The number of elements on a curve is controlled by the mesh size at its endpoints and the size field along its length.
This is the foundation — the 1D mesh defines the edge discretization that surfaces must conform to.
Dim 2 — Surfaces¶
Surfaces are meshed with triangles (type 2 tri3) and/or quadrilaterals (type 3 quad4) for linear elements. The surface mesh is bounded by the 1D mesh on its edges.
The meshing algorithm (Delaunay, Frontal-Delaunay, etc.) generates the interior nodes and connectivity. Recombination can convert triangles to quads.
Dim 3 — Volumes¶
Volumes are filled with tetrahedra (type 4 tet4), hexahedra (type 5 hex8), prisms (type 6 prism6), or pyramids (type 7 pyramid5). The volume mesh is bounded by the 2D mesh on its faces.
Tet meshing is fully automatic. Hex meshing requires structured (transfinite) setup or recombination from tets. Prisms and pyramids appear as transitions between hex and tet regions.
Default Meshing Parameters¶
When you call gmsh.model.mesh.generate() with zero configuration, these are Gmsh's defaults (from the source, DefaultOptions.h):
Algorithms¶
| Option | Default | Meaning |
|---|---|---|
Mesh.Algorithm |
6 (Frontal-Delaunay) |
2D surface meshing algorithm |
Mesh.Algorithm3D |
1 (Delaunay) |
3D volume meshing algorithm |
Mesh.ElementOrder |
1 (linear) |
Polynomial order of elements |
Mesh.Smoothing |
1 |
Number of smoothing passes |
Mesh.RecombineAll |
0 (off) |
Don't recombine tris→quads |
Size control¶
| Option | Default | Meaning |
|---|---|---|
Mesh.MeshSizeMin |
0 |
No minimum (effectively unlimited) |
Mesh.MeshSizeMax |
1e22 |
No maximum (effectively unlimited) |
Mesh.MeshSizeFactor |
1.0 |
Global multiplier applied to all sizes |
Mesh.MeshSizeFromPoints |
1 (on) |
Use sizes prescribed at geometry points |
Mesh.MeshSizeFromCurvature |
0 (off) |
Don't auto-size from curvature |
Mesh.MeshSizeExtendFromBoundary |
1 (on) |
Propagate boundary sizes into interior |
What this means in practice¶
With zero configuration, Gmsh:
- Uses the mesh sizes you set at geometry points (
meshSizeparameter inaddPoint) as the primary size control - Extends those sizes from boundaries into the interior of surfaces and volumes
- Does not adapt to curvature (you get uniform-ish elements even around tight curves)
- Applies no global min/max — sizes are purely driven by geometry point prescriptions
- If no sizes are prescribed anywhere, falls back to a size derived from the model bounding box
For structural analysis, you almost always need to override at least the mesh size. Relying on point-prescribed sizes alone gives poor control over interior refinement.
Mesh Size Control Hierarchy¶
This is the core of "why is my mesh this size?" Gmsh resolves the final element size at any point by combining multiple sources. The logic is in BGM_MeshSize() (from BackgroundMeshTools.cpp):
Step 1 — Collect candidate sizes¶
Five sources contribute a candidate size, and Gmsh takes the minimum of all active sources:
l1 ← sizes from geometry points (if MeshSizeFromPoints = 1)
l2 ← sizes from curvature (if MeshSizeFromCurvature > 0)
l3 ← sizes from background field (if a field is set)
l4 ← per-entity mesh size (if set via API)
l5 ← prescribed size at parametric point (for curves)
lc = min(l1, l2, l3, l4, l5)
If none of these yield a value, the fallback is the model characteristic length — roughly the diagonal of the bounding box.
Step 2 — Clamp to global bounds¶
Step 3 — Apply scale factors¶
The full pipeline as a diagram¶
┌──────────────────┐
│ Point sizes (l1) │──┐
└──────────────────┘ │
┌──────────────────┐ │
│ Curvature (l2) │──┤
└──────────────────┘ │ ┌─────────┐ ┌───────────┐ ┌──────────────┐
┌──────────────────┐ ├──► │ min(all) │──► │ clamp to │──► │ × SizeFactor │──► final lc
│ Fields (l3) │──┤ └─────────┘ │ [Min,Max] │ │ (entity+global)│
└──────────────────┘ │ └───────────┘ └──────────────┘
┌──────────────────┐ │
│ Entity size (l4) │──┤
└──────────────────┘ │
┌──────────────────┐ │
│ Param. pts (l5) │──┘
└──────────────────┘
[!important] The combination rule is minimum, then clamp, then scale. This means a background field can only make the mesh finer than what geometry points prescribe — it cannot coarsen it. To override point sizes, use
MeshSizeFromPoints = 0or setMeshSizeMax.
Boundary size extension¶
MeshSizeExtendFromBoundary controls whether sizes from lower-dimensional entities propagate inward:
1(default) — extend from curves into surfaces, and from surfaces into volumes0— don't extend; interior sizing relies entirely on fields or the model characteristic length2— like 1, but uses smallest (not longest) surface edge length for 3D Delaunay-2— extend only into surfaces (not volumes)-3— extend only into volumes (not surfaces)
This is why changing the size at a geometry point affects the mesh deep inside a volume — the size propagates inward from the boundary.
Meshing Workflow¶
The intended workflow in Gmsh, from geometry to final mesh:
1. Set global defaults¶
gmsh.option.setNumber("Mesh.MeshSizeMin", min_size)
gmsh.option.setNumber("Mesh.MeshSizeMax", max_size)
gmsh.option.setNumber("Mesh.Algorithm", 6) # 2D algorithm
gmsh.option.setNumber("Mesh.Algorithm3D", 1) # 3D algorithm
2. Set per-entity sizes (optional)¶
gmsh.model.mesh.setSize([(0, pt1), (0, pt2)], target_size)
# Sets mesh size at geometry points — propagates along adjacent curves/surfaces
3. Define size fields for local refinement (optional)¶
f_dist = gmsh.model.mesh.field.add("Distance")
gmsh.model.mesh.field.setNumbers(f_dist, "CurvesList", [edge_tag])
f_thresh = gmsh.model.mesh.field.add("Threshold")
gmsh.model.mesh.field.setNumber(f_thresh, "InField", f_dist)
gmsh.model.mesh.field.setNumber(f_thresh, "SizeMin", fine)
gmsh.model.mesh.field.setNumber(f_thresh, "SizeMax", coarse)
gmsh.model.mesh.field.setNumber(f_thresh, "DistMin", r_fine)
gmsh.model.mesh.field.setNumber(f_thresh, "DistMax", r_coarse)
gmsh.model.mesh.field.setAsBackgroundMesh(f_thresh)
4. Generate¶
5. Optimize (optional but recommended)¶
gmsh.model.mesh.optimize()
# or specific methods:
gmsh.model.mesh.optimize("Netgen") # Netgen optimizer for tets
gmsh.model.mesh.optimize("HighOrder") # for quadratic+ elements
gmsh.model.mesh.optimize("Relocate3D") # node relocation
6. Set element order (if not linear)¶
[!note] Element order can be set before or after generation. If set before, Gmsh generates high-order elements directly. If set after, it converts the linear mesh to high-order by adding mid-edge (and mid-face, mid-volume) nodes.
Structured vs Unstructured Meshing¶
These are two fundamentally different approaches to filling a domain with elements.
Unstructured¶
The default. Gmsh's algorithms (Delaunay, Frontal-Delaunay, etc.) fill the domain automatically. You control element size, but not element placement or topology.
- Produces triangles (2D) and tetrahedra (3D) by default
- Works on any geometry — no topological constraints
- Element count and arrangement are non-deterministic
- Recombination can convert tris→quads, tets→hexes (with mixed quality)
Structured (Transfinite)¶
You explicitly prescribe the mesh topology. The surface or volume is mapped to a regular grid.
# 1. Prescribe node count on each curve
gmsh.model.mesh.setTransfiniteCurve(tag, numNodes, meshType="Progression", coef=1.0)
# 2. Declare surface as transfinite (must have 3 or 4 bounding curves)
gmsh.model.mesh.setTransfiniteSurface(tag, arrangement="Left", cornerTags=[])
# 3. Declare volume as transfinite (must have 6 or 8 bounding surfaces)
gmsh.model.mesh.setTransfiniteVolume(tag, cornerTags=[])
# 4. Recombine to get quads/hexes
gmsh.model.mesh.setRecombine(2, surfTag)
gmsh.model.mesh.setRecombine(3, volTag)
Transfinite requirements:
- Surfaces must have 3 or 4 corner points with matching node counts on opposite edges
- Volumes must have topologically mappable shapes (e.g., a box with six faces)
- All bounding curves must have
setTransfiniteCurveapplied
Transfinite gives:
- Pure quad/hex meshes (with recombine)
- Deterministic, reproducible meshes
- Control over element grading (via Progression or Bump)
- But only works on simple topologies — no arbitrary shapes
When to use which¶
| Approach | Geometry | Element quality | Control |
|---|---|---|---|
| Unstructured | Anything | Variable | Size only |
| Structured | Simple topology | High | Full |
| Hybrid | Mixed complexity | Mixed | Per-entity |
For structural analysis, the hybrid approach is common: structured meshing on regular regions (beam flanges, plate sections) and unstructured on complex regions (connections, fillets), with transition elements at the interface.
Mesh Output¶
After generation, the mesh is accessible through the API as two parallel data structures: nodes and elements.
Nodes — position data¶
nodeTags, nodeCoords, parametricCoord = gmsh.model.mesh.getNodes(
dim=-1, tag=-1, includeBoundary=False, returnParametricCoord=True
)
| Return | Type | Shape / Layout |
|---|---|---|
nodeTags |
ndarray[int64] |
(N,) — globally unique node IDs |
nodeCoords |
ndarray[float64] |
(3N,) — flat: $[x_1, y_1, z_1, x_2, y_2, z_2, \dots]$ |
parametricCoord |
ndarray[float64] |
parametric position on owning entity |
To get a usable coordinate array:
[!warning]
nodeTagsare not necessarily contiguous or starting from 1. You might get[1, 2, 5, 8, 12, ...]. When building solver models, create a tag→index mapping:
Elements — connectivity data¶
The return is grouped by element type:
| Return | Type | Structure |
|---|---|---|
elemTypes |
list[int] |
Gmsh type codes, one per type |
elemTags |
list[ndarray[int64]] |
Element IDs, one array per type |
elemNodeTags |
list[ndarray[int64]] |
Flat connectivity, one array per type |
To parse the connectivity:
for etype, etags, enodes in zip(elemTypes, elemTags, elemNodeTags):
name, dim, order, npe, _, _ = gmsh.model.mesh.getElementProperties(etype)
conn = enodes.reshape(-1, npe) # shape (n_elements, nodes_per_element)
# conn[i] = [node_tag_1, node_tag_2, ..., node_tag_npe]
# These are node TAGS, not indices — use tag_to_idx to convert
Putting it together — the full extraction¶
# 1. Get all nodes
nodeTags, nodeCoords, _ = gmsh.model.mesh.getNodes()
coords = nodeCoords.reshape(-1, 3)
tag_to_idx = {int(t): i for i, t in enumerate(nodeTags)}
# 2. Get all elements
elemTypes, elemTags, elemNodeTags = gmsh.model.mesh.getElements()
# 3. Parse each element type
for etype, etags, enodes in zip(elemTypes, elemTags, elemNodeTags):
name, dim, order, npe, _, _ = gmsh.model.mesh.getElementProperties(etype)
conn = enodes.reshape(-1, npe)
print(f"Type: {name} (code {etype}), dim={dim}, order={order}, "
f"npe={npe}, count={len(etags)}")
# For each element: ID + connectivity as node tags
for i, eid in enumerate(etags):
node_tags = conn[i] # node tags for this element
node_indices = [tag_to_idx[int(n)] for n in node_tags] # convert to indices
node_xyz = coords[node_indices] # (npe, 3) coordinate block
Filtering by entity or physical group¶
You don't have to get everything at once. The dim and tag parameters filter by BRep entity:
# Elements on surface 5 only
elemTypes, elemTags, elemNodeTags = gmsh.model.mesh.getElements(dim=2, tag=5)
# Nodes in physical group "BC_Fixed" (dim 2, pg tag 1)
nodeTags, nodeCoords = gmsh.model.mesh.getNodesForPhysicalGroup(dim=2, tag=1)
Summary — data flow¶
BRep entity (dim, tag)
│
│ generate()
▼
Nodes: tag → (x, y, z) ← getNodes()
Elements: tag → (type, [node_tags]) ← getElements()
│
│ getElementProperties(type)
▼
Type metadata: code → (name, dim, order, npe)
│
│ reshape + tag mapping
▼
Solver-ready: element_id, [node_indices], coordinates
The mesh output is always the same three things: where are the nodes (coordinates), what are the elements (type + connectivity as node tags), and what does each element type mean (dimension, order, nodes per element). Everything else — physical group labels, entity classification, parametric coordinates — is metadata layered on top.