Working with Parts to Build an Assembly¶
apeGmsh follows an Abaqus-inspired Part/Assembly philosophy: geometry is built in isolated Parts, then imported and combined inside a apeGmsh session that acts as the assembly. This separation keeps geometry reusable, meshing independent, and constraints declarative.
Core Concepts¶
Part is a pure geometry builder. It owns an isolated Gmsh session where you create points, curves, surfaces, and volumes. It has no meshing, no physical groups, and no solver awareness. When you are done, you export the geometry to a STEP file.
apeGmsh (the main session) is the assembly. It imports Parts, positions them, fragments interfaces so the mesh is conformal, generates the mesh, defines constraints, and exports to a solver. Everything that happens after geometry is done—meshing, physical groups, mesh selections, constraints, OpenSees export—lives here.
STEP is the contract between the two. STEP preserves the full parametric OCC geometry (exact NURBS, topology, tolerances), so the assembly can re-mesh the imported geometry with any settings, apply boolean operations, and set transfinite or structured meshing after the fact.
Phase 1 — Define Parts¶
Each Part runs in its own Gmsh session. You build geometry through the familiar
part.model API, then save to disk.
from apeGmsh import Part
# ── Column ────────────────────────────────────────────────
column = Part("column")
column.begin()
column.model.geometry.add_box(0, 0, 0, 0.5, 0.5, 3.0)
column.properties["material"] = "concrete"
column.properties["section"] = "500x500"
column.save() # → column.step
column.end()
# ── Beam ──────────────────────────────────────────────────
beam = Part("beam")
beam.begin()
beam.model.geometry.add_box(0, 0, 0, 6.0, 0.3, 0.5)
beam.properties["material"] = "concrete"
beam.properties["section"] = "300x500"
beam.save() # → beam.step
beam.end()
Key points:
Part.modelgives you the full OCC geometry API (add_point, add_line, add_box, add_cylinder, extrude, etc.).Part.propertiesis a free-form dict for metadata (material, section type, thickness, etc.) that carries through to the assembly.Part.save()defaults to"{name}.step". You can pass a custom path or force IGES format withfmt="iges".- Always call
part.end()when done to release the Gmsh session.
Phase 2 — Create the Assembly Session¶
Open a apeGmsh session and import parts via g.parts. There are multiple entry
points depending on where your geometry comes from.
Entry Point A — Import a Part object¶
The most common path. The Part must be saved to disk first.
from apeGmsh import apeGmsh
g = apeGmsh("frame")
g.begin()
# Place two columns
g.parts.add(column, label="col_left", translate=(0, 0, 0))
g.parts.add(column, label="col_right", translate=(5.5, 0, 0))
# Place beam on top
g.parts.add(beam, label="beam_top", translate=(0, 0, 3.0))
The add() method imports the Part's STEP file into the current Gmsh session,
applies translation and rotation, and registers the resulting entities under a
label. If you omit label, it auto-generates one as "{part.name}_1",
"{part.name}_2", etc.
Entry Point B — Import a STEP file directly¶
When you have a CAD file from an external tool (FreeCAD, Rhino, SolidWorks):
Entry Point C — Inline geometry (context manager)¶
For parts that don't need reuse, build geometry directly in the assembly session and wrap it in a tracking block:
Everything created inside the with block is automatically recorded as a named
instance.
Entry Point D — Adopt existing geometry¶
After loading a STEP through g.model.io.load_step(), retroactively tag the
imported entities:
You can also adopt specific entities by dimension and tag:
Entry Point E — Manual registration¶
Tag entities you already have in hand:
Phase 3 — Positioning¶
Translation and rotation are applied at import time through keyword arguments on
add() and import_step().
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 format is (angle_rad, ax, ay, az) for rotation about the origin, or
(angle_rad, ax, ay, az, cx, cy, cz) to specify a custom center of rotation.
Rotation is applied before translation.
For post-import edits and array generation, every Instance exposes an inst.edit
composite (and every Part a part.edit composite) with translate, rotate,
mirror, copy, pattern_linear, pattern_polar, and align_to methods.
Phase 4 — Fragment for Conformal Meshing¶
When two parts share an interface (a beam sitting on a column, a slab touching a
wall), their meshes must be conformal at that interface. fragment_all() performs
a boolean fragmentation that splits entities at their intersections and produces a
single conformal topology.
This automatically detects the highest dimension present and fragments at that level. Instance entity tags are updated in-place, so all subsequent operations (physical groups, mesh generation, constraint resolution) see the post-fragment tags.
For selective fragmentation between two specific parts:
Important: Any entities in the session that are not tracked by a part label
will trigger a warning during fragmentation. Use g.parts.from_model() or
g.parts.register() to adopt orphan entities before fragmenting.
Phase 5 — Physical Groups and Mesh Generation¶
Promote part labels to physical groups¶
The simplest path—promote each part label to a physical group of the same name:
g.labels.promote_to_physical("col_left")
g.labels.promote_to_physical("col_right")
g.labels.promote_to_physical("beam_top")
g.labels.promote_to_physical("foundation")
Manual physical groups¶
For finer control (e.g., tagging surfaces for boundary conditions), use the
standard g.physical API:
Mesh generation¶
At this point you can also apply transfinite meshing, recombine quads, set mesh
fields, or use any other Gmsh meshing feature through g.mesh or direct
gmsh.model.mesh calls.
Phase 6 — Mesh Selections (Post-Mesh Named Sets)¶
After meshing, you may need named sets of nodes or elements that don't correspond
to geometry (e.g., "all nodes on plane Z=3.0" or "all elements inside a box").
g.mesh_selection provides spatial queries:
# Nodes at the beam-column interface
g.mesh_selection.add_nodes(name="interface_nodes", on_plane=("z", 3.0))
# Elements inside a region
g.mesh_selection.add_elements(dim=3, name="core_elements",
in_box=(1, 0, 0, 5, 0.5, 3))
# Set algebra
g.mesh_selection.union(0, tag_a, tag_b, name="all_support_nodes")
Mesh selections use the same (dim, tag) + name identity as physical groups but
live in an independent namespace.
Phase 7 — Constraints¶
Constraints follow a two-stage pipeline: define before or after meshing (they are geometric intent), then resolve after meshing to get concrete node pairs and weights.
Define constraints¶
Constraint labels can reference part labels, physical group names, or mesh selection names—the resolver handles all three transparently.
# Node-to-node: tie beam to columns at shared interface
g.constraints.equal_dof("beam_top", "col_left", tolerance=1e-3)
g.constraints.equal_dof("beam_top", "col_right", tolerance=1e-3)
# Rigid diaphragm for a floor slab
g.constraints.rigid_diaphragm("slab", "frame",
master_point=(3.0, 3.0, 3.5))
# Surface-to-surface tie
g.constraints.tie("beam_top", "slab", tolerance=0.5)
Available constraint types span four levels: node-to-node (equal_dof, rigid_link, penalty), node-to-group (rigid_diaphragm, rigid_body, kinematic_coupling), node-to-surface (tie, distributing_coupling, embedded), and surface-to-surface (tied_contact, mortar).
Resolve constraints¶
Resolution needs the mesh data and spatial maps:
fem = g.mesh.queries.get_fem_data(dim=3)
node_map = g.parts.build_node_map(fem.nodes.ids, fem.nodes.coords)
face_map = g.parts.build_face_map(node_map)
records = g.constraints.resolve(
fem.nodes.ids, fem.nodes.coords,
node_map=node_map, face_map=face_map,
)
Each record contains the concrete master/slave node tags, DOFs, and weights needed by the solver.
Phase 8 — Solver Export¶
OpenSees¶
The OpenSees bridge (apeSees) is a post-session object constructed from the
FEMData snapshot. Close (or exit) the apeGmsh session first, then build the
OpenSees model explicitly:
from apeGmsh.opensees import apeSees
fem = g.mesh.queries.get_fem_data(dim=3)
# ... exit the session (g.end() or leave the with block) ...
ops = apeSees(fem)
ops.model(ndm=3, ndf=3)
# ... materials, elements, fix, patterns ...
ops.tcl("frame_model.tcl")
ops.py("frame_model.py")
Loads, masses, and SP boundary conditions declared on the session via
g.loads / g.masses / g.constraints are not ingested automatically.
Re-declare them explicitly on ops (see skills/apegmsh/references/opensees-bridge.md).
Direct FEM data access¶
For custom solvers or post-processing:
fem = g.mesh.queries.get_fem_data(dim=3)
fem.nodes.ids # 1-based contiguous IDs
fem.nodes.coords # (N, 3) array
fem.elements.ids # 1-based contiguous IDs
fem.elements.connectivity # (E, nodes_per_element) array
fem.nodes.physical # PhysicalGroupSet — query by name or tag
fem.mesh_selection # MeshSelectionStore — query by name or tag
Closing the Session¶
Complete Example — Portal Frame¶
from apeGmsh import Part, apeGmsh
# ── Define reusable parts ─────────────────────────────────
column = Part("column")
column.begin()
column.model.geometry.add_box(0, 0, 0, 0.5, 0.5, 3.0)
column.save()
column.end()
beam = Part("beam")
beam.begin()
beam.model.geometry.add_box(0, 0, 0, 6.0, 0.3, 0.5)
beam.save()
beam.end()
# ── Assemble ──────────────────────────────────────────────
g = apeGmsh("portal_frame")
g.begin()
g.parts.add(column, label="col_left", translate=(0, 0, 0))
g.parts.add(column, label="col_right", translate=(5.5, 0, 0))
g.parts.add(beam, label="beam_top", translate=(0, 0, 3.0))
g.parts.fragment_all()
# Physical groups (one per part label)
g.labels.promote_to_physical("col_left")
g.labels.promote_to_physical("col_right")
g.labels.promote_to_physical("beam_top")
# Mesh
g.mesh.sizing.set_size_global(min_size=50, max_size=150)
g.mesh.generation.generate(dim=3)
# Constraints
g.constraints.equal_dof("beam_top", "col_left", tolerance=1e-3)
g.constraints.equal_dof("beam_top", "col_right", tolerance=1e-3)
# Resolve fem
fem = g.mesh.queries.get_fem_data(dim=3)
node_map = g.parts.build_node_map(fem.nodes.ids, fem.nodes.coords)
face_map = g.parts.build_face_map(node_map)
g.constraints.resolve(fem.nodes.ids, fem.nodes.coords,
node_map=node_map, face_map=face_map)
g.end()
# OpenSees — post-session, explicit declarations.
from apeGmsh.opensees import apeSees
ops = apeSees(fem)
ops.model(ndm=3, ndf=3)
# ... materials, elements, fix, patterns (re-declare explicitly) ...
ops.py("portal_frame.py")
Summary of the Pipeline¶
Part apeGmsh (Assembly)
───── ─────────────────
1. Build geometry 3. Import & position parts
2. Save to STEP 4. Fragment interfaces
5. Define physical groups
6. Generate mesh
7. Create mesh selections
8. Define & resolve constraints
9. Export to solver
Part is optional—you can skip it entirely and build geometry inline with
g.parts.part("name") or directly through g.model. The Part workflow shines
when you have reusable components (a standard column section, a parametric beam,
a precast panel) that appear multiple times in the model or across projects.