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
@@ -2,7 +2,7 @@
|
||||
|
||||
use modelcraft_fem::element::{Extrapolation, extrapolation};
|
||||
use modelcraft_fem::quadrature::Rule;
|
||||
use modelcraft_fem::{PointStress, von_mises};
|
||||
use modelcraft_fem::{DofKind, DofLayout, PointStress, von_mises};
|
||||
use modelcraft_model::{ElementKind, Model, Procedure, Resolved, Step};
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -17,7 +17,8 @@ use crate::{Error, Limits, Result};
|
||||
/// Size and effort of a solution.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
||||
pub struct Stats {
|
||||
/// Degrees of freedom: 3 per mesh node and 6 per coupled reference point.
|
||||
/// Degrees of freedom: those of the mesh nodes (3 per solid node) and 6 per coupled
|
||||
/// reference point.
|
||||
pub dofs: usize,
|
||||
/// Free degrees of freedom solved for.
|
||||
pub equations: usize,
|
||||
@@ -129,17 +130,18 @@ struct Assembled {
|
||||
|
||||
/// Assemble the stiffness (and the mass when `mass` is set) on the mesh's node graph.
|
||||
fn assemble(mesh: &Mesh, graph: &NodeGraph, mass: Option<MassKind>) -> Result<Assembled> {
|
||||
let mut k = graph.zero_matrix();
|
||||
let mut m = mass.map(|_| graph.zero_matrix());
|
||||
let layout = &mesh.layout;
|
||||
let mut k = graph.zero_matrix(layout);
|
||||
let mut m = mass.map(|_| graph.zero_matrix(layout));
|
||||
for e in &mesh.elements {
|
||||
let solid = e.solid()?;
|
||||
let at = |err| Error::element(e.label, err);
|
||||
add_block(&mut k, &e.nodes, &solid.stiffness(&e.d).map_err(at)?.data)?;
|
||||
add_block(&mut k, layout, &e.nodes, solid.dofs().node, &solid.stiffness(&e.d).map_err(at)?.data)?;
|
||||
if let (Some(mm), Some(kind)) = (m.as_mut(), mass) {
|
||||
let rho = e.density.ok_or_else(|| Error::Invalid(format!("element {} has no density", e.label)))?;
|
||||
match kind {
|
||||
MassKind::Consistent => add_scalar(mm, &e.nodes, &solid.mass(rho).map_err(at)?.data)?,
|
||||
MassKind::Lumped => add_diagonal(mm, &e.nodes, &solid.lumped_mass(rho).map_err(at)?)?,
|
||||
MassKind::Consistent => add_scalar(mm, layout, &e.nodes, solid.dofs().node, &solid.mass(rho).map_err(at)?.data)?,
|
||||
MassKind::Lumped => add_diagonal(mm, layout, &e.nodes, solid.dofs().node, &solid.lumped_mass(rho).map_err(at)?)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,10 +169,17 @@ fn factor_error(e: FactorError, mesh: &Mesh, reduction: &Reduction) -> Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// The mesh nodes' part of a full vector, per node, and the reference points' part, per point.
|
||||
/// The structural directions (0..N) of layout slot `slot` in a full vector, zero where the slot
|
||||
/// has none.
|
||||
fn directions<const N: usize>(full: &[f64], layout: &DofLayout, slot: usize) -> [f64; N] {
|
||||
std::array::from_fn(|c| DofKind::from_direction(c).and_then(|k| layout.dof(slot, k)).and_then(|d| full.get(d)).copied().unwrap_or(0.0))
|
||||
}
|
||||
|
||||
/// The mesh nodes' translations in a full vector, per node, and the reference points'
|
||||
/// translations and rotations, per point.
|
||||
fn split(full: &[f64], mesh: &Mesh) -> (Vec<[f64; 3]>, Vec<[f64; 6]>) {
|
||||
let (nodes, points) = full.split_at(mesh.mesh_dofs().min(full.len()));
|
||||
(nodes.as_chunks::<3>().0.to_vec(), points.as_chunks::<6>().0.to_vec())
|
||||
let l = &mesh.layout;
|
||||
((0..l.nodes()).map(|n| directions(full, l, n)).collect(), (0..mesh.points.len()).map(|k| directions(full, l, l.point_slot(k))).collect())
|
||||
}
|
||||
|
||||
/// Constraint rows, prescribed values and their elimination for a step.
|
||||
@@ -188,8 +197,8 @@ fn reduction(s: &Setup, values: bool, order: &[usize], budget: Budget, warnings:
|
||||
}
|
||||
|
||||
/// The reduced matrices' memory budget: the factorization's.
|
||||
fn budget(limits: &Limits, graph: &NodeGraph, matrices: usize) -> Budget {
|
||||
Budget { max_bytes: limits.max_factor_entries.saturating_mul(8), pattern_entries: graph.entries(), matrices }
|
||||
fn budget(limits: &Limits, mesh: &Mesh, graph: &NodeGraph, matrices: usize) -> Budget {
|
||||
Budget { max_bytes: limits.max_factor_entries.saturating_mul(8), pattern_entries: graph.entries(&mesh.layout), matrices }
|
||||
}
|
||||
|
||||
/// Solve a linear static step: the step named `step`, or the model's only static step.
|
||||
@@ -201,7 +210,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
let graph = NodeGraph::new(mesh.labels.len(), mesh.elements.iter().map(|e| e.nodes.as_slice()));
|
||||
let order = nested_dissection(&mesh.xyz, &graph.adjacency);
|
||||
let setup = Setup { model, resolved: &resolved, mesh: &mesh, step };
|
||||
let red = reduction(&setup, true, &order, budget(limits, &graph, 1), &mut warnings)?;
|
||||
let red = reduction(&setup, true, &order, budget(limits, &mesh, &graph, 1), &mut warnings)?;
|
||||
let f = loads(model, &resolved, &mesh, step, &red.active)?;
|
||||
let k = assemble(&mesh, &graph, None)?.k;
|
||||
if red.neq() == 0 {
|
||||
@@ -221,7 +230,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
let (reaction, point_reaction) = split(&reactions, &mesh);
|
||||
let (mut applied_total, mut reaction_total) = ([0.0; 3], [0.0; 3]);
|
||||
for (dof, (fv, rv)) in f.iter().zip(&reactions).enumerate() {
|
||||
let d = mesh.direction(dof);
|
||||
let Some(d) = mesh.kind(dof).and_then(DofKind::direction) else { continue };
|
||||
if let (Some(a), Some(r)) = (applied_total.get_mut(d), reaction_total.get_mut(d)) {
|
||||
*a += fv;
|
||||
*r += rv;
|
||||
@@ -328,7 +337,7 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
let graph = NodeGraph::new(mesh.labels.len(), mesh.elements.iter().map(|e| e.nodes.as_slice()));
|
||||
let order = nested_dissection(&mesh.xyz, &graph.adjacency);
|
||||
let setup = Setup { model, resolved: &resolved, mesh: &mesh, step };
|
||||
let red = reduction(&setup, false, &order, budget(limits, &graph, 2), &mut warnings)?;
|
||||
let red = reduction(&setup, false, &order, budget(limits, &mesh, &graph, 2), &mut warnings)?;
|
||||
let Assembled { k, m } = assemble(&mesh, &graph, Some(options.mass))?;
|
||||
let m = m.ok_or_else(|| Error::Internal("mass matrix".into()))?;
|
||||
// Every element's mass entries sum to its mass, once per direction.
|
||||
@@ -372,9 +381,10 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
let pairs = lanczos(&op, shift, wanted, BLOCK, max_basis, TOLERANCE)?;
|
||||
// Participation: Γ_d = φᵀ M r_d with r_d the unit translation along d on the free equations
|
||||
// (mesh and reference point translations).
|
||||
let directions: Vec<Option<usize>> = red.eq_dof.iter().map(|dof| mesh.kind(*dof).and_then(DofKind::direction)).collect();
|
||||
let mr: Vec<Vec<f64>> = (0..3)
|
||||
.map(|d| {
|
||||
let r: Vec<f64> = red.eq_dof.iter().map(|dof| if mesh.direction(*dof) == d { 1.0 } else { 0.0 }).collect();
|
||||
let r: Vec<f64> = directions.iter().map(|dir| if *dir == Some(d) { 1.0 } else { 0.0 }).collect();
|
||||
let mut y = vec![0.0; neq];
|
||||
mff.mul(&r, &mut y);
|
||||
y
|
||||
|
||||
@@ -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)
|
||||
|
||||
+102
-50
@@ -1,11 +1,14 @@
|
||||
//! Sparse symmetric matrices in compressed rows, both triangles stored.
|
||||
//!
|
||||
//! Assembly works on the node graph: a node's row holds every node it shares an element with,
|
||||
//! and each node pair is a dense 3 × 3 block, so the pattern is built once from the element
|
||||
//! connectivity and the stiffness and mass share it. Sums run in a fixed order (elements by
|
||||
//! and each node pair is a dense block of their degrees of freedom ([`DofLayout`]: a node's are
|
||||
//! contiguous), so the pattern is built once from the element connectivity and the stiffness and
|
||||
//! mass share it. Sums run in a fixed order (elements by
|
||||
//! label, then the element's own node order), so assembled values are the same bit for bit on
|
||||
//! every run and platform.
|
||||
|
||||
use modelcraft_fem::{DofLayout, DofSet};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A square sparse matrix in compressed rows with sorted column indices.
|
||||
@@ -95,21 +98,22 @@ impl NodeGraph {
|
||||
NodeGraph { adjacency }
|
||||
}
|
||||
|
||||
/// Nonzeros of the dof-level matrix (3 × 3 per node pair).
|
||||
pub fn entries(&self) -> usize {
|
||||
self.adjacency.iter().map(|r| 9 * r.len()).sum()
|
||||
/// Nonzeros of the dof-level matrix: a dense block per node pair.
|
||||
pub fn entries(&self, layout: &DofLayout) -> usize {
|
||||
self.adjacency.iter().enumerate().map(|(a, row)| layout.range(a).len() * row.iter().map(|b| layout.range(*b).len()).sum::<usize>()).sum()
|
||||
}
|
||||
|
||||
/// A zero matrix over every degree of freedom (3 per node) on this pattern.
|
||||
pub fn zero_matrix(&self) -> Csr {
|
||||
let n = 3 * self.adjacency.len();
|
||||
/// A zero matrix over the mesh nodes' degrees of freedom (layout slots `0..nodes`) on this
|
||||
/// pattern.
|
||||
pub fn zero_matrix(&self, layout: &DofLayout) -> Csr {
|
||||
let n = layout.node_dofs();
|
||||
let mut ptr = Vec::with_capacity(n + 1);
|
||||
ptr.push(0);
|
||||
let mut idx = Vec::with_capacity(self.entries());
|
||||
for row in &self.adjacency {
|
||||
for _ in 0..3 {
|
||||
let mut idx = Vec::with_capacity(self.entries(layout));
|
||||
for (a, row) in self.adjacency.iter().enumerate() {
|
||||
for _ in layout.range(a) {
|
||||
for &b in row {
|
||||
idx.extend([3 * b, 3 * b + 1, 3 * b + 2]);
|
||||
idx.extend(layout.range(b));
|
||||
}
|
||||
ptr.push(idx.len());
|
||||
}
|
||||
@@ -119,30 +123,56 @@ impl NodeGraph {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where node b's 3 × 3 block starts in each of node a's three rows (the rows of a node share
|
||||
/// one pattern, built by [`NodeGraph::zero_matrix`]): one search per node pair.
|
||||
fn block_offsets(k: &Csr, a: usize, b: usize) -> Result<[usize; 3]> {
|
||||
let (start, end) = (k.ptr.get(3 * a).copied().unwrap_or(0), k.ptr.get(3 * a + 1).copied().unwrap_or(0));
|
||||
let cols = k.idx.get(start..end).ok_or_else(|| Error::Internal("pattern".into()))?;
|
||||
let at = cols.binary_search(&(3 * b)).map_err(|_| Error::Internal("entry outside the pattern".into()))?;
|
||||
Ok([0, 1, 2].map(|r| k.ptr.get(3 * a + r).copied().unwrap_or(0) + at))
|
||||
/// Most kinds a node carries ([`modelcraft_fem::DofKind::ALL`]).
|
||||
const MAX_KINDS: usize = 8;
|
||||
|
||||
/// Where node b's block lies in node a's rows of a matrix built by [`NodeGraph::zero_matrix`],
|
||||
/// for the degrees of freedom `kinds` of both: the value index of (row r, column c) is
|
||||
/// `rows[r] + cols[c]`. The rows of a node share one pattern, so this is one search per node
|
||||
/// pair.
|
||||
struct Block {
|
||||
rows: [usize; MAX_KINDS],
|
||||
cols: [usize; MAX_KINDS],
|
||||
}
|
||||
|
||||
/// Add an element matrix (3n × 3n, row-major, degrees of freedom node by node) into a matrix
|
||||
/// built by [`NodeGraph::zero_matrix`]; `nodes` are the element's global node indices.
|
||||
pub fn add_block(k: &mut Csr, nodes: &[usize], data: &[f64]) -> Result<()> {
|
||||
let width = 3 * nodes.len();
|
||||
impl Block {
|
||||
fn new(k: &Csr, layout: &DofLayout, a: usize, b: usize, kinds: DofSet) -> Result<Block> {
|
||||
let first = layout.range(a).start;
|
||||
let (start, end) = (k.ptr.get(first).copied().unwrap_or(0), k.ptr.get(first + 1).copied().unwrap_or(0));
|
||||
let pattern = k.idx.get(start..end).ok_or_else(|| Error::Internal("pattern".into()))?;
|
||||
let at = pattern.binary_search(&layout.range(b).start).map_err(|_| Error::Internal("entry outside the pattern".into()))?;
|
||||
let missing = |kind| Error::Internal(format!("an element degree of freedom ({kind:?}) its node does not have"));
|
||||
let mut block = Block { rows: [0; MAX_KINDS], cols: [0; MAX_KINDS] };
|
||||
for ((row, col), kind) in block.rows.iter_mut().zip(&mut block.cols).zip(kinds.iter()) {
|
||||
let r = layout.dof(a, kind).ok_or_else(|| missing(kind))?;
|
||||
*row = k.ptr.get(r).copied().unwrap_or(0) + at;
|
||||
*col = layout.kinds(b).position(kind).ok_or_else(|| missing(kind))?;
|
||||
}
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
fn add_at(k: &mut Csr, at: usize, v: f64) {
|
||||
if let Some(x) = k.val.get_mut(at) {
|
||||
*x += v;
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an element matrix into a matrix built by [`NodeGraph::zero_matrix`]: row-major over the
|
||||
/// element's degrees of freedom, node by node and `kinds` in order within a node; `nodes` are
|
||||
/// the element's layout slots.
|
||||
pub fn add_block(k: &mut Csr, layout: &DofLayout, nodes: &[usize], kinds: DofSet, data: &[f64]) -> Result<()> {
|
||||
let per = kinds.len();
|
||||
let width = per * nodes.len();
|
||||
if data.len() != width * width {
|
||||
return Err(Error::Internal("element matrix size".into()));
|
||||
}
|
||||
for (i, &a) in nodes.iter().enumerate() {
|
||||
for (j, &b) in nodes.iter().enumerate() {
|
||||
let rows = block_offsets(k, a, b)?;
|
||||
for (r, start) in rows.iter().enumerate() {
|
||||
for c in 0..3 {
|
||||
if let (Some(x), Some(v)) = (k.val.get_mut(start + c), data.get((3 * i + r) * width + 3 * j + c)) {
|
||||
*x += v;
|
||||
}
|
||||
let block = Block::new(k, layout, a, b, kinds)?;
|
||||
for r in 0..per {
|
||||
for c in 0..per {
|
||||
add_at(k, block.rows[r] + block.cols[c], data.get((per * i + r) * width + per * j + c).copied().unwrap_or(0.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,34 +180,31 @@ pub fn add_block(k: &mut Csr, nodes: &[usize], data: &[f64]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add an n × n element matrix to each displacement component's block (consistent mass).
|
||||
pub fn add_scalar(k: &mut Csr, nodes: &[usize], data: &[f64]) -> Result<()> {
|
||||
/// Add an n × n element matrix to the block of each of `kinds` (a solid's consistent mass on its
|
||||
/// translations).
|
||||
pub fn add_scalar(k: &mut Csr, layout: &DofLayout, nodes: &[usize], kinds: DofSet, data: &[f64]) -> Result<()> {
|
||||
let n = nodes.len();
|
||||
if data.len() != n * n {
|
||||
return Err(Error::Internal("element matrix size".into()));
|
||||
}
|
||||
for (i, &a) in nodes.iter().enumerate() {
|
||||
for (j, &b) in nodes.iter().enumerate() {
|
||||
let rows = block_offsets(k, a, b)?;
|
||||
let block = Block::new(k, layout, a, b, kinds)?;
|
||||
let v = data.get(i * n + j).copied().unwrap_or(0.0);
|
||||
for (c, start) in rows.iter().enumerate() {
|
||||
if let Some(x) = k.val.get_mut(start + c) {
|
||||
*x += v;
|
||||
}
|
||||
for c in 0..kinds.len() {
|
||||
add_at(k, block.rows[c] + block.cols[c], v);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a per-node diagonal to each displacement component (lumped mass).
|
||||
pub fn add_diagonal(k: &mut Csr, nodes: &[usize], diagonal: &[f64]) -> Result<()> {
|
||||
/// Add a per-node diagonal to each of `kinds` (a solid's lumped mass on its translations).
|
||||
pub fn add_diagonal(k: &mut Csr, layout: &DofLayout, nodes: &[usize], kinds: DofSet, diagonal: &[f64]) -> Result<()> {
|
||||
for (&a, v) in nodes.iter().zip(diagonal) {
|
||||
let rows = block_offsets(k, a, a)?;
|
||||
for (c, start) in rows.iter().enumerate() {
|
||||
if let Some(x) = k.val.get_mut(start + c) {
|
||||
*x += v;
|
||||
}
|
||||
let block = Block::new(k, layout, a, a, kinds)?;
|
||||
for c in 0..kinds.len() {
|
||||
add_at(k, block.rows[c] + block.cols[c], *v);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -192,14 +219,15 @@ mod tests {
|
||||
// Two "elements" sharing node 1.
|
||||
let g = NodeGraph::new(3, [vec![0usize, 1], vec![1, 2]].into_iter());
|
||||
assert_eq!(g.adjacency, vec![vec![0, 1], vec![0, 1, 2], vec![1, 2]]);
|
||||
let mut k = g.zero_matrix();
|
||||
let layout = DofLayout::uniform(0..3, DofSet::TRANSLATIONS);
|
||||
let mut k = g.zero_matrix(&layout);
|
||||
let mut e = vec![0.0; 36];
|
||||
for i in 0..6 {
|
||||
e[i * 6 + i] = 2.0;
|
||||
e[i * 6 + (i + 3) % 6] = -1.0;
|
||||
}
|
||||
add_block(&mut k, &[0, 1], &e).unwrap();
|
||||
add_block(&mut k, &[1, 2], &e).unwrap();
|
||||
add_block(&mut k, &layout, &[0, 1], DofSet::TRANSLATIONS, &e).unwrap();
|
||||
add_block(&mut k, &layout, &[1, 2], DofSet::TRANSLATIONS, &e).unwrap();
|
||||
assert_eq!(k.diagonal(), vec![2.0, 2.0, 2.0, 4.0, 4.0, 4.0, 2.0, 2.0, 2.0]);
|
||||
let x = vec![1.0; 9];
|
||||
let mut y = vec![0.0; 9];
|
||||
@@ -209,14 +237,38 @@ mod tests {
|
||||
let mut mirrored = k.clone();
|
||||
mirrored.mirror_lower();
|
||||
assert_eq!(mirrored, k);
|
||||
assert!(add_block(&mut k, &[0, 2], &e).is_err(), "nodes 0 and 2 share no element");
|
||||
assert!(add_block(&mut k, &layout, &[0, 2], DofSet::TRANSLATIONS, &e).is_err(), "nodes 0 and 2 share no element");
|
||||
// Scalar (per component) and diagonal additions land on each component.
|
||||
let mut m = g.zero_matrix();
|
||||
add_scalar(&mut m, &[0, 1], &[2.0, 1.0, 1.0, 2.0]).unwrap();
|
||||
add_diagonal(&mut m, &[2], &[5.0]).unwrap();
|
||||
let mut m = g.zero_matrix(&layout);
|
||||
add_scalar(&mut m, &layout, &[0, 1], DofSet::TRANSLATIONS, &[2.0, 1.0, 1.0, 2.0]).unwrap();
|
||||
add_diagonal(&mut m, &layout, &[2], DofSet::TRANSLATIONS, &[5.0]).unwrap();
|
||||
assert_eq!(m.diagonal(), vec![2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 5.0, 5.0, 5.0]);
|
||||
assert_eq!(m.row(0).1, &[2.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
|
||||
let s = k.add_scaled(-1.0, &k).unwrap();
|
||||
assert!(s.val.iter().all(|v| *v == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_kinds_per_node_assemble_into_their_blocks() {
|
||||
// A solid on nodes 0 and 1 (translations) and a beam-like element on 1 and 2 (six each):
|
||||
// node 1 carries six, and the solid's block lands on its translations.
|
||||
let mut b = DofLayout::builder();
|
||||
b.node(10, DofSet::TRANSLATIONS).node(11, DofSet::STRUCTURAL).node(12, DofSet::STRUCTURAL);
|
||||
let layout = b.build();
|
||||
let g = NodeGraph::new(3, [vec![0usize, 1], vec![1, 2]].into_iter());
|
||||
let mut k = g.zero_matrix(&layout);
|
||||
assert_eq!((k.n, k.idx.len(), g.entries(&layout)), (15, 3 * 9 + 6 * 15 + 6 * 12, 189));
|
||||
let solid: Vec<f64> = (0..36).map(|i| i as f64).collect();
|
||||
add_block(&mut k, &layout, &[0, 1], DofSet::TRANSLATIONS, &solid).unwrap();
|
||||
let beam = vec![1.0; 144];
|
||||
add_block(&mut k, &layout, &[1, 2], DofSet::STRUCTURAL, &beam).unwrap();
|
||||
// Row Uy of node 1 (global 4): the solid's row 4 on node 0's translations and node 1's.
|
||||
let (cols, vals) = k.row(4);
|
||||
let at = |c: usize| vals[cols.binary_search(&c).unwrap()];
|
||||
assert_eq!([at(0), at(1), at(2)], [24.0, 25.0, 26.0]);
|
||||
assert_eq!([at(3), at(4), at(5)], [27.0 + 1.0, 28.0 + 1.0, 29.0 + 1.0]);
|
||||
assert_eq!(at(6), 1.0, "rotation Rx of node 1 from the beam only");
|
||||
// A translations element cannot claim rotations its node lacks.
|
||||
assert!(add_block(&mut k, &layout, &[0, 1], DofSet::STRUCTURAL, &beam).is_err());
|
||||
}
|
||||
}
|
||||
+68
-55
@@ -2,11 +2,12 @@
|
||||
//! formulations, the coupled reference points, the degrees of freedom, prescribed values and the
|
||||
//! load vector of a step.
|
||||
//!
|
||||
//! Degrees of freedom: three translations per mesh node (nodes by label), then six per coupled
|
||||
//! reference point (points by name): translations along X, Y, Z and rotations about them.
|
||||
//! Degrees of freedom ([`DofLayout`]): the kinds each mesh node's elements declare (three
|
||||
//! translations for solids), nodes by label, then six per coupled reference point (points by
|
||||
//! name): translations along X, Y, Z and rotations about them.
|
||||
|
||||
use modelcraft_fem::element::{Formulation, Solid};
|
||||
use modelcraft_fem::{Elasticity, elasticity, face};
|
||||
use modelcraft_fem::{DofKind, DofLayout, DofSet, Elasticity, elasticity, face};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use modelcraft_model::{ElementKind, Load, Model, Resolved, SectionKind, Step, Target};
|
||||
@@ -41,18 +42,18 @@ pub struct Point {
|
||||
}
|
||||
|
||||
/// The analysed mesh: the nodes that carry degrees of freedom (those of the solid elements),
|
||||
/// sorted by label, the elements sorted by label, and the coupled reference points by name.
|
||||
/// sorted by label, the elements sorted by label, the coupled reference points by name, and the
|
||||
/// numbering of their degrees of freedom (mesh node `n` is layout slot `n`, reference point `k`
|
||||
/// is [`DofLayout::point_slot`]`(k)`).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mesh {
|
||||
pub labels: Vec<u32>,
|
||||
pub xyz: Vec<[f64; 3]>,
|
||||
pub elements: Vec<Elem>,
|
||||
pub points: Vec<Point>,
|
||||
pub layout: DofLayout,
|
||||
}
|
||||
|
||||
/// The directions of a node's degrees of freedom, 0–5.
|
||||
const DIRECTIONS: [&str; 6] = ["X", "Y", "Z", "rotation about X", "rotation about Y", "rotation about Z"];
|
||||
|
||||
impl Mesh {
|
||||
pub fn node(&self, label: u32) -> Option<usize> {
|
||||
self.labels.binary_search(&label).ok()
|
||||
@@ -66,43 +67,35 @@ impl Mesh {
|
||||
self.points.binary_search_by(|p| p.name.as_str().cmp(name)).ok()
|
||||
}
|
||||
|
||||
/// The translational degrees of freedom of the mesh nodes.
|
||||
/// The degrees of freedom of the mesh nodes; the reference points' follow them.
|
||||
pub fn mesh_dofs(&self) -> usize {
|
||||
3 * self.labels.len()
|
||||
self.layout.node_dofs()
|
||||
}
|
||||
|
||||
/// Every degree of freedom: the mesh's, then six per reference point.
|
||||
/// Every degree of freedom.
|
||||
pub fn dofs(&self) -> usize {
|
||||
self.mesh_dofs() + 6 * self.points.len()
|
||||
self.layout.dofs()
|
||||
}
|
||||
|
||||
/// Degree of freedom `c` (0–5) of reference point `k`.
|
||||
pub fn point_dof(&self, k: usize, c: usize) -> usize {
|
||||
self.mesh_dofs() + 6 * k + c
|
||||
/// The degree of freedom of `kind` at layout slot `slot` (a mesh node or reference point).
|
||||
pub fn dof(&self, slot: usize, kind: DofKind) -> Result<usize> {
|
||||
self.layout.dof(slot, kind).ok_or_else(|| Error::Internal(format!("slot {slot} has no {kind:?} degree of freedom")))
|
||||
}
|
||||
|
||||
/// The direction of a degree of freedom: 0–2 translations along X, Y, Z, 3–5 rotations.
|
||||
pub fn direction(&self, dof: usize) -> usize {
|
||||
if dof < self.mesh_dofs() { dof % 3 } else { (dof - self.mesh_dofs()) % 6 }
|
||||
}
|
||||
|
||||
/// The first degree of freedom and their count (3 or 6) of a node or coupled reference
|
||||
/// point label.
|
||||
pub fn label_dofs(&self, label: u32) -> Option<(usize, usize)> {
|
||||
match self.node(label) {
|
||||
Some(n) => Some((3 * n, 3)),
|
||||
None => self.points.iter().position(|p| p.label == label).map(|k| (self.point_dof(k, 0), 6)),
|
||||
}
|
||||
/// The kind of a degree of freedom.
|
||||
pub fn kind(&self, dof: usize) -> Option<DofKind> {
|
||||
self.layout.locate(dof).map(|(_, kind)| kind)
|
||||
}
|
||||
|
||||
/// What a degree of freedom is, in the user's terms.
|
||||
pub fn describe_dof(&self, dof: usize) -> String {
|
||||
let direction = DIRECTIONS.get(self.direction(dof)).copied().unwrap_or("?");
|
||||
if dof < self.mesh_dofs() {
|
||||
format!("node {} in {direction}", self.labels.get(dof / 3).copied().unwrap_or(0))
|
||||
} else {
|
||||
let name = self.points.get((dof - self.mesh_dofs()) / 6).map_or("?", |p| p.name.as_str());
|
||||
format!("reference point `{name}` in {direction}")
|
||||
let Some((slot, kind)) = self.layout.locate(dof) else { return format!("degree of freedom {dof}") };
|
||||
match self.labels.get(slot) {
|
||||
Some(label) => format!("node {label} in {}", kind.describe()),
|
||||
None => {
|
||||
let name = self.points.get(slot - self.labels.len()).map_or("?", |p| p.name.as_str());
|
||||
format!("reference point `{name}` in {}", kind.describe())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +134,15 @@ impl Mesh {
|
||||
Ok(Point { name: name.to_string(), label: p.label, xyz: p.xyz })
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
let dofs = 3 * labels.len() + 6 * points.len();
|
||||
let mut layout = DofLayout::builder();
|
||||
for e in &elements {
|
||||
layout.element(&e.2, e.3.dofs());
|
||||
}
|
||||
for p in &points {
|
||||
layout.point(p.label, DofSet::STRUCTURAL);
|
||||
}
|
||||
let layout = layout.build();
|
||||
let dofs = layout.dofs();
|
||||
if dofs > max_dofs {
|
||||
return Err(Error::Limit(format!("the model has {dofs} degrees of freedom; the limit is {max_dofs}")));
|
||||
}
|
||||
@@ -149,7 +150,7 @@ impl Mesh {
|
||||
.iter()
|
||||
.map(|l| model.node(*l).map(|n| n.xyz).ok_or_else(|| Error::Invalid(format!("node {l} is missing"))))
|
||||
.collect::<Result<_>>()?;
|
||||
let mut mesh = Mesh { labels, xyz, elements: Vec::with_capacity(elements.len()), points };
|
||||
let mut mesh = Mesh { labels, xyz, elements: Vec::with_capacity(elements.len()), points, layout };
|
||||
for (label, kind, nodes, formulation, d, density) in elements {
|
||||
let idx: Vec<usize> = nodes.iter().filter_map(|n| mesh.node(*n)).collect();
|
||||
let exyz: Vec<[f64; 3]> = idx.iter().filter_map(|i| mesh.xyz.get(*i).copied()).collect();
|
||||
@@ -183,10 +184,10 @@ pub fn prescribed(
|
||||
.push(format!("boundary condition `{name}` acts on reference point `{rp}`, which no coupling ties to the mesh; it holds nothing"));
|
||||
continue;
|
||||
}
|
||||
for (first, count) in resolved.target_nodes(bc.target()).iter().filter_map(|l| mesh.label_dofs(*l)) {
|
||||
for dof in 1..=count as u8 {
|
||||
let Some(v) = bc.value(dof) else { continue };
|
||||
let index = first + usize::from(dof - 1);
|
||||
for slot in resolved.target_nodes(bc.target()).iter().filter_map(|l| mesh.layout.slot(*l)) {
|
||||
for kind in mesh.layout.kinds(slot).iter() {
|
||||
let Some(v) = bc.value(kind.number()) else { continue };
|
||||
let index = mesh.dof(slot, kind)?;
|
||||
if !active.get(index).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
@@ -207,14 +208,27 @@ pub fn prescribed(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn add(f: &mut [f64], first: usize, v: [f64; 3]) {
|
||||
for (k, c) in v.iter().enumerate() {
|
||||
if let Some(x) = f.get_mut(first + k) {
|
||||
/// Add a vector to the three degrees of freedom `dofs`.
|
||||
fn add(f: &mut [f64], dofs: [usize; 3], v: [f64; 3]) {
|
||||
for (d, c) in dofs.iter().zip(v) {
|
||||
if let Some(x) = f.get_mut(*d) {
|
||||
*x += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The translations (or rotations) of layout slot `slot`, if it has them.
|
||||
fn vector_dofs(mesh: &Mesh, slot: usize, rotation: bool) -> Option<[usize; 3]> {
|
||||
let kind = |c| if rotation { DofKind::rotation(c) } else { DofKind::translation(c) };
|
||||
let [a, b, c] = [0, 1, 2].map(|c| kind(c).and_then(|k| mesh.layout.dof(slot, k)));
|
||||
Some([a?, b?, c?])
|
||||
}
|
||||
|
||||
/// The translations of mesh node `node`.
|
||||
fn translations(mesh: &Mesh, node: usize) -> Result<[usize; 3]> {
|
||||
vector_dofs(mesh, node, false).ok_or_else(|| Error::Internal(format!("mesh node {node} has no translations")))
|
||||
}
|
||||
|
||||
/// The load vector of a static step: nodal forces and tractions as the model distributes them,
|
||||
/// forces and moments on coupled reference points, consistent pressure loads, gravity and body
|
||||
/// forces.
|
||||
@@ -230,15 +244,14 @@ pub fn loads(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step, activ
|
||||
return Err(Error::Invalid(format!("load `{name}` acts on reference point `{rp}`, which no coupling ties to the mesh")));
|
||||
}
|
||||
// Moments act on the rotations, which only reference points have.
|
||||
let offset = if matches!(load, Load::Moment { .. }) { 3 } else { 0 };
|
||||
let rotation = matches!(load, Load::Moment { .. });
|
||||
for (label, v) in resolved.nodal_loads.get(name).map(Vec::as_slice).unwrap_or_default() {
|
||||
let (first, count) =
|
||||
mesh.label_dofs(*label).ok_or_else(|| Error::Invalid(format!("load `{name}`: node {label} belongs to no solid element")))?;
|
||||
if offset + 3 > count {
|
||||
return Err(Error::Invalid(format!("moment `{name}`: solid nodes have no rotations; apply it to a coupled reference point")));
|
||||
}
|
||||
for (c, x) in v.iter().enumerate() {
|
||||
let dof = first + offset + c;
|
||||
let slot =
|
||||
mesh.layout.slot(*label).ok_or_else(|| Error::Invalid(format!("load `{name}`: node {label} belongs to no solid element")))?;
|
||||
let dofs = vector_dofs(mesh, slot, rotation).ok_or_else(|| {
|
||||
Error::Invalid(format!("moment `{name}`: solid nodes have no rotations; apply it to a coupled reference point"))
|
||||
})?;
|
||||
for (&dof, x) in dofs.iter().zip(v) {
|
||||
if *x != 0.0 && !active.get(dof).copied().unwrap_or(false) {
|
||||
return Err(Error::Invalid(format!(
|
||||
"load `{name}`: {} is not coupled to the mesh (its coupling leaves that direction free)",
|
||||
@@ -246,7 +259,7 @@ pub fn loads(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step, activ
|
||||
)));
|
||||
}
|
||||
}
|
||||
add(&mut f, first + offset, *v);
|
||||
add(&mut f, dofs, *v);
|
||||
}
|
||||
}
|
||||
Load::Pressure { set, magnitude } => {
|
||||
@@ -262,7 +275,7 @@ pub fn loads(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step, activ
|
||||
let xyz: Vec<[f64; 3]> = idx.iter().filter_map(|i| mesh.xyz.get(*i).copied()).collect();
|
||||
let forces = face::pressure(&xyz, *magnitude).map_err(|e| Error::element(fc.element, e))?;
|
||||
for (i, v) in idx.iter().zip(forces) {
|
||||
add(&mut f, 3 * i, v);
|
||||
add(&mut f, translations(mesh, *i)?, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,13 +286,13 @@ pub fn loads(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step, activ
|
||||
continue;
|
||||
}
|
||||
let rho = e.density.ok_or_else(|| Error::Invalid(format!("load `{name}`: element {} has no density", e.label)))?;
|
||||
body(&mut f, e, acceleration.map(|a| rho * a))?;
|
||||
body(&mut f, mesh, e, acceleration.map(|a| rho * a))?;
|
||||
}
|
||||
}
|
||||
Load::BodyForce { set, vector } => {
|
||||
for label in resolved.sets.get(set).map(|r| r.elements.as_slice()).unwrap_or_default() {
|
||||
let e = mesh.element(*label).ok_or_else(|| Error::Invalid(format!("load `{name}`: element {label} is not a solid element")))?;
|
||||
body(&mut f, e, *vector)?;
|
||||
body(&mut f, mesh, e, *vector)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,10 +300,10 @@ pub fn loads(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step, activ
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
fn body(f: &mut [f64], e: &Elem, b: [f64; 3]) -> Result<()> {
|
||||
fn body(f: &mut [f64], mesh: &Mesh, e: &Elem, b: [f64; 3]) -> Result<()> {
|
||||
let forces = e.solid()?.body_force(b).map_err(|err| Error::element(e.label, err))?;
|
||||
for (node, v) in e.nodes.iter().zip(forces) {
|
||||
add(f, 3 * node, v);
|
||||
add(f, translations(mesh, *node)?, v);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user