explicit: hyperelastic materials on the shared finite-strain material point

Neo-Hookean, Mooney–Rivlin and Yeoh materials in explicit dynamics, in
total form from the deformation gradient (objective by construction):
the one-point hex8 takes its mean F over the undeformed element and the
stress τ/J; tet4 takes the isochoric stress of its own F with the
average nodal pressure of the decoupled volumetric energy (Bonet–Burton
at finite strain). Each element's frequency bound follows the tangent's
largest normal stiffness. Small-displacement steps and thermal strain of
hyperelastic elements are refused by name.

Verified by uniaxial strain to λ = 1.5 of all three laws on hex8 and
tet4 against the material point (to rounding), and the oracle case
explicit-neo-hookean-implicit: a quasi-static explicit pull against the
implicit nonlinear solver (lateral contraction to 0.09%, stress to 0.03%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 14:29:43 -04:00
1 parent 94f7e8533a
commit ce8e23d05b
9 files changed
+600 -10

No files matched your search

+34 -3
View File
@@ -3,6 +3,7 @@
//! ([`crate::discrete`]), prescribed velocities, rigid walls, initial velocities and the load
//! vector of an explicit dynamic step.
use fieldcraft_fem::constitutive::MaterialModel;
use fieldcraft_fem::material::Elasticity;
use fieldcraft_fem::plasticity::J2;
use fieldcraft_fem::uniform::{Hex8, Tet4, modulus_bound};
@@ -38,6 +39,9 @@ pub struct Material {
/// The hypoelastic(–plastic) update of an isotropic material; `None` for orthotropic
/// elasticity, updated with [`Material::elasticity`].
pub j2: Option<J2>,
/// The finite-strain material point of a hyperelastic material (its stress from the
/// deformation gradient; `None` for the rate-form materials).
pub hyperelastic: Option<MaterialModel>,
/// The equivalent plastic strain at which elements erode.
pub erosion: Option<f64>,
/// The (isotropic) expansion coefficient and reference temperature, when the material
@@ -143,9 +147,6 @@ fn invalid(m: impl Into<String>) -> Error {
fn material(model: &Model, name: &str) -> Result<Material> {
let m = model.material(name).ok_or_else(|| invalid(format!("material `{name}` is missing")))?;
let elastic = m.elastic.as_ref().ok_or_else(|| invalid(format!("material `{name}` has no elastic constants")))?;
if let Some(h) = m.hyperelastic {
return Err(Error::NotSupported(format!("{} hyperelastic material `{name}` in explicit dynamics", h.name())));
}
let density = m
.density
.filter(|d| *d > 0.0 && d.is_finite())
@@ -174,6 +175,12 @@ fn material(model: &Model, name: &str) -> Result<Material> {
stiffest,
j2,
erosion: m.erosion.map(|e| e.plastic_strain),
hyperelastic: m
.hyperelastic
.as_ref()
.map(|h| MaterialModel::from_model(elastic, Some(h)))
.transpose()
.map_err(|e| invalid(format!("material `{name}`: {e}")))?,
})
}
@@ -368,9 +375,33 @@ impl System {
sys.load(model, resolved, step)?;
sys.temperatures(model, resolved, step, settings.end_time)?;
sys.conditions(resolved, settings)?;
sys.check_hyperelastic()?;
Ok(sys)
}
/// Refuse what hyperelastic elements cannot take yet: small-displacement steps (the
/// material is a finite-strain law) and thermal strain.
fn check_hyperelastic(&self) -> Result<()> {
let solids =
self.hexes.iter().map(|h| (h.label, h.material, h.thermal_rate)).chain(self.tets.iter().map(|t| (t.label, t.material, t.thermal_rate)));
for (label, material, thermal) in solids {
let Some(m) = self.materials.get(material).filter(|m| m.hyperelastic.is_some()) else { continue };
if !self.nonlinear {
return Err(Error::NotSupported(format!(
"hyperelastic material `{}` (element {label}) without nonlinear geometry; hyperelastic laws are finite-strain, set the step's nonlinear_geometry to true",
m.name
)));
}
if thermal != 0.0 {
return Err(Error::NotSupported(format!(
"thermal strain in hyperelastic material `{}` (element {label}) in explicit dynamics",
m.name
)));
}
}
Ok(())
}
/// Add an element's row-sum lumped masses.
fn lump(&mut self, nodes: &[usize], mass: &[f64]) {
for (node, v) in nodes.iter().zip(mass) {