forked from 1b-agent/fieldcraft-upstream
Explicit X2: large deformation, J2 and Johnson-Cook plasticity, damage and erosion (M10)
- 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
This commit is contained in:
1 parent
4c0fcc80ad
commit
50764def18
38 files changed
+2839
-254
No files matched your search
@@ -0,0 +1,484 @@
|
||||
//! Hypoelastic–plastic stress update of an isotropic material: von Mises (J2) plasticity with
|
||||
//! isotropic, linear kinematic or Johnson–Cook hardening, integrated by radial return, and
|
||||
//! Johnson–Cook damage.
|
||||
//!
|
||||
//! **Radial return** (J. C. Simo, T. J. R. Hughes, *Computational Inelasticity*, Springer 1998,
|
||||
//! §3.3, Box 3.1 and 3.2; R. D. Krieg, D. B. Krieg, J. Press. Vessel Technol. 99(4):510–515,
|
||||
//! 1977, for its accuracy): with shear modulus G and bulk modulus K, the pressure takes the
|
||||
//! volumetric increment elastically, p ← p + K Δθ, and the deviator the elastic trial
|
||||
//! s* = s + 2G Δe. With the back stress α (kinematic hardening), ξ* = s* − α and the trial
|
||||
//! von Mises stress q* = √(3/2)‖ξ*‖. If q* exceeds the current yield stress, the equivalent
|
||||
//! plastic strain increment Δε_p solves the consistency condition
|
||||
//! q* − 3G Δε_p − H_k Δε_p = σ_y(ε_p + Δε_p, Δε_p / Δt, T), and with the flow direction
|
||||
//! n = ξ*/‖ξ*‖: s = s* − 2G √(3/2) Δε_p n, α ← α + √(2/3) H_k Δε_p n (Prager's linear kinematic
|
||||
//! rule; H_k the kinematic modulus, so the uniaxial stress rises by H_k Δε_p either way).
|
||||
//! - linear hardening: Δε_p = (q* − σ_y)/(3G + H) in closed form;
|
||||
//! - a hardening table: piecewise linear, solved exactly segment by segment;
|
||||
//! - Johnson–Cook (rate and temperature dependent): a bracketed scalar Newton iteration with
|
||||
//! bisection safeguard on [0, (q* − σ_y)/(3G)] (the root is in it because σ_y only grows with
|
||||
//! Δε_p), capped at [`MAX_ITERATIONS`].
|
||||
//!
|
||||
//! **Johnson–Cook** flow stress σ_y = (A + B ε_pⁿ)(1 + C ln ε̇*)(1 − T*ᵐ) (G. R. Johnson,
|
||||
//! W. H. Cook, Proc. 7th Int. Symp. Ballistics, 1983) with ε̇* = ε̇_p / ε̇₀ taken as at least 1
|
||||
//! (the logarithm stays ≥ 0, which also removes its singularity at rest) and
|
||||
//! T* = (T − T_room)/(T_melt − T_room) clamped to [0, 1]. The temperature is that at the start
|
||||
//! of the increment; plastic work heats the material adiabatically, ΔT = χ q Δε_p / (ρ c_p).
|
||||
//!
|
||||
//! **Johnson–Cook damage** (G. R. Johnson, W. H. Cook, Eng. Fract. Mech. 21(1):31–48, 1985):
|
||||
//! D ← D + Δε_p / ε_f with ε_f = (D1 + D2 e^{D3 σ*})(1 + D4 ln ε̇*)(1 + D5 T*), σ* the mean
|
||||
//! stress over the von Mises stress after the increment. A fracture strain below
|
||||
//! [`MIN_FRACTURE_STRAIN`] counts as that (some published sets give negative values at high
|
||||
//! triaxiality, where the point fails at once). The point has failed at D ≥ 1.
|
||||
//!
|
||||
//! The strain increment the update takes is the **mechanical** one: a caller with initial or
|
||||
//! thermal strains subtracts their increment first.
|
||||
//!
|
||||
//! Stress 6-vectors hold tensor components (xx, yy, zz, xy, xz, yz), strain increments
|
||||
//! engineering shears.
|
||||
|
||||
use modelcraft_model::{Damage, Elastic, Hardening, Plastic};
|
||||
|
||||
use crate::material::von_mises;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Most iterations of the rate-dependent return mapping.
|
||||
pub const MAX_ITERATIONS: u32 = 100;
|
||||
|
||||
/// The smallest Johnson–Cook fracture strain.
|
||||
pub const MIN_FRACTURE_STRAIN: f64 = 1e-6;
|
||||
|
||||
/// The flow stress of a plastic material.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Flow {
|
||||
/// σ_y = y0 + h ε_p (isotropic) or a surface of radius y0 moving with modulus h (kinematic).
|
||||
Linear {
|
||||
y0: f64,
|
||||
h: f64,
|
||||
kinematic: bool,
|
||||
},
|
||||
/// Isotropic hardening by (plastic strain, yield stress) points, constant after the last.
|
||||
Table(Vec<(f64, f64)>),
|
||||
JohnsonCook {
|
||||
a: f64,
|
||||
b: f64,
|
||||
n: f64,
|
||||
c: f64,
|
||||
m: f64,
|
||||
rate0: f64,
|
||||
room: f64,
|
||||
melt: f64,
|
||||
},
|
||||
}
|
||||
|
||||
/// A hypoelastic, possibly plastic, isotropic material.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct J2 {
|
||||
pub shear: f64,
|
||||
pub bulk: f64,
|
||||
pub flow: Option<Flow>,
|
||||
/// Johnson–Cook damage constants D1–D5.
|
||||
pub damage: Option<[f64; 5]>,
|
||||
/// χ / c_p: the temperature rise per plastic work per mass (0 without heating).
|
||||
pub heating: f64,
|
||||
}
|
||||
|
||||
/// The state of a material point.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct PointState {
|
||||
pub stress: [f64; 6],
|
||||
pub back: [f64; 6],
|
||||
pub plastic_strain: f64,
|
||||
pub temperature: f64,
|
||||
pub damage: f64,
|
||||
}
|
||||
|
||||
impl PointState {
|
||||
pub fn new(temperature: f64) -> PointState {
|
||||
PointState { stress: [0.0; 6], back: [0.0; 6], plastic_strain: 0.0, temperature, damage: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// What one update did.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct Increment {
|
||||
/// The equivalent plastic strain increment.
|
||||
pub plastic_strain: f64,
|
||||
/// Plastic work per volume, q Δε_p.
|
||||
pub plastic_work: f64,
|
||||
}
|
||||
|
||||
/// ‖s‖² of a symmetric tensor's 6-vector (tensor components).
|
||||
fn norm2(s: &[f64; 6]) -> f64 {
|
||||
s[0] * s[0] + s[1] * s[1] + s[2] * s[2] + 2.0 * (s[3] * s[3] + s[4] * s[4] + s[5] * s[5])
|
||||
}
|
||||
|
||||
impl Flow {
|
||||
/// The flow stress at plastic strain `e`, plastic strain rate `rate` and temperature `t`, and
|
||||
/// its derivative with respect to the plastic strain increment over an increment `dt` (for
|
||||
/// the Newton iteration).
|
||||
fn stress(&self, e: f64, rate: f64, t: f64, dt: f64) -> (f64, f64) {
|
||||
match *self {
|
||||
Flow::Linear { y0, h, kinematic } => {
|
||||
if kinematic {
|
||||
(y0, 0.0)
|
||||
} else {
|
||||
(y0 + h * e, h)
|
||||
}
|
||||
}
|
||||
Flow::Table(ref points) => table(points, e),
|
||||
Flow::JohnsonCook { a, b, n, c, m, rate0, room, melt } => {
|
||||
let hardening = a + b * e.max(0.0).powf(n);
|
||||
let d_hardening = if e > 0.0 {
|
||||
b * n * e.powf(n - 1.0)
|
||||
} else if n < 1.0 && b > 0.0 {
|
||||
f64::INFINITY
|
||||
} else {
|
||||
b * n
|
||||
};
|
||||
let ratio = rate / rate0;
|
||||
let (rate_term, d_rate) = if ratio > 1.0 && dt > 0.0 { (1.0 + c * ratio.ln(), c / (rate * dt)) } else { (1.0, 0.0) };
|
||||
let homologous = ((t - room) / (melt - room)).clamp(0.0, 1.0);
|
||||
let thermal = if homologous > 0.0 { 1.0 - homologous.powf(m) } else { 1.0 };
|
||||
(hardening * rate_term * thermal, (d_hardening * rate_term + hardening * d_rate) * thermal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kinematic hardening modulus.
|
||||
fn kinematic(&self) -> f64 {
|
||||
match *self {
|
||||
Flow::Linear { h, kinematic: true, .. } => h,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The rate sensitivity's reference rate and the homologous temperature (for damage).
|
||||
fn rate_and_temperature(&self, t: f64) -> (f64, f64) {
|
||||
match *self {
|
||||
Flow::JohnsonCook { rate0, room, melt, .. } => (rate0, ((t - room) / (melt - room)).clamp(0.0, 1.0)),
|
||||
_ => (1.0, 0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The yield stress of a hardening table at plastic strain `e`, and its slope.
|
||||
fn table(points: &[(f64, f64)], e: f64) -> (f64, f64) {
|
||||
for w in points.windows(2) {
|
||||
if let [(e0, s0), (e1, s1)] = *w
|
||||
&& e < e1
|
||||
{
|
||||
let slope = (s1 - s0) / (e1 - e0);
|
||||
return (s0 + slope * (e - e0).max(0.0), slope);
|
||||
}
|
||||
}
|
||||
(points.last().map_or(0.0, |p| p.1), 0.0)
|
||||
}
|
||||
|
||||
impl J2 {
|
||||
/// The material of a model's isotropic elastic constants with its plasticity and damage.
|
||||
pub fn from_model(elastic: &Elastic, plastic: Option<&Plastic>, damage: Option<&Damage>) -> Result<J2> {
|
||||
let Elastic::Isotropic { young, poisson } = *elastic else {
|
||||
return Err(Error::NotSupported("plasticity of an orthotropic material".into()));
|
||||
};
|
||||
let shear = young / (2.0 * (1.0 + poisson));
|
||||
let bulk = young / (3.0 * (1.0 - 2.0 * poisson));
|
||||
let mut heating = 0.0;
|
||||
let flow = plastic.map(|p| match p {
|
||||
Plastic::Bilinear { yield_stress, hardening_modulus, hardening } => {
|
||||
Flow::Linear { y0: *yield_stress, h: *hardening_modulus, kinematic: *hardening == Hardening::Kinematic }
|
||||
}
|
||||
Plastic::Table { points } => Flow::Table(points.iter().map(|p| (p.strain, p.stress)).collect()),
|
||||
Plastic::JohnsonCook { a, b, n, c, m, reference_strain_rate, room_temperature, melt_temperature, specific_heat, taylor_quinney } => {
|
||||
heating = specific_heat.map_or(0.0, |cp| taylor_quinney / cp);
|
||||
Flow::JohnsonCook {
|
||||
a: *a,
|
||||
b: *b,
|
||||
n: *n,
|
||||
c: *c,
|
||||
m: *m,
|
||||
rate0: *reference_strain_rate,
|
||||
room: *room_temperature,
|
||||
melt: *melt_temperature,
|
||||
}
|
||||
}
|
||||
});
|
||||
let damage = damage.map(|Damage::JohnsonCook { d1, d2, d3, d4, d5 }| [*d1, *d2, *d3, *d4, *d5]);
|
||||
if !(shear > 0.0 && bulk > 0.0 && shear.is_finite() && bulk.is_finite()) {
|
||||
return Err(Error::Invalid(format!("E = {young}, ν = {poisson} are not a stable material")));
|
||||
}
|
||||
Ok(J2 { shear, bulk, flow, damage, heating })
|
||||
}
|
||||
|
||||
/// The temperature a point starts at.
|
||||
pub fn initial_temperature(&self) -> f64 {
|
||||
match self.flow {
|
||||
Some(Flow::JohnsonCook { room, .. }) => room,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update `state` over an increment of length `dt` with the deviatoric strain increment
|
||||
/// `dev` (engineering shears; its trace is ignored) and the volumetric strain increment
|
||||
/// `vol`, at the current density `density` (for heating).
|
||||
pub fn update(&self, state: &mut PointState, dev: &[f64; 6], vol: f64, dt: f64, density: f64) -> Increment {
|
||||
let s = &mut state.stress;
|
||||
let mean = (s[0] + s[1] + s[2]) / 3.0;
|
||||
let tr = (dev[0] + dev[1] + dev[2]) / 3.0;
|
||||
let g2 = 2.0 * self.shear;
|
||||
// Trial deviator.
|
||||
let mut trial = [s[0] - mean + g2 * (dev[0] - tr), s[1] - mean + g2 * (dev[1] - tr), s[2] - mean + g2 * (dev[2] - tr), 0.0, 0.0, 0.0];
|
||||
for k in 3..6 {
|
||||
trial[k] = s[k] + self.shear * dev[k];
|
||||
}
|
||||
let pressure = mean + self.bulk * vol;
|
||||
let mut out = Increment::default();
|
||||
if let Some(flow) = &self.flow {
|
||||
let xi: [f64; 6] = std::array::from_fn(|k| trial[k] - state.back[k]);
|
||||
let norm = norm2(&xi).sqrt();
|
||||
let q = (1.5f64).sqrt() * norm;
|
||||
let (yield_now, _) = flow.stress(state.plastic_strain, 0.0, state.temperature, dt);
|
||||
if q > yield_now && norm > 0.0 {
|
||||
let hk = flow.kinematic();
|
||||
let de = self.plastic_increment(flow, state, q, yield_now, hk, dt);
|
||||
let n: [f64; 6] = xi.map(|x| x / norm);
|
||||
let radial = g2 * (1.5f64).sqrt() * de;
|
||||
let shift = (2.0f64 / 3.0).sqrt() * hk * de;
|
||||
for k in 0..6 {
|
||||
trial[k] -= radial * n[k];
|
||||
state.back[k] += shift * n[k];
|
||||
}
|
||||
state.plastic_strain += de;
|
||||
let q_new = q - (3.0 * self.shear + hk) * de;
|
||||
out = Increment { plastic_strain: de, plastic_work: q_new * de };
|
||||
if self.heating > 0.0 && density > 0.0 {
|
||||
state.temperature += self.heating * out.plastic_work / density;
|
||||
}
|
||||
}
|
||||
}
|
||||
for k in 0..3 {
|
||||
trial[k] += pressure;
|
||||
}
|
||||
state.stress = trial;
|
||||
if let (Some(d), Some(flow)) = (self.damage, &self.flow)
|
||||
&& out.plastic_strain > 0.0
|
||||
{
|
||||
let q = von_mises(&state.stress);
|
||||
let triaxiality = if q > 0.0 { pressure / q } else { 0.0 };
|
||||
let (rate0, homologous) = flow.rate_and_temperature(state.temperature);
|
||||
let rate = if dt > 0.0 { out.plastic_strain / dt / rate0 } else { 0.0 };
|
||||
let log_rate = if rate > 1.0 { rate.ln() } else { 0.0 };
|
||||
let fracture = (d[0] + d[1] * (d[2] * triaxiality).exp()) * (1.0 + d[3] * log_rate) * (1.0 + d[4] * homologous);
|
||||
state.damage += out.plastic_strain / fracture.max(MIN_FRACTURE_STRAIN);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The plastic strain increment of a trial state that yields.
|
||||
fn plastic_increment(&self, flow: &Flow, state: &PointState, q: f64, yield_now: f64, hk: f64, dt: f64) -> f64 {
|
||||
let g3 = 3.0 * self.shear;
|
||||
let (e0, t) = (state.plastic_strain, state.temperature);
|
||||
match flow {
|
||||
Flow::Linear { h, kinematic, .. } => (q - yield_now) / (g3 + if *kinematic { hk } else { *h }),
|
||||
Flow::Table(points) => {
|
||||
// Walk the segments from the current one: on a segment of slope h starting at
|
||||
// (e_a, s_a), q − 3G Δ = s_a + h (e0 + Δ − e_a).
|
||||
for w in points.windows(2) {
|
||||
let [(ea, sa), (eb, sb)] = *w else { continue };
|
||||
if eb <= e0 {
|
||||
continue;
|
||||
}
|
||||
let h = (sb - sa) / (eb - ea);
|
||||
let at = sa + h * (e0 - ea);
|
||||
let de = (q - at) / (g3 + h);
|
||||
if e0 + de <= eb {
|
||||
return de.max(0.0);
|
||||
}
|
||||
}
|
||||
// Beyond the table: perfectly plastic at the last stress.
|
||||
let last = points.last().map_or(yield_now, |p| p.1);
|
||||
((q - last) / g3).max(0.0)
|
||||
}
|
||||
Flow::JohnsonCook { .. } => {
|
||||
let residual = |de: f64| {
|
||||
let rate = if dt > 0.0 { de / dt } else { 0.0 };
|
||||
let (y, dy) = flow.stress(e0 + de, rate, t, dt);
|
||||
(q - g3 * de - y, -g3 - dy)
|
||||
};
|
||||
let (mut lo, mut hi) = (0.0, ((q - yield_now) / g3).max(0.0));
|
||||
let mut de = 0.5 * hi;
|
||||
for _ in 0..MAX_ITERATIONS {
|
||||
let (r, dr) = residual(de);
|
||||
if r > 0.0 {
|
||||
lo = de;
|
||||
} else {
|
||||
hi = de;
|
||||
}
|
||||
if r.abs() <= 1e-13 * q || hi - lo <= 1e-15 * hi {
|
||||
break;
|
||||
}
|
||||
let newton = de - r / dr;
|
||||
de = if newton.is_finite() && newton > lo && newton < hi { newton } else { 0.5 * (lo + hi) };
|
||||
}
|
||||
de
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use modelcraft_model::HardeningPoint;
|
||||
|
||||
const E: f64 = 200_000.0;
|
||||
const NU: f64 = 0.3;
|
||||
|
||||
fn material(plastic: Plastic) -> J2 {
|
||||
J2::from_model(&Elastic::Isotropic { young: E, poisson: NU }, Some(&plastic), None).unwrap()
|
||||
}
|
||||
|
||||
/// Uniaxial stress at a constant strain rate: the axial strain increments by `de` while the
|
||||
/// lateral strain increment is found by a secant iteration on the point update itself so
|
||||
/// the lateral stresses vanish. Returns (axial strain, axial stress, plastic strain) per step.
|
||||
fn uniaxial(m: &J2, steps: &[f64], dt: f64) -> Vec<(f64, f64, f64)> {
|
||||
let mut s = PointState::new(m.initial_temperature());
|
||||
let mut out = Vec::new();
|
||||
let mut strain = 0.0;
|
||||
for de in steps {
|
||||
let run = |lateral: f64| {
|
||||
let mut t = s;
|
||||
let d = [*de, lateral * de, lateral * de, 0.0, 0.0, 0.0];
|
||||
m.update(&mut t, &d, d[0] + d[1] + d[2], dt, 7.8e-9);
|
||||
t
|
||||
};
|
||||
let (mut a, mut b) = (-NU, -0.5);
|
||||
let (mut fa, mut fb) = (run(a).stress[1], run(b).stress[1]);
|
||||
for _ in 0..100 {
|
||||
if fb == fa || fb.abs() < 1e-12 {
|
||||
break;
|
||||
}
|
||||
let c = b - fb * (b - a) / (fb - fa);
|
||||
(a, fa) = (b, fb);
|
||||
b = c;
|
||||
fb = run(b).stress[1];
|
||||
}
|
||||
s = run(b);
|
||||
strain += de;
|
||||
out.push((strain, s.stress[0], s.plastic_strain));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bilinear_uniaxial_curve_and_unloading() {
|
||||
let (y0, h) = (250.0, 2000.0);
|
||||
for kinematic in [false, true] {
|
||||
let hardening = if kinematic { Hardening::Kinematic } else { Hardening::Isotropic };
|
||||
let m = material(Plastic::Bilinear { yield_stress: y0, hardening_modulus: h, hardening });
|
||||
// Load to 1% strain, unload to −1%.
|
||||
let mut steps = vec![1e-4; 100];
|
||||
steps.extend(vec![-1e-4; 200]);
|
||||
let curve = uniaxial(&m, &steps, 1e-3);
|
||||
// Loading: σ = E ε below yield, then σ = y0 + H ε_p with ε = σ/E + ε_p, that is
|
||||
// σ = y0 + E_t (ε − y0/E), E_t = E H/(E + H).
|
||||
let et = E * h / (E + h);
|
||||
let (e1, s1, p1) = curve[99];
|
||||
let exact = y0 + et * (e1 - y0 / E);
|
||||
assert!((s1 - exact).abs() < 1e-9 * exact, "{kinematic}: {s1} vs {exact}");
|
||||
assert!((p1 - (s1 - y0) / h).abs() < 1e-12, "{p1}");
|
||||
// Unloading is elastic until the reversed yield: isotropic at −σ1, kinematic at
|
||||
// σ1 − 2 y0 (the surface keeps its size and moves).
|
||||
let reverse = if kinematic { s1 - 2.0 * y0 } else { -s1 };
|
||||
for (e, s, _) in &curve[100..] {
|
||||
let elastic = s1 - E * (e1 - e);
|
||||
if elastic > reverse + 1.0 {
|
||||
assert!((s - elastic).abs() < 1e-8 * s1, "{kinematic}: {s} vs elastic {elastic}");
|
||||
}
|
||||
}
|
||||
let (_, s_end, _) = curve[299];
|
||||
assert!(s_end < reverse, "{kinematic}: {s_end} vs {reverse}");
|
||||
eprintln!("bilinear kinematic = {kinematic}: σ(1%) = {s1:.6} (exact {exact:.6}); reversed yield at {reverse:.3}; σ(−1%) = {s_end:.4}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_and_johnson_cook_follow_their_flow_curves() {
|
||||
let points = vec![
|
||||
HardeningPoint { strain: 0.0, stress: 200.0 },
|
||||
HardeningPoint { strain: 0.01, stress: 300.0 },
|
||||
HardeningPoint { strain: 0.05, stress: 340.0 },
|
||||
];
|
||||
let m = material(Plastic::Table { points });
|
||||
let curve = uniaxial(&m, &vec![2e-4; 400], 1e-3);
|
||||
for (_, s, p) in &curve {
|
||||
if *p > 0.0 {
|
||||
let want = table(&[(0.0, 200.0), (0.01, 300.0), (0.05, 340.0)], *p).0;
|
||||
assert!((s - want).abs() < 1e-8 * want, "{s} vs {want} at {p}");
|
||||
}
|
||||
}
|
||||
assert!(curve.last().unwrap().2 > 0.05, "past the table's end");
|
||||
// Johnson–Cook at a constant plastic rate: the stress is the flow stress at that rate.
|
||||
let (a, b, n, c) = (350.0, 275.0, 0.36, 0.022);
|
||||
let jc = Plastic::JohnsonCook {
|
||||
a,
|
||||
b,
|
||||
n,
|
||||
c,
|
||||
m: 1.0,
|
||||
reference_strain_rate: 1.0,
|
||||
room_temperature: 293.0,
|
||||
melt_temperature: 1356.0,
|
||||
specific_heat: None,
|
||||
taylor_quinney: 0.9,
|
||||
};
|
||||
let m = material(jc);
|
||||
let dt = 1e-6;
|
||||
let curve = uniaxial(&m, &vec![1e-3; 200], dt);
|
||||
let mut worst: f64 = 0.0;
|
||||
for w in curve.windows(2).skip(20) {
|
||||
let (_, s, p) = w[1];
|
||||
let rate = (p - w[0].2) / dt;
|
||||
let want = (a + b * p.powf(n)) * (1.0 + c * rate.ln());
|
||||
worst = worst.max((s - want).abs() / want);
|
||||
}
|
||||
eprintln!("Johnson–Cook uniaxial: largest relative error of the flow stress {worst:.2e}");
|
||||
assert!(worst < 1e-9, "{worst}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heating_softens_and_damage_accumulates() {
|
||||
let jc = |cp: Option<f64>| Plastic::JohnsonCook {
|
||||
a: 350.0,
|
||||
b: 275.0,
|
||||
n: 0.36,
|
||||
c: 0.0,
|
||||
m: 1.0,
|
||||
reference_strain_rate: 1.0,
|
||||
room_temperature: 293.0,
|
||||
melt_temperature: 1356.0,
|
||||
specific_heat: cp,
|
||||
taylor_quinney: 0.9,
|
||||
};
|
||||
let el = Elastic::Isotropic { young: E, poisson: NU };
|
||||
let damage = Damage::JohnsonCook { d1: 0.5, d2: 0.0, d3: 0.0, d4: 0.0, d5: 0.0 };
|
||||
let cold = J2::from_model(&el, Some(&jc(None)), Some(&damage)).unwrap();
|
||||
let hot = J2::from_model(&el, Some(&jc(Some(3.83e8))), Some(&damage)).unwrap();
|
||||
let a = uniaxial(&cold, &vec![1e-3; 300], 1e-3);
|
||||
let b = uniaxial(&hot, &vec![1e-3; 300], 1e-3);
|
||||
assert!(b.last().unwrap().1 < a.last().unwrap().1);
|
||||
// Constant fracture strain 0.5: D = ε_p / 0.5.
|
||||
let mut s = PointState::new(293.0);
|
||||
for _ in 0..300 {
|
||||
cold.update(&mut s, &[1e-3, -5e-4, -5e-4, 0.0, 0.0, 0.0], 0.0, 1e-3, 7.8e-9);
|
||||
}
|
||||
assert!((s.damage - s.plastic_strain / 0.5).abs() < 1e-12, "{} vs {}", s.damage, s.plastic_strain);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_increments_stay_finite() {
|
||||
let m = material(Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 0.0, hardening: Hardening::Isotropic });
|
||||
let mut s = PointState::new(0.0);
|
||||
m.update(&mut s, &[1e3, -1e3, 0.0, 5e2, 0.0, 0.0], 0.0, 0.0, 0.0);
|
||||
assert!(s.stress.iter().all(|x| x.is_finite()) && (von_mises(&s.stress) - 250.0).abs() < 1e-9 * 250.0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user