* fem: DofKind/DofLayout and the shape-function library with second derivatives DofKind (Ux..Rz, T, P), DofSet, ElementDofs (per-node kinds and condensed internal unknowns) and DofLayout: mesh nodes by label, kinds in order, then reference points. basis::Basis covers line2/3, tri3/6, quad4/8/9, tet4/10, wedge6/15, pyramid5, hex8/20/27 with values, first and second natural derivatives (second-order forward AD, fem::jet) and the physical gradients, Hessians and Laplacians with the map's second-derivative term. Tested for Kronecker delta, partition of unity, polynomial completeness, finite differences, affine and curved maps. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * solver, explicit: number degrees of freedom with DofLayout The implicit mesh numbers its unknowns with a DofLayout (mesh nodes by label with the kinds their elements declare, then six per coupled reference point); assembly adds element blocks by (node, DofKind), so a node may carry any mix of kinds; couplings, boundary conditions, loads and results address (node, kind). The explicit system keeps node-major translations, now checked against its layout (NODE_DOFS). Solid formulations declare their ElementDofs. Every static, frequency and explicit result of the solver, explicit, engine and io test suites hashes bit-identically before and after (957 results). docs/fem.md describes the DOF model and the shape-function conventions. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * fem, solver: simplify after review Block offsets resolved once per node pair; add_scalar/add_diagonal take the kinds like add_block; one checked Jacobian inverse (dense::positive_inverse) for the solid kernels and the basis library; static natural-node tables; the Hessian path inverts the Jacobian once. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
6.2 KiB
FEM foundation: degrees of freedom and shape functions
What every element in crates/fem and every solver builds on. Code: crates/fem/src/dof.rs,
crates/fem/src/basis.rs, crates/fem/src/jet.rs.
Degrees of freedom
Every unknown is a pair (node, DofKind):
DofKind |
Meaning | Model / deck number |
|---|---|---|
Ux, Uy, Uz |
translations along X, Y, Z | 1, 2, 3 |
Rx, Ry, Rz |
rotations about X, Y, Z | 4, 5, 6 |
P |
pressure (mixed u–p elements) | 8 |
T |
temperature | 11 |
The enum order (Ux, Uy, Uz, Rx, Ry, Rz, T, P) is the numbering order within a node.
DofKind::from_number / number convert to the 1-based numbers boundary conditions use;
from_direction / direction to the structural directions 0–5.
Elements declare their unknowns with ElementDofs { node: DofSet, internal: Internal }:
the kinds every node of the element carries (DofSet::TRANSLATIONS for solids, PLANE for
2D elements, STRUCTURAL for shells and beams, TEMPERATURE for heat transfer, translations
plus PRESSURE for continuous-pressure mixed elements) and the internal unknowns the element
condenses out itself (incompatible_modes, discontinuous pressure), which never reach the
global system. An element's matrices are ordered node by node, its node kinds in DofKind
order within a node (a shell's 24 × 24 stiffness: node 1 Ux Uy Uz Rx Ry Rz, node 2 …). Solid
elements: Formulation::dofs() / Solid::dofs().
Numbering (DofLayout): collect what every node needs with DofLayout::builder()
(element(labels, dofs), node(label, kinds) for constraints and boundary conditions that
need a kind no element gives, point(label, kinds) for reference points), then build(). A
node carries the union of what was asked for. Unknowns are numbered mesh nodes in label order,
each node's kinds in DofKind order, then the points in the order added; a slot's unknowns are
contiguous, so a node pair is one dense block in the assembled matrix. The same model always
gives the same numbering.
Lookups: node(label) / slot(label) (a slot is a position: mesh nodes 0..nodes(), points
point_slot(k)), dof(slot, kind), range(slot), kinds(slot), locate(dof) -> (slot, kind), element_dofs(slots, kinds), describe(dof), stride() (uniform layouts).
Solvers: modelcraft_solver::system::Mesh::layout numbers the implicit system (mesh nodes,
then six per coupled reference point); assembly (sparse::add_block(k, layout, slots, kinds, data), add_scalar, add_diagonal) works for any mix of kinds per node, so a solid sharing
nodes with a shell adds its translation block into the shell nodes' six-unknown blocks.
Boundary conditions and loads address (node, kind): a boundary condition holds the kinds a node
carries, and a moment needs rotations. The explicit solver has solid nodes only and checks that
its layout is the uniform node-major one (NODE_DOFS = 3).
Shape functions
Basis is every family's interpolation:
| Family | Reference element | Nodes |
|---|---|---|
Line2, Line3 |
ξ ∈ [−1, 1] | ends −1, 1; line3 adds the middle |
Tri3, Tri6 |
ξ, η ≥ 0, ξ + η ≤ 1 | corners (0,0), (1,0), (0,1); mid-sides of 1-2, 2-3, 3-1 |
Quad4, Quad8, Quad9 |
[−1, 1]² | corners counter-clockwise from (−1,−1); mid-sides of 1-2, 2-3, 3-4, 4-1; centre |
Tet4, Tet10 |
unit tetrahedron | corners (0,0,0), (1,0,0), (0,1,0), (0,0,1); mid-sides of 1-2, 2-3, 3-1, 1-4, 2-4, 3-4 |
Wedge6, Wedge15 |
triangle × ζ ∈ [−1, 1] | bottom 1-3, top 4-6; mid-sides of the bottom edges, top edges, vertical edges |
Pyramid5 |
base [−1, 1]² at ζ = 0, apex (0, 0, 1) | base corners as quad4, then the apex |
Hex8, Hex20, Hex27 |
[−1, 1]³ | corners 1-4 at ζ = −1, 5-8 above; hex20 mid-sides (bottom, top, vertical edges); hex27 face centres in ElementKind::Hex8.faces() order, then the centre |
Node orders are the model's (ElementKind::edges() / faces() are the single source):
corners, mid-side nodes in edge order, face nodes, body nodes. Basis::of(ElementKind) maps a
model element; nodes() gives natural node coordinates; dim(), count(), corners(),
degree() (the complete polynomial degree reproduced: 1 for linear families including the
rational pyramid, 2 for the quadratic ones).
Evaluation at a natural point xi: [f64; 3] (unused coordinates are zero):
eval(xi) -> Shape: valuesnand first derivativesdn[i] = ∂Nᵢ/∂(ξ, η, ζ). tet4, tet10, wedge6, hex8, hex20 use the hand-written functions ofshape.rs(the solid kernels' path).eval2(xi) -> Shape2: addsd2n[i][a][b] = ∂²Nᵢ/∂ξₐ∂ξ_b, from second-order forward-mode automatic differentiation of the same formulas (jet::Jet): exact to round-off.gradients(&shape, xyz) -> Gradients: physicaldx[i] = ∇ₓNᵢ = J⁻¹ ∇_ξNᵢanddet J, with J_ab = ∂x_b/∂ξₐ; an inverted or collapsed element isError::Degenerate.hessians(&shape2, xyz) -> Hessians: physicald2x[i]= J⁻¹ (H_ξ − Σ_b (∂N/∂x_b) X_b″) J⁻ᵀ with X_b″ = Σᵢ x_{i,b} H_ξ(Nᵢ), the map's second derivative (T. J. R. Hughes, The Finite Element Method, 2000, §3.9, §4.2), andlaplacian(i). Without the X″ term a field linear in x would have a nonzero Hessian on a curved element.
A family of dimension d maps onto the first d physical coordinates (plane elements in x–y, lines along x). Elements embedded in a higher dimension (shells, beams, trusses in 3D) pass node coordinates in their own local frame.
Verification (basis::tests, dof::tests, jet::tests)
For every family: Kronecker delta at the nodes; partition of unity (Σ N = 1, Σ ∇N = 0,
Σ H = 0); exact reproduction of every monomial up to degree() with its gradient and Hessian
in natural coordinates; first and second derivatives against central differences;
on an affine map, physical gradients, Hessians and Laplacians exact for a complete polynomial
of the family's degree; on a curved map, linear fields exact with zero Hessian, and the
physical Hessian of a quadratic interpolant consistent with finite differences of its physical
gradient. The solver and explicit migration to DofLayout was checked bit for bit: every
result of the solver, explicit, engine and io test suites hashes identically before and after.