forked from 1b-agent/fieldcraft-upstream
fem: finite-strain material point, total Lagrangian elements and follower pressure
The material point interface returns the Kirchhoff stress and the spatial tangent of its Lie derivative from the deformation gradient, with per-point history committed by the solver: St Venant–Kirchhoff (pushed forward from S = D E) and the decoupled compressible neo-Hookean with its isochoric and volumetric parts. Total Lagrangian solids (tet4, tet10, wedge6, hex8, hex20 full and reduced) and plane strain and axisymmetric elements give internal forces and the consistent tangent with its geometric stiffness; hybrid solids use the Simo–Taylor–Pister three-field formulation on the existing pressure fields. Incompatible modes and plane stress are refused by name. Follower pressure on faces and 2D edges with its load stiffness. Every tangent is checked against central differences of its forces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
bc9fe2191a
commit
2321d75cc6
8 files changed
+1217
-16
No files matched your search
@@ -0,0 +1,356 @@
|
||||
//! The material point of finite-strain analysis: from the deformation gradient F (and the
|
||||
//! material's history) to the stress and its consistent tangent.
|
||||
//!
|
||||
//! **Stress measure.** A material returns the **Kirchhoff stress** τ = J σ (J = det F, σ the
|
||||
//! Cauchy stress) and the **spatial tangent** c of the Lie derivative of τ, L_v τ = c : d, with
|
||||
//! d the rate of deformation. Both are 6-vectors and 6 × 6 matrices in the order xx, yy, zz,
|
||||
//! xy, xz, yz, c taking engineering shears like the small-strain elasticity matrices (so the
|
||||
//! linear elasticity D is c at F = I). This is the total Lagrangian formulation written with
|
||||
//! spatial quantities: τ = F S Fᵀ and c_ijkl = F_iI F_jJ F_kK F_lL C_IJKL push the second
|
||||
//! Piola–Kirchhoff stress S and its material tangent C = ∂S/∂E forward, so the internal forces
|
||||
//! ∫ Bₓᵀ τ dV₀ and the stiffness ∫ (Bₓᵀ c Bₓ + ∇ₓN·τ∇ₓN) dV₀, with the shape function gradients
|
||||
//! of the current configuration and integrals over the reference volume, are exactly the total
|
||||
//! Lagrangian ∫ B₀ᵀ S dV₀ and its linearization (J. Bonet, R. D. Wood, *Nonlinear Continuum
|
||||
//! Mechanics for Finite Element Analysis*, 2nd ed., Cambridge 2008, §7.4 and §9.2; G. A.
|
||||
//! Holzapfel, *Nonlinear Solid Mechanics*, Wiley 2000, §6.6). Finite-strain plasticity (the
|
||||
//! exponential return map of J. C. Simo, CMAME 99:61–112, 1992) gives τ and c directly, which is
|
||||
//! why the interface is spatial.
|
||||
//!
|
||||
//! **History.** A material keeps [`MaterialModel::history_len`] numbers per integration point.
|
||||
//! An evaluation reads the history committed at the end of the last converged increment and
|
||||
//! writes the updated one to a separate slice; the solver commits it when the increment
|
||||
//! converges, so a rejected iteration or a cut-back increment leaves no trace. The elastic
|
||||
//! materials here keep none.
|
||||
//!
|
||||
//! **Materials:**
|
||||
//! - **St Venant–Kirchhoff**: S = D E with E = ½(FᵀF − I) the Green–Lagrange strain and D the
|
||||
//! small-strain elasticity (isotropic or orthotropic in the global axes): an elastic material
|
||||
//! in a nonlinear step. Exact for large rotations with small strains; it softens without bound
|
||||
//! in large compression, so large strains want a hyperelastic model.
|
||||
//! - **Neo-Hookean** (compressible, with the isochoric–volumetric split of P. J. Flory,
|
||||
//! *Thermodynamic relations for high elastic materials*, Trans. Faraday Soc. 57:829–838, 1961):
|
||||
//! W = μ/2 (Ī₁ − 3) + U(J), Ī₁ = J^(−2/3) tr b, U = κ/2 (J − 1)². Then
|
||||
//! τ = μ dev b̄ + J U′ I with b̄ = J^(−2/3) b, and
|
||||
//! c = ⅔ μ tr b̄ ℙ − ⅔ (τ_iso ⊗ I + I ⊗ τ_iso) + J (p̃ I ⊗ I − 2p 𝕀), with p = U′(J),
|
||||
//! p̃ = p + J U″(J), 𝕀 the symmetric fourth-order identity and ℙ = 𝕀 − ⅓ I ⊗ I (Holzapfel 2000,
|
||||
//! §6.6, eqs. 6.165–6.169 and 6.193, in Kirchhoff form). The split is what the mixed (hybrid)
|
||||
//! elements need: they take the isochoric part from [`MaterialModel::isochoric`] and the
|
||||
//! volumetric one from [`MaterialModel::volumetric`] with the element's own dilatation.
|
||||
|
||||
use fieldcraft_model::{Elastic, Hyperelastic};
|
||||
|
||||
use crate::dense::det3;
|
||||
use crate::material::{Elasticity, elasticity, stress};
|
||||
use crate::rotation::{IDENTITY, Mat3, mul, transpose, voigt};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A 6 × 6 tangent in Voigt order, engineering-shear columns.
|
||||
pub type Tangent = [[f64; 6]; 6];
|
||||
|
||||
/// The (row, column) of each Voigt component.
|
||||
pub const PAIRS: [(usize, usize); 6] = [(0, 0), (1, 1), (2, 2), (0, 1), (0, 2), (1, 2)];
|
||||
|
||||
/// The Kirchhoff stress and the spatial tangent of a material point.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Response {
|
||||
pub stress: [f64; 6],
|
||||
pub tangent: Tangent,
|
||||
}
|
||||
|
||||
/// The constitutive model of a solid in a nonlinear step.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum MaterialModel {
|
||||
StVenantKirchhoff(Box<Elasticity>),
|
||||
NeoHookean { shear: f64, bulk: f64 },
|
||||
}
|
||||
|
||||
/// The 6 × 6 matrix of the symmetric fourth-order identity 𝕀 (engineering-shear columns).
|
||||
fn identity4() -> Tangent {
|
||||
let mut i4 = [[0.0; 6]; 6];
|
||||
for k in 0..6 {
|
||||
i4[k][k] = if k < 3 { 1.0 } else { 0.5 };
|
||||
}
|
||||
i4
|
||||
}
|
||||
|
||||
/// The matrix T with (F A Fᵀ) = T a for a symmetric A of Voigt vector a (tensor components):
|
||||
/// T[(ij)][(IJ)] = F_iI F_jJ + F_iJ F_jI for I ≠ J and F_iI F_jI for I = J. A tangent pushes
|
||||
/// forward as T C Tᵀ.
|
||||
pub fn push_forward(f: &Mat3) -> Tangent {
|
||||
let mut t = [[0.0; 6]; 6];
|
||||
for (a, &(i, j)) in PAIRS.iter().enumerate() {
|
||||
for (b, &(ii, jj)) in PAIRS.iter().enumerate() {
|
||||
t[a][b] = if ii == jj { f[i][ii] * f[j][ii] } else { f[i][ii] * f[j][jj] + f[i][jj] * f[j][ii] };
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
/// T C Tᵀ.
|
||||
fn sandwich(t: &Tangent, c: &Tangent) -> Tangent {
|
||||
let mut tc = [[0.0; 6]; 6];
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
tc[a][b] = (0..6).map(|k| t[a][k] * c[k][b]).sum();
|
||||
}
|
||||
}
|
||||
let mut out = [[0.0; 6]; 6];
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
out[a][b] = (0..6).map(|k| tc[a][k] * t[b][k]).sum();
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// det F, refusing an inverted or collapsed point.
|
||||
fn jacobian(f: &Mat3) -> Result<f64> {
|
||||
let j = det3(f);
|
||||
if j > 0.0 && j.is_finite() { Ok(j) } else { Err(Error::Degenerate(format!("the material is inverted (J = det F = {j:.3e} is not positive)"))) }
|
||||
}
|
||||
|
||||
impl MaterialModel {
|
||||
/// The model of a material's elastic behaviour in a nonlinear step: neo-Hookean when the
|
||||
/// material asks for it (its isotropic constants give μ and κ), St Venant–Kirchhoff
|
||||
/// otherwise.
|
||||
pub fn from_model(elastic: &Elastic, hyperelastic: Option<&Hyperelastic>) -> Result<MaterialModel> {
|
||||
match hyperelastic {
|
||||
None => Ok(MaterialModel::StVenantKirchhoff(Box::new(elasticity(elastic)?))),
|
||||
Some(Hyperelastic::NeoHookean {}) => {
|
||||
let (young, poisson) = crate::material::isotropic_constants(elastic, "neo-Hookean")?;
|
||||
let (shear, bulk) = Hyperelastic::moduli(young, poisson);
|
||||
Ok(MaterialModel::NeoHookean { shear, bulk })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The model of an element whose elasticity is `d` (the small-strain one) and whose material
|
||||
/// has the large-strain option `hyperelastic`: neo-Hookean takes μ = D₄₄ and κ = D₁₂ + ⅔ μ of
|
||||
/// an isotropic `d`, St Venant–Kirchhoff `d` itself.
|
||||
pub fn from_elasticity(d: &Elasticity, hyperelastic: Option<&Hyperelastic>) -> MaterialModel {
|
||||
match hyperelastic {
|
||||
None => MaterialModel::StVenantKirchhoff(Box::new(*d)),
|
||||
Some(Hyperelastic::NeoHookean {}) => MaterialModel::NeoHookean { shear: d[3][3], bulk: d[0][1] + 2.0 / 3.0 * d[3][3] },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
MaterialModel::StVenantKirchhoff(_) => "St Venant–Kirchhoff",
|
||||
MaterialModel::NeoHookean { .. } => "neo-Hookean",
|
||||
}
|
||||
}
|
||||
|
||||
/// History variables per integration point.
|
||||
pub fn history_len(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
/// Whether the strain energy splits into isochoric and volumetric parts (what mixed
|
||||
/// elements need).
|
||||
pub fn is_decoupled(&self) -> bool {
|
||||
matches!(self, MaterialModel::NeoHookean { .. })
|
||||
}
|
||||
|
||||
/// The Kirchhoff stress and spatial tangent at deformation gradient `f`, from the history
|
||||
/// `committed` (written, updated, to `updated`).
|
||||
pub fn response(&self, f: &Mat3, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
updated.iter_mut().zip(committed).for_each(|(u, c)| *u = *c);
|
||||
let j = jacobian(f)?;
|
||||
match self {
|
||||
MaterialModel::StVenantKirchhoff(d) => {
|
||||
let c = mul(&transpose(f), f);
|
||||
let e = [0.5 * (c[0][0] - 1.0), 0.5 * (c[1][1] - 1.0), 0.5 * (c[2][2] - 1.0), c[0][1], c[0][2], c[1][2]];
|
||||
let s = stress(d, &e);
|
||||
let t = push_forward(f);
|
||||
Ok(Response { stress: std::array::from_fn(|a| (0..6).map(|b| t[a][b] * s[b]).sum()), tangent: sandwich(&t, d) })
|
||||
}
|
||||
MaterialModel::NeoHookean { .. } => {
|
||||
let iso = self.isochoric(f)?;
|
||||
let (p, dp) = self.volumetric(j).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
|
||||
Ok(add_pressure(&iso, j, p, j * dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The isochoric part of a decoupled material (τ_iso = dev τ̄ and its tangent).
|
||||
pub fn isochoric(&self, f: &Mat3) -> Result<Response> {
|
||||
let &MaterialModel::NeoHookean { shear: mu, .. } = self else {
|
||||
return Err(Error::NotSupported(format!("the isochoric–volumetric split of a {} material", self.name())));
|
||||
};
|
||||
let j = jacobian(f)?;
|
||||
let scale = j.powf(-2.0 / 3.0);
|
||||
let b = mul(f, &transpose(f));
|
||||
let bbar = voigt(&b).map(|v| scale * v);
|
||||
let tr = bbar[0] + bbar[1] + bbar[2];
|
||||
let mut tau = bbar.map(|v| mu * v);
|
||||
for v in tau.iter_mut().take(3) {
|
||||
*v -= mu * tr / 3.0;
|
||||
}
|
||||
let i4 = identity4();
|
||||
let mut c = [[0.0; 6]; 6];
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
let (ia, ib) = (if a < 3 { 1.0 } else { 0.0 }, if b < 3 { 1.0 } else { 0.0 });
|
||||
let projector = i4[a][b] - ia * ib / 3.0;
|
||||
c[a][b] = 2.0 / 3.0 * mu * tr * projector - 2.0 / 3.0 * (tau[a] * ib + ia * tau[b]);
|
||||
}
|
||||
}
|
||||
Ok(Response { stress: tau, tangent: c })
|
||||
}
|
||||
|
||||
/// U′(J) and U″(J) of a decoupled material's volumetric energy.
|
||||
pub fn volumetric(&self, j: f64) -> Option<(f64, f64)> {
|
||||
match *self {
|
||||
MaterialModel::NeoHookean { bulk, .. } => Some((bulk * (j - 1.0), bulk)),
|
||||
MaterialModel::StVenantKirchhoff(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An isochoric response with a pressure p acting on dilatation J: τ + J p I, and the tangent
|
||||
/// c + J (p̃ I ⊗ I − 2p 𝕀) with p̃ = p + `j_dp` (J dp/dJ, zero when p is held).
|
||||
pub fn add_pressure(iso: &Response, j: f64, p: f64, j_dp: f64) -> Response {
|
||||
let mut out = *iso;
|
||||
let i4 = identity4();
|
||||
for a in 0..3 {
|
||||
out.stress[a] += j * p;
|
||||
}
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
let ii = if a < 3 && b < 3 { 1.0 } else { 0.0 };
|
||||
out.tangent[a][b] += j * ((p + j_dp) * ii - 2.0 * p * i4[a][b]);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The logarithmic (Hencky) strain ln V = ½ ln b of a deformation gradient, as a strain 6-vector
|
||||
/// with engineering shears; `None` when F is not finite or inverted.
|
||||
pub fn logarithmic_strain(f: &Mat3) -> Option<[f64; 6]> {
|
||||
let (l, v) = crate::dense::symmetric_eigen(&mul(f, &transpose(f)))?;
|
||||
if !l.iter().all(|x| *x > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let ln = l.map(|x| 0.5 * x.ln());
|
||||
let e = |i: usize, j: usize| (0..3).map(|k| ln[k] * v[i][k] * v[j][k]).sum::<f64>();
|
||||
Some([e(0, 0), e(1, 1), e(2, 2), 2.0 * e(0, 1), 2.0 * e(0, 2), 2.0 * e(1, 2)])
|
||||
}
|
||||
|
||||
/// The deformation gradient F = I + Σ u_a ⊗ ∇₀N_a of nodal displacements and reference
|
||||
/// gradients.
|
||||
pub fn deformation_gradient(u: &[[f64; 3]], gradients: &[[f64; 3]]) -> Mat3 {
|
||||
let mut f = IDENTITY;
|
||||
for (ua, g) in u.iter().zip(gradients) {
|
||||
for i in 0..3 {
|
||||
for jj in 0..3 {
|
||||
f[i][jj] += ua[i] * g[jj];
|
||||
}
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dense::inverse3;
|
||||
use crate::material::isotropic;
|
||||
|
||||
fn deformation() -> Mat3 {
|
||||
[[1.2, 0.15, -0.05], [-0.1, 0.9, 0.2], [0.07, -0.12, 1.05]]
|
||||
}
|
||||
|
||||
/// S = F⁻¹ τ F⁻ᵀ.
|
||||
fn pull_back_stress(f: &Mat3, tau: &[f64; 6]) -> [f64; 6] {
|
||||
crate::rotation::rotate(&inverse3(f).unwrap().0, tau)
|
||||
}
|
||||
|
||||
/// The tangent's consistency: dS = C : dE with C the pull-back of c, checked by central
|
||||
/// differences of S along several directions dF (dE = sym(Fᵀ dF), engineering shears).
|
||||
fn check_tangent(m: &MaterialModel, f: &Mat3) {
|
||||
let r = m.response(f, &[], &mut []).unwrap();
|
||||
let (inv, _) = inverse3(f).unwrap();
|
||||
let back = sandwich(&push_forward(&inv), &r.tangent);
|
||||
let scale = back.iter().flatten().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let h = 1e-6;
|
||||
for k in 0..9 {
|
||||
let mut df = [[0.0; 3]; 3];
|
||||
df[k / 3][k % 3] = 1.0;
|
||||
df[(k + 1) % 3][(k + 2) % 3] += 0.3;
|
||||
let at = |s: f64| {
|
||||
let fp: Mat3 = std::array::from_fn(|i| std::array::from_fn(|j| f[i][j] + s * df[i][j]));
|
||||
pull_back_stress(&fp, &m.response(&fp, &[], &mut []).unwrap().stress)
|
||||
};
|
||||
let (sp, sm) = (at(h), at(-h));
|
||||
let ftdf = mul(&transpose(f), &df);
|
||||
let de = [ftdf[0][0], ftdf[1][1], ftdf[2][2], ftdf[0][1] + ftdf[1][0], ftdf[0][2] + ftdf[2][0], ftdf[1][2] + ftdf[2][1]];
|
||||
for a in 0..6 {
|
||||
let numeric = (sp[a] - sm[a]) / (2.0 * h);
|
||||
let analytic: f64 = (0..6).map(|b| back[a][b] * de[b]).sum();
|
||||
assert!((numeric - analytic).abs() < 1e-6 * scale, "{} {k} {a}: {numeric} vs {analytic}", m.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangents_are_consistent_with_the_stress() {
|
||||
let d = isotropic(1000.0, 0.3).unwrap();
|
||||
for m in [MaterialModel::StVenantKirchhoff(Box::new(d)), MaterialModel::NeoHookean { shear: 300.0, bulk: 5000.0 }] {
|
||||
check_tangent(&m, &deformation());
|
||||
check_tangent(&m, &IDENTITY);
|
||||
// Symmetric (hyperelastic): major symmetry of c.
|
||||
let c = m.response(&deformation(), &[], &mut []).unwrap().tangent;
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
assert!((c[a][b] - c[b][a]).abs() < 1e-9 * 5000.0, "{a} {b}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let ortho = crate::material::orthotropic([1000.0, 300.0, 500.0], [0.3, 0.2, 0.25], [100.0, 120.0, 90.0]).unwrap();
|
||||
check_tangent(&MaterialModel::StVenantKirchhoff(Box::new(ortho)), &deformation());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_strain_limit_is_linear_elasticity_and_rotations_do_not_stress() {
|
||||
let (e, nu) = (1000.0, 0.3);
|
||||
let d = isotropic(e, nu).unwrap();
|
||||
let neo = MaterialModel::from_model(&Elastic::Isotropic { young: e, poisson: nu }, Some(&Hyperelastic::NeoHookean {})).unwrap();
|
||||
for m in [MaterialModel::StVenantKirchhoff(Box::new(d)), neo] {
|
||||
let c = m.response(&IDENTITY, &[], &mut []).unwrap().tangent;
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
assert!((c[a][b] - d[a][b]).abs() < 1e-9 * e, "{}: {a} {b}", m.name());
|
||||
}
|
||||
}
|
||||
// A rigid rotation (90° about an inclined axis) leaves the stress at zero.
|
||||
let (s, cth) = (1.0f64, 0.0f64);
|
||||
let axis = [1.0 / 3f64.sqrt(); 3];
|
||||
let k = crate::rotation::skew(&axis);
|
||||
let k2 = mul(&k, &k);
|
||||
let rot: Mat3 = std::array::from_fn(|i| std::array::from_fn(|j| IDENTITY[i][j] + s * k[i][j] + (1.0 - cth) * k2[i][j]));
|
||||
let tau = m.response(&rot, &[], &mut []).unwrap().stress;
|
||||
assert!(tau.iter().all(|v| v.abs() < 1e-12 * e), "{}: {tau:?}", m.name());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uniaxial_stretch_of_neo_hookean_and_log_strain() {
|
||||
// Stretch λ along x with the lateral stretch μ chosen so that σ_yy = 0: check σ_xx
|
||||
// against W directly, σ = (μ/J^(5/3)) (λ² − (λ² + 2 l²)/3) + κ (J − 1).
|
||||
let (mu, kappa) = (300.0, 5000.0);
|
||||
let m = MaterialModel::NeoHookean { shear: mu, bulk: kappa };
|
||||
let (lam, l) = (1.5f64, 0.85f64);
|
||||
let f = [[lam, 0.0, 0.0], [0.0, l, 0.0], [0.0, 0.0, l]];
|
||||
let j = lam * l * l;
|
||||
let tau = m.response(&f, &[], &mut []).unwrap().stress;
|
||||
let i1 = lam * lam + 2.0 * l * l;
|
||||
let sxx = mu / j.powf(5.0 / 3.0) * (lam * lam - i1 / 3.0) + kappa * (j - 1.0);
|
||||
let syy = mu / j.powf(5.0 / 3.0) * (l * l - i1 / 3.0) + kappa * (j - 1.0);
|
||||
assert!((tau[0] / j - sxx).abs() < 1e-10 * mu && (tau[1] / j - syy).abs() < 1e-10 * mu, "{tau:?}");
|
||||
let e = logarithmic_strain(&f).unwrap();
|
||||
assert!((e[0] - lam.ln()).abs() < 1e-14 && (e[1] - l.ln()).abs() < 1e-14 && e[3].abs() < 1e-14, "{e:?}");
|
||||
assert!(m.response(&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]], &[], &mut []).is_err(), "inverted");
|
||||
assert!(MaterialModel::StVenantKirchhoff(Box::new(isotropic(1.0, 0.3).unwrap())).isochoric(&IDENTITY).is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user