Parts vs. Direct Session: When to Use Each¶
apeGmsh gives you two ways to build geometry for a finite element model. You can define reusable Parts that get imported into a session, or you can build everything directly inside a single apeGmsh session. Both paths end at the same place — a meshed, solver-ready model — but they suit different problems.
This guide explains the tradeoffs, then digs into what actually happens to the mesh when multiple bodies meet: fragmentation, conformal interfaces, and what goes wrong when you skip it.
Two Approaches¶
Approach A — Isolated Parts¶
Each component is built in its own Gmsh session, exported to STEP, and imported into the assembly session.
from apeGmsh import Part, apeGmsh
# Build in isolation
col = Part("column")
col.begin()
col.model.geometry.add_box(0, 0, 0, 0.5, 0.5, 3.0)
col.save()
col.end()
# Assemble
g = apeGmsh("frame")
g.begin()
g.parts.add(col, label="col_1", translate=(0, 0, 0))
g.parts.add(col, label="col_2", translate=(6, 0, 0))
g.parts.fragment_all()
g.mesh.generation.generate(dim=3)
Approach B — Direct Session¶
Everything is created inside one apeGmsh session. You can optionally wrap
geometry in g.parts.part() blocks to give it a name and track it for
fragmentation and constraints.
from apeGmsh import apeGmsh
g = apeGmsh("frame")
g.begin()
with g.parts.part("col_1"):
g.model.geometry.add_box(0, 0, 0, 0.5, 0.5, 3.0)
with g.parts.part("col_2"):
g.model.geometry.add_box(6, 0, 0, 0.5, 0.5, 3.0)
g.parts.fragment_all()
g.mesh.generation.generate(dim=3)
Or, at its simplest, skip g.parts entirely:
g = apeGmsh("plate")
g.begin()
g.model.geometry.add_rectangle(0, 0, 0, 10, 5)
g.mesh.generation.generate(dim=2)
When to Use Each¶
Use Parts when...¶
You have reusable components. A standard column section, a parametric beam, a precast panel. Define it once as a Part, save the STEP, and import it multiple times with different positions. The geometry is authored once and the STEP file becomes a portable artifact that can be version-controlled or shared across projects.
Geometry comes from external CAD. If someone hands you a SolidWorks or
FreeCAD STEP file, g.parts.import_step() brings it in without touching
the original. The Part class itself is optional here — you can import STEP files
directly — but the mental model is the same: geometry is defined elsewhere,
assembly happens in apeGmsh.
You want clean separation of concerns. Part sessions cannot mesh, cannot define physical groups, cannot set constraints. This is by design. It forces all solver-facing decisions into the assembly session, which means you can change mesh density or constraint strategy without touching the geometry definition.
Multiple people work on the model. One person defines the column geometry, another defines the slab. Each saves a STEP file. The assembly script imports both and handles positioning, meshing, and coupling.
Use a direct session when...¶
The model is a single body. A plate with a hole, a circular annulus, a cantilever beam. There are no interfaces between parts, no fragmentation needed, and no reason to save intermediate STEP files.
You need fine-grained meshing control. Transfinite meshing, structured grids, and recombined quads often require careful coordination between the geometry and the meshing calls. Doing everything in one session — building geometry, then immediately setting transfinite constraints on the curves and surfaces you just created — is more natural than building in a Part, exporting, re-importing, and then hunting for the right entity tags.
You are running parametric sweeps. In a convergence study or parameter scan, you rebuild geometry from scratch each iteration. The overhead of creating a Part, saving to disk, and importing adds no value when the geometry is disposable.
Rapid prototyping. When you are exploring a geometry idea and iterating quickly, the single-session approach has less boilerplate.
Comparison at a Glance¶
Parts + Import Direct Session
────────────── ──────────────
Reusability High (STEP files) None (inline)
External CAD Natural fit Possible via g.model.io.load_step()
Session isolation Yes (own Gmsh env) No (shared session)
Boilerplate Higher Lower
Transfinite control After re-import Immediate
Parametric sweeps Overkill Ideal
Multi-person workflow Clean handoff Harder
Fragmentation needed Almost always Only if multi-body
What Happens to the Mesh: Fragmentation and Coherence¶
This is the key question for any multi-body model. When you place two volumes next to each other in Gmsh, they are independent geometric entities. Gmsh meshes each one separately. Unless you do something about it, the nodes at their shared interface will not line up.
The Problem: Non-Conformal Interfaces¶
Imagine two boxes touching along a face. Without fragmentation, Gmsh sees two separate volumes, each with its own copy of the shared surface. It meshes them independently:
Box A Box B
┌──┬──┬──┐ ┌───┬───┐
│ │ │ │ │ │ │
├──┼──┼──┤ ├───┼───┤
│ │ │ │ │ │ │
├──┼──┼──┤ ├───┼───┤
│ │ │ │ │ │ │
└──┴──┴──┘ └───┴───┘
↑ ↑
These nodes don't match
Box A might put 4 nodes on the interface. Box B might put 3. The nodes are at different positions. There is no direct node-to-node connection between the two meshes. For a finite element solver, this means the interface either has gaps in the displacement field or requires special treatment (mortar elements, penalty coupling, tied contact).
The Solution: Boolean Fragmentation¶
g.parts.fragment_all() calls gmsh.model.occ.fragment() on all tracked
entities. This is a boolean operation that splits the geometry at every
intersection, producing a single conformal topology where shared interfaces
become shared geometric entities.
After fragmentation:
Box A Shared Box B
┌──┬──┬──┐──┬──┬──┐──┬───┬───┐
│ │ │ │ │ │ │ │ │ │
├──┼──┼──┼──┼──┼──┼──┼───┼───┤
│ │ │ │ │ │ │ │ │ │
├──┼──┼──┼──┼──┼──┼──┼───┼───┤
│ │ │ │ │ │ │ │ │ │
└──┴──┴──┴──┴──┴──┴──┴───┴───┘
↑
Shared surface: one set of nodes
The interface surface now belongs to both volumes. When Gmsh meshes, nodes on that surface are shared. The two bodies are connected node-to-node with no gaps, no overlaps, and no special constraints needed.
What Fragmentation Actually Does¶
Internally, fragment_all() performs these steps:
-
Collects all tracked entities at the highest dimension present (3D volumes, or 2D surfaces, or 1D curves).
-
Calls
gmsh.model.occ.fragment()— the OCC boolean engine computes all intersections and splits every entity at every crossing. A volume that was one piece may become several pieces if other volumes cut through it. -
Builds an old-tag to new-tag map. After fragmentation, entity tags change. What was volume tag 1 might now be volumes 5, 6, and 7 (because two other volumes cut through it). The
result_mapfrom Gmsh tells us exactly which new tags came from which old tags. -
Updates each Instance's entity list in-place. This is critical — without it, the part labels would point to tags that no longer exist. After this step,
g.parts.get("col_1").entitiescontains the correct post-fragment tags. -
Synchronizes the OCC kernel so the new topology is available for meshing.
What Can Go Wrong¶
Orphaned entities. If you create geometry outside of g.parts.part() blocks
and don't register it, fragment_all() will warn you. The orphan entities still
participate in fragmentation (they are in the Gmsh model), but their new tags
won't be tracked by any instance. This can cause confusion later when building
node maps for constraints. Fix: use g.parts.from_model() or
g.parts.register() to adopt orphans before fragmenting.
Tag invalidation. Any entity tag you saved before calling fragment_all()
is potentially invalid afterward. Don't cache tags across a fragment call. Always
query fresh tags from g.parts.get(label).entities or from physical groups
after fragmentation.
Topological surprises. Fragmentation can create new entities you didn't expect. Two cubes touching along an edge will produce shared edge and vertex entities. A beam volume passing through a slab volume will split the slab into multiple pieces. The total entity count after fragmentation is often larger than before. This is correct behavior — it's the geometric price of conformal interfaces.
Near-miss geometry. If two bodies are almost touching but have a tiny gap
(say, 1e-8 mm due to floating-point), the OCC kernel may not detect the
intersection. The result: no fragmentation at that interface, non-conformal mesh,
and potentially mysterious solver errors. Cure: make sure your geometry is
built with exact coincidence at interfaces, or use remove_duplicates() with
a tolerance before fragmenting.
When You Don't Fragment¶
Not every multi-body model needs fragmentation. There are legitimate cases where you want non-conformal interfaces:
DOF-incompatible element coupling. When coupling solid elements (3 translational
DOFs per node) with beam/frame elements (6 DOFs per node), you cannot share
nodes directly — the solid nodes lack rotational stiffness. The standard approach
is to keep the meshes separate and use constraints: duplicated interface nodes
with rigidLink for kinematics and equalDOF for translational ties.
Contact problems. If two bodies can slide or separate, their meshes must be independent. Contact algorithms (penalty, Lagrange multiplier, augmented Lagrangian) handle the interface coupling at the solver level.
Domain reduction methods. In soil-structure interaction, the near-field and far-field may be meshed independently with coupling through DRM forces at the boundary.
Multi-scale analysis. A coarse global model and a fine local model may overlap in space but are solved separately with boundary conditions passed between them.
In all these cases, you still use g.parts to track the bodies and build node
maps, but you skip fragment_all() and rely on constraints instead:
g.parts.add(solid_part, label="solid")
g.parts.add(frame_part, label="frame")
# No fragment_all() — meshes are independent
g.mesh.generation.generate(dim=3)
g.constraints.equal_dof("solid", "frame", dofs=[1, 2, 3], tolerance=1e-3)
g.constraints.rigid_link("frame", "solid", link_type="beam")
fem = g.mesh.queries.get_fem_data(dim=3)
node_map = g.parts.build_node_map(fem.nodes.ids, fem.nodes.coords)
records = g.constraints.resolve(fem.nodes.ids, fem.nodes.coords, node_map=node_map)
The Middle Ground: g.model.queries.make_conformal()¶
For single-session models (no Parts, no g.parts), there is a lower-level
fragmentation path on the Model composite:
g = apeGmsh("multi_region")
g.begin()
g.model.geometry.add_box(0, 0, 0, 1, 1, 1)
g.model.geometry.add_box(1, 0, 0, 1, 1, 1)
g.model.queries.make_conformal() # fragments all entities at their intersections
g.mesh.generation.generate(dim=3)
g.model.queries.make_conformal() (aliased as g.model.queries.fragment_all()) performs the
same OCC boolean fragmentation but without instance tracking. Use it when you
have multiple geometric regions in one session but haven't bothered with
g.parts. The tradeoff: you won't have labeled instances, so building node maps
and constraints is manual.
Geometric Construction: Composing One Body from Multiple Primitives¶
There is a third pattern that sits between "one Part = one primitive" and "just build everything in the session." Sometimes you need to compose a complex shape from simpler geometric operations — an I-beam from three boxes, an L-shaped wall from two rectangles, a foundation with cutouts. The result is conceptually one body with one material, one set of properties, and no internal interfaces.
This is geometric construction, not assembly. The difference matters for the mesh: you don't want conformal interfaces inside the I-beam web-flange junction — you want those internal surfaces to disappear entirely so the mesher treats it as a single volume.
At the Part level¶
The simplest approach: build and fuse inside a Part's isolated session using
model.fuse(). The fused result saves to one STEP file and imports as one
Instance.
from apeGmsh import Part
beam = Part("i_beam")
beam.begin()
web = beam.model.geometry.add_box(0, 0, 0, 0.01, 0.3, 5.0)
flange_bot = beam.model.geometry.add_box(-0.1, -0.005, 0, 0.2, 0.005, 5.0)
flange_top = beam.model.geometry.add_box(-0.1, 0.295, 0, 0.2, 0.005, 5.0)
beam.model.boolean.fuse([web], [flange_bot, flange_top])
beam.properties["material"] = "steel"
beam.properties["section"] = "W360x33"
beam.save()
beam.end()
After fuse(), the three boxes become one volume. The surfaces where the flanges
meet the web are gone — the mesher sees a single solid. This is the recommended
approach when the composed body will be reused or imported into an assembly.
At the session level with fuse_group()¶
When you are building geometry inline and don't need a separate Part, you can define the primitives as tracked parts and then fuse them:
g = apeGmsh("frame")
g.begin()
with g.parts.part("web"):
g.model.geometry.add_box(0, 0, 0, 0.01, 0.3, 5.0)
with g.parts.part("flange_bot"):
g.model.geometry.add_box(-0.1, -0.005, 0, 0.2, 0.005, 5.0)
with g.parts.part("flange_top"):
g.model.geometry.add_box(-0.1, 0.295, 0, 0.2, 0.005, 5.0)
g.parts.fuse_group(["web", "flange_bot", "flange_top"], label="i_beam")
fuse_group() calls OCC fuse on the listed instances, removes the old labels
from the registry, and creates a single new Instance under the given label. The
internal surfaces vanish. Subsequent fragment_all() against other parts will
treat the I-beam as one body.
Fuse vs. Fragment — When to Use Which¶
The distinction comes down to whether the internal boundaries should exist in the mesh:
Fuse erases internal surfaces. Use it when the primitives are construction aids for one physical body. After fusing, there is no interface — the volume is monolithic. The mesher generates elements freely across what used to be the web-flange junction.
Fragment preserves internal surfaces as shared boundaries. Use it when the parts represent different physical regions (different materials, different element types, different mesh densities). After fragmenting, each region is a separate volume that shares nodes at the interface.
Fuse (geometric construction) Fragment (assembly)
───────────────────────────── ────────────────────
Input: 3 boxes (web + flanges) Input: beam + column
Result: 1 volume, no internal Result: 2 volumes, shared interface
surfaces surface with shared nodes
Use: One material, one body Use: Different materials/regions
A typical workflow combines both: fuse primitives into bodies, then fragment the bodies against each other.
# Build two composite parts
g.parts.fuse_group(["web", "fl_top", "fl_bot"], label="beam")
g.parts.fuse_group(["col_core", "col_cover"], label="column")
# Now fragment them for conformal meshing at the beam-column interface
g.parts.fragment_all()
g.mesh.generation.generate(dim=3)
Decision Flowchart¶
Is the model a single body?
├── Yes → Direct session, no fragmentation needed
└── No → Multiple bodies
│
Do the bodies share interfaces that need conformal nodes?
├── Yes → Use g.parts + fragment_all()
│ (or g.model.queries.make_conformal() for quick models)
└── No → Are they DOF-incompatible or in contact?
├── Yes → Use g.parts without fragment,
│ couple via constraints
└── No → They are independent models,
consider separate apeGmsh sessions
Summary¶
Parts give you reusability and clean separation at the cost of an extra
save/import step. Direct sessions give you speed and immediate control at the
cost of everything being tangled in one place. For most structural analysis
workflows with multiple components, the Part approach plus fragment_all() is
the right default — it produces conformal meshes where bodies share interface
nodes, which is what standard FEM solvers expect. When the physics demands
non-conformal coupling (contact, DOF-incompatible elements, multi-scale), skip
fragmentation and use the constraint system instead.