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

+113 -6
View File
@@ -54,6 +54,7 @@
//! reaches its material's erosion plastic strain is removed: it carries no stress from then on, //! 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. //! its nodes keep their mass and momentum, and its internal energy at that moment is reported.
use fieldcraft_fem::constitutive::MaterialModel;
use fieldcraft_fem::material::stress; use fieldcraft_fem::material::stress;
use fieldcraft_fem::plasticity::{Increment, PointState}; use fieldcraft_fem::plasticity::{Increment, PointState};
use fieldcraft_fem::rotation::{self, IDENTITY, Mat3, Polar}; use fieldcraft_fem::rotation::{self, IDENTITY, Mat3, Polar};
@@ -76,6 +77,10 @@ pub struct Workspace {
/// volume. /// volume.
theta: Vec<f64>, theta: Vec<f64>,
tet_volume: Vec<f64>, tet_volume: Vec<f64>,
/// Per node, for hyperelastic tetrahedra: the current and the rest volume of its
/// tetrahedra, then their ratio.
tet_ratio: Vec<f64>,
tet_rest: Vec<f64>,
/// Per tetrahedron (in [`System::tets`] order): its increment between the two passes. /// Per tetrahedron (in [`System::tets`] order): its increment between the two passes.
tet_increment: Vec<Option<TetIncrement>>, tet_increment: Vec<Option<TetIncrement>>,
} }
@@ -84,6 +89,7 @@ impl Workspace {
pub fn new(sys: &System) -> Workspace { pub fn new(sys: &System) -> Workspace {
let n = sys.labels.len(); let n = sys.labels.len();
let dofs = sys.dofs(); let dofs = sys.dofs();
let hyper_tets = sys.tets.iter().any(|t| sys.materials.get(t.material).is_some_and(|m| m.hyperelastic.is_some()));
Workspace { Workspace {
stress_force: vec![0.0; dofs], stress_force: vec![0.0; dofs],
hourglass_force: vec![0.0; dofs], hourglass_force: vec![0.0; dofs],
@@ -91,6 +97,8 @@ impl Workspace {
contact_force: if sys.contacts.is_empty() { Vec::new() } else { vec![0.0; dofs] }, contact_force: if sys.contacts.is_empty() { Vec::new() } else { vec![0.0; dofs] },
theta: vec![0.0; n], theta: vec![0.0; n],
tet_volume: vec![0.0; n], tet_volume: vec![0.0; n],
tet_ratio: if hyper_tets { vec![1.0; n] } else { Vec::new() },
tet_rest: if hyper_tets { vec![0.0; n] } else { Vec::new() },
tet_increment: vec![None; sys.tets.len()], tet_increment: vec![None; sys.tets.len()],
} }
} }
@@ -314,6 +322,52 @@ fn update(mat: &Material, es: &mut ElementState, q: &Mat3, d: &[f64; 6], vol: f6
} }
} }
/// The hyperelastic update of an element at deformation gradient `f` (the hexahedron's mean,
/// or the tetrahedron's with its averaged volume ratio): the material point's Kirchhoff stress τ
/// gives the stress of the nodal forces τ/`j` (`j` the element's own volume ratio, so the forces
/// are ∫ τ ∇ₓN dV₀), the logarithmic strain is kept for output, and the stress work over the
/// increment `d` (on the midpoint volume `volume`) is added to the element's energy. Returns the
/// ratio of the tangent's largest normal stiffness (with the stress) to the material's
/// small-strain one, at least 1, which scales the element's frequency bound.
#[allow(clippy::too_many_arguments)]
fn hyperelastic(
model: &MaterialModel,
mat: &Material,
es: &mut ElementState,
f: &Mat3,
j: f64,
pressure: Option<f64>,
d: &[f64; 6],
volume: f64,
) -> Result<f64> {
let unstable = |e: fieldcraft_fem::Error| Error::Unstable(e.to_string());
if !(j > 0.0 && j.is_finite()) {
return Err(Error::Unstable(format!("the material is inverted (volume ratio {j:.3e})")));
}
let r = model.response(f, None, &[], &mut []).map_err(unstable)?;
let before = es.point.stress;
es.point.stress = match pressure {
// The isochoric part of the element's own F with the averaged pressure.
Some(p) => {
let iso = model.isochoric(f, None, &[], &mut []).map_err(unstable)?;
let mut s = iso.stress.map(|t| t / j);
for v in s.iter_mut().take(3) {
*v += p;
}
s
}
None => r.stress.map(|t| t / j),
};
let work: f64 = (0..6).map(|k| 0.5 * (before[k] + es.point.stress[k]) * d[k]).sum();
es.energy += work * volume;
if let Some(e) = fieldcraft_fem::constitutive::logarithmic_strain(f) {
es.strain = e;
}
let normal = (0..3).map(|k| r.tangent[k][k]).fold(0.0, f64::max) / j;
let stress = es.point.stress.iter().fold(0.0f64, |m, s| m.max(s.abs()));
Ok(((normal + stress) / mat.stiffest).max(1.0))
}
/// The mechanical part of a strain increment: less the thermal strain increment `thermal` in /// The mechanical part of a strain increment: less the thermal strain increment `thermal` in
/// every normal direction (isotropic expansion, the same in any frame). /// every normal direction (isotropic expansion, the same in any frame).
fn mechanical(mut d: [f64; 6], thermal: f64) -> [f64; 6] { fn mechanical(mut d: [f64; 6], thermal: f64) -> [f64; 6] {
@@ -493,7 +547,20 @@ fn hexahedron(sys: &System, h: &crate::system::Hex, m: &Motion, es: &mut Element
let (d, _) = rotation::split(&g); let (d, _) = rotation::split(&g);
let d = mechanical(d, h.thermal_rate * m.dt); let d = mechanical(d, h.thermal_rate * m.dt);
let (rho, c) = current(mat, h.kinematics.volume, now.volume); 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); let stiffer = match &mat.hyperelastic {
Some(model) => {
let mut f = h.kinematics.gradient(&e.u);
for k in 0..3 {
f[k][k] += 1.0;
}
let j = fieldcraft_fem::dense::det3(&f);
hyperelastic(model, mat, es, &f, j, None, &d, mid.volume).map_err(|_| inverted(h.label, m.time))?
}
None => {
update(mat, es, &q, &d, d[0] + d[1] + d[2], m.dt, rho, mid.volume);
1.0
}
};
if let Some(failure) = es.failed { if let Some(failure) = es.failed {
return Ok(Outcome::Eroded(failure)); return Ok(Outcome::Eroded(failure));
} }
@@ -526,7 +593,7 @@ fn hexahedron(sys: &System, h: &crate::system::Hex, m: &Motion, es: &mut Element
} }
} }
} }
let element = Element { now: &now, mid: &mid, density: rho, wave_speed: c, length: 2.0 * c / scales.omega2.sqrt() }; let element = Element { now: &now, mid: &mid, density: rho, wave_speed: c, length: 2.0 * c / (stiffer * scales.omega2).sqrt() };
let mut forces = element.finish(&es.point.stress, &e.v, m.bulk); let mut forces = element.finish(&es.point.stress, &e.v, m.bulk);
forces.hourglass = Some(f); forces.hourglass = Some(f);
Ok(Outcome::Forces(forces)) Ok(Outcome::Forces(forces))
@@ -554,6 +621,7 @@ fn tetrahedron(
t: &crate::system::Tet, t: &crate::system::Tet,
inc: Option<TetIncrement>, inc: Option<TetIncrement>,
theta: &[f64], theta: &[f64],
ratio: &[f64],
m: &Motion, m: &Motion,
es: &mut ElementState, es: &mut ElementState,
) -> Result<Outcome<4>> { ) -> Result<Outcome<4>> {
@@ -564,11 +632,34 @@ fn tetrahedron(
let thermal = t.thermal_rate * m.dt; let thermal = t.thermal_rate * m.dt;
let d = mechanical(d, thermal); let d = mechanical(d, thermal);
let (rho, c) = current(mat, t.kinematics.volume, inc.now.volume); let (rho, c) = current(mat, t.kinematics.volume, inc.now.volume);
update(mat, es, &q, &d, averaged - 3.0 * thermal, m.dt, rho, inc.mid.volume); let stiffer = match &mat.hyperelastic {
// The average nodal pressure of a decoupled material (Bonet & Burton 1998, at finite
// strain): each node's pressure U′(J_a) from its tetrahedra's volume ratio, the element's
// the mean of its nodes', with the isochoric stress of its own F.
Some(model) => {
let u = gather(m.u, &t.nodes);
let mut f = t.kinematics.gradient(&u);
for k in 0..3 {
f[k][k] += 1.0;
}
let j = fieldcraft_fem::dense::det3(&f);
let pressure = if model.is_decoupled() {
let p: Option<f64> = t.nodes.iter().map(|n| model.volumetric(ratio.get(*n).copied().unwrap_or(1.0), false).map(|(p, _)| p)).sum();
p.map(|p| p / 4.0)
} else {
None
};
hyperelastic(model, mat, es, &f, j, pressure, &d, inc.mid.volume).map_err(|_| inverted(t.label, m.time))?
}
None => {
update(mat, es, &q, &d, averaged - 3.0 * thermal, m.dt, rho, inc.mid.volume);
1.0
}
};
if let Some(failure) = es.failed { if let Some(failure) = es.failed {
return Ok(Outcome::Eroded(failure)); return Ok(Outcome::Eroded(failure));
} }
let omega2 = if sys.nonlinear { frequency_bound(&inc.now, inc.now.gradient_norm(), mat.modulus, &t.mass) } else { t.omega2 }; let omega2 = stiffer * 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, density: rho, wave_speed: c, length: 2.0 * c / omega2.sqrt() }; let element = Element { now: &inc.now, mid: &inc.mid, density: rho, wave_speed: c, length: 2.0 * c / omega2.sqrt() };
Ok(Outcome::Forces(element.finish(&es.point.stress, &inc.v, m.bulk))) Ok(Outcome::Forces(element.finish(&es.point.stress, &inc.v, m.bulk)))
} }
@@ -632,16 +723,32 @@ pub fn internal_forces(sys: &System, m: &Motion, state: &mut State, ws: &mut Wor
*x /= w; *x /= w;
} }
} }
if !ws.tet_ratio.is_empty() {
ws.tet_ratio.fill(0.0);
ws.tet_rest.fill(0.0);
for (t, inc) in tets.iter().zip(&ws.tet_increment) {
let Some(inc) = inc else { continue };
for node in t.nodes {
if let (Some(x), Some(w)) = (ws.tet_ratio.get_mut(node), ws.tet_rest.get_mut(node)) {
*x += inc.now.volume;
*w += t.kinematics.volume;
}
}
}
for (x, w) in ws.tet_ratio.iter_mut().zip(&ws.tet_rest) {
*x = if *w > 0.0 { *x / w } else { 1.0 };
}
}
// Pass 2: each tetrahedron's update. // Pass 2: each tetrahedron's update.
let State { elements, eroded } = state; let State { elements, eroded } = state;
let (theta, increments) = (&ws.theta, &ws.tet_increment); let (theta, ratio, increments) = (&ws.theta, &ws.tet_ratio, &ws.tet_increment);
let mut forces: Vec<(usize, Forces<4>)> = Vec::new(); let mut forces: Vec<(usize, Forces<4>)> = Vec::new();
run( run(
elements, elements,
tets.len(), tets.len(),
|k| tets.get(k).map_or(usize::MAX, |t| t.index), |k| tets.get(k).map_or(usize::MAX, |t| t.index),
|k, es| match tets.get(k) { |k, es| match tets.get(k) {
Some(t) => tetrahedron(sys, t, increments.get(k).copied().flatten(), theta, m, es), Some(t) => tetrahedron(sys, t, increments.get(k).copied().flatten(), theta, ratio, m, es),
None => Ok(Outcome::Skipped), None => Ok(Outcome::Skipped),
}, },
|k, outcome| { |k, outcome| {
+34 -3
View File
@@ -3,6 +3,7 @@
//! ([`crate::discrete`]), prescribed velocities, rigid walls, initial velocities and the load //! ([`crate::discrete`]), prescribed velocities, rigid walls, initial velocities and the load
//! vector of an explicit dynamic step. //! vector of an explicit dynamic step.
use fieldcraft_fem::constitutive::MaterialModel;
use fieldcraft_fem::material::Elasticity; use fieldcraft_fem::material::Elasticity;
use fieldcraft_fem::plasticity::J2; use fieldcraft_fem::plasticity::J2;
use fieldcraft_fem::uniform::{Hex8, Tet4, modulus_bound}; 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 /// The hypoelastic(–plastic) update of an isotropic material; `None` for orthotropic
/// elasticity, updated with [`Material::elasticity`]. /// elasticity, updated with [`Material::elasticity`].
pub j2: Option<J2>, 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. /// The equivalent plastic strain at which elements erode.
pub erosion: Option<f64>, pub erosion: Option<f64>,
/// The (isotropic) expansion coefficient and reference temperature, when the material /// 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> { fn material(model: &Model, name: &str) -> Result<Material> {
let m = model.material(name).ok_or_else(|| invalid(format!("material `{name}` is missing")))?; 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")))?; 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 let density = m
.density .density
.filter(|d| *d > 0.0 && d.is_finite()) .filter(|d| *d > 0.0 && d.is_finite())
@@ -174,6 +175,12 @@ fn material(model: &Model, name: &str) -> Result<Material> {
stiffest, stiffest,
j2, j2,
erosion: m.erosion.map(|e| e.plastic_strain), 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.load(model, resolved, step)?;
sys.temperatures(model, resolved, step, settings.end_time)?; sys.temperatures(model, resolved, step, settings.end_time)?;
sys.conditions(resolved, settings)?; sys.conditions(resolved, settings)?;
sys.check_hyperelastic()?;
Ok(sys) 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. /// Add an element's row-sum lumped masses.
fn lump(&mut self, nodes: &[usize], mass: &[f64]) { fn lump(&mut self, nodes: &[usize], mass: &[f64]) {
for (node, v) in nodes.iter().zip(mass) { for (node, v) in nodes.iter().zip(mass) {
+148
View File
@@ -0,0 +1,148 @@
//! Hyperelastic materials in explicit dynamics: the stress of the shared finite-strain material
//! point (neo-Hookean, Mooney–Rivlin, Yeoh) from the deformation gradient, on one-point hex8 and
//! F-bar tet4, against the closed form of uniaxial strain reached quasi-statically, and the
//! refusals.
//!
//! In uniaxial strain F = diag(λ, 1, 1): the hyperelastic Cauchy stress is the material point's
//! Kirchhoff stress over J = λ (fieldcraft_fem::constitutive, whose laws are checked there
//! against their strain energies), so the explicit elements must reproduce it once the motion has
//! settled; the wave dynamics of a slow pull leave a fraction of a percent.
use fieldcraft_explicit::{ExplicitResult, Limits, solve_explicit};
use fieldcraft_fem::constitutive::MaterialModel;
use fieldcraft_model::*;
const N: usize = 2;
/// A unit cube of N³ hex8 (or six tet4 per cell), held on its faces x = 0, y = 0, y = 1, z = 0,
/// z = 1 in their normal directions and pulled on x = 1 to a stretch λ over `end`.
fn block(tet: bool, hyper: Hyperelastic, stretch: f64, end: f64, nonlinear: bool) -> Model {
let mut m = Model::default();
let h = 1.0 / N as f64;
let label = |i: usize, j: usize, k: usize| (1 + i + (N + 1) * (j + (N + 1) * k)) as u32;
for k in 0..=N {
for j in 0..=N {
for i in 0..=N {
m.set_node(label(i, j, k), [i as f64 * h, j as f64 * h, k as f64 * h]).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 {
for j in 0..N {
for i in 0..N {
let at = |c: [usize; 3]| label(i + c[0], j + c[1], k + c[2]);
if !tet {
m.set_element(next, ElementKind::Hex8, 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 e = |a: usize| [0, 1, 2].map(|d| v[a][d] as f64 - v[0][d] as f64);
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, ElementKind::Tet4, 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();
let nodes = |f: &dyn Fn([f64; 3]) -> bool| -> Vec<u32> { m.nodes().iter().filter(|(_, n)| f(n.xyz)).map(|(l, _)| *l).collect() };
let sets = [
("x0", nodes(&|x| x[0] == 0.0)),
("x1", nodes(&|x| x[0] == 1.0)),
("y", nodes(&|x| x[1] == 0.0 || x[1] == 1.0)),
("z", nodes(&|x| x[2] == 0.0 || x[2] == 1.0)),
];
for (name, list) in sets {
m.add_set(name, Set { nodes: list, ..Set::default() }).unwrap();
}
let elastic = Elastic::Isotropic { young: 1.0, poisson: 0.45 };
m.add_material("Rubber", Material { elastic: Some(elastic), density: Some(1.0), hyperelastic: Some(hyper), ..Material::default() }).unwrap();
let integration = if tet { Integration::Full } else { Integration::Reduced };
let formulation = Formulation { integration, ..Formulation::default() };
m.add_section("all", Section { set: "all".into(), material: "Rubber".into(), kind: SectionKind::Solid {}, formulation }).unwrap();
let held = |set: &str, k: usize, v: f64| {
let mut values = [None; 6];
values[k] = Some(v);
BoundaryCondition::Displacement { target: Target::Set(set.into()), values }
};
let mut step = ExplicitDynamic::new(end);
step.nonlinear_geometry = nonlinear;
step.output_interval = Some(end / 2.0);
let mut s = Step::new("Pull", Procedure::ExplicitDynamic(step));
for (name, bc) in [("x0", held("x0", 0, 0.0)), ("x1", held("x1", 0, stretch - 1.0)), ("y", held("y", 1, 0.0)), ("z", held("z", 2, 0.0))] {
m.add_boundary_condition(name, bc).unwrap();
s.boundary_conditions.insert(name.into());
}
m.add_step(s, None).unwrap();
m
}
/// σ of uniaxial strain λ from the material point.
fn closed_form(hyper: Hyperelastic, stretch: f64) -> [f64; 6] {
let model = MaterialModel::from_model(&Elastic::Isotropic { young: 1.0, poisson: 0.45 }, Some(&hyper)).unwrap();
let f = [[stretch, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
model.response(&f, None, &[], &mut []).unwrap().stress.map(|t| t / stretch)
}
fn mean_stress(r: &ExplicitResult) -> [f64; 6] {
let f = r.frames.last().unwrap();
let mut s = [0.0; 6];
for e in &f.stress {
for k in 0..6 {
s[k] += e[k] / f.stress.len() as f64;
}
}
s
}
#[test]
fn hyperelastic_uniaxial_strain_matches_the_material_point() {
let stretch = 1.5;
// Dilatational wave speed √((κ + 4μ/3)/ρ) ≈ 2.0: a pull over 400 transits of the cube.
let end = 200.0;
let materials = [Hyperelastic::NeoHookean {}, Hyperelastic::MooneyRivlin { c01: 0.08 }, Hyperelastic::Yeoh { c20: -0.01, c30: 0.002 }];
for hyper in materials {
let reference = closed_form(hyper, stretch);
for tet in [false, true] {
let r = solve_explicit(&block(tet, hyper, stretch, end, true), None, &Limits::default()).unwrap();
assert!(r.completed);
let s = mean_stress(&r);
let spread = r.frames.last().unwrap().stress.iter().map(|e| (e[0] - s[0]).abs()).fold(0.0, f64::max);
eprintln!(
"{} {}: σ_xx {:.5} vs {:.5}, σ_yy {:.5} vs {:.5}; spread {spread:.2e}; balance {:.2e}; {} increments",
hyper.name(),
if tet { "tet4" } else { "hex8" },
s[0],
reference[0],
s[1],
reference[1],
r.energy.relative_error(r.initial_kinetic),
r.increments
);
for k in 0..3 {
assert!((s[k] - reference[k]).abs() < 0.01 * reference[0].abs(), "{} tet {tet}: {s:?} vs {reference:?}", hyper.name());
}
assert!(spread < 0.01 * reference[0].abs(), "uniform state: {spread}");
assert!(r.energy.relative_error(r.initial_kinetic) < 0.01, "{:?}", r.energy);
}
}
}
#[test]
fn hyperelastic_materials_need_nonlinear_geometry() {
let e = solve_explicit(&block(false, Hyperelastic::NeoHookean {}, 1.1, 1.0, false), None, &Limits::default()).unwrap_err().to_string();
assert!(e.contains("nonlinear geometry") && e.contains("Rubber"), "{e}");
}
+18
View File
@@ -450,3 +450,21 @@ contact patch test on non-matching meshes (σ_zz uniform to 0.1%), two spheres c
Hertz (peak force +2.3%, duration −0.4% on a graded quarter-sphere mesh), a free body untouched Hertz (peak force +2.3%, duration −0.4% on a graded quarter-sphere mesh), a free body untouched
by general contact on all its faces, initial penetrations adjusted or refused, the face cap, and by general contact on all its faces, initial penetrations adjusted or refused, the face cap, and
bit-for-bit results on one and four threads. bit-for-bit results on one and four threads.
## Hyperelastic materials in explicit dynamics
Hyperelastic materials (neo-Hookean, Mooney–Rivlin, Yeoh) use the shared finite-strain material
point (`fieldcraft_fem::constitutive`) in explicit dynamics too, in total form, which is objective
by construction (no stress rate): the one-point hexahedron takes F = I + ∂u/∂X, its mean over the
undeformed element, and the Cauchy stress of the nodal forces τ/J; the tetrahedron takes the
isochoric stress of its own F and the **average nodal pressure** of a decoupled material (each
node's U′(J_a) from its tetrahedra's volume ratio, the element's the mean of its nodes': J. Bonet,
A. J. Burton, Commun. Numer. Methods Eng. 14:437–449, 1998, at finite strain), which does not lock
when the material is nearly incompressible. (Averaging J into an F-bar instead was unstable on
the Kuhn mesh of the tests.) The stable increment follows the tangent: each element's frequency
bound is scaled by the ratio of its tangent's largest normal stiffness, plus the stress, to the
small-strain one. Logarithmic strain is the strain output. Hyperelastic elements need nonlinear
geometry and take no thermal strain yet (both refused by name). Verified by uniaxial strain to
λ = 1.5 of all three laws on hex8 and tet4 against the material point (`crates/explicit/tests/hyperelastic.rs`)
and by `tests/oracle/explicit-neo-hookean-implicit`, a quasi-static explicit pull against the
implicit nonlinear solver.
+4 -1
View File
@@ -2,7 +2,7 @@
Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the policy is [verification.md](verification.md). Every case is our own model, built with FieldCraft commands from the problem's published description, and compared with the reference value its `expected.json` cites, within the stated tolerance and for the stated reason. Accepted results are frozen in each case's `fixture.json`: a later change that moves one beyond its band (10⁻⁹ of its size) fails CI even while the reference still passes. No reference value was produced with a commercial product. Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the policy is [verification.md](verification.md). Every case is our own model, built with FieldCraft commands from the problem's published description, and compared with the reference value its `expected.json` cites, within the stated tolerance and for the stated reason. Accepted results are frozen in each case's `fixture.json`: a later change that moves one beyond its band (10⁻⁹ of its size) fails CI even while the reference still passes. No reference value was produced with a commercial product.
**199 of 205 quantities pass** (6 known failure(s)) in 61 active cases: 52 theory, 9 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built. **201 of 207 quantities pass** (6 known failure(s)) in 62 active cases: 53 theory, 9 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built.
## Theory: closed-form solutions, patch tests and mesh invariants ## Theory: closed-form solutions, patch tests and mesh invariants
@@ -46,6 +46,8 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
| [explicit-contact-patch](../tests/oracle/explicit-contact-patch/) | σ_zz of the upper block's top far corner element (element 26) over that of element 19 | 1 | 0.99944 | -0.0564% | 0.5% | pass | matches | | [explicit-contact-patch](../tests/oracle/explicit-contact-patch/) | σ_zz of the upper block's top far corner element (element 26) over that of element 19 | 1 | 0.99944 | -0.0564% | 0.5% | pass | matches |
| [explicit-contact-patch](../tests/oracle/explicit-contact-patch/) | the force across the interface over −σ_zz A (A = 1) | 1 | 1.0026 | 0.259% | 1% | pass | matches | | [explicit-contact-patch](../tests/oracle/explicit-contact-patch/) | the force across the interface over −σ_zz A (A = 1) | 1 | 1.0026 | 0.259% | 1% | pass | matches |
| [explicit-contact-patch](../tests/oracle/explicit-contact-patch/) | σ_xx of element 14 over its σ_zz (uniaxial stress: 0) | 0 | -3.6052e-4 | -3.6e-4 | ± 0.005 | pass | matches | | [explicit-contact-patch](../tests/oracle/explicit-contact-patch/) | σ_xx of element 14 over its σ_zz (uniaxial stress: 0) | 0 | -3.6052e-4 | -3.6e-4 | ± 0.005 | pass | matches |
| [explicit-neo-hookean-implicit](../tests/oracle/explicit-neo-hookean-implicit/) | lateral displacement of the corner (1, 1, 1) along y, explicit over implicit | 1 | 0.99908 | -0.0922% | 0.5% | pass | matches |
| [explicit-neo-hookean-implicit](../tests/oracle/explicit-neo-hookean-implicit/) | σ_xx of element 1, explicit, over σ_xx at the node at (0, 0, 0), implicit | 1 | 1.0003 | 0.0255% | 0.5% | pass | matches |
| [explicit-sliding-block-contact](../tests/oracle/explicit-sliding-block-contact/) | distance travelled (top centre node) against ∫ v dv / (g μ(v)) = 2.01524 | 2.0152 | 2.0118 | -0.172% | 1% | pass | matches | | [explicit-sliding-block-contact](../tests/oracle/explicit-sliding-block-contact/) | distance travelled (top centre node) against ∫ v dv / (g μ(v)) = 2.01524 | 2.0152 | 2.0118 | -0.172% | 1% | pass | matches |
| [explicit-sliding-block-contact](../tests/oracle/explicit-sliding-block-contact/) | velocity of the top centre node after the stop | 0 | ≈ 0 | ≈ 0 | ± 0.03 | pass | matches | | [explicit-sliding-block-contact](../tests/oracle/explicit-sliding-block-contact/) | velocity of the top centre node after the stop | 0 | ≈ 0 | ≈ 0 | ± 0.03 | pass | matches |
| [explicit-sliding-block-wall](../tests/oracle/explicit-sliding-block-wall/) | distance travelled (top centre node) against v₀²/(2μg) = 1.5 | 1.5 | 1.4999 | -0.0053% | 1% | pass | matches | | [explicit-sliding-block-wall](../tests/oracle/explicit-sliding-block-wall/) | distance travelled (top centre node) against v₀²/(2μg) = 1.5 | 1.5 | 1.4999 | -0.0053% | 1% | pass | matches |
@@ -235,6 +237,7 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
- **[cdt-stepped-block](../tests/oracle/cdt-stepped-block/)** (theory): Constrained Delaunay mesh of a stepped block: exact volume, valid elements. Source: mesh invariants: a tetrahedralization of a polyhedron fills its volume exactly (Si, TetGen paper ACM TOMS 41(2) 2015 for the definition of a CDT; Diazzi et al., ACM TOG 42(6) 2023); the volume is 60·40·10 + 40·40·10 + 20·40·10 − 10·10·10. Tolerances — volume: a planar boundary is tessellated exactly and the constrained Delaunay mesh fills it exactly: only rounding; invalid: every element of a valid mesh has positive volume; undetermined: a linear tetrahedron's sign is exact - **[cdt-stepped-block](../tests/oracle/cdt-stepped-block/)** (theory): Constrained Delaunay mesh of a stepped block: exact volume, valid elements. Source: mesh invariants: a tetrahedralization of a polyhedron fills its volume exactly (Si, TetGen paper ACM TOMS 41(2) 2015 for the definition of a CDT; Diazzi et al., ACM TOG 42(6) 2023); the volume is 60·40·10 + 40·40·10 + 20·40·10 − 10·10·10. Tolerances — volume: a planar boundary is tessellated exactly and the constrained Delaunay mesh fills it exactly: only rounding; invalid: every element of a valid mesh has positive volume; undetermined: a linear tetrahedron's sign is exact
- **[explicit-bars-collision](../tests/oracle/explicit-bars-collision/)** (theory): Two identical elastic bars colliding (explicit contact). Source: One-dimensional elastic impact of equal bars: contact for 2L/c, interface force ρcAv₀/2, exchange of velocities, no loss of energy; Graff, Wave Motion in Elastic Solids, Dover 1991, §2.1–2.3; Johnson, Impact Strength of Materials, Arnold 1972, §1.6. Tolerances — duration: the penalty springs at the interface delay the release by a few element transit times (about 1% of 2L/c with 200 elements per bar); force: the plateau is ρcAv₀/2 to 0.01%; the peak is the plateau plus the ringing behind the discrete wave front, a few percent; v_a: the striker stops on average (its kinetic energy is under 0.01% of the start's, see ke); its far end node keeps the ringing of the discrete release front, a few percent of v₀; v_b: the struck bar leaves at v₀ on average; its far end node keeps the ringing of the discrete wave fronts, a few percent; ke: an elastic impact keeps the kinetic energy; the quadratic bulk viscosity at the compression fronts and the vibration left in the bars take a fraction of a percent; contact_energy: the penalty springs give back what they took; the trapezoidal work of a spring that engages between increments drifts by far less than 0.1% - **[explicit-bars-collision](../tests/oracle/explicit-bars-collision/)** (theory): Two identical elastic bars colliding (explicit contact). Source: One-dimensional elastic impact of equal bars: contact for 2L/c, interface force ρcAv₀/2, exchange of velocities, no loss of energy; Graff, Wave Motion in Elastic Solids, Dover 1991, §2.1–2.3; Johnson, Impact Strength of Materials, Arnold 1972, §1.6. Tolerances — duration: the penalty springs at the interface delay the release by a few element transit times (about 1% of 2L/c with 200 elements per bar); force: the plateau is ρcAv₀/2 to 0.01%; the peak is the plateau plus the ringing behind the discrete wave front, a few percent; v_a: the striker stops on average (its kinetic energy is under 0.01% of the start's, see ke); its far end node keeps the ringing of the discrete release front, a few percent of v₀; v_b: the struck bar leaves at v₀ on average; its far end node keeps the ringing of the discrete wave fronts, a few percent; ke: an elastic impact keeps the kinetic energy; the quadratic bulk viscosity at the compression fronts and the vibration left in the bars take a fraction of a percent; contact_energy: the penalty springs give back what they took; the trapezoidal work of a spring that engages between increments drifts by far less than 0.1%
- **[explicit-contact-patch](../tests/oracle/explicit-contact-patch/)** (theory): Contact patch test: two blocks with non-matching meshes in uniaxial compression (explicit contact). Source: The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; Crisfield, Re-visiting the contact patch test, IJNME 48:435–449, 2000. Uniaxial stress between frictionless symmetry planes is uniform in both blocks. Tolerances — lower_corner: a uniform stress is an equilibrium of the discrete system (the integrated contact forces are the consistent nodal forces of a uniform pressure on both meshes); what is left is the ringing of the loading, a hundred wave transits slow; lower_centre: as for the corner element; lower_base: as for the corner element; upper_top: as for the corner element; force: the integrated contact pressure carries the whole load; the largest force of the run is the final one in a slow squeeze, up to the ringing; lateral: frictionless symmetry planes and a frictionless interface leave the blocks free to expand sideways; the non-matching interface must not hold them - **[explicit-contact-patch](../tests/oracle/explicit-contact-patch/)** (theory): Contact patch test: two blocks with non-matching meshes in uniaxial compression (explicit contact). Source: The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; Crisfield, Re-visiting the contact patch test, IJNME 48:435–449, 2000. Uniaxial stress between frictionless symmetry planes is uniform in both blocks. Tolerances — lower_corner: a uniform stress is an equilibrium of the discrete system (the integrated contact forces are the consistent nodal forces of a uniform pressure on both meshes); what is left is the ringing of the loading, a hundred wave transits slow; lower_centre: as for the corner element; lower_base: as for the corner element; upper_top: as for the corner element; force: the integrated contact pressure carries the whole load; the largest force of the run is the final one in a slow squeeze, up to the ringing; lateral: frictionless symmetry planes and a frictionless interface leave the blocks free to expand sideways; the non-matching interface must not hold them
- **[explicit-neo-hookean-implicit](../tests/oracle/explicit-neo-hookean-implicit/)** (theory): Neo-Hookean cube stretched to λ = 1.5: explicit dynamics, loaded slowly, against the implicit nonlinear solver. Source: Uniaxial stress of a compressible neo-Hookean solid (Holzapfel, Nonlinear Solid Mechanics, Wiley 2000, §6.5): a homogeneous state that a quasi-static explicit run must share with the implicit Newton solution on the same material point; the implicit solver is itself checked against the closed form by nl-neo-hookean-uniaxial. Tolerances — lateral: the same material point and a homogeneous state; the explicit run is pulled over a few hundred wave transits, and what is left of its waves is a fraction of a percent; stress: as for the displacement
- **[explicit-sliding-block-contact](../tests/oracle/explicit-sliding-block-contact/)** (theory): Block sliding to a stop on a deformable plate, friction decaying with the slip rate (explicit contact). Source: A rigid block launched at v₀ with friction μ(v) = μk + (μs − μk) e^(−d v) decelerates at g μ(v); it travels ∫₀^v₀ v dv / (g μ(v)) (Simpson's rule, 200 000 intervals) and stops after [v₀ + ln((μk + (μs − μk) e^(−d v₀))/μs)/d]/(g μk) = 1.2001; Meriam & Kraige, Engineering Mechanics: Dynamics, §3/5; the exponential decay law as in Oden & Martins, CMAME 52:527–634, 1985. Tolerances — distance: the block bounces on the penalty springs and the plate under it shears elastically, so the normal force and the slip rate it feels vary by a little around their rigid-body values; velocity: the block sticks; what is left is its elastic vibration, under 1% of v₀ - **[explicit-sliding-block-contact](../tests/oracle/explicit-sliding-block-contact/)** (theory): Block sliding to a stop on a deformable plate, friction decaying with the slip rate (explicit contact). Source: A rigid block launched at v₀ with friction μ(v) = μk + (μs − μk) e^(−d v) decelerates at g μ(v); it travels ∫₀^v₀ v dv / (g μ(v)) (Simpson's rule, 200 000 intervals) and stops after [v₀ + ln((μk + (μs − μk) e^(−d v₀))/μs)/d]/(g μk) = 1.2001; Meriam & Kraige, Engineering Mechanics: Dynamics, §3/5; the exponential decay law as in Oden & Martins, CMAME 52:527–634, 1985. Tolerances — distance: the block bounces on the penalty springs and the plate under it shears elastically, so the normal force and the slip rate it feels vary by a little around their rigid-body values; velocity: the block sticks; what is left is its elastic vibration, under 1% of v₀
- **[explicit-sliding-block-wall](../tests/oracle/explicit-sliding-block-wall/)** (theory): Block sliding to a stop on a rigid wall with Coulomb friction (explicit). Source: A rigid block launched at v₀ on a horizontal plane with Coulomb friction μ decelerates at μg and stops after v₀/(μg) at v₀²/(2μg), its kinetic energy all dissipated by friction; Meriam & Kraige, Engineering Mechanics: Dynamics, §3/5 (work and energy). Tolerances — distance: the wall's friction impulses are exact Coulomb impulses; the block's elastic vibration from the sudden gravity and the friction moment move the node by a fraction of a percent; velocity: the block sticks; what is left is its elastic vibration, under 1% of v₀; dissipated: the kinetic energy goes into friction; the vibration and the work of gravity as the block settles add or take a fraction of a percent - **[explicit-sliding-block-wall](../tests/oracle/explicit-sliding-block-wall/)** (theory): Block sliding to a stop on a rigid wall with Coulomb friction (explicit). Source: A rigid block launched at v₀ on a horizontal plane with Coulomb friction μ decelerates at μg and stops after v₀/(μg) at v₀²/(2μg), its kinetic energy all dissipated by friction; Meriam & Kraige, Engineering Mechanics: Dynamics, §3/5 (work and energy). Tolerances — distance: the wall's friction impulses are exact Coulomb impulses; the block's elastic vibration from the sudden gravity and the friction moment move the node by a fraction of a percent; velocity: the block sticks; what is left is its elastic vibration, under 1% of v₀; dissipated: the kinetic energy goes into friction; the vibration and the work of gravity as the block settles add or take a fraction of a percent
- **[filleted-bracket-fillet-stress](../tests/oracle/filleted-bracket-fillet-stress/)** (theory): Filleted L-bracket under a side load: the peak von Mises stress on the R6 fillet, fine tet10 mesh. Source: mesh convergence of the finite element solution (P. J. Roache, Verification and Validation in Computational Science and Engineering, Hermosa, 1998, ch. 5): the bracket has no closed-form stress; the reference is the mesh-independent peak of a four-level study of quadratic tetrahedral meshes refined by √2 in size and in curvature resolution (README.md); equilibrium. Tolerances — fillet_peak: the four-level study gives 213.3, 210.6, 214.3, 213.3 MPa (2k to 13.5k elements): the bending stress is converged across the fillet, and the peak nodal value moves only with which of the fillet's tessellation lines (fixed angles, through every level) carries the largest node, by under 2%; reaction: equilibrium holds to rounding - **[filleted-bracket-fillet-stress](../tests/oracle/filleted-bracket-fillet-stress/)** (theory): Filleted L-bracket under a side load: the peak von Mises stress on the R6 fillet, fine tet10 mesh. Source: mesh convergence of the finite element solution (P. J. Roache, Verification and Validation in Computational Science and Engineering, Hermosa, 1998, ch. 5): the bracket has no closed-form stress; the reference is the mesh-independent peak of a four-level study of quadratic tetrahedral meshes refined by √2 in size and in curvature resolution (README.md); equilibrium. Tolerances — fillet_peak: the four-level study gives 213.3, 210.6, 214.3, 213.3 MPa (2k to 13.5k elements): the bending stress is converged across the fillet, and the peak nodal value moves only with which of the fillet's tessellation lines (fixed angles, through every level) carries the largest node, by under 2%; reaction: equilibrium holds to rounding
@@ -0,0 +1,8 @@
# Neo-Hookean cube: explicit against implicit (quasi-static)
A unit cube of 2 × 2 × 2 cells of tet4 (six per cell) of a compressible neo-Hookean material
(E = 10, ν = 0.3, ρ = 1), quarter symmetric (symmetry planes x = 0, y = 0, z = 0), pulled on
x = 1 to λ = 1.5 in uniaxial stress. The same model is solved by the implicit nonlinear solver
and by explicit dynamics with the pull applied over 100 time units (a few hundred wave transits):
the lateral contraction and the axial stress must agree, both coming from the shared
finite-strain material point.
@@ -0,0 +1,17 @@
{
"title": "Neo-Hookean cube stretched to λ = 1.5: explicit dynamics, loaded slowly, against the implicit nonlinear solver",
"kind": "theory",
"source": "Uniaxial stress of a compressible neo-Hookean solid (Holzapfel, Nonlinear Solid Mechanics, Wiley 2000, §6.5): a homogeneous state that a quasi-static explicit run must share with the implicit Newton solution on the same material point; the implicit solver is itself checked against the closed form by nl-neo-hookean-uniaxial",
"quantities": [
{"id": "lateral", "quantity": "lateral displacement of the corner (1, 1, 1) along y, explicit over implicit",
"measure": {"command": "solve.probe", "params": {"step": "Dynamic", "at": [[1, 1, 1]]}, "pointer": "/nodes/0/displacement/1",
"over": {"command": "solve.probe", "params": {"step": "Static", "at": [[1, 1, 1]]}, "pointer": "/nodes/0/displacement/1"}},
"value": 1.0, "tolerance": {"relative": 0.005},
"reason": "the same material point and a homogeneous state; the explicit run is pulled over a few hundred wave transits, and what is left of its waves is a fraction of a percent"},
{"id": "stress", "quantity": "σ_xx of element 1, explicit, over σ_xx at the node at (0, 0, 0), implicit",
"measure": {"command": "solve.probe", "params": {"step": "Dynamic", "elements": [1]}, "pointer": "/elements/0/stress/0",
"over": {"command": "solve.probe", "params": {"step": "Static", "at": [[0, 0, 0]]}, "pointer": "/nodes/0/stress/0"}},
"value": 1.0, "tolerance": {"relative": 0.005},
"reason": "as for the displacement"}
]
}
@@ -0,0 +1,13 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"lateral": {
"band": 1e-9,
"value": 0.9990781754549147
},
"stress": {
"band": 1.0002554925493198e-9,
"value": 1.0002554925493197
}
}
}
@@ -0,0 +1,245 @@
[
{
"command": "mesh.block",
"params": {
"body": "Cube",
"kind": "tet4",
"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
]
],
"divisions": [
2,
2,
2
],
"faces": {
"6": "x0",
"3": "y0",
"1": "z0",
"4": "x1"
}
}
},
{
"command": "model.set.create",
"params": {
"name": "all",
"geometry": [
{
"body": "Cube"
}
]
}
},
{
"command": "model.set.create",
"params": {
"name": "x0",
"geometry": [
{
"body": "Cube",
"face": "x0"
}
]
}
},
{
"command": "model.set.create",
"params": {
"name": "y0",
"geometry": [
{
"body": "Cube",
"face": "y0"
}
]
}
},
{
"command": "model.set.create",
"params": {
"name": "z0",
"geometry": [
{
"body": "Cube",
"face": "z0"
}
]
}
},
{
"command": "model.set.create",
"params": {
"name": "x1",
"geometry": [
{
"body": "Cube",
"face": "x1"
}
]
}
},
{
"command": "model.material.create",
"params": {
"name": "Rubber",
"elastic": {
"type": "isotropic",
"young": 10.0,
"poisson": 0.3
},
"density": 1.0,
"hyperelastic": {
"type": "neo_hookean"
}
}
},
{
"command": "model.section.create",
"params": {
"name": "cube",
"set": "all",
"material": "Rubber",
"kind": {
"type": "solid"
}
}
},
{
"command": "model.boundary_condition.create",
"params": {
"name": "sx",
"type": "symmetry",
"target": {
"set": "x0"
},
"plane": "x"
}
},
{
"command": "model.boundary_condition.create",
"params": {
"name": "sy",
"type": "symmetry",
"target": {
"set": "y0"
},
"plane": "y"
}
},
{
"command": "model.boundary_condition.create",
"params": {
"name": "sz",
"type": "symmetry",
"target": {
"set": "z0"
},
"plane": "z"
}
},
{
"command": "model.boundary_condition.create",
"params": {
"name": "pull",
"type": "displacement",
"target": {
"set": "x1"
},
"values": [
0.5,
null,
null,
null,
null,
null
]
}
},
{
"command": "model.step.create",
"params": {
"name": "Static",
"procedure": {
"type": "nonlinear_static"
},
"boundary_conditions": [
"sx",
"sy",
"sz",
"pull"
],
"loads": []
}
},
{
"command": "model.step.create",
"params": {
"name": "Dynamic",
"procedure": {
"type": "explicit_dynamic",
"end_time": 100.0
},
"boundary_conditions": [
"sx",
"sy",
"sz",
"pull"
],
"loads": []
}
},
{
"command": "solve.nonlinear",
"params": {
"step": "Static"
},
"id": "implicit"
},
{
"command": "solve.explicit",
"params": {
"step": "Dynamic"
},
"id": "explicit"
}
]