forked from 1b-agent/fieldcraft-upstream
Implicit solver: fem kernels (tet4/10, hex8, hex20), sparse Cholesky, block Lanczos, solve.* commands (M4) (#23)
* Renumber the layer table for solver crates: fem at L1, solver at L2, fsi at L3 L0 model -> L1 geometry, mesh, fem, decks, render -> L2 solver (explicit, lbm later) -> L3 fsi (later) -> L4 doc -> L5 io -> L6 engine -> L7 ui-egui, mcp -> apps. UI_MIN_LAYER moves to 7, the testkit sits above it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * modelcraft-fem: element kernels for tet4, tet10, hex8 (full, incompatible modes), hex20 (full, reduced) Shape functions, Gauss and conical-product quadrature, stiffness, consistent and HRZ-lumped mass, body forces, consistent pressure loads on faces, integration-point stresses and their least-squares extrapolation to nodes, isotropic and orthotropic elasticity. The element table is one row per kind, so wedges, shells and beams are added locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * modelcraft-solver: linear static and frequency analysis with an analytic verification suite Assembly from the resolved model on the node graph, boundary conditions by elimination, consistent pressure, gravity and body-force loads, nested-dissection ordering, sparse up-looking Cholesky with a factor-size cap, reactions, integration-point and nodal-averaged stresses, von Mises; shift-invert block Lanczos with full reorthogonalization for natural modes, participation and effective mass. ADR 0001 records the own-linear-algebra decision (determinism, wasm). Verified against closed forms: displacement and traction patch tests for every element, cantilever tip deflection (Timoshenko) with convergence rates, Lamé cylinder, Kirsch hole (Kt -> 3), cantilever and free-free beam frequencies with six rigid-body modes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Engine: solve.static, solve.frequency and solve.probe; results kept in the session Results are stored per step, restored on a failed command and cleared when the model changes. Caps (degrees of freedom, factor entries, Lanczos vectors) may only be lowered; hostile parameters are tested against a solvable session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: implicit solver architecture, roadmap M4, parity and references Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Simplify the solver: shared face integration, block assembly, cached extrapolation - model: one face integrator (nodal::face_normal_weights) for tractions and pressures - fem: mid-side tables from ElementKind::edges, orthotropic via inverse3, symmetric accumulation without a symmetrize pass, incompatible-mode recovery without K_uu, model's coordinate check, dead table rows removed - solver: one pattern search per node pair (block, scalar and diagonal adds), extrapolation built once per element type, reactions from the prescribed rows only, matrices dropped after restriction, the free-body shift keyed on the factorization error, step selection in the solver, Error::Element naming the element - engine: the solver owns the default step and the shift check; the model comparison runs once Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: results are stamped with the session revision 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:
33 files changed
+4559
-53
No files matched your search
@@ -0,0 +1,138 @@
|
||||
//! Small dense matrices: element matrices and the little systems inside an element (static
|
||||
//! condensation, extrapolation fits).
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A square matrix, row-major.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Matrix {
|
||||
pub n: usize,
|
||||
pub data: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
pub fn zeros(n: usize) -> Matrix {
|
||||
Matrix { n, data: vec![0.0; n * n] }
|
||||
}
|
||||
|
||||
pub fn get(&self, i: usize, j: usize) -> f64 {
|
||||
self.data.get(i * self.n + j).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub fn add(&mut self, i: usize, j: usize, v: f64) {
|
||||
if let Some(x) = self.data.get_mut(i * self.n + j) {
|
||||
*x += v;
|
||||
}
|
||||
}
|
||||
|
||||
/// y = A x (x shorter than n is padded with zeros).
|
||||
pub fn mul_vec(&self, x: &[f64]) -> Vec<f64> {
|
||||
self.data.chunks_exact(self.n.max(1)).map(|row| row.iter().zip(x).map(|(a, b)| a * b).sum()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The Cholesky factor of a small symmetric positive definite matrix (lower triangle, row-major).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cholesky {
|
||||
n: usize,
|
||||
l: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Cholesky {
|
||||
/// Factor `a` (only its lower triangle is read). Fails when a pivot is not positive relative
|
||||
/// to the matrix's diagonal.
|
||||
pub fn new(a: &Matrix) -> Result<Cholesky> {
|
||||
let n = a.n;
|
||||
let mut l = vec![0.0; n * n];
|
||||
let scale = (0..n).map(|i| a.get(i, i).abs()).fold(0.0, f64::max);
|
||||
for j in 0..n {
|
||||
let mut d = a.get(j, j);
|
||||
for k in 0..j {
|
||||
let v = l[j * n + k];
|
||||
d -= v * v;
|
||||
}
|
||||
if !(d > 1e-13 * scale) {
|
||||
return Err(Error::Singular(format!("a {n} × {n} element matrix is not positive definite (pivot {j})")));
|
||||
}
|
||||
let djj = d.sqrt();
|
||||
l[j * n + j] = djj;
|
||||
for i in j + 1..n {
|
||||
let mut s = a.get(i, j);
|
||||
for k in 0..j {
|
||||
s -= l[i * n + k] * l[j * n + k];
|
||||
}
|
||||
l[i * n + j] = s / djj;
|
||||
}
|
||||
}
|
||||
Ok(Cholesky { n, l })
|
||||
}
|
||||
|
||||
/// Solve A x = b in place.
|
||||
pub fn solve(&self, b: &mut [f64]) {
|
||||
let n = self.n.min(b.len());
|
||||
for i in 0..n {
|
||||
let mut s = b[i];
|
||||
for k in 0..i {
|
||||
s -= self.l[i * self.n + k] * b[k];
|
||||
}
|
||||
b[i] = s / self.l[i * self.n + i];
|
||||
}
|
||||
for i in (0..n).rev() {
|
||||
let mut s = b[i];
|
||||
for k in i + 1..n {
|
||||
s -= self.l[k * self.n + i] * b[k];
|
||||
}
|
||||
b[i] = s / self.l[i * self.n + i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The inverse and determinant of a 3 × 3 matrix; `None` when it is singular or not finite.
|
||||
pub fn inverse3(m: &[[f64; 3]; 3]) -> Option<([[f64; 3]; 3], f64)> {
|
||||
let det = det3(m);
|
||||
if !(det.abs() > 0.0 && det.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let mut inv = [[0.0; 3]; 3];
|
||||
for (i, row) in inv.iter_mut().enumerate() {
|
||||
for (j, v) in row.iter_mut().enumerate() {
|
||||
let (j1, j2) = ((j + 1) % 3, (j + 2) % 3);
|
||||
let (i1, i2) = ((i + 1) % 3, (i + 2) % 3);
|
||||
// inv[i][j] = cofactor[j][i] / det.
|
||||
*v = (m[j1][i1] * m[j2][i2] - m[j1][i2] * m[j2][i1]) / det;
|
||||
}
|
||||
}
|
||||
Some((inv, det))
|
||||
}
|
||||
|
||||
pub fn det3(m: &[[f64; 3]; 3]) -> f64 {
|
||||
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
|
||||
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cholesky_solves_and_rejects_indefinite() {
|
||||
let mut a = Matrix::zeros(3);
|
||||
a.data = vec![4.0, 2.0, 0.6, 2.0, 5.0, 1.0, 0.6, 1.0, 3.0];
|
||||
let x = [1.0, -2.0, 0.5];
|
||||
let mut b = a.mul_vec(&x);
|
||||
Cholesky::new(&a).unwrap().solve(&mut b);
|
||||
assert!(b.iter().zip(x).all(|(p, q)| (p - q).abs() < 1e-14), "{b:?}");
|
||||
a.data[8] = -1.0;
|
||||
assert!(Cholesky::new(&a).is_err());
|
||||
let m = [[2.0, 0.5, 0.0], [0.1, 3.0, 0.2], [0.0, 0.4, 1.0]];
|
||||
let (inv, det) = inverse3(&m).unwrap();
|
||||
assert!((det - det3(&m)).abs() < 1e-15);
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
let p: f64 = (0..3).map(|k| m[i][k] * inv[k][j]).sum();
|
||||
assert!((p - if i == j { 1.0 } else { 0.0 }).abs() < 1e-14);
|
||||
}
|
||||
}
|
||||
assert!(inverse3(&[[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [0.0, 0.0, 1.0]]).is_none());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user