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,113 @@
|
||||
//! Linear elastic constitutive matrices.
|
||||
//!
|
||||
//! Stress and strain are 6-vectors in the order xx, yy, zz, xy, xz, yz, with engineering shear
|
||||
//! strains (γ = 2ε), so σ = D ε. The order puts the orthotropic shear moduli G₁₂, G₁₃, G₂₃ on
|
||||
//! the diagonal in the order the model lists them. Material axes are the global axes until the
|
||||
//! model has material orientations.
|
||||
//!
|
||||
//! Orthotropic stiffness is the inverse of the compliance written with engineering constants
|
||||
//! (R. M. Jones, *Mechanics of Composite Materials*, 2nd ed., Taylor & Francis 1999, §2.4):
|
||||
//! S₁₁ = 1/E₁, S₁₂ = −ν₁₂/E₁, S₁₃ = −ν₁₃/E₁, S₂₃ = −ν₂₃/E₂ (ν_ji/E_j = ν_ij/E_i), and
|
||||
//! 1/G on the shear diagonal.
|
||||
|
||||
use modelcraft_model::Elastic;
|
||||
|
||||
use crate::dense::inverse3;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A 6 × 6 elasticity matrix, row-major.
|
||||
pub type Elasticity = [[f64; 6]; 6];
|
||||
|
||||
/// The isotropic elasticity matrix.
|
||||
pub fn isotropic(young: f64, poisson: f64) -> Result<Elasticity> {
|
||||
if !(young > 0.0 && young.is_finite() && poisson > -1.0 && poisson < 0.5) {
|
||||
return Err(Error::Invalid(format!("isotropic constants E = {young}, ν = {poisson} are not a stable material")));
|
||||
}
|
||||
let lambda = young * poisson / ((1.0 + poisson) * (1.0 - 2.0 * poisson));
|
||||
let mu = young / (2.0 * (1.0 + poisson));
|
||||
let mut d = [[0.0; 6]; 6];
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
d[i][j] = lambda;
|
||||
}
|
||||
d[i][i] = lambda + 2.0 * mu;
|
||||
d[i + 3][i + 3] = mu;
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// The orthotropic elasticity matrix from engineering constants.
|
||||
pub fn orthotropic(young: [f64; 3], poisson: [f64; 3], shear: [f64; 3]) -> Result<Elasticity> {
|
||||
let [e1, e2, e3] = young;
|
||||
let [n12, n13, n23] = poisson;
|
||||
let s = [[1.0 / e1, -n12 / e1, -n13 / e1], [-n12 / e1, 1.0 / e2, -n23 / e2], [-n13 / e1, -n23 / e2, 1.0 / e3]];
|
||||
let scale = (1.0 / e1) * (1.0 / e2) * (1.0 / e3);
|
||||
let inverse = inverse3(&s).filter(|(_, det)| *det > 1e-12 * scale);
|
||||
let Some((normal, _)) = inverse.filter(|_| shear.iter().all(|g| *g > 0.0 && g.is_finite())) else {
|
||||
return Err(Error::Invalid("the orthotropic constants do not describe a stable material".into()));
|
||||
};
|
||||
let mut d = [[0.0; 6]; 6];
|
||||
for i in 0..3 {
|
||||
d[i][..3].copy_from_slice(&normal[i]);
|
||||
}
|
||||
for (k, g) in shear.iter().enumerate() {
|
||||
d[3 + k][3 + k] = *g;
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// The elasticity matrix of a model material's elastic behaviour.
|
||||
pub fn elasticity(e: &Elastic) -> Result<Elasticity> {
|
||||
match *e {
|
||||
Elastic::Isotropic { young, poisson } => isotropic(young, poisson),
|
||||
Elastic::EngineeringConstants { young, poisson, shear } => orthotropic(young, poisson, shear),
|
||||
}
|
||||
}
|
||||
|
||||
/// The von Mises equivalent stress of a stress 6-vector.
|
||||
pub fn von_mises(s: &[f64; 6]) -> f64 {
|
||||
let [xx, yy, zz, xy, xz, yz] = *s;
|
||||
let normal = (xx - yy) * (xx - yy) + (yy - zz) * (yy - zz) + (zz - xx) * (zz - xx);
|
||||
(0.5 * normal + 3.0 * (xy * xy + xz * xz + yz * yz)).sqrt()
|
||||
}
|
||||
|
||||
/// σ = D ε.
|
||||
pub fn stress(d: &Elasticity, strain: &[f64; 6]) -> [f64; 6] {
|
||||
let mut s = [0.0; 6];
|
||||
for (si, row) in s.iter_mut().zip(d) {
|
||||
*si = row.iter().zip(strain).map(|(a, b)| a * b).sum();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn orthotropic_with_isotropic_constants_is_isotropic() {
|
||||
let (e, nu) = (210e3, 0.3);
|
||||
let g = e / (2.0 * (1.0 + nu));
|
||||
let a = isotropic(e, nu).unwrap();
|
||||
let b = orthotropic([e; 3], [nu; 3], [g; 3]).unwrap();
|
||||
for i in 0..6 {
|
||||
for j in 0..6 {
|
||||
assert!((a[i][j] - b[i][j]).abs() < 1e-9 * e, "{i} {j}: {} vs {}", a[i][j], b[i][j]);
|
||||
}
|
||||
}
|
||||
assert!(isotropic(1.0, 0.5).is_err());
|
||||
assert!(orthotropic([1.0; 3], [0.9; 3], [1.0; 3]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orthotropic_uniaxial_response() {
|
||||
// Stress along 1 alone gives strains σ/E1, −ν12 σ/E1, −ν13 σ/E1.
|
||||
let (young, poisson) = ([140e3, 10e3, 12e3], [0.3, 0.25, 0.4]);
|
||||
let d = orthotropic(young, poisson, [5e3, 4e3, 3.8e3]).unwrap();
|
||||
let strain = [1.0 / young[0], -poisson[0] / young[0], -poisson[1] / young[0], 0.0, 0.0, 0.0];
|
||||
let s = stress(&d, &strain);
|
||||
assert!((s[0] - 1.0).abs() < 1e-12 && s[1].abs() < 1e-12 && s[2].abs() < 1e-12, "{s:?}");
|
||||
assert!((von_mises(&[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - 1.0).abs() < 1e-15);
|
||||
assert!((von_mises(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0]) - 3f64.sqrt()).abs() < 1e-15);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user