Skip to content

Constraints — g.constraints

Solver-agnostic kinematic-constraint engine. Constraints are declared on geometry (part labels, optional entity scopes) and resolved on the mesh by g.mesh.queries.get_fem_data.

Two-stage pipeline

Stage 1 — declare before meshing. The factory methods on g.constraints (equal_dof, rigid_link, tie, …) store ConstraintDef dataclasses describing intent at the geometry level. These definitions carry no node tags and survive remeshing.

Stage 2 — resolve after meshing. ConstraintResolver walks the def list and produces concrete ConstraintRecord objects (actual node tags, weights, offset vectors). Records land on the FEM broker:

Record family Lives on
NodePairRecord fem.nodes.constraints
NodeGroupRecord fem.nodes.constraints
NodeToSurfaceRecord fem.nodes.constraints
InterpolationRecord fem.elements.constraints
SurfaceCouplingRecord fem.elements.constraints

Constraint taxonomy

Five tiers, ordered by topology:

Tier Methods Record family
1 — Pair equal_dof, rigid_link, penalty NodePairRecord
2 — Group rigid_diaphragm, rigid_body, kinematic_coupling NodeGroupRecord
2b — Mixed node_to_surface, node_to_surface_spring NodeToSurfaceRecord
3 — Surface tie, distributing_coupling, embedded InterpolationRecord
4 — Contact tied_contact, mortar SurfaceCouplingRecord

All constraints ultimately express the linear MPC equation u_slave = C · u_master. Tiers differ in how C is built: node co-location (Tier 1), kinematic transformation around a master point (Tier 2), shape-function interpolation (Tier 3), or numerical integration on the interface (Tier 4).

Target identification

Most methods identify their master and slave sides by part label (a key of g.parts._instances). _add_def validates both labels against the registry and raises KeyError on a typo.

Optional master_entities / slave_entities arguments (lists of (dim, tag)) narrow the search to a subset of the part's entities — useful when a part has many surfaces and only one is the interface.

Exceptions to the part-label scheme:

  • node_to_surface and node_to_surface_spring take bare tags instead — the master is a Gmsh point entity (dim=0) and the slave is one or more surface entities (dim=2).
  • embedded uses host_label / embedded_label to mirror Abaqus's vocabulary; the lookup logic otherwise matches the part-label scheme.

Worked example

from apeGmsh import apeGmsh

with apeGmsh(model_name="frame") as g:
    # ... geometry + Parts already imported ...

    # Tier 1 — co-located nodes share x/y/z
    g.constraints.equal_dof("col", "beam", dofs=[1, 2, 3])

    # Tier 2 — slab nodes follow a centre-of-mass node
    g.constraints.rigid_diaphragm(
        "slab", "slab_master",
        master_point=(2.5, 2.5, 3.0),
        plane_normal=(0, 0, 1),
    )

    # Tier 3 — non-matching shell-to-solid interface
    g.constraints.tie(
        "shell_floor", "solid_column",
        master_entities=[(2, 17)],
        slave_entities=[(2, 41)],
        tolerance=5.0,
    )

    g.mesh.generation.generate(dim=3)
    fem = g.mesh.queries.get_fem_data(dim=3)

    # Grouped emission — accumulates rigid_beam / rigid_diaphragm /
    # node_to_surface phantom links by master node.
    for master, slaves in fem.nodes.constraints.rigid_link_groups():
        for slave in slaves:
            ops.rigidLink("beam", master, slave)

Composite

apeGmsh.core.ConstraintsComposite.ConstraintsComposite

ConstraintsComposite(parent: '_ApeGmshSession')

Solver-agnostic kinematic-constraint composite — declare on geometry, resolve to nodes after meshing.

Two-stage pipeline
  1. Declare (pre-mesh): the factory methods on this composite (equal_dof, rigid_link, rigid_diaphragm, tie, …) store :class:~apeGmsh.solvers.Constraints.ConstraintDef dataclasses describing intent at the geometry level. Defs carry no node tags and survive remeshing.
  2. Resolve (post-mesh): :meth:resolve (called automatically by :meth:Mesh.queries.get_fem_data) walks the def list and hands each one to :class:~apeGmsh.solvers.Constraints.ConstraintResolver, which produces concrete :class:~apeGmsh.solvers.Constraints.ConstraintRecord objects — actual node tags, weights, and offset vectors.

The resolved records land on the FEM broker:

  • node-pair / node-group / node_to_surface records → fem.nodes.constraints
  • surface-coupling / interpolation records → fem.elements.constraints
Constraint taxonomy

Five tiers, ordered by topology and the role each plays in a structural model:

============= ===================================================== ================================= Tier Methods Record family ============= ===================================================== ================================= 1 — Pair :meth:equal_dof, :meth:rigid_link, NodePairRecord :meth:penalty 2 — Group :meth:rigid_diaphragm, :meth:rigid_body, NodeGroupRecord :meth:kinematic_coupling 2b — Mixed :meth:node_to_surface, NodeToSurfaceRecord :meth:node_to_surface_spring (+ phantom nodes) 3 — Surface :meth:tie, :meth:distributing_coupling, InterpolationRecord :meth:embedded 4 — Contact :meth:tied_contact, :meth:mortar SurfaceCouplingRecord ============= ===================================================== =================================

All constraints ultimately express the linear MPC equation u_slave = C · u_master. Tiers differ in how C is built — by node co-location (Tier 1), kinematic transformation around a master point (Tier 2), shape-function interpolation (Tier 3), or numerical integration on the interface (Tier 4).

Target identification

Most methods identify their master and slave sides by part label (a key of g.parts._instances). :meth:_add_def validates both labels against the registry and raises KeyError on a typo::

g.constraints.tie(master_label="column",
                  slave_label="slab",
                  master_entities=[(2, 13)],   # optional scope
                  slave_entities=[(2, 17)])

Optional master_entities / slave_entities (list of (dim, tag)) narrow the search to a subset of the part's entities — useful when a part has many surfaces and only one is the interface.

Exceptions to the part-label scheme ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  • :meth:node_to_surface and :meth:node_to_surface_spring take bare tags instead. The master is a Gmsh point entity (dim=0) and slave is one or more surface entities (dim=2). Both arguments accept int, str, or (dim, tag); label validation is skipped.
  • :meth:embedded uses host_label / embedded_label to mirror the host/embedded vocabulary, but the lookup logic otherwise matches the part-label scheme.
Resolution semantics

:meth:resolve is dependency-injected — it never imports PartsRegistry. The caller (typically Mesh.queries.get_fem_data) supplies:

  • node_map: {part_label → set[int]} of mesh node tags
  • face_map: {part_label → ndarray(F, n_per_face)} built only when surface constraints (Tier 3 / 4) are present.
See Also

apeGmsh.solvers.Constraints : Module-level taxonomy and theory. apeGmsh.solvers._constraint_defs : Stage-1 dataclasses with full per-method theory. apeGmsh.solvers._constraint_resolver.ConstraintResolver : Stage-2 implementation. apeGmsh.mesh._record_set.NodeConstraintSet : Iteration helpers (rigid_link_groups, equal_dofs, rigid_diaphragms, pairs).

Examples

Declare a mix of constraints, mesh, and read out grouped rigid-link masters for OpenSees emission::

with apeGmsh(model_name="frame") as g:
    # Tier 1 — co-located nodes share x/y/z
    g.constraints.equal_dof("col", "beam", dofs=[1, 2, 3])

    # Tier 2 — slab nodes follow the centre-of-mass node
    g.constraints.rigid_diaphragm(
        "slab", "slab_master",
        master_point=(2.5, 2.5, 3.0),
        plane_normal=(0, 0, 1),
    )

    # Tier 3 — non-matching shell-to-solid interface
    g.constraints.tie(
        "shell", "solid",
        master_entities=[(2, 17)],
        slave_entities=[(2, 41)],
    )

    g.mesh.generation.generate(dim=3)
    fem = g.mesh.queries.get_fem_data(dim=3)

    for master, slaves in fem.nodes.constraints.rigid_link_groups():
        for slave in slaves:
            ops.rigidLink("beam", master, slave)
Source code in src/apeGmsh/core/ConstraintsComposite.py
def __init__(self, parent: "_ApeGmshSession") -> None:
    self._parent = parent
    self.constraint_defs: list[ConstraintDef] = []
    self.constraint_records: list[ConstraintRecord] = []
    # Single-point constraints (BCDef) are kept apart from
    # constraint_defs: they carry no master/slave, are not in
    # _DISPATCH, and resolve to fem.nodes.sp (not
    # fem.nodes.constraints).  Mixing them into constraint_defs
    # would break _add_def validation and the resolve() dispatch.
    self._bc_defs: list[BCDef] = []

bc

bc(target=None, *, pg=None, label=None, tag=None, dofs=None, name=None) -> BCDef

Homogeneous single-point constraint — fix a pattern to ground.

The natural (essential / Dirichlet) boundary condition: every mesh node in the resolved pattern gets ops.fix(node, *mask) downstream. There is no master and no slave — unlike every other method on this composite, this is a constraint to ground, not between two parts. It resolves into fem.nodes.sp (homogeneous :class:SPRecord\ s) — the same broker channel as :meth:g.loads.face_spnot fem.nodes.constraints.

Because it is a permanent constraint (not a pattern-scoped quantity), it lives here on g.constraints rather than on g.loads: there is no load-pattern context to accidentally scope it into, and the downstream emitter places it in the model → bcs → patterns deck order via ops.fix.

Parameters

target : str or list[(dim, tag)] Pattern to fix. Resolved label → physical group → raw tags (or a mesh selection) — the same flexible target model as :meth:g.loads.face_sp. Pass pg= / label= / tag= instead to force a specific resolution path. dofs : list[int], optional Restraint mask (1 = constrained, 0 = free), in DOF order [ux, uy, uz, rx, ry, rz]. Default [1, 1, 1] (pin all translations). This is the OpenSees ops.fix / face_sp convention — not the index-list convention used by :meth:equal_dof (dofs=[1,2,3]). name : str, optional Friendly name shown in summaries / the viewer.

Returns

BCDef The stored definition; the same object is appended to self._bc_defs.

Warnings

Resolution is dimension-agnostic — a point, edge, surface, or volume pattern all just contribute their mesh nodes. Pointing a BC at a volume physical group therefore fixes every interior node of the solid, which is almost never intended; target a boundary surface/edge instead.

Examples

::

g.constraints.bc("base_face")                  # pin x,y,z
g.constraints.bc(pg="Supports", dofs=[1, 1, 0])
g.constraints.bc(label="col.base",
                 dofs=[1, 1, 1, 1, 1, 1])       # full fixity
Source code in src/apeGmsh/core/ConstraintsComposite.py
def bc(self, target=None, *, pg=None, label=None, tag=None,
       dofs=None, name=None) -> BCDef:
    """Homogeneous single-point constraint — fix a pattern to ground.

    The natural (essential / Dirichlet) boundary condition: every
    mesh node in the resolved pattern gets ``ops.fix(node, *mask)``
    downstream. There is **no master and no slave** — unlike every
    other method on this composite, this is a constraint *to
    ground*, not between two parts. It resolves into
    ``fem.nodes.sp`` (homogeneous :class:`SPRecord`\\ s) — the same
    broker channel as :meth:`g.loads.face_sp` — **not**
    ``fem.nodes.constraints``.

    Because it is a *permanent* constraint (not a pattern-scoped
    quantity), it lives here on ``g.constraints`` rather than on
    ``g.loads``: there is no load-pattern context to accidentally
    scope it into, and the downstream emitter places it in the
    ``model → bcs → patterns`` deck order via ``ops.fix``.

    Parameters
    ----------
    target : str or list[(dim, tag)]
        Pattern to fix. Resolved label → physical group → raw
        tags (or a mesh selection) — the same flexible target
        model as :meth:`g.loads.face_sp`. Pass ``pg=`` / ``label=``
        / ``tag=`` instead to force a specific resolution path.
    dofs : list[int], optional
        Restraint **mask** (``1`` = constrained, ``0`` = free), in
        DOF order ``[ux, uy, uz, rx, ry, rz]``. Default
        ``[1, 1, 1]`` (pin all translations). This is the
        OpenSees ``ops.fix`` / ``face_sp`` convention — **not**
        the index-list convention used by
        :meth:`equal_dof` (``dofs=[1,2,3]``).
    name : str, optional
        Friendly name shown in summaries / the viewer.

    Returns
    -------
    BCDef
        The stored definition; the same object is appended to
        ``self._bc_defs``.

    Warnings
    --------
    Resolution is dimension-agnostic — a point, edge, surface, or
    volume pattern all just contribute their mesh nodes. Pointing
    a BC at a **volume** physical group therefore fixes *every
    interior node of the solid*, which is almost never intended;
    target a boundary surface/edge instead.

    Examples
    --------
    ::

        g.constraints.bc("base_face")                  # pin x,y,z
        g.constraints.bc(pg="Supports", dofs=[1, 1, 0])
        g.constraints.bc(label="col.base",
                         dofs=[1, 1, 1, 1, 1, 1])       # full fixity
    """
    t, src = self._parent.loads._coalesce_target(
        target, pg=pg, label=label, tag=tag)
    defn = BCDef(target=t, target_source=src,
                 dofs=list(dofs) if dofs is not None else [1, 1, 1],
                 name=name)
    self._bc_defs.append(defn)
    return defn

resolve_bcs

resolve_bcs(node_tags, *, node_map=None) -> list

Resolve every :meth:bc def to homogeneous SPRecord\ s.

Mirrors the load/SP resolution path: each BCDef target is run through the loads composite's dimension-agnostic _target_nodes (label → PG → tag → mesh-selection, any dim), then one SPRecord(value=0.0, is_homogeneous=True) is emitted per restrained DOF per node.

Fails loud — consistent with :meth:_resolve_nodes and the resolver contract — if a pattern resolves to zero mesh nodes: a BC that silently binds nothing is worse than one that errors.

Source code in src/apeGmsh/core/ConstraintsComposite.py
def resolve_bcs(self, node_tags, *, node_map=None) -> list:
    """Resolve every :meth:`bc` def to homogeneous ``SPRecord``\\ s.

    Mirrors the load/SP resolution path: each ``BCDef`` target is
    run through the loads composite's dimension-agnostic
    ``_target_nodes`` (label → PG → tag → mesh-selection, any
    dim), then one ``SPRecord(value=0.0, is_homogeneous=True)`` is
    emitted per restrained DOF per node.

    Fails loud — consistent with :meth:`_resolve_nodes` and the
    resolver contract — if a pattern resolves to zero mesh nodes:
    a BC that silently binds nothing is worse than one that errors.
    """
    from apeGmsh._kernel.records._loads import SPRecord

    if not self._bc_defs:
        return []

    loads = self._parent.loads
    all_nodes = {int(t) for t in node_tags}
    out: list = []
    for defn in self._bc_defs:
        nodes = loads._target_nodes(
            defn.target, node_map or {}, all_nodes,
            source=defn.target_source, expected_dim=None,
        )
        if not nodes:
            raise ValueError(
                f"bc: target {defn.target!r} (source="
                f"{defn.target_source!r}) resolved to zero mesh "
                f"nodes — check it is meshed and names a real "
                f"label / physical group / entity. Refusing to "
                f"emit an empty boundary condition.")
        for nid in sorted(nodes):
            for d_idx, mask in enumerate(defn.dofs):
                if mask != 1:
                    continue
                out.append(SPRecord(
                    name=defn.name,
                    node_id=int(nid),
                    dof=d_idx + 1,
                    value=0.0,
                    is_homogeneous=True,
                ))
    return out

equal_dof

equal_dof(master_label, slave_label, *, master_entities=None, slave_entities=None, dofs=None, tolerance=1e-06, name=None) -> EqualDOFDef

Tie matching DOFs between co-located node pairs.

At resolution time the resolver finds every master node whose coordinates match a slave node within tolerance and emits one :class:~apeGmsh.solvers.Constraints.NodePairRecord per match. Each pair becomes ops.equalDOF(master, slave, *dofs) downstream — i.e. u_slave[i] = u_master[i] for every i in dofs.

Use this for conformal interfaces only — meshes that share nodes at the boundary. For non-matching meshes use :meth:tie.

Parameters

master_label : str Part label whose nodes drive the constraint. slave_label : str Part label whose matching nodes are slaved. master_entities, slave_entities : list of (dim, tag), optional Restrict the node search to specific Gmsh entities of each side. Useful when only one face of a multi-face part is the interface. dofs : list[int], optional 1-based DOF indices to constrain (1=ux, 2=uy, 3=uz, 4=rx, 5=ry, 6=rz). None (default) means all DOFs available — the actual count depends on the model's ndf. tolerance : float, default 1e-6 Maximum distance (in model units) between two nodes for them to be treated as co-located. Unit-sensitive: 1e-3 for millimetre models, 1e-6 for metre models. name : str, optional Friendly name shown in :meth:summary and the viewer.

Returns

EqualDOFDef The stored definition; the same object is appended to self.constraint_defs.

Raises

KeyError If master_label or slave_label is not in g.parts.

See Also

tie : Non-matching mesh equivalent (shape-function projection). rigid_link : Add a kinematic offset on top of co-location.

Examples

Translational continuity between a column and a beam at a joint::

g.constraints.equal_dof(
    "column", "beam",
    dofs=[1, 2, 3],
    tolerance=1e-3,        # mm model
)
Source code in src/apeGmsh/core/ConstraintsComposite.py
def equal_dof(self, master_label, slave_label, *, master_entities=None,
              slave_entities=None, dofs=None, tolerance=1e-6,
              name=None) -> EqualDOFDef:
    """Tie matching DOFs between **co-located** node pairs.

    At resolution time the resolver finds every master node
    whose coordinates match a slave node within ``tolerance``
    and emits one
    :class:`~apeGmsh.solvers.Constraints.NodePairRecord` per
    match. Each pair becomes ``ops.equalDOF(master, slave, *dofs)``
    downstream — i.e. ``u_slave[i] = u_master[i]`` for every
    ``i`` in ``dofs``.

    Use this for **conformal** interfaces only — meshes that share
    nodes at the boundary. For non-matching meshes use :meth:`tie`.

    Parameters
    ----------
    master_label : str
        Part label whose nodes drive the constraint.
    slave_label : str
        Part label whose matching nodes are slaved.
    master_entities, slave_entities : list of (dim, tag), optional
        Restrict the node search to specific Gmsh entities of
        each side. Useful when only one face of a multi-face
        part is the interface.
    dofs : list[int], optional
        1-based DOF indices to constrain (``1=ux, 2=uy, 3=uz,
        4=rx, 5=ry, 6=rz``). ``None`` (default) means *all DOFs
        available* — the actual count depends on the model's
        ``ndf``.
    tolerance : float, default 1e-6
        Maximum distance (in model units) between two nodes for
        them to be treated as co-located. **Unit-sensitive**:
        ``1e-3`` for millimetre models, ``1e-6`` for metre
        models.
    name : str, optional
        Friendly name shown in :meth:`summary` and the viewer.

    Returns
    -------
    EqualDOFDef
        The stored definition; the same object is appended to
        ``self.constraint_defs``.

    Raises
    ------
    KeyError
        If ``master_label`` or ``slave_label`` is not in
        ``g.parts``.

    See Also
    --------
    tie : Non-matching mesh equivalent (shape-function projection).
    rigid_link : Add a kinematic offset on top of co-location.

    Examples
    --------
    Translational continuity between a column and a beam at a
    joint::

        g.constraints.equal_dof(
            "column", "beam",
            dofs=[1, 2, 3],
            tolerance=1e-3,        # mm model
        )
    """
    return self._add_def(EqualDOFDef(
        master_label=master_label, slave_label=slave_label,
        master_entities=master_entities, slave_entities=slave_entities,
        dofs=dofs, tolerance=tolerance, name=name))
rigid_link(master_label, slave_label, *, link_type='beam', master_point=None, slave_entities=None, tolerance=1e-06, name=None) -> RigidLinkDef

Rigid bar between a master node and one or more slave nodes.

Each slave node is constrained to follow the master through a rigid offset arm r = x_slave − x_master::

link_type="beam":     u_s = u_m + θ_m × r,   θ_s = θ_m
link_type="rod":      u_s = u_m + θ_m × r,   θ_s free

Use "beam" for fully rigid kinematic offsets (eccentric connections, lumped-mass arms, fictitious rigid extensions). Use "rod" when you want to transmit translation but leave the slave free to rotate — e.g. pinned eccentric supports.

master_label : str Part label that owns the master node. The master is identified inside this part either by master_point (proximity match) or by being the unique node when the part collapses to a single point. slave_label : str Part label whose nodes become slaves. link_type : "beam" or "rod", default "beam" "beam" couples 6 DOFs with rotational offset; "rod" couples translations only. master_point : (x, y, z), optional Explicit master coordinates. If None, the resolver picks the master node by proximity within tolerance. slave_entities : list of (dim, tag), optional Restrict the slave node search to specific entities. tolerance : float, default 1e-6 Proximity tolerance for master-node detection. name : str, optional Friendly name.

RigidLinkDef

KeyError If either label is not in g.parts.

kinematic_coupling : Same idea, but lets you pick which DOFs to couple instead of the fixed beam/rod sets. node_to_surface : When the slave side has only translational DOFs (3-DOF solid nodes).

Lumped-mass arm at the top of a tower::

g.constraints.rigid_link(
    "tower_top", "lumped_mass",
    link_type="beam",
    master_point=(0, 0, 30.0),
)
Source code in src/apeGmsh/core/ConstraintsComposite.py
def rigid_link(self, master_label, slave_label, *, link_type="beam",
               master_point=None, slave_entities=None,
               tolerance=1e-6, name=None) -> RigidLinkDef:
    """Rigid bar between a master node and one or more slave nodes.

    Each slave node is constrained to follow the master through
    a rigid offset arm ``r = x_slave − x_master``::

        link_type="beam":     u_s = u_m + θ_m × r,   θ_s = θ_m
        link_type="rod":      u_s = u_m + θ_m × r,   θ_s free

    Use ``"beam"`` for fully rigid kinematic offsets (eccentric
    connections, lumped-mass arms, fictitious rigid extensions).
    Use ``"rod"`` when you want to transmit translation but leave
    the slave free to rotate — e.g. pinned eccentric supports.

    Parameters
    ----------
    master_label : str
        Part label that owns the master node. The master is
        identified inside this part either by ``master_point``
        (proximity match) or by being the unique node when the
        part collapses to a single point.
    slave_label : str
        Part label whose nodes become slaves.
    link_type : ``"beam"`` or ``"rod"``, default ``"beam"``
        ``"beam"`` couples 6 DOFs with rotational offset;
        ``"rod"`` couples translations only.
    master_point : (x, y, z), optional
        Explicit master coordinates. If ``None``, the resolver
        picks the master node by proximity within ``tolerance``.
    slave_entities : list of (dim, tag), optional
        Restrict the slave node search to specific entities.
    tolerance : float, default 1e-6
        Proximity tolerance for master-node detection.
    name : str, optional
        Friendly name.

    Returns
    -------
    RigidLinkDef

    Raises
    ------
    KeyError
        If either label is not in ``g.parts``.

    See Also
    --------
    kinematic_coupling : Same idea, but lets you pick which DOFs
        to couple instead of the fixed beam/rod sets.
    node_to_surface : When the slave side has only translational
        DOFs (3-DOF solid nodes).

    Examples
    --------
    Lumped-mass arm at the top of a tower::

        g.constraints.rigid_link(
            "tower_top", "lumped_mass",
            link_type="beam",
            master_point=(0, 0, 30.0),
        )
    """
    return self._add_def(RigidLinkDef(
        master_label=master_label, slave_label=slave_label,
        link_type=link_type, master_point=master_point,
        slave_entities=slave_entities, tolerance=tolerance, name=name))

penalty

penalty(master_label, slave_label, *, stiffness=10000000000.0, dofs=None, tolerance=1e-06, name=None) -> PenaltyDef

Soft-spring (penalty) coupling between co-located node pairs.

Numerically approximates :meth:equal_dof as stiffness → ∞. The resolver still requires master and slave nodes to be co-located within tolerance, but downstream the constraint is enforced by inserting a stiff spring element between each pair instead of a hard MPC.

Use this when:

  • The hard equal_dof constraint causes the constraint-handler to ill-condition the reduced stiffness matrix (typical with mismatched DOF spaces).
  • You want a tunable interface compliance — e.g. a soft contact at a bearing pad.
Parameters

master_label : str Part label of the master side. slave_label : str Part label of the slave side. stiffness : float, default 1e10 Penalty spring stiffness in force/length units. Pick ~3–6 orders of magnitude above the stiffest neighbouring element diagonal — overshoot causes ill-conditioning, undershoot leaks displacement. dofs : list[int], optional 1-based DOFs to penalise. None = all available. tolerance : float, default 1e-6 Spatial co-location tolerance. name : str, optional Friendly name.

Returns

PenaltyDef

Raises

KeyError If either label is not in g.parts.

See Also

equal_dof : Hard MPC equivalent (no tunable stiffness).

Source code in src/apeGmsh/core/ConstraintsComposite.py
def penalty(self, master_label, slave_label, *, stiffness=1e10,
            dofs=None, tolerance=1e-6, name=None) -> PenaltyDef:
    """Soft-spring (penalty) coupling between co-located node pairs.

    Numerically approximates :meth:`equal_dof` as
    ``stiffness → ∞``. The resolver still requires master and
    slave nodes to be co-located within ``tolerance``, but
    downstream the constraint is enforced by inserting a stiff
    spring element between each pair instead of a hard MPC.

    Use this when:

    * The hard ``equal_dof`` constraint causes the
      constraint-handler to ill-condition the reduced stiffness
      matrix (typical with mismatched DOF spaces).
    * You want a tunable interface compliance — e.g. a soft
      contact at a bearing pad.

    Parameters
    ----------
    master_label : str
        Part label of the master side.
    slave_label : str
        Part label of the slave side.
    stiffness : float, default 1e10
        Penalty spring stiffness in force/length units. Pick
        ~3–6 orders of magnitude above the stiffest neighbouring
        element diagonal — overshoot causes ill-conditioning,
        undershoot leaks displacement.
    dofs : list[int], optional
        1-based DOFs to penalise. ``None`` = all available.
    tolerance : float, default 1e-6
        Spatial co-location tolerance.
    name : str, optional
        Friendly name.

    Returns
    -------
    PenaltyDef

    Raises
    ------
    KeyError
        If either label is not in ``g.parts``.

    See Also
    --------
    equal_dof : Hard MPC equivalent (no tunable stiffness).
    """
    return self._add_def(PenaltyDef(
        master_label=master_label, slave_label=slave_label,
        stiffness=stiffness, dofs=dofs, tolerance=tolerance, name=name))

rigid_diaphragm

rigid_diaphragm(master_label, slave_label, *, master_point=(0.0, 0.0, 0.0), plane_normal=(0.0, 0.0, 1.0), constrained_dofs=None, plane_tolerance=1.0, name=None) -> RigidDiaphragmDef

In-plane rigid floor — slaves follow master in the diaphragm plane.

Classic use: each floor of a multi-storey building. All slab nodes within plane_tolerance of the diaphragm plane share in-plane translation and rotation about the out-of-plane axis with the master node, while remaining free in the out-of-plane direction.

Resolution emits a single :class:~apeGmsh.solvers.Constraints.NodeGroupRecord with one master and many slaves. Downstream this becomes ops.rigidDiaphragm(perpDirn, master, *slaves).

Parameters

master_label : str Part label that contains (or whose proximity will select) the master node — typically a centre-of-mass point. slave_label : str Part label whose nodes are gathered into the diaphragm. master_point : (x, y, z), default (0, 0, 0) Coordinates of the master node. Used to disambiguate when the master part has more than one node. plane_normal : (nx, ny, nz), default (0, 0, 1) Unit normal to the diaphragm plane. (0, 0, 1) is a horizontal floor; (0, 1, 0) is a vertical wall, etc. constrained_dofs : list[int], optional DOFs slaved to the master. Default for a horizontal floor (Z up) is [1, 2, 6] — ux, uy, rz. For a vertical wall use [1, 3, 5]. plane_tolerance : float, default 1.0 Perpendicular distance (in model units) from the diaphragm plane within which a slave node is collected. Unit-sensitive — set this to a fraction of slab thickness. name : str, optional Friendly name.

Returns

RigidDiaphragmDef

Raises

KeyError If either label is not in g.parts.

See Also

kinematic_coupling : When you need a different DOF subset than [1, 2, 6] and don't need plane filtering. rigid_body : When all 6 DOFs must follow the master.

Examples

A horizontal slab at z = 3.0 m::

g.constraints.rigid_diaphragm(
    "slab", "slab_master",
    master_point=(2.5, 2.5, 3.0),
    plane_normal=(0, 0, 1),
    constrained_dofs=[1, 2, 6],
    plane_tolerance=0.05,
)
Source code in src/apeGmsh/core/ConstraintsComposite.py
def rigid_diaphragm(self, master_label, slave_label, *,
                    master_point=(0., 0., 0.),
                    plane_normal=(0., 0., 1.),
                    constrained_dofs=None, plane_tolerance=1.0,
                    name=None) -> RigidDiaphragmDef:
    """In-plane rigid floor — slaves follow master in the
    diaphragm plane.

    Classic use: each floor of a multi-storey building. All
    slab nodes within ``plane_tolerance`` of the diaphragm
    plane share in-plane translation and rotation about the
    out-of-plane axis with the master node, while remaining
    free in the out-of-plane direction.

    Resolution emits a single
    :class:`~apeGmsh.solvers.Constraints.NodeGroupRecord`
    with one master and many slaves. Downstream this becomes
    ``ops.rigidDiaphragm(perpDirn, master, *slaves)``.

    Parameters
    ----------
    master_label : str
        Part label that contains (or whose proximity will
        select) the master node — typically a centre-of-mass
        point.
    slave_label : str
        Part label whose nodes are gathered into the diaphragm.
    master_point : (x, y, z), default (0, 0, 0)
        Coordinates of the master node. Used to disambiguate
        when the master part has more than one node.
    plane_normal : (nx, ny, nz), default (0, 0, 1)
        Unit normal to the diaphragm plane. ``(0, 0, 1)`` is a
        horizontal floor; ``(0, 1, 0)`` is a vertical wall, etc.
    constrained_dofs : list[int], optional
        DOFs slaved to the master. Default for a horizontal
        floor (Z up) is ``[1, 2, 6]`` — ux, uy, rz. For a
        vertical wall use ``[1, 3, 5]``.
    plane_tolerance : float, default 1.0
        Perpendicular distance (in model units) from the
        diaphragm plane within which a slave node is
        collected. **Unit-sensitive** — set this to a fraction
        of slab thickness.
    name : str, optional
        Friendly name.

    Returns
    -------
    RigidDiaphragmDef

    Raises
    ------
    KeyError
        If either label is not in ``g.parts``.

    See Also
    --------
    kinematic_coupling : When you need a different DOF subset
        than ``[1, 2, 6]`` and don't need plane filtering.
    rigid_body : When all 6 DOFs must follow the master.

    Examples
    --------
    A horizontal slab at z = 3.0 m::

        g.constraints.rigid_diaphragm(
            "slab", "slab_master",
            master_point=(2.5, 2.5, 3.0),
            plane_normal=(0, 0, 1),
            constrained_dofs=[1, 2, 6],
            plane_tolerance=0.05,
        )
    """
    return self._add_def(RigidDiaphragmDef(
        master_label=master_label, slave_label=slave_label,
        master_point=master_point, plane_normal=plane_normal,
        constrained_dofs=constrained_dofs or [1, 2, 6],
        plane_tolerance=plane_tolerance, name=name))

rigid_body

rigid_body(master_label, slave_label, *, master_point=(0.0, 0.0, 0.0), name=None) -> RigidBodyDef

Fully rigid cluster — every slave DOF follows the master.

All six DOFs (ux, uy, uz, rx, ry, rz) of every node in the slave part follow the master node through a rigid transformation::

u_s = u_m + θ_m × (x_s − x_m)
θ_s = θ_m

Use this for genuinely rigid pieces (bearing blocks, lumped rigid masses) where the slave region must not deform.

Parameters

master_label : str Part label that contains (or whose proximity selects) the master node. slave_label : str Part label whose nodes are gathered into the rigid body. master_point : (x, y, z), default (0, 0, 0) Coordinates of the master node. name : str, optional Friendly name.

Returns

RigidBodyDef

Raises

KeyError If either label is not in g.parts.

See Also

kinematic_coupling : Same topology but with a user-selectable DOF subset. rigid_diaphragm : In-plane variant with plane filtering.

Source code in src/apeGmsh/core/ConstraintsComposite.py
def rigid_body(self, master_label, slave_label, *,
               master_point=(0., 0., 0.), name=None) -> RigidBodyDef:
    """Fully rigid cluster — every slave DOF follows the master.

    All six DOFs (``ux, uy, uz, rx, ry, rz``) of every node in
    the slave part follow the master node through a rigid
    transformation::

        u_s = u_m + θ_m × (x_s − x_m)
        θ_s = θ_m

    Use this for genuinely rigid pieces (bearing blocks, lumped
    rigid masses) where the slave region must not deform.

    Parameters
    ----------
    master_label : str
        Part label that contains (or whose proximity selects)
        the master node.
    slave_label : str
        Part label whose nodes are gathered into the rigid
        body.
    master_point : (x, y, z), default (0, 0, 0)
        Coordinates of the master node.
    name : str, optional
        Friendly name.

    Returns
    -------
    RigidBodyDef

    Raises
    ------
    KeyError
        If either label is not in ``g.parts``.

    See Also
    --------
    kinematic_coupling : Same topology but with a user-selectable
        DOF subset.
    rigid_diaphragm : In-plane variant with plane filtering.
    """
    return self._add_def(RigidBodyDef(
        master_label=master_label, slave_label=slave_label,
        master_point=master_point, name=name))

kinematic_coupling

kinematic_coupling(master_label, slave_label, *, master_point=(0.0, 0.0, 0.0), dofs=None, name=None) -> KinematicCouplingDef

Generalised one-master-many-slaves coupling on a chosen DOF subset.

The "parent" of :meth:rigid_diaphragm and :meth:rigid_body — they are special cases with pre-set DOF lists. Use this directly when you need a non-standard combination, e.g.::

* vertical-only follower: dofs=[3]
* 2-D in-plane rigid:     dofs=[1, 2, 6]
* symmetry plane:         dofs=[1, 4, 5]

Resolution emits a single :class:~apeGmsh.solvers.Constraints.NodeGroupRecord. Downstream this is typically expanded to one ops.equalDOF per slave.

Parameters

master_label : str Part label that owns the master node. slave_label : str Part label whose nodes are slaved. master_point : (x, y, z), default (0, 0, 0) Coordinates of the master node. dofs : list[int], optional 1-based DOFs to couple. Default [1, 2, 3, 4, 5, 6] (full 6-DOF, equivalent to :meth:rigid_body). name : str, optional Friendly name.

Returns

KinematicCouplingDef

Raises

KeyError If either label is not in g.parts.

Source code in src/apeGmsh/core/ConstraintsComposite.py
def kinematic_coupling(self, master_label, slave_label, *,
                       master_point=(0., 0., 0.), dofs=None,
                       name=None) -> KinematicCouplingDef:
    """Generalised one-master-many-slaves coupling on a chosen
    DOF subset.

    The "parent" of :meth:`rigid_diaphragm` and :meth:`rigid_body`
    — they are special cases with pre-set DOF lists. Use this
    directly when you need a non-standard combination, e.g.::

        * vertical-only follower: dofs=[3]
        * 2-D in-plane rigid:     dofs=[1, 2, 6]
        * symmetry plane:         dofs=[1, 4, 5]

    Resolution emits a single
    :class:`~apeGmsh.solvers.Constraints.NodeGroupRecord`.
    Downstream this is typically expanded to one
    ``ops.equalDOF`` per slave.

    Parameters
    ----------
    master_label : str
        Part label that owns the master node.
    slave_label : str
        Part label whose nodes are slaved.
    master_point : (x, y, z), default (0, 0, 0)
        Coordinates of the master node.
    dofs : list[int], optional
        1-based DOFs to couple. Default ``[1, 2, 3, 4, 5, 6]``
        (full 6-DOF, equivalent to :meth:`rigid_body`).
    name : str, optional
        Friendly name.

    Returns
    -------
    KinematicCouplingDef

    Raises
    ------
    KeyError
        If either label is not in ``g.parts``.
    """
    return self._add_def(KinematicCouplingDef(
        master_label=master_label, slave_label=slave_label,
        master_point=master_point, dofs=dofs or [1, 2, 3, 4, 5, 6],
        name=name))

tie

tie(master_label, slave_label, *, master_entities=None, slave_entities=None, dofs=None, tolerance=1.0, name=None) -> TieDef

Non-matching mesh tie via shape-function interpolation.

For each slave node, the resolver finds the closest master element face, projects the node onto it, and constrains its DOFs to the master corner DOFs through that face's shape functions::

u_slave = Σ N_i(ξ, η) · u_master_i

where (ξ, η) are the projected parametric coordinates and N_i are the master face's shape functions (tri3, quad4, tri6, quad8 supported). This is what Abaqus *TIE does — it preserves displacement continuity across non-matching meshes.

Resolution emits one :class:~apeGmsh.mesh.records.InterpolationRecord per successfully projected slave node. Downstream the apeGmsh OpenSees bridge emits these as ASDEmbeddedNodeElement penalty elements (default K = 1e18; tunable on the bridge ingest API).

Parameters

master_label : str Part label of the master surface (the side whose mesh will provide the shape functions). slave_label : str Part label of the slave surface (whose nodes are projected). master_entities : list of (dim, tag), optional Restrict the master surface to specific Gmsh entities. Strongly recommended when the master part has more than one face. slave_entities : list of (dim, tag), optional Restrict the slave surface to specific entities. dofs : list[int], optional DOFs to tie. None (default) ties all translational DOFs available — typically [1, 2, 3]. tolerance : float, default 1.0 Maximum allowed projection distance from a slave node to the master surface. Slave nodes farther than this are silently skipped — set generously if the two meshes have a small geometric gap, but not so large that the wrong face is selected. Unit-sensitive. name : str, optional Friendly name.

Returns

TieDef

Raises

KeyError If either label is not in g.parts.

See Also

equal_dof : Conformal-mesh equivalent (no interpolation). tied_contact : Bidirectional surface-to-surface tie. mortar : Higher-accuracy variant via Lagrange multipliers.

Notes

Master/slave choice matters for accuracy. As a rule:

  • The master should have the finer mesh (more shape functions to project onto).
  • The slave should have the coarser mesh (fewer projection operations).
Examples

Shell-to-solid tie at a column-top interface::

g.constraints.tie(
    "shell_floor", "solid_column",
    master_entities=[(2, 17)],     # column top face
    slave_entities=[(2, 41)],      # shell bottom face
    tolerance=5.0,                 # mm gap
)
Source code in src/apeGmsh/core/ConstraintsComposite.py
def tie(self, master_label, slave_label, *, master_entities=None,
        slave_entities=None, dofs=None, tolerance=1.0,
        name=None) -> TieDef:
    """Non-matching mesh tie via shape-function interpolation.

    For each slave node, the resolver finds the closest master
    element face, projects the node onto it, and constrains its
    DOFs to the master corner DOFs through that face's shape
    functions::

        u_slave = Σ N_i(ξ, η) · u_master_i

    where ``(ξ, η)`` are the projected parametric coordinates
    and ``N_i`` are the master face's shape functions (tri3,
    quad4, tri6, quad8 supported). This is what Abaqus
    ``*TIE`` does — it preserves displacement continuity across
    non-matching meshes.

    Resolution emits one
    :class:`~apeGmsh.mesh.records.InterpolationRecord` per
    successfully projected slave node. Downstream the apeGmsh
    OpenSees bridge emits these as ``ASDEmbeddedNodeElement``
    penalty elements (default K = 1e18; tunable on the bridge
    ingest API).

    Parameters
    ----------
    master_label : str
        Part label of the master surface (the side whose mesh
        will provide the shape functions).
    slave_label : str
        Part label of the slave surface (whose nodes are
        projected).
    master_entities : list of (dim, tag), optional
        Restrict the master surface to specific Gmsh
        entities. **Strongly recommended** when the master
        part has more than one face.
    slave_entities : list of (dim, tag), optional
        Restrict the slave surface to specific entities.
    dofs : list[int], optional
        DOFs to tie. ``None`` (default) ties all translational
        DOFs available — typically ``[1, 2, 3]``.
    tolerance : float, default 1.0
        Maximum allowed projection distance from a slave node
        to the master surface. Slave nodes farther than this
        are silently skipped — set generously if the two
        meshes have a small geometric gap, but not so large
        that the wrong face is selected. **Unit-sensitive.**
    name : str, optional
        Friendly name.

    Returns
    -------
    TieDef

    Raises
    ------
    KeyError
        If either label is not in ``g.parts``.

    See Also
    --------
    equal_dof : Conformal-mesh equivalent (no interpolation).
    tied_contact : Bidirectional surface-to-surface tie.
    mortar : Higher-accuracy variant via Lagrange multipliers.

    Notes
    -----
    Master/slave choice matters for accuracy. As a rule:

    * The master should have the **finer** mesh (more shape
      functions to project onto).
    * The slave should have the **coarser** mesh (fewer
      projection operations).

    Examples
    --------
    Shell-to-solid tie at a column-top interface::

        g.constraints.tie(
            "shell_floor", "solid_column",
            master_entities=[(2, 17)],     # column top face
            slave_entities=[(2, 41)],      # shell bottom face
            tolerance=5.0,                 # mm gap
        )
    """
    return self._add_def(TieDef(
        master_label=master_label, slave_label=slave_label,
        master_entities=master_entities, slave_entities=slave_entities,
        dofs=dofs, tolerance=tolerance, name=name))

distributing_coupling

distributing_coupling(master_label, slave_label, *, master_point=(0.0, 0.0, 0.0), dofs=None, weighting='uniform', name=None) -> DistributingCouplingDef

Not implemented — raises NotImplementedError.

A true distributing coupling (RBE3) distributes a master force/moment to the slave nodes so that Σ Fᵢ = F and Σ rᵢ × Fᵢ = M while leaving the surface free to deform — a force relation, not a kinematic one.

The previous implementation did not do this: it emitted a kinematic mean constraint (u_ref = Σ wᵢ u_surfᵢ) and its weighting="area" option was inverse-distance-from-centroid (physically meaningless — the opposite of tributary area), with no moment-equilibrium term. It silently produced a mechanically wrong model under a correct-looking API, so it is refused rather than shipped.

Use instead, depending on intent:

  • :meth:kinematic_coupling — a DOF-selective rigid coupling of the surface to a reference node.
  • :meth:tie — shape-function interpolation onto a master face (compatible, non-rigid).
  • a distributed nodal load (g.loads) — to introduce a statically-equivalent load without any kinematic tie.
Raises

NotImplementedError Always. Parameters are accepted only so the message is actionable.

Source code in src/apeGmsh/core/ConstraintsComposite.py
def distributing_coupling(self, master_label, slave_label, *,
                          master_point=(0., 0., 0.), dofs=None,
                          weighting="uniform",
                          name=None) -> DistributingCouplingDef:
    """**Not implemented — raises ``NotImplementedError``.**

    A true distributing coupling (RBE3) distributes a master
    force/moment to the slave nodes so that ``Σ Fᵢ = F`` **and**
    ``Σ rᵢ × Fᵢ = M`` while leaving the surface free to deform —
    a *force* relation, not a kinematic one.

    The previous implementation did **not** do this: it emitted a
    *kinematic* mean constraint (``u_ref = Σ wᵢ u_surfᵢ``) and its
    ``weighting="area"`` option was inverse-distance-from-centroid
    (physically meaningless — the opposite of tributary area),
    with no moment-equilibrium term.  It silently produced a
    mechanically wrong model under a correct-looking API, so it is
    refused rather than shipped.

    Use instead, depending on intent:

    * :meth:`kinematic_coupling` — a DOF-selective rigid coupling
      of the surface to a reference node.
    * :meth:`tie` — shape-function interpolation onto a master
      face (compatible, non-rigid).
    * a distributed nodal load (``g.loads``) — to introduce a
      statically-equivalent load without any kinematic tie.

    Raises
    ------
    NotImplementedError
        Always.  Parameters are accepted only so the message is
        actionable.
    """
    raise NotImplementedError(
        "distributing_coupling (RBE3 force distribution) is not "
        "implemented.  The prior implementation silently emitted a "
        "kinematic mean with a physically-meaningless 'area' "
        "weighting and no moment equilibrium.  Use "
        "kinematic_coupling (DOF-selective rigid coupling), tie "
        "(shape-function interpolation), or a distributed nodal "
        "load instead."
    )

embedded

embedded(host_label, embedded_label, *, tolerance=1.0, host_entities=None, embedded_entities=None, name=None) -> EmbeddedDef

Embed lower-dimensional elements inside a host volume or surface.

Each node of the embedded part is constrained to the displacement field of the host element it falls inside via host shape functions. Used for rebar in concrete, stiffeners in shells, fibres in composite hosts, etc.

Currently supports:

  • 3-D host: tet4 (Gmsh element type 4) volumes.
  • 2-D host: tri3 (Gmsh element type 2) surfaces.

Higher-order or hex/quad hosts are not yet supported and will be silently skipped — fall back to :meth:tie if you need that.

The resolver automatically drops embedded nodes that coincide with host element corners, since those are already rigidly attached through shared connectivity.

Parameters

host_label : str Part label whose tet4/tri3 elements form the host field. Stored internally as master_label. embedded_label : str Part label whose nodes are embedded. Stored as slave_label. (Label validation is bypassed for EmbeddedDef — these labels may also be physical group names if no part registry is in use.) tolerance : float, default 1.0 Reserved; not currently enforced (the resolver accepts any located host record). Retained for API stability. host_entities, embedded_entities : list of (dim, tag), optional Restrict the host / embedded sides to specific Gmsh entities. When omitted the whole label is used. name : str, optional Friendly name.

Returns

EmbeddedDef

Notes

Emitted downstream as ASDEmbeddedNodeElement. The host_label / embedded_label argument names mirror Abaqus's *EMBEDDED ELEMENT vocabulary; internally the composite still stores them as master/slave for consistency with the rest of the constraint records.

Examples

Rebar curve embedded inside a concrete tet mesh::

g.constraints.embedded(
    host_label="concrete_block",
    embedded_label="rebar_curve",
    tolerance=2.0,        # mm
)
Source code in src/apeGmsh/core/ConstraintsComposite.py
def embedded(self, host_label, embedded_label, *, tolerance=1.0,
             host_entities=None, embedded_entities=None,
             name=None) -> EmbeddedDef:
    """Embed lower-dimensional elements inside a host volume or
    surface.

    Each node of the embedded part is constrained to the
    displacement field of the host element it falls inside via
    host shape functions. Used for **rebar in concrete**,
    stiffeners in shells, fibres in composite hosts, etc.

    Currently supports:

    * **3-D host:** tet4 (Gmsh element type 4) volumes.
    * **2-D host:** tri3 (Gmsh element type 2) surfaces.

    Higher-order or hex/quad hosts are not yet supported and
    will be silently skipped — fall back to :meth:`tie` if you
    need that.

    The resolver automatically drops embedded nodes that
    coincide with host element corners, since those are
    already rigidly attached through shared connectivity.

    Parameters
    ----------
    host_label : str
        Part label whose tet4/tri3 elements form the host
        field. Stored internally as ``master_label``.
    embedded_label : str
        Part label whose nodes are embedded. Stored as
        ``slave_label``. (Label validation is bypassed for
        ``EmbeddedDef`` — these labels may also be physical
        group names if no part registry is in use.)
    tolerance : float, default 1.0
        Reserved; not currently enforced (the resolver accepts
        any located host record).  Retained for API stability.
    host_entities, embedded_entities : list of (dim, tag), optional
        Restrict the host / embedded sides to specific Gmsh
        entities.  When omitted the whole label is used.
    name : str, optional
        Friendly name.

    Returns
    -------
    EmbeddedDef

    Notes
    -----
    Emitted downstream as ``ASDEmbeddedNodeElement``. The
    ``host_label`` / ``embedded_label`` argument names mirror
    Abaqus's ``*EMBEDDED ELEMENT`` vocabulary; internally the
    composite still stores them as ``master``/``slave`` for
    consistency with the rest of the constraint records.

    Examples
    --------
    Rebar curve embedded inside a concrete tet mesh::

        g.constraints.embedded(
            host_label="concrete_block",
            embedded_label="rebar_curve",
            tolerance=2.0,        # mm
        )
    """
    return self._add_def(EmbeddedDef(
        master_label=host_label, slave_label=embedded_label,
        tolerance=tolerance, host_entities=host_entities,
        embedded_entities=embedded_entities, name=name))

node_to_surface

node_to_surface(master, slave, *, dofs=None, tolerance=1e-06, name=None)

6-DOF node to 3-DOF surface coupling via phantom nodes.

Creates a single constraint that aggregates all surface entities in slave. Shared-edge mesh nodes are deduplicated so each original slave node gets exactly one phantom.

Parameters

master : int, str, or (dim, tag) The 6-DOF reference node. slave : int, str, or (dim, tag) The surface(s) to couple. If it resolves to multiple surface entities, they are combined into a single constraint and slave nodes are deduplicated.

Returns

NodeToSurfaceDef A single def covering all resolved surface entities.

Source code in src/apeGmsh/core/ConstraintsComposite.py
def node_to_surface(self, master, slave, *,
                    dofs=None, tolerance=1e-6,
                    name=None):
    """6-DOF node to 3-DOF surface coupling via phantom nodes.

    Creates a single constraint that aggregates all surface
    entities in *slave*.  Shared-edge mesh nodes are deduplicated
    so each original slave node gets exactly one phantom.

    Parameters
    ----------
    master : int, str, or (dim, tag)
        The 6-DOF reference node.
    slave : int, str, or (dim, tag)
        The surface(s) to couple.  If it resolves to multiple
        surface entities, they are combined into a single
        constraint and slave nodes are deduplicated.

    Returns
    -------
    NodeToSurfaceDef
        A single def covering all resolved surface entities.
    """
    from ._helpers import resolve_to_tags
    m_tags = resolve_to_tags(master, dim=0, session=self._parent)
    s_tags = resolve_to_tags(slave,  dim=2, session=self._parent)
    if len(m_tags) != 1:
        raise ValueError(
            f"node_to_surface master {master!r} resolved to "
            f"{len(m_tags)} dim-0 entities {m_tags} — the master "
            f"must identify exactly one reference point.")
    if not s_tags:
        raise ValueError(
            f"node_to_surface slave {slave!r} resolved to no "
            f"dim-2 surface entities.")
    master_tag = m_tags[0]

    if name is None:
        m_name = str(master) if isinstance(master, str) else str(master_tag)
        s_name = str(slave) if isinstance(slave, str) else "surface"
        display_name = f"{m_name}{s_name}"
    else:
        display_name = name

    # Store ALL surface tags as a comma-separated string so the
    # resolver can union their slave nodes and deduplicate.
    slave_label = ",".join(str(int(t)) for t in s_tags)

    return self._add_def(NodeToSurfaceDef(
        master_label=str(master_tag),
        slave_label=slave_label,
        dofs=dofs, tolerance=tolerance,
        name=display_name))

node_to_surface_spring

node_to_surface_spring(master, slave, *, dofs=None, tolerance=1e-06, name=None)

Spring-based variant of :meth:node_to_surface.

Identical topology and call signature, but the master → phantom links are tagged for downstream emission as stiff elasticBeamColumn elements instead of kinematic rigidLink('beam', ...) constraints. Use this variant when the master carries free rotational DOFs (fork support on a solid end face) that receive direct moment loading — the constraint-based variant of node_to_surface can produce an ill-conditioned reduced stiffness matrix in that case because the master rotation DOFs get stiffness only through the kinematic constraint back-propagation, with nothing attaching directly to them.

See :class:~apeGmsh.solvers.Constraints.NodeToSurfaceSpringDef for the full rationale.

Emission in OpenSees::

# Each master → phantom link becomes a stiff beam element
next_eid = max_tet_eid + 1
for master, slaves in fem.nodes.constraints.stiff_beam_groups():
    for phantom in slaves:
        ops.element(
            'elasticBeamColumn', next_eid,
            master, phantom,
            A_big, E, I_big, I_big, J_big, transf_tag,
        )
        next_eid += 1

# equalDOFs are unchanged from the normal variant
for pair in fem.nodes.constraints.equal_dofs():
    ops.equalDOF(
        pair.master_node, pair.slave_node, *pair.dofs)
Parameters

Same as :meth:node_to_surface.

Returns

NodeToSurfaceSpringDef

Source code in src/apeGmsh/core/ConstraintsComposite.py
def node_to_surface_spring(self, master, slave, *,
                           dofs=None, tolerance=1e-6,
                           name=None):
    """Spring-based variant of :meth:`node_to_surface`.

    Identical topology and call signature, but the master → phantom
    links are tagged for downstream emission as stiff
    ``elasticBeamColumn`` elements instead of kinematic
    ``rigidLink('beam', ...)`` constraints. Use this variant when
    the master carries **free rotational DOFs** (fork support on a
    solid end face) that receive direct moment loading — the
    constraint-based variant of ``node_to_surface`` can produce an
    ill-conditioned reduced stiffness matrix in that case because
    the master rotation DOFs get stiffness only through the
    kinematic constraint back-propagation, with nothing attaching
    directly to them.

    See :class:`~apeGmsh.solvers.Constraints.NodeToSurfaceSpringDef`
    for the full rationale.

    Emission in OpenSees::

        # Each master → phantom link becomes a stiff beam element
        next_eid = max_tet_eid + 1
        for master, slaves in fem.nodes.constraints.stiff_beam_groups():
            for phantom in slaves:
                ops.element(
                    'elasticBeamColumn', next_eid,
                    master, phantom,
                    A_big, E, I_big, I_big, J_big, transf_tag,
                )
                next_eid += 1

        # equalDOFs are unchanged from the normal variant
        for pair in fem.nodes.constraints.equal_dofs():
            ops.equalDOF(
                pair.master_node, pair.slave_node, *pair.dofs)

    Parameters
    ----------
    Same as :meth:`node_to_surface`.

    Returns
    -------
    NodeToSurfaceSpringDef
    """
    from ._helpers import resolve_to_tags
    m_tags = resolve_to_tags(master, dim=0, session=self._parent)
    s_tags = resolve_to_tags(slave,  dim=2, session=self._parent)
    if len(m_tags) != 1:
        raise ValueError(
            f"node_to_surface_spring master {master!r} resolved "
            f"to {len(m_tags)} dim-0 entities {m_tags} — the "
            f"master must identify exactly one reference point.")
    if not s_tags:
        raise ValueError(
            f"node_to_surface_spring slave {slave!r} resolved to "
            f"no dim-2 surface entities.")
    master_tag = m_tags[0]

    if name is None:
        m_name = str(master) if isinstance(master, str) else str(master_tag)
        s_name = str(slave) if isinstance(slave, str) else "surface"
        display_name = f"{m_name} \u2192 {s_name} (spring)"
    else:
        display_name = name

    slave_label = ",".join(str(int(t)) for t in s_tags)

    return self._add_def(NodeToSurfaceSpringDef(
        master_label=str(master_tag),
        slave_label=slave_label,
        dofs=dofs, tolerance=tolerance,
        name=display_name))

tied_contact

tied_contact(master_label, slave_label, *, master_entities=None, slave_entities=None, dofs=None, tolerance=1.0, name=None) -> TiedContactDef

Bidirectional surface-to-surface tie.

Conceptually a :meth:tie applied in both directions — slave nodes are projected onto the master surface, and master nodes are also projected onto the slave surface, so every node on either side is interpolated against the opposite mesh. Useful when neither side can be picked as clearly finer than the other and you want a symmetric treatment.

Resolution emits :class:~apeGmsh.solvers.Constraints.SurfaceCouplingRecord objects on fem.elements.constraints.

Parameters

master_label : str Part label of the first surface. slave_label : str Part label of the second surface. master_entities, slave_entities : list of (dim, tag), optional Restrict each side to specific Gmsh entities. dofs : list[int], optional DOFs to tie. None = all translational. tolerance : float, default 1.0 Maximum projection distance. Unit-sensitive. name : str, optional Friendly name.

Returns

TiedContactDef

Raises

KeyError If either label is not in g.parts.

See Also

tie : One-directional tie (slave-projected only). mortar : Mathematically rigorous Lagrange-multiplier coupling.

Source code in src/apeGmsh/core/ConstraintsComposite.py
def tied_contact(self, master_label, slave_label, *,
                 master_entities=None, slave_entities=None,
                 dofs=None, tolerance=1.0,
                 name=None) -> TiedContactDef:
    """Bidirectional surface-to-surface tie.

    Conceptually a :meth:`tie` applied in **both directions** —
    slave nodes are projected onto the master surface, and
    master nodes are also projected onto the slave surface, so
    every node on either side is interpolated against the
    opposite mesh. Useful when neither side can be picked as
    clearly finer than the other and you want a symmetric
    treatment.

    Resolution emits
    :class:`~apeGmsh.solvers.Constraints.SurfaceCouplingRecord`
    objects on ``fem.elements.constraints``.

    Parameters
    ----------
    master_label : str
        Part label of the first surface.
    slave_label : str
        Part label of the second surface.
    master_entities, slave_entities : list of (dim, tag), optional
        Restrict each side to specific Gmsh entities.
    dofs : list[int], optional
        DOFs to tie. ``None`` = all translational.
    tolerance : float, default 1.0
        Maximum projection distance. **Unit-sensitive.**
    name : str, optional
        Friendly name.

    Returns
    -------
    TiedContactDef

    Raises
    ------
    KeyError
        If either label is not in ``g.parts``.

    See Also
    --------
    tie : One-directional tie (slave-projected only).
    mortar : Mathematically rigorous Lagrange-multiplier
        coupling.
    """
    return self._add_def(TiedContactDef(
        master_label=master_label, slave_label=slave_label,
        master_entities=master_entities, slave_entities=slave_entities,
        dofs=dofs, tolerance=tolerance, name=name))

mortar

mortar(master_label, slave_label, *, master_entities=None, slave_entities=None, dofs=None, integration_order=2, name=None) -> MortarDef

Not implemented — raises NotImplementedError.

A true mortar method introduces a Lagrange-multiplier space ψᵢ on the slave side and integrates the coupling operator Bᵢⱼ = ∫_Γ ψᵢ·Nⱼ dΓ over the overlapping surface segments, satisfying the inf-sup (LBB) condition.

The previous implementation did none of that: no segment intersection, no surface integral, no dual basis — it scattered tied_contact collocation weights onto a block-diagonal B with a hardcoded tolerance=10.0 (model-unit dependent → mis-pairs on millimetre models), yet returned a record labelled MORTAR that downstream could not distinguish from a real one. It is refused rather than shipped as a plausible-but-wrong operator.

Use :meth:tied_contact for a collocation-based non-matching tie (the honest version of what the old code actually did).

Raises

NotImplementedError Always. Parameters are accepted only so the message is actionable.

Source code in src/apeGmsh/core/ConstraintsComposite.py
def mortar(self, master_label, slave_label, *,
           master_entities=None, slave_entities=None,
           dofs=None, integration_order=2,
           name=None) -> MortarDef:
    """**Not implemented — raises ``NotImplementedError``.**

    A true mortar method introduces a Lagrange-multiplier space
    ``ψᵢ`` on the slave side and integrates the coupling operator
    ``Bᵢⱼ = ∫_Γ ψᵢ·Nⱼ dΓ`` over the overlapping surface segments,
    satisfying the inf-sup (LBB) condition.

    The previous implementation did **none** of that: no segment
    intersection, no surface integral, no dual basis — it scattered
    ``tied_contact`` collocation weights onto a block-diagonal
    ``B`` with a hardcoded ``tolerance=10.0`` (model-unit
    dependent → mis-pairs on millimetre models), yet returned a
    record labelled ``MORTAR`` that downstream could not
    distinguish from a real one.  It is refused rather than
    shipped as a plausible-but-wrong operator.

    Use :meth:`tied_contact` for a collocation-based non-matching
    tie (the honest version of what the old code actually did).

    Raises
    ------
    NotImplementedError
        Always.  Parameters are accepted only so the message is
        actionable.
    """
    raise NotImplementedError(
        "mortar (∫ ψ·N dΓ Lagrange-multiplier coupling) is not "
        "implemented.  The prior implementation was a collocation "
        "tie with a unit-dependent hardcoded tolerance mislabelled "
        "as MORTAR.  Use tied_contact for a non-matching "
        "collocation tie."
    )

validate_pre_mesh

validate_pre_mesh() -> None

No-op: constraints validate targets eagerly at _add_def.

Present so :meth:Mesh.generate can invoke validate_pre_mesh on all three composites uniformly.

Source code in src/apeGmsh/core/ConstraintsComposite.py
def validate_pre_mesh(self) -> None:
    """No-op: constraints validate targets eagerly at ``_add_def``.

    Present so :meth:`Mesh.generate` can invoke ``validate_pre_mesh``
    on all three composites uniformly.
    """
    return None

summary

summary()

DataFrame of the declared constraint intent — one row per def.

Columns: kind, name, master, slave, params. params is a short stringified view of the kind-specific fields (dofs, tolerance, etc.).

Source code in src/apeGmsh/core/ConstraintsComposite.py
def summary(self):
    """DataFrame of the declared constraint intent — one row per def.

    Columns: ``kind, name, master, slave, params``.  ``params`` is a
    short stringified view of the kind-specific fields (``dofs``,
    ``tolerance``, etc.).
    """
    import pandas as pd
    from dataclasses import fields

    _COMMON = {"kind", "name", "master_label", "slave_label"}

    rows: list[dict] = []
    for d in self.constraint_defs:
        params = {
            f.name: getattr(d, f.name)
            for f in fields(d)
            if f.name not in _COMMON
        }
        params = {k: v for k, v in params.items() if v is not None}
        rows.append({
            "kind"  : d.kind,
            "name"  : d.name or "",
            "master": d.master_label,
            "slave" : d.slave_label,
            "params": ", ".join(f"{k}={v}" for k, v in params.items()),
        })

    cols = ["kind", "name", "master", "slave", "params"]
    if not rows:
        return pd.DataFrame(columns=cols)
    return pd.DataFrame(rows, columns=cols)

Base class

All Stage-1 definitions inherit from ConstraintDef — a thin dataclass carrying kind, master_label, slave_label, and an optional friendly name. Subclasses add their kind-specific parameters.

apeGmsh._kernel.defs.constraints.ConstraintDef dataclass

ConstraintDef(kind: str, master_label: str, slave_label: str, name: str | None = None)

Base class for all constraint definitions.

Tier 1 — Node-to-Node

Pairwise constraints between co-located nodes. The resolver matches master-side nodes against slave-side nodes within tolerance and emits one NodePairRecord per match.

apeGmsh._kernel.defs.constraints.EqualDOFDef dataclass

EqualDOFDef(kind: str, master_label: str, slave_label: str, name: str | None = None, dofs: list[int] | None = None, tolerance: float = 1e-06, master_entities: list[tuple[int, int]] | None = None, slave_entities: list[tuple[int, int]] | None = None)

Bases: ConstraintDef

Co-located nodes share selected DOFs.

After meshing, the resolver finds node pairs within tolerance on the interface between master and slave instances, and produces one :class:NodePairRecord per pair.

Parameters

dofs : list[int] or None DOF numbers to constrain (1-based: 1=ux, 2=uy, 3=uz, 4=rx, 5=ry, 6=rz). None = all DOFs. tolerance : float Spatial distance (in model units) within which two nodes are considered co-located. master_entities : list of (dim, tag), optional Limit the master search to specific geometric entities. slave_entities : list of (dim, tag), optional Limit the slave search to specific geometric entities.

apeGmsh._kernel.defs.constraints.RigidLinkDef dataclass

RigidLinkDef(kind: str, master_label: str, slave_label: str, name: str | None = None, link_type: str = 'beam', master_point: tuple[float, float, float] | None = None, slave_entities: list[tuple[int, int]] | None = None, tolerance: float = 1e-06)

Bases: ConstraintDef

Rigid bar connecting master and slave nodes.

rigid_beam -> full 6-DOF coupling (translations + rotations)::

u_s = u_m + θ_m × r       (translations)
θ_s = θ_m                  (rotations)

rigid_rod -> translations only, rotations independent::

u_s = u_m + θ_m × r
(θ_s free)
Parameters

link_type : "beam" or "rod" master_point : (x,y,z) or None If given, the master is the nearest node in the master set to this point. If None, the master is the node nearest the master set's centroid. slave_entities : list of (dim, tag), optional Geometric entities whose nodes become slaves. tolerance : float Reserved. Not currently enforced for master selection (the nearest node is taken unconditionally); kept for API stability and a future proximity-gated check.

apeGmsh._kernel.defs.constraints.PenaltyDef dataclass

PenaltyDef(kind: str, master_label: str, slave_label: str, name: str | None = None, stiffness: float = 10000000000.0, dofs: list[int] | None = None, tolerance: float = 1e-06, master_entities: list[tuple[int, int]] | None = None, slave_entities: list[tuple[int, int]] | None = None)

Bases: ConstraintDef

Soft spring between co-located node pairs.

Numerically approximates EqualDOF when K -> ∞. Useful when hard constraints cause ill-conditioning.

Parameters

stiffness : float Penalty spring stiffness (force/length units). dofs : list[int] or None DOFs to penalise. tolerance : float Node-matching tolerance.

Tier 2 — Node-to-Group

One master node drives many slave nodes through a kinematic transformation about a master point. Use these for floor diaphragms, lumped rigid bodies, or any cluster sharing a chosen DOF subset.

apeGmsh._kernel.defs.constraints.RigidDiaphragmDef dataclass

RigidDiaphragmDef(kind: str, master_label: str, slave_label: str, name: str | None = None, master_point: tuple[float, float, float] = (0.0, 0.0, 0.0), plane_normal: tuple[float, float, float] = (0.0, 0.0, 1.0), constrained_dofs: list[int] = (lambda: [1, 2, 6])(), plane_tolerance: float = 1.0)

Bases: ConstraintDef

In-plane rigid body constraint. All slave nodes at a given plane follow the master node for in-plane DOFs.

Classic use: floor slabs in multi-story buildings — all nodes at a floor elevation share in-plane translation + rotation about the out-of-plane axis.

Parameters

master_point : (x, y, z) Master node location (typically center of mass). plane_normal : (nx, ny, nz) Normal to the diaphragm plane. (0,0,1) = horizontal floor. constrained_dofs : list[int] DOFs constrained in-plane. For a horizontal floor with Z as vertical: [1, 2, 6] (ux, uy, rz). plane_tolerance : float Distance from the plane within which nodes are collected.

apeGmsh._kernel.defs.constraints.RigidBodyDef dataclass

RigidBodyDef(kind: str, master_label: str, slave_label: str, name: str | None = None, master_point: tuple[float, float, float] = (0.0, 0.0, 0.0), slave_entities: list[tuple[int, int]] | None = None)

Bases: ConstraintDef

Full rigid body constraint: all 6 DOFs of every slave node follow the master.

Parameters

master_point : (x, y, z) Master node location. slave_entities : list of (dim, tag), optional Geometric entities whose nodes become slaves.

apeGmsh._kernel.defs.constraints.KinematicCouplingDef dataclass

KinematicCouplingDef(kind: str, master_label: str, slave_label: str, name: str | None = None, master_point: tuple[float, float, float] = (0.0, 0.0, 0.0), slave_entities: list[tuple[int, int]] | None = None, dofs: list[int] = (lambda: [1, 2, 3, 4, 5, 6])())

Bases: ConstraintDef

Generalised master-slave: user picks which DOFs.

This is the parent of rigid_diaphragm and rigid_body — they are special cases with pre-set DOF lists.

Parameters

master_point : (x, y, z) Master node location. slave_entities : list of (dim, tag), optional Geometric entities whose nodes become slaves. dofs : list[int] DOFs to couple.

Tier 2b — Mixed-DOF

A 6-DOF master node coupled to 3-DOF slave nodes (typically a beam end framing into a solid face). The resolver duplicates each slave to a 6-DOF phantom node so that rotational kinematics can propagate through a rigid arm before being equal-DOF-coupled to the original 3-DOF slave.

Two variants:

  • NodeToSurfaceDef emits the master → phantom link as a kinematic rigidLink('beam', …) constraint. Cheap and exact.
  • NodeToSurfaceSpringDef emits it as a stiff elasticBeamColumn element. Use this when the master has free rotational DOFs that receive direct moment loading — the constraint variant can produce an ill-conditioned reduced stiffness matrix in that case.

apeGmsh._kernel.defs.constraints.NodeToSurfaceDef dataclass

NodeToSurfaceDef(kind: str, master_label: str, slave_label: str, name: str | None = None, master_point: tuple[float, float, float] | None = None, dofs: list[int] | None = None, tolerance: float = 1e-06)

Bases: ConstraintDef

6-DOF node to 3-DOF surface coupling via phantom (duplicate) nodes.

Connects a 6-DOF master node (beam, frame, or any reference point) to a group of 3-DOF slave nodes on a surface (solid elements) through an intermediate layer of phantom nodes that carry full 6-DOF kinematics.

The resolver:

  1. Duplicates each slave node -> creates phantom node tags at the same coordinates (6-DOF intermediaries).
  2. Rigid links master -> each phantom node (rigid_beam), propagating rotational effects through the offset arm::

    u_phantom = u_master + θ_master × r

  3. EqualDOF phantom -> original slave, translations only [1, 2, 3] (rotations discarded since the solid has none).

This is the standard technique for mixed-dimensionality coupling (Abaqus *COUPLING, KINEMATIC on solids; OpenSees manual rigid-link + equalDOF pattern).

Unlike other constraint definitions that take string labels, this one accepts bare tags:

  • master_label: node tag (int, dim=0) — the 6-DOF node.
  • slave_label: surface entity tag (int, dim=2) — the Gmsh surface whose nodes become the 3-DOF slaves.
Parameters

dofs : list[int] or None Translational DOFs coupled to the solid. Default [1, 2, 3]. master_point : (x, y, z) or None Ignored. The master is taken directly from the master_label node tag (this def uses bare tags, see above); there is no proximity master-detection. Retained only for dataclass/API stability. tolerance : float Ignored for the same reason. Retained for API stability.

apeGmsh._kernel.defs.constraints.NodeToSurfaceSpringDef dataclass

NodeToSurfaceSpringDef(kind: str, master_label: str, slave_label: str, name: str | None = None, master_point: tuple[float, float, float] | None = None, dofs: list[int] | None = None, tolerance: float = 1e-06)

Bases: NodeToSurfaceDef

Spring-based variant of :class:NodeToSurfaceDef.

Same topology as NodeToSurfaceDef — a 6-DOF master node is coupled to the 3-DOF nodes of a surface through an intermediate layer of phantom nodes — but the master → phantom link is emitted downstream as a stiff elasticBeamColumn element instead of a kinematic rigidLink('beam', …) constraint.

Why this variant exists

The standard NodeToSurfaceDef uses rigidLink + equalDOF. That chain works perfectly for most cases — rigid load transfer, prescribed translations at a master, fully-fixed masters — but breaks down when all three of the following are true:

  • The master has free rotational DOFs (fork support, free bending rotations at a simply-supported end).
  • A moment is applied directly to those free rotation DOFs.
  • The slave side is a solid element with ndf=3 (tet4, hex8, …), so the rigid-link constraint back-propagates stiffness to the master rotations only through kinematic coupling — no element attaches directly to master.ry / master.rz.

Under those conditions the reduced stiffness matrix becomes ill-conditioned and OpenSees's solver fails with "numeric analysis returns 1 -- UmfpackGenLinSolver::solve".

The spring variant fixes it by giving the master's rotation DOFs direct element stiffness: each master → phantom link becomes a stiff elasticBeamColumn element whose 6-DOF stiffness matrix contributes terms on the master's rotation diagonal regardless of any constraint handler gymnastics. Conditioning stays good.

Trade-offs
  • Pro — robust for fork supports + moment loading.
  • Pro — element-level stiffness is directly assembled into K, so no penalty factor to tune.
  • Con — each master → phantom link is now an element, so the element count grows by n_slaves per coupling. For a typical face with ~30 slave nodes this is ~30 extra elasticBeamColumn elements per node_to_surface_spring call. Negligible in solve time.
  • Con — approximate-rigid rather than truly rigid: the stiff beams have finite stiffness, so there is a tiny compliance in the coupling. Choose the section properties so they are orders of magnitude stiffer than the downstream elements.
Parameters

Inherited from :class:NodeToSurfaceDef.

See Also

NodeToSurfaceDef : constraint-based variant.

Tier 3 — Node-to-Surface

A slave node is constrained to the displacement field of a master surface or volume through shape-function interpolation. Handles non-matching meshes, distributed loads, and embedded reinforcement.

apeGmsh._kernel.defs.constraints.TieDef dataclass

TieDef(kind: str, master_label: str, slave_label: str, name: str | None = None, master_entities: list[tuple[int, int]] | None = None, slave_entities: list[tuple[int, int]] | None = None, dofs: list[int] | None = None, tolerance: float = 1.0)

Bases: ConstraintDef

Surface tie via shape function interpolation.

Each slave node is projected onto the closest master element face. Its DOFs are constrained to the master face via::

u_slave = Σ  N_i(ξ,η) · u_master_i

where N_i are the shape functions of the master face element evaluated at the projected parametric coordinates.

This is what Abaqus *TIE does. It preserves displacement continuity even with non-matching meshes.

Parameters

master_entities : list of (dim, tag) Master surface entities. slave_entities : list of (dim, tag) Slave surface entities (nodes on these are projected). dofs : list[int] or None DOFs to tie. None = all translational DOFs [1,2,3]. tolerance : float Maximum projection distance. Slave nodes farther than this from the master surface are skipped.

apeGmsh._kernel.defs.constraints.DistributingCouplingDef dataclass

DistributingCouplingDef(kind: str, master_label: str, slave_label: str, name: str | None = None, master_point: tuple[float, float, float] = (0.0, 0.0, 0.0), slave_entities: list[tuple[int, int]] | None = None, dofs: list[int] | None = None, weighting: str = 'uniform')

Bases: ConstraintDef

Distributing coupling (RBE3-style force distribution).

.. warning::

Not implemented. g.constraints.distributing_coupling raises NotImplementedError. A correct RBE3 distributes a master force/moment so that ΣF and Σr×F are preserved while the surface deforms freely; the prior implementation was a mislabelled kinematic mean (and its "area" weighting was inverse-distance-from-centroid, not tributary area). This dataclass is retained for a future correct implementation.

Parameters

master_point : (x, y, z) Reference point where the load is applied. slave_entities : list of (dim, tag) Surface entities that receive the distributed load. dofs : list[int] or None DOFs to couple. weighting : "uniform" or "area" How to distribute: uniform gives equal weights; area weights by tributary area (more physical).

apeGmsh._kernel.defs.constraints.EmbeddedDef dataclass

EmbeddedDef(kind: str, master_label: str, slave_label: str, name: str | None = None, host_entities: list[tuple[int, int]] | None = None, embedded_entities: list[tuple[int, int]] | None = None, tolerance: float = 1.0)

Bases: ConstraintDef

Embedded element constraint: nodes of a lower-dimensional element (beam, truss) are constrained to the displacement field of a higher-dimensional host element (solid).

Used for reinforcement in concrete, stiffeners in shells, etc.

Parameters

host_entities : list of (dim, tag), optional Host volume/surface entities. Settable via g.constraints.embedded(..., host_entities=...); when omitted the whole host_label is used. embedded_entities : list of (dim, tag), optional Embedded line/surface entities. Settable via embedded(..., embedded_entities=...); when omitted the whole embedded_label is used. tolerance : float Reserved. Not currently enforced — the resolver accepts any located host record (barycentric gating uses a fixed internal tolerance). Kept for API stability.

Tier 4 — Surface-to-Surface

Bidirectional surface couplings. Use these when neither side can be clearly picked as finer than the other and you want a symmetric treatment.

apeGmsh._kernel.defs.constraints.TiedContactDef dataclass

TiedContactDef(kind: str, master_label: str, slave_label: str, name: str | None = None, master_entities: list[tuple[int, int]] | None = None, slave_entities: list[tuple[int, int]] | None = None, dofs: list[int] | None = None, tolerance: float = 1.0)

Bases: ConstraintDef

Full surface-to-surface tie. Every node on the slave surface is tied to the master surface via shape function interpolation. Bidirectional — also checks master nodes against slave faces.

Parameters

master_entities : list of (dim, tag) slave_entities : list of (dim, tag) dofs : list[int] or None tolerance : float

apeGmsh._kernel.defs.constraints.MortarDef dataclass

MortarDef(kind: str, master_label: str, slave_label: str, name: str | None = None, master_entities: list[tuple[int, int]] | None = None, slave_entities: list[tuple[int, int]] | None = None, dofs: list[int] | None = None, integration_order: int = 2)

Bases: ConstraintDef

Mortar coupling: Lagrange-multiplier space on the interface.

.. warning::

Not implemented. g.constraints.mortar raises NotImplementedError. A correct mortar operator is Bᵢⱼ = ∫_Γ ψᵢ·Nⱼ dΓ (segment integration, dual basis, inf-sup/LBB). The prior implementation was a tied_contact collocation tie with a hardcoded unit-dependent tolerance=10.0 mislabelled MORTAR. This dataclass is retained for a future correct implementation; use tied_contact for a collocation-based non-matching tie.

Parameters

master_entities : list of (dim, tag) slave_entities : list of (dim, tag) dofs : list[int] or None integration_order : int Gauss quadrature order for the coupling integral.

Records

Resolved records — what the FEM broker exposes after meshing.

apeGmsh._kernel.records._constraints

Stage 2 — Constraint Records (post-mesh, resolved).

These dataclasses carry the concrete mesh-level outputs of constraint resolution: node tags, shape-function weights, offset vectors, and phantom-node bookkeeping. Records are solver-agnostic — any adapter (OpenSees, Abaqus, Code_Aster, …) can consume them.

All records ultimately express the linear MPC equation::

u_slave = C · u_master

ConstraintRecord dataclass

ConstraintRecord(kind: str, name: str | None = None)

Base for all resolved constraint records.

Every record expresses (or can be expanded to) the general linear MPC equation: u_slave = C · u_master.

NodePairRecord dataclass

NodePairRecord(kind: str, name: str | None = None, master_node: int = 0, slave_node: int = 0, dofs: list[int] = list(), offset: ndarray | None = None, penalty_stiffness: float | None = None)

Bases: ConstraintRecord

One master node ↔ one slave node.

Covers: equal_dof, rigid_beam, rigid_rod, penalty.

Attributes

master_node : int Master node tag (from mesh). slave_node : int Slave node tag (from mesh). dofs : list[int] Constrained DOFs (1-based). offset : ndarray or None Rigid arm vector r = x_slave − x_master. Present for rigid link types; None for equal_dof. penalty_stiffness : float or None For penalty type only.

constraint_matrix
constraint_matrix(ndof: int = 6) -> ndarray

Build the constraint transformation matrix C such that u_slave[dofs] = C · u_master[all_dofs].

For equal_dof: C is a selection matrix (rows of identity). For rigid_beam: C includes the skew-symmetric offset matrix.

Parameters

ndof : int DOFs per node (default 6 for shell/beam).

Returns

ndarray of shape (len(dofs), ndof)

Source code in src/apeGmsh/_kernel/records/_constraints.py
def constraint_matrix(self, ndof: int = 6) -> ndarray:
    """
    Build the constraint transformation matrix C such that
    u_slave[dofs] = C · u_master[all_dofs].

    For equal_dof: C is a selection matrix (rows of identity).
    For rigid_beam: C includes the skew-symmetric offset matrix.

    Parameters
    ----------
    ndof : int
        DOFs per node (default 6 for shell/beam).

    Returns
    -------
    ndarray of shape (len(dofs), ndof)
    """
    n = len(self.dofs)
    C = np.zeros((n, ndof))

    if self.kind in (ConstraintKind.EQUAL_DOF, ConstraintKind.PENALTY):
        # u_slave_i = u_master_i
        for row, dof in enumerate(self.dofs):
            C[row, dof - 1] = 1.0

    elif self.kind in (ConstraintKind.RIGID_BEAM, ConstraintKind.RIGID_ROD):
        # u_s = u_m + θ_m × r
        #
        # In matrix form for translations (DOFs 1-3):
        #   [u_s]   [I  | -[r×]] [u_m ]
        #   [   ] = [   |      ] [    ]
        #   [θ_s]   [0  |   I  ] [θ_m ]  (beam only)
        #
        # Skew-symmetric matrix of r:
        #   [r×] = [ 0   -rz   ry]
        #          [ rz   0   -rx]
        #          [-ry   rx   0 ]
        r = self.offset if self.offset is not None else np.zeros(3)
        rx, ry, rz = r

        skew = np.array([
            [ 0,  -rz,  ry],
            [ rz,  0,  -rx],
            [-ry,  rx,   0],
        ])

        for row, dof in enumerate(self.dofs):
            idx = dof - 1
            if idx < 3:
                # Translation: u_s_i = u_m_i + (skew · θ_m)_i
                C[row, idx] = 1.0                   # I term
                C[row, 3:6] = -skew[idx, :]         # -[r×] · θ_m
            elif idx < 6 and self.kind == ConstraintKind.RIGID_BEAM:
                # Rotation (beam only): θ_s = θ_m
                C[row, idx] = 1.0

    return C

NodeGroupRecord dataclass

NodeGroupRecord(kind: str, name: str | None = None, master_node: int = 0, slave_nodes: list[int] = list(), dofs: list[int] = list(), offsets: ndarray | None = None, plane_normal: ndarray | None = None)

Bases: ConstraintRecord

One master node ↔ multiple slave nodes.

Covers: rigid_diaphragm, rigid_body, kinematic_coupling.

Attributes

master_node : int slave_nodes : list[int] dofs : list[int] DOFs constrained for all slaves. offsets : ndarray Array of shape (n_slaves, 3) — offset vector for each slave. plane_normal : ndarray or None For rigid_diaphragm: normal to the constraint plane.

expand_to_pairs
expand_to_pairs() -> list[NodePairRecord]

Expand this group constraint into individual :class:NodePairRecord objects — one per slave node.

This is the most common consumption path: most solvers implement group constraints as loops of pair constraints (e.g., OpenSees rigidDiaphragm or repeated equalDOF).

Source code in src/apeGmsh/_kernel/records/_constraints.py
def expand_to_pairs(self) -> list[NodePairRecord]:
    """
    Expand this group constraint into individual
    :class:`NodePairRecord` objects — one per slave node.

    This is the most common consumption path: most solvers
    implement group constraints as loops of pair constraints
    (e.g., OpenSees ``rigidDiaphragm`` or repeated ``equalDOF``).
    """
    pairs = []
    for i, sn in enumerate(self.slave_nodes):
        offset = self.offsets[i] if self.offsets is not None else None
        if self.kind == ConstraintKind.RIGID_DIAPHRAGM:
            pair_kind = ConstraintKind.RIGID_BEAM
        elif self.kind == ConstraintKind.RIGID_BODY:
            pair_kind = ConstraintKind.RIGID_BEAM
        else:
            pair_kind = ConstraintKind.KINEMATIC_COUPLING

        pairs.append(NodePairRecord(
            kind=pair_kind,
            name=self.name,
            master_node=self.master_node,
            slave_node=sn,
            dofs=list(self.dofs),
            offset=offset,
        ))
    return pairs

InterpolationRecord dataclass

InterpolationRecord(kind: str, name: str | None = None, slave_node: int = 0, master_nodes: list[int] = list(), weights: ndarray | None = None, dofs: list[int] = list(), projected_point: ndarray | None = None, parametric_coords: ndarray | None = None)

Bases: ConstraintRecord

One slave node interpolated from a master element face.

Covers: tie, distributing, embedded.

The constraint equation is::

u_slave = Σ  w_i · u_master_i

where w_i are the interpolation weights (shape function values at the projected parametric coordinates on the master face).

Attributes

slave_node : int master_nodes : list[int] Nodes of the master element face (ordered). weights : ndarray Shape function values N_i(ξ,η) — same length as master_nodes. Sum to 1.0 for partition of unity. dofs : list[int] projected_point : ndarray or None Physical coordinates of the projection onto the master face (useful for verification / visualisation). parametric_coords : ndarray or None (ξ, η) on the master face.

constraint_matrix
constraint_matrix(ndof: int = 3) -> ndarray

Build the constraint matrix C of shape (ndof, n_master_nodes * ndof).

u_slave[i] = Σ_j w_j · u_master_j[i] for each DOF i

Source code in src/apeGmsh/_kernel/records/_constraints.py
def constraint_matrix(self, ndof: int = 3) -> ndarray:
    """
    Build the constraint matrix C of shape
    (ndof, n_master_nodes * ndof).

    u_slave[i] = Σ_j  w_j · u_master_j[i]   for each DOF i
    """
    n_master = len(self.master_nodes)
    n_dof = len(self.dofs)
    C = np.zeros((n_dof, n_master * n_dof))
    w = self.weights if self.weights is not None else np.ones(n_master) / n_master
    for row, dof in enumerate(self.dofs):
        for j in range(n_master):
            C[row, j * n_dof + row] = w[j]
    return C

SurfaceCouplingRecord dataclass

SurfaceCouplingRecord(kind: str, name: str | None = None, slave_records: list[InterpolationRecord] = list(), mortar_operator: ndarray | None = None, master_nodes: list[int] = list(), slave_nodes: list[int] = list(), dofs: list[int] = list())

Bases: ConstraintRecord

Surface-to-surface coupling operator.

Covers: tied_contact, mortar.

The coupling is stored as a sparse set of interpolation records (one per slave node for tied_contact), or as the full mortar operator matrix B.

Attributes

slave_records : list[InterpolationRecord] Per-slave-node interpolation data (for tied_contact). mortar_operator : ndarray or None Dense coupling matrix B (for mortar method). Shape: (n_slave_dofs, n_master_dofs). master_nodes : list[int] All master nodes involved. slave_nodes : list[int] All slave nodes involved. dofs : list[int]

NodeToSurfaceRecord dataclass

NodeToSurfaceRecord(kind: str, name: str | None = None, master_node: int = 0, slave_nodes: list[int] = list(), phantom_nodes: list[int] = list(), phantom_coords: ndarray | None = None, rigid_link_records: list[NodePairRecord] = list(), equal_dof_records: list[NodePairRecord] = list(), dofs: list[int] = (lambda: [1, 2, 3])())

Bases: ConstraintRecord

Compound record for 6-DOF node to 3-DOF surface coupling via phantom nodes.

This record encapsulates the three-step coupling:

  1. Phantom nodes duplicated from the original slave positions.
  2. Rigid links from the 6-DOF master to each phantom node.
  3. EqualDOF from each phantom node to the original slave (translations only).

Solvers consume this by: - Creating the phantom nodes (6-DOF, same coords as slaves). - Emitting rigid_beam constraints master -> phantom. - Emitting equal_dof constraints phantom -> slave for DOFs [1,2,3].

Attributes

master_node : int The 6-DOF master node tag. slave_nodes : list[int] Original 3-DOF slave node tags (from the surface mesh). phantom_nodes : list[int] Generated 6-DOF phantom node tags (one per slave, same coordinates). Tag generation is handled by the resolver using an offset above the maximum existing node tag. phantom_coords : ndarray Coordinates of phantom nodes, shape (n_slaves, 3). Identical to the slave coordinates. rigid_link_records : list[NodePairRecord] Master -> phantom rigid beam records (with offset vectors). equal_dof_records : list[NodePairRecord] Phantom -> slave equalDOF records (translations only). dofs : list[int] Translational DOFs coupled to the surface (default [1,2,3]).

expand
expand() -> list[NodePairRecord]

Flatten into individual :class:NodePairRecord objects.

Returns the rigid link records followed by the equalDOF records — the natural emission order for solvers.

Source code in src/apeGmsh/_kernel/records/_constraints.py
def expand(self) -> list[NodePairRecord]:
    """
    Flatten into individual :class:`NodePairRecord` objects.

    Returns the rigid link records followed by the equalDOF
    records — the natural emission order for solvers.
    """
    return list(self.rigid_link_records) + list(self.equal_dof_records)

Resolver

apeGmsh._kernel.resolvers._constraint_resolver._resolver.ConstraintResolver

ConstraintResolver(node_tags: ndarray, node_coords: ndarray, elem_tags: ndarray | None = None, connectivity: ndarray | None = None)

Converts constraint definitions into resolved records.

The resolver works with raw numpy arrays of node coordinates and connectivity — it does NOT depend on Gmsh or any solver. This makes it fully portable.

Parameters

node_tags : ndarray, shape (n_nodes,) Node tags (IDs) from the mesh. node_coords : ndarray, shape (n_nodes, 3) Nodal coordinates. elem_tags : ndarray, shape (n_elems,) Element tags. connectivity : ndarray, shape (n_elems, n_nodes_per_elem) Element connectivity (node tags). face_connectivity : list of ndarray, optional Element face connectivity for surface elements. If None, the resolver extracts faces from the volume connectivity.

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def __init__(
    self,
    node_tags: ndarray,
    node_coords: ndarray,
    elem_tags: ndarray | None = None,
    connectivity: ndarray | None = None,
) -> None:
    self.node_tags = np.asarray(node_tags, dtype=int)
    self.node_coords = np.asarray(node_coords, dtype=float)

    # Tag -> index mapping
    self._tag_to_idx: dict[int, int] = {
        int(t): i for i, t in enumerate(self.node_tags)
    }

    self.elem_tags = (
        np.asarray(elem_tags, dtype=int) if elem_tags is not None
        else None
    )
    self.connectivity = (
        np.asarray(connectivity, dtype=int) if connectivity is not None
        else None
    )

    # Running high-water mark for phantom node tag generation.
    # Each resolve_node_to_surface() call advances this so that
    # multiple calls never produce overlapping phantom tag ranges.
    self._next_phantom_tag: int = int(self.node_tags.max()) + 1

    # KD-tree for spatial queries (built lazily)
    self._tree = None

tree property

tree

Lazily build a KD-tree for nearest-neighbour queries.

resolve_equal_dof

resolve_equal_dof(defn: EqualDOFDef, master_nodes: set[int], slave_nodes: set[int]) -> list[NodePairRecord]

Resolve an EqualDOF definition into node pair records.

Parameters

defn : EqualDOFDef master_nodes : set[int] Node tags belonging to the master instance. slave_nodes : set[int] Node tags belonging to the slave instance.

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_equal_dof(
    self,
    defn: EqualDOFDef,
    master_nodes: set[int],
    slave_nodes: set[int],
) -> list[NodePairRecord]:
    """
    Resolve an EqualDOF definition into node pair records.

    Parameters
    ----------
    defn : EqualDOFDef
    master_nodes : set[int]
        Node tags belonging to the master instance.
    slave_nodes : set[int]
        Node tags belonging to the slave instance.
    """
    pairs = self._match_node_pairs(
        master_nodes, slave_nodes, defn.tolerance,
    )
    dofs = defn.dofs or [1, 2, 3, 4, 5, 6]
    return [
        NodePairRecord(
            kind=ConstraintKind.EQUAL_DOF,
            name=defn.name,
            master_node=mt,
            slave_node=st,
            dofs=list(dofs),
        )
        for mt, st in pairs
    ]
resolve_rigid_link(defn: RigidLinkDef, master_nodes: set[int], slave_nodes: set[int]) -> list[NodePairRecord]

Resolve a rigid link definition.

If master_point is specified, find the closest master node. Then link all slave nodes to that master via rigid offset.

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_rigid_link(
    self,
    defn: RigidLinkDef,
    master_nodes: set[int],
    slave_nodes: set[int],
) -> list[NodePairRecord]:
    """
    Resolve a rigid link definition.

    If ``master_point`` is specified, find the closest master node.
    Then link all slave nodes to that master via rigid offset.
    """
    # Find master node
    if defn.master_point is not None:
        master_tag, _ = self._closest_node_in_set(defn.master_point, master_nodes)
    else:
        if master_nodes:
            coords = np.array([self._coords_of(t) for t in master_nodes])
            centroid = coords.mean(axis=0)
            master_tag, _ = self._closest_node_in_set(centroid, master_nodes)
        else:
            centroid = self.node_coords.mean(axis=0)
            master_tag, _ = self._closest_node(centroid)

    master_xyz = self._coords_of(master_tag)
    kind = f"rigid_{defn.link_type}"

    if kind == ConstraintKind.RIGID_BEAM:
        dofs = [1, 2, 3, 4, 5, 6]
    else:
        dofs = [1, 2, 3]

    records = []
    for st in sorted(slave_nodes):
        if st == master_tag:
            continue
        slave_xyz = self._coords_of(st)
        offset = slave_xyz - master_xyz
        records.append(NodePairRecord(
            kind=kind,
            name=defn.name,
            master_node=master_tag,
            slave_node=st,
            dofs=list(dofs),
            offset=offset,
        ))
    return records

resolve_penalty

resolve_penalty(defn: PenaltyDef, master_nodes: set[int], slave_nodes: set[int]) -> list[NodePairRecord]

Resolve a penalty definition into node pair records.

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_penalty(
    self,
    defn: PenaltyDef,
    master_nodes: set[int],
    slave_nodes: set[int],
) -> list[NodePairRecord]:
    """Resolve a penalty definition into node pair records."""
    pairs = self._match_node_pairs(
        master_nodes, slave_nodes, defn.tolerance,
    )
    dofs = defn.dofs or [1, 2, 3, 4, 5, 6]
    return [
        NodePairRecord(
            kind=ConstraintKind.PENALTY,
            name=defn.name,
            master_node=mt,
            slave_node=st,
            dofs=list(dofs),
            penalty_stiffness=defn.stiffness,
        )
        for mt, st in pairs
    ]

resolve_rigid_diaphragm

resolve_rigid_diaphragm(defn: RigidDiaphragmDef, all_nodes: set[int]) -> NodeGroupRecord

Resolve a rigid diaphragm.

Collects all nodes within plane_tolerance of the diaphragm plane, then the closest to master_point becomes master.

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_rigid_diaphragm(
    self,
    defn: RigidDiaphragmDef,
    all_nodes: set[int],
) -> NodeGroupRecord:
    """
    Resolve a rigid diaphragm.

    Collects all nodes within ``plane_tolerance`` of the diaphragm
    plane, then the closest to ``master_point`` becomes master.
    """
    normal = np.asarray(defn.plane_normal, dtype=float)
    normal = normal / np.linalg.norm(normal)
    mp = np.asarray(defn.master_point, dtype=float)
    d = np.dot(normal, mp)

    # Collect nodes near the plane
    plane_nodes = []
    for tag in all_nodes:
        c = self._coords_of(tag)
        dist_to_plane = abs(np.dot(normal, c) - d)
        if dist_to_plane <= defn.plane_tolerance:
            plane_nodes.append(tag)

    if not plane_nodes:
        return NodeGroupRecord(
            kind=ConstraintKind.RIGID_DIAPHRAGM,
            name=defn.name,
            dofs=list(defn.constrained_dofs),
        )

    # Find master: closest to master_point
    master_tag, _ = self._closest_node(mp)
    if master_tag not in plane_nodes:
        # Pick the closest plane node instead
        dists = [np.linalg.norm(self._coords_of(t) - mp)
                 for t in plane_nodes]
        master_tag = plane_nodes[int(np.argmin(dists))]

    slave_tags = [t for t in plane_nodes if t != master_tag]
    master_xyz = self._coords_of(master_tag)
    offsets = np.array([
        self._coords_of(t) - master_xyz for t in slave_tags
    ]) if slave_tags else None

    return NodeGroupRecord(
        kind=ConstraintKind.RIGID_DIAPHRAGM,
        name=defn.name,
        master_node=master_tag,
        slave_nodes=slave_tags,
        dofs=list(defn.constrained_dofs),
        offsets=offsets,
        plane_normal=normal,
    )

resolve_kinematic_coupling

resolve_kinematic_coupling(defn: KinematicCouplingDef | RigidBodyDef, master_nodes: set[int], slave_nodes: set[int]) -> NodeGroupRecord

Resolve kinematic coupling or rigid body constraint.

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_kinematic_coupling(
    self,
    defn: KinematicCouplingDef | RigidBodyDef,
    master_nodes: set[int],
    slave_nodes: set[int],
) -> NodeGroupRecord:
    """
    Resolve kinematic coupling or rigid body constraint.
    """
    master_tag, _ = self._closest_node_in_set(defn.master_point, master_nodes)
    master_xyz = self._coords_of(master_tag)

    slaves = sorted(slave_nodes - {master_tag})
    offsets = np.array([
        self._coords_of(t) - master_xyz for t in slaves
    ]) if slaves else None

    if isinstance(defn, RigidBodyDef):
        dofs = [1, 2, 3, 4, 5, 6]
    else:
        dofs = list(defn.dofs)

    return NodeGroupRecord(
        kind=defn.kind,
        name=defn.name,
        master_node=master_tag,
        slave_nodes=slaves,
        dofs=dofs,
        offsets=offsets,
    )

resolve_tie

resolve_tie(defn: TieDef, master_face_conn: ndarray, slave_nodes: set[int]) -> list[InterpolationRecord]

Resolve a surface tie via closest-point projection.

For each slave node, find the closest master face, project onto it, and compute shape function weights.

Parameters

defn : TieDef master_face_conn : ndarray, shape (n_faces, n_nodes_per_face) Connectivity of master surface element faces (node tags). slave_nodes : set[int] Slave node tags to project.

Returns

list[InterpolationRecord]

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_tie(
    self,
    defn: TieDef,
    master_face_conn: ndarray,
    slave_nodes: set[int],
) -> list[InterpolationRecord]:
    """
    Resolve a surface tie via closest-point projection.

    For each slave node, find the closest master face, project
    onto it, and compute shape function weights.

    Parameters
    ----------
    defn : TieDef
    master_face_conn : ndarray, shape (n_faces, n_nodes_per_face)
        Connectivity of master surface element faces (node tags).
    slave_nodes : set[int]
        Slave node tags to project.

    Returns
    -------
    list[InterpolationRecord]
    """
    dofs = defn.dofs or [1, 2, 3]
    records = []

    # Pre-compute face centroids for quick nearest-face search
    n_faces = master_face_conn.shape[0]
    n_fpn = master_face_conn.shape[1]
    face_centroids = np.zeros((n_faces, 3))
    face_coords_list = []
    for fi in range(n_faces):
        nodes = master_face_conn[fi]
        coords = np.array([self._coords_of(int(n)) for n in nodes])
        face_coords_list.append(coords)
        face_centroids[fi] = coords.mean(axis=0)

    face_tree = _SpatialIndex(face_centroids)

    for st in sorted(slave_nodes):
        s_xyz = self._coords_of(st)

        # Find K nearest face centroids, try projection on each
        K = min(5, n_faces)
        _, face_indices = face_tree.query(s_xyz, k=K)
        if isinstance(face_indices, (int, np.integer)):
            face_indices = [face_indices]

        best_dist = float('inf')
        best_record = None

        for fi in face_indices:
            fi = int(fi)
            fc = face_coords_list[fi]
            fn = master_face_conn[fi]

            try:
                xi_eta, proj, dist = _project_point_to_face(s_xyz, fc)
            except Exception:
                continue

            if dist > defn.tolerance:
                continue

            if not _is_inside_parametric(xi_eta, n_fpn):
                continue

            if dist < best_dist:
                best_dist = dist
                shape_fn = SHAPE_FUNCTIONS[n_fpn]
                weights = shape_fn(xi_eta[0], xi_eta[1])

                best_record = InterpolationRecord(
                    kind=ConstraintKind.TIE,
                    name=defn.name,
                    slave_node=st,
                    master_nodes=[int(n) for n in fn],
                    weights=weights,
                    dofs=list(dofs),
                    projected_point=proj,
                    parametric_coords=xi_eta,
                )

        if best_record is not None:
            records.append(best_record)

    return records

resolve_distributing

resolve_distributing(defn: DistributingCouplingDef, master_nodes: set[int], slave_nodes: set[int]) -> InterpolationRecord

Not implemented — raises NotImplementedError.

Defence-in-depth: the distributing_coupling factory already refuses (see ConstraintsComposite). This guards the case where a DistributingCouplingDef is hand-constructed and dispatched directly — it must not silently emit the old mechanically-wrong kinematic-mean record.

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_distributing(
    self,
    defn: DistributingCouplingDef,
    master_nodes: set[int],
    slave_nodes: set[int],
) -> InterpolationRecord:
    """Not implemented — raises ``NotImplementedError``.

    Defence-in-depth: the ``distributing_coupling`` factory
    already refuses (see ConstraintsComposite).  This guards the
    case where a ``DistributingCouplingDef`` is hand-constructed
    and dispatched directly — it must not silently emit the old
    mechanically-wrong kinematic-mean record.
    """
    raise NotImplementedError(
        "resolve_distributing: RBE3 force distribution is not "
        "implemented; the prior kinematic-mean implementation was "
        "mechanically wrong.  Use kinematic_coupling / tie / a "
        "distributed nodal load instead."
    )

resolve_tied_contact

resolve_tied_contact(defn: TiedContactDef, master_face_conn: ndarray, slave_face_conn: ndarray, master_nodes: set[int], slave_nodes: set[int]) -> SurfaceCouplingRecord

Resolve a surface-to-surface tie — one-directional.

Slave-surface nodes are interpolated onto the master faces (the standard tied-contact / Abaqus *TIE convention: the slave conforms to the master, which is the reference).

The previous implementation also projected master nodes onto slave faces and concatenated both directions — a node could then be a slave in one direction and a master-face node in the other, producing cyclic / over-determined MPCs the constraint handler cannot satisfy. slave_face_conn is accepted for dispatch-signature stability but unused.

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_tied_contact(
    self,
    defn: TiedContactDef,
    master_face_conn: ndarray,
    slave_face_conn: ndarray,
    master_nodes: set[int],
    slave_nodes: set[int],
) -> SurfaceCouplingRecord:
    """Resolve a surface-to-surface tie — **one-directional**.

    Slave-surface nodes are interpolated onto the master faces
    (the standard tied-contact / Abaqus ``*TIE`` convention: the
    slave conforms to the master, which is the reference).

    The previous implementation also projected master nodes onto
    slave faces and concatenated both directions — a node could
    then be a slave in one direction and a master-face node in
    the other, producing cyclic / over-determined MPCs the
    constraint handler cannot satisfy.  ``slave_face_conn`` is
    accepted for dispatch-signature stability but unused.
    """
    dofs = defn.dofs or [1, 2, 3]

    # Slave nodes -> master faces (slave conforms to master).
    tie_fwd = TieDef(
        master_label=defn.master_label,
        slave_label=defn.slave_label,
        tolerance=defn.tolerance,
        dofs=dofs,
    )
    all_records = self.resolve_tie(
        tie_fwd, master_face_conn, slave_nodes,
    )

    return SurfaceCouplingRecord(
        kind=ConstraintKind.TIED_CONTACT,
        name=defn.name,
        slave_records=all_records,
        master_nodes=sorted(master_nodes),
        slave_nodes=sorted(slave_nodes),
        dofs=list(dofs),
    )

resolve_mortar

resolve_mortar(defn: MortarDef, master_face_conn: ndarray, slave_face_conn: ndarray, master_nodes: set[int], slave_nodes: set[int]) -> SurfaceCouplingRecord

Not implemented — raises NotImplementedError.

Defence-in-depth: the mortar factory already refuses (see ConstraintsComposite). This guards a hand-constructed MortarDef dispatched directly — it must not silently emit the old collocation-tie operator mislabelled MORTAR with a unit-dependent hardcoded tolerance.

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_mortar(
    self,
    defn: MortarDef,
    master_face_conn: ndarray,
    slave_face_conn: ndarray,
    master_nodes: set[int],
    slave_nodes: set[int],
) -> SurfaceCouplingRecord:
    """Not implemented — raises ``NotImplementedError``.

    Defence-in-depth: the ``mortar`` factory already refuses (see
    ConstraintsComposite).  This guards a hand-constructed
    ``MortarDef`` dispatched directly — it must not silently emit
    the old collocation-tie operator mislabelled ``MORTAR`` with a
    unit-dependent hardcoded tolerance.
    """
    raise NotImplementedError(
        "resolve_mortar: ∫ ψ·N dΓ Lagrange-multiplier coupling is "
        "not implemented; the prior implementation was a "
        "collocation tie (hardcoded tolerance=10.0) mislabelled "
        "MORTAR.  Use tied_contact instead."
    )

resolve_node_to_surface

resolve_node_to_surface(defn: NodeToSurfaceDef, master_tag: int, slave_nodes: set[int]) -> NodeToSurfaceRecord

Resolve a 6-DOF node to 3-DOF surface coupling.

Steps:

  1. Use the master node tag directly (already resolved from master_label as bare node tag).
  2. Generate phantom node tags — one per slave, starting at max(all_existing_tags) + 1.
  3. Build rigid-beam records: master -> each phantom.
  4. Build equalDOF records: each phantom -> original slave (translations only).
Parameters

defn : NodeToSurfaceDef master_tag : int The 6-DOF master node tag (dim=0). slave_nodes : set[int] Node tags belonging to the slave surface (dim=2, 3-DOF).

Returns

NodeToSurfaceRecord

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_node_to_surface(
    self,
    defn: NodeToSurfaceDef,
    master_tag: int,
    slave_nodes: set[int],
) -> NodeToSurfaceRecord:
    """
    Resolve a 6-DOF node to 3-DOF surface coupling.

    Steps:

    1. Use the master node tag directly (already resolved from
       ``master_label`` as bare node tag).
    2. Generate phantom node tags — one per slave, starting at
       ``max(all_existing_tags) + 1``.
    3. Build rigid-beam records: master -> each phantom.
    4. Build equalDOF records: each phantom -> original slave
       (translations only).

    Parameters
    ----------
    defn : NodeToSurfaceDef
    master_tag : int
        The 6-DOF master node tag (dim=0).
    slave_nodes : set[int]
        Node tags belonging to the slave surface (dim=2, 3-DOF).

    Returns
    -------
    NodeToSurfaceRecord
    """

    master_xyz = self._coords_of(master_tag)
    slave_list = sorted(slave_nodes - {master_tag})
    dofs = defn.dofs or [1, 2, 3]

    # -- 2. Generate phantom node tags (unique across calls) --
    start = self._next_phantom_tag
    phantom_tags = list(range(start, start + len(slave_list)))
    self._next_phantom_tag = start + len(slave_list)

    phantom_coords = np.array([
        self._coords_of(t) for t in slave_list
    ])

    # -- 3. Rigid beam: master -> phantom --
    # No dofs list: OpenSees `rigidLink('beam', ...)` picks DOFs
    # from the model's ndf at emit time. The caller's DOF space is
    # not known at resolve time and apeGmsh refuses to guess.
    rigid_records = []
    for phantom_tag, slave_tag in zip(phantom_tags, slave_list):
        slave_xyz = self._coords_of(slave_tag)
        offset = slave_xyz - master_xyz
        rigid_records.append(NodePairRecord(
            kind=ConstraintKind.RIGID_BEAM,
            name=defn.name,
            master_node=master_tag,
            slave_node=phantom_tag,
            offset=offset,
        ))

    # -- 4. EqualDOF: phantom -> slave (translations only) --
    edof_records = []
    for phantom_tag, slave_tag in zip(phantom_tags, slave_list):
        edof_records.append(NodePairRecord(
            kind=ConstraintKind.EQUAL_DOF,
            name=defn.name,
            master_node=phantom_tag,
            slave_node=slave_tag,
            dofs=list(dofs),
        ))

    return NodeToSurfaceRecord(
        kind=ConstraintKind.NODE_TO_SURFACE,
        name=defn.name,
        master_node=master_tag,
        slave_nodes=slave_list,
        phantom_nodes=phantom_tags,
        phantom_coords=phantom_coords,
        rigid_link_records=rigid_records,
        equal_dof_records=edof_records,
        dofs=list(dofs),
    )

resolve_embedded

resolve_embedded(defn, host_elems: ndarray, embedded_nodes: set[int] | list[int]) -> list[InterpolationRecord]

Resolve an embedded-element constraint.

Each embedded node is located inside a host element (tri3 in 2D or tet4 in 3D) via barycentric coordinates. The resulting shape-function weights couple the embedded node to the host element's corner nodes, matching the kinematics of ASDEmbeddedNodeElement in OpenSees.

Parameters

defn : EmbeddedDef Only defn.tolerance and defn.name are consulted. host_elems : ndarray, shape (n_elems, 3 | 4) Node-tag connectivity of the host elements. A row of 3 is treated as tri3; a row of 4 is treated as tet4. embedded_nodes : iterable of int Node tags to embed.

Returns

list[InterpolationRecord] One record per embedded node successfully located.

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_embedded(
    self,
    defn,
    host_elems: ndarray,
    embedded_nodes: set[int] | list[int],
) -> list[InterpolationRecord]:
    """
    Resolve an embedded-element constraint.

    Each embedded node is located inside a host element (tri3 in
    2D or tet4 in 3D) via barycentric coordinates. The resulting
    shape-function weights couple the embedded node to the host
    element's corner nodes, matching the kinematics of
    ``ASDEmbeddedNodeElement`` in OpenSees.

    Parameters
    ----------
    defn : EmbeddedDef
        Only ``defn.tolerance`` and ``defn.name`` are consulted.
    host_elems : ndarray, shape (n_elems, 3 | 4)
        Node-tag connectivity of the host elements. A row of 3
        is treated as tri3; a row of 4 is treated as tet4.
    embedded_nodes : iterable of int
        Node tags to embed.

    Returns
    -------
    list[InterpolationRecord]
        One record per embedded node successfully located.
    """
    host_elems = np.asarray(host_elems, dtype=int)
    if host_elems.ndim != 2 or host_elems.shape[0] == 0:
        return []

    npe = int(host_elems.shape[1])
    if npe not in (3, 4):
        raise ValueError(
            f"resolve_embedded: host elements must be tri3 (npe=3) "
            f"or tet4 (npe=4), got npe={npe}"
        )

    # Pre-compute corner coords per element and centroids for the
    # nearest-element search.
    n_elems = host_elems.shape[0]
    host_coords = np.zeros((n_elems, npe, 3), dtype=float)
    for ei in range(n_elems):
        for ni in range(npe):
            host_coords[ei, ni] = self._coords_of(int(host_elems[ei, ni]))
    centroids = host_coords.mean(axis=1)
    centroid_tree = _SpatialIndex(centroids)

    tol = float(defn.tolerance)
    # Barycentric out-of-element tolerance is unitless; keep it
    # small so we don't falsely claim a node sits inside an element
    # it is only grazing.
    bary_tol = 1e-6

    records: list[InterpolationRecord] = []
    K = min(16, n_elems)

    for en in sorted(int(t) for t in embedded_nodes):
        p = self._coords_of(en)
        _, cand = centroid_tree.query(p, k=K)
        if isinstance(cand, (int, np.integer)):
            cand = [int(cand)]
        else:
            cand = [int(c) for c in np.atleast_1d(cand)]

        best_record: InterpolationRecord | None = None
        best_excess = float("inf")

        for ei in cand:
            corners = host_coords[ei]
            if npe == 3:
                weights, excess, xi_eta = _barycentric_tri3(p, corners)
                parametric = xi_eta
            else:
                weights, excess, xi_etz = _barycentric_tet4(p, corners)
                parametric = xi_etz

            if excess is None:
                continue

            # "Inside" when all barycentric coords are non-negative
            # within bary_tol. Take the first hit; if none is fully
            # inside, keep the one with the smallest excess so the
            # caller can inspect via the log.
            if excess < best_excess:
                best_excess = excess
                best_record = InterpolationRecord(
                    kind=ConstraintKind.EMBEDDED,
                    name=defn.name,
                    slave_node=en,
                    master_nodes=[int(t) for t in host_elems[ei]],
                    weights=weights,
                    dofs=[1, 2, 3],
                    projected_point=p.copy(),
                    parametric_coords=parametric,
                )
            if excess <= bary_tol:
                break

        if best_record is None:
            continue

        # Use defn.tolerance as a soft gate on barycentric excess
        # scaled by a characteristic host edge length. We simply
        # accept any located record; the caller (composite) decides
        # whether to warn.
        records.append(best_record)

    return records

resolve_node_to_surface_spring

resolve_node_to_surface_spring(defn: 'NodeToSurfaceSpringDef', master_tag: int, slave_nodes: set[int]) -> NodeToSurfaceRecord

Resolve a spring-variant 6-DOF → 3-DOF surface coupling.

Identical phantom-node generation and equalDOF records as :meth:resolve_node_to_surface. The only difference is that the master → phantom rigid-link records are tagged with kind='rigid_beam_stiff' so they are routed through stiff_beam_groups() at emission time (becoming stiff elasticBeamColumn elements) instead of rigid_link_groups() (which would emit rigidLink and hit the ill-conditioning described in :class:NodeToSurfaceSpringDef).

Source code in src/apeGmsh/_kernel/resolvers/_constraint_resolver/_resolver.py
def resolve_node_to_surface_spring(
    self,
    defn: "NodeToSurfaceSpringDef",
    master_tag: int,
    slave_nodes: set[int],
) -> NodeToSurfaceRecord:
    """
    Resolve a spring-variant 6-DOF → 3-DOF surface coupling.

    Identical phantom-node generation and equalDOF records as
    :meth:`resolve_node_to_surface`. The only difference is that
    the master → phantom rigid-link records are tagged with
    ``kind='rigid_beam_stiff'`` so they are routed through
    ``stiff_beam_groups()`` at emission time (becoming stiff
    ``elasticBeamColumn`` elements) instead of
    ``rigid_link_groups()`` (which would emit ``rigidLink`` and
    hit the ill-conditioning described in
    :class:`NodeToSurfaceSpringDef`).
    """

    master_xyz = self._coords_of(master_tag)
    slave_list = sorted(slave_nodes - {master_tag})
    dofs = defn.dofs or [1, 2, 3]

    start = self._next_phantom_tag
    phantom_tags = list(range(start, start + len(slave_list)))
    self._next_phantom_tag = start + len(slave_list)

    phantom_coords = np.array([
        self._coords_of(t) for t in slave_list
    ])

    # Stiff beams: master → phantom. Same structure as the
    # constraint-based variant but tagged with a distinct kind so
    # the mesh iterators can route them to the element emission
    # path.
    stiff_records = []
    for phantom_tag, slave_tag in zip(phantom_tags, slave_list):
        slave_xyz = self._coords_of(slave_tag)
        offset = slave_xyz - master_xyz
        stiff_records.append(NodePairRecord(
            kind=ConstraintKind.RIGID_BEAM_STIFF,
            name=defn.name,
            master_node=master_tag,
            slave_node=phantom_tag,
            offset=offset,
        ))

    edof_records = []
    for phantom_tag, slave_tag in zip(phantom_tags, slave_list):
        edof_records.append(NodePairRecord(
            kind=ConstraintKind.EQUAL_DOF,
            name=defn.name,
            master_node=phantom_tag,
            slave_node=slave_tag,
            dofs=list(dofs),
        ))

    return NodeToSurfaceRecord(
        kind=ConstraintKind.NODE_TO_SURFACE_SPRING,
        name=defn.name,
        master_node=master_tag,
        slave_nodes=slave_list,
        phantom_nodes=phantom_tags,
        phantom_coords=phantom_coords,
        rigid_link_records=stiff_records,
        equal_dof_records=edof_records,
        dofs=list(dofs),
    )

Module shim

The top-level apeGmsh.core.ConstraintsComposite module re-exports all public names from the _constraint_* modules for backwards compatibility. Module-level docstring contains the canonical taxonomy.

apeGmsh.core.ConstraintsComposite

ConstraintsComposite -- Define and resolve kinematic constraints.

Two-stage pipeline:

  1. Define (pre-mesh): factory methods store :class:ConstraintDef objects describing geometric intent.
  2. Resolve (post-mesh): :meth:resolve delegates to :class:ConstraintResolver (in solvers/Constraints.py) with caller-provided node/face maps. Dependency-injected -- this module never imports PartsRegistry.

Usage::

g.constraints.equal_dof("beam", "slab", tolerance=1e-3)
g.constraints.tie("beam", "slab", master_entities=[(2, 5)])

fem = g.mesh.queries.get_fem_data(dim=2)
nm  = g.parts.build_node_map(fem.nodes.ids, fem.nodes.coords)
fm  = g.parts.build_face_map(nm)
recs = g.constraints.resolve(
    fem.nodes.ids, fem.nodes.coords, node_map=nm, face_map=fm,
)