forked from 1b-agent/fieldcraft-upstream
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
d2fe8a8303
commit
bc9fe2191a
13 files changed
+408
-19
No files matched your search
@@ -322,6 +322,7 @@ impl Model {
|
||||
Procedure::ExplicitDynamic(_) => "is dynamic",
|
||||
Procedure::HeatTransfer { transient: Some(_) } => "is transient heat transfer",
|
||||
Procedure::Static {}
|
||||
| Procedure::NonlinearStatic(_)
|
||||
| Procedure::Fluid { .. }
|
||||
| Procedure::HeatTransfer { transient: None }
|
||||
| Procedure::CoupledTemperatureDisplacement {} => {
|
||||
@@ -469,7 +470,7 @@ impl Model {
|
||||
}
|
||||
}
|
||||
match &s.procedure {
|
||||
Procedure::Static {} => {
|
||||
Procedure::Static {} | Procedure::NonlinearStatic(_) => {
|
||||
if s.loads.is_empty() && !prescribes_motion {
|
||||
d.warning(&subject, "nothing loads the model in this step");
|
||||
}
|
||||
|
||||
@@ -57,12 +57,13 @@ pub use discrete::{Direction, Mass, PointMass, ResolvedDirection, ResolvedMass,
|
||||
pub use fluids::{BoxFace, BoxFaces, FaceCondition, FluidBoundary, FluidDomain, Profile, SurfaceBoundary, SurfaceCondition};
|
||||
pub use geometry::{Classification, Entity, GeometryRef};
|
||||
pub use loads::{Distribution, Load, centrifugal_acceleration};
|
||||
pub use materials::{Damage, Directional, Elastic, Erosion, Fluid, Hardening, HardeningPoint, Material, Plastic, Thermal};
|
||||
pub use materials::{Damage, Directional, Elastic, Erosion, Fluid, Hardening, HardeningPoint, Hyperelastic, Material, Plastic, Thermal};
|
||||
pub use mesh::{Element, ElementEdge, ElementFace, ElementKind, Node, check_coordinates};
|
||||
pub use sections::{BeamProfile, Extent, Formulation, Integration, Section, SectionKind};
|
||||
pub use sets::Set;
|
||||
pub use steps::{
|
||||
BulkViscosity, ExplicitDynamic, Hourglass, HourglassKind, InitialVelocity, OutputVariable, Procedure, RigidWall, Step, StressRate, Transient,
|
||||
BulkViscosity, ExplicitDynamic, Hourglass, HourglassKind, InitialVelocity, MAX_NONLINEAR_INCREMENTS, MAX_NONLINEAR_ITERATIONS, NonlinearStatic,
|
||||
OutputVariable, Procedure, RigidWall, Step, StressRate, Tolerances, Transient,
|
||||
};
|
||||
pub use units::UnitSystem;
|
||||
|
||||
|
||||
@@ -74,6 +74,37 @@ impl Elastic {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hyperelastic behaviour of an isotropic material at large strain, in nonlinear steps.
|
||||
///
|
||||
/// The constants are the material's isotropic elastic ones: the shear modulus μ = E / (2(1 + ν))
|
||||
/// and the bulk modulus κ = E / (3(1 − 2ν)), so at small strain the material is that linear
|
||||
/// elastic one (linear steps use it so). Without this option an elastic material in a nonlinear
|
||||
/// step is St Venant–Kirchhoff (the second Piola–Kirchhoff stress linear in the Green–Lagrange
|
||||
/// strain), which is right for large rotations with small strains.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Hyperelastic {
|
||||
/// Compressible neo-Hookean with the isochoric–volumetric split: the strain energy per
|
||||
/// reference volume W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)², with Ī₁ = J^(−2/3) tr C (C₁₀ = μ/2,
|
||||
/// D₁ = 2/κ in the usual polynomial notation).
|
||||
NeoHookean {},
|
||||
}
|
||||
|
||||
impl Hyperelastic {
|
||||
/// The shear and bulk moduli μ = E / (2(1 + ν)) and κ = E / (3(1 − 2ν)) the model takes from
|
||||
/// the isotropic constants.
|
||||
pub fn moduli(young: f64, poisson: f64) -> (f64, f64) {
|
||||
(young / (2.0 * (1.0 + poisson)), young / (3.0 * (1.0 - 2.0 * poisson)))
|
||||
}
|
||||
|
||||
/// The name of the model, for messages.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Hyperelastic::NeoHookean {} => "neo-Hookean",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fluid behaviour.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
@@ -332,6 +363,9 @@ pub struct Erosion {
|
||||
pub struct Material {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub elastic: Option<Elastic>,
|
||||
/// Large-strain behaviour in nonlinear steps, built on the isotropic elastic constants.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub hyperelastic: Option<Hyperelastic>,
|
||||
/// Mass per volume, needed by frequency steps, gravity and transient heat transfer.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub density: Option<f64>,
|
||||
@@ -366,6 +400,14 @@ impl Material {
|
||||
if let Some(e) = &self.elastic {
|
||||
e.validate()?;
|
||||
}
|
||||
if let Some(h) = &self.hyperelastic {
|
||||
if !matches!(self.elastic, Some(Elastic::Isotropic { .. })) {
|
||||
return Err(Error::Invalid(format!("{} hyperelasticity needs isotropic elastic constants", h.name())));
|
||||
}
|
||||
if self.plastic.is_some() {
|
||||
return Err(Error::Invalid(format!("plasticity of a {} hyperelastic material is not supported yet", h.name())));
|
||||
}
|
||||
}
|
||||
if let Some(d) = self.density {
|
||||
positive("density", d)?;
|
||||
}
|
||||
@@ -412,6 +454,11 @@ impl Model {
|
||||
self.materials.get(name)
|
||||
}
|
||||
|
||||
/// The hyperelastic materials some section uses, by name.
|
||||
pub fn hyperelastic_in_use(&self) -> std::collections::BTreeSet<&str> {
|
||||
self.sections.values().map(|s| s.material.as_str()).filter(|m| self.material(m).is_some_and(|m| m.hyperelastic.is_some())).collect()
|
||||
}
|
||||
|
||||
pub fn add_material(&mut self, name: &str, material: Material) -> Result<()> {
|
||||
material.validate().map_err(|e| context("material", name, e))?;
|
||||
insert_new(&mut self.materials, "material", name, material)
|
||||
@@ -544,6 +591,25 @@ mod tests {
|
||||
assert_eq!(serde_json::from_str::<Material>(&text).unwrap(), Material { plastic: Some(jc()), damage: Some(damage), ..steel() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyperelasticity_builds_on_isotropic_constants() {
|
||||
let m: Material = serde_json::from_value(serde_json::json!({
|
||||
"elastic": {"type": "isotropic", "young": 3.0, "poisson": 0.49}, "hyperelastic": {"type": "neo_hookean"}
|
||||
}))
|
||||
.unwrap();
|
||||
m.validate().unwrap();
|
||||
assert_eq!(m.hyperelastic, Some(Hyperelastic::NeoHookean {}));
|
||||
let back: Material = serde_json::from_value(serde_json::to_value(&m).unwrap()).unwrap();
|
||||
assert_eq!(back, m);
|
||||
assert!(serde_json::from_value::<Material>(serde_json::json!({"hyperelastic": {"type": "neo_hookean", "c10": 1}})).is_err());
|
||||
let alone = Material { hyperelastic: Some(Hyperelastic::NeoHookean {}), density: Some(1.0), ..Material::default() };
|
||||
assert!(alone.validate().is_err(), "needs isotropic constants");
|
||||
let ortho = Elastic::EngineeringConstants { young: [1.0; 3], poisson: [0.0; 3], shear: [1.0; 3] };
|
||||
assert!(Material { elastic: Some(ortho), ..alone }.validate().is_err());
|
||||
let plastic = Plastic::Bilinear { yield_stress: 1.0, hardening_modulus: 0.0, hardening: Hardening::Isotropic };
|
||||
assert!(Material { plastic: Some(plastic), ..m }.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thermal_properties() {
|
||||
let m: Material = serde_json::from_value(serde_json::json!({
|
||||
|
||||
+151
-2
@@ -86,6 +86,114 @@ pub enum Procedure {
|
||||
/// Steady heat conduction and the static equilibrium of the thermal strains it causes,
|
||||
/// together: every boundary condition and load of the step acts.
|
||||
CoupledTemperatureDisplacement {},
|
||||
/// Static equilibrium followed in load increments with Newton–Raphson iterations: large
|
||||
/// displacements, rotations and strains (`nonlinear_geometry`), hyperelastic materials and
|
||||
/// follower pressure. Loads and prescribed displacements grow in proportion to the step's
|
||||
/// load factor, from 0 to 1.
|
||||
NonlinearStatic(NonlinearStatic),
|
||||
}
|
||||
|
||||
/// Most load increments a nonlinear static step may take.
|
||||
pub const MAX_NONLINEAR_INCREMENTS: u32 = 100_000;
|
||||
|
||||
/// Most Newton iterations a nonlinear static step may take in one increment.
|
||||
pub const MAX_NONLINEAR_ITERATIONS: u32 = 1000;
|
||||
|
||||
/// The settings of a nonlinear static step.
|
||||
///
|
||||
/// The step's load factor λ runs from 0 to 1: every load is λ times its value and every
|
||||
/// prescribed displacement λ times its value (a linear ramp). Increments are fractions of the
|
||||
/// step: the first is `initial_increment`; an increment that converges quickly lets the next
|
||||
/// grow (up to `max_increment`), one that diverges, fails to converge in `max_iterations` or
|
||||
/// inverts an element is retried with a quarter of its size, and the step fails, naming the
|
||||
/// load factor and the reason, when an increment would fall below `min_increment` or the step
|
||||
/// has taken `max_increments` increments.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct NonlinearStatic {
|
||||
/// Large displacements, rotations and strains (total Lagrangian elements, follower
|
||||
/// pressure); `false` keeps the kinematics of small displacements (only the materials are
|
||||
/// nonlinear).
|
||||
pub nonlinear_geometry: bool,
|
||||
/// The first increment, as a fraction of the step.
|
||||
pub initial_increment: f64,
|
||||
/// The smallest increment a cutback may reach.
|
||||
pub min_increment: f64,
|
||||
/// The largest increment.
|
||||
pub max_increment: f64,
|
||||
/// Cap on the increments (cutbacks included).
|
||||
pub max_increments: u32,
|
||||
/// Cap on the Newton iterations of one increment.
|
||||
pub max_iterations: u32,
|
||||
/// Convergence tolerances.
|
||||
pub tolerances: Tolerances,
|
||||
/// A line search along each Newton correction (off by default: full Newton steps converge
|
||||
/// best for elastic large-rotation problems; a search helps where the material softens).
|
||||
pub line_search: bool,
|
||||
}
|
||||
|
||||
/// When an increment's Newton iterations have converged: all three hold.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Tolerances {
|
||||
/// The largest residual force component over the largest force (applied, internal or
|
||||
/// reaction) the step has seen.
|
||||
pub force: f64,
|
||||
/// The largest displacement correction over the largest displacement of the increment.
|
||||
pub displacement: f64,
|
||||
/// The work of the last correction against the residual over that of the first.
|
||||
pub energy: f64,
|
||||
}
|
||||
|
||||
impl Default for Tolerances {
|
||||
fn default() -> Tolerances {
|
||||
Tolerances { force: 1e-6, displacement: 1e-5, energy: 1e-10 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NonlinearStatic {
|
||||
fn default() -> NonlinearStatic {
|
||||
NonlinearStatic {
|
||||
nonlinear_geometry: true,
|
||||
initial_increment: 0.1,
|
||||
min_increment: 1e-5,
|
||||
max_increment: 1.0,
|
||||
max_increments: 100,
|
||||
max_iterations: 16,
|
||||
tolerances: Tolerances::default(),
|
||||
line_search: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NonlinearStatic {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
let fraction = |what: &str, v: f64| {
|
||||
if v > 0.0 && v <= 1.0 { Ok(v) } else { Err(Error::Invalid(format!("the {what} is a fraction of the step in (0, 1], got {v}"))) }
|
||||
};
|
||||
let initial = fraction("initial increment", self.initial_increment)?;
|
||||
let min = fraction("minimum increment", self.min_increment)?;
|
||||
let max = fraction("maximum increment", self.max_increment)?;
|
||||
if !(min <= initial && initial <= max) {
|
||||
return Err(Error::Invalid(format!("the increments must satisfy minimum ≤ initial ≤ maximum, got {min}, {initial} and {max}")));
|
||||
}
|
||||
if !(1..=MAX_NONLINEAR_INCREMENTS).contains(&self.max_increments) {
|
||||
return Err(Error::Invalid(format!("a nonlinear step takes 1 to {MAX_NONLINEAR_INCREMENTS} increments, got {}", self.max_increments)));
|
||||
}
|
||||
if !(1..=MAX_NONLINEAR_ITERATIONS).contains(&self.max_iterations) {
|
||||
return Err(Error::Invalid(format!(
|
||||
"a nonlinear step takes 1 to {MAX_NONLINEAR_ITERATIONS} iterations per increment, got {}",
|
||||
self.max_iterations
|
||||
)));
|
||||
}
|
||||
let t = self.tolerances;
|
||||
for (what, v) in [("force", t.force), ("displacement", t.displacement), ("energy", t.energy)] {
|
||||
if !(v > 0.0 && v < 1.0) {
|
||||
return Err(Error::Invalid(format!("the {what} tolerance is in (0, 1), got {v}")));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Most time increments of a transient heat transfer step.
|
||||
@@ -386,7 +494,7 @@ impl BulkViscosity {
|
||||
}
|
||||
|
||||
impl Procedure {
|
||||
/// `static` or `frequency`.
|
||||
/// The procedure's type, as the model file names it.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Procedure::Static {} => "static",
|
||||
@@ -395,6 +503,7 @@ impl Procedure {
|
||||
Procedure::HeatTransfer { .. } => "heat_transfer",
|
||||
Procedure::CoupledTemperatureDisplacement {} => "coupled_temperature_displacement",
|
||||
Procedure::Fluid { .. } => "fluid",
|
||||
Procedure::NonlinearStatic(_) => "nonlinear_static",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,6 +544,9 @@ impl Procedure {
|
||||
if let Procedure::ExplicitDynamic(e) = self {
|
||||
return e.validate();
|
||||
}
|
||||
if let Procedure::NonlinearStatic(n) = self {
|
||||
return n.validate();
|
||||
}
|
||||
if let Procedure::Fluid { end_time, steps, time_step, frames, magic, .. } = *self {
|
||||
match (end_time, steps) {
|
||||
(Some(t), None) => {
|
||||
@@ -480,7 +592,7 @@ impl Procedure {
|
||||
/// The output a step of this procedure requests unless told otherwise.
|
||||
pub fn default_output(&self) -> BTreeSet<OutputVariable> {
|
||||
match self {
|
||||
Procedure::Static {} => OutputVariable::ALL.into_iter().collect(),
|
||||
Procedure::Static {} | Procedure::NonlinearStatic(_) => OutputVariable::ALL.into_iter().collect(),
|
||||
Procedure::Frequency { .. } => [OutputVariable::Displacement].into_iter().collect(),
|
||||
Procedure::ExplicitDynamic(_) => [OutputVariable::Displacement, OutputVariable::Stress].into_iter().collect(),
|
||||
// Velocity, pressure and wall forces, always.
|
||||
@@ -753,6 +865,43 @@ mod tests {
|
||||
assert!(serde_json::from_value::<Procedure>(serde_json::json!({"type": "explicit_dynamic", "end_time": 1, "dt": 2})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonlinear_static_steps_validate_their_controls() {
|
||||
let p: Procedure = serde_json::from_value(serde_json::json!({"type": "nonlinear_static"})).unwrap();
|
||||
assert_eq!(p, Procedure::NonlinearStatic(NonlinearStatic::default()));
|
||||
assert_eq!(p.name(), "nonlinear_static");
|
||||
assert!(p.validate().is_ok() && p.is_structural() && !p.is_dynamic());
|
||||
assert_eq!(p.default_output(), Procedure::Static {}.default_output());
|
||||
let p: Procedure = serde_json::from_value(serde_json::json!({
|
||||
"type": "nonlinear_static", "nonlinear_geometry": false, "initial_increment": 0.5, "min_increment": 0.01,
|
||||
"max_increments": 20, "max_iterations": 8, "tolerances": {"force": 1e-4}, "line_search": true
|
||||
}))
|
||||
.unwrap();
|
||||
let Procedure::NonlinearStatic(n) = &p else { panic!("{p:?}") };
|
||||
assert_eq!((n.nonlinear_geometry, n.max_increment, n.tolerances.force, n.tolerances.energy), (false, 1.0, 1e-4, 1e-10));
|
||||
let back: Procedure = serde_json::from_value(serde_json::to_value(&p).unwrap()).unwrap();
|
||||
assert_eq!(back, p);
|
||||
let base = NonlinearStatic::default();
|
||||
let bad = [
|
||||
NonlinearStatic { initial_increment: 0.0, ..base.clone() },
|
||||
NonlinearStatic { initial_increment: f64::NAN, ..base.clone() },
|
||||
NonlinearStatic { initial_increment: 2.0, max_increment: 2.0, ..base.clone() },
|
||||
NonlinearStatic { min_increment: 0.5, initial_increment: 0.1, ..base.clone() },
|
||||
NonlinearStatic { max_increment: 0.05, ..base.clone() },
|
||||
NonlinearStatic { max_increments: 0, ..base.clone() },
|
||||
NonlinearStatic { max_increments: MAX_NONLINEAR_INCREMENTS + 1, ..base.clone() },
|
||||
NonlinearStatic { max_iterations: 0, ..base.clone() },
|
||||
NonlinearStatic { max_iterations: u32::MAX, ..base.clone() },
|
||||
NonlinearStatic { tolerances: Tolerances { force: 0.0, ..Tolerances::default() }, ..base.clone() },
|
||||
NonlinearStatic { tolerances: Tolerances { energy: f64::INFINITY, ..Tolerances::default() }, ..base.clone() },
|
||||
NonlinearStatic { tolerances: Tolerances { displacement: 1.0, ..Tolerances::default() }, ..base.clone() },
|
||||
];
|
||||
for b in bad {
|
||||
assert!(Procedure::NonlinearStatic(b.clone()).validate().is_err(), "{b:?}");
|
||||
}
|
||||
assert!(serde_json::from_value::<Procedure>(serde_json::json!({"type": "nonlinear_static", "nlgeom": true})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heat_transfer_procedures() {
|
||||
let steady: Procedure = serde_json::from_value(serde_json::json!({"type": "heat_transfer"})).unwrap();
|
||||
|
||||
Reference in new issue
Block a user