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

+47 -43
View File
@@ -3,11 +3,14 @@
//! vector of an explicit dynamic step.
use modelcraft_fem::material::Elasticity;
use modelcraft_fem::plasticity::J2;
use modelcraft_fem::uniform::{Hex8, Tet4, modulus_bound};
use modelcraft_fem::{DofKind, DofLayout, DofSet, face};
use modelcraft_model::{Elastic, ElementKind, ExplicitDynamic, HourglassKind, Integration, Load, Model, Resolved, SectionKind, Step, Target};
use modelcraft_model::{
Elastic, ElementKind, ExplicitDynamic, HourglassKind, Integration, Load, Model, Resolved, SectionKind, Step, StressRate, Target,
};
use crate::elements::add_at;
use crate::elements::{HexScales, add_at, frequency_bound, hex_scales};
use crate::{Error, Result};
/// Degrees of freedom per node: the three translations, node-major (see [`System`]).
@@ -27,8 +30,13 @@ pub struct Material {
pub wave_speed: f64,
/// The bound of [`modelcraft_fem::uniform::modulus_bound`].
pub modulus: f64,
/// Bulk modulus, for the nodal-pressure tetrahedron (isotropic materials).
pub bulk: Option<f64>,
/// The largest normal stiffness M (λ + 2μ when isotropic): c = √(M/ρ).
pub stiffest: f64,
/// The hypoelastic(–plastic) update of an isotropic material; `None` for orthotropic
/// elasticity, updated with [`Material::elasticity`].
pub j2: Option<J2>,
/// The equivalent plastic strain at which elements erode.
pub erosion: Option<f64>,
}
/// A one-point hexahedron.
@@ -41,9 +49,10 @@ pub struct Hex {
pub material: usize,
pub kinematics: Hex8,
pub gamma: [[f64; 8]; 4],
/// The hourglass coefficient: a damping (viscous) or a stiffness (stiffness form), see
/// [`crate::elements`].
pub hourglass: f64,
/// Row-sum lumped masses of the element's nodes.
pub mass: [f64; 8],
/// Size, hourglass coefficient and frequency bound of the undeformed element.
pub rest: HexScales,
/// The element's stable length: its critical increment without bulk viscosity is
/// `length / wave_speed`.
pub length: f64,
@@ -57,6 +66,9 @@ pub struct Tet {
pub nodes: [usize; 4],
pub material: usize,
pub kinematics: Tet4,
pub mass: [f64; 4],
/// The frequency bound ω² of the undeformed element.
pub omega2: f64,
pub length: f64,
}
@@ -97,6 +109,10 @@ pub struct System {
/// Nodes whose histories are recorded.
pub history_nodes: Vec<usize>,
pub hourglass: HourglassKind,
pub hourglass_coefficient: f64,
/// Large deformation (updated Lagrangian) or the undeformed mesh's gradients.
pub nonlinear: bool,
pub stress_rate: StressRate,
pub warnings: Vec<String>,
}
@@ -114,8 +130,11 @@ fn material(model: &Model, name: &str) -> Result<Material> {
.ok_or_else(|| invalid(format!("material `{name}` has no density, and explicit dynamics needs one")))?;
let d = modelcraft_fem::elasticity(elastic).map_err(|e| invalid(format!("material `{name}`: {e}")))?;
let stiffest = d[0][0].max(d[1][1]).max(d[2][2]);
let bulk = match *elastic {
Elastic::Isotropic { young, poisson } => Some(young / (3.0 * (1.0 - 2.0 * poisson))),
// The model keeps plasticity, damage and erosion to isotropic materials.
let j2 = match *elastic {
Elastic::Isotropic { .. } => {
Some(J2::from_model(elastic, m.plastic.as_ref(), m.damage.as_ref()).map_err(|e| invalid(format!("material `{name}`: {e}")))?)
}
Elastic::EngineeringConstants { .. } => None,
};
Ok(Material {
@@ -124,22 +143,12 @@ fn material(model: &Model, name: &str) -> Result<Material> {
density,
wave_speed: (stiffest / density).sqrt(),
modulus: modulus_bound(elastic, &d),
bulk,
stiffest,
j2,
erosion: m.erosion.map(|e| e.plastic_strain),
})
}
/// The largest absolute row sum of the hourglass vectors' Gram matrix Σ_I γ_αI γ_βI / v_I (an
/// upper bound of its largest eigenvalue, exact for parallelepipeds where the γ_α are
/// orthogonal).
fn hourglass_gram_bound(gamma: &[[f64; 8]; 4], nodal_volume: &[f64; 8]) -> f64 {
let mut worst: f64 = 0.0;
for a in gamma {
let row: f64 = gamma.iter().map(|b| a.iter().zip(b).zip(nodal_volume).map(|((x, y), v)| x * y / v).sum::<f64>().abs()).sum();
worst = worst.max(row);
}
worst
}
/// The section's elements of a kind the explicit solver has, or why not.
fn check_formulation(kind: ElementKind, section_name: &str, f: &modelcraft_model::Formulation) -> Result<()> {
let refuse = |why: &str| Err(Error::NotSupported(format!("section `{section_name}`: {why}")));
@@ -183,7 +192,7 @@ impl System {
for &label in &set.elements {
let el = model.element(label).ok_or_else(|| invalid(format!("element {label} is missing")))?;
check_formulation(el.kind, name, &section.formulation)?;
if el.kind == ElementKind::Tet4 && materials.get(mi).is_some_and(|m| m.bulk.is_none()) {
if el.kind == ElementKind::Tet4 && materials.get(mi).is_some_and(|m| m.j2.is_none()) {
return Err(Error::NotSupported(format!(
"nodal-pressure tet4 elements of the orthotropic material `{}` (section `{name}`)",
section.material
@@ -222,6 +231,9 @@ impl System {
external: vec![0.0; dofs],
history_nodes: Vec::new(),
hourglass: settings.hourglass.kind,
hourglass_coefficient: settings.hourglass.coefficient,
nonlinear: settings.nonlinear_geometry,
stress_rate: settings.stress_rate,
warnings: resolved.warnings.iter().map(ToString::to_string).collect(),
labels,
xyz,
@@ -237,28 +249,20 @@ impl System {
let x = nodes.map(|i| sys.xyz.get(i).copied().unwrap_or_default());
let k = Hex8::hex8(&x).map_err(at)?;
let gamma = k.hourglass_vectors(&x);
// h: the edge of the cube with the element's volume and gradient norm.
let h = k.volume / (2.0 * k.gradient_norm()).sqrt();
let kappa = settings.hourglass.coefficient;
let mut omega2 = k.frequency_bound(mat.modulus, rho);
let hourglass = match settings.hourglass.kind {
HourglassKind::Viscous => kappa * rho * c * h * h / 16.0,
HourglassKind::Stiffness => {
let stiffness = kappa * rho * c * c * h / 16.0;
omega2 += stiffness * hourglass_gram_bound(&gamma, &k.nodal_volume) / rho;
stiffness
}
};
sys.lump(&nodes, &k.nodal_volume, rho);
sys.hexes.push(Hex { label, index, nodes, material: mi, kinematics: k, gamma, hourglass, length: 2.0 * c / omega2.sqrt() });
let mass = k.nodal_volume.map(|v| rho * v);
let rest = hex_scales(&k, &gamma, &mass, mat, rho, settings.hourglass);
sys.lump(&nodes, &mass);
let length = 2.0 * c / rest.omega2.sqrt();
sys.hexes.push(Hex { label, index, nodes, material: mi, kinematics: k, gamma, mass, rest, length });
}
ElementKind::Tet4 => {
let nodes: [usize; 4] = idx.try_into().map_err(|_| invalid(format!("element {label} does not have 4 nodes")))?;
let x = nodes.map(|i| sys.xyz.get(i).copied().unwrap_or_default());
let k = Tet4::tet4(&x).map_err(at)?;
let omega2 = k.frequency_bound(mat.modulus, rho);
sys.lump(&nodes, &k.nodal_volume, rho);
sys.tets.push(Tet { label, index, nodes, material: mi, kinematics: k, length: 2.0 * c / omega2.sqrt() });
let mass = k.nodal_volume.map(|v| rho * v);
let omega2 = frequency_bound(&k, k.gradient_norm(), mat.modulus, &mass);
sys.lump(&nodes, &mass);
sys.tets.push(Tet { label, index, nodes, material: mi, kinematics: k, mass, omega2, length: 2.0 * c / omega2.sqrt() });
}
other => return Err(Error::Internal(format!("{} element kind", other.name()))),
}
@@ -270,10 +274,10 @@ impl System {
}
/// Add an element's row-sum lumped masses.
fn lump(&mut self, nodes: &[usize], nodal_volume: &[f64], density: f64) {
for (node, v) in nodes.iter().zip(nodal_volume) {
fn lump(&mut self, nodes: &[usize], mass: &[f64]) {
for (node, v) in nodes.iter().zip(mass) {
if let Some(m) = self.mass.get_mut(*node) {
*m += density * v;
*m += v;
}
}
}