forked from 1b-agent/fieldcraft-upstream
- model: materials gain solver-neutral plastic (bilinear isotropic/kinematic, hardening table, Johnson-Cook with adiabatic heating), damage (Johnson-Cook D1-D5) and erosion (plastic strain); explicit steps gain nonlinear_geometry (default on) and stress_rate (jaumann | green_naghdi). - fem: rotation (Hughes-Winget, Flanagan-Taylor), plasticity (radial return, capped bracketed Newton for Johnson-Cook, damage; the update takes the mechanical strain increment so thermal strains can be subtracted), closed-form hex8 gradients for deformed configurations. - explicit: updated Lagrangian hex8/tet4, per-element material state, erosion with reported energy, plastic work in the energy account. - solver: warns that the linear solver uses the elastic constants of plastic materials (results hash-identical). - decks: Abaqus *PLASTIC (+ Johnson-Cook keywords), Nastran MATS1/TABLES1, LS-DYNA *MAT_PLASTIC_KINEMATIC / *MAT_PIECEWISE_LINEAR_PLASTICITY; what a format cannot express is reported as Unsupported. - engine: model.material.plasticity, model.material.damage (catalogue ready), explicit element probe, eroded elements in the solve summary. Step commits before the rebase onto the DOF framework: branch explicit-x2-steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
264 lines
10 KiB
Rust
264 lines
10 KiB
Rust
//! Incrementally objective stress updates for large deformation: the rotation that carries a
|
||
//! stress (and every other tensor of the material state) from one configuration to the next.
|
||
//!
|
||
//! Increments are given as the gradient of the displacement increment with respect to the
|
||
//! **midpoint configuration**, ΔG = ∂Δu/∂x_{n+½} (= Δt L at the midpoint). Its symmetric part is
|
||
//! the strain increment ΔD and its skew part the spin increment ΔW.
|
||
//!
|
||
//! **Jaumann rate, Hughes–Winget integration** (T. J. R. Hughes, J. Winget, *Finite rotation
|
||
//! effects in numerical integration of rate constitutive equations arising in large-deformation
|
||
//! analysis*, IJNME 15(12):1862–1867, 1980): the stress is rotated with
|
||
//! Q = (I − ½ΔW)⁻¹(I + ½ΔW) and the constitutive increment added: σ_{n+1} = Q σ_n Qᵀ + Δσ(ΔD).
|
||
//! Because ΔG is taken on the midpoint configuration, a rigid rotation x_{n+1} = R x_n gives
|
||
//! ΔG = 2(R − I)(R + I)⁻¹, which is skew (ΔD = 0), and its Cayley transform Q is R itself: a
|
||
//! rigid rotation of any size rotates the stress exactly and strains nothing.
|
||
//!
|
||
//! **Green–Naghdi rate, Flanagan–Taylor integration** (D. P. Flanagan, L. M. Taylor, *An
|
||
//! accurate numerical algorithm for stress integration with finite rotations*, CMAME
|
||
//! 62(3):305–320, 1987): the rotation R of the polar decomposition F = V R is integrated
|
||
//! incrementally instead of decomposing F. From V̇ = L V − V Ω with Ω = Ṙ Rᵀ and V symmetric,
|
||
//! Ω = W + X where the skew X solves V X + X V = D V − V D, that is, with axial vectors,
|
||
//! [(tr V) I − V] x = axial(D V − V D). R is advanced with the Cayley transform of Ω Δt, V with
|
||
//! V̇, and the material is updated in the unrotated frame: σ̂ = Rᵀ σ R, d̂ = Rᵀ D R. A material
|
||
//! whose axes are those of the undeformed body (orthotropic elasticity) is updated correctly in
|
||
//! that frame.
|
||
//!
|
||
//! Tensors are symmetric 6-vectors in the order xx, yy, zz, xy, xz, yz; strain-like vectors carry
|
||
//! engineering shears (γ = 2ε), stress-like ones tensor components.
|
||
|
||
use crate::dense::inverse3;
|
||
|
||
/// A 3 × 3 matrix, row-major.
|
||
pub type Mat3 = [[f64; 3]; 3];
|
||
|
||
pub const IDENTITY: Mat3 = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||
|
||
pub fn mul(a: &Mat3, b: &Mat3) -> Mat3 {
|
||
let mut c = [[0.0; 3]; 3];
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
|
||
}
|
||
}
|
||
c
|
||
}
|
||
|
||
pub fn transpose(a: &Mat3) -> Mat3 {
|
||
[[a[0][0], a[1][0], a[2][0]], [a[0][1], a[1][1], a[2][1]], [a[0][2], a[1][2], a[2][2]]]
|
||
}
|
||
|
||
/// A symmetric tensor from its 6-vector of tensor components.
|
||
pub fn tensor(s: &[f64; 6]) -> Mat3 {
|
||
[[s[0], s[3], s[4]], [s[3], s[1], s[5]], [s[4], s[5], s[2]]]
|
||
}
|
||
|
||
/// The 6-vector of tensor components of (the symmetric part of) a matrix.
|
||
pub fn voigt(a: &Mat3) -> [f64; 6] {
|
||
[a[0][0], a[1][1], a[2][2], 0.5 * (a[0][1] + a[1][0]), 0.5 * (a[0][2] + a[2][0]), 0.5 * (a[1][2] + a[2][1])]
|
||
}
|
||
|
||
/// Q s Qᵀ for a stress-like 6-vector.
|
||
pub fn rotate(q: &Mat3, s: &[f64; 6]) -> [f64; 6] {
|
||
voigt(&mul(&mul(q, &tensor(s)), &transpose(q)))
|
||
}
|
||
|
||
/// Q e Qᵀ for a strain-like 6-vector (engineering shears).
|
||
pub fn rotate_strain(q: &Mat3, e: &[f64; 6]) -> [f64; 6] {
|
||
let half = [e[0], e[1], e[2], 0.5 * e[3], 0.5 * e[4], 0.5 * e[5]];
|
||
let r = rotate(q, &half);
|
||
[r[0], r[1], r[2], 2.0 * r[3], 2.0 * r[4], 2.0 * r[5]]
|
||
}
|
||
|
||
/// Q v for a vector.
|
||
pub fn apply(q: &Mat3, v: &[f64; 3]) -> [f64; 3] {
|
||
[0, 1, 2].map(|i| q[i][0] * v[0] + q[i][1] * v[1] + q[i][2] * v[2])
|
||
}
|
||
|
||
/// The strain increment ΔD (engineering shears) and the spin increment ΔW of a displacement
|
||
/// increment gradient.
|
||
pub fn split(g: &Mat3) -> ([f64; 6], Mat3) {
|
||
let d = [g[0][0], g[1][1], g[2][2], g[0][1] + g[1][0], g[0][2] + g[2][0], g[1][2] + g[2][1]];
|
||
let mut w = [[0.0; 3]; 3];
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
w[i][j] = 0.5 * (g[i][j] - g[j][i]);
|
||
}
|
||
}
|
||
(d, w)
|
||
}
|
||
|
||
/// The Cayley transform (I − ½A)⁻¹(I + ½A) of a skew matrix: an exact rotation. `None` only
|
||
/// for non-finite input (I − ½A is invertible for every real skew A).
|
||
pub fn cayley(a: &Mat3) -> Option<Mat3> {
|
||
let mut minus = IDENTITY;
|
||
let mut plus = IDENTITY;
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
minus[i][j] -= 0.5 * a[i][j];
|
||
plus[i][j] += 0.5 * a[i][j];
|
||
}
|
||
}
|
||
let (inv, _) = inverse3(&minus)?;
|
||
let q = mul(&inv, &plus);
|
||
q.iter().flatten().all(|v| v.is_finite()).then_some(q)
|
||
}
|
||
|
||
/// The axial vector a of a skew matrix A (A v = a × v).
|
||
pub fn axial(a: &Mat3) -> [f64; 3] {
|
||
[0.5 * (a[2][1] - a[1][2]), 0.5 * (a[0][2] - a[2][0]), 0.5 * (a[1][0] - a[0][1])]
|
||
}
|
||
|
||
/// The skew matrix of an axial vector.
|
||
pub fn skew(a: &[f64; 3]) -> Mat3 {
|
||
[[0.0, -a[2], a[1]], [a[2], 0.0, -a[0]], [-a[1], a[0], 0.0]]
|
||
}
|
||
|
||
/// The polar rotation R and left stretch V (a 6-vector of tensor components) of the
|
||
/// Flanagan–Taylor integration.
|
||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||
pub struct Polar {
|
||
pub rotation: Mat3,
|
||
pub stretch: [f64; 6],
|
||
}
|
||
|
||
impl Default for Polar {
|
||
fn default() -> Polar {
|
||
Polar { rotation: IDENTITY, stretch: [1.0, 1.0, 1.0, 0.0, 0.0, 0.0] }
|
||
}
|
||
}
|
||
|
||
impl Polar {
|
||
/// Advance R and V over an increment with displacement increment gradient `g` (on the
|
||
/// midpoint configuration). V̇ = L V − V Ω is integrated with the midpoint rule (a predictor
|
||
/// for V at the midpoint, then Ω and the increment from it), so V and R are second-order
|
||
/// accurate in the increment. Returns the increment's rotation (R_{n+1} = q R_n); `None`
|
||
/// when the increment is not finite or V degenerates.
|
||
pub fn advance(&mut self, g: &Mat3) -> Option<Mat3> {
|
||
let v0 = tensor(&self.stretch);
|
||
let (dv, omega) = Polar::rate(g, &v0)?;
|
||
let mut mid = v0;
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
mid[i][j] += 0.5 * dv[i][j];
|
||
}
|
||
}
|
||
let (dv, omega_mid) = Polar::rate(g, &mid).unwrap_or((dv, omega));
|
||
let mut vn = v0;
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
vn[i][j] += dv[i][j];
|
||
}
|
||
}
|
||
let q = cayley(&omega_mid)?;
|
||
self.rotation = mul(&q, &self.rotation);
|
||
self.stretch = voigt(&vn);
|
||
self.stretch.iter().all(|s| s.is_finite()).then_some(q)
|
||
}
|
||
|
||
/// The increments of V (L V − V Ω, times Δt) and the polar spin Ω Δt at stretch `v`.
|
||
fn rate(g: &Mat3, v: &Mat3) -> Option<(Mat3, Mat3)> {
|
||
let (d6, w) = split(g);
|
||
let d = tensor(&[d6[0], d6[1], d6[2], 0.5 * d6[3], 0.5 * d6[4], 0.5 * d6[5]]);
|
||
let (dv, vd) = (mul(&d, v), mul(v, &d));
|
||
let mut rhs = [[0.0; 3]; 3];
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
rhs[i][j] = dv[i][j] - vd[i][j];
|
||
}
|
||
}
|
||
let trace = v[0][0] + v[1][1] + v[2][2];
|
||
let mut m = v.map(|r| r.map(|x| -x));
|
||
for (i, row) in m.iter_mut().enumerate() {
|
||
row[i] += trace;
|
||
}
|
||
let (inv, _) = inverse3(&m)?;
|
||
let xs = skew(&apply(&inv, &axial(&rhs)));
|
||
let mut omega = w;
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
omega[i][j] += xs[i][j];
|
||
}
|
||
}
|
||
let (lv, vo) = (mul(g, v), mul(v, &omega));
|
||
let mut rate = [[0.0; 3]; 3];
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
rate[i][j] = lv[i][j] - vo[i][j];
|
||
}
|
||
}
|
||
Some((rate, omega))
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn rotation_about(axis: [f64; 3], angle: f64) -> Mat3 {
|
||
let n = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
|
||
let k = skew(&axis.map(|c| c / n));
|
||
let k2 = mul(&k, &k);
|
||
let mut r = IDENTITY;
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
r[i][j] += angle.sin() * k[i][j] + (1.0 - angle.cos()) * k2[i][j];
|
||
}
|
||
}
|
||
r
|
||
}
|
||
|
||
/// The midpoint gradient of the increment x → R x: 2 (R − I)(R + I)⁻¹.
|
||
fn rigid_increment(r: &Mat3) -> Mat3 {
|
||
let mut a = *r;
|
||
let mut b = *r;
|
||
for i in 0..3 {
|
||
a[i][i] -= 1.0;
|
||
b[i][i] += 1.0;
|
||
}
|
||
let (inv, _) = inverse3(&b).unwrap();
|
||
mul(&a, &inv).map(|row| row.map(|v| 2.0 * v))
|
||
}
|
||
|
||
#[test]
|
||
fn a_rigid_rotation_is_exact_with_both_rates() {
|
||
let r = rotation_about([0.3, -0.5, 0.8], 1.1);
|
||
let g = rigid_increment(&r);
|
||
let (d, w) = split(&g);
|
||
assert!(d.iter().all(|x| x.abs() < 1e-15), "{d:?}");
|
||
let q = cayley(&w).unwrap();
|
||
assert!(q.iter().flatten().zip(r.iter().flatten()).all(|(a, b)| (a - b).abs() < 1e-15), "{q:?} vs {r:?}");
|
||
let mut p = Polar::default();
|
||
p.advance(&g).unwrap();
|
||
assert!(p.rotation.iter().flatten().zip(r.iter().flatten()).all(|(a, b)| (a - b).abs() < 1e-14));
|
||
assert!(p.stretch.iter().zip([1.0, 1.0, 1.0, 0.0, 0.0, 0.0]).all(|(a, b)| (a - b).abs() < 1e-14), "{:?}", p.stretch);
|
||
let s = [1.0, 2.0, 3.0, 0.4, 0.5, 0.6];
|
||
let rs = rotate(&q, &s);
|
||
// Invariants: trace and the norm.
|
||
let norm = |s: &[f64; 6]| s[..3].iter().map(|x| x * x).sum::<f64>() + 2.0 * s[3..].iter().map(|x| x * x).sum::<f64>();
|
||
assert!((rs[0] + rs[1] + rs[2] - 6.0).abs() < 1e-14 && (norm(&rs) - norm(&s)).abs() < 1e-13);
|
||
}
|
||
|
||
#[test]
|
||
fn polar_rotation_of_a_stretch_then_rotation() {
|
||
// Stretch along x by 1.5 in many small steps, then rotate 0.7 rad about z in many small
|
||
// steps: V = R diag(1.5, 1, 1) Rᵀ, R = rotation.
|
||
let mut p = Polar::default();
|
||
let n = 2000;
|
||
let step = 1.5f64.powf(1.0 / n as f64);
|
||
for _ in 0..n {
|
||
// x_{n+1} = diag(step) x_n; midpoint gradient 2 (s − 1)/(s + 1).
|
||
let mut g = [[0.0; 3]; 3];
|
||
g[0][0] = 2.0 * (step - 1.0) / (step + 1.0);
|
||
p.advance(&g).unwrap();
|
||
}
|
||
let r = rotation_about([0.0, 0.0, 1.0], 0.7 / n as f64);
|
||
for _ in 0..n {
|
||
p.advance(&rigid_increment(&r)).unwrap();
|
||
}
|
||
let want = rotation_about([0.0, 0.0, 1.0], 0.7);
|
||
let v = rotate(&want, &[1.5, 1.0, 1.0, 0.0, 0.0, 0.0]);
|
||
assert!(p.rotation.iter().flatten().zip(want.iter().flatten()).all(|(a, b)| (a - b).abs() < 1e-9), "{:?}", p.rotation);
|
||
assert!(p.stretch.iter().zip(v).all(|(a, b)| (a - b).abs() < 1e-6), "{:?} vs {v:?}", p.stretch);
|
||
}
|
||
}
|