Parts & assembly¶
This page explains how a multi-part model becomes one connected whole — how a Part is authored as a reusable template, how placing it creates instances whose names stay addressable, and what fragmenting (or fusing) the assembly actually does to the model you'll mesh.
The session already fixed the hierarchy: a Part is a template, an instance is a placement, and the session is the one true model. What follows is the machinery under that sentence — where the geometry lives, how the names travel, and where the topology gets stitched.
Authoring a template¶
Inside a Part you have the full geometry API — primitives, booleans,
transforms — and the two things worth doing deliberately are naming and
annotating. Names first: any label= you attach at geometry time is
recorded, and it will resurface on every instance you later place (how, in a
moment). Annotation second: part.properties is a free-form dict — material,
section, whatever metadata belongs to the template — and it rides along into
each instance placed from it.
from apeGmsh import Part
column = Part("column")
column.begin()
column.model.geometry.add_box(0, 0, 0, 0.5, 0.5, 3.0, label="shaft")
column.properties["material"] = "concrete"
column.save() # → column.step
column.end()
The file on disk is the contract between template and assembly, and the format
is STEP for a reason: STEP preserves the full parametric OCC geometry — exact
surfaces, topology, tolerances — not a tessellation. That's what lets the
assembly treat imported geometry as if it had been drawn there: boolean
operations still work, structured-mesh constraints can be applied after the
fact, and the mesh can be regenerated with any settings without ever reopening
the Part. (IGES is accepted too, for legacy exchange; STEP is the default and
the better choice.) Alongside the STEP file, save() writes a small JSON
sidecar carrying each label's geometric fingerprint — that sidecar is how your
names survive a round trip through a format that has no idea what an apeGmsh
label is.
You own the file when you name it — save("column.step") is a CAD artifact
you can version-control and hand to someone else. Skip save() and the Part
auto-persists to a tempfile on close, which is exactly enough to flow into
g.parts.add(...) in the same script.
Placement: one template, many instances¶
g.parts.add(part, ...) imports the template's file into the session, applies
the placement transform, and registers the result as an instance under a
label:
import math
g.parts.add(column, label="col_1", translate=(0, 0, 0))
g.parts.add(column, label="col_2", translate=(6, 0, 0))
g.parts.add(column, label="col_3",
translate=(3, 0, 0),
rotate=(math.pi/4, 0, 0, 1)) # 45° about Z
Rotation is (angle_rad, ax, ay, az) about the origin, or
(angle_rad, ax, ay, az, cx, cy, cz) about a custom center, and it is applied
before the translation — rotate the template in place, then carry it to its
position. If you omit label, one is generated from the part name and a
running counter; in practice, name every instance — the label is how you'll
address it for the rest of the model.
External CAD takes the same door: g.parts.import_step("slab.step",
label="slab", ...) imports a file from any tool with the same placement
arguments, plus optional healing and deduplication for geometry that arrives
less than clean (see Import a STEP file). And
geometry that is already in the session can be adopted into the registry
rather than imported: wrap inline builds in with g.parts.part("name"): (as
the session page showed), or claim loaded entities after the fact with
g.parts.from_model("name") or g.parts.register("name", dimtags). However
an instance came to be, it ends up the same kind of record — a label, a set of
entities, a bounding box — and that uniformity is what the next two sections
build on.
Placement isn't necessarily final, either. Every instance carries an
inst.edit composite (and every Part a part.edit) with translate,
rotate, mirror, copy, pattern_linear, pattern_polar, and align_to
— so a column placed once can be arrayed into a grid, and a template can be
mirrored into its opposite-hand twin, without touching the original geometry.
How instances stay addressable¶
Place the same template twice and you have a naming problem: both copies were
authored with a face labeled shaft. apeGmsh resolves it by prefixing —
every label a Part carried is re-created on import as
"{instance_label}.{label}", rebound to the placed geometry through the
sidecar's fingerprints (raw tags don't survive an import; geometric anchors,
transformed by the same placement, do). The instance also gets an umbrella
label equal to its own name, covering everything it owns. So after the
placements above, "col_1" is all of the first column, "col_1.shaft" is its
labeled box, and "col_2.shaft" is the same face on the other instance —
individually addressable, from one template.
The instance object makes those strings discoverable rather than memorized:
A typo raises immediately with the list of labels the instance actually has. These prefixed names are ordinary labels — Tier 1, not yet solver-facing — so the usual rule from the mental model applies: promote what the solver needs, when you need it:
From here the instances are just names like any others — loads, constraints,
and selections target "col_1" exactly as they'd target a label you created
by hand.
Fragment or fuse¶
Placed instances occupy space; they don't yet share topology. As Meshing warned, two solids that merely touch get two independent meshes — coincident nodes, no connection, a silently broken load path. The assembly-level fix is one call:
Fragmentation is a boolean operation that splits every shape at every
intersection, so each contact interface becomes a single shared surface in the
topology and the mesher produces one set of nodes on it. Three things about
the apeGmsh version are worth knowing. First, it fragments every dimension
present at once — a 2D shell wall standing on a 3D foundation becomes
conformal against the volume's face, not silently dropped (pass dim= to
restrict it). Second, it is registry-aware: OCC renumbers entities during the
operation, and fragment_all() rewrites every instance's entity records in
place, so labels, physical groups, and instance addressing all keep working on
the post-fragment geometry. Third, it warns about entities not tracked by
any instance — those participate in the boolean but can't be remapped, so
adopt strays with from_model() or register() before fragmenting. When only
one interface needs stitching, g.parts.fragment_pair("beam_top", "col_1")
does the same thing between two named instances and leaves the rest alone.
Fragmenting keeps the parts distinct — separate bodies that share nodes at their interfaces, each still addressable by its own label. Sometimes that's wrong: two placed volumes are really one continuous body, and the boundary between them is an artifact of how you modeled it. Then you fuse:
fuse_group dissolves the internal interfaces entirely and replaces the
listed instances with one new one — one body, one label, no interface to mesh.
The decision rule: fragment when the parts must remain distinct (different
materials or sections meeting at an interface), fuse when the split was never
physical to begin with. And when the two sides can't share a mesh — a coarse
solid against a fine one, a beam inside a volume — you do neither: leave the
meshes non-matching and declare a tie, as covered in
Tie non-matching meshes.
Composing finished models¶
Everything above happens before the mesh: Parts contribute geometry, the
session stitches it, and one mesh covers the whole. apeGmsh has a second,
later seam with a deliberately similar shape — g.compose merges a
previously saved model (model.h5) into a host session as a module:
already meshed, with its own loads, masses, and constraints resolved. Where a
Part is a geometry template, a module is a finished sub-model; where
instances get prefixed labels, composed modules get prefixed physical groups
(the host keeps its bare names), and the interfaces between modules are
coupled by constraints rather than fragmented — by compose time there is no
geometry left to fragment. The Assembly builder (imported from
apeGmsh.assembly, deliberately not top-level — the session is the
assembly) is a declarative front over the same machinery: declare saved
models as parts, declare the interface couples, and materialize the graph
into one composed session.
One consequence of the two seams is worth stating as a rule, because the
choice is sometimes forced rather than stylistic. Element order is global
within a gmsh session — set_order applies to the whole mesh — so the
geometry seam can never produce hex20 ribs next to hex8 covers, no matter how
the Parts are arranged. If parts need different element orders (or you want
each part meshed, saved, and iterated independently), the compose seam is the
route: one full session per part, each with its own recipe and order, saved
to its own model.h5, assembled with compose or Assembly, interfaces
tied. Don't reach for Part there — a Part is a geometry template and
cannot mesh, which is a deliberate contract (ADR 0085), not a gap awaiting a
feature.
Concept-level, that's the whole map: Parts compose geometry before meshing
inside one session; g.compose composes finished models across sessions. The
mechanics live in Compose saved modules, and
the in-session workflow — Parts, placement, fragmentation, all the way to a
solved frame — is walked end to end in
Multipart assembly.
Next: Sections.