forked from 1b-agent/fieldcraft-upstream
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:
16 files changed
+1805
-192
No files matched your search
@@ -38,7 +38,7 @@ use modelcraft_fem::material::stress;
|
||||
use modelcraft_fem::uniform::UniformStrain;
|
||||
use modelcraft_model::{BulkViscosity, HourglassKind};
|
||||
|
||||
use crate::system::{Material, System};
|
||||
use crate::system::{Material, NODE_DOFS, System};
|
||||
|
||||
/// Scratch vectors and the element state of the latest evaluation.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -82,13 +82,13 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub(crate) fn gather<const N: usize>(x: &[f64], nodes: &[usize; N]) -> [[f64; 3]; N] {
|
||||
nodes.map(|n| [0, 1, 2].map(|k| x.get(3 * n + k).copied().unwrap_or(0.0)))
|
||||
nodes.map(|n| [0, 1, 2].map(|k| x.get(NODE_DOFS * n + k).copied().unwrap_or(0.0)))
|
||||
}
|
||||
|
||||
/// Add a node's vector into a per-degree-of-freedom vector.
|
||||
pub(crate) fn add_at(f: &mut [f64], node: usize, v: &[f64; 3]) {
|
||||
for (k, c) in v.iter().enumerate() {
|
||||
if let Some(x) = f.get_mut(3 * node + k) {
|
||||
if let Some(x) = f.get_mut(NODE_DOFS * node + k) {
|
||||
*x += c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use modelcraft_model::{ExplicitDynamic, Model, OutputVariable, Procedure, Step};
|
||||
|
||||
use crate::elements::{Workspace, internal_forces};
|
||||
use crate::result::{ControllingElement, Energies, ExplicitResult, Frame, History};
|
||||
use crate::system::System;
|
||||
use crate::system::{NODE_DOFS, System};
|
||||
use crate::{Error, Limits, Result};
|
||||
|
||||
/// How many elements the result names as controlling the time increment.
|
||||
@@ -76,7 +76,7 @@ fn constrain(sys: &System, u: &[f64], v: &mut [f64], dt: f64) {
|
||||
for w in &sys.walls {
|
||||
for &node in &w.nodes {
|
||||
let x = sys.xyz.get(node).copied().unwrap_or_default();
|
||||
let at = |k: usize| 3 * node + k;
|
||||
let at = |k: usize| NODE_DOFS * node + k;
|
||||
let gap: f64 = (0..3).map(|k| (x[k] + u.get(at(k)).copied().unwrap_or(0.0) - w.point[k]) * w.normal[k]).sum();
|
||||
let normal: f64 = (0..3).map(|k| v.get(at(k)).copied().unwrap_or(0.0) * w.normal[k]).sum();
|
||||
if gap + dt * normal < 0.0 {
|
||||
@@ -113,7 +113,7 @@ fn impulses(mass: &[f64], free: &[f64], constrained: &[f64], dt: f64, r: &mut [f
|
||||
}
|
||||
|
||||
fn per_node(x: &[f64]) -> Vec<[f64; 3]> {
|
||||
x.as_chunks::<3>().0.to_vec()
|
||||
x.as_chunks::<NODE_DOFS>().0.to_vec()
|
||||
}
|
||||
|
||||
/// Solve an explicit dynamic step: the step named `step`, or the model's only one.
|
||||
@@ -171,7 +171,7 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res
|
||||
}
|
||||
|
||||
let n = sys.dofs();
|
||||
let mass: Vec<f64> = (0..n).map(|d| sys.mass.get(d / 3).copied().unwrap_or(0.0)).collect();
|
||||
let mass: Vec<f64> = (0..n).map(|d| sys.mass.get(d / NODE_DOFS).copied().unwrap_or(0.0)).collect();
|
||||
if mass.iter().any(|m| !(*m > 0.0 && m.is_finite())) {
|
||||
return Err(Error::Internal("a node has no positive mass".into()));
|
||||
}
|
||||
@@ -209,7 +209,7 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res
|
||||
history.energies.push(e);
|
||||
let sum = |x: &[f64], weight: &dyn Fn(usize) -> f64| {
|
||||
let mut s = [0.0; 3];
|
||||
for (node, c) in x.as_chunks::<3>().0.iter().enumerate() {
|
||||
for (node, c) in x.as_chunks::<NODE_DOFS>().0.iter().enumerate() {
|
||||
for k in 0..3 {
|
||||
s[k] += weight(node) * c[k];
|
||||
}
|
||||
@@ -218,7 +218,7 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res
|
||||
};
|
||||
history.momentum.push(sum(v, &|node| sys.mass.get(node).copied().unwrap_or(0.0)));
|
||||
history.constraint_force.push(sum(r, &|_| 1.0));
|
||||
let pick = |x: &[f64]| sys.history_nodes.iter().map(|i| [0, 1, 2].map(|k| x.get(3 * i + k).copied().unwrap_or(0.0))).collect();
|
||||
let pick = |x: &[f64]| sys.history_nodes.iter().map(|i| [0, 1, 2].map(|k| x.get(NODE_DOFS * i + k).copied().unwrap_or(0.0))).collect();
|
||||
history.displacement.push(pick(u));
|
||||
history.velocity.push(pick(v));
|
||||
};
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
//! lengths, lumped masses, prescribed velocities, rigid walls, initial velocities and the load
|
||||
//! vector of an explicit dynamic step.
|
||||
|
||||
use modelcraft_fem::face;
|
||||
use modelcraft_fem::material::Elasticity;
|
||||
use modelcraft_fem::uniform::{Hex8, Tet4, modulus_bound};
|
||||
use modelcraft_fem::{DofKind, DofLayout, DofSet, face};
|
||||
use modelcraft_model::{Elastic, ElementKind, ExplicitDynamic, HourglassKind, Integration, Load, Model, Resolved, SectionKind, Step, Target};
|
||||
|
||||
use crate::elements::add_at;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Degrees of freedom per node: the three translations, node-major (see [`System`]).
|
||||
pub const NODE_DOFS: usize = 3;
|
||||
|
||||
/// Most nodes whose histories a step records.
|
||||
pub const MAX_HISTORY_NODES: usize = 1000;
|
||||
|
||||
@@ -67,10 +70,15 @@ pub struct Wall {
|
||||
}
|
||||
|
||||
/// The discrete system of an explicit dynamic step.
|
||||
///
|
||||
/// Its elements are solids, so every node carries the three translations and the layout is
|
||||
/// node-major: degree of freedom 3 n + axis, which the integrator's per-node loops rely on
|
||||
/// (checked when the system is built).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct System {
|
||||
/// Node labels with degrees of freedom (those of the elements), sorted.
|
||||
pub labels: Vec<u32>,
|
||||
pub layout: DofLayout,
|
||||
pub xyz: Vec<[f64; 3]>,
|
||||
/// Element labels, sorted.
|
||||
pub elements: Vec<u32>,
|
||||
@@ -188,25 +196,30 @@ impl System {
|
||||
let mut labels: Vec<u32> = chosen.iter().flat_map(|e| e.2.iter().copied()).collect();
|
||||
labels.sort_unstable();
|
||||
labels.dedup();
|
||||
if 3 * labels.len() > max_dofs {
|
||||
return Err(Error::Limit(format!("the model has {} degrees of freedom; the limit is {max_dofs}", 3 * labels.len())));
|
||||
let layout = DofLayout::uniform(labels.iter().copied(), DofSet::TRANSLATIONS);
|
||||
if layout.dofs() > max_dofs {
|
||||
return Err(Error::Limit(format!("the model has {} degrees of freedom; the limit is {max_dofs}", layout.dofs())));
|
||||
}
|
||||
if layout.stride() != Some(NODE_DOFS) {
|
||||
return Err(Error::Internal("explicit dynamics needs three translations per node".into()));
|
||||
}
|
||||
if labels.is_empty() {
|
||||
return Err(invalid("the model has no elements to analyse"));
|
||||
}
|
||||
let xyz: Vec<[f64; 3]> =
|
||||
labels.iter().map(|l| model.node(*l).map(|n| n.xyz).ok_or_else(|| invalid(format!("node {l} is missing")))).collect::<Result<_>>()?;
|
||||
let n = labels.len();
|
||||
let (n, dofs) = (labels.len(), layout.dofs());
|
||||
let mut sys = System {
|
||||
layout,
|
||||
elements: chosen.iter().map(|e| e.0).collect(),
|
||||
materials,
|
||||
hexes: Vec::new(),
|
||||
tets: Vec::new(),
|
||||
mass: vec![0.0; n],
|
||||
prescribed: vec![None; 3 * n],
|
||||
prescribed: vec![None; dofs],
|
||||
walls: Vec::new(),
|
||||
initial_velocity: vec![0.0; 3 * n],
|
||||
external: vec![0.0; 3 * n],
|
||||
initial_velocity: vec![0.0; dofs],
|
||||
external: vec![0.0; dofs],
|
||||
history_nodes: Vec::new(),
|
||||
hourglass: settings.hourglass.kind,
|
||||
warnings: resolved.warnings.iter().map(ToString::to_string).collect(),
|
||||
@@ -270,7 +283,7 @@ impl System {
|
||||
}
|
||||
|
||||
pub fn dofs(&self) -> usize {
|
||||
3 * self.labels.len()
|
||||
self.layout.dofs()
|
||||
}
|
||||
|
||||
/// Prescribed velocities from the step's boundary conditions: zero for held degrees of
|
||||
@@ -283,13 +296,15 @@ impl System {
|
||||
}
|
||||
for &label in resolved.target_nodes(bc.target()) {
|
||||
let Some(node) = self.node(label) else { continue };
|
||||
for dof in 1..=3u8 {
|
||||
for kind in self.layout.kinds(node).iter() {
|
||||
let dof = kind.number();
|
||||
let Some(value) = bc.value(dof) else { continue };
|
||||
let velocity = value / end_time;
|
||||
if !velocity.is_finite() {
|
||||
return Err(invalid(format!("boundary condition `{name}`: the velocity {value} / {end_time} is not finite")));
|
||||
}
|
||||
let slot = self.prescribed.get_mut(3 * node + usize::from(dof - 1)).ok_or_else(|| Error::Internal("dof index".into()))?;
|
||||
let index = self.layout.dof(node, kind).ok_or_else(|| Error::Internal("dof index".into()))?;
|
||||
let slot = self.prescribed.get_mut(index).ok_or_else(|| Error::Internal("dof index".into()))?;
|
||||
match *slot {
|
||||
Some(old) if old != velocity => {
|
||||
return Err(invalid(format!(
|
||||
@@ -381,7 +396,9 @@ impl System {
|
||||
for iv in &settings.initial_velocities {
|
||||
for node in self.nodes_of(resolved, &iv.set) {
|
||||
for (k, c) in iv.velocity.iter().enumerate() {
|
||||
if let Some(v) = self.initial_velocity.get_mut(3 * node + k) {
|
||||
if let Some(v) =
|
||||
DofKind::translation(k).and_then(|kind| self.layout.dof(node, kind)).and_then(|d| self.initial_velocity.get_mut(d))
|
||||
{
|
||||
*v = *c;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user