apeGmsh constraints¶
A guide to defining multi-point constraints in an apeGmsh session —
from simple DOF ties to surface coupling and mortar methods. This
document covers the apeGmsh abstraction; see guide_fem_broker.md for
how constraints land in the broker and get consumed by a solver.
Grounded in the current source:
src/apeGmsh/core/ConstraintsComposite.py— the user-facing compositesrc/apeGmsh/solvers/_constraint_defs.py—ConstraintDefsubclassessrc/apeGmsh/solvers/_constraint_records.py—ConstraintRecordsubclassessrc/apeGmsh/solvers/_constraint_resolver.py—ConstraintResolversrc/apeGmsh/solvers/_constraint_geom.py— geometry helpers shared by the resolversrc/apeGmsh/solvers/_kinds.py—ConstraintKind(andLoadKind) constantssrc/apeGmsh/solvers/Constraints.py— re-export shim that surfaces the names above under the historical import pathsrc/apeGmsh/mesh/_record_set.py—NodeConstraintSet,SurfaceConstraintSet
All snippets assume an open session:
from apeGmsh import apeGmsh
g = apeGmsh(model_name="demo")
g.begin()
# ... geometry, parts, mesh ...
1. The two-stage pipeline: define, then resolve¶
Constraints follow the same pattern as loads and masses. You define constraints before meshing against high-level targets (part labels, physical groups), and the library resolves them to node-level records after the mesh exists.
# Stage 1 — define (pre-mesh)
g.constraints.equal_dof("slab", "column_top", dofs=[1, 2, 3])
# Stage 2 — resolve (happens inside get_fem_data)
fem = g.mesh.queries.get_fem_data(dim=3)
# Resolved records are now on the broker
fem.nodes.constraints # NodeConstraintSet (node-pair types)
fem.elements.constraints # SurfaceConstraintSet (surface types)
The definitions reference names, not tags — so they survive remeshing.
2. DOF numbering¶
All constraint definitions use 1-based DOF numbering:
| DOF | Meaning |
|---|---|
| 1 | ux — translation X |
| 2 | uy — translation Y |
| 3 | uz — translation Z |
| 4 | rx — rotation X |
| 5 | ry — rotation Y |
| 6 | rz — rotation Z |
For a 3-DOF model (ndf=3, solids), only DOFs 1-3 exist. For a 6-DOF model (ndf=6, frames/shells), all 6 are available.
3. Constraint types¶
Level 1 — Node-to-node¶
equal_dof — tie co-located nodes so they share selected DOFs.
The most common constraint. Finds node pairs within a tolerance and
couples them.
g.constraints.equal_dof(
"slab", "column_top",
dofs=[1, 2, 3], # couple translations only
tolerance=1e-6,
)
Use this when two parts share a boundary and you want continuity of
displacement across it. If you omit dofs, all DOFs are tied.
rigid_link — rigid bar coupling between a master node (or point)
and slave nodes. Two types:
# "beam" — full 6-DOF rigid body motion (default)
g.constraints.rigid_link("center", "perimeter", link_type="beam")
# "rod" — translations only, rotations free
g.constraints.rigid_link("center", "perimeter", link_type="rod")
Use rigid_link for connecting a single master (e.g., column centroid)
to a ring of slave nodes. The difference from equal_dof is that
rigid links enforce rigid body kinematics — rotations at the master
produce translations at the slaves proportional to their offset.
penalty — soft spring approximation of equal_dof. Useful when
the solver has trouble with algebraic constraints (Lagrange multipliers)
and a spring coupling is more stable:
The stiffness should be large enough to be effectively rigid but not so large that it causes ill-conditioning. A rule of thumb: 10x to 1000x the stiffest element in your model.
Level 2 — Node-to-group¶
rigid_diaphragm — enforces in-plane rigidity at a floor level.
All slave nodes at the plane follow the master node's in-plane DOFs:
g.constraints.rigid_diaphragm(
"center_col", # master label (the reference node)
"floor_nodes", # slave label (all floor nodes)
plane_normal=(0, 0, 1),
plane_tolerance=0.5, # tolerance for "at the plane"
)
This is the classic floor-diaphragm constraint for building models.
The plane_tolerance filters slave nodes to only those within the
specified distance of the diaphragm plane.
rigid_body — full 6-DOF rigid body constraint. Every slave
follows the master as if welded:
Use this for modeling rigid blocks, pile caps, or foundation mats that are much stiffer than the surrounding structure.
kinematic_coupling — generalized master-slave. You specify which
DOFs couple:
Level 2b — Mixed-DOF coupling¶
node_to_surface — couples a 6-DOF node (e.g., a frame node) to a
3-DOF surface mesh (e.g., a solid). This is a compound constraint that
creates phantom nodes:
- Duplicate slave positions as phantom nodes (6-DOF)
- Rigid link from master to each phantom
- EqualDOF from each phantom to the original slave (translations only)
When master is a string it is resolved via resolve_to_tags(..., dim=0) —
i.e. the name must refer to a geometric point entity (a vertex), not a
curve/surface/volume label. slave, in contrast, is resolved at dim=2
(surface entities). See ConstraintsComposite.py:837.
Note: node_to_surface (and embedded, below) bypass the strict
part-label validation that other constraint factories run in
_add_def — both definition types are allowed to carry bare entity
tags / mixed labels because they are looked up later through their
own resolvers (ConstraintsComposite.py:236).
For pure BC application (force/moment or prescribed displacement on a
face without a structural element at the reference), prefer
g.loads.face_load() / g.loads.face_sp() instead — they distribute
directly to face nodes and avoid the phantom node conditioning issue.
See guide_loads.md §10–11.
This is the constraint you need when connecting a beam/frame model to a solid model. The phantom nodes bridge the DOF mismatch.
node_to_surface_spring — same topology as node_to_surface, same
call signature, but the master → phantom links are emitted as stiff
elasticBeamColumn elements instead of kinematic rigidLink('beam', ...)
constraints:
Use this variant when the master carries free rotational DOFs (e.g. a
fork support on a solid end face) that receive direct moment loading. The
constraint-based node_to_surface can produce an ill-conditioned reduced
stiffness matrix in that case because the master rotation DOFs only get
stiffness through kinematic constraint back-propagation, with nothing
attaching directly to them. Stiff beams give those rotations a real
elastic stiffness path and the matrix conditioning recovers.
Pick node_to_surface when the master rotations are themselves
constrained or carry no moment; pick node_to_surface_spring when the
master is a free rotation node receiving moments.
Emission contrast. The plain node_to_surface variant emits its
master→phantom links through fem.nodes.constraints.phantom_nodes()
plus pairs() (kinematic rigidLink('beam', ...)). The spring
variant routes the same topology through
fem.nodes.constraints.stiff_beam_groups() instead — each group
becomes one elasticBeamColumn element per master/phantom pair. The
factory entry point lives at ConstraintsComposite.py:906.
Level 3 — Surface coupling¶
tie — surface tie via shape function interpolation. Slave nodes
project onto the closest master face and their DOFs are interpolated
from the master face nodes:
Use tie when two non-matching meshes share a boundary and you want
displacement continuity without requiring conformal meshing. The
tolerance controls how far a slave node can be from the master surface.
distributing_coupling — distributes a point load from a master
to a slave surface:
g.constraints.distributing_coupling(
"load_point", "bearing_surface",
weighting="area", # or "uniform"
)
embedded — embedded element constraint (reinforcement in concrete):
Slave (embedded) nodes follow the displacement field of the host elements.
Implemented end-to-end: at resolution time _resolve_embedded
(ConstraintsComposite.py:1218) collects the host tet4/tri3 elements
and the embedded-curve nodes, drops embedded nodes that coincide with
a host corner (already rigidly attached via shared connectivity), and
hands the remainder to resolver.resolve_embedded which lands real
InterpolationRecords on fem.elements.constraints.
Level 4 — Surface-to-surface¶
tied_contact — full surface-to-surface tie. Bidirectional check:
More robust than tie for large non-matching meshes because it checks
projections in both directions.
mortar — mortar coupling via Lagrange multiplier space. The
mathematically rigorous option for non-matching meshes:
Use mortar when you need optimal convergence rates in the constraint
error (e.g., for academic benchmarks or problems where the interface
accuracy matters). For practical engineering, tied_contact or tie
are usually sufficient.
4. How constraints land in the broker¶
After get_fem_data(), constraints are split across two composites
based on what solver commands they produce:
Node-level (fem.nodes.constraints):
- equal_dof → NodePairRecord
- rigid_link → NodePairRecord
- penalty → NodePairRecord
- rigid_diaphragm → NodeGroupRecord (expands to pairs)
- rigid_body → NodeGroupRecord
- kinematic_coupling → NodeGroupRecord
- node_to_surface → NodeToSurfaceRecord (phantom nodes + pairs)
- node_to_surface_spring → NodeToSurfaceRecord (phantom nodes + stiff-beam pairs)
Surface-level (fem.elements.constraints):
- tie → InterpolationRecord
- distributing_coupling → InterpolationRecord
- embedded → InterpolationRecord
- tied_contact → SurfaceCouplingRecord
- mortar → SurfaceCouplingRecord
5. Consuming constraints in a solver¶
Use the Kind constants for linter-friendly comparisons — no magic
strings:
K = fem.nodes.constraints.Kind
# 1. Create phantom nodes first
for nid, xyz in fem.nodes.constraints.phantom_nodes():
ops.node(nid, *xyz)
# 2. Emit node-pair constraints (compound records expanded automatically)
for c in fem.nodes.constraints.pairs():
if c.kind == K.RIGID_BEAM:
ops.rigidLink("beam", c.master_node, c.slave_node)
elif c.kind == K.RIGID_ROD:
ops.rigidLink("rod", c.master_node, c.slave_node)
elif c.kind == K.EQUAL_DOF:
ops.equalDOF(c.master_node, c.slave_node, *c.dofs)
elif c.kind == K.PENALTY:
# custom penalty spring element
...
# 3. Surface constraints
for interp in fem.elements.constraints.interpolations():
# build multi-point constraint from interpolation weights
# interp.slave_node, interp.master_nodes, interp.weights
...
6. Introspection¶
# What constraints do I have?
print(fem.inspect.constraint_summary())
# Node constraints (12 records):
# equal_dof 8 (source: 'slab_column_tie')
# rigid_beam 4 (source: 'node_to_surface coupling')
# phantom nodes 4 (created by node_to_surface)
# Surface constraints (2 records):
# tie 2 (source: 'flange_web_tie')
# DataFrame summaries
fem.nodes.constraints.summary() # kind, count, n_node_pairs
fem.elements.constraints.summary() # kind, count, n_interpolations
7. Guidelines¶
- Start with
equal_dof— it covers 80% of constraint needs. - Use
tiefor non-matching meshes — it handles mesh incompatibility without conformal remeshing. - Use
node_to_surfacefor beam-to-solid — it bridges the DOF mismatch automatically. - Prefer
tied_contactovermortarfor practical work — mortar is more accurate but harder to debug. - Check
phantom_nodes()— if you havenode_to_surfaceconstraints, phantom nodes must be created in the solver before emitting constraints. - Set
tolerancecarefully — too tight and no pairs are found; too loose and you couple nodes that shouldn't be coupled.
See also¶
guide_fem_broker.md— how the broker organizes constraintsguide_loads.md— the analogous two-stage pipeline for loadsguide_basics.md— session lifecycle