Boundary conditions and loads#

Dirichlet constraints (fixed DOFs) and nodal forces / moments are the minimum set of boundary conditions every structural analysis needs. The native, Python-first surface is fix() for Dirichlet and apply_force() for loads. The legacy APDL-dialect verbs (d / f) are still supported through the APDL wrapper in femorph_solver.interop.mapdl for users porting a MAPDL deck line-by-line.

DOF labels#

Every node has at most six DOFs, drawn from the enum femorph_solver.DOFLabel:

Label

Meaning

UX, UY, UZ

Translational displacement along global x / y / z.

ROTX, ROTY, ROTZ

Rotation about global x / y / z (beam / shell only).

The effective DOF set at a node is the union of the DOFs every adjacent element needs — a QUAD4_SHELL (SHELL181) node has all six; a HEX8 (SOLID185) node has three (UX, UY, UZ); a node touched by both sees all six. Rotational DOFs on a solid-only region are silently dropped.

Loads use femorph_solver.ForceLabel — the parallel enum with FX / FY / FZ and MX / MY / MZ.

Model.fix — Dirichlet constraints#

Model.fix takes a boolean mask over the grid’s nodes (or an explicit node-id array) plus the DOF label(s) to pin. Typical usage on the bundled quarter-arc example:

import numpy as np
import femorph_solver as fs

grid = fs.examples.quarter_arc_sector()
model = fs.Model.from_grid(grid)
model.assign("HEX8", {"EX": 2.1e11, "PRXY": 0.30, "DENS": 7850.0})

pts = np.asarray(grid.points)
clamped = np.isclose(pts[:, 2], pts[:, 2].min())

model.fix(where=clamped, dof=["UX", "UY", "UZ"])   # clamp the base
model.fix(where=pts[:, 0] > 0.99, dof="UZ", value=1e-3)  # prescribed z-disp

The same call accepts either the raw string or a DOFLabel member — both "UX" and DOFLabel.UX resolve to the same DOF index.

Multiple fix calls with the same (node, label) pair overwrite, so re-pinning a DOF updates its value.

Model.apply_force — nodal forces / moments#

Model.apply_force is keyword-driven — one call can set several components on a single node:

model.apply_force(101, fy=-1000.0)          # 1 kN downward at node 101
model.apply_force(101, fy=-1000.0, mz=50.0) # or several components at once

As with fix(), labels pass through the ForceLabel enum; unknown labels raise ValueError at call time rather than silently masking a zero.

Inspecting BC state#

Model.bcs and Model.loads are typed views over the active constraints and loads:

>>> model.bcs
BoundaryConditions(n_pinned=12)
>>> model.bcs.to_table()   # DataFrame-like summary
>>> model.loads
Loads(n_applied=1)

Both objects surface clear() / pin() / apply() helpers so you can tweak state imperatively without reaching into the Model’s internals.

The prescribed= argument#

Every solver entry point also accepts a prescribed dict so you can bypass the Model’s BC records and set constraints directly from an array-driven script:

K = model.stiffness_matrix()
M = model.mass_matrix()

# Fix DOFs for nodes 1..10 in all three translational directions.
dof_map = model.dof_map()
fixed_dofs = {
    i for i, (node, d) in enumerate(dof_map)
    if node <= 10 and d < 3      # 0=UX, 1=UY, 2=UZ
}
prescribed = {i: 0.0 for i in fixed_dofs}

result = solve_modal(K, M, prescribed=prescribed, n_modes=10)

Model.solve() / Model.modal_solve() / Model.transient_solve() merge the Model’s BC records into this dict automatically — use prescribed only when you’re calling solve_modal / solve_static / solve_transient directly.

Cyclic-symmetry face constraints#

Cyclic-symmetry analyses add a second kind of constraint: paired low-face / high-face DOFs relate by a phase-dependent rotation. That’s a first-class argument to solve_cyclic_modal() rather than a BC record — see Cyclic-symmetry modal analysis.

Porting an APDL deck#

Users porting a MAPDL deck line-by-line can reach the APDL-dialect verbs (D / F) through the context-managed wrapper:

from femorph_solver.interop.mapdl import APDL

with APDL(model) as apdl:
    apdl.d(1, "ALL")               # pin all DOFs at node 1
    apdl.f(101, "FY", -1000.0)     # nodal force

APDL writes into the same Model.bcs / Model.loads state as fix() / apply_force(); the two styles are interchangeable and can be mixed in one model. Calling d / f directly on Model emits a DeprecationWarning that points at these two replacements.

Limitations (today)#

  • Only zero-value Dirichlet is lossless. Non-zero fix(where=..., value=...) constraints work in solve_static() but the modal / cyclic paths require zero-value BCs at prescribed DOFs (they drop the row/column entirely rather than shifting the eigenproblem).

  • No inertia / body-force loads. ACEL-style gravity isn’t exposed yet; build the equivalent nodal force yourself if you need a body load.

  • No element-face pressure. SFE pressure loads are parsed from a CDB but not converted into equivalent nodal forces.

See also#

  • The Model — grid construction and Model.assign() for the element + material setup that bracket BC application.

  • Static analysis — how K u = F is partitioned when Dirichlet DOFs are present.