Isoparametric machinery shared by the continuum elements: shape
functions and their reference derivatives (shape), Gauss rules
(quadrature), the Jacobian and physical derivatives (jacobian), and
scale-aware geometry validation (validate). Everything here is
dimension-agnostic bookkeeping; what is specific to an element (the B
matrix shape, the constitutive reduction) stays in the element.
A 2-node, displacement-based, fiber-discretized 2D beam-column:
nodal displacements directly give the strain/curvature field along the
length (cubic Hermite transverse + linear axial shape functions, same
as ElasticBeamColumn’s), each BeamIntegration point’s FiberSection
converts that to stress-resultants, and one length-integral gives the
element’s resisting force/tangent — a single, direct evaluation, unlike
ForceBeamColumn, which needs its own internal equilibrium
iteration per element.
DispBeamColumn’s spatial (biaxial) counterpart: a 2-node,
displacement-based, fiber-discretized 3D beam-column. Bending in both
planes comes directly from FiberSection3 the same way DispBeamColumn
gets its single bending plane from FiberSection; torsion is not
fiber-derived (see FiberSection3’s doc comment) and is instead a
simple decoupled elastic G*J/L term, folded directly into the local
tangent/resistance alongside the fiber-integrated axial/biaxial-bending
terms — the same decoupling ElasticBeamColumn3 uses for torsion.
Owns all nodes, elements, and load patterns. No serialization/broker
machinery — this is the whole model, in memory, for one worker.
Clone backs Analysis/TransientAnalysis’s snapshot-and-restore-on-
failure (a failed step shouldn’t leave nodal displacement/velocity/
acceleration partway through a discarded Newton iteration) — see
Material’s doc comment for why materials themselves don’t need this
protection. Also what makes multi-phase analysis composition possible:
Analysis::into_domain/TransientAnalysis::into_domain hand this same
Domain (nodal state, committed material history, and load-pattern
freeze state all intact) to the next phase’s builder.
A 2-node, prismatic, linear-elastic 2D beam-column (Euler-Bernoulli, no
shear deformation): closed-form stiffness, no iteration — unlike
Truss/ZeroLength, there’s no Material dispatch here, since the
element’s response is fully determined by e/a/iz with no nonlinear
stress-strain law to evaluate.
ElasticBeamColumn’s spatial counterpart: a 2-node, prismatic,
linear-elastic 3D Euler-Bernoulli beam-column (E, G, A, J, Iy, Iz),
closed-form, GeomTransf3::Linear3 only — see this struct’s
module-level context for why 3D P-Delta isn’t implemented yet: it needs
force/tangent recovery already consistent in the linear case first.
An element’s local nodal force (or equivalent-load) vector, for results
recording. Its length is ElementOps::local_force_width. Heap-backed:
this is only ever used on recorder paths, never in assembly.
The load on a 2D element: an additive accumulator with one field group per
load kind, so that the sum of every pattern’s load on an element (each
scaled by its factor) is just a field-wise sum. Uniform + Body is simply
both fields set. Which kinds an element accepts is decided by
ElementOps::accepts_load, checked by Domain::validate before any load
is accumulated.
One fiber: a small patch of cross-section area at distance y from the
section centroid, carrying a Material. 2D only: y alone, no z,
since bending is about the section’s local z-axis only.
FiberSection’s spatial (biaxial) counterpart: each fiber carries both
a y and a z coordinate, since bending can now occur about either
local section axis. Torsion is deliberately not modeled here — a
fiber’s uniaxial material only sees axial strain, so torsional
stiffness needs a separate (elastic, G*J) term, kept on the element
(DispBeamColumn3/ForceBeamColumn3) rather than the section, the same
decoupled treatment ElasticBeamColumn3 already uses. This mirrors
Xara’s FiberSection3d, which likewise keeps torsion out of the fiber
loop and folds it in afterward via a separate elastic term.
A fiber-discretized cross-section: given the section’s axial strain
eps0 (at the centroid) and curvature kappa, each fiber’s strain is
eps0 - y*kappa. Work-conjugate to [eps0, kappa] is [N, M]
(N = sum(stress*area), M = -sum(stress*area*y)) — derived directly
from virtual work, not assumed: d(stress power) = integral(stress * d(strain)) dA = integral(stress*(d(eps0) - y*d(kappa))) dA = d(eps0)*N + d(kappa)*M with M as defined above.
Biaxial fiber-discretized cross-section: given axial strain eps0,
curvature about local z kappa_z (bending that displaces local y, the
plane FiberSection/ElasticBeamColumn3’s Iz governs), and curvature
about local y kappa_y (displaces local z, Iy’s plane), each fiber’s
strain is eps0 - y*kappa_z + z*kappa_y — the natural biaxial extension
of FiberSection’s eps0 - y*kappa, verified against Xara’s
FiberSection3d::addFiber/getStressResultant (matData/sData
layout: strain = e0 - y*e1 + z*e2, N = sum(stress*A),
Mz = sum(-y*stress*A), My = sum(z*stress*A)) rather than re-derived
from scratch.
A 2-node, force-based (flexibility-method) 2D beam-column:
unlike every other element in the catalog, its section forces are
determined directly from the element’s basic (reduced, rigid-body-mode-
free) end forces via statics-exact force-interpolation functions —
always in equilibrium, regardless of material state — while section
deformations (and hence the element’s tangent) come from inverting each
section’s nonlinear force-deformation response. That inversion is why
this element needs its own internal Newton iteration
(state_determination) every time form_tangent_and_resistance is
called, instead of a single direct evaluation like DispBeamColumn.
ForceBeamColumn’s spatial (biaxial) counterpart: a 2-node, force-based
3D beam-column. Basic system, force interpolation, and state-
determination algorithm are the direct biaxial generalization of
ForceBeamColumn’s (see that type’s doc comment for the algorithm
itself, ported unchanged in structure — only NSD/NBD and the
matrices below grow); torsion is excluded from the basic system and
handled as a decoupled elastic G*J/L term, same as DispBeamColumn3.
Couples one “shear” DOF to a different DOF’s own material — the
normal direction’s current force — via a bilinear-kinematic-hardening
Coulomb return map: sticks (stiffness k0) until |V| reaches
mu * yield-eligible normal force, then slides with post-slip tangent
b*k0 (see the b field doc comment for why b is required, not
optional).
Friction’s spatial counterpart: one normal DOF, two shear DOFs, each
run independently through Friction’s return map against the same
normal force (a square, not circular, interaction surface between the
two shear directions — the physically correct circular friction cone is
a materially harder shared-slip-vector return-map problem, deferred
until needed).
One integration point’s committed strain and stress, for continuum
elements. Plane elements report [x, y, xy] with engineering shear
(PlaneMaterial’s convention).
A node in a concrete kinematic profile. Coordinates have NDIM
components; state, boundary conditions, and mass have NDOF components.
Node defaults to the existing planar (2, 3) profile, so its public API
remains source-compatible; Node3 is the spatial (3, 6) specialization.
Generational index for a future spatial Domain3 node store. It is
distinct from NodeId, so planar elements cannot connect to spatial
nodes at compile time.
Generational index into the planar Domain node store. Stays valid
across removals of other nodes; using a stale key against a different
domain is a logic error the type cannot catch, but a use-after-free
within one domain’s lifetime is.
Read access to the domain’s nodes, handed to every element method that
needs node coordinates or state. A thin wrapper so the element interface
does not expose how Domain stores nodes.
Local axes of a zero-length element, built the OpenSees way. In 3D that
is -orient x1 x2 x3 yp1 yp2 yp3: local x is x, local z is x × yp, and
local y is z × x. In 2D OpenSees takes only x (see in_plane) and
derives y by a 90 degree turn. Every per-DOF material (and a section’s axial and
flexural directions) is then evaluated on the relative displacement and
rotation projected onto these axes rather than the global ones.
Without an orientation the local axes are the global axes.
One shell integration point’s committed generalized strain and stress resultants in the
element’s local axes, in ShellSection’s ordering and sign convention:
[eps_x, eps_y, gamma_xy, kappa_x, kappa_y, 2 kappa_xy, gamma_xz, gamma_yz] and
[Nx, Ny, Nxy, Mx, My, Mxy, Qx, Qy].
A 2-node axial truss: fixed-size element-local linear algebra, no
heap allocation in the hot path. Only ever populates the translational
DOF entries of the (3-DOF-per-node) local/global matrices; the rotation
entries stay zero. Its dof_mask is therefore translations only, so a
node connected only to trusses has no rotation unknown and the model
need not fix it.
A 2-node, zero-length connector: no geometry or integration, just direct
per-DOF material evaluation — each direction with a material
assigned independently relates that DOF’s relative displacement between
the two nodes to a force along that same direction. Directions are the
global DOF axes (including rotation) unless an
Orientation (OpenSees’ -orient) rotates them to local axes.
ZeroLength‘s spatial counterpart: independent per-DOF materials along
the six directions [ux, uy, uz, rx, ry, rz] — global by default, or the
local axes of an Orientation (OpenSees’ -orient).
friction — see ZeroLength’s doc comment — adds two independent
(see Friction3) shear-DOF couplings on top of the
independent materials.
A 2-node, zero-length connector driven by a coupled FiberSection
instead of ZeroLength’s independent per-DOF materials — the axial
(ux) and flexural (rz) response come out of the same fiber
discretization, so axial force and bending moment interact through
shared fiber strain, the way a concentrated-plasticity “fiber hinge”
needs (e.g. at a beam-column end) and ZeroLength structurally cannot
produce. Modeled on OpenSees/Xara’s ZeroLengthSection
(SRC/element/Point/ZeroLengthSection.cpp): the section’s basic
deformation is the direct nodal DOF difference (no shape functions or
integration — there’s no length to integrate over), and K/P are
Bᵀ*k_section*B/Bᵀ*q with B selecting [ux, rz], the same pattern
DispBeamColumn::form_tangent_and_resistance uses per integration point
(here with a single “point” and unit scale).
ZeroLengthSection’s spatial counterpart: coupled axial-biaxial-moment
response (ux, ry, rz) from a FiberSection3, plus optional
independent springs (via with_material) for uy, uz (shear) and
rx (torsion) — the three DOFs the section has no resultant for.
FiberSection3 deliberately excludes torsion from its fiber loop (see
its doc comment), and there’s no meaningful G*J/length term at zero
length the way DispBeamColumn3 has, so an explicit rx material is
the only way to give this element torsional stiffness at all.
A global coordinate axis (0..SPATIAL_NDIM) — used where a spatial API
needs to name one of [x, y, z] itself rather than a DOF (e.g.
Domain3::rigid_diaphragm_about’s diaphragm-normal parameter), where a
bare usize would silently also accept a rotational SpatialDof index.
as usize gives the matching translational SpatialDof/coordinate
index directly (Axis3::Y as usize == SpatialDof::Uy as usize == 1).
Coordinate-transformation strategy for planar frame elements: relates
an element’s local (basic) stiffness/forces to the global system. The
co-rotational option follows the deformed chord and removes rigid-body
translation/rotation from the element’s basic deformations.
GeomTransf’s spatial counterpart: builds a member’s local basis and
local↔global rotation from its two end-node coordinates and an
orientation vector (vec_xz): x = normalize(pJ - pI), y = normalize(vec_xz × x), z = x × y). Unlike planar GeomTransf, this
isn’t just a behavior flag: the local-axis construction is real,
nontrivial, and shared by every spatial frame element (ElasticBeamColumn3
today; DispBeamColumn3/ForceBeamColumn3 will reuse it once they
exist), so it’s a real type rather than something each element inlines a
copy of. Verified against Xara’s LinearCrdTransf3d.
The pseudo-time-to-load-factor function a LoadPattern scales its
reference loads by — Xara/OpenSees’s TimeSeries family, cut down to
exactly the three shapes Carapace’s scope actually needs (Trig/
Rectangular/Pulse etc. are for shaping a dynamic excitation
pattern, not a quasi-static load/displacement protocol — out of scope
until something needs them). Closed enum.
A model that cannot be analyzed as built, found by Domain::validate
before any equation is assembled. Node and element indices are insertion
order (the order the objects were added to the Domain), so a caller
that created them from a table can map them straight back to a row.
Pinching4Material’s DmgCyc flag (a raw int in OpenSees): whether
the second damage term is driven by the dissipated-energy ratio
(DmgCyc == 0) or by an accumulated half-cycle count (DmgCyc == 1).
2 * NDF — the size of a 2-node element’s local/global stiffness and
force vectors. A named constant instead of a const expression because
stable Rust’s const generics can’t yet compute 2 * NDF inline at each
SMatrix/SVector use site.
The most nodes any element in this crate connects: two-node frame
elements, three- and four-node planar elements (2D membranes, 3D shell
triangles and quadrilaterals). 3D solid elements are not planned.
Degrees of freedom per node: 2D translation (x, y) plus in-plane
rotation (z). The third DOF carries ElasticBeamColumn’s
bending. A node’s DOF becomes an equation only if something uses it
(an element stiffens it, a constraint ties to it, it carries a mass, or
it is fixed), so a node connected only to trusses has no rotation
unknown and needs none fixed by hand (see Domain::is_active).
What Domain’s generic assembly/state plumbing needs from an element
catalog. A trait rather than a shared enum because Element (2D) and
Element3 (3D) hold different variant sets, and a single enum would
either duplicate every variant or need Box<dyn Trait> in the assembly
loop. NDIM/NDOF are separate const generics (not computed from each
other) because stable Rust cannot evaluate 2 * NDOF in a generic item.
Receives an element’s tangent stiffness k and internal resisting force
r, indexed by the element’s DOFs dofs (row a of k and entry a
of r belong to dofs[a]).
Domain’s spatial instantiation — six-DOF Node3/Element3. See
Domain’s doc comment for why this is a type alias over one generic
implementation rather than a hand-duplicated struct.