FEM foundation A: DofKind/DofLayout, shape-function library with Hessians, solvers on DofLayout (#35)

* 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 19:54:32 -04:00
1 parent c686cbd531
commit bd55abea7f
16 files changed
+1805 -192

No files matched your search

+15 -6
View File
@@ -34,7 +34,8 @@
use modelcraft_model::ElementKind;
use crate::dense::{Cholesky, Matrix, inverse3};
use crate::dense::{Cholesky, Matrix, inverse3, positive_inverse};
use crate::dof::{DofSet, ElementDofs, Internal};
use crate::material::{Elasticity, stress};
use crate::quadrature::{Point, Rule};
use crate::shape::{self, MAX_NODES, Shape};
@@ -144,6 +145,13 @@ pub enum Formulation {
}
impl Formulation {
/// The unknowns of a solid element so formulated: three translations per node, and the
/// incompatible modes it condenses out.
pub fn dofs(self) -> ElementDofs {
let incompatible_modes = if self == Formulation::IncompatibleModes { INCOMPATIBLE_DOFS } else { 0 };
ElementDofs { node: DofSet::TRANSLATIONS, internal: Internal { incompatible_modes, pressure: 0 } }
}
/// The formulation a model section asks for, if this element type has it.
pub fn from_section(kind: ElementKind, f: &modelcraft_model::Formulation) -> Result<Formulation> {
let ty = element_type(kind)?;
@@ -220,6 +228,11 @@ impl<'a> Solid<'a> {
Ok(Solid { ty, xyz, formulation })
}
/// The element's unknowns, in the order of its matrices.
pub fn dofs(&self) -> ElementDofs {
self.formulation.dofs()
}
/// The stiffness integration rule.
pub fn rule(&self) -> Rule {
match (self.formulation, self.ty.reduced) {
@@ -249,11 +262,7 @@ impl<'a> Solid<'a> {
fn gradients(&self, xi: [f64; 3]) -> Result<Gradients> {
let shape = (self.ty.shape)(xi);
let j = self.jacobian(&shape);
let (inv, det) = inverse3(&j).ok_or_else(|| self.degenerate())?;
let norms: f64 = j.iter().map(|r| (r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt()).product();
if !(det > 1e-12 * norms) {
return Err(self.degenerate());
}
let (inv, det) = positive_inverse(&j).ok_or_else(|| self.degenerate())?;
let mut dx = [[0.0; 3]; MAX_NODES];
for (g, d) in dx.iter_mut().zip(shape.derivatives()) {
for b in 0..3 {