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 <[email protected]>
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

+71 -2
View File
@@ -82,6 +82,21 @@
//! materials outside solid sections (shell and beam material axes need orientations), and
//! distributing couplings that leave a translation free.
//!
//! # Materials
//!
//! Isotropic elasticity is `*ELASTIC, TYPE=ISOTROPIC`, engineering constants
//! `TYPE=ENGINEERING CONSTANTS`, density `*DENSITY`. Von Mises plasticity is `*PLASTIC`: a
//! hardening table as its (yield stress, plastic strain) lines; bilinear isotropic hardening as
//! two lines, the second at a plastic strain of [`BILINEAR_REACH`] (Abaqus holds the last yield
//! stress beyond a table's end, so the line is written out further than any analysis strains);
//! bilinear kinematic hardening as `HARDENING=KINEMATIC` with its yield stress at plastic strains
//! 0 and 1 (Abaqus's linear kinematic model extends that line); Johnson–Cook as
//! `HARDENING=JOHNSON COOK` (A, B, n, m, melting and transition temperature, the transition
//! temperature being the model's room temperature) with `*RATE DEPENDENT, TYPE=JOHNSON COOK`
//! (C, ε̇₀) when C is not zero, and adiabatic heating as `*SPECIFIC HEAT` and
//! `*INELASTIC HEAT FRACTION`. Damage and erosion are failure models of Abaqus/Explicit, and this
//! writer writes Abaqus/Standard procedures: they are reported, not written.
//!
//! Node order: the model stores elements in Abaqus order, so connectivity passes through; the
//! model's face numbers are Abaqus's S1–S6 for solids, and SPOS/SNEG for shell faces 1/2.
@@ -89,14 +104,17 @@ use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use modelcraft_model::{
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Integration, Load, Model, OutputVariable, Procedure, Resolved,
Section, SectionKind, Step, Symmetry, Target,
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Hardening, Integration, Load, Model, OutputVariable, Plastic,
Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
};
use crate::{Error, Result, unsupported};
const FORMAT: &str = "Abaqus";
/// The plastic strain a bilinear isotropic hardening line is written out to (1000%).
pub const BILINEAR_REACH: f64 = 10.0;
/// Longest line Abaqus reads.
pub const MAX_LINE: usize = 256;
@@ -251,6 +269,13 @@ fn check_expressible(model: &Model) -> Result<()> {
for step in model.steps().iter().filter(|s| matches!(s.procedure, Procedure::ExplicitDynamic(_))) {
bad.insert(format!("step `{}`: explicit dynamic steps (not written to Abaqus input yet)", step.name));
}
for (name, m) in model.materials() {
if m.damage.is_some() || m.erosion.is_some() {
bad.insert(format!(
"material `{name}`: damage and erosion (Abaqus/Explicit failure models; this writer writes Abaqus/Standard procedures so far)"
));
}
}
for (name, c) in model.couplings() {
if c.kind == CouplingKind::Distributing && !(1..=3).all(|d| c.dofs.contains(d)) {
bad.insert(format!("coupling `{name}`: an Abaqus distributing coupling always couples translations 1-3"));
@@ -621,6 +646,50 @@ impl Deck {
self.line("*DENSITY");
self.numbers_line("", &[rho])?;
}
if let Some(p) = &m.plastic {
self.plastic(p)?;
}
}
Ok(())
}
/// Von Mises plasticity (see the module documentation for the mapping).
fn plastic(&mut self, p: &Plastic) -> Result<()> {
match p {
Plastic::Bilinear { yield_stress, hardening_modulus, hardening } => {
if *hardening_modulus == 0.0 {
self.line("*PLASTIC");
self.numbers_line("", &[*yield_stress, 0.0])?;
} else if *hardening == Hardening::Kinematic {
self.line("*PLASTIC, HARDENING=KINEMATIC");
self.numbers_line("", &[*yield_stress, 0.0])?;
self.numbers_line("", &[yield_stress + hardening_modulus, 1.0])?;
} else {
self.line("*PLASTIC");
self.numbers_line("", &[*yield_stress, 0.0])?;
self.numbers_line("", &[yield_stress + hardening_modulus * BILINEAR_REACH, BILINEAR_REACH])?;
}
}
Plastic::Table { points } => {
self.line("*PLASTIC");
for point in points {
self.numbers_line("", &[point.stress, point.strain])?;
}
}
Plastic::JohnsonCook { a, b, n, c, m, reference_strain_rate, room_temperature, melt_temperature, specific_heat, taylor_quinney } => {
self.line("*PLASTIC, HARDENING=JOHNSON COOK");
self.numbers_line("", &[*a, *b, *n, *m, *melt_temperature, *room_temperature])?;
if *c != 0.0 {
self.line("*RATE DEPENDENT, TYPE=JOHNSON COOK");
self.numbers_line("", &[*c, *reference_strain_rate])?;
}
if let Some(cp) = specific_heat {
self.line("*SPECIFIC HEAT");
self.numbers_line("", &[*cp])?;
self.line("*INELASTIC HEAT FRACTION");
self.numbers_line("", &[*taylor_quinney])?;
}
}
}
Ok(())
}
+78 -7
View File
@@ -8,7 +8,13 @@
//! `*SECTION_BEAM` (rods as trusses) and the element formulation mapped from the section's
//! ([`elements`]); `*HOURGLASS` where a reduced-integration formulation needs it;
//! - `*MAT_ELASTIC` for isotropic and `*MAT_ORTHOTROPIC_ELASTIC` (global material axes) for
//! orthotropic materials;
//! orthotropic materials; von Mises plasticity as `*MAT_PLASTIC_KINEMATIC` (bilinear: the
//! tangent modulus E·H/(E + H) of the uniaxial curve, β = 1 isotropic or 0 kinematic, the
//! erosion plastic strain as its failure strain) or `*MAT_PIECEWISE_LINEAR_PLASTICITY` (a
//! hardening table as a `*DEFINE_CURVE` of yield stress against plastic strain, the erosion
//! plastic strain as its failure strain). Johnson–Cook plasticity and damage are reported: their
//! LS-DYNA material needs an equation of state and belongs to explicit analyses, which this
//! writer does not write yet;
//! - `*ELEMENT_SOLID` (tetrahedra and wedges as degenerate hexahedra, 10-node tetrahedra),
//! `*ELEMENT_SOLID_H20`, `*ELEMENT_SHELL` and `*ELEMENT_BEAM`;
//! - `*SET_NODE_LIST` and `*SET_SEGMENT` for the sets boundary conditions, loads and couplings
@@ -47,8 +53,8 @@ mod elements;
use std::collections::{BTreeMap, BTreeSet};
use modelcraft_model::{
BoundaryCondition, CouplingKind, Diagnostic, Dofs, Elastic, Element, ElementKind, Load, Model, OutputVariable, Procedure, Resolved, Section,
SectionKind, Step, Target,
BoundaryCondition, CouplingKind, Diagnostic, Dofs, Elastic, Element, ElementKind, Hardening, Load, Material, Model, OutputVariable, Plastic,
Procedure, Resolved, Section, SectionKind, Step, Target,
};
use crate::{Error, Result};
@@ -117,6 +123,8 @@ struct Plan<'a> {
uniform: BTreeMap<&'a str, ([f64; 3], u32)>,
/// The part set gravity acts on, when it acts on a set.
gravity_parts: Option<Vec<u32>>,
/// Material name → the curve id of its hardening table.
curves: BTreeMap<&'a str, u32>,
}
/// Write the model as an LS-DYNA keyword deck.
@@ -304,13 +312,25 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
None
}
};
for m in materials.keys() {
if model.material(m).is_some_and(|m| m.density.is_none()) {
refused.insert(format!("material `{m}` has no density, and LS-DYNA requires a mass density on every material"));
let mut curves = BTreeMap::new();
for name in materials.keys() {
let Some(m) = model.material(name) else { continue };
if m.density.is_none() {
refused.insert(format!("material `{name}` has no density, and LS-DYNA requires a mass density on every material"));
}
if matches!(m.plastic, Some(Plastic::JohnsonCook { .. })) || m.damage.is_some() {
refused.insert(format!(
"material `{name}`: Johnson–Cook plasticity and damage (LS-DYNA's Johnson–Cook material needs an equation of state and is for explicit analyses, which this writer does not write yet)"
));
}
if matches!(m.plastic, Some(Plastic::Table { .. })) {
// Curve ids after the steps' load ramps (ids 1 to the number of steps).
let lcid = u32::try_from(model.steps().len() + 1 + curves.len()).map_err(|_| Error::Limit("too many curves".into()))?;
curves.insert(*name, lcid);
}
}
crate::unsupported(FORMAT, refused)?;
Ok(Plan { parts, materials, hourglasses, sets, node_sets, segment_sets, uniform, gravity_parts })
Ok(Plan { parts, materials, hourglasses, sets, node_sets, segment_sets, uniform, gravity_parts, curves })
}
/// A 1-based id from a 0-based index.
@@ -374,6 +394,7 @@ fn materials(d: &mut Deck, model: &Model, plan: &Plan) -> Result<()> {
let m = model.material(name).ok_or_else(|| broken(format!("material `{name}`")))?;
let rho = m.density.unwrap_or_default();
match m.elastic {
Some(Elastic::Isotropic { young, poisson }) if m.plastic.is_some() => plastic_material(d, name, mid, rho, young, poisson, m, plan)?,
Some(Elastic::Isotropic { young, poisson }) => {
d.line("*MAT_ELASTIC_TITLE");
d.line(name);
@@ -401,6 +422,56 @@ fn materials(d: &mut Deck, model: &Model, plan: &Plan) -> Result<()> {
Ok(())
}
/// The plastic materials: `*MAT_PLASTIC_KINEMATIC` or `*MAT_PIECEWISE_LINEAR_PLASTICITY` with its
/// curve, in place of `*MAT_ELASTIC`.
fn plastic_material(d: &mut Deck, name: &str, mid: u32, rho: f64, young: f64, poisson: f64, m: &Material, plan: &Plan) -> Result<()> {
let fail = m.erosion.map_or(Field::Blank, |e| Field::from(e.plastic_strain));
match &m.plastic {
Some(Plastic::Bilinear { yield_stress, hardening_modulus, hardening }) => {
d.line("*MAT_PLASTIC_KINEMATIC_TITLE");
d.line(name);
d.header(&["mid", "ro", "e", "pr", "sigy", "etan", "beta"]);
let tangent = young * hardening_modulus / (young + hardening_modulus);
let beta = if *hardening == Hardening::Kinematic { 0.0 } else { 1.0 };
d.card([
Field::from(mid),
Field::from(rho),
Field::from(young),
Field::from(poisson),
Field::from(*yield_stress),
Field::from(tangent),
Field::from(beta),
]);
d.header(&["src", "srp", "fs"]);
d.card([Field::Blank, Field::Blank, fail]);
}
Some(Plastic::Table { points }) => {
let lcid = plan.curves.get(name).copied().ok_or_else(|| broken(format!("material `{name}` has no curve")))?;
d.line("*MAT_PIECEWISE_LINEAR_PLASTICITY_TITLE");
d.line(name);
d.header(&["mid", "ro", "e", "pr", "sigy", "etan", "fail"]);
let first = m.plastic.as_ref().map_or(0.0, Plastic::initial_yield);
d.card([Field::from(mid), Field::from(rho), Field::from(young), Field::from(poisson), Field::from(first), Field::Blank, fail]);
d.header(&["c", "p", "lcss"]);
d.card([Field::Blank, Field::Blank, Field::from(lcid)]);
d.header(&["eps1"]);
d.card([Field::Blank]);
d.header(&["es1"]);
d.card([Field::Blank]);
d.line("*DEFINE_CURVE_TITLE");
d.line(&format!("{name} hardening"));
d.header(&["lcid"]);
d.card([Field::from(lcid)]);
d.header(&["a1", "o1"]);
for p in points {
d.card([Field::from(p.strain), Field::from(p.stress)]);
}
}
_ => return Err(broken(format!("material `{name}`: a plasticity LS-DYNA cannot take"))),
}
Ok(())
}
fn parts(d: &mut Deck, plan: &Plan) -> Result<()> {
for p in &plan.parts {
match p.section.kind {
+54 -7
View File
@@ -14,9 +14,12 @@
//! - the element formulation: `PSOLID`'s integration network and scheme (see [`solid_rule`]);
//! formulations Nastran's linear elements have no counterpart for are reported, not
//! approximated;
//! - isotropic materials: `MAT1`; orthotropic engineering constants: `MAT9` (the stiffness matrix
//! in the basic axes) for solids and `MAT8` for shells lying in planes of constant Z, with the
//! material X axis tied to the basic X axis through a `CORD2R`;
//! - isotropic materials: `MAT1`, with `MATS1` for von Mises plasticity (bilinear by its
//! hardening slope, a hardening table as a `TABLES1` stress–strain curve; Johnson–Cook, damage
//! and erosion are reported, not written); a linear solution reads the elastic entry alone;
//! - orthotropic engineering constants: `MAT9` (the stiffness matrix in the basic axes) for
//! solids and `MAT8` for shells lying in planes of constant Z, with the material X axis tied to
//! the basic X axis through a `CORD2R`;
//! - boundary conditions: per step, one constraint set of `SPC1` (held degrees of freedom) and
//! `SPC` (prescribed values); in a modes step every constrained degree of freedom is held;
//! - forces, moments and tractions: `FORCE` and `MOMENT` at the nodal values the model resolves,
@@ -44,8 +47,8 @@ use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use modelcraft_model::{
BeamProfile, Coupling, CouplingKind, Dofs, Elastic, ElementKind, Formulation, Integration, Load, MAX_LABEL, Model, OutputVariable, Procedure,
Resolved, SectionKind, Step,
BeamProfile, Coupling, CouplingKind, Dofs, Elastic, ElementKind, Formulation, Hardening, Integration, Load, MAX_LABEL, Model, OutputVariable,
Plastic, Procedure, Resolved, SectionKind, Step,
};
use crate::{Error, Result};
@@ -184,6 +187,8 @@ struct MaterialIds {
solid: Option<u32>,
/// `MAT8`, for orthotropic materials of shells.
shell: Option<u32>,
/// `TABLES1` of a tabulated plasticity's stress–strain curve.
table: Option<u32>,
}
/// Which element families use a material.
@@ -307,7 +312,7 @@ impl<'m> Deck<'m> {
}
fn plan(&mut self) -> Result<()> {
self.plan_materials();
self.plan_materials()?;
self.plan_sections()?;
self.plan_couplings()?;
self.plan_constraints()?;
@@ -321,7 +326,7 @@ impl<'m> Deck<'m> {
}
/// Material ids, for the forms the sections need.
fn plan_materials(&mut self) {
fn plan_materials(&mut self) -> Result<()> {
let model = self.model;
let mut uses: BTreeMap<&str, MaterialUse> = BTreeMap::new();
for s in model.sections().values() {
@@ -353,8 +358,24 @@ impl<'m> Deck<'m> {
}
None => MaterialIds::default(),
};
let mut ids = ids;
if let Some(m) = model.material(name) {
match &m.plastic {
Some(Plastic::JohnsonCook { .. }) => {
self.problems
.insert(format!("material `{name}`: Johnson–Cook plasticity (MATS1 has no rate- and temperature-dependent flow stress)"));
}
Some(Plastic::Table { .. }) => ids.table = Some(next_id(&mut self.next_sid)?),
_ => {}
}
if m.damage.is_some() || m.erosion.is_some() {
self.problems
.insert(format!("material `{name}`: damage and erosion (Nastran's linear and implicit solutions have no element failure)"));
}
}
self.materials.insert(name, ids);
}
Ok(())
}
/// A new property for `elements`.
@@ -706,6 +727,9 @@ impl<'m> Deck<'m> {
}
if let (Some(card), Some(mid)) = (card, ids.isotropic) {
write_named(out, &format!("material {name}"), mid, card)?;
if let (Some(plastic), Some(Elastic::Isotropic { young, .. })) = (&m.plastic, m.elastic) {
plasticity(out, name, mid, young, plastic, ids.table)?;
}
}
}
@@ -907,6 +931,29 @@ fn beam_dimensions(p: BeamProfile) -> (&'static str, Vec<f64>) {
}
}
/// `MATS1` (von Mises yield, `YF` 1) of an isotropic material's plasticity: a bilinear law by its
/// work-hardening slope `H` (stress against plastic strain) and initial yield stress `LIMIT1`,
/// isotropic (`HR` 1) or kinematic (`HR` 2); a hardening table as a `TABLES1` stress–strain
/// curve through the origin, with the total strain σ/E + ε_p of each point.
fn plasticity(out: &mut String, name: &str, mid: u32, young: f64, plastic: &Plastic, table: Option<u32>) -> Result<()> {
let what = format!("plasticity of {name}");
match (plastic, table) {
(Plastic::Bilinear { yield_stress, hardening_modulus, hardening }, _) => {
let hr = if *hardening == Hardening::Kinematic { 2 } else { 1 };
let card = Card::new("MATS1").int(mid).blank().text("PLASTIC").real(*hardening_modulus).int(1).int(hr).real(*yield_stress);
write_named(out, &what, mid, card)
}
(Plastic::Table { points }, Some(tid)) => {
write_named(out, &what, mid, Card::new("MATS1").int(mid).int(tid).text("PLASTIC").blank().int(1).int(1))?;
let curve = std::iter::once([0.0, 0.0]).chain(points.iter().map(|p| [p.stress / young + p.strain, p.stress])).flatten();
write_named(out, &format!("stress-strain curve of {name}"), tid, Card::new("TABLES1").int(tid).end_line().reals(curve).text("ENDT"))
}
(Plastic::Table { .. }, None) | (Plastic::JohnsonCook { .. }, _) => {
Err(Error::Model(modelcraft_model::Error::Invalid(format!("material `{name}`: its plasticity was not planned for MATS1"))))
}
}
}
/// An entry preceded by a comment naming what it holds (`material Steel: MAT1 1`).
fn write_named(out: &mut String, what: &str, id: u32, card: Card) -> Result<()> {
comment(out, &format!("{what}: {} {id}", card.name()));
+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();