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

+20 -14
View File
@@ -50,6 +50,7 @@
use std::collections::BTreeMap;
use modelcraft_fem::DofKind;
use modelcraft_fem::dense::inverse3;
use modelcraft_model::{CouplingKind, Model, Resolved};
@@ -94,7 +95,10 @@ pub fn coupling_rows<'m>(model: &'m Model, resolved: &Resolved, mesh: &Mesh) ->
for (name, c) in model.couplings() {
let point = mesh.point(&c.reference_point).ok_or_else(|| Error::Internal(format!("reference point `{}`", c.reference_point)))?;
let xr = mesh.points.get(point).map(|p| p.xyz).ok_or_else(|| Error::Internal("reference point index".into()))?;
let rp = mesh.point_dof(point, 0);
let slot = mesh.layout.point_slot(point);
// Reference point direction c (0–5), and translation k of mesh node n.
let rp = |c: usize| DofKind::from_direction(c).ok_or_else(|| Error::Internal("direction".into())).and_then(|kind| mesh.dof(slot, kind));
let u = |n: usize, k: usize| DofKind::translation(k).ok_or_else(|| Error::Internal("axis".into())).and_then(|kind| mesh.dof(n, kind));
let set = resolved.sets.get(&c.set).ok_or_else(|| Error::Invalid(format!("coupling `{name}`: set `{}` is not resolved", c.set)))?;
if set.nodes.is_empty() {
return Err(Error::Invalid(format!("coupling `{name}`: set `{}` has no nodes", c.set)));
@@ -119,14 +123,15 @@ pub fn coupling_rows<'m>(model: &'m Model, resolved: &Resolved, mesh: &Mesh) ->
for k in coupled(0..3) {
// u_s,k − u_r,k − (θ × r)_k = 0, (θ × r)_k = θ_a r_b − θ_b r_a.
let (a, b) = ((k + 1) % 3, (k + 2) % 3);
let mut terms = vec![(3 * n + k, 1.0), (rp + k, -1.0)];
let pivot = u(n, k)?;
let mut terms = vec![(pivot, 1.0), (rp(k)?, -1.0)];
if r[b] != 0.0 {
terms.push((rp + 3 + a, -r[b]));
terms.push((rp(3 + a)?, -r[b]));
}
if r[a] != 0.0 {
terms.push((rp + 3 + b, r[a]));
terms.push((rp(3 + b)?, r[a]));
}
rows.push(Row { terms, pivot: 3 * n + k, coupling: name });
rows.push(Row { terms, pivot, coupling: name });
}
}
}
@@ -162,7 +167,8 @@ pub fn coupling_rows<'m>(model: &'m Model, resolved: &Resolved, mesh: &Mesh) ->
let g: Vec<[[f64; 3]; 3]> = arms.iter().map(|r| inverse.map_or([[0.0; 3]; 3], |inv| mat_mul(&inv, &cross_matrix(*r)))).collect();
for k in coupled(0..6) {
let mut terms = Vec::with_capacity(1 + 3 * nodes.len());
terms.push((rp + k, 1.0));
let pivot = rp(k)?;
terms.push((pivot, 1.0));
for (&n, gi) in nodes.iter().zip(&g) {
for comp in 0..3 {
let a = if k < 3 {
@@ -172,11 +178,11 @@ pub fn coupling_rows<'m>(model: &'m Model, resolved: &Resolved, mesh: &Mesh) ->
gi[k - 3][comp]
};
if a != 0.0 {
terms.push((3 * n + comp, -a));
terms.push((u(n, comp)?, -a));
}
}
}
rows.push(Row { terms, pivot: rp + k, coupling: name });
rows.push(Row { terms, pivot, coupling: name });
}
}
}
@@ -184,8 +190,8 @@ pub fn coupling_rows<'m>(model: &'m Model, resolved: &Resolved, mesh: &Mesh) ->
Ok(rows)
}
/// The degrees of freedom that exist: every mesh translation, and the reference point
/// directions some coupling row involves.
/// The degrees of freedom that exist: every mesh node's, and the reference point directions
/// some coupling row involves.
fn active_dofs(mesh: &Mesh, rows: &[Row]) -> Vec<bool> {
let mut active = vec![false; mesh.dofs()];
active.iter_mut().take(mesh.mesh_dofs()).for_each(|a| *a = true);
@@ -288,8 +294,8 @@ fn combine(mut terms: Vec<(usize, f64)>) -> Vec<(usize, f64)> {
pub struct Reduction {
/// The degree of freedom of each free equation.
pub eq_dof: Vec<usize>,
/// Whether a degree of freedom exists (mesh translations, and the reference point
/// directions some coupling ties).
/// Whether a degree of freedom exists (the mesh nodes', and the reference point directions
/// some coupling ties).
pub active: Vec<bool>,
/// T_f: per degree of freedom, (equation, coefficient).
free: Terms,
@@ -376,7 +382,7 @@ impl Reduction {
for (name, mut dofs) in coupled {
dofs.sort_unstable();
dofs.dedup();
let mut nodes: Vec<usize> = dofs.iter().map(|d| d / 3).collect();
let mut nodes: Vec<usize> = dofs.iter().filter_map(|d| mesh.layout.locate(*d).map(|(slot, _)| slot)).collect();
nodes.dedup();
blocks.push((name, nodes.len(), dofs.len().saturating_mul(dofs.len())));
}
@@ -456,7 +462,7 @@ impl Reduction {
// Equations: free mesh degrees of freedom in node order, then reference point ones.
let mut eq = vec![usize::MAX; n];
let mut eq_dof = Vec::new();
let mesh_order = order.iter().flat_map(|node| (0..3).map(move |c| 3 * node + c));
let mesh_order = order.iter().flat_map(|node| mesh.layout.range(*node));
for dof in mesh_order.chain(mesh.mesh_dofs()..n) {
if state.get(dof) == Some(&State::Free)
&& let Some(e) = eq.get_mut(dof)