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

+49 -16
View File
@@ -22,8 +22,8 @@
use modelcraft_model::{ExplicitDynamic, Model, OutputVariable, Procedure, Step};
use crate::elements::{Workspace, internal_forces};
use crate::result::{ControllingElement, Energies, ExplicitResult, Frame, History};
use crate::elements::{Motion, State, Workspace, internal_forces};
use crate::result::{ControllingElement, Energies, ErodedElement, ExplicitResult, Frame, History};
use crate::system::{NODE_DOFS, System};
use crate::{Error, Limits, Result};
@@ -178,13 +178,17 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res
let total_mass: f64 = sys.mass.iter().sum();
let mut ws = Workspace::new(&sys);
let mut previous = Workspace::new(&sys);
let mut state = State::new(&sys);
let mut eroded: Vec<ErodedElement> = Vec::new();
let mut u = vec![0.0; n];
let mut du = vec![0.0; n];
let v0 = sys.initial_velocity.clone();
let initial_kinetic = 0.5 * v0.iter().zip(&mass).map(|(v, m)| m * v * v).sum::<f64>();
let inelastic = sys.materials.iter().any(|m| m.j2.as_ref().is_some_and(|j| j.flow.is_some()));
// Start: forces at rest with the initial velocities, then the half increment.
let crit = internal_forces(&sys, &u, &v0, &bulk, &mut ws);
let start = Motion { u: &u, du: &du, v: &v0, dt: 0.0, time: 0.0, bulk: &bulk };
let crit = internal_forces(&sys, &start, &mut state, &mut ws)?;
let mut dt_half = (scale * crit).min(end);
let initial_increment = dt_half;
let mut v_half = vec![0.0; n];
@@ -222,17 +226,28 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res
history.displacement.push(pick(u));
history.velocity.push(pick(v));
};
let frame = |t: f64, u: &[f64], v: &[f64], ws: &Workspace| Frame {
time: t,
displacement: if output.displacement { per_node(u) } else { Vec::new() },
velocity: if output.displacement { per_node(v) } else { Vec::new() },
stress: if output.stress { ws.stress.clone() } else { Vec::new() },
strain: if output.strain { ws.strain.clone() } else { Vec::new() },
let frame = |t: f64, u: &[f64], v: &[f64], state: &State| {
let each = |on: bool, f: &dyn Fn(&crate::elements::ElementState) -> f64| -> Vec<f64> {
if on { state.elements.iter().map(f).collect() } else { Vec::new() }
};
let inelastic = output.stress && inelastic;
Frame {
time: t,
displacement: if output.displacement { per_node(u) } else { Vec::new() },
velocity: if output.displacement { per_node(v) } else { Vec::new() },
stress: if output.stress { state.elements.iter().map(|e| e.point.stress).collect() } else { Vec::new() },
strain: if output.strain { state.elements.iter().map(|e| e.strain).collect() } else { Vec::new() },
plastic_strain: each(inelastic, &|e| e.point.plastic_strain),
damage: each(inelastic, &|e| e.point.damage),
temperature: each(inelastic, &|e| e.point.temperature),
failed: if output.stress { state.elements.iter().map(|e| e.failed.is_some()).collect() } else { Vec::new() },
}
};
let plastic = |state: &State| state.elements.iter().map(|e| e.plastic_work).sum::<f64>();
let mut histories = Schedule { interval: settings.history_interval(), next: settings.history_interval() };
let mut frames_due = Schedule { interval: settings.output_interval(), next: settings.output_interval() };
sample(&mut history, 0.0, dt_half, energy, &u, &v0, &reaction);
out_frames.push(frame(0.0, &u, &v0, &ws));
out_frames.push(frame(0.0, &u, &v0, &state));
let mut t = 0.0;
let mut increments = 0u64;
@@ -253,7 +268,14 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res
t += dt_half;
increments += 1;
std::mem::swap(&mut ws, &mut previous);
let crit = internal_forces(&sys, &u, &v_half, &bulk, &mut ws);
let motion = Motion { u: &u, du: &du, v: &v_half, dt: dt_half, time: t, bulk: &bulk };
let crit = internal_forces(&sys, &motion, &mut state, &mut ws)?;
for &(index, failure) in &state.eroded {
let element = state.elements.get(index);
let internal = element.map_or(0.0, |e| e.energy);
energy.eroded += internal;
eroded.push(ErodedElement { label: sys.elements.get(index).copied().unwrap_or(0), time: t, failure, internal_energy: internal });
}
let remaining = end - t;
let last = remaining <= 1e-12 * end;
let dt_next = if last { 0.0 } else { (scale * crit).min(remaining) };
@@ -269,8 +291,8 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res
let [mut internal, mut hourglass, mut viscous, mut external, mut constraint, mut kinetic] = [0.0; 6];
let forces = ws.stress_force.iter().zip(&ws.hourglass_force).zip(&ws.viscous_force);
let before = previous.stress_force.iter().zip(&previous.hourglass_force).zip(&previous.viscous_force);
let state = v_now.iter_mut().zip(&v_half).zip(&v_new).zip(&mass).zip(&du).zip(&sys.external).zip(&reaction);
for ((((((((now, vh), vn), m), dx), e), r), ((s, h), q)), ((s0, h0), q0)) in state.zip(forces).zip(before) {
let per_dof = v_now.iter_mut().zip(&v_half).zip(&v_new).zip(&mass).zip(&du).zip(&sys.external).zip(&reaction);
for ((((((((now, vh), vn), m), dx), e), r), ((s, h), q)), ((s0, h0), q0)) in per_dof.zip(forces).zip(before) {
*now = vh + weight * (vn - vh);
internal += dx * (s0 + s);
hourglass += dx * (h0 + h);
@@ -287,14 +309,16 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res
energy.kinetic = 0.5 * kinetic;
peak_internal = peak_internal.max(energy.internal.abs());
energy.balance = energy.kinetic + energy.internal + energy.hourglass + energy.viscous - energy.external - energy.constraint - initial_kinetic;
if !(energy.balance.is_finite() && crit.is_finite()) {
// Every element eroded leaves an infinite critical increment: the nodes fly freely.
if !(energy.balance.is_finite() && !crit.is_nan()) {
return Err(Error::Unstable(format!("the solution is not finite at time {t:.6e} (increment {increments})")));
}
if histories.due(t) || last {
energy.plastic = plastic(&state);
sample(&mut history, t, dt_half, energy, &u, &v_now, &reaction);
}
if frames_due.due(t) || last {
out_frames.push(frame(t, &u, &v_now, &ws));
out_frames.push(frame(t, &u, &v_now, &state));
}
std::mem::swap(&mut v_half, &mut v_new);
dt_half = dt_next;
@@ -307,9 +331,17 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res
sample(&mut history, t, dt_half, energy, &u, &v_now, &reaction);
}
if out_frames.last().map(|f| f.time) != Some(t) {
out_frames.push(frame(t, &u, &v_now, &ws));
out_frames.push(frame(t, &u, &v_now, &state));
}
}
energy.plastic = plastic(&state);
if !eroded.is_empty() {
warnings.push(format!(
"{} elements eroded (their material failed); their internal energy at erosion was {:.4e}",
eroded.len(),
energy.eroded
));
}
let error = energy.relative_error(initial_kinetic);
if error > BALANCE_TOLERANCE {
@@ -338,6 +370,7 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res
total_mass,
energy,
initial_kinetic,
eroded,
warnings,
})
}