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:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-13 20:52:57 -04:00
1 parent 4c0fcc80ad
commit 50764def18
38 files changed
+2839 -254

No files matched your search

+60 -6
View File
@@ -15,7 +15,7 @@ use modelcraft_model::{
// Models
fn steel() -> Material {
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9), fluid: None }
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9), ..Material::default() }
}
fn faces(list: &[(u32, u8)]) -> Set {
@@ -91,7 +91,7 @@ fn plate() -> Model {
m.add_set("top", faces(&[(1, 1), (2, 1), (3, 1), (4, 1)])).unwrap();
m.add_set("edge", Set { nodes: vec![1, 4, 7], ..Set::default() }).unwrap();
m.add_set("mid", Set { nodes: vec![2, 5, 8], ..Set::default() }).unwrap();
let aluminium = Material { elastic: Some(Elastic::Isotropic { young: 70_000.0, poisson: 0.33 }), density: Some(2.7e-9), fluid: None };
let aluminium = Material { elastic: Some(Elastic::Isotropic { young: 70_000.0, poisson: 0.33 }), density: Some(2.7e-9), ..Material::default() };
m.add_material("Aluminium", aluminium).unwrap();
let kind = SectionKind::Shell { thickness: 2.0, offset: 0.5, points: 5 };
let formulation = Formulation { integration: Integration::Reduced, ..Formulation::default() };
@@ -523,8 +523,11 @@ fn largest_labels_longest_numbers_and_names_fit_a_line() {
m.set_element(modelcraft_model::MAX_LABEL, ElementKind::Hex20, (0..20).map(|i| base + i).collect()).unwrap();
let long = format!("A{}", "x".repeat(modelcraft_model::MAX_NAME_LEN - 1));
m.add_set(&long, Set { elements: vec![modelcraft_model::MAX_LABEL], nodes: (0..20).map(|i| base + i).collect(), ..Set::default() }).unwrap();
m.add_material(&long, Material { elastic: Some(Elastic::Isotropic { young: ugly.abs(), poisson: 0.3 }), density: Some(1.0), fluid: None })
.unwrap();
m.add_material(
&long,
Material { elastic: Some(Elastic::Isotropic { young: ugly.abs(), poisson: 0.3 }), density: Some(1.0), ..Material::default() },
)
.unwrap();
m.add_section(&long, Section { set: long.clone(), material: long.clone(), kind: SectionKind::Solid {}, formulation: Formulation::default() })
.unwrap();
m.add_reference_point(&long, ReferencePoint { xyz: [ugly; 3], label: 0 }).unwrap();
@@ -640,7 +643,7 @@ fn what_abaqus_cannot_express_is_named() {
let mut ortho = plate();
let carbon = Elastic::EngineeringConstants { young: [140e3, 10e3, 10e3], poisson: [0.3, 0.3, 0.4], shear: [5e3, 5e3, 3.8e3] };
ortho.replace_material("Aluminium", Material { elastic: Some(carbon), density: Some(1.6e-9), fluid: None }).unwrap();
ortho.replace_material("Aluminium", Material { elastic: Some(carbon), density: Some(1.6e-9), ..Material::default() }).unwrap();
assert!(unsupported(&ortho).contains("orthotropic material `Aluminium` in a shell section"));
let mut loose = frequency();
@@ -658,7 +661,7 @@ fn what_abaqus_cannot_express_is_named() {
fn orthotropic_solids_beams_and_rods_are_written() {
let mut m = cantilever();
let carbon = Elastic::EngineeringConstants { young: [140e3, 10e3, 10e3], poisson: [0.3, 0.3, 0.4], shear: [5e3, 5e3, 3.8e3] };
m.replace_material("Steel", Material { elastic: Some(carbon), density: None, fluid: None }).unwrap();
m.replace_material("Steel", Material { elastic: Some(carbon), density: None, ..Material::default() }).unwrap();
let deck = abaqus::write(&m).unwrap();
assert!(deck.contains("*ELASTIC, TYPE=ENGINEERING CONSTANTS\n140000.0, 10000.0, 10000.0, 0.3, 0.3, 0.4, 5000.0, 5000.0\n3800.0\n"), "{deck}");
@@ -699,3 +702,54 @@ fn a_model_that_is_not_ready_is_refused() {
assert!(matches!(abaqus::write(&m), Err(Error::Model(_))));
assert!(matches!(abaqus::write(&Model::default()), Err(Error::Model(_))));
}
/// The cantilever with its steel given each plasticity in turn.
fn plastic_cantilever(p: modelcraft_model::Plastic) -> Model {
let mut m = cantilever();
m.replace_material("Steel", Material { plastic: Some(p), ..steel() }).unwrap();
m
}
/// Plasticity is written as `*PLASTIC` (and Johnson–Cook's rate and heat keywords); damage and
/// erosion, failure models of Abaqus/Explicit, are reported.
#[test]
fn plasticity_is_written_and_failure_reported() {
use modelcraft_model::{Damage, Erosion, Hardening, HardeningPoint, Plastic};
let section = |deck: &str| deck.split("*MATERIAL, NAME=Steel").nth(1).and_then(|s| s.split("*SOLID SECTION").next()).unwrap().to_string();
let deck =
abaqus::write(&plastic_cantilever(Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 1000.0, hardening: Hardening::Isotropic }))
.unwrap();
assert!(section(&deck).contains("*PLASTIC\n250.0, 0.0\n10250.0, 10.0\n"), "{}", section(&deck));
let deck =
abaqus::write(&plastic_cantilever(Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 1000.0, hardening: Hardening::Kinematic }))
.unwrap();
assert!(section(&deck).contains("*PLASTIC, HARDENING=KINEMATIC\n250.0, 0.0\n1250.0, 1.0\n"), "{}", section(&deck));
let table = Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: 200.0 }, HardeningPoint { strain: 0.1, stress: 300.0 }] };
let deck = abaqus::write(&plastic_cantilever(table)).unwrap();
assert!(section(&deck).contains("*PLASTIC\n200.0, 0.0\n300.0, 0.1\n"), "{}", section(&deck));
let jc = Plastic::JohnsonCook {
a: 90.0,
b: 292.0,
n: 0.31,
c: 0.025,
m: 1.09,
reference_strain_rate: 1.0,
room_temperature: 294.0,
melt_temperature: 1356.0,
specific_heat: Some(3.83e8),
taylor_quinney: 0.9,
};
let deck = abaqus::write(&plastic_cantilever(jc.clone())).unwrap();
let s = section(&deck);
assert!(
s.contains("*PLASTIC, HARDENING=JOHNSON COOK\n90.0, 292.0, 0.31, 1.09, 1356.0, 294.0\n*RATE DEPENDENT, TYPE=JOHNSON COOK\n0.025, 1.0\n*SPECIFIC HEAT\n383000000.0\n*INELASTIC HEAT FRACTION\n0.9\n"),
"{s}"
);
let mut failing = plastic_cantilever(jc);
let mut steel = failing.material("Steel").unwrap().clone();
steel.damage = Some(Damage::JohnsonCook { d1: 0.54, d2: 4.89, d3: -3.03, d4: 0.014, d5: 1.12 });
steel.erosion = Some(Erosion { plastic_strain: 1.0 });
failing.replace_material("Steel", steel).unwrap();
let Err(Error::Unsupported { what, .. }) = abaqus::write(&failing) else { panic!("damage is not written") };
assert!(what.contains("material `Steel`: damage and erosion"), "{what}");
}
+55 -5
View File
@@ -13,7 +13,7 @@ use modelcraft_model::*;
// Our own models.
fn steel() -> Material {
Material { elastic: Some(Elastic::Isotropic { young: 210000.0, poisson: 0.3 }), density: Some(7.85e-9), fluid: None }
Material { elastic: Some(Elastic::Isotropic { young: 210000.0, poisson: 0.3 }), density: Some(7.85e-9), ..Material::default() }
}
fn step(name: &str, procedure: Procedure, bcs: &[&str], loads: &[&str]) -> Step {
@@ -72,7 +72,7 @@ fn cantilever_modes() -> Model {
let mut m = Model::default();
cantilever_mesh(&mut m);
let ortho = Elastic::EngineeringConstants { young: [140000.0, 10000.0, 10000.0], poisson: [0.3, 0.3, 0.45], shear: [5000.0, 5000.0, 3500.0] };
m.add_material("Ply", Material { elastic: Some(ortho), density: Some(1.6e-9), fluid: None }).unwrap();
m.add_material("Ply", Material { elastic: Some(ortho), density: Some(1.6e-9), ..Material::default() }).unwrap();
let f = Formulation { incompatible_modes: true, ..Formulation::default() };
m.add_section("beam", Section { set: "all".into(), material: "Ply".into(), kind: SectionKind::Solid {}, formulation: f }).unwrap();
m.add_reference_point("RP", ReferencePoint { xyz: [5.0, 0.5, 0.5], label: 0 }).unwrap();
@@ -155,8 +155,11 @@ fn solids() -> Model {
let h: Vec<u32> = h.iter().map(|p| node(&mut m, *p)).collect();
m.set_element(4, ElementKind::Hex20, h).unwrap();
m.add_material("Aluminium", Material { elastic: Some(Elastic::Isotropic { young: 70e9, poisson: 0.33 }), density: Some(2700.0), fluid: None })
.unwrap();
m.add_material(
"Aluminium",
Material { elastic: Some(Elastic::Isotropic { young: 70e9, poisson: 0.33 }), density: Some(2700.0), ..Material::default() },
)
.unwrap();
for (e, name) in [(1, "tet4"), (2, "tet10"), (3, "wedge"), (4, "hex20")] {
m.add_set(name, Set { elements: vec![e], ..Set::default() }).unwrap();
let f = Formulation { hybrid: e == 1, ..Formulation::default() };
@@ -658,7 +661,11 @@ fn hostile_numbers_stay_within_their_fields() {
m.set_node(2, [f64::MIN_POSITIVE, -f64::MIN_POSITIVE, 5e-324]).unwrap();
m.replace_material(
"Steel",
Material { elastic: Some(Elastic::Isotropic { young: 1e300, poisson: 0.499_999_999_999_999_9 }), density: Some(1e-300), fluid: None },
Material {
elastic: Some(Elastic::Isotropic { young: 1e300, poisson: 0.499_999_999_999_999_9 }),
density: Some(1e-300),
..Material::default()
},
)
.unwrap();
m.replace_load(
@@ -687,3 +694,46 @@ fn explicit_steps_are_refused_by_every_format() {
assert!(e.to_string().contains("step `Impact`: explicit dynamic"), "{}: {e}", f.name);
}
}
/// Plasticity: `*MAT_PLASTIC_KINEMATIC` (bilinear) and `*MAT_PIECEWISE_LINEAR_PLASTICITY` with a
/// curve numbered after the steps' ramps; the erosion strain is the failure strain; Johnson–Cook
/// and damage are reported.
#[test]
fn plasticity_is_written_as_plastic_materials() {
let with = |p: Plastic, erosion: Option<f64>| {
let mut m = cantilever();
m.replace_material("Steel", Material { plastic: Some(p), erosion: erosion.map(|e| Erosion { plastic_strain: e }), ..steel() }).unwrap();
m
};
let w = lsdyna::write(&with(Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 1000.0, hardening: Hardening::Isotropic }, Some(0.5)))
.unwrap();
let mat = w.text.split("*MAT_PLASTIC_KINEMATIC_TITLE\nSteel\n").nth(1).unwrap();
let cards: Vec<Vec<&str>> = mat.lines().filter(|l| !l.starts_with('$')).take(2).map(|l| l.split_whitespace().collect()).collect();
// E_t = E H / (E + H) = 210000 · 1000 / 211000.
let etan: f64 = cards[0][5].parse().unwrap();
assert!((etan - 210000.0 * 1000.0 / 211000.0).abs() < 1e-9, "{cards:?}");
assert_eq!((cards[0][4], cards[0][6], cards[1][0]), ("250.0", "1.0", "0.5"), "{cards:?}");
assert!(!w.text.contains("*MAT_ELASTIC"));
let table = Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: 200.0 }, HardeningPoint { strain: 0.1, stress: 300.0 }] };
let w = lsdyna::write(&with(table, None)).unwrap();
let mat = w.text.split("*MAT_PIECEWISE_LINEAR_PLASTICITY_TITLE\nSteel\n").nth(1).unwrap();
let cards: Vec<&str> = mat.lines().filter(|l| !l.starts_with('$')).take(2).collect();
assert_eq!(cards[1].split_whitespace().collect::<Vec<_>>(), ["2"], "the curve after the one step's ramp: {cards:?}");
let curve = w.text.split("*DEFINE_CURVE_TITLE\nSteel hardening\n").nth(1).unwrap();
let rows: Vec<Vec<&str>> = curve.lines().filter(|l| !l.starts_with('$')).take(3).map(|l| l.split_whitespace().collect()).collect();
assert_eq!(rows, [vec!["2"], vec!["0.0", "200.0"], vec!["0.1", "300.0"]]);
let jc = Plastic::JohnsonCook {
a: 90.0,
b: 292.0,
n: 0.31,
c: 0.0,
m: 1.0,
reference_strain_rate: 1.0,
room_temperature: 294.0,
melt_temperature: 1356.0,
specific_heat: None,
taylor_quinney: 0.9,
};
let Err(Error::Unsupported { what, .. }) = lsdyna::write(&with(jc, None)) else { panic!() };
assert!(what.contains("Johnson–Cook"), "{what}");
}
+52 -5
View File
@@ -131,7 +131,7 @@ impl Read {
// Our own models.
fn steel() -> Material {
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9), fluid: None }
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9), ..Material::default() }
}
/// A bar of `n` unit hex8 along X; node (i, y, z) = 1 + 4i + 2y + z.
@@ -190,7 +190,7 @@ fn plate() -> Model {
m.add_set("top", Set { faces: (1..=4).map(|element| ElementFace { element, face: 1 }).collect(), ..Set::default() }).unwrap();
m.add_material(
"Aluminium",
Material { elastic: Some(Elastic::Isotropic { young: 70_000.0, poisson: 0.33 }), density: Some(2.7e-9), fluid: None },
Material { elastic: Some(Elastic::Isotropic { young: 70_000.0, poisson: 0.33 }), density: Some(2.7e-9), ..Material::default() },
)
.unwrap();
let shell = SectionKind::Shell { thickness: 2.0, offset: 0.0, points: 5 };
@@ -521,7 +521,7 @@ fn beams_rods_orthotropy_couplings_and_prescribed_motion() {
let ortho = Material {
elastic: Some(Elastic::EngineeringConstants { young: [140e3, 10e3, 12e3], poisson: [0.3, 0.28, 0.4], shear: [5e3, 4.5e3, 3.8e3] }),
density: Some(1.6e-9),
fluid: None,
..Material::default()
};
m.add_material("Carbon", ortho.clone()).unwrap();
m.replace_section(
@@ -676,7 +676,7 @@ fn what_nastran_cannot_express_is_named() {
// Orthotropic beams, tilted orthotropic shells, one-point shells.
let mut m = plate();
let ortho = Elastic::EngineeringConstants { young: [140e3, 10e3, 10e3], poisson: [0.3, 0.3, 0.4], shear: [5e3, 5e3, 3.8e3] };
m.replace_material("Aluminium", Material { elastic: Some(ortho), density: Some(1.6e-9), fluid: None }).unwrap();
m.replace_material("Aluminium", Material { elastic: Some(ortho), density: Some(1.6e-9), ..Material::default() }).unwrap();
m.set_node(9, [10.0, 10.0, 0.5]).unwrap();
assert!(unsupported(&m).contains("planes of constant Z"));
m.set_node(9, [10.0, 10.0, 0.0]).unwrap();
@@ -695,7 +695,7 @@ fn what_nastran_cannot_express_is_named() {
let mut m = plate();
m.set_element(9, ElementKind::Rod2, vec![1, 9]).unwrap();
m.add_set("bars", Set { elements: vec![9], ..Set::default() }).unwrap();
m.add_material("Fibre", Material { elastic: Some(ortho), density: Some(1.6e-9), fluid: None }).unwrap();
m.add_material("Fibre", Material { elastic: Some(ortho), density: Some(1.6e-9), ..Material::default() }).unwrap();
m.add_section(
"bars",
Section { set: "bars".into(), material: "Fibre".into(), kind: SectionKind::Rod { area: 1.0 }, formulation: Formulation::default() },
@@ -735,3 +735,50 @@ fn conflicting_values_and_ids_at_the_top_of_the_range() {
m.add_coupling("pad", Coupling { reference_point: "RP".into(), set: "end".into(), kind: CouplingKind::Kinematic, dofs: Dofs::all() }).unwrap();
assert!(matches!(nastran::write(&m), Err(Error::Limit(_))));
}
/// Plasticity: `MATS1` beside `MAT1` (bilinear by slope, a table through `TABLES1`); Johnson–Cook,
/// damage and erosion are reported.
#[test]
fn plasticity_is_written_as_mats1() {
use modelcraft_model::{Erosion, Hardening, HardeningPoint, Plastic};
let with = |p: Plastic| {
let mut m = cantilever();
m.replace_material("Steel", Material { plastic: Some(p), ..steel() }).unwrap();
m
};
let deck = nastran::write(&with(Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 1000.0, hardening: Hardening::Kinematic })).unwrap();
let r = read(&deck);
let (_, f) = r.cards.iter().find(|(n, _)| n == "MATS1").unwrap();
assert_eq!((f[0].as_str(), f[1].as_str(), f[2].as_str(), int(&f[4]), int(&f[5])), ("1", "", "PLASTIC", 1, 2), "{f:?}");
assert_eq!((real(&f[3]), real(&f[6])), (1000.0, 250.0), "{f:?}");
let table = Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: 210.0 }, HardeningPoint { strain: 0.1, stress: 315.0 }] };
let deck = nastran::write(&with(table)).unwrap();
let r = read(&deck);
let (_, mats1) = r.cards.iter().find(|(n, _)| n == "MATS1").unwrap();
let (_, table) = r.cards.iter().find(|(n, _)| n == "TABLES1").unwrap();
assert_eq!(mats1[1], table[0], "MATS1 names its table");
// The curve after the header line: the origin, then total strain σ/E + ε_p against σ.
let data: Vec<&String> = table[8..].iter().filter(|f| !f.is_empty()).collect();
assert_eq!(data.last().map(|s| s.as_str()), Some("ENDT"));
let values: Vec<f64> = data[..data.len() - 1].iter().map(|s| real(s)).collect();
let want = [0.0, 0.0, 0.001, 210.0, 0.1015, 315.0];
assert!(values.len() == 6 && values.iter().zip(want).all(|(a, b)| (a - b).abs() < 1e-12), "{values:?}");
let jc = Plastic::JohnsonCook {
a: 90.0,
b: 292.0,
n: 0.31,
c: 0.0,
m: 1.0,
reference_strain_rate: 1.0,
room_temperature: 294.0,
melt_temperature: 1356.0,
specific_heat: None,
taylor_quinney: 0.9,
};
let mut m = with(jc);
let mut steel = m.material("Steel").unwrap().clone();
steel.erosion = Some(Erosion { plastic_strain: 1.0 });
m.replace_material("Steel", steel).unwrap();
let Err(Error::Unsupported { what, .. }) = nastran::write(&m) else { panic!() };
assert!(what.contains("Johnson–Cook plasticity") && what.contains("damage and erosion"), "{what}");
}
+5 -2
View File
@@ -32,8 +32,11 @@ fn plate(kind: SectionKind, element: ElementKind, formulation: Formulation) -> M
m.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
m.add_set("left", Set { nodes: vec![1, 4], ..Set::default() }).unwrap();
m.add_set("right", Set { edges: vec![right], ..Set::default() }).unwrap();
m.add_material("Steel", Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9), fluid: None })
.unwrap();
m.add_material(
"Steel",
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9), ..Material::default() },
)
.unwrap();
m.add_section("plate", Section { set: "all".into(), material: "Steel".into(), kind, formulation }).unwrap();
m.add_boundary_condition("hold", BoundaryCondition::Fixed { target: Target::Set("left".into()) }).unwrap();
m.add_load("press", Load::Pressure { set: "right".into(), magnitude: 5.0 }).unwrap();