model: nonlinear static steps and neo-Hookean hyperelasticity, mapped by the deck writers

A nonlinear_static procedure (nonlinear geometry, initial/min/max increments,
increment and iteration caps, force/displacement/energy tolerances, line
search), validated with caps against hostile values. Materials gain an
explicit hyperelastic option (decoupled compressible neo-Hookean on the
isotropic constants). Abaqus writes *STEP, NLGEOM, INC / *STATIC with the
increments and *HYPERELASTIC, NEO HOOKE; LS-DYNA writes the ramped implicit
run with full Newton and *CONTROL_IMPLICIT_AUTO; Nastran refuses SOL 400 by
name, as LS-DYNA does hyperelastic materials in nonlinear steps. Explicit
dynamics refuses hyperelastic materials by name; solve.run names the solver
still to come.

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-14 04:00:21 -04:00
1 parent d2fe8a8303
commit bc9fe2191a
13 files changed
+408 -19

No files matched your search

+50 -6
View File
@@ -30,7 +30,14 @@
//! - a frequency step is a perturbation of the last general step: its boundary conditions are
//! written against that base state and do not carry on to later steps; loads have no effect on
//! natural frequencies and are not written in it;
//! - every step restates its output request (`*OUTPUT, FIELD, OP=NEW`).
//! - every step restates its output request (`*OUTPUT, FIELD, OP=NEW`);
//! - a nonlinear static step is `*STEP, NLGEOM=YES` (or `NO` without nonlinear geometry) with
//! `INC=` its increment cap, `*STATIC` with its initial, minimum and maximum increments over a
//! step period of 1, and `*CONTROLS, PARAMETERS=LINE SEARCH` when it searches; its iteration cap
//! and tolerances are left to Abaqus's controls, whose residual norms are defined differently.
//! Abaqus starts each general step from the end of the previous one, while every model step
//! starts from the undeformed state; for elastic materials the end state depends on the final
//! loads alone, so the two agree.
//!
//! # Loads
//!
@@ -96,7 +103,10 @@
//!
//! # Materials
//!
//! Isotropic elasticity is `*ELASTIC, TYPE=ISOTROPIC`, engineering constants
//! Isotropic elasticity is `*ELASTIC, TYPE=ISOTROPIC` (or, for a neo-Hookean material in a deck
//! with nonlinear steps, `*HYPERELASTIC, NEO HOOKE` with C₁₀ = μ/2 and D₁ = 2/κ, the same strain
//! energy; a linear static step is then refused, since Abaqus's general static step would apply
//! the hyperelastic law), engineering constants
//! `TYPE=ENGINEERING CONSTANTS`, density `*DENSITY`. Von Mises plasticity is `*PLASTIC`: a
//! hardening table as its (yield stress, plastic strain) lines; bilinear isotropic hardening as
//! two lines, the second at a plastic strain of [`BILINEAR_REACH`] (Abaqus holds the last yield
@@ -219,6 +229,12 @@ pub fn element_type(kind: ElementKind, section: &Section) -> std::result::Result
Ok(if f.hybrid { format!("{plain}H") } else { plain.to_string() })
}
/// Whether the model has a nonlinear static step: hyperelastic materials are then written as
/// `*HYPERELASTIC` (in a deck of linear steps their elastic constants are the whole of it).
fn has_nonlinear_step(model: &Model) -> bool {
model.steps().iter().any(|s| matches!(s.procedure, Procedure::NonlinearStatic(_)))
}
/// Every element, grouped by element type (from its section) and sorted by label.
fn elements_by_type<'m>(model: &'m Model, r: &Resolved) -> Result<BTreeMap<String, Vec<(u32, &'m Element)>>> {
let mut by_type: BTreeMap<String, Vec<(u32, &Element)>> = BTreeMap::new();
@@ -287,6 +303,16 @@ fn check_expressible(model: &Model) -> Result<()> {
for step in model.steps().iter().filter(|s| matches!(s.procedure, Procedure::ExplicitDynamic(_))) {
bad.insert(format!("step `{}`: explicit dynamic steps (not written to Abaqus input yet)", step.name));
}
if has_nonlinear_step(model)
&& let Some(material) = model.hyperelastic_in_use().into_iter().next()
{
for step in model.steps().iter().filter(|s| matches!(s.procedure, Procedure::Static {})) {
bad.insert(format!(
"step `{}`: a linear static step beside nonlinear steps of hyperelastic material `{material}` (Abaqus's general static step applies the hyperelastic law; write the linear step to its own deck)",
step.name
));
}
}
for (name, m) in model.materials() {
if m.damage.is_some() || m.erosion.is_some() {
bad.insert(format!(
@@ -759,6 +785,12 @@ impl Deck {
for (name, m) in model.materials() {
self.line(&format!("*MATERIAL, NAME={name}"));
match m.elastic {
Some(Elastic::Isotropic { young, poisson }) if m.hyperelastic.is_some() && has_nonlinear_step(model) => {
// W = C10 (Ī1 − 3) + (J − 1)²/D1 with C10 = μ/2 and D1 = 2/κ.
let (shear, bulk) = fieldcraft_model::Hyperelastic::moduli(young, poisson);
self.line("*HYPERELASTIC, NEO HOOKE");
self.numbers_line("", &[shear / 2.0, 2.0 / bulk])?;
}
Some(Elastic::Isotropic { young, poisson }) => {
self.line("*ELASTIC, TYPE=ISOTROPIC");
self.numbers_line("", &[young, poisson])?;
@@ -1038,10 +1070,22 @@ impl Deck {
self.comment(&format!("Step {}", step.name));
match step.procedure {
Procedure::Fluid { .. } => return Err(crate::fluid_step(FORMAT, &step.name)),
Procedure::Static {} => {
self.line(&format!("*STEP, NAME={}, NLGEOM=NO", step.name));
self.line("*STATIC");
self.line("1.0, 1.0");
Procedure::Static {} | Procedure::NonlinearStatic(_) => {
if let Procedure::NonlinearStatic(n) = &step.procedure {
let nlgeom = if n.nonlinear_geometry { "YES" } else { "NO" };
self.line(&format!("*STEP, NAME={}, NLGEOM={nlgeom}, INC={}", step.name, n.max_increments));
self.line("*STATIC");
self.numbers_line("", &[n.initial_increment, 1.0, n.min_increment, n.max_increment])?;
if n.line_search {
self.line("*CONTROLS, PARAMETERS=LINE SEARCH");
self.line("5,");
}
self.comment("iterations and convergence: Abaqus's own controls (its residual norms differ from FieldCraft's)");
} else {
self.line(&format!("*STEP, NAME={}, NLGEOM=NO", step.name));
self.line("*STATIC");
self.line("1.0, 1.0");
}
self.step_boundaries(model, r, &base_bcs, &step.boundary_conditions)?;
if step.loads == base_loads {
self.comment("loads: as in the previous general step");
+37 -6
View File
@@ -24,7 +24,13 @@
//! the set's translations, equal weights);
//! - per step: `*CONTROL_IMPLICIT_GENERAL`, `*CONTROL_IMPLICIT_SOLUTION` (linear) or
//! `*CONTROL_IMPLICIT_EIGENVALUE`, `*CONTROL_TERMINATION` (a static step is one implicit step
//! from time 0 to 1 with every load ramped by one curve, `*DEFINE_CURVE`); boundary conditions
//! from time 0 to 1 with every load ramped by one curve, `*DEFINE_CURVE`); a nonlinear static
//! step runs over the same time with the same ramp, its first increment as `dt0`, the nonlinear
//! solver with a stiffness reformation every iteration (`ilimit` 1, full Newton) and its
//! iteration cap as `maxref`, and `*CONTROL_IMPLICIT_AUTO` with its minimum and maximum
//! increments (its increment cap and tolerances have no LS-DYNA counterpart and are left to
//! LS-DYNA's defaults; a step without nonlinear geometry and hyperelastic materials in nonlinear
//! steps are refused); boundary conditions
//! as `*BOUNDARY_SPC_*_ID` (named) and `*BOUNDARY_PRESCRIBED_MOTION_*_ID` for non-zero
//! displacements; forces, moments and tractions as `*LOAD_NODE_SET`/`_POINT` from the
//! resolved nodal values; pressure as `*LOAD_SEGMENT_SET`; gravity as `*LOAD_BODY_X/Y/Z`
@@ -267,7 +273,21 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
if matches!(step.procedure, Procedure::ExplicitDynamic(_)) {
refused.insert(format!("step `{}`: explicit dynamic steps (this writer writes implicit analyses so far)", step.name));
}
let is_static = matches!(step.procedure, Procedure::Static {});
if let Procedure::NonlinearStatic(n) = &step.procedure {
if !n.nonlinear_geometry {
refused.insert(format!(
"step `{}`: a nonlinear step without nonlinear geometry (LS-DYNA's nonlinear implicit analysis always follows the deformed shape)",
step.name
));
}
for material in model.hyperelastic_in_use() {
refused.insert(format!(
"step `{}`: hyperelastic material `{material}` in a nonlinear step (LS-DYNA's rubber materials define their volumetric energy differently; not written yet)",
step.name
));
}
}
let is_static = matches!(step.procedure, Procedure::Static {} | Procedure::NonlinearStatic(_));
for b in &step.boundary_conditions {
let Some(bc) = model.boundary_conditions().get(b) else { continue };
match bc.target() {
@@ -793,18 +813,29 @@ fn dof_code(dof: u8) -> i32 {
fn write_step(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan, step: &Step, n: u32, constraint_id: &mut u32) -> Result<()> {
d.line("*TITLE");
d.line(&step.name);
let is_static = matches!(step.procedure, Procedure::Static {});
let is_static = matches!(step.procedure, Procedure::Static {} | Procedure::NonlinearStatic(_));
let first_increment = if let Procedure::NonlinearStatic(n) = &step.procedure { n.initial_increment } else { 1.0 };
d.line("*CONTROL_IMPLICIT_GENERAL");
d.header(&["imflag", "dt0"]);
d.card([Field::from(1), Field::from(1.0)]);
match step.procedure {
d.card([Field::from(1), Field::from(first_increment)]);
match &step.procedure {
Procedure::Fluid { .. } => return Err(crate::fluid_step(FORMAT, &step.name)),
Procedure::Static {} => {
d.line("*CONTROL_IMPLICIT_SOLUTION");
d.header(&["nsolvr"]);
d.card([Field::from(1)]);
}
Procedure::Frequency { modes, min_hz, max_hz } => {
Procedure::NonlinearStatic(n) => {
// Nonlinear with a stiffness reformation every iteration (full Newton), at most
// `max_iterations` per increment; LS-DYNA's own convergence norms and line search.
d.line("*CONTROL_IMPLICIT_SOLUTION");
d.header(&["nsolvr", "ilimit", "maxref"]);
d.card([Field::from(2), Field::from(1), Field::from(n.max_iterations)]);
d.line("*CONTROL_IMPLICIT_AUTO");
d.header(&["iauto", "iteopt", "itewin", "dtmin", "dtmax"]);
d.card([Field::from(1), Field::Blank, Field::Blank, Field::from(n.min_increment), Field::from(n.max_increment)]);
}
&Procedure::Frequency { modes, min_hz, max_hz } => {
d.line("*CONTROL_IMPLICIT_EIGENVALUE");
d.header(&["neig", "center", "lflag", "lftend", "rflag", "rhtend"]);
let bound = |b: Option<f64>| match b {
+3
View File
@@ -280,6 +280,9 @@ fn solution(steps: &[&Step]) -> std::result::Result<u32, String> {
if let Some(s) = steps.iter().find(|s| matches!(s.procedure, Procedure::ExplicitDynamic(_))) {
return Err(format!("step `{}`: explicit dynamic steps (Nastran's linear solutions have none)", s.name));
}
if let Some(s) = steps.iter().find(|s| matches!(s.procedure, Procedure::NonlinearStatic(_))) {
return Err(format!("step `{}`: nonlinear static steps (Nastran's SOL 400; this writer writes SOL 101 and 103 so far)", s.name));
}
let statics = steps.iter().find(|s| matches!(s.procedure, Procedure::Static {}));
let modes = steps.iter().find(|s| matches!(s.procedure, Procedure::Frequency { .. }));
match (statics, modes) {