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

+412 -111
View File
@@ -1,24 +1,41 @@
//! Element internal forces and critical time increments.
//! Element internal forces, the material update and critical time increments.
//!
//! **Kinematics.** With nonlinear geometry the elements are updated Lagrangian (T. Belytschko,
//! W. K. Liu, B. Moran, K. Elkhodary, *Nonlinear Finite Elements for Continua and Structures*,
//! 2nd ed., Wiley 2014, ch. 4 and 6): the strain and spin increments of an increment come from
//! the displacement increment Δu and the gradients of the **midpoint** configuration
//! x_{n−½} = X + u_n − ½Δu, ΔG = (1/V) Σ_I Δu_I ⊗ b_I(x_{n−½}), which makes the update
//! incrementally objective (T. J. R. Hughes, J. Winget, IJNME 15(12):1862–1867, 1980); the nodal
//! forces of the Cauchy stress use the gradients of the current configuration,
//! f_iI = σ_ij b_jI(x_n). Without it, both are the gradients of the undeformed mesh and the
//! update is small-strain elasticity (and plasticity).
//!
//! **Stress update** in rate form: the material state (stress, back stress, the accumulated
//! strain and the stiffness-form hourglass forces) is rotated with the increment's rotation —
//! the Cayley transform of the spin increment (Jaumann rate, Hughes & Winget 1980) or the
//! increment of the polar rotation (Green–Naghdi rate, D. P. Flanagan, L. M. Taylor, CMAME
//! 62(3):305–320, 1987), see [`modelcraft_fem::rotation`] — then the constitutive increment is
//! added: hypoelastic, or J2 radial return with Johnson–Cook damage
//! ([`modelcraft_fem::plasticity`]). Orthotropic elasticity is updated in the polar frame (its
//! axes turn with the material) whichever rate the step names.
//!
//! **One-point hexahedron** (D. P. Flanagan, T. Belytschko, IJNME 17(5):679–706, 1981): the
//! stress of the mean strain gives the forces f_iI = σ_ij b_jI ([`modelcraft_fem::uniform`]).
//! The hourglass modes are resisted through the shape vectors γ_α: with the hourglass rates
//! q̇_iα = Σ_I v_iI γ_αI (or displacements q_iα = Σ_I u_iI γ_αI for the stiffness form), the
//! generalized forces Q_iα = C q̇_iα (viscous) or k q_iα (stiffness) give the nodal forces
//! f_iI = Σ_α Q_iα γ_αI. Because the γ_α are orthogonal to linear fields, these forces do no work
//! in a constant strain state. The coefficients are scaled from the dimensionless hourglass
//! coefficient κ with the element's size h = V / √(2 λ_max(Σ b bᵀ)) (the edge, for a cube),
//! density ρ and wave speed c: C = κ ρ c h² / 16 damps a cube's hourglass modes at a fraction κ
//! of critical at the frequency 2c/h, and k = κ ρ c² h / 16 puts their frequency at √κ · 2c/h.
//! (These normalizations are ours; the paper leaves the scale of the coefficient to the user.)
//! hourglass modes are resisted through the shape vectors γ_α of the current configuration:
//! with the hourglass rates q̇_iα = Σ_I v_iI γ_αI, the generalized forces are Q_iα = C q̇_iα
//! (viscous) or accumulate as Q_iα += k Σ_I Δu_iI γ_αI (stiffness, rotated with the element),
//! and give the nodal forces f_iI = Σ_α Q_iα γ_αI. Because the γ_α are orthogonal to linear
//! fields, these forces do no work in a constant strain state. The coefficients are scaled from
//! the dimensionless hourglass coefficient κ with the element's current size
//! h = V / √(2 λ_max(Σ b bᵀ)) (the edge, for a cube), density ρ and wave speed c:
//! C = κ ρ c h² / 16 damps a cube's hourglass modes at a fraction κ of critical at the frequency
//! 2c/h, and k = κ ρ c² h / 16 puts their frequency at √κ · 2c/h. (These normalizations are ours;
//! the paper leaves the scale of the coefficient to the user.)
//!
//! **Average nodal pressure tetrahedron** (J. Bonet, A. J. Burton, *A simple average nodal
//! pressure tetrahedral element for incompressible and nearly incompressible dynamic explicit
//! applications*, Commun. Numer. Methods Eng. 14(5):437–449, 1998): the volumetric strain of each
//! node is the volume-weighted mean of its tetrahedra's, and each tetrahedron takes the mean of
//! its nodes' as its own: σ = 2μ dev ε + K θ̄ I. The forces are those of the strain energy
//! Σ_e V_e μ ‖dev ε_e‖² + ½ K Σ_a V_a θ_a², so the element conserves energy, and it does not lock
//! when the material is nearly incompressible.
//! **Average nodal pressure tetrahedron** (J. Bonet, A. J. Burton, Commun. Numer. Methods Eng.
//! 14(5):437–449, 1998), in rate form: the volumetric strain increment of each node is the
//! volume-weighted mean of its tetrahedra's, and each tetrahedron takes the mean of its nodes' as
//! its own, so its pressure increment is K times that mean while the deviator is its own. It does
//! not lock when the material is nearly incompressible or flows plastically.
//!
//! **Bulk viscosity** (J. von Neumann, R. D. Richtmyer, J. Appl. Phys. 21(3):232–237, 1950; M. L.
//! Wilkins, J. Comput. Phys. 36(3):281–303, 1980): in compression (volumetric strain rate
@@ -27,60 +44,118 @@
//! Δt = L / (Q + √(Q² + c²)), Q = C_l c + C_q L |ε̇ᵥ| (Wilkins 1980).
//!
//! **Critical increment.** Each element's stable length L = 2c/ω_max comes from the bound on its
//! highest frequency ([`modelcraft_fem::uniform::UniformStrain::frequency_bound`], plus the
//! hourglass stiffness when it is used), so without bulk viscosity its critical increment is
//! 2/ω_max (T. Belytschko, W. K. Liu, B. Moran, K. Elkhodary, 2014, §6.3; the element eigenvalue
//! inequality makes the smallest of them stable for the mesh). For the nodal-pressure
//! tetrahedron the averaging only lowers the volumetric energy (the mean of squares bounds the
//! square of the mean), so the plain tetrahedron's bound holds.
//! highest frequency in its current shape ([`modelcraft_fem::uniform`], with the element's lumped
//! masses, plus the hourglass stiffness when it is used), so without bulk viscosity its critical
//! increment is 2/ω_max (Belytschko et al. 2014, §6.3). The elastic moduli bound the tangent
//! moduli of plastic flow, so the bound holds for the inelastic materials too.
//!
//! **Erosion** (G. R. Johnson, R. A. Stryk, Int. J. Impact Eng. 5:411–421, 1987; T. Belytschko,
//! J. I. Lin, ibid. 5:111–127, 1987): an element whose material point fails (damage 1) or
//! reaches its material's erosion plastic strain is removed: it carries no stress from then on,
//! its nodes keep their mass and momentum, and its internal energy at that moment is reported.
use modelcraft_fem::material::stress;
use modelcraft_fem::uniform::UniformStrain;
use modelcraft_model::{BulkViscosity, HourglassKind};
use modelcraft_fem::plasticity::{Increment, PointState};
use modelcraft_fem::rotation::{self, IDENTITY, Mat3, Polar};
use modelcraft_fem::uniform::{Hex8, Tet4, UniformStrain};
use modelcraft_model::{BulkViscosity, HourglassKind, StressRate};
use crate::system::{Material, NODE_DOFS, System};
use crate::{Error, Result};
/// Scratch vectors and the element state of the latest evaluation.
/// Scratch vectors of one evaluation.
#[derive(Debug, Clone)]
pub struct Workspace {
/// Internal forces per degree of freedom, by origin.
pub stress_force: Vec<f64>,
pub hourglass_force: Vec<f64>,
pub viscous_force: Vec<f64>,
/// Per element (in [`System::elements`] order): the material stress and the strain.
pub stress: Vec<[f64; 6]>,
pub strain: Vec<[f64; 6]>,
/// Per node: the mean volumetric strain of its tetrahedra (a volume-weighted sum until it is
/// divided by `tet_volume`).
/// Per node: the volume-weighted volumetric strain increments of its tetrahedra, and their
/// volume.
theta: Vec<f64>,
tet_volume: Vec<f64>,
/// Per tetrahedron (in [`System::tets`] order): its increment between the two passes.
tet_increment: Vec<Option<TetIncrement>>,
}
impl Workspace {
pub fn new(sys: &System) -> Workspace {
let n = sys.labels.len();
let mut tet_volume = vec![0.0; n];
for t in &sys.tets {
for node in t.nodes {
if let Some(v) = tet_volume.get_mut(node) {
*v += t.kinematics.volume;
}
}
}
let dofs = sys.dofs();
let elements = sys.elements.len();
Workspace {
stress_force: vec![0.0; dofs],
hourglass_force: vec![0.0; dofs],
viscous_force: vec![0.0; dofs],
stress: vec![[0.0; 6]; elements],
strain: vec![[0.0; 6]; elements],
theta: vec![0.0; n],
tet_volume,
tet_volume: vec![0.0; n],
tet_increment: vec![None; sys.tets.len()],
}
}
}
/// Why an element was eroded.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Failure {
/// Its damage reached 1.
Damage,
/// Its equivalent plastic strain reached the material's erosion strain.
PlasticStrain,
}
/// The state of one element (its single material point).
#[derive(Debug, Clone)]
pub struct ElementState {
pub point: PointState,
/// The accumulated (rotated) strain increments: engineering shears.
pub strain: [f64; 6],
/// The polar rotation and stretch (Green–Naghdi rate and orthotropic materials).
pub polar: Polar,
/// Stiffness-form hourglass generalized forces Q_α (one-point hexahedra).
pub hourglass: [[f64; 3]; 4],
/// Work of the stress on the element (its internal energy, hourglass apart).
pub energy: f64,
/// Plastic work on the element.
pub plastic_work: f64,
pub failed: Option<Failure>,
}
/// The element states and the erosions of a run.
#[derive(Debug, Clone)]
pub struct State {
/// Per element, in [`System::elements`] order.
pub elements: Vec<ElementState>,
/// Elements eroded during the latest evaluation: (index, failure).
pub eroded: Vec<(usize, Failure)>,
}
impl State {
pub fn new(sys: &System) -> State {
let mut material = vec![usize::MAX; sys.elements.len()];
for (index, m) in sys.hexes.iter().map(|h| (h.index, h.material)).chain(sys.tets.iter().map(|t| (t.index, t.material))) {
if let Some(slot) = material.get_mut(index) {
*slot = m;
}
}
let elements = material
.into_iter()
.map(|m| {
let temperature = sys.materials.get(m).and_then(|m| m.j2.as_ref()).map_or(0.0, |j| j.initial_temperature());
ElementState {
point: PointState::new(temperature),
strain: [0.0; 6],
polar: Polar::default(),
hourglass: [[0.0; 3]; 4],
energy: 0.0,
plastic_work: 0.0,
failed: None,
}
})
.collect();
State { elements, eroded: Vec::new() }
}
}
pub(crate) fn gather<const N: usize>(x: &[f64], nodes: &[usize; N]) -> [[f64; 3]; N] {
nodes.map(|n| [0, 1, 2].map(|k| x.get(NODE_DOFS * n + k).copied().unwrap_or(0.0)))
}
@@ -112,102 +187,328 @@ pub fn critical_increment(rate: f64, length: f64, wave_speed: f64, b: &BulkVisco
length / (q + (q * q + wave_speed * wave_speed).sqrt())
}
/// What the hexahedron and the tetrahedron share once their stress is known: the stress and bulk
/// viscosity forces, the element state and the critical increment.
struct Finish<'a, const N: usize> {
kinematics: &'a UniformStrain<N>,
nodes: &'a [usize; N],
index: usize,
length: f64,
/// The largest absolute row sum of the hourglass vectors' Gram matrix Σ_I γ_αI γ_βI / m_I (an
/// upper bound of its largest eigenvalue, exact for parallelepipeds where the γ_α are
/// orthogonal).
pub fn hourglass_gram_bound(gamma: &[[f64; 8]; 4], mass: &[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(mass).map(|((x, y), m)| x * y / m).sum::<f64>().abs()).sum();
worst = worst.max(row);
}
worst
}
impl<const N: usize> Finish<'_, N> {
fn apply(&self, ws: &mut Workspace, mat: &Material, v: &[f64], sigma: [f64; 6], strain: [f64; 6], bulk: &BulkViscosity) -> f64 {
let k = self.kinematics;
let rate = k.divergence(&gather(v, self.nodes));
let q = bulk_pressure(rate, mat.density, self.length, mat.wave_speed, bulk);
scatter(&mut ws.stress_force, self.nodes, &k.forces(&sigma));
if q != 0.0 {
scatter(&mut ws.viscous_force, self.nodes, &k.pressure_forces(q));
}
if let Some(s) = ws.stress.get_mut(self.index) {
*s = sigma;
}
if let Some(s) = ws.strain.get_mut(self.index) {
*s = strain;
}
critical_increment(rate, self.length, mat.wave_speed, bulk)
/// What one evaluation of the internal forces is given.
pub struct Motion<'a> {
/// Displacements u^n.
pub u: &'a [f64],
/// The displacement increment that led to `u`, Δt^{n−½} v^{n−½}.
pub du: &'a [f64],
/// The velocity v^{n−½}.
pub v: &'a [f64],
/// The increment's length Δt^{n−½} (0 at the start).
pub dt: f64,
pub time: f64,
pub bulk: &'a BulkViscosity,
}
/// The element's nodal values: rest positions, displacements, displacement increments and
/// velocities, gathered once per evaluation.
struct Nodal<const N: usize> {
x: [[f64; 3]; N],
u: [[f64; 3]; N],
du: [[f64; 3]; N],
v: [[f64; 3]; N],
}
impl<const N: usize> Nodal<N> {
fn gather(sys: &System, nodes: &[usize; N], m: &Motion) -> Nodal<N> {
Nodal { x: nodes.map(|n| sys.xyz.get(n).copied().unwrap_or_default()), u: gather(m.u, nodes), du: gather(m.du, nodes), v: gather(m.v, nodes) }
}
/// The positions X + u − s·Δu.
fn positions(&self, s: f64) -> [[f64; 3]; N] {
std::array::from_fn(|i| [0, 1, 2].map(|k| self.x[i][k] + self.u[i][k] - s * self.du[i][k]))
}
}
/// Evaluate the internal forces at displacements `u` and velocities `v` into `ws` and return the
/// smallest critical increment of the elements.
pub fn internal_forces(sys: &System, u: &[f64], v: &[f64], bulk: &BulkViscosity, ws: &mut Workspace) -> f64 {
fn inverted(label: u32, time: f64) -> Error {
Error::Unstable(format!("element {label} turned inside out at time {time:.6e} (its volume is no longer positive)"))
}
/// The rotation of an increment for an element's state, advancing its polar decomposition when
/// it is tracked.
fn rotation_of(sys: &System, mat: &Material, es: &mut ElementState, g: &Mat3, label: u32, time: f64) -> Result<Mat3> {
if !sys.nonlinear {
return Ok(IDENTITY);
}
if sys.stress_rate == StressRate::GreenNaghdi || mat.j2.is_none() {
es.polar.advance(g).ok_or_else(|| inverted(label, time))
} else {
let (_, w) = rotation::split(g);
rotation::cayley(&w).ok_or_else(|| inverted(label, time))
}
}
/// Rotate an element's state with `q`, then add the constitutive increment for the strain
/// increment `d` (engineering shears) with volumetric part `vol` (the tetrahedron's averaged
/// one; the trace of `d` for the hexahedron), track its energy at the midpoint volume `volume`,
/// and check for erosion.
#[allow(clippy::too_many_arguments)]
fn update(mat: &Material, es: &mut ElementState, q: &Mat3, d: &[f64; 6], vol: f64, dt: f64, density: f64, volume: f64) {
let p = &mut es.point;
if *q != IDENTITY {
// Only the parts of the state that are in use (a back stress with kinematic hardening,
// hourglass forces with stiffness-form control).
p.stress = rotation::rotate(q, &p.stress);
if p.back != [0.0; 6] {
p.back = rotation::rotate(q, &p.back);
}
es.strain = rotation::rotate_strain(q, &es.strain);
if es.hourglass != [[0.0; 3]; 4] {
for h in &mut es.hourglass {
*h = rotation::apply(q, h);
}
}
}
let before = p.stress;
let inc = match &mat.j2 {
Some(j2) => j2.update(p, d, vol, dt, density),
None => {
// Orthotropic: in the polar frame, σ̂ += D d̂.
let r = es.polar.rotation;
if r == IDENTITY {
for (s, x) in p.stress.iter_mut().zip(stress(&mat.elasticity, d)) {
*s += x;
}
} else {
let local = rotation::rotate_strain(&rotation::transpose(&r), d);
for (s, x) in p.stress.iter_mut().zip(rotation::rotate(&r, &stress(&mat.elasticity, &local))) {
*s += x;
}
}
Increment::default()
}
};
// Stress work over the increment, ½(σ_n + σ_{n+1}) : Δε, with the volumetric strain the
// stress took.
let shift = (d[0] + d[1] + d[2] - vol) / 3.0;
let mut work = 0.0;
for k in 0..6 {
let dk = if k < 3 { d[k] - shift } else { d[k] };
work += 0.5 * (before[k] + p.stress[k]) * dk;
es.strain[k] += dk;
}
es.energy += work * volume;
es.plastic_work += inc.plastic_work * volume;
if es.failed.is_none() {
if p.damage >= 1.0 {
es.failed = Some(Failure::Damage);
} else if mat.erosion.is_some_and(|e| p.plastic_strain >= e) {
es.failed = Some(Failure::PlasticStrain);
}
}
}
/// The density and dilatational wave speed of an element of rest volume `rest`, now of volume
/// `volume`.
fn current(mat: &Material, rest: f64, volume: f64) -> (f64, f64) {
let rho = mat.density * rest / volume;
(rho, (mat.stiffest / rho).sqrt())
}
/// The bound ω² ≤ k λ_max(Σ b bᵀ) / (V m_min) on an element's highest frequency
/// ([`modelcraft_fem::uniform`]), with the lumped masses of the undeformed element and
/// `gradient_norm` = λ_max(Σ b bᵀ).
pub fn frequency_bound<const N: usize>(k: &UniformStrain<N>, gradient_norm: f64, modulus: f64, mass: &[f64; N]) -> f64 {
let lightest = mass.iter().copied().fold(f64::INFINITY, f64::min);
modulus * gradient_norm / (k.volume * lightest)
}
/// A hexahedron's size h, hourglass coefficient (damping or stiffness) and frequency bound ω²
/// (with the hourglass stiffness), at density `density`.
#[derive(Debug, Clone, Copy)]
pub struct HexScales {
pub size: f64,
pub coefficient: f64,
pub omega2: f64,
}
/// The [`HexScales`] of a hexahedron in configuration `k` with shape vectors `gamma`.
pub fn hex_scales(
k: &Hex8,
gamma: &[[f64; 8]; 4],
mass: &[f64; 8],
mat: &Material,
density: f64,
hourglass: modelcraft_model::Hourglass,
) -> HexScales {
let norm = k.gradient_norm();
// h: the edge of the cube with the element's volume and gradient norm.
let size = k.volume / (2.0 * norm).sqrt();
let wave2 = mat.stiffest / density;
let mut omega2 = frequency_bound(k, norm, mat.modulus, mass);
let kappa = hourglass.coefficient;
let coefficient = match hourglass.kind {
HourglassKind::Viscous => kappa * density * wave2.sqrt() * size * size / 16.0,
HourglassKind::Stiffness => {
let stiffness = kappa * density * wave2 * size / 16.0;
omega2 += stiffness * hourglass_gram_bound(gamma, mass);
stiffness
}
};
HexScales { size, coefficient, omega2 }
}
/// Evaluate the internal forces at the motion `m` into `ws`, updating the element states, and
/// return the smallest critical increment of the elements still in the analysis.
pub fn internal_forces(sys: &System, m: &Motion, state: &mut State, ws: &mut Workspace) -> Result<f64> {
for f in [&mut ws.stress_force, &mut ws.hourglass_force, &mut ws.viscous_force] {
f.fill(0.0);
}
state.eroded.clear();
let mut critical = f64::INFINITY;
let hourglass = modelcraft_model::Hourglass { kind: sys.hourglass, coefficient: sys.hourglass_coefficient };
for h in &sys.hexes {
let Some(mat) = sys.materials.get(h.material) else { continue };
let ue = gather(u, &h.nodes);
let strain = h.kinematics.strain(&ue);
let sigma = stress(&mat.elasticity, &strain);
// Hourglass forces on the displacement (stiffness) or velocity (viscous) modes.
let modes = match sys.hourglass {
HourglassKind::Viscous => gather(v, &h.nodes),
HourglassKind::Stiffness => ue,
let (Some(mat), Some(es)) = (sys.materials.get(h.material), state.elements.get_mut(h.index)) else { continue };
if es.failed.is_some() {
continue;
}
let e = Nodal::gather(sys, &h.nodes, m);
let x_now = e.positions(0.0);
let (mid, now) = if sys.nonlinear {
let at = |x: &[[f64; 3]; 8]| Hex8::hex8_deformed(x).map_err(|_| inverted(h.label, m.time));
(at(&e.positions(0.5))?, at(&x_now)?)
} else {
(h.kinematics, h.kinematics)
};
let g = mid.gradient(&e.du);
let q = rotation_of(sys, mat, es, &g, h.label, m.time)?;
let (d, _) = rotation::split(&g);
let (rho, c) = current(mat, h.kinematics.volume, now.volume);
update(mat, es, &q, &d, d[0] + d[1] + d[2], m.dt, rho, mid.volume);
if let Some(failure) = es.failed {
state.eroded.push((h.index, failure));
continue;
}
// Hourglass forces with the current shape vectors and size (fixed without nonlinear
// geometry).
let (gamma, scales) = if sys.nonlinear {
let gamma = now.hourglass_vectors(&x_now);
let scales = hex_scales(&now, &gamma, &h.mass, mat, rho, hourglass);
(gamma, scales)
} else {
(h.gamma, h.rest)
};
let mut f = [[0.0; 3]; 8];
for g in &h.gamma {
let mut generalized = [0.0; 3];
for (x, gk) in modes.iter().zip(g) {
for i in 0..3 {
generalized[i] += x[i] * gk;
let modes = if sys.hourglass == HourglassKind::Viscous { &e.v } else { &e.du };
for (k, g) in gamma.iter().enumerate() {
let rate: [f64; 3] = [0, 1, 2].map(|i| modes.iter().zip(g).map(|(x, gk)| x[i] * gk).sum());
// Viscous: Q = C q̇; stiffness: Q accumulates k Δq.
let generalized = match (sys.hourglass, es.hourglass.get_mut(k)) {
(HourglassKind::Stiffness, Some(qa)) => {
for i in 0..3 {
qa[i] += scales.coefficient * rate[i];
}
*qa
}
}
_ => rate.map(|r| scales.coefficient * r),
};
for (fk, gk) in f.iter_mut().zip(g) {
for i in 0..3 {
fk[i] += h.hourglass * generalized[i] * gk;
fk[i] += generalized[i] * gk;
}
}
}
scatter(&mut ws.hourglass_force, &h.nodes, &f);
let finish = Finish { kinematics: &h.kinematics, nodes: &h.nodes, index: h.index, length: h.length };
critical = critical.min(finish.apply(ws, mat, v, sigma, strain, bulk));
let element = Element { now: &now, mid: &mid, nodes: &h.nodes, density: rho, wave_speed: c, length: 2.0 * c / scales.omega2.sqrt() };
critical = critical.min(element.finish(ws, &es.point.stress, &e.v, m.bulk));
}
if !sys.tets.is_empty() {
// Pass 1: each tetrahedron's strain, and each node's mean volumetric strain.
// Pass 1: each tetrahedron's increment, and each node's mean volumetric increment.
ws.theta.fill(0.0);
for t in &sys.tets {
let strain = t.kinematics.strain(&gather(u, &t.nodes));
ws.tet_volume.fill(0.0);
for (k, t) in sys.tets.iter().enumerate() {
let Some(slot) = ws.tet_increment.get_mut(k) else { continue };
*slot = None;
if state.elements.get(t.index).is_none_or(|e| e.failed.is_some()) {
continue;
}
let e = Nodal::gather(sys, &t.nodes, m);
let (mid, now) = if sys.nonlinear {
let at = |x: &[[f64; 3]; 4]| Tet4::tet4(x).map_err(|_| inverted(t.label, m.time));
(at(&e.positions(0.5))?, at(&e.positions(0.0))?)
} else {
(t.kinematics, t.kinematics)
};
let g = mid.gradient(&e.du);
for node in t.nodes {
if let Some(x) = ws.theta.get_mut(node) {
*x += t.kinematics.volume * (strain[0] + strain[1] + strain[2]);
if let (Some(x), Some(w)) = (ws.theta.get_mut(node), ws.tet_volume.get_mut(node)) {
*x += mid.volume * (g[0][0] + g[1][1] + g[2][2]);
*w += mid.volume;
}
}
if let Some(s) = ws.strain.get_mut(t.index) {
*s = strain;
*slot = Some(TetIncrement { g, mid, now, v: e.v });
}
for (x, w) in ws.theta.iter_mut().zip(&ws.tet_volume) {
if *w > 0.0 {
*x /= w;
}
}
for (x, v) in ws.theta.iter_mut().zip(&ws.tet_volume) {
if *v > 0.0 {
*x /= v;
}
}
// Pass 2: each tetrahedron's pressure from the mean of its nodes' volumetric strains.
for t in &sys.tets {
let Some(mat) = sys.materials.get(t.material) else { continue };
let strain = ws.strain.get(t.index).copied().unwrap_or_default();
// Pass 2: each tetrahedron's update with the mean of its nodes' volumetric increments.
for (k, t) in sys.tets.iter().enumerate() {
let (Some(mat), Some(es), Some(Some(inc))) =
(sys.materials.get(t.material), state.elements.get_mut(t.index), ws.tet_increment.get(k).copied())
else {
continue;
};
let averaged = t.nodes.iter().map(|n| ws.theta.get(*n).copied().unwrap_or(0.0)).sum::<f64>() / 4.0;
let own = strain[0] + strain[1] + strain[2];
let mut sigma = stress(&mat.elasticity, &strain);
let shift = mat.bulk.unwrap_or(0.0) * (averaged - own);
for s in sigma.iter_mut().take(3) {
*s += shift;
let q = rotation_of(sys, mat, es, &inc.g, t.label, m.time)?;
let (d, _) = rotation::split(&inc.g);
let (rho, c) = current(mat, t.kinematics.volume, inc.now.volume);
update(mat, es, &q, &d, averaged, m.dt, rho, inc.mid.volume);
if let Some(failure) = es.failed {
state.eroded.push((t.index, failure));
continue;
}
let finish = Finish { kinematics: &t.kinematics, nodes: &t.nodes, index: t.index, length: t.length };
critical = critical.min(finish.apply(ws, mat, v, sigma, strain, bulk));
let omega2 = if sys.nonlinear { frequency_bound(&inc.now, inc.now.gradient_norm(), mat.modulus, &t.mass) } else { t.omega2 };
let element = Element { now: &inc.now, mid: &inc.mid, nodes: &t.nodes, density: rho, wave_speed: c, length: 2.0 * c / omega2.sqrt() };
critical = critical.min(element.finish(ws, &es.point.stress, &inc.v, m.bulk));
}
}
critical
Ok(critical)
}
/// A tetrahedron's increment between the two passes: the gradient of its displacement increment,
/// its midpoint and current kinematics, and its nodal velocities.
#[derive(Debug, Clone, Copy)]
struct TetIncrement {
g: Mat3,
mid: Tet4,
now: Tet4,
v: [[f64; 3]; 4],
}
/// What the hexahedron and the tetrahedron share once their stress is known.
struct Element<'a, const N: usize> {
now: &'a UniformStrain<N>,
mid: &'a UniformStrain<N>,
nodes: &'a [usize; N],
density: f64,
wave_speed: f64,
length: f64,
}
impl<const N: usize> Element<'_, N> {
/// Add the stress and bulk viscosity forces and return the critical increment.
fn finish(&self, ws: &mut Workspace, sigma: &[f64; 6], v: &[[f64; 3]; N], bulk: &BulkViscosity) -> f64 {
let rate = self.mid.divergence(v);
let q = bulk_pressure(rate, self.density, self.length, self.wave_speed, bulk);
scatter(&mut ws.stress_force, self.nodes, &self.now.forces(sigma));
if q != 0.0 {
scatter(&mut ws.viscous_force, self.nodes, &self.now.pressure_forces(q));
}
critical_increment(rate, self.length, self.wave_speed, bulk)
}
}
+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,
})
}
+8 -5
View File
@@ -17,10 +17,12 @@
//! - **Integration** ([`integrate`]): the time loop, kinematic constraints, energy accounting
//! with a balance check, history samples and field frames ([`result`]).
//!
//! This first version is geometrically linear (small displacements and strains; the elements'
//! gradients are those of the undeformed mesh) with linear elastic materials, which is what the
//! analytic verification problems need. Large deformation kinematics with objective stress rates
//! and inelastic materials come next (docs/solvers/literature.md, Part F, X2).
//! With nonlinear geometry (the default) the elements are updated Lagrangian and the material
//! state is integrated in rate form with an objective stress rate (Jaumann by Hughes–Winget,
//! Green–Naghdi by Flanagan–Taylor); materials are hypoelastic or J2 plastic (bilinear,
//! tabulated, Johnson–Cook) with Johnson–Cook damage and element erosion ([`elements`];
//! docs/solvers/literature.md, Parts F and J). Without it the gradients stay those of the
//! undeformed mesh (small displacements).
//!
//! Everything is sequential, with elements in label order and no parallel reductions, so the
//! same model gives the same numbers bit for bit; sizes, increments and output are capped by
@@ -35,8 +37,9 @@ pub mod integrate;
pub mod result;
pub mod system;
pub use elements::Failure;
pub use integrate::solve_explicit;
pub use result::{ControllingElement, Energies, ExplicitResult, Frame, History};
pub use result::{ControllingElement, Energies, ErodedElement, ExplicitResult, Frame, History};
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum Error {
+27
View File
@@ -3,6 +3,8 @@
use serde::Serialize;
use crate::elements::Failure;
/// The energy account at one time (Belytschko, Liu, Moran, Elkhodary 2014, §6.2.3). The works of
/// the internal and external forces are integrated with the trapezoidal rule over each
/// increment's displacement; the constraints' with their impulses (see [`crate::integrate`]).
@@ -20,6 +22,10 @@ pub struct Energies {
pub external: f64,
/// Work of the boundary conditions and rigid walls on the model.
pub constraint: f64,
/// Plastic work: the part of `internal` dissipated by plastic flow.
pub plastic: f64,
/// Internal energy of the eroded elements when they eroded (part of `internal`).
pub eroded: f64,
/// kinetic + internal + hourglass + viscous − external − constraint − the initial kinetic
/// energy: zero when energy is conserved.
pub balance: f64,
@@ -45,7 +51,26 @@ pub struct Frame {
/// Per element in [`ExplicitResult::elements`] order (one-point elements: one value each):
/// xx, yy, zz, xy, xz, yz, engineering shear strains.
pub stress: Vec<[f64; 6]>,
/// The accumulated strain increments (small strain: the strain; large deformation: the
/// integral of the rate of deformation, rotated with the material).
pub strain: Vec<[f64; 6]>,
/// Per element, with stress output in a model with plasticity: equivalent plastic strain,
/// damage and temperature.
pub plastic_strain: Vec<f64>,
pub damage: Vec<f64>,
pub temperature: Vec<f64>,
/// Per element, with stress output: whether it has eroded.
pub failed: Vec<bool>,
}
/// An element removed from the analysis.
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct ErodedElement {
pub label: u32,
pub time: f64,
pub failure: Failure,
/// Its internal energy (stress work) when it eroded.
pub internal_energy: f64,
}
/// Quantities sampled through the run.
@@ -95,5 +120,7 @@ pub struct ExplicitResult {
/// The energy account at the end.
pub energy: Energies,
pub initial_kinetic: f64,
/// Elements eroded during the run, in the order they eroded.
pub eroded: Vec<ErodedElement>,
pub warnings: Vec<String>,
}
+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;
}
}
}
+482
View File
@@ -0,0 +1,482 @@
//! Large deformation and plasticity in explicit dynamics against closed-form solutions
//! (docs/solvers/literature.md, A4, A6, A10):
//! - simple shear of a hypoelastic material (J. K. Dienes, *On the analysis of rotation and
//! stress rate in deforming bodies*, Acta Mech. 32:217–232, 1979): the Jaumann rate gives the
//! oscillating σ12 = G sin γ, σ11 = −σ22 = G(1 − cos γ), the Green–Naghdi rate the monotonic
//! solution in β = arctan(γ/2);
//! - a stressed element turned rigidly by 90° in many increments: the stress turns with it and
//! keeps its magnitude (objectivity), where the small-strain update does not;
//! - uniaxial stress under large stretch with J2 plasticity, loaded and unloaded: Cauchy stress
//! against the logarithmic strain follows the bilinear flow curve and unloads elastically;
//! - the Taylor bar (G. I. Taylor, Proc. R. Soc. A 194:289–299, 1948): the yield stress Taylor's
//! one-dimensional estimate recovers from the computed final shape.
//!
//! Run with `--nocapture` to see the errors.
use modelcraft_explicit::elements::{Motion, State, Workspace, internal_forces};
use modelcraft_explicit::system::System;
use modelcraft_explicit::{ExplicitResult, Failure, Limits, solve_explicit};
use modelcraft_model::*;
/// A block [0, size] of `n` cells: one hex8 per cell, or six tet4 (Kuhn's split).
fn block(n: [usize; 3], size: [f64; 3], kind: ElementKind) -> Model {
let mut m = Model::default();
let label = |i: usize, j: usize, k: usize| (1 + i + (n[0] + 1) * (j + (n[1] + 1) * k)) as u32;
for k in 0..=n[2] {
for j in 0..=n[1] {
for i in 0..=n[0] {
let x = [i as f64 * size[0] / n[0] as f64, j as f64 * size[1] / n[1] as f64, k as f64 * size[2] / n[2] as f64];
m.set_node(label(i, j, k), x).unwrap();
}
}
}
let corners = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]];
let mut next = 1;
for k in 0..n[2] {
for j in 0..n[1] {
for i in 0..n[0] {
let at = |c: [usize; 3]| label(i + c[0], j + c[1], k + c[2]);
if kind == ElementKind::Hex8 {
m.set_element(next, kind, corners.iter().map(|c| at(*c)).collect()).unwrap();
next += 1;
continue;
}
for perm in [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]] {
let mut v = [[0usize; 3]; 4];
for s in 0..3 {
v[s + 1] = v[s];
v[s + 1][perm[s]] = 1;
}
let p = v.map(|c| [0, 1, 2].map(|d| c[d] as f64 * size[d] / n[d] as f64));
let e = |a: usize| [0, 1, 2].map(|d| p[a][d] - p[0][d]);
let (a, b, c) = (e(1), e(2), e(3));
let vol = a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0]) + a[2] * (b[0] * c[1] - b[1] * c[0]);
if vol < 0.0 {
v.swap(1, 2);
}
m.set_element(next, kind, v.iter().map(|c| at(*c)).collect()).unwrap();
next += 1;
}
}
}
}
let all: Vec<u32> = m.elements().keys().copied().collect();
m.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
m
}
fn node_set(m: &mut Model, name: &str, f: impl Fn([f64; 3]) -> bool) {
let nodes: Vec<u32> = m.nodes().iter().filter(|(_, n)| f(n.xyz)).map(|(l, _)| *l).collect();
m.add_set(name, Set { nodes, ..Set::default() }).unwrap();
}
fn define(m: &mut Model, kind: ElementKind, material: Material, step: ExplicitDynamic) {
m.add_material("Mat", material).unwrap();
let integration = if kind == ElementKind::Hex8 { Integration::Reduced } else { Integration::Full };
let formulation = Formulation { integration, ..Formulation::default() };
m.add_section("solid", Section { set: "all".into(), material: "Mat".into(), kind: SectionKind::Solid {}, formulation }).unwrap();
m.add_step(Step::new("Dynamic", Procedure::ExplicitDynamic(step)), None).unwrap();
}
fn bc(m: &mut Model, name: &str, bc: BoundaryCondition) {
m.add_boundary_condition(name, bc).unwrap();
m.set_boundary_condition_active("Dynamic", name, true).unwrap();
}
fn elastic(young: f64, poisson: f64, density: f64) -> Material {
Material { elastic: Some(Elastic::Isotropic { young, poisson }), density: Some(density), ..Material::default() }
}
/// Dienes' closed forms: (σ11, σ12) / G at shear γ.
fn dienes(rate: StressRate, gamma: f64) -> (f64, f64) {
match rate {
StressRate::Jaumann => (1.0 - gamma.cos(), gamma.sin()),
StressRate::GreenNaghdi => {
let beta = (gamma / 2.0).atan();
let lc = beta.cos().ln();
let s11 = 4.0 * ((2.0 * beta).cos() * lc + beta * (2.0 * beta).sin() - beta.sin().powi(2));
let s12 = 2.0 * (2.0 * beta).cos() * (2.0 * beta - 2.0 * (2.0 * beta).tan() * lc - beta.tan());
(s11, s12)
}
}
}
/// A unit cube sheared to γ = 6 by prescribing every node: x = X + γ(t) Y e_x.
#[test]
fn dienes_simple_shear_with_both_rates() {
let (young, poisson) = (1.0, 0.3);
let g = young / (2.0 * (1.0 + poisson));
let gamma_end = 6.0;
for kind in [ElementKind::Hex8, ElementKind::Tet4] {
for rate in [StressRate::Jaumann, StressRate::GreenNaghdi] {
let mut m = block([1, 1, 1], [1.0; 3], kind);
let end = 4000.0;
let mut step = ExplicitDynamic::new(end);
step.stress_rate = rate;
step.output_interval = Some(end / 60.0);
step.max_increments = 100_000;
define(&mut m, kind, elastic(young, poisson, 1.0), step);
node_set(&mut m, "bottom", |x| x[1] == 0.0);
node_set(&mut m, "top", |x| x[1] == 1.0);
bc(&mut m, "hold", BoundaryCondition::Fixed { target: Target::Set("bottom".into()) });
bc(
&mut m,
"shear",
BoundaryCondition::Displacement {
target: Target::Set("top".into()),
values: [Some(gamma_end), Some(0.0), Some(0.0), None, None, None],
},
);
let r = solve_explicit(&m, None, &Limits::default()).unwrap();
assert!(r.completed);
let mut worst: f64 = 0.0;
for f in &r.frames {
let gamma = gamma_end * f.time / end;
let (s11, s12) = dienes(rate, gamma);
for s in &f.stress {
let e = [(s[0] / g - s11).abs(), (s[1] / g + s11).abs(), (s[3] / g - s12).abs(), s[2].abs() / g, s[4].abs() / g, s[5].abs() / g];
worst = worst.max(e.into_iter().fold(0.0, f64::max));
}
}
let last = r.frames.last().unwrap();
eprintln!(
"Dienes {} {rate:?}: σ/G at γ = 6: ({:.5}, {:.5}) vs ({:.5}, {:.5}); largest error over the run {worst:.2e} G; {} increments",
kind.name(),
last.stress[0][0] / g,
last.stress[0][3] / g,
dienes(rate, gamma_end).0,
dienes(rate, gamma_end).1,
r.increments
);
assert!(worst < 2e-3, "{} {rate:?}: {worst}", kind.name());
}
}
}
/// Drive one element through a sequence of nodal displacements with the solver's own element
/// update, returning its stress after each.
struct Driver {
sys: System,
state: State,
ws: Workspace,
u: Vec<f64>,
time: f64,
}
impl Driver {
fn new(model: &Model) -> Driver {
let resolved = model.resolve().unwrap();
let step = model.step("Dynamic").unwrap();
let Procedure::ExplicitDynamic(settings) = &step.procedure else { panic!() };
let sys = System::new(model, &resolved, step, settings, 1000).unwrap();
let (state, ws) = (State::new(&sys), Workspace::new(&sys));
let u = vec![0.0; sys.dofs()];
Driver { sys, state, ws, u, time: 0.0 }
}
/// Move the nodes to `x(X)` over an increment of length `dt`.
fn go(&mut self, to: impl Fn([f64; 3]) -> [f64; 3], dt: f64) -> [f64; 6] {
self.try_go(to, dt).unwrap()
}
fn try_go(&mut self, to: impl Fn([f64; 3]) -> [f64; 3], dt: f64) -> modelcraft_explicit::Result<[f64; 6]> {
let mut du = vec![0.0; self.u.len()];
for (i, x0) in self.sys.xyz.iter().enumerate() {
let x = to(*x0);
for k in 0..3 {
du[3 * i + k] = x[k] - x0[k] - self.u[3 * i + k];
self.u[3 * i + k] = x[k] - x0[k];
}
}
self.time += dt;
let v: Vec<f64> = du.iter().map(|d| d / dt).collect();
let bulk = BulkViscosity { linear: 0.0, quadratic: 0.0 };
let m = Motion { u: &self.u, du: &du, v: &v, dt, time: self.time, bulk: &bulk };
internal_forces(&self.sys, &m, &mut self.state, &mut self.ws)?;
Ok(self.state.elements[0].point.stress)
}
}
fn rotation_z(angle: f64, x: [f64; 3]) -> [f64; 3] {
let (s, c) = angle.sin_cos();
[c * x[0] - s * x[1], s * x[0] + c * x[1], x[2]]
}
/// Stretch an element 1% along x, then turn it rigidly by 90° about z in 50 increments: the
/// stress turns with it (σ_yy takes σ_xx's value), exactly for both rates, and the internal
/// forces stay those of the turned stress. The small-strain update keeps σ_xx and adds a
/// spurious stress from the rotation's "strain".
#[test]
fn rigid_rotation_turns_the_stress_with_the_body() {
for (rate, nonlinear) in [(StressRate::Jaumann, true), (StressRate::GreenNaghdi, true), (StressRate::Jaumann, false)] {
let mut m = block([1, 1, 1], [1.0; 3], ElementKind::Hex8);
let mut step = ExplicitDynamic::new(1.0);
step.stress_rate = rate;
step.nonlinear_geometry = nonlinear;
define(&mut m, ElementKind::Hex8, elastic(1000.0, 0.3, 1.0), step);
let mut d = Driver::new(&m);
let stretch = |x: [f64; 3]| [1.01 * x[0], x[1], x[2]];
let before = d.go(stretch, 1.0);
let n = 50;
let mut after = before;
for k in 1..=n {
let angle = std::f64::consts::FRAC_PI_2 * k as f64 / n as f64;
after = d.go(|x| rotation_z(angle, stretch(x)), 1.0);
}
// R σ Rᵀ for 90° about z: xx ↔ yy, xy → −xy, xz → yz, yz → −xz.
let want = [before[1], before[0], before[2], -before[3], -before[5], before[4]];
let error = after.iter().zip(want).map(|(a, b)| (a - b).abs()).fold(0.0, f64::max) / before[0].abs();
eprintln!("rigid rotation {rate:?} nonlinear {nonlinear}: σ before {before:.4?}, after {after:.4?}; error {error:.2e} of σ_xx");
if nonlinear {
assert!(error < 1e-12, "{rate:?}: {error}");
// Forces of the turned stress balance on the turned element: ∑ f = 0, ∑ x × f = 0.
let f = &d.ws.stress_force;
let total: f64 = (0..3).map(|k| f.iter().skip(k).step_by(3).sum::<f64>().abs()).sum();
assert!(total < 1e-10 * before[0].abs());
} else {
assert!(error > 0.5, "the small-strain update is not objective: {error}");
}
}
}
/// Uniaxial stress to a logarithmic strain of 0.3 and back by 0.006 (inside the elastic range) in a single hexahedron with
/// bilinear isotropic hardening: the lateral stretch is iterated each increment so the lateral
/// stresses vanish. The Cauchy stress follows σ = σ_y + H ε_p with ln λ = σ/E + ε_p (for a
/// hypoelastic–plastic law under coaxial stretching the integral of D is the logarithmic
/// strain), then unloads with slope E.
#[test]
fn j2_uniaxial_stress_strain_curve_and_unloading_at_large_strain() {
let (young, poisson, y0, h) = (200_000.0, 0.3, 250.0, 2000.0);
for kind in [ElementKind::Hex8, ElementKind::Tet4] {
let mut m = block([1, 1, 1], [1.0; 3], kind);
let plastic = Plastic::Bilinear { yield_stress: y0, hardening_modulus: h, hardening: Hardening::Isotropic };
define(&mut m, kind, Material { plastic: Some(plastic), ..elastic(young, poisson, 7.8e-9) }, ExplicitDynamic::new(1.0));
let mut d = Driver::new(&m);
let mut path: Vec<f64> = (1..=600).map(|k| 0.3 * k as f64 / 600.0).collect();
path.extend((1..=200).map(|k| 0.3 - 0.006 * k as f64 / 200.0));
let (mut lateral, mut worst, mut peak) = (0.0f64, 0.0f64, (0.0, 0.0, 0.0));
let mut last = [0.0; 6];
for (step, axial) in path.iter().enumerate() {
// Secant iteration on the lateral log strain for σ_yy = 0 (σ_zz follows by symmetry).
let saved = (d.state.clone(), d.u.clone(), d.time);
let trial = |lat: f64, d: &mut Driver| {
(d.state, d.u, d.time) = saved.clone();
let (a, l) = (axial.exp(), lat.exp());
d.go(|x| [a * x[0], l * x[1], l * x[2]], 1e-6)
};
let (mut a, mut b) = (lateral, lateral - 1e-4);
let (mut fa, mut fb) = (trial(a, &mut d)[1], trial(b, &mut d)[1]);
for _ in 0..60 {
if fb.abs() < 1e-9 || fb == fa {
break;
}
let c = b - fb * (b - a) / (fb - fa);
(a, fa) = (b, fb);
b = c;
fb = trial(b, &mut d)[1];
}
lateral = b;
let s = trial(b, &mut d);
let p = d.state.elements.iter().map(|e| e.point.plastic_strain).fold(0.0, f64::max);
if step < 600 {
// Loading: σ = y0 + H ε_p and ε = σ/E + ε_p, so σ = y0 + E_t (ε − y0/E).
let et = young * h / (young + h);
let exact = if *axial * young <= y0 { young * axial } else { y0 + et * (axial - y0 / young) };
worst = worst.max((s[0] - exact).abs() / exact);
peak = (*axial, s[0], p);
} else {
// Unloading: elastic from the peak.
let exact = peak.1 - young * (peak.0 - axial);
worst = worst.max((s[0] - exact).abs() / peak.1);
assert!((p - peak.2).abs() < 1e-12, "no plastic flow while unloading");
}
last = s;
}
eprintln!(
"J2 uniaxial {}: peak σ {:.4} at ln λ = 0.3 (exact {:.4}); after unloading to 0.294 σ = {:.4}; largest relative error {worst:.2e}",
kind.name(),
peak.1,
y0 + young * h / (young + h) * (0.3 - y0 / young),
last[0]
);
assert!(worst < 1e-6, "{}: {worst}", kind.name());
}
}
/// The Taylor bar: a copper-like bar of square section hits a rigid wall at 190 m/s (units mm,
/// t, s, MPa) and mushrooms. Taylor's one-dimensional analysis of a rigid–perfectly plastic bar
/// (Taylor 1948) relates the final length L₁ and the length X of the undeformed rear part to the
/// yield stress: σ_y = ρ U² (L − X) / (2 (L − L₁) ln(L / X)). Applied to the computed shape it
/// should give back the yield stress the material has, within the accuracy of the estimate
/// itself (which neglects elastic waves and radial inertia): 25%.
#[test]
fn taylor_bar_recovers_its_yield_stress() {
let (rho, young, poisson, yield_stress) = (8.93e-9, 117_000.0, 0.35, 400.0);
let (length, width, speed) = (25.4, 7.6, 190_000.0);
let end = 80e-6;
let mut step = ExplicitDynamic::new(end);
step.output_interval = Some(end / 4.0);
step.history_interval = Some(end / 400.0);
step.initial_velocities = vec![InitialVelocity { set: "nodes".into(), velocity: [0.0, 0.0, -speed] }];
step.rigid_walls = vec![RigidWall { set: "nodes".into(), point: [0.0; 3], normal: [0.0, 0.0, 1.0] }];
step.hourglass = Hourglass { kind: HourglassKind::Stiffness, coefficient: 0.1 };
let mut m = block([4, 4, 30], [width, width, length], ElementKind::Hex8);
node_set(&mut m, "nodes", |_| true);
let plastic = Plastic::Bilinear { yield_stress, hardening_modulus: 0.0, hardening: Hardening::Isotropic };
define(&mut m, ElementKind::Hex8, Material { plastic: Some(plastic), ..elastic(young, poisson, rho) }, step);
let r = solve_explicit(&m, None, &Limits::default()).unwrap();
let f = r.frames.last().unwrap();
// Final length: the top face's mean height; undeformed part: the axis above the last
// element with plastic strain, along the centre column of elements.
let z = |label: u32| m.node(label).unwrap().xyz[2] + f.displacement[r.nodes.binary_search(&label).unwrap()][2];
let top: Vec<u32> = m.nodes().iter().filter(|(_, n)| n.xyz[2] == length).map(|(l, _)| *l).collect();
let l1 = top.iter().map(|l| z(*l)).sum::<f64>() / top.len() as f64;
let column: Vec<usize> = (0..30).map(|k| 4 * 4 * k + 4 + 1).collect();
// The rear part counts as undeformed from the first element (from the impact end) whose
// plastic strain is below 1%, small against the mushroom's.
let deformed = column.iter().position(|&i| f.plastic_strain[i] < 0.01).unwrap_or(30);
let x = length * (30 - deformed) as f64 / 30.0;
let estimate = rho * speed * speed * (length - x) / (2.0 * (length - l1) * (length / x).ln());
let kinetic = r.energy.kinetic / r.initial_kinetic;
eprintln!(
"Taylor bar: L₁/L = {:.4}, X/L = {:.4}; Taylor's estimate σ_y = {estimate:.1} vs {yield_stress} ({:+.1}%); final KE/KE₀ {kinetic:.4}; plastic work {:.3e} of {:.3e}; balance {:.2e}; {} increments",
l1 / length,
x / length,
100.0 * (estimate / yield_stress - 1.0),
r.energy.plastic,
r.initial_kinetic,
r.energy.relative_error(r.initial_kinetic),
r.increments
);
assert!(r.completed && l1 < 0.95 * length && l1 > 0.5 * length);
assert!((estimate / yield_stress - 1.0).abs() < 0.25, "{estimate}");
assert!(r.energy.relative_error(r.initial_kinetic) < 0.01, "{:?}", r.energy);
assert!(r.energy.plastic > 0.5 * r.initial_kinetic, "most of the impact energy goes into plastic work: {:?}", r.energy);
}
fn taylor_with(material: Material) -> (Model, ExplicitResult) {
let end = 40e-6;
let mut step = ExplicitDynamic::new(end);
step.output_interval = Some(end / 2.0);
step.initial_velocities = vec![InitialVelocity { set: "nodes".into(), velocity: [0.0, 0.0, -300_000.0] }];
step.rigid_walls = vec![RigidWall { set: "nodes".into(), point: [0.0; 3], normal: [0.0, 0.0, 1.0] }];
step.hourglass = Hourglass { kind: HourglassKind::Stiffness, coefficient: 0.1 };
let mut m = block([2, 2, 12], [7.6, 7.6, 25.4], ElementKind::Hex8);
node_set(&mut m, "nodes", |_| true);
define(&mut m, ElementKind::Hex8, material, step);
let r = solve_explicit(&m, None, &Limits::default()).unwrap();
(m, r)
}
/// Johnson–Cook strength, adiabatic heating, damage and erosion in a fast impact: elements at
/// the impact face reach the erosion strain (compression keeps their damage low, as the
/// fracture strain grows with confinement), are removed and reported; the run stays balanced
/// and repeatable bit for bit.
#[test]
fn johnson_cook_impact_erodes_elements() {
// OFHC copper constants as published by Johnson & Cook (1983, 1985), units mm, t, s, MPa, K.
let plastic = 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 damage = Damage::JohnsonCook { d1: 0.54, d2: 4.89, d3: -3.03, d4: 0.014, d5: 1.12 };
let material = Material {
plastic: Some(plastic),
damage: Some(damage),
erosion: Some(Erosion { plastic_strain: 0.8 }),
..elastic(124_000.0, 0.34, 8.96e-9)
};
let (_, r) = taylor_with(material.clone());
let f = r.frames.last().unwrap();
let hottest = f.temperature.iter().copied().fold(0.0, f64::max);
eprintln!(
"Johnson–Cook impact: {} elements eroded ({:?}…), eroded energy {:.3e}; hottest element {hottest:.1} K; largest damage {:.3}; balance {:.2e}",
r.eroded.len(),
r.eroded.first(),
r.energy.eroded,
f.damage.iter().copied().fold(0.0, f64::max),
r.energy.relative_error(r.initial_kinetic)
);
assert!(!r.eroded.is_empty() && r.eroded.iter().all(|e| matches!(e.failure, Failure::Damage | Failure::PlasticStrain)));
assert_eq!(f.failed.iter().filter(|x| **x).count(), r.eroded.len());
assert!(hottest > 294.0 + 10.0);
assert!(r.energy.eroded > 0.0 && r.warnings.iter().any(|w| w.contains("eroded")));
assert!(r.energy.relative_error(r.initial_kinetic) < 0.02, "{:?}", r.energy);
let (_, again) = taylor_with(material);
assert!(r == again, "bit-identical runs");
}
/// Damage with a constant fracture strain D1 (D2 = D4 = D5 = 0): an element stretched in
/// tension fails when its plastic strain reaches D1, and from then on carries no stress.
#[test]
fn damage_fails_an_element_at_its_fracture_strain() {
let mut m = block([1, 1, 1], [1.0; 3], ElementKind::Hex8);
let plastic = Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 1000.0, hardening: Hardening::Isotropic };
let damage = Damage::JohnsonCook { d1: 0.2, d2: 0.0, d3: 0.0, d4: 0.0, d5: 0.0 };
define(
&mut m,
ElementKind::Hex8,
Material { plastic: Some(plastic), damage: Some(damage), ..elastic(200_000.0, 0.3, 7.8e-9) },
ExplicitDynamic::new(1.0),
);
let mut d = Driver::new(&m);
let mut failed_at = None;
for k in 1..=1000 {
let stretch = 1.0 + 0.5 * k as f64 / 1000.0;
let before = d.state.elements[0].point.plastic_strain;
d.go(|x| [stretch * x[0], x[1] / stretch.sqrt(), x[2] / stretch.sqrt()], 1e-6);
let e = &d.state.elements[0];
if e.failed.is_some() {
failed_at = Some((before, e.point.plastic_strain, e.failed));
break;
}
}
let (before, at, why) = failed_at.expect("the element fails");
eprintln!("damage: failed between plastic strain {before:.5} and {at:.5} (D1 = 0.2)");
assert_eq!(why, Some(Failure::Damage));
assert!(before < 0.2 && at >= 0.2 - 1e-12, "{before} {at}");
// Failed: no forces from it.
d.go(|x| [1.6 * x[0], x[1], x[2]], 1e-6);
assert!(d.ws.stress_force.iter().all(|f| *f == 0.0));
}
/// Inelastic models that cannot run are refused by name, never panics.
#[test]
fn inelastic_hostile_models_are_errors() {
// Plasticity with orthotropic elasticity is refused by the model itself.
let mut m = block([1, 1, 1], [1.0; 3], ElementKind::Hex8);
let ortho = Elastic::EngineeringConstants { young: [1.0; 3], poisson: [0.0; 3], shear: [0.5; 3] };
let plastic = Plastic::Bilinear { yield_stress: 1.0, hardening_modulus: 0.0, hardening: Hardening::Isotropic };
assert!(m.add_material("Bad", Material { elastic: Some(ortho), density: Some(1.0), plastic: Some(plastic), ..Material::default() }).is_err());
// An element turned inside out stops the run with an error naming it.
define(&mut m, ElementKind::Hex8, elastic(1.0, 0.3, 1.0), ExplicitDynamic::new(1.0));
let mut d = Driver::new(&m);
d.go(|x| [x[0], x[1], 0.5 * x[2]], 1.0);
let e = d.try_go(|x| [x[0], x[1], 0.5 - x[2]], 1.0).unwrap_err().to_string();
assert!(e.contains("element 1") && e.contains("inside out"), "{e}");
// A crushed element shrinks the increment until the cap stops the run, with a warning.
let mut m = block([1, 1, 1], [1.0; 3], ElementKind::Hex8);
let mut step = ExplicitDynamic::new(1.0);
step.max_increments = 20_000;
define(&mut m, ElementKind::Hex8, elastic(1.0, 0.3, 1.0), step);
node_set(&mut m, "top", |x| x[2] == 1.0);
node_set(&mut m, "bottom", |x| x[2] == 0.0);
bc(&mut m, "hold", BoundaryCondition::Fixed { target: Target::Set("bottom".into()) });
bc(
&mut m,
"crush",
BoundaryCondition::Displacement { target: Target::Set("top".into()), values: [Some(0.0), Some(0.0), Some(-2.0), None, None, None] },
);
match solve_explicit(&m, None, &Limits::default()) {
Ok(r) => assert!(!r.completed && r.warnings.iter().any(|w| w.contains("cap"))),
Err(e) => assert!(e.to_string().contains("element 1"), "{e}"),
}
}
+7 -4
View File
@@ -77,8 +77,11 @@ impl Bar {
}
}
m.add_set("end_face", Set { faces, ..Set::default() }).unwrap();
m.add_material("Mat", Material { elastic: Some(Elastic::Isotropic { young: YOUNG, poisson: 0.0 }), density: Some(DENSITY), fluid: None })
.unwrap();
m.add_material(
"Mat",
Material { elastic: Some(Elastic::Isotropic { young: YOUNG, poisson: 0.0 }), density: Some(DENSITY), ..Material::default() },
)
.unwrap();
let integration = if kind == ElementKind::Hex8 { Integration::Reduced } else { Integration::Full };
let formulation = Formulation { integration, ..Formulation::default() };
m.add_section("bar", Section { set: "all".into(), material: "Mat".into(), kind: SectionKind::Solid {}, formulation }).unwrap();
@@ -334,12 +337,12 @@ fn caps_and_unsupported_models_are_errors() {
assert!(solve_explicit(&full.model, None, &Limits::default()).unwrap_err().to_string().contains("not supported yet"));
let mut ortho = Bar::new(ElementKind::Tet4, 4, ExplicitDynamic::new(end));
let constants = Elastic::EngineeringConstants { young: [YOUNG; 3], poisson: [0.0; 3], shear: [YOUNG / 2.0; 3] };
ortho.model.replace_material("Mat", Material { elastic: Some(constants), density: Some(DENSITY), fluid: None }).unwrap();
ortho.model.replace_material("Mat", Material { elastic: Some(constants), density: Some(DENSITY), ..Material::default() }).unwrap();
assert!(solve_explicit(&ortho.model, None, &Limits::default()).unwrap_err().to_string().contains("orthotropic"));
let mut light = Bar::new(ElementKind::Tet4, 4, ExplicitDynamic::new(end));
light
.model
.replace_material("Mat", Material { elastic: Some(Elastic::Isotropic { young: YOUNG, poisson: 0.0 }), density: None, fluid: None })
.replace_material("Mat", Material { elastic: Some(Elastic::Isotropic { young: YOUNG, poisson: 0.0 }), density: None, ..Material::default() })
.unwrap();
assert!(solve_explicit(&light.model, None, &Limits::default()).is_err());
// A node behind its wall at the start.