forked from 1b-agent/fieldcraft-upstream
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
741a803785 | ||
|
|
d586d84859 | ||
|
|
3f9e9e30d8 | ||
|
|
53adab54c2 | ||
|
|
7a69fe5e2f | ||
|
|
17b2c837f2 | ||
|
|
569b69c061 | ||
|
|
72940fe78f |
No files matched your search
+4
-4
@@ -41,9 +41,9 @@ pressure) is a degree of freedom of the existing DOF framework, not a separate s
|
||||
| M12 CFD: lattice Boltzmann | D3Q19/D3Q27 on grids voxelised from the face-tagged surface; TRT, regularised and cumulant collision; interpolated walls, inlets and outlets; LES (Smagorinsky, WALE) with wall models; thermal LBM and conjugate heat transfer with the structure's thermal solver; non-Newtonian viscosity (power-law, Carreau–Yasuda, Cross, Herschel–Bulkley, Bingham) and temperature-dependent viscosity; porous media (Brinkman, Darcy–Forchheimer); multiphase and free surface (M13); validation by the lid-driven cavity, backward-facing step, cylinder wake (Strouhal) and turbulent channel; octree refinement shared with the hex mesher; wgpu backend (deterministic per device, checked against the CPU) (CPU core: done — `fieldcraft-lbm` with D3Q19 TRT and Guo forcing, halfway bounce-back and moving walls, slip and periodic faces, Zou–He velocity and pressure faces, exact voxelisation of obstacle bodies, momentum-exchange forces per face; fluid materials, fluid domains and fluid steps in the model; `model.fluid_domain.*`, `solve.fluid`; verified by Poiseuille channel and pipe, Couette, Taylor–Green decay, sphere-array Stokes drag and mass conservation; Schäfer–Turek 2D-1 as a benchmark) | started |
|
||||
| M13 Fluid–structure interaction | Immersed-boundary coupling of the lattice Boltzmann fluid with the explicit or implicit structure; interface force transfer; sub-cycling; Aitken then interface quasi-Newton coupling; free-surface flow | planned |
|
||||
| M14 Blast and impact fluids | EOS-driven fluids in the explicit solver: ALE multi-material or SPH, with fluid–structure contact | planned |
|
||||
| M15 Nonlinear implicit core | Newton–Raphson with line search, automatic increments and cutbacks, convergence on force, displacement and energy; geometric nonlinearity (total and updated Lagrangian, large rotations for shells and beams, follower pressure); every `fem` material behind one interface with its consistent tangent (plasticity, damage and thermal strain from the explicit solver reused implicitly); arc-length (Riks) for snap-through; linear buckling (eigen) with preload stress stiffening, and imperfections from buckling modes; nonlinear stabilisation; restart. Verified by the elastica, Lee's frame, the snap-through arch, the hinged cylindrical shell and plastic collapse loads (solids core: done — `nonlinear_static` steps with automatic increments and cutbacks, Newton–Raphson with the consistent tangent, force, displacement and energy convergence and an optional line search; a material point interface (Kirchhoff stress and spatial tangent from F, with history) with St Venant–Kirchhoff and compressible neo-Hookean; total Lagrangian tet4, tet10, wedge6, hex8, hex20 (full and reduced), hybrid hex8/hex20/tet10 by the three-field mixed form, plane strain and axisymmetric elements; follower pressure with its symmetrised load stiffness; Cauchy stress and logarithmic strain, increment history and frames, `solve.nonlinear`; Abaqus and LS-DYNA decks; verified by the patch and objectivity tests, uniaxial stretch, the elastica and tube inflation by a follower pressure (finite-strain Cook's membrane pending its transcribed reference). Next: J2 plasticity and Mooney–Rivlin on the interface, thermal strain, incompatible modes (EAS), shells and beams, arc-length) | started |
|
||||
| M15 Nonlinear implicit core | Newton–Raphson with line search, automatic increments and cutbacks, convergence on force, displacement and energy; geometric nonlinearity (total and updated Lagrangian, large rotations for shells and beams, follower pressure); every `fem` material behind one interface with its consistent tangent (plasticity, damage and thermal strain from the explicit solver reused implicitly); arc-length (Riks) for snap-through; linear buckling (eigen) with preload stress stiffening, and imperfections from buckling modes; nonlinear stabilisation; restart. Verified by the elastica, Lee's frame, the snap-through arch, the hinged cylindrical shell and plastic collapse loads (solids core: done — `nonlinear_static` steps with automatic increments and cutbacks, Newton–Raphson with the consistent tangent, force, displacement and energy convergence and an optional line search; a material point interface (Kirchhoff stress and spatial tangent from F, with history) with St Venant–Kirchhoff and compressible neo-Hookean; total Lagrangian tet4, tet10, wedge6, hex8, hex20 (full and reduced), hybrid hex8/hex20/tet10 by the three-field mixed form, plane strain and axisymmetric elements; follower pressure with its symmetrised load stiffness; Cauchy stress and logarithmic strain, increment history and frames, `solve.nonlinear`; Abaqus and LS-DYNA decks; verified by the patch and objectivity tests, uniaxial stretch, the elastica and tube inflation by a follower pressure (finite-strain Cook's membrane pending its transcribed reference; J2 plasticity on the interface: Simo's exponential return map at finite strain and the radial return at small strain, both with consistent tangents and the return shared with explicit dynamics, bilinear isotropic and kinematic (small strain), tabulated and Johnson–Cook (rate-independent) hardening, hybrid elements included, load paths for cyclic loading, a tangent predictor for path-dependent materials, verified by uniaxial tension to large strain, Hill's thick cylinder (plastic zone and collapse pressure), a cantilever's collapse and the Bauschinger effect, with quadratic Newton convergence; Mooney–Rivlin and Yeoh options (decoupled, hybrid elements), verified in Treloar's three modes and by a stretched tube's inflation; thermal strain by the multiplicative split at finite strain and the additive one at small strain, with temperatures from temperature loads or the step's own conduction, verified by free and constrained expansion and a heated bar held past yield. Next: incompatible modes (EAS), kinematic hardening at finite strain, shells and beams, linear buckling, arc-length with an indefinite factorization) | started |
|
||||
| M16 Structural dynamics | Implicit transient (Newmark, HHT-α, generalised-α); modal superposition transient; steady-state harmonic (direct and modal), with Rayleigh, modal, structural and material damping; random vibration from PSDs and response spectrum (SRSS, CQC); complex eigenvalues with damping; rotating machinery (Campbell diagrams, gyroscopic terms); substructures (Craig–Bampton) | planned |
|
||||
| M17 Constitutive library and material library | Elastic: transversely isotropic, fully anisotropic (21 constants), temperature-dependent. Elastomers: hyperelastic neo-Hookean, Mooney–Rivlin, Yeoh, Ogden, Arruda–Boyce, Gent, Marlow, with Mullins softening and finite-strain viscoelasticity (Prony series; parallel networks after Bergström–Boyce), foams. Viscoelasticity: small- and finite-strain Prony series, time–temperature superposition (WLF, Arrhenius), storage and loss moduli for harmonic steps. Viscoplasticity and creep: Norton power law, time and strain hardening, Perzyna, Anand, Chaboche combined hardening. Plasticity: Hill anisotropic, Drucker–Prager, Mohr–Coulomb, Cam-clay, crushable foam, concrete damaged plasticity, Gurson. Fluids: Newtonian and non-Newtonian viscosity shared with M12. Material library: curated, cited data sets (steels, aluminium and titanium alloys, polymers, elastomers, composites, tissues) with units and temperature dependence; calibration from test data (uniaxial, biaxial, shear, relaxation, DMA) by least squares with Drucker stability checks; user materials as a safe Rust trait | planned |
|
||||
| M17 Constitutive library and material library | Elastic: transversely isotropic, fully anisotropic (21 constants), temperature-dependent. Elastomers: hyperelastic neo-Hookean, Mooney–Rivlin, Yeoh (these three done, in nonlinear steps), Ogden, Arruda–Boyce, Gent, Marlow, with Mullins softening and finite-strain viscoelasticity (Prony series; parallel networks after Bergström–Boyce), foams. Viscoelasticity: small- and finite-strain Prony series, time–temperature superposition (WLF, Arrhenius), storage and loss moduli for harmonic steps. Viscoplasticity and creep: Norton power law, time and strain hardening, Perzyna, Anand, Chaboche combined hardening. Plasticity: Hill anisotropic, Drucker–Prager, Mohr–Coulomb, Cam-clay, crushable foam, concrete damaged plasticity, Gurson. Fluids: Newtonian and non-Newtonian viscosity shared with M12. Material library: curated, cited data sets (steels, aluminium and titanium alloys, polymers, elastomers, composites, tissues) with units and temperature dependence; calibration from test data (uniaxial, biaxial, shear, relaxation, DMA) by least squares with Drucker stability checks; user materials as a safe Rust trait | planned |
|
||||
| M18 Composites and anisotropy | Material orientations (rectangular, cylindrical, spherical, from geometry edges and faces, per-element fields, draping of fibres over curved parts); layups on shells and continuum shells, ply stacks on swept hex; classical lamination theory checks; ply stresses and strains through the thickness; failure indices (Tsai–Wu, Tsai–Hill, Hashin, Puck, LaRC) and progressive damage; homogenisation (rule of mixtures, Mori–Tanaka, RVEs with periodic boundary conditions); delamination through M20's cohesive zones. Verified by laminate closed forms and the NAFEMS composite benchmarks | planned |
|
||||
| M19 Coupled fields | On the DOF framework, each a degree of freedom beside the displacements: piezoelectricity (electric potential, d/e coupling and permittivity matrices, voltage and charge conditions, resonance and impedance through M16 harmonic steps); poroelasticity (Biot, u–p, Darcy flow, consolidation; Terzaghi and Mandel verification; soils and biphasic tissue); electro-thermal (Joule heating) and thermo-electro-mechanical coupling; acoustics (Helmholtz elements, structure–acoustic coupling, absorbing boundaries); low-frequency electromagnetics later | planned |
|
||||
| M20 Damage, fracture, fatigue and adhesion | Continuum damage beyond Johnson–Cook (Lemaitre, ductile and shear criteria) for implicit and explicit; fracture mechanics (J-integral and stress intensity factors, crack-front meshes from the hex mesher; XFEM later); fatigue (S–N and ε–N with mean-stress corrections, rainflow counting, Paris crack growth); adhesion and debonding by cohesive zones (bilinear and exponential traction–separation, mixed mode with Benzeggagh–Kenane) as cohesive elements and cohesive contact; adhesive joints. Verified by DCB, ENF and MMB delamination tests and published fatigue data | planned |
|
||||
@@ -76,9 +76,9 @@ What exists and what is missing, by area; each gap is in a milestone above.
|
||||
|
||||
| Area | Today | Missing |
|
||||
|---|---|---|
|
||||
| Solution procedures | Linear static, nonlinear static (solids with geometric nonlinearity), frequency (Lanczos), steady and transient heat, coupled temperature–displacement, explicit dynamics, LBM flow | Nonlinear shells and beams, plasticity in implicit steps, arc-length, buckling (M15); implicit, harmonic, random and spectrum dynamics (M16) |
|
||||
| Solution procedures | Linear static, nonlinear static (solids with geometric and material nonlinearity: hyperelasticity, J2 plasticity), frequency (Lanczos), steady and transient heat, coupled temperature–displacement, explicit dynamics, LBM flow | Nonlinear shells and beams, arc-length, buckling (M15); implicit, harmonic, random and spectrum dynamics (M16) |
|
||||
| Contact, friction, adhesion | Explicit planar rigid walls (frictionless); couplings, springs, dashpots | Implicit and explicit deformable contact, tie, friction laws, thermal contact, wear (M7); cohesive adhesion and debonding (M20) |
|
||||
| Materials | Isotropic and orthotropic elasticity, density, thermal, J2 plasticity (bilinear, table, Johnson–Cook), Johnson–Cook damage and erosion, compressible neo-Hookean hyperelasticity, Newtonian fluids | Elastomers beyond neo-Hookean (Mooney–Rivlin and the rest), viscoelasticity, viscoplasticity and creep, anisotropic and pressure-dependent plasticity, non-Newtonian viscosity, a cited material library and calibration (M17) |
|
||||
| Materials | Isotropic and orthotropic elasticity, density, thermal, J2 plasticity (bilinear, table, Johnson–Cook), Johnson–Cook damage and erosion, compressible neo-Hookean, Mooney–Rivlin and Yeoh hyperelasticity, Newtonian fluids | Elastomers beyond Yeoh (Ogden, Arruda–Boyce and the rest), viscoelasticity, viscoplasticity and creep, anisotropic and pressure-dependent plasticity, non-Newtonian viscosity, a cited material library and calibration (M17) |
|
||||
| Composites and anisotropy | Orthotropic constants | Orientations, layups, ply results, failure criteria, homogenisation (M18) |
|
||||
| Coupled fields | Thermal–structural (one way and coupled) | Piezoelectric, poroelastic, electro-thermal, acoustics (M19) |
|
||||
| Failure | Johnson–Cook damage and element erosion (explicit) | Fracture mechanics, fatigue, cohesive zones, composite progressive damage (M18, M20) |
|
||||
|
||||
@@ -37,7 +37,10 @@
|
||||
//! 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 alone, so the two agree. A plastic material's state depends on the path, so a deck
|
||||
//! with nonlinear steps, a plastic material and more than one general step is refused, as is
|
||||
//! a nonlinear step's load path (an amplitude on every load and boundary condition, not
|
||||
//! written yet).
|
||||
//!
|
||||
//! # Loads
|
||||
//!
|
||||
@@ -105,7 +108,8 @@
|
||||
//!
|
||||
//! 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
|
||||
//! energy, and likewise `MOONEY-RIVLIN` (C₁₀, C₀₁, D₁) and `YEOH` (C₁₀, C₂₀, C₃₀, D₁, the higher
|
||||
//! volumetric terms zero); 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
|
||||
@@ -126,8 +130,8 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use fieldcraft_model::{
|
||||
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Hardening, Integration, Load, Model, OutputVariable, Plastic,
|
||||
Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
||||
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Hardening, Hyperelastic, Integration, Load, Model, OutputVariable,
|
||||
Plastic, Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
||||
};
|
||||
|
||||
use crate::{Error, Result, unsupported};
|
||||
@@ -313,6 +317,27 @@ fn check_expressible(model: &Model) -> Result<()> {
|
||||
));
|
||||
}
|
||||
}
|
||||
for step in model.steps() {
|
||||
if let Procedure::NonlinearStatic(n) = &step.procedure
|
||||
&& n.load_path.is_some()
|
||||
{
|
||||
bad.insert(format!(
|
||||
"step `{}`: a load path (it would be an amplitude on every load and boundary condition of the step; not written yet)",
|
||||
step.name
|
||||
));
|
||||
}
|
||||
}
|
||||
let general = model.steps().iter().filter(|s| matches!(s.procedure, Procedure::Static {} | Procedure::NonlinearStatic(_))).count();
|
||||
let plastic_in_use = model.sections().values().find(|s| model.material(&s.material).is_some_and(|m| m.plastic.is_some()));
|
||||
if has_nonlinear_step(model)
|
||||
&& general > 1
|
||||
&& let Some(section) = plastic_in_use
|
||||
{
|
||||
bad.insert(format!(
|
||||
"plastic material `{}` in a deck of {general} general steps (Abaqus carries the plastic state from one general step to the next, while every model step starts from the undeformed state; write each step to its own deck)",
|
||||
section.material
|
||||
));
|
||||
}
|
||||
for (name, m) in model.materials() {
|
||||
if m.damage.is_some() || m.erosion.is_some() {
|
||||
bad.insert(format!(
|
||||
@@ -785,11 +810,20 @@ 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 })
|
||||
if has_nonlinear_step(model)
|
||||
&& let Some(h) = m.hyperelastic =>
|
||||
{
|
||||
// The polynomial constants of the option and U = (J − 1)²/D1 with D1 = 2/κ.
|
||||
let (shear, bulk) = Hyperelastic::moduli(young, poisson);
|
||||
let [c10, c01, c20, c30] = h.constants(shear);
|
||||
let (keyword, values) = match h {
|
||||
Hyperelastic::NeoHookean {} => ("NEO HOOKE", vec![c10, 2.0 / bulk]),
|
||||
Hyperelastic::MooneyRivlin { .. } => ("MOONEY-RIVLIN", vec![c10, c01, 2.0 / bulk]),
|
||||
Hyperelastic::Yeoh { .. } => ("YEOH", vec![c10, c20, c30, 2.0 / bulk]),
|
||||
};
|
||||
self.line(&format!("*HYPERELASTIC, {keyword}"));
|
||||
self.numbers_line("", &values)?;
|
||||
}
|
||||
Some(Elastic::Isotropic { young, poisson }) => {
|
||||
self.line("*ELASTIC, TYPE=ISOTROPIC");
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
//! - 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`); a nonlinear static
|
||||
//! step runs over the same time with the same ramp, its first increment as `dt0`, the nonlinear
|
||||
//! step runs over the same time with the same ramp (or its load path as the curve), 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
|
||||
@@ -872,8 +873,13 @@ fn write_step(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan, step: &Ste
|
||||
d.header(&["lcid"]);
|
||||
d.card([Field::from(n)]);
|
||||
d.header(&["a1", "o1"]);
|
||||
d.card([Field::from(0.0), Field::from(0.0)]);
|
||||
d.card([Field::from(1.0), Field::from(1.0)]);
|
||||
let path = match &step.procedure {
|
||||
Procedure::NonlinearStatic(s) => s.load_path.clone(),
|
||||
_ => None,
|
||||
};
|
||||
for [t, f] in path.unwrap_or_else(|| vec![[0.0, 0.0], [1.0, 1.0]]) {
|
||||
d.card([Field::from(t), Field::from(f)]);
|
||||
}
|
||||
}
|
||||
|
||||
for b in &step.boundary_conditions {
|
||||
|
||||
@@ -790,8 +790,41 @@ fn nonlinear_steps_and_hyperelastic_materials_are_written() {
|
||||
assert!((values[0] - c10).abs() < 1e-9 * c10 && (values[1] - d1).abs() < 1e-9 * d1, "{line}");
|
||||
assert!(!deck.contains("*ELASTIC"), "{deck}");
|
||||
|
||||
// Mooney–Rivlin and Yeoh: C10 = μ/2 − C01, C01, D1 and C10, C20, C30, D1.
|
||||
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::MooneyRivlin { c01: 10000.0 }), ..steel() }).unwrap();
|
||||
let deck = abaqus::write(&m).unwrap();
|
||||
let line = deck.split("*HYPERELASTIC, MOONEY-RIVLIN\n").nth(1).and_then(|s| s.lines().next()).unwrap();
|
||||
let values: Vec<f64> = line.split(", ").map(|v| v.parse().unwrap()).collect();
|
||||
assert!((values[0] - (c10 - 10000.0)).abs() < 1e-9 * c10 && values[1] == 10000.0 && (values[2] - d1).abs() < 1e-9 * d1, "{line}");
|
||||
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::Yeoh { c20: -50.0, c30: 2.0 }), ..steel() }).unwrap();
|
||||
let deck = abaqus::write(&m).unwrap();
|
||||
let line = deck.split("*HYPERELASTIC, YEOH\n").nth(1).and_then(|s| s.lines().next()).unwrap();
|
||||
let values: Vec<f64> = line.split(", ").map(|v| v.parse().unwrap()).collect();
|
||||
assert!((values[0] - c10).abs() < 1e-9 * c10 && values[1..3] == [-50.0, 2.0] && (values[3] - d1).abs() < 1e-9 * d1, "{line}");
|
||||
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::NeoHookean {}), ..steel() }).unwrap();
|
||||
|
||||
m.add_step(step("Linear", Procedure::Static {}, &["clamp"], &["end"]), None).unwrap();
|
||||
assert!(unsupported(&m).contains("step `Linear`: a linear static step beside nonlinear steps of hyperelastic material `Steel`"));
|
||||
m.remove_step("Bend").unwrap();
|
||||
assert!(abaqus::write(&m).unwrap().contains("*ELASTIC, TYPE=ISOTROPIC\n"), "with linear steps only, the elastic constants are the whole of it");
|
||||
}
|
||||
|
||||
/// A plastic material in nonlinear steps is `*PLASTIC` in a general step; what Abaqus would
|
||||
/// read differently is refused: a load path, and several general steps (Abaqus carries the
|
||||
/// plastic state between them).
|
||||
#[test]
|
||||
fn nonlinear_plastic_steps_and_what_is_refused() {
|
||||
use fieldcraft_model::{Hardening, NonlinearStatic, Plastic};
|
||||
let mut m = cantilever();
|
||||
let plastic = Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 2000.0, hardening: Hardening::Isotropic };
|
||||
m.replace_material("Steel", Material { plastic: Some(plastic), ..steel() }).unwrap();
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic::default()), &["clamp"], &["end"])).unwrap();
|
||||
let deck = abaqus::write(&m).unwrap();
|
||||
assert!(deck.contains("*PLASTIC\n250.0, 0.0\n") && deck.contains("NLGEOM=YES"), "{deck}");
|
||||
let path = NonlinearStatic { load_path: Some(vec![[0.0, 0.0], [0.5, 1.0], [1.0, 0.0]]), ..NonlinearStatic::default() };
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(path), &["clamp"], &["end"])).unwrap();
|
||||
assert!(unsupported(&m).contains("step `Bend`: a load path"));
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic::default()), &["clamp"], &["end"])).unwrap();
|
||||
m.add_step(step("Again", Procedure::NonlinearStatic(NonlinearStatic::default()), &["clamp"], &["end"]), None).unwrap();
|
||||
assert!(unsupported(&m).contains("plastic material `Steel` in a deck of 2 general steps"));
|
||||
}
|
||||
@@ -760,6 +760,19 @@ fn nonlinear_static_steps() {
|
||||
assert_eq!(auto.iter().map(num).collect::<Vec<_>>(), [1.0, 1e-4, 0.5], "blank fields are skipped: {auto:?}");
|
||||
assert!(deck.contains("*DEFINE_CURVE_TITLE\nBend ramp\n"), "the loads follow the ramp");
|
||||
assert!(deck.contains("*LOAD_NODE"), "{deck}");
|
||||
// A load path is the curve.
|
||||
let path = NonlinearStatic { load_path: Some(vec![[0.0, 0.0], [0.5, 1.0], [1.0, -1.0]]), ..controls.clone() };
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(path), &["clamp"], &["tip_load"])).unwrap();
|
||||
let deck = lsdyna::write(&m).unwrap().text;
|
||||
let curve: Vec<Vec<f64>> = deck
|
||||
.lines()
|
||||
.skip_while(|l| *l != "Bend ramp")
|
||||
.filter(|l| !l.starts_with('$'))
|
||||
.skip(2)
|
||||
.take(3)
|
||||
.map(|l| l.split_whitespace().map(|v| v.parse().unwrap()).collect())
|
||||
.collect();
|
||||
assert_eq!(curve, [vec![0.0, 0.0], vec![0.5, 1.0], vec![1.0, -1.0]], "{deck}");
|
||||
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic { nonlinear_geometry: false, ..controls }), &["clamp"], &["tip_load"]))
|
||||
.unwrap();
|
||||
|
||||
@@ -81,7 +81,7 @@ definition_commands!(
|
||||
Material,
|
||||
"material",
|
||||
"Material",
|
||||
r#"{"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3} | {"type": "engineering_constants", "young": [E1, E2, E3], "poisson": [ν12, ν13, ν23], "shear": [G12, G13, G23]}, "density": 7.85e-9, "fluid": {"type": "newtonian", "viscosity": 1.0e-9} (dynamic viscosity; a fluid needs a density), "plastic": {…} (see model.material.plasticity), "damage": {…}, "erosion": {…} (see model.material.damage), "hyperelastic": {"type": "neo_hookean"} (large strain in nonlinear steps, from the isotropic constants: μ = E/(2(1 + ν)), κ = E/(3(1 − 2ν)); without it an elastic material is St Venant–Kirchhoff there)} — in the model's unit system (model.units), never converted"#,
|
||||
r#"{"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3} | {"type": "engineering_constants", "young": [E1, E2, E3], "poisson": [ν12, ν13, ν23], "shear": [G12, G13, G23]}, "density": 7.85e-9, "fluid": {"type": "newtonian", "viscosity": 1.0e-9} (dynamic viscosity; a fluid needs a density), "plastic": {…} (see model.material.plasticity), "damage": {…}, "erosion": {…} (see model.material.damage), "hyperelastic": {"type": "neo_hookean"} | {"type": "mooney_rivlin", "c01": 0.1} (C10 = μ/2 − C01) | {"type": "yeoh", "c20": -0.01, "c30": 0.001} (C10 = μ/2) (large strain in nonlinear steps, from the isotropic constants: μ = E/(2(1 + ν)), κ = E/(3(1 − 2ν)); without it an elastic material is St Venant–Kirchhoff there)} — in the model's unit system (model.units), never converted"#,
|
||||
add_material,
|
||||
replace_material,
|
||||
remove_material,
|
||||
@@ -266,7 +266,7 @@ fn fluid_boundary(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
pub const STEP_CREATE: CommandSpec = CommandSpec {
|
||||
id: "model.step.create",
|
||||
label: "New Step",
|
||||
params: r#"{"name": "Bend", "procedure": {"type": "static"} | {"type": "nonlinear_static", "nonlinear_geometry": true, "initial_increment": 0.1, "min_increment": 1e-5, "max_increment": 1.0 (fractions of the step), "max_increments": 100, "max_iterations": 16 (per increment), "tolerances": {"force": 1e-6, "displacement": 1e-5, "energy": 1e-10}, "line_search": false} (loads and prescribed displacements ramp with the load factor from 0 to 1) | {"type": "frequency", "modes": 10, "min_hz": 0, "max_hz": 500} | {"type": "fluid", "domain": "Flow", "end_time": 1.0 | "steps": 1000, "time_step": (default: chosen for Mach 0.087 and τ ≤ 1), "frames": 10, "magic": 0.1875 (the two-relaxation-time Λ)} | {"type": "explicit_dynamic", "end_time": 0.001, "dt_scale": 0.9, "max_increments": 10000000, "output_interval", "history_interval", "history_nodes": "set", "initial_velocities": [{"set", "velocity": [vx, vy, vz]}], "rigid_walls": [{"set", "point", "normal"}], "hourglass": {"type": "viscous|stiffness", "coefficient": 0.1}, "bulk_viscosity": {"linear": 0.06, "quadratic": 1.5}}, "output": ["displacement", "reaction_force", "stress", "strain"] (default by procedure), "boundary_conditions": [names], "loads": [names] (active in this step; nothing carries over between steps), "before": "step" (default: last)}"#,
|
||||
params: r#"{"name": "Bend", "procedure": {"type": "static"} | {"type": "nonlinear_static", "nonlinear_geometry": true, "initial_increment": 0.1, "min_increment": 1e-5, "max_increment": 1.0 (fractions of the step), "max_increments": 100, "max_iterations": 16 (per increment), "tolerances": {"force": 1e-6, "displacement": 1e-5, "energy": 1e-10}, "line_search": false, "load_path": [[0, 0], [0.5, 1], [1, 0]] (optional: the load factor against the step fraction, piecewise linear from [0, 0] to fraction 1; increments end at its points)} (loads and prescribed displacements are the load factor times their values; it ramps from 0 to 1 without a load path) | {"type": "frequency", "modes": 10, "min_hz": 0, "max_hz": 500} | {"type": "fluid", "domain": "Flow", "end_time": 1.0 | "steps": 1000, "time_step": (default: chosen for Mach 0.087 and τ ≤ 1), "frames": 10, "magic": 0.1875 (the two-relaxation-time Λ)} | {"type": "explicit_dynamic", "end_time": 0.001, "dt_scale": 0.9, "max_increments": 10000000, "output_interval", "history_interval", "history_nodes": "set", "initial_velocities": [{"set", "velocity": [vx, vy, vz]}], "rigid_walls": [{"set", "point", "normal"}], "hourglass": {"type": "viscous|stiffness", "coefficient": 0.1}, "bulk_viscosity": {"linear": 0.06, "quadratic": 1.5}}, "output": ["displacement", "reaction_force", "stress", "strain"] (default by procedure), "boundary_conditions": [names], "loads": [names] (active in this step; nothing carries over between steps), "before": "step" (default: last)}"#,
|
||||
kind: CommandKind::Model,
|
||||
run: step_create,
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ pub const STATIC: CommandSpec = CommandSpec {
|
||||
pub const NONLINEAR: CommandSpec = CommandSpec {
|
||||
id: "solve.nonlinear",
|
||||
label: "Solve Nonlinear Static Step",
|
||||
params: r#"{"step": "Inflate", "max_dofs": 6000000, "max_factor_entries": 400000000, "max_newton_iterations": 100000, "threads": 8} — solves a nonlinear static step (the only one when `step` is left out) in load increments with Newton–Raphson iterations and keeps the result in the session: the final displacements, reactions, Cauchy stresses and logarithmic strains, every increment's load factor, iterations and residuals (force, displacement and energy, relative to their tolerances' scales), and a displacement frame per converged increment; the caps may only be lowered (`max_newton_iterations` over the whole step); `threads` as for solve.static"#,
|
||||
params: r#"{"step": "Inflate", "max_dofs": 6000000, "max_factor_entries": 400000000, "max_newton_iterations": 100000, "threads": 8} — solves a nonlinear static step (the only one when `step` is left out) in load increments with Newton–Raphson iterations and keeps the result in the session: the final displacements, reactions, Cauchy stresses and logarithmic strains (small strains without nonlinear geometry), the equivalent plastic strain of plastic materials (the largest in the summary), every increment's load factor, iterations and residuals (force, displacement and energy, relative to their tolerances' scales), and a displacement frame per converged increment; the caps may only be lowered (`max_newton_iterations` over the whole step); `threads` as for solve.static"#,
|
||||
kind: CommandKind::Model,
|
||||
run: run_nonlinear,
|
||||
};
|
||||
@@ -82,7 +82,7 @@ pub const FLUID: CommandSpec = CommandSpec {
|
||||
pub const PROBE: CommandSpec = CommandSpec {
|
||||
id: "solve.probe",
|
||||
label: "Probe Results",
|
||||
params: r#"{"step": "Load", "nodes": [1, 2], "mode": 1, "frame": 0} — the stored result at nodes: displacement, rotation, reaction, reaction moment, stress and von Mises (static, with `elements` each element's results: solid, 2D and truss nodal stresses, shell section forces (N, M, Q per length in the element's axes) and bottom, middle and top stresses, beam end forces (N, V_y, V_z, T, M_y, M_z in its axes)), the shape of mode `mode` with its rotations (frequency, from 1), or displacement and velocity in field frame `frame` (explicit, from 0; the last by default) at `nodes` (or at the nodes nearest to points: "at": [[x, y, z], ...], lowest label on a tie, each entry then giving its `xyz` and `distance`) and, with `elements` (explicit), each element's stress, von Mises stress, strain, equivalent plastic strain, damage, temperature and whether it has eroded; a coupled reference point's label gives its six displacements (translations, rotations) and reactions (forces, moments). Fluid steps: {"step": "Flow", "points": [[x, y, z]], "frame": 10 (default the last)} gives velocity and pressure in the cells at the points; {"step": "Flow", "forces": ["x_min", "F2:top", "Body1/F2:top"]} gives those walls' force histories"#,
|
||||
params: r#"{"step": "Load", "nodes": [1, 2], "mode": 1, "frame": 0} — the stored result at nodes: displacement, rotation, reaction, reaction moment, stress and von Mises (static, with `elements` each element's results: solid, 2D and truss nodal stresses (and, in a nonlinear step, the equivalent plastic strain at the integration points of a plastic element), shell section forces (N, M, Q per length in the element's axes) and bottom, middle and top stresses, beam end forces (N, V_y, V_z, T, M_y, M_z in its axes)), the shape of mode `mode` with its rotations (frequency, from 1), or displacement and velocity in field frame `frame` (explicit, from 0; the last by default) at `nodes` (or at the nodes nearest to points: "at": [[x, y, z], ...], lowest label on a tie, each entry then giving its `xyz` and `distance`) and, with `elements` (explicit), each element's stress, von Mises stress, strain, equivalent plastic strain, damage, temperature and whether it has eroded; a coupled reference point's label gives its six displacements (translations, rotations) and reactions (forces, moments). Fluid steps: {"step": "Flow", "points": [[x, y, z]], "frame": 10 (default the last)} gives velocity and pressure in the cells at the points; {"step": "Flow", "forces": ["x_min", "F2:top", "Body1/F2:top"]} gives those walls' force histories"#,
|
||||
kind: CommandKind::Model,
|
||||
run: probe,
|
||||
};
|
||||
@@ -220,6 +220,7 @@ fn run_nonlinear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"frames": history.map(|h| h.load_factors()),
|
||||
"max_displacement": largest(r.nodes.iter().zip(&r.displacement).map(|(l, u)| (*l, crate::results::magnitude(*u)))),
|
||||
"max_von_mises": largest(r.nodes.iter().zip(&r.von_mises).map(|(l, v)| (*l, *v))),
|
||||
"max_plastic_strain": largest(r.elements.iter().flat_map(|e| e.plastic_strain.iter().map(|v| (e.label, *v)))),
|
||||
"applied_total": r.applied_total,
|
||||
"reaction_total": r.reaction_total,
|
||||
"reference_points": r.reference_points.iter().map(|p| json!({
|
||||
@@ -613,7 +614,11 @@ fn by_label<T>(list: &[T], label: u32, key: impl Fn(&T) -> u32) -> Option<&T> {
|
||||
/// The stored results of one element of a static step.
|
||||
fn element_result(r: &StaticResult, label: u32) -> Option<Value> {
|
||||
if let Some(e) = by_label(&r.elements, label, |e| e.label) {
|
||||
return Some(json!({"element": label, "kind": e.kind.name(), "stress": e.nodal}));
|
||||
let mut v = json!({"element": label, "kind": e.kind.name(), "stress": e.nodal});
|
||||
if !e.plastic_strain.is_empty() {
|
||||
v["plastic_strain"] = json!(e.plastic_strain);
|
||||
}
|
||||
return Some(v);
|
||||
}
|
||||
if let Some(e) = by_label(&r.shells, label, |e| e.label) {
|
||||
let s = &e.stress;
|
||||
|
||||
+168
-33
@@ -36,11 +36,22 @@
|
||||
//! §6.6, eqs. 6.165–6.169 and 6.193, in Kirchhoff form). The split is what the mixed (hybrid)
|
||||
//! elements need: they take the isochoric part from [`MaterialModel::isochoric`] and the
|
||||
//! volumetric one from [`MaterialModel::volumetric`] with the element's own dilatation.
|
||||
//! - **Mooney–Rivlin and Yeoh**: decoupled polynomials
|
||||
//! W̄ = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ with the neo-Hookean volume; their fictitious stress and
|
||||
//! elasticity from Holzapfel 2000, eqs. 6.193 and 6.168 ([`MaterialModel::Polynomial`]).
|
||||
//! - **J2 plasticity** ([`crate::elastoplastic`]): Simo's multiplicative plasticity with the
|
||||
//! exponential return map in the principal logarithmic strains (finite strain), or the
|
||||
//! additive radial return (small strain, [`MaterialModel::small_strain`]); decoupled too, the
|
||||
//! return deviatoric and the Hencky volume K/2 (ln J)².
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use fieldcraft_model::{Elastic, Hyperelastic};
|
||||
|
||||
use crate::dense::det3;
|
||||
use crate::elastoplastic;
|
||||
use crate::material::{Elasticity, elasticity, stress};
|
||||
use crate::plasticity::J2;
|
||||
use crate::rotation::{IDENTITY, Mat3, mul, transpose, voigt};
|
||||
use crate::{Error, Result};
|
||||
|
||||
@@ -61,11 +72,24 @@ pub struct Response {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum MaterialModel {
|
||||
StVenantKirchhoff(Box<Elasticity>),
|
||||
NeoHookean { shear: f64, bulk: f64 },
|
||||
NeoHookean {
|
||||
shear: f64,
|
||||
bulk: f64,
|
||||
},
|
||||
/// A decoupled polynomial in the isochoric invariants,
|
||||
/// W̄ = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ with `c` = (C₁₀, C₀₁, C₂₀, C₃₀), and U = κ/2 (J − 1)²:
|
||||
/// Mooney–Rivlin and Yeoh.
|
||||
Polynomial {
|
||||
name: &'static str,
|
||||
c: [f64; 4],
|
||||
bulk: f64,
|
||||
},
|
||||
/// Von Mises plasticity (shared: one per material, whatever the element count).
|
||||
Plastic(Arc<J2>),
|
||||
}
|
||||
|
||||
/// The 6 × 6 matrix of the symmetric fourth-order identity 𝕀 (engineering-shear columns).
|
||||
fn identity4() -> Tangent {
|
||||
pub(crate) fn identity4() -> Tangent {
|
||||
let mut i4 = [[0.0; 6]; 6];
|
||||
for k in 0..6 {
|
||||
i4[k][k] = if k < 3 { 1.0 } else { 0.5 };
|
||||
@@ -110,27 +134,35 @@ fn jacobian(f: &Mat3) -> Result<f64> {
|
||||
}
|
||||
|
||||
impl MaterialModel {
|
||||
/// The model of a material's elastic behaviour in a nonlinear step: neo-Hookean when the
|
||||
/// material asks for it (its isotropic constants give μ and κ), St Venant–Kirchhoff
|
||||
/// The model of a material's elastic behaviour in a nonlinear step: the hyperelastic option
|
||||
/// when the material has one (its isotropic constants give μ and κ), St Venant–Kirchhoff
|
||||
/// otherwise.
|
||||
pub fn from_model(elastic: &Elastic, hyperelastic: Option<&Hyperelastic>) -> Result<MaterialModel> {
|
||||
match hyperelastic {
|
||||
None => Ok(MaterialModel::StVenantKirchhoff(Box::new(elasticity(elastic)?))),
|
||||
Some(Hyperelastic::NeoHookean {}) => {
|
||||
let (young, poisson) = crate::material::isotropic_constants(elastic, "neo-Hookean")?;
|
||||
Some(h) => {
|
||||
let (young, poisson) = crate::material::isotropic_constants(elastic, h.name())?;
|
||||
let (shear, bulk) = Hyperelastic::moduli(young, poisson);
|
||||
Ok(MaterialModel::NeoHookean { shear, bulk })
|
||||
Ok(Self::hyperelastic(h, shear, bulk))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The hyperelastic model of an option with small-strain moduli μ and κ.
|
||||
fn hyperelastic(h: &Hyperelastic, shear: f64, bulk: f64) -> MaterialModel {
|
||||
match h {
|
||||
Hyperelastic::NeoHookean {} => MaterialModel::NeoHookean { shear, bulk },
|
||||
_ => MaterialModel::Polynomial { name: h.name(), c: h.constants(shear), bulk },
|
||||
}
|
||||
}
|
||||
|
||||
/// The model of an element whose elasticity is `d` (the small-strain one) and whose material
|
||||
/// has the large-strain option `hyperelastic`: neo-Hookean takes μ = D₄₄ and κ = D₁₂ + ⅔ μ of
|
||||
/// an isotropic `d`, St Venant–Kirchhoff `d` itself.
|
||||
/// has the large-strain option `hyperelastic`: the hyperelastic models take μ = D₄₄ and
|
||||
/// κ = D₁₂ + ⅔ μ of an isotropic `d`, St Venant–Kirchhoff `d` itself.
|
||||
pub fn from_elasticity(d: &Elasticity, hyperelastic: Option<&Hyperelastic>) -> MaterialModel {
|
||||
match hyperelastic {
|
||||
None => MaterialModel::StVenantKirchhoff(Box::new(*d)),
|
||||
Some(Hyperelastic::NeoHookean {}) => MaterialModel::NeoHookean { shear: d[3][3], bulk: d[0][1] + 2.0 / 3.0 * d[3][3] },
|
||||
Some(h) => Self::hyperelastic(h, d[3][3], d[0][1] + 2.0 / 3.0 * d[3][3]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,23 +170,43 @@ impl MaterialModel {
|
||||
match self {
|
||||
MaterialModel::StVenantKirchhoff(_) => "St Venant–Kirchhoff",
|
||||
MaterialModel::NeoHookean { .. } => "neo-Hookean",
|
||||
MaterialModel::Polynomial { name, .. } => name,
|
||||
MaterialModel::Plastic(_) => "J2 plastic",
|
||||
}
|
||||
}
|
||||
|
||||
/// History variables per integration point.
|
||||
pub fn history_len(&self) -> usize {
|
||||
0
|
||||
match self {
|
||||
MaterialModel::Plastic(_) => elastoplastic::HISTORY,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the response depends on the path (the material keeps history), so the solver
|
||||
/// treats it with the inelastic kinematics and predictors.
|
||||
pub fn is_path_dependent(&self) -> bool {
|
||||
self.history_len() > 0
|
||||
}
|
||||
|
||||
/// The equivalent plastic strain in one point's history; `None` for an elastic material.
|
||||
pub fn plastic_strain(&self, history: &[f64]) -> Option<f64> {
|
||||
match self {
|
||||
MaterialModel::Plastic(_) => Some(elastoplastic::value(history, elastoplastic::PLASTIC_STRAIN)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the strain energy splits into isochoric and volumetric parts (what mixed
|
||||
/// elements need).
|
||||
pub fn is_decoupled(&self) -> bool {
|
||||
matches!(self, MaterialModel::NeoHookean { .. })
|
||||
!matches!(self, MaterialModel::StVenantKirchhoff(_))
|
||||
}
|
||||
|
||||
/// The Kirchhoff stress and spatial tangent at deformation gradient `f`, from the history
|
||||
/// The Kirchhoff stress and spatial tangent at deformation gradient `f` and (for
|
||||
/// temperature-dependent plasticity) temperature `temperature`, from the history
|
||||
/// `committed` (written, updated, to `updated`).
|
||||
pub fn response(&self, f: &Mat3, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
pub fn response(&self, f: &Mat3, temperature: Option<f64>, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
updated.iter_mut().zip(committed).for_each(|(u, c)| *u = *c);
|
||||
let j = jacobian(f)?;
|
||||
match self {
|
||||
@@ -165,18 +217,25 @@ impl MaterialModel {
|
||||
let t = push_forward(f);
|
||||
Ok(Response { stress: std::array::from_fn(|a| (0..6).map(|b| t[a][b] * s[b]).sum()), tangent: sandwich(&t, d) })
|
||||
}
|
||||
MaterialModel::NeoHookean { .. } => {
|
||||
let iso = self.isochoric(f)?;
|
||||
let (p, dp) = self.volumetric(j).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
|
||||
MaterialModel::NeoHookean { .. } | MaterialModel::Polynomial { .. } | MaterialModel::Plastic(_) => {
|
||||
let iso = self.isochoric(f, temperature, committed, updated)?;
|
||||
let (p, dp) = self.volumetric(j, false).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
|
||||
Ok(add_pressure(&iso, j, p, j * dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The isochoric part of a decoupled material (τ_iso = dev τ̄ and its tangent).
|
||||
pub fn isochoric(&self, f: &Mat3) -> Result<Response> {
|
||||
let &MaterialModel::NeoHookean { shear: mu, .. } = self else {
|
||||
return Err(Error::NotSupported(format!("the isochoric–volumetric split of a {} material", self.name())));
|
||||
/// The isochoric part of a decoupled material (τ_iso = dev τ̄ and its tangent), from the
|
||||
/// history `committed`; the history that changes is written to `updated`, which the caller
|
||||
/// starts as a copy of `committed`.
|
||||
pub fn isochoric(&self, f: &Mat3, temperature: Option<f64>, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
let mu = match self {
|
||||
MaterialModel::NeoHookean { shear, .. } => *shear,
|
||||
MaterialModel::Plastic(j2) => return elastoplastic::finite_isochoric(j2, f, temperature, committed, updated),
|
||||
MaterialModel::Polynomial { c, .. } => return polynomial_isochoric(c, f),
|
||||
MaterialModel::StVenantKirchhoff(_) => {
|
||||
return Err(Error::NotSupported(format!("the isochoric–volumetric split of a {} material", self.name())));
|
||||
}
|
||||
};
|
||||
let j = jacobian(f)?;
|
||||
let scale = j.powf(-2.0 / 3.0);
|
||||
@@ -199,13 +258,74 @@ impl MaterialModel {
|
||||
Ok(Response { stress: tau, tangent: c })
|
||||
}
|
||||
|
||||
/// U′(J) and U″(J) of a decoupled material's volumetric energy.
|
||||
pub fn volumetric(&self, j: f64) -> Option<(f64, f64)> {
|
||||
match *self {
|
||||
MaterialModel::NeoHookean { bulk, .. } => Some((bulk * (j - 1.0), bulk)),
|
||||
/// U′(J) and U″(J) of a decoupled material's volumetric energy at dilatation J (with
|
||||
/// `small`, J = 1 + tr ε of small strain, where every material's is K/2 (J − 1)²).
|
||||
pub fn volumetric(&self, j: f64, small: bool) -> Option<(f64, f64)> {
|
||||
match self {
|
||||
MaterialModel::NeoHookean { bulk, .. } | MaterialModel::Polynomial { bulk, .. } => Some((bulk * (j - 1.0), *bulk)),
|
||||
MaterialModel::Plastic(j2) => elastoplastic::volumetric(j2, j, small),
|
||||
MaterialModel::StVenantKirchhoff(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Small strain: the stress and its tangent at the (mechanical) strain `strain`
|
||||
/// (engineering shears), from the history `committed` (written, updated, to `updated`);
|
||||
/// `bulk` false keeps the deviatoric part only (what a mixed element takes). St
|
||||
/// Venant–Kirchhoff is linear elasticity here; hyperelastic materials have no small-strain
|
||||
/// form.
|
||||
pub fn small_strain(&self, strain: &[f64; 6], temperature: Option<f64>, committed: &[f64], updated: &mut [f64], bulk: bool) -> Result<Response> {
|
||||
updated.iter_mut().zip(committed).for_each(|(u, c)| *u = *c);
|
||||
match self {
|
||||
MaterialModel::Plastic(j2) => elastoplastic::small(j2, strain, temperature, committed, updated, bulk),
|
||||
MaterialModel::StVenantKirchhoff(d) if bulk => Ok(Response { stress: stress(d, strain), tangent: **d }),
|
||||
_ => Err(Error::NotSupported(format!("the {} material at small strain{}", self.name(), if bulk { "" } else { " in mixed elements" }))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The isochoric response of the polynomial energy W̄(Ī₁, Ī₂) with `c` = (C₁₀, C₀₁, C₂₀, C₃₀)
|
||||
/// (Holzapfel 2000, §6.5–6.6, in Kirchhoff form): the fictitious stress
|
||||
/// τ̄ = 2 (Ψ₁ + Ī₁ Ψ₂) b̄ − 2 Ψ₂ b̄², τ_iso = dev τ̄, and with the fictitious elasticity
|
||||
/// c̄ = 4 (Ψ₁₁ + Ψ₂) b̄ ⊗ b̄ − 4 Ψ₂ b̄ ⊙ b̄ (eq. 6.193 pushed forward, Ψ₁₂ = Ψ₂₂ = 0 here),
|
||||
/// c_iso = ℙ : c̄ : ℙ + ⅔ tr τ̄ ℙ − ⅔ (τ_iso ⊗ I + I ⊗ τ_iso) (eq. 6.168). With C₁₀ alone it is
|
||||
/// the neo-Hookean response.
|
||||
fn polynomial_isochoric(c: &[f64; 4], f: &Mat3) -> Result<Response> {
|
||||
let [c10, c01, c20, c30] = *c;
|
||||
let j = jacobian(f)?;
|
||||
let scale = j.powf(-2.0 / 3.0);
|
||||
let b = mul(f, &transpose(f)).map(|row| row.map(|v| scale * v));
|
||||
let b2 = mul(&b, &b);
|
||||
let i1 = b[0][0] + b[1][1] + b[2][2];
|
||||
let x = i1 - 3.0;
|
||||
let (psi1, psi2, psi11) = (c10 + 2.0 * c20 * x + 3.0 * c30 * x * x, c01, 2.0 * c20 + 6.0 * c30 * x);
|
||||
let (bv, b2v) = (voigt(&b), voigt(&b2));
|
||||
let fict: [f64; 6] = std::array::from_fn(|a| 2.0 * (psi1 + i1 * psi2) * bv[a] - 2.0 * psi2 * b2v[a]);
|
||||
let tr = fict[0] + fict[1] + fict[2];
|
||||
let mut tau = fict;
|
||||
for v in tau.iter_mut().take(3) {
|
||||
*v -= tr / 3.0;
|
||||
}
|
||||
let mut cbar = [[0.0; 6]; 6];
|
||||
for (a, &(i, jj)) in PAIRS.iter().enumerate() {
|
||||
for (bb, &(k, l)) in PAIRS.iter().enumerate() {
|
||||
cbar[a][bb] = 4.0 * (psi11 + psi2) * bv[a] * bv[bb] - 2.0 * psi2 * (b[i][k] * b[jj][l] + b[i][l] * b[jj][k]);
|
||||
}
|
||||
}
|
||||
// ℙ : c̄ : ℙ = c̄ − ⅓ I ⊗ (I : c̄) − ⅓ (c̄ : I) ⊗ I + ⅑ (I : c̄ : I) I ⊗ I.
|
||||
let row: [f64; 6] = std::array::from_fn(|a| (0..3).map(|bb| cbar[a][bb]).sum());
|
||||
let col: [f64; 6] = std::array::from_fn(|bb| (0..3).map(|a| cbar[a][bb]).sum());
|
||||
let all: f64 = (0..3).map(|a| row[a]).sum();
|
||||
let i4 = identity4();
|
||||
let mut out = [[0.0; 6]; 6];
|
||||
for a in 0..6 {
|
||||
for bb in 0..6 {
|
||||
let (ia, ib) = (if a < 3 { 1.0 } else { 0.0 }, if bb < 3 { 1.0 } else { 0.0 });
|
||||
let projected = cbar[a][bb] - ia * col[bb] / 3.0 - row[a] * ib / 3.0 + ia * ib * all / 9.0;
|
||||
let projector = i4[a][bb] - ia * ib / 3.0;
|
||||
out[a][bb] = projected + 2.0 / 3.0 * tr * projector - 2.0 / 3.0 * (tau[a] * ib + ia * tau[bb]);
|
||||
}
|
||||
}
|
||||
Ok(Response { stress: tau, tangent: out })
|
||||
}
|
||||
|
||||
/// An isochoric response with a pressure p acting on dilatation J: τ + J p I, and the tangent
|
||||
@@ -269,7 +389,7 @@ mod tests {
|
||||
/// The tangent's consistency: dS = C : dE with C the pull-back of c, checked by central
|
||||
/// differences of S along several directions dF (dE = sym(Fᵀ dF), engineering shears).
|
||||
fn check_tangent(m: &MaterialModel, f: &Mat3) {
|
||||
let r = m.response(f, &[], &mut []).unwrap();
|
||||
let r = m.response(f, None, &[], &mut []).unwrap();
|
||||
let (inv, _) = inverse3(f).unwrap();
|
||||
let back = sandwich(&push_forward(&inv), &r.tangent);
|
||||
let scale = back.iter().flatten().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
@@ -280,7 +400,7 @@ mod tests {
|
||||
df[(k + 1) % 3][(k + 2) % 3] += 0.3;
|
||||
let at = |s: f64| {
|
||||
let fp: Mat3 = std::array::from_fn(|i| std::array::from_fn(|j| f[i][j] + s * df[i][j]));
|
||||
pull_back_stress(&fp, &m.response(&fp, &[], &mut []).unwrap().stress)
|
||||
pull_back_stress(&fp, &m.response(&fp, None, &[], &mut []).unwrap().stress)
|
||||
};
|
||||
let (sp, sm) = (at(h), at(-h));
|
||||
let ftdf = mul(&transpose(f), &df);
|
||||
@@ -300,13 +420,28 @@ mod tests {
|
||||
check_tangent(&m, &deformation());
|
||||
check_tangent(&m, &IDENTITY);
|
||||
// Symmetric (hyperelastic): major symmetry of c.
|
||||
let c = m.response(&deformation(), &[], &mut []).unwrap().tangent;
|
||||
let c = m.response(&deformation(), None, &[], &mut []).unwrap().tangent;
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
assert!((c[a][b] - c[b][a]).abs() < 1e-9 * 5000.0, "{a} {b}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Mooney–Rivlin and Yeoh: consistent and symmetric; with C₁₀ alone the neo-Hookean model.
|
||||
for m in [
|
||||
MaterialModel::Polynomial { name: "Mooney–Rivlin", c: [120.0, 30.0, 0.0, 0.0], bulk: 5000.0 },
|
||||
MaterialModel::Polynomial { name: "Yeoh", c: [150.0, 0.0, -12.0, 1.5], bulk: 5000.0 },
|
||||
] {
|
||||
check_tangent(&m, &deformation());
|
||||
check_tangent(&m, &IDENTITY);
|
||||
let c = m.response(&deformation(), None, &[], &mut []).unwrap().tangent;
|
||||
assert!((0..36).all(|k| (c[k / 6][k % 6] - c[k % 6][k / 6]).abs() < 1e-9 * 5000.0), "{} symmetric", m.name());
|
||||
}
|
||||
let neo = MaterialModel::NeoHookean { shear: 300.0, bulk: 5000.0 };
|
||||
let poly = MaterialModel::Polynomial { name: "neo", c: [150.0, 0.0, 0.0, 0.0], bulk: 5000.0 };
|
||||
let (a, b) = (neo.response(&deformation(), None, &[], &mut []).unwrap(), poly.response(&deformation(), None, &[], &mut []).unwrap());
|
||||
assert!(a.stress.iter().zip(&b.stress).all(|(x, y)| (x - y).abs() < 1e-11 * 300.0));
|
||||
assert!(a.tangent.iter().flatten().zip(b.tangent.iter().flatten()).all(|(x, y)| (x - y).abs() < 1e-10 * 5000.0));
|
||||
let ortho = crate::material::orthotropic([1000.0, 300.0, 500.0], [0.3, 0.2, 0.25], [100.0, 120.0, 90.0]).unwrap();
|
||||
check_tangent(&MaterialModel::StVenantKirchhoff(Box::new(ortho)), &deformation());
|
||||
}
|
||||
@@ -317,7 +452,7 @@ mod tests {
|
||||
let d = isotropic(e, nu).unwrap();
|
||||
let neo = MaterialModel::from_model(&Elastic::Isotropic { young: e, poisson: nu }, Some(&Hyperelastic::NeoHookean {})).unwrap();
|
||||
for m in [MaterialModel::StVenantKirchhoff(Box::new(d)), neo] {
|
||||
let c = m.response(&IDENTITY, &[], &mut []).unwrap().tangent;
|
||||
let c = m.response(&IDENTITY, None, &[], &mut []).unwrap().tangent;
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
assert!((c[a][b] - d[a][b]).abs() < 1e-9 * e, "{}: {a} {b}", m.name());
|
||||
@@ -329,7 +464,7 @@ mod tests {
|
||||
let k = crate::rotation::skew(&axis);
|
||||
let k2 = mul(&k, &k);
|
||||
let rot: Mat3 = std::array::from_fn(|i| std::array::from_fn(|j| IDENTITY[i][j] + s * k[i][j] + (1.0 - cth) * k2[i][j]));
|
||||
let tau = m.response(&rot, &[], &mut []).unwrap().stress;
|
||||
let tau = m.response(&rot, None, &[], &mut []).unwrap().stress;
|
||||
assert!(tau.iter().all(|v| v.abs() < 1e-12 * e), "{}: {tau:?}", m.name());
|
||||
}
|
||||
}
|
||||
@@ -343,14 +478,14 @@ mod tests {
|
||||
let (lam, l) = (1.5f64, 0.85f64);
|
||||
let f = [[lam, 0.0, 0.0], [0.0, l, 0.0], [0.0, 0.0, l]];
|
||||
let j = lam * l * l;
|
||||
let tau = m.response(&f, &[], &mut []).unwrap().stress;
|
||||
let tau = m.response(&f, None, &[], &mut []).unwrap().stress;
|
||||
let i1 = lam * lam + 2.0 * l * l;
|
||||
let sxx = mu / j.powf(5.0 / 3.0) * (lam * lam - i1 / 3.0) + kappa * (j - 1.0);
|
||||
let syy = mu / j.powf(5.0 / 3.0) * (l * l - i1 / 3.0) + kappa * (j - 1.0);
|
||||
assert!((tau[0] / j - sxx).abs() < 1e-10 * mu && (tau[1] / j - syy).abs() < 1e-10 * mu, "{tau:?}");
|
||||
let e = logarithmic_strain(&f).unwrap();
|
||||
assert!((e[0] - lam.ln()).abs() < 1e-14 && (e[1] - l.ln()).abs() < 1e-14 && e[3].abs() < 1e-14, "{e:?}");
|
||||
assert!(m.response(&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]], &[], &mut []).is_err(), "inverted");
|
||||
assert!(MaterialModel::StVenantKirchhoff(Box::new(isotropic(1.0, 0.3).unwrap())).isochoric(&IDENTITY).is_err());
|
||||
assert!(m.response(&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]], None, &[], &mut []).is_err(), "inverted");
|
||||
assert!(MaterialModel::StVenantKirchhoff(Box::new(isotropic(1.0, 0.3).unwrap())).isochoric(&IDENTITY, None, &[], &mut []).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
//! Von Mises (J2) elastoplasticity at a material point of implicit (Newton) analysis: the
|
||||
//! stress after an increment and its consistent tangent, at finite strain and at small strain.
|
||||
//! Both kinematics call the radial return of [`crate::plasticity`], the update explicit
|
||||
//! dynamics uses, so the yield surface, the hardening (bilinear isotropic or linear kinematic,
|
||||
//! tabulated, Johnson–Cook) and the plastic strain increment of a trial state are one
|
||||
//! implementation for both solvers.
|
||||
//!
|
||||
//! **Finite strain** (J. C. Simo, *Algorithms for static and dynamic multiplicative plasticity
|
||||
//! that preserve the classical return mapping schemes of the infinitesimal theory*, CMAME
|
||||
//! 99:61–112, 1992; Simo & Hughes 1998, ch. 9): the multiplicative split F = Fₑ Fₚ with the
|
||||
//! elastic left Cauchy–Green tensor bₑ = F Cₚ⁻¹ Fᵀ and a Hencky (quadratic in the logarithmic
|
||||
//! strain) elastic energy, so the Kirchhoff stress is τ = K ln J I + 2G dev εₑ with
|
||||
//! εₑ = ½ ln bₑ. The history is Cₚ⁻¹ (stored as Cₚ⁻¹ − I, so the virgin state is zeros) and the
|
||||
//! equivalent plastic strain. An increment freezes Cₚ⁻¹ at its committed value: the trial
|
||||
//! bₑᵗʳ = F Cₚ⁻¹ Fᵀ, its principal stretches and directions, the trial logarithmic strains
|
||||
//! ε_A = ½ ln λ_A², and then the **return map of the infinitesimal theory on the principal
|
||||
//! values** (the exponential map integrates the flow rule exactly in the principal frame, which
|
||||
//! the isotropy of the model keeps fixed during the return). The update writes back
|
||||
//! bₑ = Σ exp(2 εₑ,A) n_A ⊗ n_A and Cₚ⁻¹ = F⁻¹ bₑ F⁻ᵀ, which keeps det Cₚ = 1 (plastic flow
|
||||
//! is isochoric).
|
||||
//!
|
||||
//! **Its tangent.** With Cₚ⁻¹ frozen, the Lie derivative of bₑᵗʳ vanishes, so for a rate of
|
||||
//! deformation d the trial strain changes by dεᵗʳ = ½ (∂ ln b/∂b) : (d b + b d), and
|
||||
//! L_v τ = 𝔻ᵉᵖ : dεᵗʳ − (d τ + τ d), with 𝔻ᵉᵖ the algorithmic tangent of the return map
|
||||
//! ([`crate::plasticity::J2::tangent`]). In the principal frame of b the derivative of the
|
||||
//! logarithm is the Daleckii–Krein divided difference, so dεᵗʳ_AB = θ_AB d_AB with θ_AA = 1 and
|
||||
//! θ_AB = ½ (λ²_A + λ²_B) ln(λ²_A/λ²_B)/(λ²_A − λ²_B) = artanh(r)/r, r = (λ²_A − λ²_B)/(λ²_A + λ²_B),
|
||||
//! smooth through equal stretches (its series 1 + r²/3 + r⁴/5 + … near r = 0), so uniaxial and
|
||||
//! undeformed states need no special case (Simo 1992, §3.3, gives the same tangent in spectral
|
||||
//! form; R. Bhatia, *Matrix Analysis*, Springer 1997, §V.3, for the divided differences). The
|
||||
//! tangent is symmetric for isotropic hardening: 𝔻ᵉᵖ and the θ scaling share the principal
|
||||
//! frame.
|
||||
//!
|
||||
//! **Kinematic hardening at finite strain** is refused: a back stress is not coaxial with bₑ, so
|
||||
//! the principal-frame return no longer holds, and it needs a formulation of its own (for
|
||||
//! example the Lagrangian logarithmic strain space of C. Miehe, N. Apel, M. Lambrecht, CMAME
|
||||
//! 191:5383–5425, 2002). It works with small strain.
|
||||
//!
|
||||
//! **Small strain** (Simo & Hughes 1998, §3.3): ε = εₑ + εₚ additively, the history the plastic
|
||||
//! strain εₚ, the back stress α and the equivalent plastic strain; the trial εₑ = ε − εₚ,ₙ goes
|
||||
//! through the same return, and εₚ advances by √(3/2) Δε̄ₚ n.
|
||||
//!
|
||||
//! **Mixed (hybrid) elements** take the isochoric part (the deviatoric return, without K) and the
|
||||
//! volumetric energy U(J) = K/2 (ln J)², U′ = K ln J / J (finite strain), or U = K/2 (J − 1)² with
|
||||
//! J the 1 + tr ε of small strain: J2 plasticity is pressure insensitive and isochoric, so the
|
||||
//! split is exact.
|
||||
//!
|
||||
//! **History layout** per integration point ([`HISTORY`] values): 0–5 Cₚ⁻¹ − I (finite strain,
|
||||
//! tensor components) or εₚ (small strain, engineering shears), 6–11 the back stress α, 12 the
|
||||
//! equivalent plastic strain.
|
||||
|
||||
use crate::constitutive::{Response, Tangent};
|
||||
use crate::dense::{inverse3, symmetric_eigen};
|
||||
use crate::plasticity::{J2, PointState, Return};
|
||||
use crate::rotation::{IDENTITY, Mat3, mul, tensor, transpose, voigt};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// History values per integration point.
|
||||
pub const HISTORY: usize = 13;
|
||||
|
||||
/// Where the equivalent plastic strain is in a point's history.
|
||||
pub const PLASTIC_STRAIN: usize = 12;
|
||||
|
||||
/// History value `i` (0 when the slice is short).
|
||||
pub(crate) fn value(h: &[f64], i: usize) -> f64 {
|
||||
h.get(i).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Cₚ⁻¹ of a finite-strain history (stored as Cₚ⁻¹ − I).
|
||||
fn plastic_stretch(h: &[f64]) -> Mat3 {
|
||||
let stored = tensor(&std::array::from_fn(|k| value(h, k)));
|
||||
std::array::from_fn(|i| std::array::from_fn(|j| IDENTITY[i][j] + stored[i][j]))
|
||||
}
|
||||
|
||||
/// Store Cₚ⁻¹ as Cₚ⁻¹ − I in the first six history values.
|
||||
fn store_plastic_stretch(cp: &Mat3, h: &mut [f64]) {
|
||||
let v = voigt(cp);
|
||||
for (k, slot) in h.iter_mut().take(6).enumerate() {
|
||||
*slot = v[k] - if k < 3 { 1.0 } else { 0.0 };
|
||||
}
|
||||
}
|
||||
|
||||
/// artanh(r)/r, smooth through r = 0.
|
||||
fn log_divided_difference(r: f64) -> f64 {
|
||||
if r.abs() < 1e-4 {
|
||||
let r2 = r * r;
|
||||
1.0 + r2 * (1.0 / 3.0 + r2 * (1.0 / 5.0 + r2 / 7.0))
|
||||
} else {
|
||||
r.atanh() / r
|
||||
}
|
||||
}
|
||||
|
||||
/// The state a return starts from: the committed back stress and plastic strain, at the point's
|
||||
/// temperature (Johnson–Cook softening; room temperature without one).
|
||||
fn start(j2: &J2, committed: &[f64], temperature: Option<f64>) -> PointState {
|
||||
let mut s = PointState::new(temperature.unwrap_or_else(|| j2.initial_temperature()));
|
||||
s.back = std::array::from_fn(|k| value(committed, 6 + k));
|
||||
s.plastic_strain = value(committed, PLASTIC_STRAIN);
|
||||
s
|
||||
}
|
||||
|
||||
fn singular() -> Error {
|
||||
Error::Degenerate("the plastic material point has no tangent: its hardening falls faster than −3G (a softening material localises)".into())
|
||||
}
|
||||
|
||||
/// The deviatoric trial stress 2G dev ε of a strain 6-vector (engineering shears), as tensor
|
||||
/// components.
|
||||
fn trial_deviator(j2: &J2, e: &[f64; 6]) -> [f64; 6] {
|
||||
let mean = (e[0] + e[1] + e[2]) / 3.0;
|
||||
let g2 = 2.0 * j2.shear;
|
||||
[g2 * (e[0] - mean), g2 * (e[1] - mean), g2 * (e[2] - mean), j2.shear * e[3], j2.shear * e[4], j2.shear * e[5]]
|
||||
}
|
||||
|
||||
/// Finite strain: the isochoric (deviatoric) Kirchhoff stress and its spatial tangent at
|
||||
/// deformation gradient `f`, from the history `committed` (written, updated, to `updated`). Add
|
||||
/// the volumetric part with [`volumetric`] (or the mixed element's pressure) for the whole
|
||||
/// response.
|
||||
pub fn finite_isochoric(j2: &J2, f: &Mat3, temperature: Option<f64>, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
if let Some(crate::plasticity::Flow::Linear { kinematic: true, .. }) = j2.flow {
|
||||
return Err(Error::NotSupported(
|
||||
"kinematic hardening at finite strain (its back stress is not coaxial with the elastic stretch; use nonlinear_geometry false or isotropic hardening)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
let b = mul(&mul(f, &plastic_stretch(committed)), &transpose(f));
|
||||
let (l, q) = symmetric_eigen(&b)
|
||||
.filter(|(l, _)| l.iter().all(|x| *x > 0.0))
|
||||
.ok_or_else(|| Error::Degenerate("the elastic stretch of a plastic material point is not positive (the material is inverted)".into()))?;
|
||||
let eps = l.map(|x| 0.5 * x.ln());
|
||||
let mut state = start(j2, committed, temperature);
|
||||
let mut trial = trial_deviator(j2, &[eps[0], eps[1], eps[2], 0.0, 0.0, 0.0]);
|
||||
let ret: Option<Return> = j2.radial_return(&mut state, &mut trial, 0.0);
|
||||
// Back to the global frame: τ = Σ τ_A q_A ⊗ q_A, n likewise.
|
||||
let spectral = |v: [f64; 3]| -> Mat3 { std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|a| v[a] * q[i][a] * q[j][a]).sum())) };
|
||||
let tau = voigt(&spectral([trial[0], trial[1], trial[2]]));
|
||||
let global = ret.map(|r| Return { normal: voigt(&spectral([r.normal[0], r.normal[1], r.normal[2]])), ..r });
|
||||
let d = j2.tangent(global.as_ref(), false).ok_or_else(singular)?;
|
||||
// θ_AB of the logarithm's derivative.
|
||||
let theta: Mat3 =
|
||||
std::array::from_fn(|a| std::array::from_fn(|bb| if a == bb { 1.0 } else { log_divided_difference((l[a] - l[bb]) / (l[a] + l[bb])) }));
|
||||
let tau_m = tensor(&tau);
|
||||
let qt = transpose(&q);
|
||||
let mut c: Tangent = [[0.0; 6]; 6];
|
||||
for (col, &(i, j)) in crate::constitutive::PAIRS.iter().enumerate() {
|
||||
// The unit rate of deformation of this column (engineering shear 1: d_ij = d_ji = ½).
|
||||
let mut dm = [[0.0; 3]; 3];
|
||||
let half = if i == j { 1.0 } else { 0.5 };
|
||||
dm[i][j] = half;
|
||||
dm[j][i] = half;
|
||||
let mut dp = mul(&mul(&qt, &dm), &q);
|
||||
for a in 0..3 {
|
||||
for bb in 0..3 {
|
||||
dp[a][bb] *= theta[a][bb];
|
||||
}
|
||||
}
|
||||
let de = voigt(&mul(&mul(&q, &dp), &qt));
|
||||
let de = [de[0], de[1], de[2], 2.0 * de[3], 2.0 * de[4], 2.0 * de[5]];
|
||||
let spin = voigt(&{
|
||||
let (x, y) = (mul(&dm, &tau_m), mul(&tau_m, &dm));
|
||||
std::array::from_fn(|r| std::array::from_fn(|s| x[r][s] + y[r][s]))
|
||||
});
|
||||
for row in 0..6 {
|
||||
c[row][col] = (0..6).map(|k| d[row][k] * de[k]).sum::<f64>() - spin[row];
|
||||
}
|
||||
}
|
||||
if let Some(r) = ret {
|
||||
// bₑ = Σ exp(2 εₑ,A) q_A ⊗ q_A with εₑ = ε − √(3/2) Δε̄ₚ n, and Cₚ⁻¹ = F⁻¹ bₑ F⁻ᵀ.
|
||||
let dg = (1.5f64).sqrt() * r.plastic_strain;
|
||||
let be = spectral(std::array::from_fn(|a| (2.0 * (eps[a] - dg * r.normal[a])).exp()));
|
||||
let (inv, _) = inverse3(f).ok_or_else(|| Error::Degenerate("the deformation gradient is singular".into()))?;
|
||||
store_plastic_stretch(&mul(&mul(&inv, &be), &transpose(&inv)), updated);
|
||||
if let Some(slot) = updated.get_mut(PLASTIC_STRAIN) {
|
||||
*slot = state.plastic_strain;
|
||||
}
|
||||
}
|
||||
Ok(Response { stress: tau, tangent: c })
|
||||
}
|
||||
|
||||
/// U′(J) and U″(J) of the Hencky volumetric energy K/2 (ln J)² (finite strain), or of
|
||||
/// K/2 (J − 1)² for the dilatation J = 1 + tr ε of small strain.
|
||||
pub fn volumetric(j2: &J2, j: f64, small: bool) -> Option<(f64, f64)> {
|
||||
if !(j > 0.0 && j.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let k = j2.bulk;
|
||||
Some(if small { (k * (j - 1.0), k) } else { (k * j.ln() / j, k * (1.0 - j.ln()) / (j * j)) })
|
||||
}
|
||||
|
||||
/// Small strain: the stress and the consistent tangent at total (mechanical) strain `strain`
|
||||
/// (engineering shears), from the history `committed` (written, updated, to `updated`); with
|
||||
/// `bulk` false only the deviatoric part (what a mixed element takes).
|
||||
pub fn small(j2: &J2, strain: &[f64; 6], temperature: Option<f64>, committed: &[f64], updated: &mut [f64], bulk: bool) -> Result<Response> {
|
||||
let elastic: [f64; 6] = std::array::from_fn(|k| strain[k] - value(committed, k));
|
||||
let mut state = start(j2, committed, temperature);
|
||||
let mut stress = trial_deviator(j2, &elastic);
|
||||
let ret = j2.radial_return(&mut state, &mut stress, 0.0);
|
||||
if bulk {
|
||||
let p = j2.bulk * (elastic[0] + elastic[1] + elastic[2]);
|
||||
for s in stress.iter_mut().take(3) {
|
||||
*s += p;
|
||||
}
|
||||
}
|
||||
let tangent = j2.tangent(ret.as_ref(), bulk).ok_or_else(singular)?;
|
||||
if let Some(r) = ret {
|
||||
let dg = (1.5f64).sqrt() * r.plastic_strain;
|
||||
for (k, slot) in updated.iter_mut().take(6).enumerate() {
|
||||
*slot = value(committed, k) + dg * r.normal[k] * if k < 3 { 1.0 } else { 2.0 };
|
||||
}
|
||||
for (k, slot) in updated.iter_mut().skip(6).take(6).enumerate() {
|
||||
*slot = state.back[k];
|
||||
}
|
||||
if let Some(slot) = updated.get_mut(PLASTIC_STRAIN) {
|
||||
*slot = state.plastic_strain;
|
||||
}
|
||||
}
|
||||
Ok(Response { stress, tangent })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::constitutive::{add_pressure, push_forward};
|
||||
use fieldcraft_model::{Elastic, Hardening, HardeningPoint, Plastic};
|
||||
|
||||
const E: f64 = 200_000.0;
|
||||
const NU: f64 = 0.3;
|
||||
|
||||
fn j2(plastic: Plastic) -> J2 {
|
||||
J2::from_model(&Elastic::Isotropic { young: E, poisson: NU }, Some(&plastic), None).unwrap()
|
||||
}
|
||||
|
||||
fn bilinear(h: f64, hardening: Hardening) -> J2 {
|
||||
j2(Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: h, hardening })
|
||||
}
|
||||
|
||||
/// The whole finite-strain response, isochoric plus the Hencky volume.
|
||||
fn finite(m: &J2, f: &Mat3, committed: &[f64], updated: &mut [f64]) -> Response {
|
||||
let iso = finite_isochoric(m, f, None, committed, updated).unwrap();
|
||||
let j = crate::dense::det3(f);
|
||||
let (p, dp) = volumetric(m, j, false).unwrap();
|
||||
add_pressure(&iso, j, p, j * dp)
|
||||
}
|
||||
|
||||
/// The pulled-back tangent C (dS = C : dE) against central differences of S along several
|
||||
/// directions of F, as `constitutive::tests::check_tangent` does, from fixed history.
|
||||
fn check_finite_tangent(m: &J2, f: &Mat3, committed: &[f64]) -> f64 {
|
||||
let mut scratch = vec![0.0; HISTORY];
|
||||
let r = finite(m, f, committed, &mut scratch);
|
||||
let (inv, _) = inverse3(f).unwrap();
|
||||
let t = push_forward(&inv);
|
||||
let back: Tangent =
|
||||
std::array::from_fn(|a| std::array::from_fn(|b| (0..6).map(|k| (0..6).map(|l| t[a][k] * r.tangent[k][l] * t[b][l]).sum::<f64>()).sum()));
|
||||
let scale = back.iter().flatten().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let h = 1e-7;
|
||||
let mut worst: f64 = 0.0;
|
||||
for k in 0..9 {
|
||||
let mut df = [[0.0; 3]; 3];
|
||||
df[k / 3][k % 3] = 1.0;
|
||||
df[(k + 1) % 3][(k + 2) % 3] += 0.3;
|
||||
let at = |s: f64| {
|
||||
let fp: Mat3 = std::array::from_fn(|i| std::array::from_fn(|j| f[i][j] + s * df[i][j]));
|
||||
let mut scratch = vec![0.0; HISTORY];
|
||||
let tau = finite(m, &fp, committed, &mut scratch).stress;
|
||||
crate::rotation::rotate(&inverse3(&fp).unwrap().0, &tau)
|
||||
};
|
||||
let (sp, sm) = (at(h), at(-h));
|
||||
let ftdf = mul(&transpose(f), &df);
|
||||
let de = [ftdf[0][0], ftdf[1][1], ftdf[2][2], ftdf[0][1] + ftdf[1][0], ftdf[0][2] + ftdf[2][0], ftdf[1][2] + ftdf[2][1]];
|
||||
for a in 0..6 {
|
||||
let numeric = (sp[a] - sm[a]) / (2.0 * h);
|
||||
let analytic: f64 = (0..6).map(|b| back[a][b] * de[b]).sum();
|
||||
worst = worst.max((numeric - analytic).abs() / scale);
|
||||
}
|
||||
}
|
||||
worst
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finite_strain_tangent_is_consistent_elastic_and_plastic() {
|
||||
let deformations: [Mat3; 3] = [
|
||||
[[1.02, 0.015, -0.005], [-0.01, 0.99, 0.02], [0.007, -0.012, 1.005]],
|
||||
// Large: well into plasticity.
|
||||
[[1.3, 0.15, -0.05], [-0.1, 0.85, 0.2], [0.07, -0.12, 1.05]],
|
||||
// Uniaxial (two equal principal stretches).
|
||||
[[1.2, 0.0, 0.0], [0.0, 0.92, 0.0], [0.0, 0.0, 0.92]],
|
||||
];
|
||||
let tables = Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: 250.0 }, HardeningPoint { strain: 0.5, stress: 600.0 }] };
|
||||
let jc = Plastic::JohnsonCook {
|
||||
a: 350.0,
|
||||
b: 275.0,
|
||||
n: 0.36,
|
||||
c: 0.0,
|
||||
m: 1.0,
|
||||
reference_strain_rate: 1.0,
|
||||
room_temperature: 293.0,
|
||||
melt_temperature: 1356.0,
|
||||
specific_heat: None,
|
||||
taylor_quinney: 0.9,
|
||||
};
|
||||
// A plastic history to start from: a prior stretch committed.
|
||||
let mut prior = vec![0.0; HISTORY];
|
||||
finite(&bilinear(2000.0, Hardening::Isotropic), &[[1.1, 0.02, 0.0], [0.0, 0.95, 0.03], [0.0, 0.0, 0.96]], &[0.0; HISTORY], &mut prior);
|
||||
assert!(prior[PLASTIC_STRAIN] > 0.0);
|
||||
for m in [bilinear(2000.0, Hardening::Isotropic), bilinear(0.0, Hardening::Isotropic), j2(tables), j2(jc)] {
|
||||
for f in &deformations {
|
||||
for committed in [&[0.0; HISTORY][..], &prior[..]] {
|
||||
let worst = check_finite_tangent(&m, f, committed);
|
||||
assert!(worst < 1e-7, "{:?} at {f:?}: {worst}", m.flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finite_strain_tangent_is_symmetric_and_elastic_at_rest() {
|
||||
let m = bilinear(2000.0, Hardening::Isotropic);
|
||||
let mut scratch = vec![0.0; HISTORY];
|
||||
let r = finite(&m, &[[1.3, 0.15, -0.05], [-0.1, 0.85, 0.2], [0.07, -0.12, 1.05]], &[0.0; HISTORY], &mut scratch);
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
assert!((r.tangent[a][b] - r.tangent[b][a]).abs() < 1e-9 * E, "{a} {b}");
|
||||
}
|
||||
}
|
||||
let rest = finite(&m, &IDENTITY, &[0.0; HISTORY], &mut scratch);
|
||||
let d = crate::material::isotropic(E, NU).unwrap();
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
assert!((rest.tangent[a][b] - d[a][b]).abs() < 1e-9 * E);
|
||||
}
|
||||
}
|
||||
assert!(rest.stress.iter().all(|v| *v == 0.0));
|
||||
}
|
||||
|
||||
/// Uniaxial stress along x at finite strain, from a virgin point to a logarithmic strain ε
|
||||
/// in one increment (the lateral stretch found so σ_yy = 0): the Kirchhoff stress is the
|
||||
/// small-strain curve in the logarithmic strain, τ = σ_y + E_t (ε − σ_y/E), because the
|
||||
/// exponential map integrates a proportional path exactly.
|
||||
#[test]
|
||||
fn finite_uniaxial_matches_the_logarithmic_closed_form() {
|
||||
let (y0, h) = (250.0, 2000.0);
|
||||
let m = bilinear(h, Hardening::Isotropic);
|
||||
let strain: f64 = 0.2;
|
||||
let lam = strain.exp();
|
||||
let sigma = |l: f64| {
|
||||
let f = [[lam, 0.0, 0.0], [0.0, l, 0.0], [0.0, 0.0, l]];
|
||||
let mut u = vec![0.0; HISTORY];
|
||||
let r = finite(&m, &f, &[0.0; HISTORY], &mut u);
|
||||
let j = lam * l * l;
|
||||
(r.stress[0] / j, r.stress[1] / j, u)
|
||||
};
|
||||
// Secant on the lateral stretch.
|
||||
let (mut a, mut b) = (1.0, 0.9);
|
||||
for _ in 0..60 {
|
||||
let (fa, fb) = (sigma(a).1, sigma(b).1);
|
||||
if fb.abs() < 1e-11 || fb == fa {
|
||||
break;
|
||||
}
|
||||
(a, b) = (b, b - fb * (b - a) / (fb - fa));
|
||||
}
|
||||
let (sxx, _, u) = sigma(b);
|
||||
let j = lam * b * b;
|
||||
// In Kirchhoff stress τ = J σ: τ = y0 + E_t (ε − y0/E) along the log strain path.
|
||||
let et = E * h / (E + h);
|
||||
let exact = y0 + et * (strain - y0 / E);
|
||||
assert!((sxx * j - exact).abs() < 1e-8 * exact, "{} vs {exact}", sxx * j);
|
||||
assert!((u[PLASTIC_STRAIN] - (exact - y0) / h).abs() < 1e-10);
|
||||
// The plastic deformation is isochoric: det Cₚ⁻¹ = 1.
|
||||
let cp = plastic_stretch(&u);
|
||||
assert!((crate::dense::det3(&cp) - 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_strain_tangent_is_consistent_and_cyclic_kinematic_shows_bauschinger() {
|
||||
let m = bilinear(10_000.0, Hardening::Kinematic);
|
||||
let strain = [3e-3, -1e-3, 5e-4, 2e-3, -1e-3, 7e-4];
|
||||
let mut prior = vec![0.0; HISTORY];
|
||||
small(&m, &[2e-3, -1e-3, -1e-3, 0.0, 0.0, 0.0], None, &[0.0; HISTORY], &mut prior, true).unwrap();
|
||||
for committed in [&[0.0; HISTORY][..], &prior[..]] {
|
||||
let mut scratch = vec![0.0; HISTORY];
|
||||
let r = small(&m, &strain, None, committed, &mut scratch, true).unwrap();
|
||||
let h = 1e-9;
|
||||
for b in 0..6 {
|
||||
let at = |s: f64| {
|
||||
let mut e = strain;
|
||||
e[b] += s;
|
||||
small(&m, &e, None, committed, &mut [0.0; HISTORY], true).unwrap().stress
|
||||
};
|
||||
let (p, q) = (at(h), at(-h));
|
||||
for a in 0..6 {
|
||||
let numeric = (p[a] - q[a]) / (2.0 * h);
|
||||
assert!((numeric - r.tangent[a][b]).abs() < 1e-5 * E, "{a} {b}: {numeric} vs {}", r.tangent[a][b]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Kinematic hardening is refused at finite strain, named.
|
||||
assert!(matches!(finite_isochoric(&m, &IDENTITY, None, &[0.0; HISTORY], &mut [0.0; HISTORY]), Err(Error::NotSupported(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softening_steeper_than_3g_has_no_tangent() {
|
||||
let g = E / (2.0 * (1.0 + NU));
|
||||
let points = vec![HardeningPoint { strain: 0.0, stress: 250.0 }, HardeningPoint { strain: 1e-4, stress: 250.0 - 4.0 * g * 1e-4 }];
|
||||
let m = j2(Plastic::Table { points });
|
||||
let r = small(&m, &[5e-3, -2.5e-3, -2.5e-3, 0.0, 0.0, 0.0], None, &[0.0; HISTORY], &mut [0.0; HISTORY], true);
|
||||
assert!(matches!(r, Err(Error::Degenerate(_))), "{r:?}");
|
||||
}
|
||||
}
|
||||
+483
-83
@@ -1,6 +1,6 @@
|
||||
//! Finite-strain (total Lagrangian) solid and 2D elements: internal forces and the consistent
|
||||
//! tangent stiffness for nodal displacements u, and the Cauchy stress and logarithmic strain at
|
||||
//! the integration points.
|
||||
//! Solid and 2D elements of nonlinear steps: total Lagrangian (finite strain) or with small
|
||||
//! strain and an inelastic material; internal forces and the consistent tangent stiffness for
|
||||
//! nodal displacements u, and the Cauchy stress and the strain at the integration points.
|
||||
//!
|
||||
//! **Formulation.** Total Lagrangian: the integrals run over the reference (undeformed) element
|
||||
//! with its own quadrature, F = I + Σ u_a ⊗ ∇₀N_a at each point, and the material returns the
|
||||
@@ -26,7 +26,26 @@
|
||||
//! symmetric; at u = 0 it is the linear hybrid stiffness K_dev + κ G M⁻¹ Gᵀ. The constant
|
||||
//! pressure case is the mean dilatation method of J. C. Nagtegaal, D. M. Parks, J. R. Rice
|
||||
//! (CMAME 4:153–177, 1974) at finite strain. St Venant–Kirchhoff has no such split, so hybrid
|
||||
//! elements of an elastic material are refused in nonlinear steps.
|
||||
//! elements of an elastic material are refused in nonlinear steps; J2 plasticity has it exactly
|
||||
//! (the return is deviatoric, the volume Hencky's K/2 (ln J)², [`crate::elastoplastic`]).
|
||||
//!
|
||||
//! **Thermal strain** ([`Temperature`]): at finite strain the multiplicative split F = F_m F_θ
|
||||
//! with the thermal stretches F_θ = diag(1 + αᵢ (T − T_ref)) at each point (T interpolated from
|
||||
//! the nodes; V. A. Lubarda, *Constitutive theories based on the multiplicative decomposition of
|
||||
//! deformation gradient: thermoelasticity, elastoplasticity, and biomechanics*, Appl. Mech. Rev.
|
||||
//! 57(2):95–108, 2004): the material sees
|
||||
//! F_m, the stress per reference volume is J_θ τ_m(F_m), and since F_θ is fixed over an
|
||||
//! iteration the spatial tangent is J_θ c_m, so the loops run unchanged on the thermally
|
||||
//! expanded volume J_θ dV₀ (the mixed elements project det F_m on it). The linear stretch
|
||||
//! 1 + α ΔT is the engineering definition of α, so a free body grows by exactly α ΔT. At small
|
||||
//! strain the split is additive, ε_m = ε − α ΔT. Reported strains are the total ones.
|
||||
//!
|
||||
//! **Small strain** ([`Kinematics::Small`], a step without nonlinear geometry with an inelastic
|
||||
//! material): the same loops on the linear kinematics, ε = sym ∇u with the reference gradients,
|
||||
//! f = ∫ Bᵀ σ dV₀ and K = ∫ Bᵀ 𝔻 B dV₀ with the material's consistent tangent 𝔻, and no
|
||||
//! geometric stiffness. Mixed elements project the dilatation 1 + tr ε and the pressure K tr ε
|
||||
//! the same way, which is the linear hybrid element of [`crate::hybrid`] while the material is
|
||||
//! elastic.
|
||||
//!
|
||||
//! **2D elements**: plane strain (F₃₃ = 1) and axisymmetric (F₃₃ = r/R, the hoop stretch, with
|
||||
//! the hoop column N/r of the current radius in Bₓ and a geometric term τ_θθ N_a N_b / r² on the
|
||||
@@ -37,7 +56,8 @@
|
||||
//! (enhanced assumed strain, J. C. Simo, F. Armero, IJNME 33:1413–1449, 1992) needs the internal
|
||||
//! modes as element unknowns carried between iterations, and it develops spurious hourglass
|
||||
//! modes in large compression (P. Wriggers, S. Reese, CMAME 135:201–209, 1996), which want a
|
||||
//! stabilised variant of their own.
|
||||
//! stabilised variant of their own. With small strain they are refused for inelastic materials
|
||||
//! too: their internal modes would carry the plastic history between iterations.
|
||||
|
||||
use crate::constitutive::{MaterialModel, Response, add_pressure, logarithmic_strain};
|
||||
use crate::dense::{Cholesky, Matrix, inverse3};
|
||||
@@ -54,12 +74,87 @@ pub struct Finite {
|
||||
pub tangent: Option<Matrix>,
|
||||
}
|
||||
|
||||
/// The kinematics of an evaluation.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Kinematics {
|
||||
/// Total Lagrangian, large displacements, rotations and strains.
|
||||
Finite,
|
||||
/// Small displacements and strains (ε = sym ∇u, the reference gradients, no geometric
|
||||
/// stiffness): only the material is nonlinear.
|
||||
Small,
|
||||
}
|
||||
|
||||
/// The temperatures of an element in a nonlinear step: the linear expansion coefficients along
|
||||
/// X, Y, Z (zero for a material that does not expand), the reference temperature at which the
|
||||
/// thermal strain is zero, and the temperature at each node.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Temperature {
|
||||
pub alpha: [f64; 3],
|
||||
pub reference: f64,
|
||||
pub nodal: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Temperature {
|
||||
fn check(&self, nodes: usize) -> Result<()> {
|
||||
if self.nodal.len() != nodes {
|
||||
return Err(Error::Invalid(format!("{} temperatures for a {nodes}-node element", self.nodal.len())));
|
||||
}
|
||||
if self.alpha.iter().chain(&self.nodal).chain([&self.reference]).any(|v| !v.is_finite()) {
|
||||
return Err(Error::Invalid("temperatures and expansion coefficients must be finite".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The temperature at a point and its thermal stretches ϑᵢ = 1 + αᵢ (T − T_ref).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Heat {
|
||||
temperature: Option<f64>,
|
||||
stretch: [f64; 3],
|
||||
}
|
||||
|
||||
impl Heat {
|
||||
/// At a point of shape function values `n`.
|
||||
fn at(t: Option<&Temperature>, n: &[f64]) -> Result<Heat> {
|
||||
let Some(t) = t else { return Ok(Heat { temperature: None, stretch: [1.0; 3] }) };
|
||||
let temperature: f64 = t.nodal.iter().zip(n).map(|(a, b)| a * b).sum();
|
||||
let stretch = t.alpha.map(|a| 1.0 + a * (temperature - t.reference));
|
||||
if !stretch.iter().all(|v| *v > 0.0 && v.is_finite()) {
|
||||
return Err(Error::Degenerate(format!("the thermal stretches {stretch:?} at T = {temperature} are not positive")));
|
||||
}
|
||||
Ok(Heat { temperature: Some(temperature), stretch })
|
||||
}
|
||||
|
||||
/// J_θ = ϑ₁ ϑ₂ ϑ₃.
|
||||
fn volume(&self) -> f64 {
|
||||
self.stretch.iter().product()
|
||||
}
|
||||
|
||||
/// The mechanical part F_m = F F_θ⁻¹ of a deformation gradient.
|
||||
fn mechanical(&self, f: &Mat3) -> Mat3 {
|
||||
std::array::from_fn(|i| std::array::from_fn(|j| f[i][j] / self.stretch[j]))
|
||||
}
|
||||
|
||||
/// The mechanical part ε − α (T − T_ref) of a small strain.
|
||||
fn mechanical_strain(&self, e: &[f64; 6]) -> [f64; 6] {
|
||||
std::array::from_fn(|k| if k < 3 { e[k] - (self.stretch[k] - 1.0) } else { e[k] })
|
||||
}
|
||||
}
|
||||
|
||||
/// What one integration point gives the element.
|
||||
struct PointKinematics {
|
||||
/// The point's share of the reference volume (of the thermally expanded volume J_θ dV₀ at
|
||||
/// finite strain).
|
||||
weight: f64,
|
||||
/// The deformation gradient, and its mechanical part F_m = F F_θ⁻¹ the material sees.
|
||||
total: Mat3,
|
||||
f: Mat3,
|
||||
/// det F_m, or the dilatation 1 + tr ε_m of small strain (what a mixed element projects).
|
||||
j: f64,
|
||||
/// ∇ₓN of every node.
|
||||
/// The mechanical small strain ε − ε_th (engineering shears); zero at finite strain.
|
||||
strain: [f64; 6],
|
||||
temperature: Option<f64>,
|
||||
/// ∇ₓN of every node (∇₀N at small strain).
|
||||
gradients: Vec<[f64; 3]>,
|
||||
/// Reference position.
|
||||
x0: [f64; 3],
|
||||
@@ -70,8 +165,22 @@ fn point_history(history: &[f64], len: usize, point: usize) -> &[f64] {
|
||||
history.get(point * len..(point + 1) * len).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Add f_a += w Bₓ,aᵀ τ and, with `k`, the material and geometric tangent.
|
||||
fn add_point(force: &mut [f64], k: Option<&mut Matrix>, gradients: &[[f64; 3]], r: &Response, w: f64) {
|
||||
/// The small strain of a displacement gradient F − I (engineering shears).
|
||||
fn small_strain(f: &Mat3) -> [f64; 6] {
|
||||
[f[0][0] - 1.0, f[1][1] - 1.0, f[2][2] - 1.0, f[0][1] + f[1][0], f[0][2] + f[2][0], f[1][2] + f[2][1]]
|
||||
}
|
||||
|
||||
/// The response of one point's material with its history slices.
|
||||
fn point_response(m: &MaterialModel, kin: Kinematics, p: &PointKinematics, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
match kin {
|
||||
Kinematics::Finite => m.response(&p.f, p.temperature, committed, updated),
|
||||
Kinematics::Small => m.small_strain(&p.strain, p.temperature, committed, updated, true),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add f_a += w Bₓ,aᵀ τ and, with `k`, the material tangent and (with `geometric`) the
|
||||
/// geometric one.
|
||||
fn add_point(force: &mut [f64], k: Option<&mut Matrix>, gradients: &[[f64; 3]], r: &Response, w: f64, geometric: bool) {
|
||||
for (a, g) in gradients.iter().enumerate() {
|
||||
let b = b_block(g);
|
||||
for c in 0..3 {
|
||||
@@ -82,6 +191,9 @@ fn add_point(force: &mut [f64], k: Option<&mut Matrix>, gradients: &[[f64; 3]],
|
||||
}
|
||||
let Some(k) = k else { return };
|
||||
accumulate_symmetric(&mut k.data, gradients, &r.tangent, w);
|
||||
if !geometric {
|
||||
return;
|
||||
}
|
||||
let tau = crate::rotation::tensor(&r.stress);
|
||||
let n = k.n;
|
||||
for (a, ga) in gradients.iter().enumerate() {
|
||||
@@ -122,14 +234,17 @@ fn check_history(m: &MaterialModel, points: usize, committed: &[f64], updated: &
|
||||
}
|
||||
|
||||
impl Solid<'_> {
|
||||
/// Refuse what has no finite-strain form here.
|
||||
fn check_finite(&self, m: &MaterialModel, u: &[[f64; 3]]) -> Result<()> {
|
||||
/// Refuse what has no form here.
|
||||
fn check_nonlinear(&self, m: &MaterialModel, u: &[[f64; 3]], kin: Kinematics) -> Result<()> {
|
||||
check_displacements(u, self.ty.nodes)?;
|
||||
match self.formulation {
|
||||
Formulation::IncompatibleModes => Err(Error::NotSupported(format!(
|
||||
"{} incompatible modes in nonlinear steps (their finite-strain form, enhanced assumed strain, is not implemented; use full integration, hybrid or hex20)",
|
||||
self.ty.kind.name()
|
||||
))),
|
||||
Formulation::IncompatibleModes => {
|
||||
let why = match kin {
|
||||
Kinematics::Finite => "in nonlinear steps (their finite-strain form, enhanced assumed strain, is not implemented",
|
||||
Kinematics::Small => "with an inelastic material (their internal modes would carry the plastic history",
|
||||
};
|
||||
Err(Error::NotSupported(format!("{} incompatible modes {why}; use full integration, hybrid or hex20)", self.ty.kind.name())))
|
||||
}
|
||||
Formulation::Hybrid { .. } if !m.is_decoupled() => Err(Error::NotSupported(format!(
|
||||
"hybrid elements of a {} material in nonlinear steps (the mixed formulation needs a volumetric–isochoric split of the strain energy; give the material the neo-Hookean option)",
|
||||
m.name()
|
||||
@@ -138,59 +253,87 @@ impl Solid<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// F, J and the current gradients at every stiffness point.
|
||||
fn kinematics(&self, u: &[[f64; 3]]) -> Result<Vec<PointKinematics>> {
|
||||
/// F, J and the gradients at every stiffness point: the current ones at finite strain, the
|
||||
/// reference ones at small strain.
|
||||
fn kinematics(&self, u: &[[f64; 3]], kin: Kinematics, thermal: Option<&Temperature>) -> Result<Vec<PointKinematics>> {
|
||||
let n = self.ty.nodes;
|
||||
if let Some(t) = thermal {
|
||||
t.check(n)?;
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for p in self.rule().points() {
|
||||
let g = self.gradients(p.xi)?;
|
||||
let reference = g.dx.get(..n).unwrap_or_default();
|
||||
let f = crate::constitutive::deformation_gradient(u, reference);
|
||||
let (inv, j) = inverse3(&f).ok_or_else(|| inverted(self.ty.kind.name(), 0.0))?;
|
||||
if !(j > 0.0 && j.is_finite()) {
|
||||
return Err(inverted(self.ty.kind.name(), j));
|
||||
let total = crate::constitutive::deformation_gradient(u, reference);
|
||||
let (weight, x0) = (g.det * p.weight, self.position_of(g.shape.values()));
|
||||
let heat = Heat::at(thermal, g.shape.values())?;
|
||||
if kin == Kinematics::Small {
|
||||
let strain = heat.mechanical_strain(&small_strain(&total));
|
||||
let j = 1.0 + strain[0] + strain[1] + strain[2];
|
||||
let gradients = reference.to_vec();
|
||||
out.push(PointKinematics { weight, total, f: total, j, strain, temperature: heat.temperature, gradients, x0 });
|
||||
continue;
|
||||
}
|
||||
let (inv, det) = inverse3(&total).ok_or_else(|| inverted(self.ty.kind.name(), 0.0))?;
|
||||
let j = det / heat.volume();
|
||||
if !(det > 0.0 && j.is_finite()) {
|
||||
return Err(inverted(self.ty.kind.name(), det));
|
||||
}
|
||||
let gradients = reference.iter().map(|d| std::array::from_fn(|i| (0..3).map(|jj| d[jj] * inv[jj][i]).sum())).collect();
|
||||
out.push(PointKinematics { weight: g.det * p.weight, f, j, gradients, x0: self.position_of(g.shape.values()) });
|
||||
let f = heat.mechanical(&total);
|
||||
out.push(PointKinematics { weight: weight * heat.volume(), total, f, j, strain: [0.0; 6], temperature: heat.temperature, gradients, x0 });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The effective response (Kirchhoff stress and tangent) at every point, the forces, and the
|
||||
/// tangent when `tangent`.
|
||||
fn finite_points(
|
||||
/// The effective response (Kirchhoff stress and tangent, or the stress of small strain) at
|
||||
/// every point, the forces, and the tangent when `tangent`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn nonlinear_points(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 3]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> Result<(Finite, Vec<(PointKinematics, Response)>)> {
|
||||
self.check_finite(m, u)?;
|
||||
let points = self.kinematics(u)?;
|
||||
self.check_nonlinear(m, u, kin)?;
|
||||
let points = self.kinematics(u, kin, thermal)?;
|
||||
let len = check_history(m, points.len(), committed, updated)?;
|
||||
let ndof = 3 * self.ty.nodes;
|
||||
let mut force = vec![0.0; ndof];
|
||||
let mut k = tangent.then(|| Matrix::zeros(ndof));
|
||||
let responses: Vec<Response> = if let Formulation::Hybrid { .. } = self.formulation {
|
||||
self.mixed_responses(m, &points, k.as_mut())?
|
||||
self.mixed_responses(m, &points, kin, committed, updated, k.as_mut())?
|
||||
} else {
|
||||
let mut out = Vec::with_capacity(points.len());
|
||||
for (i, p) in points.iter().enumerate() {
|
||||
let updated_point = updated.get_mut(i * len..(i + 1) * len).unwrap_or_default();
|
||||
out.push(m.response(&p.f, point_history(committed, len, i), updated_point)?);
|
||||
out.push(point_response(m, kin, p, point_history(committed, len, i), updated_point)?);
|
||||
}
|
||||
out
|
||||
};
|
||||
for (p, r) in points.iter().zip(&responses) {
|
||||
add_point(&mut force, k.as_mut(), &p.gradients, r, p.weight);
|
||||
add_point(&mut force, k.as_mut(), &p.gradients, r, p.weight, kin == Kinematics::Finite);
|
||||
}
|
||||
Ok((Finite { force, tangent: k }, points.into_iter().zip(responses).collect()))
|
||||
}
|
||||
|
||||
/// The three-field mixed responses τ_iso + J p I with their tangents, and the dilatation
|
||||
/// stiffness Gᵀ M⁻¹ H M⁻¹ G added to `k`.
|
||||
fn mixed_responses(&self, m: &MaterialModel, points: &[PointKinematics], k: Option<&mut Matrix>) -> Result<Vec<Response>> {
|
||||
/// The three-field mixed responses τ_iso + J p I with their tangents (small strain:
|
||||
/// s + p I), and the dilatation stiffness Gᵀ M⁻¹ H M⁻¹ G added to `k`.
|
||||
fn mixed_responses(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
points: &[PointKinematics],
|
||||
kin: Kinematics,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
k: Option<&mut Matrix>,
|
||||
) -> Result<Vec<Response>> {
|
||||
let small = kin == Kinematics::Small;
|
||||
let len = m.history_len();
|
||||
let (np, frame) = (self.np(), self.frame());
|
||||
let basis: Vec<[f64; 4]> = points.iter().map(|p| pressure_basis(np, p.x0, &frame)).collect();
|
||||
let mut mm = Matrix::zeros(np);
|
||||
@@ -212,7 +355,7 @@ impl Solid<'_> {
|
||||
if !(th > 0.0 && th.is_finite()) {
|
||||
return Err(inverted(self.ty.kind.name(), th));
|
||||
}
|
||||
let (dp, ddp) = m.volumetric(th).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
|
||||
let (dp, ddp) = m.volumetric(th, small).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
|
||||
for a in 0..np {
|
||||
pressure[a] += p.weight * nb[a] * dp;
|
||||
for b in 0..np {
|
||||
@@ -222,20 +365,28 @@ impl Solid<'_> {
|
||||
}
|
||||
chol.solve(&mut pressure);
|
||||
let mut out = Vec::with_capacity(points.len());
|
||||
for (p, nb) in points.iter().zip(&basis) {
|
||||
for (i, (p, nb)) in points.iter().zip(&basis).enumerate() {
|
||||
let pr: f64 = (0..np).map(|a| nb[a] * pressure[a]).sum();
|
||||
out.push(add_pressure(&m.isochoric(&p.f)?, p.j, pr, 0.0));
|
||||
let (c, u) = (point_history(committed, len, i), updated.get_mut(i * len..(i + 1) * len).unwrap_or_default());
|
||||
out.push(if small {
|
||||
let mut r = m.small_strain(&p.strain, p.temperature, c, u, false)?;
|
||||
r.stress.iter_mut().take(3).for_each(|s| *s += pr);
|
||||
r
|
||||
} else {
|
||||
add_pressure(&m.isochoric(&p.f, p.temperature, c, u)?, p.j, pr, 0.0)
|
||||
});
|
||||
}
|
||||
if let Some(k) = k {
|
||||
// A = M⁻¹ G, np × ndof; K += Aᵀ H A.
|
||||
// A = M⁻¹ G, np × ndof; K += Aᵀ H A. At small strain G = ∫ N_p ∇₀N dV₀.
|
||||
let ndof = k.n;
|
||||
let mut a_cols = vec![vec![0.0; np]; ndof];
|
||||
for (p, nb) in points.iter().zip(&basis) {
|
||||
let jw = p.weight * if small { 1.0 } else { p.j };
|
||||
for (node, g) in p.gradients.iter().enumerate() {
|
||||
for c in 0..3 {
|
||||
if let Some(col) = a_cols.get_mut(3 * node + c) {
|
||||
for (x, n) in col.iter_mut().zip(nb) {
|
||||
*x += p.weight * n * p.j * g[c];
|
||||
*x += jw * n * g[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,32 +413,71 @@ impl Solid<'_> {
|
||||
/// with the material history `committed` at the start of the increment (points × the
|
||||
/// material's history length) written, updated, to `updated`.
|
||||
pub fn finite_strain(&self, m: &MaterialModel, u: &[[f64; 3]], committed: &[f64], updated: &mut [f64], tangent: bool) -> Result<Finite> {
|
||||
Ok(self.finite_points(m, u, committed, updated, tangent)?.0)
|
||||
Ok(self.nonlinear_points(m, u, Kinematics::Finite, None, committed, updated, tangent)?.0)
|
||||
}
|
||||
|
||||
/// The Cauchy stress σ = τ/J and the logarithmic strain at every integration point (at its
|
||||
/// reference position).
|
||||
pub fn finite_strain_stresses(&self, m: &MaterialModel, u: &[[f64; 3]], committed: &[f64]) -> Result<Vec<PointStress>> {
|
||||
/// As [`Self::finite_strain`] with either kinematics and the element's temperatures when
|
||||
/// it has some; with small strain an inelastic material on the linear kinematics,
|
||||
/// f = ∫ Bᵀ σ dV and K = ∫ Bᵀ 𝔻 B dV with the material's consistent tangent 𝔻.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn nonlinear(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 3]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> Result<Finite> {
|
||||
Ok(self.nonlinear_points(m, u, kin, thermal, committed, updated, tangent)?.0)
|
||||
}
|
||||
|
||||
/// The Cauchy stress and the total strain (logarithmic at finite strain, ε at small strain)
|
||||
/// at every integration point (at its reference position).
|
||||
pub fn nonlinear_stresses(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 3]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
) -> Result<Vec<PointStress>> {
|
||||
let mut scratch = committed.to_vec();
|
||||
let (_, points) = self.finite_points(m, u, committed, &mut scratch, false)?;
|
||||
let (_, points) = self.nonlinear_points(m, u, kin, thermal, committed, &mut scratch, false)?;
|
||||
points
|
||||
.iter()
|
||||
.map(|(p, r)| {
|
||||
let strain = logarithmic_strain(&p.f).ok_or_else(|| inverted(self.ty.kind.name(), p.j))?;
|
||||
if kin == Kinematics::Small {
|
||||
return Ok(PointStress { xyz: p.x0, strain: small_strain(&p.total), stress: r.stress });
|
||||
}
|
||||
let strain = logarithmic_strain(&p.total).ok_or_else(|| inverted(self.ty.kind.name(), p.j))?;
|
||||
Ok(PointStress { xyz: p.x0, strain, stress: r.stress.map(|v| v / p.j) })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// One integration point of a 2D element in a nonlinear step: the total deformation gradient,
|
||||
/// det F_m, the reference position and the response.
|
||||
struct PlanePoint {
|
||||
f: Mat3,
|
||||
j: f64,
|
||||
x: [f64; 3],
|
||||
response: Response,
|
||||
}
|
||||
|
||||
impl Plane<'_> {
|
||||
fn check_finite(&self, u: &[[f64; 2]]) -> Result<()> {
|
||||
fn check_nonlinear(&self, u: &[[f64; 2]], kin: Kinematics) -> Result<()> {
|
||||
check_displacements(u, self.ty.nodes)?;
|
||||
if let Theory::PlaneStress { .. } = self.theory {
|
||||
return Err(Error::NotSupported(
|
||||
"plane stress elements in nonlinear steps (the thickness change needs a local iteration; use plane strain, axisymmetric or solid elements)"
|
||||
.into(),
|
||||
));
|
||||
let why = match kin {
|
||||
Kinematics::Finite => "the thickness change needs a local iteration",
|
||||
Kinematics::Small => "σ_zz = 0 needs a local iteration of the inelastic material",
|
||||
};
|
||||
return Err(Error::NotSupported(format!(
|
||||
"plane stress elements in nonlinear steps ({why}; use plane strain, axisymmetric or solid elements)"
|
||||
)));
|
||||
}
|
||||
if self.formulation == Formulation::IncompatibleModes {
|
||||
return Err(Error::NotSupported(format!(
|
||||
@@ -298,24 +488,31 @@ impl Plane<'_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Per point: (dv, F, J, current columns (∂N/∂x, ∂N/∂y, N/r), reference position, response).
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn finite_points(
|
||||
/// Per point: F, J, the reference position, the small strain and the response; the forces
|
||||
/// and the tangent.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn nonlinear_points(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 2]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> Result<(Finite, Vec<(Mat3, f64, [f64; 3], Response)>)> {
|
||||
self.check_finite(u)?;
|
||||
) -> Result<(Finite, Vec<PlanePoint>)> {
|
||||
self.check_nonlinear(u, kin)?;
|
||||
let n = self.ty.nodes;
|
||||
if let Some(t) = thermal {
|
||||
t.check(n)?;
|
||||
}
|
||||
let rule = self.rule().points();
|
||||
let len = check_history(m, rule.len(), committed, updated)?;
|
||||
let ndof = 2 * n;
|
||||
let mut force = vec![0.0; ndof];
|
||||
let mut k = tangent.then(|| Matrix::zeros(ndof));
|
||||
let axisymmetric = self.theory == Theory::Axisymmetric;
|
||||
let small = kin == Kinematics::Small;
|
||||
let mut out = Vec::with_capacity(rule.len());
|
||||
for (i, p) in rule.iter().enumerate() {
|
||||
let at = self.at(p)?;
|
||||
@@ -331,37 +528,47 @@ impl Plane<'_> {
|
||||
f[2][2] += ua[0] * g[2];
|
||||
}
|
||||
}
|
||||
let det2 = f[0][0] * f[1][1] - f[0][1] * f[1][0];
|
||||
let j = det2 * f[2][2];
|
||||
if !(j > 0.0 && j.is_finite() && f[2][2] > 0.0) {
|
||||
return Err(inverted(&format!("2D {}", self.ty.kind.name()), j));
|
||||
}
|
||||
let inv = [[f[1][1] / det2, -f[0][1] / det2], [-f[1][0] / det2, f[0][0] / det2]];
|
||||
let current: Vec<[f64; 3]> =
|
||||
cols.iter().map(|g| [g[0] * inv[0][0] + g[1] * inv[1][0], g[0] * inv[0][1] + g[1] * inv[1][1], g[2] / f[2][2]]).collect();
|
||||
let updated_point = updated.get_mut(i * len..(i + 1) * len).unwrap_or_default();
|
||||
let r = m.response(&f, point_history(committed, len, i), updated_point)?;
|
||||
let heat = Heat::at(thermal, at.shape.values())?;
|
||||
let (j, dv, current, r) = if small {
|
||||
let strain = heat.mechanical_strain(&small_strain(&f));
|
||||
let r = m.small_strain(&strain, heat.temperature, point_history(committed, len, i), updated_point, true)?;
|
||||
(1.0, at.dv, cols.to_vec(), r)
|
||||
} else {
|
||||
let det2 = f[0][0] * f[1][1] - f[0][1] * f[1][0];
|
||||
let j = det2 * f[2][2];
|
||||
if !(j > 0.0 && j.is_finite() && f[2][2] > 0.0) {
|
||||
return Err(inverted(&format!("2D {}", self.ty.kind.name()), j));
|
||||
}
|
||||
let inv = [[f[1][1] / det2, -f[0][1] / det2], [-f[1][0] / det2, f[0][0] / det2]];
|
||||
let current: Vec<[f64; 3]> =
|
||||
cols.iter().map(|g| [g[0] * inv[0][0] + g[1] * inv[1][0], g[0] * inv[0][1] + g[1] * inv[1][1], g[2] / f[2][2]]).collect();
|
||||
let r = m.response(&heat.mechanical(&f), heat.temperature, point_history(committed, len, i), updated_point)?;
|
||||
(j / heat.volume(), at.dv * heat.volume(), current, r)
|
||||
};
|
||||
let tau = [r.stress[0], r.stress[1], r.stress[2], r.stress[3]];
|
||||
for (a, g) in current.iter().enumerate() {
|
||||
if let Some(x) = force.get_mut(2 * a) {
|
||||
*x += at.dv * (g[0] * tau[0] + g[2] * tau[2] + g[1] * tau[3]);
|
||||
*x += dv * (g[0] * tau[0] + g[2] * tau[2] + g[1] * tau[3]);
|
||||
}
|
||||
if let Some(x) = force.get_mut(2 * a + 1) {
|
||||
*x += at.dv * (g[1] * tau[1] + g[0] * tau[3]);
|
||||
*x += dv * (g[1] * tau[1] + g[0] * tau[3]);
|
||||
}
|
||||
}
|
||||
if let Some(k) = k.as_mut() {
|
||||
let c4: [[f64; 4]; 4] = std::array::from_fn(|a| std::array::from_fn(|b| r.tangent[a][b]));
|
||||
crate::plane::accumulate(&mut k.data, ¤t, &c4, at.dv);
|
||||
for (a, ga) in current.iter().enumerate() {
|
||||
for (b, gb) in current.iter().enumerate() {
|
||||
let s = ga[0] * gb[0] * tau[0] + ga[1] * gb[1] * tau[1] + (ga[0] * gb[1] + ga[1] * gb[0]) * tau[3];
|
||||
k.add(2 * a, 2 * b, at.dv * (s + tau[2] * ga[2] * gb[2]));
|
||||
k.add(2 * a + 1, 2 * b + 1, at.dv * s);
|
||||
crate::plane::accumulate(&mut k.data, ¤t, &c4, dv);
|
||||
if !small {
|
||||
for (a, ga) in current.iter().enumerate() {
|
||||
for (b, gb) in current.iter().enumerate() {
|
||||
let s = ga[0] * gb[0] * tau[0] + ga[1] * gb[1] * tau[1] + (ga[0] * gb[1] + ga[1] * gb[0]) * tau[3];
|
||||
k.add(2 * a, 2 * b, dv * (s + tau[2] * ga[2] * gb[2]));
|
||||
k.add(2 * a + 1, 2 * b + 1, dv * s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push((f, j, at.x, r));
|
||||
out.push(PlanePoint { f, j, x: at.x, response: r });
|
||||
}
|
||||
Ok((Finite { force, tangent: k }, out))
|
||||
}
|
||||
@@ -369,19 +576,44 @@ impl Plane<'_> {
|
||||
/// Internal forces (u, v per node) and (with `tangent`) the tangent stiffness at nodal
|
||||
/// displacements `u`, as for [`Solid::finite_strain`].
|
||||
pub fn finite_strain(&self, m: &MaterialModel, u: &[[f64; 2]], committed: &[f64], updated: &mut [f64], tangent: bool) -> Result<Finite> {
|
||||
Ok(self.finite_points(m, u, committed, updated, tangent)?.0)
|
||||
Ok(self.nonlinear_points(m, u, Kinematics::Finite, None, committed, updated, tangent)?.0)
|
||||
}
|
||||
|
||||
/// The Cauchy stress and logarithmic strain at every integration point, as the solid
|
||||
/// As [`Solid::nonlinear`], on the 2D element.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn nonlinear(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 2]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> Result<Finite> {
|
||||
Ok(self.nonlinear_points(m, u, kin, thermal, committed, updated, tangent)?.0)
|
||||
}
|
||||
|
||||
/// The Cauchy stress and the total strain at every integration point, as the solid
|
||||
/// elements' 6-vectors (axisymmetric: rr, axial, hoop, r-axial).
|
||||
pub fn finite_strain_stresses(&self, m: &MaterialModel, u: &[[f64; 2]], committed: &[f64]) -> Result<Vec<PointStress>> {
|
||||
pub fn nonlinear_stresses(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 2]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
) -> Result<Vec<PointStress>> {
|
||||
let mut scratch = committed.to_vec();
|
||||
let (_, points) = self.finite_points(m, u, committed, &mut scratch, false)?;
|
||||
let (_, points) = self.nonlinear_points(m, u, kin, thermal, committed, &mut scratch, false)?;
|
||||
points
|
||||
.iter()
|
||||
.map(|(f, j, x, r)| {
|
||||
let strain = logarithmic_strain(f).ok_or_else(|| inverted(&format!("2D {}", self.ty.kind.name()), *j))?;
|
||||
Ok(PointStress { xyz: *x, strain, stress: r.stress.map(|v| v / j) })
|
||||
.map(|p| {
|
||||
if kin == Kinematics::Small {
|
||||
return Ok(PointStress { xyz: p.x, strain: small_strain(&p.f), stress: p.response.stress });
|
||||
}
|
||||
let strain = logarithmic_strain(&p.f).ok_or_else(|| inverted(&format!("2D {}", self.ty.kind.name()), p.j))?;
|
||||
Ok(PointStress { xyz: p.x, strain, stress: p.response.stress.map(|v| v / p.j) })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -420,20 +652,26 @@ mod tests {
|
||||
|
||||
/// Central differences of the internal forces against the tangent.
|
||||
fn check_tangent(solid: &Solid, m: &MaterialModel, u: &[[f64; 3]]) {
|
||||
let k = solid.finite_strain(m, u, &[], &mut [], true).unwrap().tangent.unwrap();
|
||||
check_tangent_from(solid, m, u, Kinematics::Finite, &[], 1e-6);
|
||||
}
|
||||
|
||||
/// The same from a committed history, with either kinematics.
|
||||
fn check_tangent_from(solid: &Solid, m: &MaterialModel, u: &[[f64; 3]], kin: Kinematics, committed: &[f64], tol: f64) {
|
||||
let mut scratch = committed.to_vec();
|
||||
let k = solid.nonlinear_points(m, u, kin, None, committed, &mut scratch, true).unwrap().0.tangent.unwrap();
|
||||
let scale = k.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let n = 3 * u.len();
|
||||
let h = 1e-6;
|
||||
let h = if kin == Kinematics::Small { 1e-9 } else { 1e-6 };
|
||||
for j in 0..n {
|
||||
let shifted = |s: f64| {
|
||||
let mut v = u.to_vec();
|
||||
v[j / 3][j % 3] += s;
|
||||
solid.finite_strain(m, &v, &[], &mut [], false).unwrap().force
|
||||
solid.nonlinear_points(m, &v, kin, None, committed, &mut committed.to_vec(), false).unwrap().0.force
|
||||
};
|
||||
let (p, q) = (shifted(h), shifted(-h));
|
||||
for i in 0..n {
|
||||
let numeric = (p[i] - q[i]) / (2.0 * h);
|
||||
assert!((numeric - k.get(i, j)).abs() < 1e-6 * scale, "{:?} {i} {j}: {numeric} vs {}", solid.ty.kind, k.get(i, j));
|
||||
assert!((numeric - k.get(i, j)).abs() < tol * scale, "{:?} {kin:?} {i} {j}: {numeric} vs {}", solid.ty.kind, k.get(i, j));
|
||||
}
|
||||
}
|
||||
for i in 0..n {
|
||||
@@ -465,6 +703,10 @@ mod tests {
|
||||
for (kind, x, np) in [(ElementKind::Hex8, &x8, 1), (ElementKind::Hex20, &x20, 4), (ElementKind::Tet10, &x10, 1)] {
|
||||
let hybrid = Solid::new(kind, x, Formulation::Hybrid { pressures: np }).unwrap();
|
||||
check_tangent(&hybrid, &neo, &displacement(x.len(), 0.08));
|
||||
let mr = MaterialModel::Polynomial { name: "Mooney–Rivlin", c: [100.0, 50.0, 0.0, 0.0], bulk: 3000.0 };
|
||||
let yeoh = MaterialModel::Polynomial { name: "Yeoh", c: [150.0, 0.0, -10.0, 2.0], bulk: 3000.0 };
|
||||
check_tangent(&hybrid, &mr, &displacement(x.len(), 0.08));
|
||||
check_tangent(&hybrid, &yeoh, &displacement(x.len(), 0.08));
|
||||
assert!(hybrid.finite_strain(&svk, &displacement(x.len(), 0.0), &[], &mut [], false).is_err(), "SVK has no split");
|
||||
}
|
||||
let im = Solid::new(ElementKind::Hex8, &x8, Formulation::IncompatibleModes).unwrap();
|
||||
@@ -493,6 +735,164 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// J2 plasticity in full and hybrid elements, at finite and small strain: the tangent is
|
||||
/// the derivative of the forces from a plastic history too, and the plastic points are
|
||||
/// really plastic.
|
||||
#[test]
|
||||
fn plastic_tangents_are_the_derivative_of_the_forces() {
|
||||
use crate::elastoplastic::{HISTORY, PLASTIC_STRAIN};
|
||||
use fieldcraft_model::{Elastic, Hardening, Plastic};
|
||||
let j2 = crate::plasticity::J2::from_model(
|
||||
&Elastic::Isotropic { young: 1000.0, poisson: 0.3 },
|
||||
Some(&Plastic::Bilinear { yield_stress: 5.0, hardening_modulus: 50.0, hardening: Hardening::Isotropic }),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let m = MaterialModel::Plastic(std::sync::Arc::new(j2));
|
||||
let (x20, x10) = (hex20(), tet10());
|
||||
let x8: Vec<[f64; 3]> = x20[..8].to_vec();
|
||||
for (kind, x, f) in [
|
||||
(ElementKind::Hex8, &x8, Formulation::Full),
|
||||
(ElementKind::Tet10, &x10, Formulation::Full),
|
||||
(ElementKind::Hex20, &x20, Formulation::Reduced),
|
||||
(ElementKind::Hex8, &x8, Formulation::Hybrid { pressures: 1 }),
|
||||
(ElementKind::Hex20, &x20, Formulation::Hybrid { pressures: 4 }),
|
||||
] {
|
||||
let solid = Solid::new(kind, x, f).unwrap();
|
||||
let points = solid.rule().points().len();
|
||||
for (kin, scale) in [(Kinematics::Finite, 0.05), (Kinematics::Small, 0.05)] {
|
||||
// A first increment from the virgin state, committed, then a second from it.
|
||||
let mut committed = vec![0.0; points * HISTORY];
|
||||
let first = displacement(x.len(), 0.5 * scale);
|
||||
solid.nonlinear_points(&m, &first, kin, None, &vec![0.0; points * HISTORY], &mut committed, false).unwrap();
|
||||
assert!((0..points).all(|p| committed[p * HISTORY + PLASTIC_STRAIN] > 0.0), "{kind:?} {f:?} {kin:?} yields everywhere");
|
||||
check_tangent_from(&solid, &m, &displacement(x.len(), scale), kin, &committed, 1e-6);
|
||||
}
|
||||
}
|
||||
// Incompatible modes have no inelastic form.
|
||||
let im = Solid::new(ElementKind::Hex8, &x8, Formulation::IncompatibleModes).unwrap();
|
||||
assert!(matches!(
|
||||
im.nonlinear(&m, &displacement(8, 0.0), Kinematics::Small, None, &[0.0; 8 * HISTORY], &mut [0.0; 8 * HISTORY], true),
|
||||
Err(Error::NotSupported(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plastic_plane_strain_and_axisymmetric_tangents_are_the_derivative_of_the_forces() {
|
||||
use crate::elastoplastic::{HISTORY, PLASTIC_STRAIN};
|
||||
use fieldcraft_model::{Elastic, Hardening, Plastic};
|
||||
let j2 = crate::plasticity::J2::from_model(
|
||||
&Elastic::Isotropic { young: 1000.0, poisson: 0.3 },
|
||||
Some(&Plastic::Bilinear { yield_stress: 5.0, hardening_modulus: 50.0, hardening: Hardening::Isotropic }),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let m = MaterialModel::Plastic(std::sync::Arc::new(j2));
|
||||
let quad8 = [
|
||||
[1.0, 0.0, 0.0],
|
||||
[2.0, 0.1, 0.0],
|
||||
[2.2, 1.0, 0.0],
|
||||
[0.9, 1.1, 0.0],
|
||||
[1.5, 0.0, 0.0],
|
||||
[2.1, 0.55, 0.0],
|
||||
[1.5, 1.1, 0.0],
|
||||
[0.95, 0.5, 0.0],
|
||||
];
|
||||
for theory in [Theory::PlaneStrain { thickness: 2.0 }, Theory::Axisymmetric] {
|
||||
for (kind, x) in [(ElementKind::Quad8, &quad8[..]), (ElementKind::Tri3, &quad8[..3])] {
|
||||
let plane = Plane::new(kind, x, Formulation::Full, theory).unwrap();
|
||||
let points = plane.rule().points().len();
|
||||
for kin in [Kinematics::Finite, Kinematics::Small] {
|
||||
let u = |scale: f64| -> Vec<[f64; 2]> { displacement(x.len(), scale).iter().map(|v| [v[0], v[1]]).collect() };
|
||||
let mut committed = vec![0.0; points * HISTORY];
|
||||
plane.nonlinear_points(&m, &u(0.03), kin, None, &vec![0.0; points * HISTORY], &mut committed, false).unwrap();
|
||||
assert!((0..points).all(|p| committed[p * HISTORY + PLASTIC_STRAIN] > 0.0), "{theory:?} {kind:?} {kin:?}");
|
||||
let at = u(0.06);
|
||||
let k = plane.nonlinear_points(&m, &at, kin, None, &committed, &mut committed.clone(), true).unwrap().0.tangent.unwrap();
|
||||
let scale = k.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let h = 1e-8;
|
||||
for j in 0..2 * x.len() {
|
||||
let shifted = |s: f64| {
|
||||
let mut v = at.clone();
|
||||
v[j / 2][j % 2] += s;
|
||||
plane.nonlinear_points(&m, &v, kin, None, &committed, &mut committed.clone(), false).unwrap().0.force
|
||||
};
|
||||
let (p, q) = (shifted(h), shifted(-h));
|
||||
for i in 0..2 * x.len() {
|
||||
let numeric = (p[i] - q[i]) / (2.0 * h);
|
||||
assert!(
|
||||
(numeric - k.get(i, j)).abs() < 1e-6 * scale,
|
||||
"{theory:?} {kind:?} {kin:?} {i} {j}: {numeric} vs {}",
|
||||
k.get(i, j)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let stress = Plane::new(ElementKind::Quad4, &quad8[..4], Formulation::Full, Theory::PlaneStress { thickness: 1.0 }).unwrap();
|
||||
assert!(matches!(
|
||||
stress.nonlinear(&m, &[[0.0; 2]; 4], Kinematics::Small, None, &[0.0; 4 * HISTORY], &mut [0.0; 4 * HISTORY], true),
|
||||
Err(Error::NotSupported(_))
|
||||
));
|
||||
}
|
||||
|
||||
/// A temperature field: the tangent is the derivative of the forces (finite and small strain,
|
||||
/// full and hybrid, elastic and plastic), and a free expansion u = (F_θ − I) X of a uniform
|
||||
/// temperature stresses nothing.
|
||||
#[test]
|
||||
fn thermal_tangents_and_free_expansion() {
|
||||
use crate::elastoplastic::HISTORY;
|
||||
use fieldcraft_model::{Elastic, Hardening, Plastic};
|
||||
let j2 = crate::plasticity::J2::from_model(
|
||||
&Elastic::Isotropic { young: 1000.0, poisson: 0.3 },
|
||||
Some(&Plastic::Bilinear { yield_stress: 5.0, hardening_modulus: 50.0, hardening: Hardening::Isotropic }),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let plastic = MaterialModel::Plastic(std::sync::Arc::new(j2));
|
||||
let neo = MaterialModel::NeoHookean { shear: 300.0, bulk: 3000.0 };
|
||||
let x20 = hex20();
|
||||
let field = |n: usize| Temperature {
|
||||
alpha: [1e-3, 2e-3, 1.5e-3],
|
||||
reference: 20.0,
|
||||
nodal: (0..n).map(|i| 20.0 + 40.0 * (i as f64 * 0.37).sin()).collect(),
|
||||
};
|
||||
for f in [Formulation::Full, Formulation::Hybrid { pressures: 4 }] {
|
||||
let solid = Solid::new(ElementKind::Hex20, &x20, f).unwrap();
|
||||
let points = solid.rule().points().len();
|
||||
let t = field(20);
|
||||
for (m, kin, len) in [(&neo, Kinematics::Finite, 0), (&plastic, Kinematics::Finite, HISTORY), (&plastic, Kinematics::Small, HISTORY)] {
|
||||
let u = displacement(20, 0.03);
|
||||
let committed = vec![0.0; points * len];
|
||||
let k = solid.nonlinear(m, &u, kin, Some(&t), &committed, &mut committed.clone(), true).unwrap().tangent.unwrap();
|
||||
let scale = k.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let h = if kin == Kinematics::Small { 1e-9 } else { 1e-7 };
|
||||
for j in 0..60 {
|
||||
let shifted = |s: f64| {
|
||||
let mut v = u.clone();
|
||||
v[j / 3][j % 3] += s;
|
||||
solid.nonlinear(m, &v, kin, Some(&t), &committed, &mut committed.clone(), false).unwrap().force
|
||||
};
|
||||
let (p, q) = (shifted(h), shifted(-h));
|
||||
for i in 0..60 {
|
||||
let numeric = (p[i] - q[i]) / (2.0 * h);
|
||||
assert!((numeric - k.get(i, j)).abs() < 1e-6 * scale, "{f:?} {} {kin:?} {i} {j}: {numeric} vs {}", m.name(), k.get(i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Free expansion by a uniform 150 degrees: no force, no stress.
|
||||
let uniform = Temperature { alpha: [1e-3, 2e-3, 1.5e-3], reference: 20.0, nodal: vec![170.0; 20] };
|
||||
let stretch = uniform.alpha.map(|a| 1.0 + 150.0 * a);
|
||||
let u: Vec<[f64; 3]> = x20.iter().map(|x| std::array::from_fn(|k| (stretch[k] - 1.0) * x[k])).collect();
|
||||
let r = solid.nonlinear(&neo, &u, Kinematics::Finite, Some(&uniform), &[], &mut [], false).unwrap();
|
||||
assert!(r.force.iter().all(|v| v.abs() < 1e-10 * 3000.0), "{f:?}: {:?}", r.force);
|
||||
let s = solid.nonlinear_stresses(&neo, &u, Kinematics::Finite, Some(&uniform), &[]).unwrap();
|
||||
assert!(s.iter().all(|p| p.stress.iter().all(|v| v.abs() < 1e-10 * 3000.0)), "{f:?}");
|
||||
assert!((s[0].strain[1] - stretch[1].ln()).abs() < 1e-12, "total logarithmic strain: {:?}", s[0].strain);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rigid_rotation_strains_and_stresses_nothing() {
|
||||
let x20 = hex20();
|
||||
@@ -501,7 +901,7 @@ mod tests {
|
||||
let neo = MaterialModel::NeoHookean { shear: 300.0, bulk: 3000.0 };
|
||||
for f in [Formulation::Full, Formulation::Hybrid { pressures: 4 }] {
|
||||
let solid = Solid::new(ElementKind::Hex20, &x20, f).unwrap();
|
||||
let s = solid.finite_strain_stresses(&neo, &u, &[]).unwrap();
|
||||
let s = solid.nonlinear_stresses(&neo, &u, Kinematics::Finite, None, &[]).unwrap();
|
||||
assert!(s.iter().all(|p| p.stress.iter().chain(&p.strain).all(|v| v.abs() < 1e-12 * 3000.0)), "{f:?}: {:?}", s[0]);
|
||||
let r = solid.finite_strain(&neo, &u, &[], &mut [], false).unwrap();
|
||||
assert!(r.force.iter().all(|v| v.abs() < 1e-11 * 3000.0));
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
//! the solid kernels evaluate;
|
||||
//! - [`material`]: isotropic and orthotropic elasticity matrices, von Mises stress;
|
||||
//! - [`constitutive`]: the material point of finite-strain analysis (Kirchhoff stress and
|
||||
//! spatial tangent from the deformation gradient, with history): St Venant–Kirchhoff and
|
||||
//! neo-Hookean;
|
||||
//! spatial tangent from the deformation gradient, with history): St Venant–Kirchhoff,
|
||||
//! neo-Hookean and J2 plasticity;
|
||||
//! - [`element`]: the element table, stiffness (full, reduced, incompatible modes), consistent
|
||||
//! and lumped mass, body forces, integration-point stresses and their extrapolation to nodes;
|
||||
//! - [`finite`]: total Lagrangian solid and 2D elements (internal forces, the consistent tangent
|
||||
//! with its geometric part, mixed u–p at finite strain, Cauchy stress and logarithmic strain);
|
||||
//! with its geometric part, mixed u–p at finite strain, Cauchy stress and logarithmic strain),
|
||||
//! and the same with small strain for inelastic materials;
|
||||
//! - [`plane`]: 2D elements (plane stress, plane strain, axisymmetric) on tri3, tri6, quad4 and
|
||||
//! quad8, with two displacement components per node;
|
||||
//! - [`shell`]: flat MITC4 quadrilateral and MITC3 triangle shells with a drilling rotation, six
|
||||
@@ -33,6 +34,9 @@
|
||||
//! shape vectors and element frequency bounds for explicit dynamics;
|
||||
//! - [`rotation`]: incrementally objective stress updates (Hughes–Winget Jaumann, Flanagan–Taylor
|
||||
//! Green–Naghdi);
|
||||
//! - [`elastoplastic`]: J2 plasticity at a material point of implicit analysis, at finite strain
|
||||
//! (Simo's exponential map in the principal logarithmic strains) and at small strain, with the
|
||||
//! consistent tangents, on the radial return of [`plasticity`];
|
||||
//! - [`plasticity`]: J2 radial return with isotropic, kinematic and Johnson–Cook hardening,
|
||||
//! Johnson–Cook damage;
|
||||
//! - [`truss`]: two- and three-node trusses (straight or curved), their stiffness, mass, body
|
||||
@@ -52,6 +56,7 @@ pub mod beam;
|
||||
pub mod constitutive;
|
||||
pub mod dense;
|
||||
pub mod dof;
|
||||
pub mod elastoplastic;
|
||||
pub mod element;
|
||||
pub mod face;
|
||||
pub mod finite;
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
//! [`MIN_FRACTURE_STRAIN`] counts as that (some published sets give negative values at high
|
||||
//! triaxiality, where the point fails at once). The point has failed at D ≥ 1.
|
||||
//!
|
||||
//! **One return map for both solvers.** [`J2::radial_return`] is the return above; explicit
|
||||
//! dynamics calls it through [`J2::update`] on its hypoelastic stress, the implicit material
|
||||
//! point ([`crate::elastoplastic`]) on its trial elastic strain, small or logarithmic, and adds
|
||||
//! the consistent tangent [`J2::tangent`].
|
||||
//!
|
||||
//! The strain increment the update takes is the **mechanical** one: a caller with initial or
|
||||
//! thermal strains subtracts their increment first.
|
||||
//!
|
||||
@@ -98,6 +103,21 @@ impl PointState {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a radial return did: what the consistent tangent needs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Return {
|
||||
/// The equivalent plastic strain increment Δε_p.
|
||||
pub plastic_strain: f64,
|
||||
/// The trial von Mises stress of ξ* = s* − α, q* = √(3/2)‖ξ*‖.
|
||||
pub trial: f64,
|
||||
/// The flow direction ξ*/‖ξ*‖ (tensor components).
|
||||
pub normal: [f64; 6],
|
||||
/// The kinematic hardening modulus H_k.
|
||||
pub kinematic: f64,
|
||||
/// The slope of the flow stress against the plastic strain at the end of the increment.
|
||||
pub slope: f64,
|
||||
}
|
||||
|
||||
/// What one update did.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct Increment {
|
||||
@@ -209,6 +229,11 @@ impl J2 {
|
||||
Ok(J2 { shear, bulk, flow, damage, heating })
|
||||
}
|
||||
|
||||
/// Whether the flow stress depends on the temperature (Johnson–Cook softening).
|
||||
pub fn is_temperature_dependent(&self) -> bool {
|
||||
matches!(self.flow, Some(Flow::JohnsonCook { .. }))
|
||||
}
|
||||
|
||||
/// The temperature a point starts at.
|
||||
pub fn initial_temperature(&self) -> f64 {
|
||||
match self.flow {
|
||||
@@ -232,27 +257,11 @@ impl J2 {
|
||||
}
|
||||
let pressure = mean + self.bulk * vol;
|
||||
let mut out = Increment::default();
|
||||
if let Some(flow) = &self.flow {
|
||||
let xi: [f64; 6] = std::array::from_fn(|k| trial[k] - state.back[k]);
|
||||
let norm = norm2(&xi).sqrt();
|
||||
let q = (1.5f64).sqrt() * norm;
|
||||
let (yield_now, _) = flow.stress(state.plastic_strain, 0.0, state.temperature, dt);
|
||||
if q > yield_now && norm > 0.0 {
|
||||
let hk = flow.kinematic();
|
||||
let de = self.plastic_increment(flow, state, q, yield_now, hk, dt);
|
||||
let n: [f64; 6] = xi.map(|x| x / norm);
|
||||
let radial = g2 * (1.5f64).sqrt() * de;
|
||||
let shift = (2.0f64 / 3.0).sqrt() * hk * de;
|
||||
for k in 0..6 {
|
||||
trial[k] -= radial * n[k];
|
||||
state.back[k] += shift * n[k];
|
||||
}
|
||||
state.plastic_strain += de;
|
||||
let q_new = q - (3.0 * self.shear + hk) * de;
|
||||
out = Increment { plastic_strain: de, plastic_work: q_new * de };
|
||||
if self.heating > 0.0 && density > 0.0 {
|
||||
state.temperature += self.heating * out.plastic_work / density;
|
||||
}
|
||||
if let Some(r) = self.radial_return(state, &mut trial, dt) {
|
||||
let q_new = r.trial - (3.0 * self.shear + r.kinematic) * r.plastic_strain;
|
||||
out = Increment { plastic_strain: r.plastic_strain, plastic_work: q_new * r.plastic_strain };
|
||||
if self.heating > 0.0 && density > 0.0 {
|
||||
state.temperature += self.heating * out.plastic_work / density;
|
||||
}
|
||||
}
|
||||
for k in 0..3 {
|
||||
@@ -273,6 +282,68 @@ impl J2 {
|
||||
out
|
||||
}
|
||||
|
||||
/// The radial return of a trial deviator `trial` (tensor components) from `state`: when the
|
||||
/// trial von Mises stress exceeds the yield stress, `trial` is moved back to the yield
|
||||
/// surface, the back stress and the equivalent plastic strain of `state` are advanced, and
|
||||
/// what the consistent tangent needs is returned; `None` when the point stays elastic.
|
||||
pub fn radial_return(&self, state: &mut PointState, trial: &mut [f64; 6], dt: f64) -> Option<Return> {
|
||||
let flow = self.flow.as_ref()?;
|
||||
let xi: [f64; 6] = std::array::from_fn(|k| trial[k] - state.back[k]);
|
||||
let norm = norm2(&xi).sqrt();
|
||||
let q = (1.5f64).sqrt() * norm;
|
||||
let (yield_now, _) = flow.stress(state.plastic_strain, 0.0, state.temperature, dt);
|
||||
if !(q > yield_now && norm > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let hk = flow.kinematic();
|
||||
let de = self.plastic_increment(flow, state, q, yield_now, hk, dt);
|
||||
let n: [f64; 6] = xi.map(|x| x / norm);
|
||||
let radial = 2.0 * self.shear * (1.5f64).sqrt() * de;
|
||||
let shift = (2.0f64 / 3.0).sqrt() * hk * de;
|
||||
for k in 0..6 {
|
||||
trial[k] -= radial * n[k];
|
||||
state.back[k] += shift * n[k];
|
||||
}
|
||||
let rate = if dt > 0.0 { de / dt } else { 0.0 };
|
||||
let (_, slope) = flow.stress(state.plastic_strain + de, rate, state.temperature, dt);
|
||||
state.plastic_strain += de;
|
||||
Some(Return { plastic_strain: de, trial: q, normal: n, kinematic: hk, slope })
|
||||
}
|
||||
|
||||
/// The consistent (algorithmic) tangent of the return map, dσ/dε of the stress after the
|
||||
/// update against the total strain of the increment (engineering-shear columns): the
|
||||
/// elasticity K I ⊗ I + 2G ℙ while elastic, and after a return
|
||||
/// K I ⊗ I + 2G θ ℙ − 2G θ̄ n ⊗ n with θ = 1 − 3G Δε_p / q*, θ̄ = 1/(1 + H/3G) − (1 − θ) and
|
||||
/// H the slope of the flow stress plus the kinematic modulus at the end of the increment
|
||||
/// (Simo & Hughes 1998, Box 3.2, eq. 3.3.10, in the equivalent plastic strain). `bulk`
|
||||
/// leaves out the volumetric part K I ⊗ I when false (the isochoric part a mixed element
|
||||
/// takes). `None` when 3G + H is not positive: the point softens faster than it unloads
|
||||
/// elastically and has no tangent.
|
||||
pub fn tangent(&self, r: Option<&Return>, bulk: bool) -> Option<[[f64; 6]; 6]> {
|
||||
let g = self.shear;
|
||||
let (theta, theta_bar, n) = match r {
|
||||
None => (1.0, 0.0, [0.0; 6]),
|
||||
Some(r) => {
|
||||
let stiff = 3.0 * g + r.slope + r.kinematic;
|
||||
if !(stiff > 0.0 && stiff.is_finite() && r.trial > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let theta = 1.0 - 3.0 * g * r.plastic_strain / r.trial;
|
||||
(theta, 3.0 * g / stiff - (1.0 - theta), r.normal)
|
||||
}
|
||||
};
|
||||
let k = if bulk { self.bulk } else { 0.0 };
|
||||
let identity = crate::constitutive::identity4();
|
||||
let mut d = [[0.0; 6]; 6];
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
let (ia, ib) = (if a < 3 { 1.0 } else { 0.0 }, if b < 3 { 1.0 } else { 0.0 });
|
||||
d[a][b] = k * ia * ib + 2.0 * g * theta * (identity[a][b] - ia * ib / 3.0) - 2.0 * g * theta_bar * n[a] * n[b];
|
||||
}
|
||||
}
|
||||
Some(d)
|
||||
}
|
||||
|
||||
/// The plastic strain increment of a trial state that yields.
|
||||
fn plastic_increment(&self, flow: &Flow, state: &PointState, q: f64, yield_now: f64, hk: f64, dt: f64) -> f64 {
|
||||
let g3 = 3.0 * self.shear;
|
||||
|
||||
@@ -62,8 +62,8 @@ pub use mesh::{Element, ElementEdge, ElementFace, ElementKind, Node, check_coord
|
||||
pub use sections::{BeamProfile, Extent, Formulation, Integration, Section, SectionKind};
|
||||
pub use sets::Set;
|
||||
pub use steps::{
|
||||
BulkViscosity, ExplicitDynamic, Hourglass, HourglassKind, InitialVelocity, MAX_NONLINEAR_INCREMENTS, MAX_NONLINEAR_ITERATIONS, NonlinearStatic,
|
||||
OutputVariable, Procedure, RigidWall, Step, StressRate, Tolerances, Transient,
|
||||
BulkViscosity, ExplicitDynamic, Hourglass, HourglassKind, InitialVelocity, MAX_LOAD_PATH_POINTS, MAX_NONLINEAR_INCREMENTS,
|
||||
MAX_NONLINEAR_ITERATIONS, NonlinearStatic, OutputVariable, Procedure, RigidWall, Step, StressRate, Tolerances, Transient,
|
||||
};
|
||||
pub use units::UnitSystem;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Materials: linear elasticity, mass density, fluid behaviour, thermal properties (conductivity,
|
||||
//! specific heat, expansion), and the inelastic behaviours of
|
||||
//! explicit dynamics: von Mises (J2) plasticity with a bilinear, tabulated or Johnson–Cook flow stress,
|
||||
//! Johnson–Cook damage, and element erosion criteria.
|
||||
//! specific heat, expansion), and the inelastic behaviours: von Mises (J2) plasticity with a
|
||||
//! bilinear, tabulated or Johnson–Cook flow stress (explicit dynamics and nonlinear static
|
||||
//! steps), Johnson–Cook damage and element erosion criteria (explicit dynamics).
|
||||
//!
|
||||
//! The records are solver-neutral and named for what they describe, never after a product's
|
||||
//! keyword or material number; the deck writers map them to each format.
|
||||
@@ -88,6 +88,16 @@ pub enum Hyperelastic {
|
||||
/// reference volume W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)², with Ī₁ = J^(−2/3) tr C (C₁₀ = μ/2,
|
||||
/// D₁ = 2/κ in the usual polynomial notation).
|
||||
NeoHookean {},
|
||||
/// Mooney–Rivlin (M. Mooney, J. Appl. Phys. 11:582–592, 1940; R. S. Rivlin, Phil. Trans. R.
|
||||
/// Soc. A 241:379–397, 1948), decoupled: W = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + κ/2 (J − 1)². The
|
||||
/// small-strain shear modulus 2 (C₁₀ + C₀₁) is the elastic μ, so `c01` sets the second
|
||||
/// invariant's share and C₁₀ = μ/2 − C₀₁ (0 ≤ C₀₁ ≤ μ/2, both constants non-negative).
|
||||
MooneyRivlin { c01: f64 },
|
||||
/// Yeoh (O. H. Yeoh, Rubber Chem. Technol. 66(5):754–771, 1993), decoupled:
|
||||
/// W = C₁₀ (Ī₁ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ + κ/2 (J − 1)², with C₁₀ = μ/2 from the
|
||||
/// elastic constants (the small-strain shear modulus); `c20` (often negative) and `c30`
|
||||
/// shape the large-strain response.
|
||||
Yeoh { c20: f64, c30: f64 },
|
||||
}
|
||||
|
||||
impl Hyperelastic {
|
||||
@@ -101,8 +111,39 @@ impl Hyperelastic {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Hyperelastic::NeoHookean {} => "neo-Hookean",
|
||||
Hyperelastic::MooneyRivlin { .. } => "Mooney–Rivlin",
|
||||
Hyperelastic::Yeoh { .. } => "Yeoh",
|
||||
}
|
||||
}
|
||||
|
||||
/// The polynomial constants (C₁₀, C₀₁, C₂₀, C₃₀) for the shear modulus μ.
|
||||
pub fn constants(&self, shear: f64) -> [f64; 4] {
|
||||
match *self {
|
||||
Hyperelastic::NeoHookean {} => [shear / 2.0, 0.0, 0.0, 0.0],
|
||||
Hyperelastic::MooneyRivlin { c01 } => [shear / 2.0 - c01, c01, 0.0, 0.0],
|
||||
Hyperelastic::Yeoh { c20, c30 } => [shear / 2.0, 0.0, c20, c30],
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the option's constants against the elastic shear modulus μ.
|
||||
pub fn validate(&self, shear: f64) -> Result<()> {
|
||||
match *self {
|
||||
Hyperelastic::NeoHookean {} => {}
|
||||
Hyperelastic::MooneyRivlin { c01 } => {
|
||||
if !(c01 >= 0.0 && c01 <= shear / 2.0) {
|
||||
return Err(Error::Invalid(format!(
|
||||
"the Mooney–Rivlin C01 = {c01} must be within [0, μ/2 = {}] (C10 = μ/2 − C01 and C01 both non-negative)",
|
||||
shear / 2.0
|
||||
)));
|
||||
}
|
||||
}
|
||||
Hyperelastic::Yeoh { c20, c30 } => {
|
||||
finite("Yeoh C20", c20)?;
|
||||
finite("Yeoh C30", c30)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fluid behaviour.
|
||||
@@ -372,8 +413,8 @@ pub struct Material {
|
||||
/// How it flows, for fluid domains (needs a density).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fluid: Option<Fluid>,
|
||||
/// Von Mises plasticity (explicit dynamics; the linear implicit solver uses the elastic
|
||||
/// constants alone).
|
||||
/// Von Mises plasticity (explicit dynamics and nonlinear static steps; the linear solvers use
|
||||
/// the elastic constants alone).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub plastic: Option<Plastic>,
|
||||
/// Damage to failure (explicit dynamics: a failed element erodes).
|
||||
@@ -407,6 +448,9 @@ impl Material {
|
||||
if self.plastic.is_some() {
|
||||
return Err(Error::Invalid(format!("plasticity of a {} hyperelastic material is not supported yet", h.name())));
|
||||
}
|
||||
if let Some(Elastic::Isotropic { young, poisson }) = self.elastic {
|
||||
h.validate(Hyperelastic::moduli(young, poisson).0)?;
|
||||
}
|
||||
}
|
||||
if let Some(d) = self.density {
|
||||
positive("density", d)?;
|
||||
@@ -607,7 +651,28 @@ mod tests {
|
||||
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());
|
||||
assert!(Material { plastic: Some(plastic), ..m.clone() }.validate().is_err());
|
||||
// Mooney–Rivlin and Yeoh: μ = E/(2(1 + ν)) = 1.00671 here, so C01 is within [0, μ/2].
|
||||
let mr: Material = serde_json::from_value(serde_json::json!({
|
||||
"elastic": {"type": "isotropic", "young": 3.0, "poisson": 0.49}, "hyperelastic": {"type": "mooney_rivlin", "c01": 0.1}
|
||||
}))
|
||||
.unwrap();
|
||||
mr.validate().unwrap();
|
||||
let mu = Hyperelastic::moduli(3.0, 0.49).0;
|
||||
let c = Hyperelastic::MooneyRivlin { c01: 0.1 }.constants(mu);
|
||||
assert!((2.0 * (c[0] + c[1]) - mu).abs() < 1e-15 && c[1] == 0.1);
|
||||
let yeoh = Hyperelastic::Yeoh { c20: -0.01, c30: 0.001 };
|
||||
assert_eq!(yeoh.constants(mu), [mu / 2.0, 0.0, -0.01, 0.001]);
|
||||
assert!(Material { hyperelastic: Some(yeoh), ..m.clone() }.validate().is_ok());
|
||||
for bad in [
|
||||
Hyperelastic::MooneyRivlin { c01: -0.1 },
|
||||
Hyperelastic::MooneyRivlin { c01: 0.6 },
|
||||
Hyperelastic::MooneyRivlin { c01: f64::NAN },
|
||||
Hyperelastic::Yeoh { c20: f64::INFINITY, c30: 0.0 },
|
||||
Hyperelastic::Yeoh { c20: 0.0, c30: f64::NAN },
|
||||
] {
|
||||
assert!(Material { hyperelastic: Some(bad), ..m.clone() }.validate().is_err(), "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -87,9 +87,9 @@ pub enum Procedure {
|
||||
/// 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.
|
||||
/// displacements, rotations and strains (`nonlinear_geometry`), hyperelastic and plastic
|
||||
/// materials and follower pressure. Loads and prescribed displacements are the step's load
|
||||
/// factor times their values: a ramp from 0 to 1, or the step's load path.
|
||||
NonlinearStatic(NonlinearStatic),
|
||||
}
|
||||
|
||||
@@ -130,8 +130,18 @@ pub struct NonlinearStatic {
|
||||
/// 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,
|
||||
/// The load factor against the step fraction: piecewise linear through these
|
||||
/// `[fraction, factor]` points, from `[0, 0]` to fraction 1 (`None`: the linear ramp, the
|
||||
/// factor equal to the fraction). Loads and prescribed displacements are the factor times
|
||||
/// their values, so a path that comes down unloads and one that turns negative reverses
|
||||
/// them (cyclic loading). Increments end at every point of the path.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub load_path: Option<Vec<[f64; 2]>>,
|
||||
}
|
||||
|
||||
/// Most points of a nonlinear step's load path.
|
||||
pub const MAX_LOAD_PATH_POINTS: usize = 1000;
|
||||
|
||||
/// When an increment's Newton iterations have converged: all three hold.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
@@ -162,6 +172,7 @@ impl Default for NonlinearStatic {
|
||||
max_iterations: 16,
|
||||
tolerances: Tolerances::default(),
|
||||
line_search: false,
|
||||
load_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,8 +203,41 @@ impl NonlinearStatic {
|
||||
return Err(Error::Invalid(format!("the {what} tolerance is in (0, 1), got {v}")));
|
||||
}
|
||||
}
|
||||
if let Some(path) = &self.load_path {
|
||||
if !(2..=MAX_LOAD_PATH_POINTS).contains(&path.len()) {
|
||||
return Err(Error::Invalid(format!("a load path has 2 to {MAX_LOAD_PATH_POINTS} points, got {}", path.len())));
|
||||
}
|
||||
if path.first() != Some(&[0.0, 0.0]) || path.last().map(|p| p[0]) != Some(1.0) {
|
||||
return Err(Error::Invalid("a load path starts at [0, 0] (the unloaded start of the step) and ends at fraction 1".into()));
|
||||
}
|
||||
if path.iter().flatten().any(|v| !v.is_finite()) {
|
||||
return Err(Error::Invalid("the points of a load path must be finite".into()));
|
||||
}
|
||||
if path.windows(2).any(|w| w.get(1).map(|b| b[0]) <= w.first().map(|a| a[0])) {
|
||||
return Err(Error::Invalid("the fractions of a load path must increase".into()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The load factor at step fraction `t` (in [0, 1]).
|
||||
pub fn load_factor(&self, t: f64) -> f64 {
|
||||
let Some(path) = &self.load_path else { return t };
|
||||
for w in path.windows(2) {
|
||||
if let [[t0, f0], [t1, f1]] = *w
|
||||
&& t <= t1
|
||||
{
|
||||
return if t1 > t0 { f0 + (f1 - f0) * ((t - t0) / (t1 - t0)).clamp(0.0, 1.0) } else { f1 };
|
||||
}
|
||||
}
|
||||
path.last().map_or(t, |p| p[1])
|
||||
}
|
||||
|
||||
/// The first point of the load path after fraction `t` (1 without a path), where an
|
||||
/// increment from `t` must end.
|
||||
pub fn next_turn(&self, t: f64) -> f64 {
|
||||
self.load_path.as_ref().and_then(|path| path.iter().map(|p| p[0]).find(|f| *f > t * (1.0 + 1e-12) + 1e-12)).unwrap_or(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Most time increments of a transient heat transfer step.
|
||||
@@ -900,6 +944,24 @@ mod tests {
|
||||
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());
|
||||
// A load path: up, down through zero, back to zero.
|
||||
let cyclic = NonlinearStatic { load_path: Some(vec![[0.0, 0.0], [0.25, 1.0], [0.75, -1.0], [1.0, 0.0]]), ..base.clone() };
|
||||
assert!(Procedure::NonlinearStatic(cyclic.clone()).validate().is_ok());
|
||||
for (t, f) in [(0.0, 0.0), (0.125, 0.5), (0.25, 1.0), (0.5, 0.0), (0.75, -1.0), (0.875, -0.5), (1.0, 0.0)] {
|
||||
assert!((cyclic.load_factor(t) - f).abs() < 1e-15, "{t}: {}", cyclic.load_factor(t));
|
||||
}
|
||||
assert_eq!((cyclic.next_turn(0.0), cyclic.next_turn(0.25), cyclic.next_turn(0.3), cyclic.next_turn(0.75)), (0.25, 0.75, 0.75, 1.0));
|
||||
assert_eq!((base.load_factor(0.3), base.next_turn(0.3)), (0.3, 1.0));
|
||||
for path in [
|
||||
vec![[0.0, 0.0]],
|
||||
vec![[0.0, 0.5], [1.0, 1.0]],
|
||||
vec![[0.0, 0.0], [0.9, 1.0]],
|
||||
vec![[0.0, 0.0], [0.5, 1.0], [0.5, 2.0], [1.0, 0.0]],
|
||||
vec![[0.0, 0.0], [0.5, f64::NAN], [1.0, 0.0]],
|
||||
vec![[0.0, 0.0]; MAX_LOAD_PATH_POINTS + 1],
|
||||
] {
|
||||
assert!(Procedure::NonlinearStatic(NonlinearStatic { load_path: Some(path.clone()), ..base.clone() }).validate().is_err(), "{path:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -61,6 +61,9 @@ pub struct ElementStress {
|
||||
pub points: Vec<PointStress>,
|
||||
/// Extrapolated to the element's nodes, in its node order.
|
||||
pub nodal: Vec<[f64; 6]>,
|
||||
/// The equivalent plastic strain at the integration points (nonlinear steps with a plastic
|
||||
/// material; empty otherwise).
|
||||
pub plastic_strain: Vec<f64>,
|
||||
}
|
||||
|
||||
/// The motion and reaction of a coupled reference point: translations along X, Y, Z, then
|
||||
@@ -350,9 +353,9 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
// loads.
|
||||
let (field, temperature) = match step.procedure {
|
||||
Procedure::CoupledTemperatureDisplacement {} => {
|
||||
let (labels, t) = crate::heat::temperatures(model, &resolved, step, limits)?;
|
||||
let at = |l: &u32| labels.binary_search(l).ok().and_then(|i| t.get(i)).copied();
|
||||
(Some(mesh.labels.iter().map(at).collect::<Vec<_>>()), Some(mesh.labels.iter().map(|l| at(l).unwrap_or(0.0)).collect()))
|
||||
let field = crate::heat::temperatures_at(model, &resolved, step, limits, &mesh.labels)?;
|
||||
let temperature = field.iter().map(|t| t.unwrap_or(0.0)).collect();
|
||||
(Some(field), Some(temperature))
|
||||
}
|
||||
_ => (temperature_loads(model, &resolved, &mesh, step)?, None),
|
||||
};
|
||||
@@ -431,7 +434,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
elements.push(ElementStress { label: e.label, kind: e.kind, points, nodal });
|
||||
elements.push(ElementStress { label: e.label, kind: e.kind, points, nodal, plastic_strain: Vec::new() });
|
||||
}
|
||||
for t in &mesh.trusses {
|
||||
let truss = t.truss()?;
|
||||
@@ -448,7 +451,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
elements.push(ElementStress { label: t.label, kind: t.kind, points, nodal });
|
||||
elements.push(ElementStress { label: t.label, kind: t.kind, points, nodal, plastic_strain: Vec::new() });
|
||||
}
|
||||
elements.sort_by_key(|e| e.label);
|
||||
let (mut shells, mut beams) = (Vec::new(), Vec::new());
|
||||
|
||||
@@ -414,6 +414,13 @@ pub(crate) fn temperatures(model: &Model, resolved: &Resolved, step: &Step, limi
|
||||
Ok((s.mesh.labels, t))
|
||||
}
|
||||
|
||||
/// The steady temperatures of a step's thermal conditions at the nodes `labels` (sorted), `None`
|
||||
/// at the nodes conduction does not reach.
|
||||
pub(crate) fn temperatures_at(model: &Model, resolved: &Resolved, step: &Step, limits: &Limits, labels: &[u32]) -> Result<Vec<Option<f64>>> {
|
||||
let (solved, t) = temperatures(model, resolved, step, limits)?;
|
||||
Ok(labels.iter().map(|l| solved.binary_search(l).ok().and_then(|i| t.get(i)).copied()).collect())
|
||||
}
|
||||
|
||||
/// Solve a heat transfer step: the step named `step`, or the model's only one.
|
||||
pub fn solve_heat(model: &Model, step: Option<&str>, limits: &Limits) -> Result<HeatResult> {
|
||||
let resolved = model.resolve()?;
|
||||
|
||||
+262
-50
@@ -25,7 +25,9 @@
|
||||
//! for the solution of nonlinear finite element equations*, CMAME 22:59–85, 1980);
|
||||
//!
|
||||
//! or when the force residual is a thousandth of its tolerance (round-off: no correction can
|
||||
//! improve it).
|
||||
//! improve it). With path-dependent materials, an increment that starts within the force
|
||||
//! tolerance (a predictor that already solved it) waives the energy criterion, whose reference
|
||||
//! would be round-off.
|
||||
//!
|
||||
//! **Automatic incrementation.** Each increment starts from the linear extrapolation of the
|
||||
//! last converged one (the free motion scaled by the ratio of their sizes), which keeps the
|
||||
@@ -50,10 +52,31 @@
|
||||
//!
|
||||
//! **Elements.** Solids (tet4, tet10, wedge6, hex8, hex20 full and reduced, hybrid hex8, hex20
|
||||
//! and tet10) and 2D plane strain and axisymmetric elements, total Lagrangian
|
||||
//! ([`fieldcraft_fem::finite`]); without nonlinear geometry the elements are the linear ones
|
||||
//! (every material is elastic today, so such a step is the linear solution reached by Newton).
|
||||
//! ([`fieldcraft_fem::finite`]); without nonlinear geometry an element of a plastic material
|
||||
//! takes the same loops with small strain, and an elastic one its linear stiffness (K u).
|
||||
//! Springs act along their fixed directions. Trusses, shells and beams (large rotations), plane
|
||||
//! stress, incompatible modes, temperature and centrifugal loads are refused by name.
|
||||
//! stress, incompatible modes (with nonlinear geometry, or with an inelastic material) and
|
||||
//! centrifugal loads are refused by name.
|
||||
//!
|
||||
//! **Thermal strain.** The step's temperatures are its temperature loads, or, when it has
|
||||
//! temperature boundary conditions or heat loads, their steady conduction solved first (one-way
|
||||
//! coupling, as the linear coupled temperature–displacement step). They ramp with the load
|
||||
//! factor from each material's reference temperature, ΔT(λ) = λ (T − T_ref). With nonlinear
|
||||
//! geometry the deformation gradient splits multiplicatively, F = F_m F_θ with
|
||||
//! F_θ = diag(1 + αᵢ ΔT), the material sees F_m and the stress is J_θ τ_m per reference volume
|
||||
//! (fem's `Temperature`); with small strain the split is additive, ε_m = ε − α ΔT; elements on
|
||||
//! their linear stiffness take the equivalent nodal forces, a dead load. A Johnson–Cook material
|
||||
//! softens at the point's temperature.
|
||||
//!
|
||||
//! **Plasticity** (`Material::plastic`, J2 with the radial return explicit dynamics uses,
|
||||
//! [`fieldcraft_fem::elastoplastic`]): Simo's exponential return map at finite strain, the
|
||||
//! additive radial return at small strain, both with their consistent tangents, so Newton keeps
|
||||
//! its quadratic rate. The plastic history is committed when an increment converges and
|
||||
//! discarded when it is cut back. A static step has no time, so the Johnson–Cook strain-rate term
|
||||
//! is refused (C = 0 is the rate-independent law), and kinematic hardening at finite strain is
|
||||
//! refused (its back stress breaks the principal-frame return); damage, erosion and adiabatic
|
||||
//! heating act in explicit dynamics only, with a warning. Results add the equivalent plastic
|
||||
//! strain at the integration points.
|
||||
//! Couplings stay the linear (small-rotation) constraints of the linear solver, with a warning.
|
||||
//!
|
||||
//! Element forces and tangents are computed in parallel and added in label order, so results are
|
||||
@@ -62,9 +85,11 @@
|
||||
use fieldcraft_fem::constitutive::MaterialModel;
|
||||
use fieldcraft_fem::dense::Matrix;
|
||||
use fieldcraft_fem::element::{Extrapolation, extrapolation};
|
||||
use fieldcraft_fem::finite::{Kinematics, Temperature};
|
||||
use fieldcraft_fem::plane::Theory;
|
||||
use fieldcraft_fem::quadrature::Rule;
|
||||
use fieldcraft_fem::{DofKind, DofSet, ElementType, PointStress, face, von_mises};
|
||||
use fieldcraft_model::{Extent, Load, Model, NonlinearStatic, Procedure, Step};
|
||||
use fieldcraft_model::{BoundaryCondition, Extent, Hardening, Load, Model, NonlinearStatic, Plastic, Procedure, SectionKind, Step};
|
||||
use fieldcraft_parallel as parallel;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -75,7 +100,7 @@ use crate::analysis::{
|
||||
use crate::cholesky::FactorError;
|
||||
use crate::constraints::Reduction;
|
||||
use crate::sparse::{Csr, add_block};
|
||||
use crate::system::{Mesh, inelastic_warnings, loads, translations};
|
||||
use crate::system::{Mesh, add_element_thermal_loads, loads, temperature_loads, translations};
|
||||
use crate::{Error, Limits, Result};
|
||||
|
||||
/// One Newton iteration of an increment.
|
||||
@@ -179,6 +204,9 @@ struct Problem<'a> {
|
||||
element_kinds: Vec<DofSet>,
|
||||
/// Each element's material model.
|
||||
materials: Vec<MaterialModel>,
|
||||
/// The step's temperature field at λ = 1, per mesh node (`None` where the field leaves a
|
||||
/// node at its material's reference temperature); `None` without one.
|
||||
field: Option<Vec<Option<f64>>>,
|
||||
/// The tangent's pattern, zero.
|
||||
pattern: Csr,
|
||||
}
|
||||
@@ -236,21 +264,45 @@ impl Problem<'_> {
|
||||
self.element_dofs.get(e).map(|dofs| dofs.iter().map(|d| u.get(*d).copied().unwrap_or(0.0)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// How element `e` is evaluated: total Lagrangian with nonlinear geometry, with small
|
||||
/// strain when its material is inelastic, and by its linear stiffness otherwise (`None`).
|
||||
fn kinematics(&self, e: usize) -> Option<Kinematics> {
|
||||
if self.finite { Some(Kinematics::Finite) } else { self.materials.get(e).filter(|m| m.is_path_dependent()).map(|_| Kinematics::Small) }
|
||||
}
|
||||
|
||||
/// The temperatures of element `e` at load factor λ, which ramps them from the reference
|
||||
/// temperature to the field's (J2 Johnson–Cook softening and thermal strain): the
|
||||
/// material's expansion and reference temperature, or no expansion from the Johnson–Cook
|
||||
/// room temperature; `None` without a field or for a material neither affects.
|
||||
fn temperature(&self, e: usize, lambda: f64) -> Option<Temperature> {
|
||||
let field = self.field.as_ref()?;
|
||||
let el = self.mesh.elements.get(e)?;
|
||||
let softens = el.plastic.as_ref().is_some_and(|j2| j2.is_temperature_dependent());
|
||||
let (alpha, reference) = match el.expansion {
|
||||
Some(x) => x,
|
||||
None if softens => ([0.0; 3], el.plastic.as_ref().map_or(0.0, |j2| j2.initial_temperature())),
|
||||
None => return None,
|
||||
};
|
||||
let nodal: Vec<f64> =
|
||||
el.nodes.iter().map(|n| field.get(*n).copied().flatten().map_or(reference, |t| reference + lambda * (t - reference))).collect();
|
||||
(softens || nodal.iter().any(|t| *t != reference)).then_some(Temperature { alpha, reference, nodal })
|
||||
}
|
||||
|
||||
/// Internal forces and the tangent of element `e` at displacements `u` (all degrees of
|
||||
/// freedom).
|
||||
fn element(&self, e: usize, u: &[f64], tangent: bool, committed: &[f64]) -> Result<(Vec<f64>, Option<Matrix>, Vec<f64>)> {
|
||||
/// freedom) and load factor λ.
|
||||
fn element(&self, e: usize, u: &[f64], lambda: f64, tangent: bool, committed: &[f64]) -> Result<(Vec<f64>, Option<Matrix>, Vec<f64>)> {
|
||||
let el = self.mesh.elements.get(e).ok_or_else(|| Error::Internal("element index".into()))?;
|
||||
let at = |err| Error::element(el.label, err);
|
||||
let ue = self.element_vector(e, u);
|
||||
let kernel = el.kernel()?;
|
||||
if !self.finite {
|
||||
let Some(kin) = self.kinematics(e) else {
|
||||
let k = kernel.stiffness(&el.d).map_err(at)?;
|
||||
let f = k.mul_vec(&ue);
|
||||
return Ok((f, tangent.then_some(k), committed.to_vec()));
|
||||
}
|
||||
};
|
||||
let material = self.materials.get(e).ok_or_else(|| Error::Internal("element material".into()))?;
|
||||
let mut updated = committed.to_vec();
|
||||
let r = kernel.finite_strain(material, &ue, committed, &mut updated, tangent).map_err(at)?;
|
||||
let r = kernel.nonlinear(material, &ue, kin, self.temperature(e, lambda).as_ref(), committed, &mut updated, tangent).map_err(at)?;
|
||||
Ok((r.force, r.tangent, updated))
|
||||
}
|
||||
|
||||
@@ -284,7 +336,7 @@ impl Problem<'_> {
|
||||
for round in parallel::blocks(self.mesh.elements.len(), ROUND) {
|
||||
let start = round.start;
|
||||
let computed = parallel::map_grained(round.len(), GRAIN, |i| {
|
||||
self.element(start + i, u, tangent, committed.get(start + i).map(Vec::as_slice).unwrap_or_default())
|
||||
self.element(start + i, u, lambda, tangent, committed.get(start + i).map(Vec::as_slice).unwrap_or_default())
|
||||
});
|
||||
for (i, c) in computed.into_iter().enumerate() {
|
||||
let (f, ke, updated) = c?;
|
||||
@@ -381,6 +433,10 @@ struct Run<'a> {
|
||||
/// Every element's material history at the end of the last converged increment
|
||||
/// (integration points × the material's history length).
|
||||
committed: Vec<Vec<f64>>,
|
||||
/// Whether a material keeps history (the tangent predictor and its convergence rules).
|
||||
path_dependent: bool,
|
||||
/// Whether the undeformed model's tangent factors, once asked.
|
||||
undeformed_stable: Option<bool>,
|
||||
}
|
||||
|
||||
/// The outcome of a converged increment.
|
||||
@@ -404,12 +460,53 @@ impl Run<'_> {
|
||||
Ok(self.problem.evaluate(&self.problem.red.expand_scaled(q, lambda), lambda, tangent, &self.committed)?)
|
||||
}
|
||||
|
||||
/// Whether the tangent of the undeformed, unloaded model is positive definite: a first
|
||||
/// tangent that is not, at a state the first increment has already strained, is then the
|
||||
/// material or the geometry (a cutback), not a model that can move freely (an error).
|
||||
fn undeformed_is_stable(&mut self) -> std::result::Result<bool, Failure> {
|
||||
if let Some(stable) = self.undeformed_stable {
|
||||
return Ok(stable);
|
||||
}
|
||||
let mut e = self.problem.evaluate(&vec![0.0; self.problem.mesh.dofs()], 0.0, true, &self.committed)?;
|
||||
let k = e.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
|
||||
let [kff] = self.problem.red.reduce([&k])?;
|
||||
let stable = factor_raw(&kff, self.limits).is_ok();
|
||||
self.undeformed_stable = Some(stable);
|
||||
Ok(stable)
|
||||
}
|
||||
|
||||
/// The tangent predictor of an increment from the converged `q0` at load factor `from` to
|
||||
/// `to`, and its work δq · r (the increment's reference for the energy criterion, since the
|
||||
/// Newton iterations after it only correct it): one linear solve with the tangent of the converged state for the change of the
|
||||
/// loads and of the prescribed displacements, K δq = T_fᵀ (f_ext(to) − f_int(u₀) − K T_p Δλ u_p).
|
||||
/// It spreads a prescribed motion into the interior before the material sees it, which a
|
||||
/// path-dependent material needs: started from the boundary's jump alone, the elements next
|
||||
/// to it would flow plastically far beyond the solution. `None` when the tangent cannot be
|
||||
/// factored (the increment then starts from `q0`).
|
||||
fn tangent_predictor(&mut self, q0: &[f64], from: f64, to: f64) -> std::result::Result<Option<(Vec<f64>, f64)>, Failure> {
|
||||
let red = self.problem.red;
|
||||
let mut e = self.problem.evaluate(&red.expand_scaled(q0, from), to, true, &self.committed)?;
|
||||
let k = e.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
|
||||
let jump = red.expand_scaled(&vec![0.0; red.neq()], to - from);
|
||||
let mut kj = vec![0.0; jump.len()];
|
||||
k.mul(&jump, &mut kj);
|
||||
let rf: Vec<f64> = free_residual(red, &e).iter().zip(red.restrict(&kj)).map(|(a, b)| a - b).collect();
|
||||
let mut dq = rf.clone();
|
||||
let [kff] = red.reduce([&k])?;
|
||||
drop(k);
|
||||
let Ok(chol) = factor_raw(&kff, self.limits) else { return Ok(None) };
|
||||
chol.solve(&mut dq)?;
|
||||
let energy = dot(&dq, &rf).abs();
|
||||
Ok(dq.iter().all(|v| v.is_finite()).then(|| (q0.iter().zip(&dq).map(|(a, d)| a + d).collect(), energy)))
|
||||
}
|
||||
|
||||
/// Newton iterations from the converged `q0` (at the previous load factor) to load factor
|
||||
/// `lambda`.
|
||||
fn increment(
|
||||
&mut self,
|
||||
q0: &[f64],
|
||||
predictor: Option<&[f64]>,
|
||||
predicted_energy: Option<f64>,
|
||||
lambda: f64,
|
||||
size: f64,
|
||||
iterations: &mut Vec<Iteration>,
|
||||
@@ -424,8 +521,14 @@ impl Run<'_> {
|
||||
Some(Err(Failure::Cutback(_))) | None => (q0.to_vec(), self.state(q0, lambda, true)?),
|
||||
};
|
||||
let mut r = self.residual(&eval);
|
||||
// With path-dependent materials, an increment that starts within the force tolerance (a
|
||||
// predictor that already solved it) has only round-off left for the energy criterion to
|
||||
// measure against: it is waived.
|
||||
let started_converged = self.path_dependent && self.force_floor.is_some() && norm_inf(&r) <= tol.force * self.scale();
|
||||
let prescribed = size * red.largest_prescribed();
|
||||
let mut first_energy: Option<f64> = None;
|
||||
// The energy criterion's reference: the first correction's work, or the tangent
|
||||
// predictor's when the increment started from one that is still in use.
|
||||
let mut first_energy: Option<f64> = predicted_energy.filter(|_| q.as_slice() != q0);
|
||||
let mut history: Vec<f64> = Vec::new();
|
||||
let max_iterations = usize::try_from(self.settings.max_iterations).unwrap_or(usize::MAX);
|
||||
for _ in 0..max_iterations {
|
||||
@@ -452,7 +555,7 @@ impl Run<'_> {
|
||||
drop(k);
|
||||
let chol = match factor_raw(&kff, self.limits) {
|
||||
Ok(c) => c,
|
||||
Err(FactorError::NotPositiveDefinite { .. }) if !first => {
|
||||
Err(FactorError::NotPositiveDefinite { .. }) if !first || self.undeformed_is_stable()? => {
|
||||
return Err(Failure::Cutback(
|
||||
"the tangent stiffness is not positive definite (a limit point, a bifurcation or an unstable material)".into(),
|
||||
));
|
||||
@@ -487,7 +590,8 @@ impl Run<'_> {
|
||||
if !it.force.is_finite() {
|
||||
return Err(Failure::Cutback("the residual is not finite".into()));
|
||||
}
|
||||
if (it.force <= tol.force && it.displacement <= tol.displacement && it.energy <= tol.energy) || it.force <= ROUND_OFF * tol.force {
|
||||
let energy_met = it.energy <= tol.energy || started_converged;
|
||||
if (it.force <= tol.force && it.displacement <= tol.displacement && energy_met) || it.force <= ROUND_OFF * tol.force {
|
||||
return Ok(Converged { q, evaluation: eval });
|
||||
}
|
||||
history.push(it.force);
|
||||
@@ -574,13 +678,10 @@ fn check_supported(model: &Model, mesh: &Mesh, step: &Step, settings: &Nonlinear
|
||||
return Err(at(format!("a hyperelastic material without nonlinear geometry (element {})", e.label)));
|
||||
}
|
||||
}
|
||||
for name in &step.loads {
|
||||
match model.load(name) {
|
||||
Some(Load::Temperature { .. }) => return Err(at(format!("temperature loads (load `{name}`): thermal strain"))),
|
||||
Some(Load::Centrifugal { .. }) => return Err(at(format!("centrifugal loads (load `{name}`), which depend on the deformed position"))),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(name) = step.loads.iter().find(|n| matches!(model.load(n), Some(Load::Centrifugal { .. }))) {
|
||||
return Err(at(format!("centrifugal loads (load `{name}`), which depend on the deformed position")));
|
||||
}
|
||||
plasticity(model, step, settings, warnings)?;
|
||||
if settings.nonlinear_geometry
|
||||
&& let Some(name) = model.couplings().keys().next()
|
||||
{
|
||||
@@ -591,6 +692,62 @@ fn check_supported(model: &Model, mesh: &Mesh, step: &Step, settings: &Nonlinear
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Refuse the plasticity a static step cannot take, and warn about what it leaves out, for the
|
||||
/// materials of the solid and 2D sections.
|
||||
fn plasticity(model: &Model, step: &Step, settings: &NonlinearStatic, warnings: &mut Vec<String>) -> Result<()> {
|
||||
let at = |what: String| Error::NotSupported(format!("{what} in nonlinear steps (step `{}`)", step.name));
|
||||
let used: std::collections::BTreeSet<&str> = model
|
||||
.sections()
|
||||
.values()
|
||||
.filter(|s| matches!(s.kind, SectionKind::Solid {}) || Theory::from_section(&s.kind).is_some())
|
||||
.map(|s| s.material.as_str())
|
||||
.collect();
|
||||
for name in used {
|
||||
let Some(m) = model.material(name) else { continue };
|
||||
match &m.plastic {
|
||||
Some(Plastic::JohnsonCook { c, specific_heat, .. }) => {
|
||||
if *c != 0.0 {
|
||||
return Err(at(format!(
|
||||
"the Johnson–Cook strain-rate term of material `{name}` (C = {c}): a static step has no time, so no strain rate; set C to 0 or use explicit dynamics"
|
||||
)));
|
||||
}
|
||||
if specific_heat.is_some() {
|
||||
warnings.push(format!(
|
||||
"material `{name}`: a static step is isothermal, so plastic work does not heat it (adiabatic heating acts in explicit dynamics)"
|
||||
));
|
||||
}
|
||||
}
|
||||
Some(Plastic::Bilinear { hardening: Hardening::Kinematic, .. }) if settings.nonlinear_geometry => {
|
||||
return Err(at(format!(
|
||||
"the kinematic hardening of material `{name}` at finite strain (its back stress is not coaxial with the elastic stretch, which the exponential return map needs); use nonlinear_geometry false or isotropic hardening"
|
||||
)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if m.plastic.is_some() && (m.damage.is_some() || m.erosion.is_some()) {
|
||||
warnings.push(format!("material `{name}`: damage and erosion act in explicit dynamics; this static step leaves them out"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The step's temperature field per mesh node: the steady heat solution of its temperature
|
||||
/// boundary conditions and heat loads when it has some (one-way coupling, as the coupled
|
||||
/// temperature–displacement step of the linear solver), or its temperature loads.
|
||||
fn temperatures(model: &Model, resolved: &fieldcraft_model::Resolved, mesh: &Mesh, step: &Step, limits: &Limits) -> Result<Option<Vec<Option<f64>>>> {
|
||||
let conducts = step.boundary_conditions.iter().any(|b| model.boundary_conditions().get(b).is_some_and(BoundaryCondition::is_thermal))
|
||||
|| step.loads.iter().any(|l| model.load(l).is_some_and(Load::is_thermal));
|
||||
let loads = temperature_loads(model, resolved, mesh, step)?;
|
||||
match (conducts, loads) {
|
||||
(true, Some(_)) => Err(Error::Invalid(format!(
|
||||
"step `{}` gives temperatures both by temperature loads and by heat conditions (temperature boundary conditions, heat loads); use one",
|
||||
step.name
|
||||
))),
|
||||
(true, None) => Ok(Some(crate::heat::temperatures_at(model, resolved, step, limits, &mesh.labels)?)),
|
||||
(false, field) => Ok(field),
|
||||
}
|
||||
}
|
||||
|
||||
/// The follower pressures of a step (with nonlinear geometry), and the step without them for
|
||||
/// the dead loads.
|
||||
fn followers(model: &Model, resolved: &fieldcraft_model::Resolved, mesh: &Mesh, step: &Step) -> Result<(Vec<Follower>, Step)> {
|
||||
@@ -645,7 +802,6 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
settings.validate()?;
|
||||
let mesh = Mesh::new(model, &resolved, limits.max_dofs)?;
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).chain(mesh.warnings.iter().cloned()).collect();
|
||||
warnings.extend(inelastic_warnings(model));
|
||||
check_supported(model, &mesh, step, settings, &mut warnings)?;
|
||||
let (graph, order) = pattern(&mesh);
|
||||
ignored_dashpots(&mesh, &mut warnings);
|
||||
@@ -657,6 +813,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
let (follower_loads, dead_step) =
|
||||
if settings.nonlinear_geometry { followers(model, &resolved, &mesh, step)? } else { (Vec::new(), step.clone()) };
|
||||
let dead = loads(model, &resolved, &mesh, &dead_step, &red.active)?;
|
||||
let field = temperatures(model, &resolved, &mesh, step, limits)?;
|
||||
let springs = if mesh.discrete.springs.iter().any(|s| !s.dashpot) {
|
||||
let mut s = graph.zero_matrix(&mesh.layout);
|
||||
mesh.discrete.add_stiffness(&mut s)?;
|
||||
@@ -673,7 +830,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
mesh.layout.element_dofs(&e.nodes, *kinds).ok_or_else(|| Error::Internal(format!("element {} degrees of freedom", e.label)))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let problem = Problem {
|
||||
let mut problem = Problem {
|
||||
mesh: &mesh,
|
||||
red: &red,
|
||||
dead,
|
||||
@@ -682,9 +839,24 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
finite: settings.nonlinear_geometry,
|
||||
element_dofs,
|
||||
element_kinds,
|
||||
materials: mesh.elements.iter().map(|e| MaterialModel::from_elasticity(&e.d, e.hyperelastic.as_ref())).collect(),
|
||||
materials: mesh
|
||||
.elements
|
||||
.iter()
|
||||
.map(|e| match &e.plastic {
|
||||
Some(j2) => MaterialModel::Plastic(j2.clone()),
|
||||
None => MaterialModel::from_elasticity(&e.d, e.hyperelastic.as_ref()),
|
||||
})
|
||||
.collect(),
|
||||
field,
|
||||
pattern: graph.zero_matrix(&mesh.layout),
|
||||
};
|
||||
// Elements on their linear stiffness take the thermal strain as the equivalent nodal
|
||||
// forces, a dead load proportional to the load factor like the temperatures.
|
||||
if let Some(field) = problem.field.take() {
|
||||
let linear: Vec<bool> = (0..mesh.elements.len()).map(|i| problem.kinematics(i).is_none()).collect();
|
||||
add_element_thermal_loads(&mut problem.dead, &mesh, &field, |i| linear.get(i).copied().unwrap_or(false))?;
|
||||
problem.field = Some(field);
|
||||
}
|
||||
let (lo, hi) = mesh.xyz.iter().fold(([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]), |(lo, hi), p| {
|
||||
(std::array::from_fn(|k| lo[k].min(p[k])), std::array::from_fn(|k| hi[k].max(p[k])))
|
||||
});
|
||||
@@ -698,8 +870,20 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
// The undeformed state first: an element that is bad before anything moves is an error in
|
||||
// the model, not a reason to cut increments back.
|
||||
problem.evaluate(&vec![0.0; mesh.dofs()], 0.0, false, &committed)?;
|
||||
let mut run =
|
||||
Run { problem: &problem, settings, limits, force_scale: 0.0, force_floor: None, length, iterations: 0, factor_entries: 0, committed };
|
||||
let path_dependent = problem.materials.iter().any(MaterialModel::is_path_dependent);
|
||||
let mut run = Run {
|
||||
problem: &problem,
|
||||
settings,
|
||||
limits,
|
||||
force_scale: 0.0,
|
||||
force_floor: None,
|
||||
length,
|
||||
iterations: 0,
|
||||
factor_entries: 0,
|
||||
committed,
|
||||
path_dependent,
|
||||
undeformed_stable: None,
|
||||
};
|
||||
|
||||
let mut history = History::default();
|
||||
let (mut frame_values, mut frames_full) = (0usize, false);
|
||||
@@ -718,32 +902,50 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
};
|
||||
keep_frame(&mut history, 0.0, &vec![0.0; mesh.dofs()], &mut warnings);
|
||||
let mut q = vec![0.0; red.neq()];
|
||||
let mut lambda = 0.0f64;
|
||||
// The step fraction t and the load factor λ(t) of the step's load path (λ = t without one).
|
||||
let (mut t, mut lambda) = (0.0f64, 0.0f64);
|
||||
let mut size = settings.initial_increment;
|
||||
let mut fast = 0usize;
|
||||
let mut last: Option<Evaluation> = None;
|
||||
// The last converged increment's free motion and the change of the load factor over it.
|
||||
let mut previous: Option<(Vec<f64>, f64)> = None;
|
||||
let max_increments = usize::try_from(settings.max_increments).unwrap_or(usize::MAX);
|
||||
while lambda < 1.0 {
|
||||
while t < 1.0 {
|
||||
if history.increments.len() >= max_increments {
|
||||
return Err(Error::Limit(format!(
|
||||
"step `{}`: {} increments reached the step's cap at load factor {lambda:.6} (raise max_increments)",
|
||||
step.name, settings.max_increments
|
||||
)));
|
||||
}
|
||||
let remaining = 1.0 - lambda;
|
||||
let (target, this) = if size >= remaining * (1.0 - 1e-9) { (1.0, remaining) } else { (lambda + size, size) };
|
||||
// An increment ends at the next point of the load path at the latest.
|
||||
let turn = settings.next_turn(t);
|
||||
let remaining = turn - t;
|
||||
let (target, this) = if size >= remaining * (1.0 - 1e-9) { (turn, remaining) } else { (t + size, size) };
|
||||
let target_lambda = settings.load_factor(target);
|
||||
let change = if settings.load_path.is_some() { target_lambda - lambda } else { this };
|
||||
let mut iterations = Vec::new();
|
||||
let number = history.increments.len() + 1;
|
||||
// Linear extrapolation of the last converged increment's free motion.
|
||||
let predictor: Option<Vec<f64>> =
|
||||
previous.as_ref().map(|(dq, prev): &(Vec<f64>, f64)| q.iter().zip(dq).map(|(a, d)| a + this / prev * d).collect());
|
||||
match run.increment(&q, predictor.as_deref(), target, this, &mut iterations) {
|
||||
// Linear extrapolation of the last converged increment's free motion, in proportion to
|
||||
// the change of the load factor (so it turns with the load path).
|
||||
let mut predictor: Option<Vec<f64>> = previous
|
||||
.as_ref()
|
||||
.filter(|(_, prev)| *prev != 0.0)
|
||||
.map(|(dq, prev): &(Vec<f64>, f64)| q.iter().zip(dq).map(|(a, d)| a + change / prev * d).collect());
|
||||
// Path-dependent materials start without one from the tangent predictor.
|
||||
let mut predicted_energy = None;
|
||||
if predictor.is_none() && path_dependent {
|
||||
match run.tangent_predictor(&q, lambda, target_lambda) {
|
||||
Ok(Some((p, energy))) => (predictor, predicted_energy) = (Some(p), Some(energy)),
|
||||
Ok(None) | Err(Failure::Cutback(_)) => {}
|
||||
Err(Failure::Fatal(e)) => return Err(e),
|
||||
}
|
||||
}
|
||||
match run.increment(&q, predictor.as_deref(), predicted_energy, target_lambda, change.abs(), &mut iterations) {
|
||||
Ok(c) => {
|
||||
previous = Some((c.q.iter().zip(&q).map(|(a, b)| a - b).collect(), this));
|
||||
previous = Some((c.q.iter().zip(&q).map(|(a, b)| a - b).collect(), change));
|
||||
let count = iterations.len();
|
||||
history.increments.push(Increment { number, load_factor: target, size: this, iterations, converged: true, cutback: None });
|
||||
lambda = target;
|
||||
history.increments.push(Increment { number, load_factor: target_lambda, size: this, iterations, converged: true, cutback: None });
|
||||
(t, lambda) = (target, target_lambda);
|
||||
q = c.q;
|
||||
keep_frame(&mut history, lambda, &red.expand_scaled(&q, lambda), &mut warnings);
|
||||
run.committed.clone_from(&c.evaluation.history);
|
||||
@@ -760,7 +962,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
Err(Failure::Cutback(reason)) => {
|
||||
history.increments.push(Increment {
|
||||
number,
|
||||
load_factor: target,
|
||||
load_factor: target_lambda,
|
||||
size: this,
|
||||
iterations,
|
||||
converged: false,
|
||||
@@ -770,7 +972,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
size = this * CUTBACK;
|
||||
if size < settings.min_increment * (1.0 - 1e-9) {
|
||||
return Err(Error::NotConverged(format!(
|
||||
"step `{}` stopped at load factor {lambda:.6}: the increment to {target:.6} failed ({reason}) and a smaller one would be below the minimum increment {}",
|
||||
"step `{}` stopped at load factor {lambda:.6} (step fraction {t:.6}): the increment to {target_lambda:.6} failed ({reason}) and a smaller one would be below the minimum increment {}",
|
||||
step.name, settings.min_increment
|
||||
)));
|
||||
}
|
||||
@@ -790,7 +992,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
"follower pressure: the skew part of its load stiffness ({a:.1e} of it, over the free degrees of freedom) is left out of the tangent; the solution is exact, Newton may converge more slowly"
|
||||
));
|
||||
}
|
||||
finish(&mesh, step, &red, &problem, u, evaluation, &run.committed, history, run.factor_entries, warnings)
|
||||
finish(&mesh, step, &red, &problem, u, lambda, evaluation, &run.committed, history, run.factor_entries, warnings)
|
||||
}
|
||||
|
||||
/// The final state as a static result: displacements, reactions, Cauchy stresses and
|
||||
@@ -802,6 +1004,7 @@ fn finish(
|
||||
red: &Reduction,
|
||||
problem: &Problem,
|
||||
u: Vec<f64>,
|
||||
lambda: f64,
|
||||
e: Evaluation,
|
||||
committed: &[Vec<f64>],
|
||||
history: History,
|
||||
@@ -826,27 +1029,36 @@ fn finish(
|
||||
.zip(point_motion.iter().zip(&point_reaction))
|
||||
.map(|(p, (d, r))| PointResult { name: p.name.clone(), label: p.label, displacement: *d, reaction: *r })
|
||||
.collect();
|
||||
let all_points = parallel::map_grained(mesh.elements.len(), GRAIN, |i| -> Result<(Vec<PointStress>, &'static ElementType, Rule)> {
|
||||
#[allow(clippy::type_complexity)]
|
||||
let all_points = parallel::map_grained(mesh.elements.len(), GRAIN, |i| -> Result<(Vec<PointStress>, Vec<f64>, &'static ElementType, Rule)> {
|
||||
let el = mesh.elements.get(i).ok_or_else(|| Error::Internal("element index".into()))?;
|
||||
let at = |err| Error::element(el.label, err);
|
||||
let ue = problem.element_vector(i, &u);
|
||||
let kernel = el.kernel()?;
|
||||
let points = if problem.finite {
|
||||
let material = problem.materials.get(i).ok_or_else(|| Error::Internal("element material".into()))?;
|
||||
kernel.finite_strain_stresses(material, &ue, committed.get(i).map(Vec::as_slice).unwrap_or_default())
|
||||
} else {
|
||||
let per_node: Vec<[f64; 3]> = el.nodes.iter().map(|n| displacement.get(*n).copied().unwrap_or_default()).collect();
|
||||
kernel.stresses_with(&el.d, &per_node, None)
|
||||
let history = committed.get(i).map(Vec::as_slice).unwrap_or_default();
|
||||
let material = problem.materials.get(i).ok_or_else(|| Error::Internal("element material".into()))?;
|
||||
let points = match problem.kinematics(i) {
|
||||
Some(kin) => kernel.nonlinear_stresses(material, &ue, kin, problem.temperature(i, lambda).as_ref(), history),
|
||||
None => {
|
||||
let per_node: Vec<[f64; 3]> = el.nodes.iter().map(|n| displacement.get(*n).copied().unwrap_or_default()).collect();
|
||||
let thermal = problem.field.as_deref().and_then(|f| el.thermal_strain(f)).map(|mut t| {
|
||||
t.delta.iter_mut().for_each(|d| *d *= lambda);
|
||||
t
|
||||
});
|
||||
kernel.stresses_with(&el.d, &per_node, thermal.as_ref())
|
||||
}
|
||||
}
|
||||
.map_err(at)?;
|
||||
Ok((points, kernel.ty(), kernel.rule()))
|
||||
let len = material.history_len();
|
||||
let plastic: Vec<f64> = if len > 0 { history.chunks(len).filter_map(|h| material.plastic_strain(h)).collect() } else { Vec::new() };
|
||||
Ok((points, plastic, kernel.ty(), kernel.rule()))
|
||||
});
|
||||
let mut extrapolations: Vec<(&'static ElementType, Rule, Extrapolation)> = Vec::new();
|
||||
let mut elements = Vec::with_capacity(mesh.elements.len());
|
||||
let mut sum = vec![[0.0; 6]; mesh.labels.len()];
|
||||
let mut count = vec![0u32; mesh.labels.len()];
|
||||
for (el, computed) in mesh.elements.iter().zip(all_points) {
|
||||
let (points, ty, rule) = computed?;
|
||||
let (points, plastic_strain, ty, rule) = computed?;
|
||||
let values: Vec<[f64; 6]> = points.iter().map(|p| p.stress).collect();
|
||||
let index = match extrapolations.iter().position(|(t, r, _)| std::ptr::eq(*t, ty) && *r == rule) {
|
||||
Some(i) => i,
|
||||
@@ -864,7 +1076,7 @@ fn finish(
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
elements.push(ElementStress { label: el.label, kind: el.kind, points, nodal });
|
||||
elements.push(ElementStress { label: el.label, kind: el.kind, points, nodal, plastic_strain });
|
||||
}
|
||||
let stress: Vec<[f64; 6]> = sum.iter().zip(&count).map(|(s, c)| s.map(|v| if *c > 0 { v / f64::from(*c) } else { 0.0 })).collect();
|
||||
let von_mises = stress.iter().map(von_mises).collect();
|
||||
|
||||
+97
-26
@@ -11,12 +11,15 @@ use fieldcraft_fem::beam::{Beam, BeamSection};
|
||||
use fieldcraft_fem::constitutive::MaterialModel;
|
||||
use fieldcraft_fem::dense::Matrix;
|
||||
use fieldcraft_fem::element::{Formulation, Solid, ThermalStrain};
|
||||
use fieldcraft_fem::finite::{Kinematics, Temperature};
|
||||
use fieldcraft_fem::plane::{Plane, Theory};
|
||||
use fieldcraft_fem::plasticity::J2;
|
||||
use fieldcraft_fem::shell::{Shell, ShellSection};
|
||||
use fieldcraft_fem::truss::{Truss, young_modulus};
|
||||
use fieldcraft_fem::{DofKind, DofLayout, DofSet, Elasticity, ElementDofs, PointStress, elasticity, face, plane};
|
||||
use fieldcraft_model::nodal::edge_pressure;
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fieldcraft_model::{Elastic, ElementKind, Hyperelastic, Integration, Load, Model, Resolved, SectionKind, Step, Target, centrifugal_acceleration};
|
||||
|
||||
@@ -41,6 +44,9 @@ pub struct Elem {
|
||||
pub expansion: Option<([f64; 3], f64)>,
|
||||
/// The large-strain option of the element's material (nonlinear steps).
|
||||
pub hyperelastic: Option<Hyperelastic>,
|
||||
/// The element's material's plasticity (nonlinear steps), shared by the elements of a
|
||||
/// section.
|
||||
pub plastic: Option<Arc<J2>>,
|
||||
}
|
||||
|
||||
/// An element's kernel: a solid or a 2D element.
|
||||
@@ -113,27 +119,39 @@ impl Kernel<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Finite-strain internal forces and tangent ([`fieldcraft_fem::finite`]) for the element's
|
||||
/// displacements `u` (its degrees of freedom in order: 3 per node, or 2 for 2D elements).
|
||||
pub fn finite_strain(
|
||||
/// Nonlinear internal forces and tangent ([`fieldcraft_fem::finite`]) for the element's
|
||||
/// displacements `u` (its degrees of freedom in order: 3 per node, or 2 for 2D elements),
|
||||
/// total Lagrangian or with small strain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn nonlinear(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[f64],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> fieldcraft_fem::Result<fieldcraft_fem::finite::Finite> {
|
||||
match self {
|
||||
Kernel::Solid(s) => s.finite_strain(m, u.as_chunks::<3>().0, committed, updated, tangent),
|
||||
Kernel::Plane(p) => p.finite_strain(m, u.as_chunks::<2>().0, committed, updated, tangent),
|
||||
Kernel::Solid(s) => s.nonlinear(m, u.as_chunks::<3>().0, kin, thermal, committed, updated, tangent),
|
||||
Kernel::Plane(p) => p.nonlinear(m, u.as_chunks::<2>().0, kin, thermal, committed, updated, tangent),
|
||||
}
|
||||
}
|
||||
|
||||
/// Cauchy stress and logarithmic strain at the integration points, as [`Self::finite_strain`].
|
||||
pub fn finite_strain_stresses(&self, m: &MaterialModel, u: &[f64], committed: &[f64]) -> fieldcraft_fem::Result<Vec<PointStress>> {
|
||||
/// Cauchy stress and strain (logarithmic at finite strain) at the integration points, as
|
||||
/// [`Self::nonlinear`].
|
||||
pub fn nonlinear_stresses(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[f64],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
) -> fieldcraft_fem::Result<Vec<PointStress>> {
|
||||
match self {
|
||||
Kernel::Solid(s) => s.finite_strain_stresses(m, u.as_chunks::<3>().0, committed),
|
||||
Kernel::Plane(p) => p.finite_strain_stresses(m, u.as_chunks::<2>().0, committed),
|
||||
Kernel::Solid(s) => s.nonlinear_stresses(m, u.as_chunks::<3>().0, kin, thermal, committed),
|
||||
Kernel::Plane(p) => p.nonlinear_stresses(m, u.as_chunks::<2>().0, kin, thermal, committed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +222,21 @@ fn thermal_strain(expansion: Option<([f64; 3], f64)>, nodes: &[usize], field: &[
|
||||
delta.iter().any(|d| *d != 0.0).then_some(ThermalStrain { alpha, delta })
|
||||
}
|
||||
|
||||
/// A solid or 2D element of a section, by model labels, before the mesh numbers its nodes.
|
||||
struct SolidEntry {
|
||||
label: u32,
|
||||
kind: ElementKind,
|
||||
nodes: Vec<u32>,
|
||||
formulation: Formulation,
|
||||
d: Elasticity,
|
||||
density: Option<f64>,
|
||||
theory: Option<Theory>,
|
||||
dofs: ElementDofs,
|
||||
expansion: Option<([f64; 3], f64)>,
|
||||
hyperelastic: Option<Hyperelastic>,
|
||||
plastic: Option<Arc<J2>>,
|
||||
}
|
||||
|
||||
/// What a shell or beam element is made of.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Section {
|
||||
@@ -468,6 +501,13 @@ impl Mesh {
|
||||
continue;
|
||||
}
|
||||
let d = elasticity(elastic).map_err(|e| Error::Invalid(format!("material `{}`: {e}", section.material)))?;
|
||||
let plastic = match &material.plastic {
|
||||
Some(p) => Some(Arc::new(
|
||||
J2::from_model(elastic, Some(p), material.damage.as_ref())
|
||||
.map_err(|e| Error::Invalid(format!("material `{}`: {e}", section.material)))?,
|
||||
)),
|
||||
None => None,
|
||||
};
|
||||
for &label in &set.elements {
|
||||
let el = model.element(label).ok_or_else(|| Error::Invalid(format!("element {label} is missing")))?;
|
||||
let (formulation, dofs) = match theory {
|
||||
@@ -476,20 +516,32 @@ impl Mesh {
|
||||
}
|
||||
.map_err(|e| Error::element(label, e))?;
|
||||
let expansion = material.thermal.and_then(|t| t.expansion.map(|a| (a.axes(), t.reference_temperature)));
|
||||
elements.push((label, el.kind, el.nodes.clone(), formulation, d, material.density, theory, dofs, expansion, material.hyperelastic));
|
||||
elements.push(SolidEntry {
|
||||
label,
|
||||
kind: el.kind,
|
||||
nodes: el.nodes.clone(),
|
||||
formulation,
|
||||
d,
|
||||
density: material.density,
|
||||
theory,
|
||||
dofs,
|
||||
expansion,
|
||||
hyperelastic: material.hyperelastic,
|
||||
plastic: plastic.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if let (Some(_), Some(name)) = (planar, model.couplings().keys().next()) {
|
||||
return Err(Error::NotSupported(format!("couplings in 2D models (coupling `{name}`)")));
|
||||
}
|
||||
elements.sort_by_key(|e| e.0);
|
||||
elements.sort_by_key(|e| e.label);
|
||||
trusses.sort_by_key(|e| e.0);
|
||||
structural.sort_by_key(|e| e.0);
|
||||
let translations = translation_kinds(planar.is_some());
|
||||
// What discrete elements need: at mesh nodes (some only they use), and at reference
|
||||
// points, which then get their six degrees of freedom like coupled ones.
|
||||
let needs: Vec<(u32, DofSet)> = crate::discrete::needs(resolved, translations).collect();
|
||||
let mut labels: Vec<u32> = elements.iter().flat_map(|e| e.2.iter().copied()).collect();
|
||||
let mut labels: Vec<u32> = elements.iter().flat_map(|e| e.nodes.iter().copied()).collect();
|
||||
labels.extend(trusses.iter().flat_map(|e| e.2.iter().copied()));
|
||||
labels.extend(structural.iter().flat_map(|e| e.2.iter().copied()));
|
||||
labels.extend(needs.iter().map(|n| n.0).filter(|l| model.node(*l).is_some()));
|
||||
@@ -507,7 +559,7 @@ impl Mesh {
|
||||
.collect::<Result<_>>()?;
|
||||
let mut layout = DofLayout::builder();
|
||||
for e in &elements {
|
||||
layout.element(&e.2, e.7);
|
||||
layout.element(&e.nodes, e.dofs);
|
||||
}
|
||||
for e in &trusses {
|
||||
layout.element(&e.2, fieldcraft_fem::ElementDofs::new(translations));
|
||||
@@ -542,10 +594,22 @@ impl Mesh {
|
||||
warnings,
|
||||
discrete: crate::discrete::Discrete::default(),
|
||||
};
|
||||
for (label, kind, nodes, formulation, d, density, theory, _, expansion, hyperelastic) in elements {
|
||||
let idx: Vec<usize> = nodes.iter().filter_map(|n| mesh.node(*n)).collect();
|
||||
for e in elements {
|
||||
let idx: Vec<usize> = e.nodes.iter().filter_map(|n| mesh.node(*n)).collect();
|
||||
let exyz: Vec<[f64; 3]> = idx.iter().filter_map(|i| mesh.xyz.get(*i).copied()).collect();
|
||||
mesh.elements.push(Elem { label, kind, nodes: idx, xyz: exyz, formulation, d, density, theory, expansion, hyperelastic });
|
||||
mesh.elements.push(Elem {
|
||||
label: e.label,
|
||||
kind: e.kind,
|
||||
nodes: idx,
|
||||
xyz: exyz,
|
||||
formulation: e.formulation,
|
||||
d: e.d,
|
||||
density: e.density,
|
||||
theory: e.theory,
|
||||
expansion: e.expansion,
|
||||
hyperelastic: e.hyperelastic,
|
||||
plastic: e.plastic,
|
||||
});
|
||||
}
|
||||
for (label, kind, nodes, young, area, density, expansion) in trusses {
|
||||
let idx: Vec<usize> = nodes.iter().filter_map(|n| mesh.node(*n)).collect();
|
||||
@@ -881,13 +945,7 @@ pub(crate) fn hold(slot: Option<&mut Option<f64>>, value: f64, conflict: impl Fn
|
||||
|
||||
/// Add the equivalent nodal forces of the thermal strains of a temperature field.
|
||||
pub fn add_thermal_loads(f: &mut [f64], mesh: &Mesh, field: &[Option<f64>]) -> Result<()> {
|
||||
for e in &mesh.elements {
|
||||
let Some(th) = e.thermal_strain(field) else { continue };
|
||||
let forces = e.kernel()?.thermal_load(&e.d, &th).map_err(|err| Error::element(e.label, err))?;
|
||||
for (node, v) in e.nodes.iter().zip(forces) {
|
||||
add(f, &translations(mesh, *node)?, v);
|
||||
}
|
||||
}
|
||||
add_element_thermal_loads(f, mesh, field, |_| true)?;
|
||||
for t in &mesh.trusses {
|
||||
let Some(th) = t.thermal_strain(field) else { continue };
|
||||
let forces = t.truss()?.thermal_load(t.young, t.area, &th).map_err(|err| Error::element(t.label, err))?;
|
||||
@@ -898,6 +956,19 @@ pub fn add_thermal_loads(f: &mut [f64], mesh: &Mesh, field: &[Option<f64>]) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add the equivalent nodal forces of the thermal strains of the solid and 2D elements whose
|
||||
/// index `keep` accepts.
|
||||
pub fn add_element_thermal_loads(f: &mut [f64], mesh: &Mesh, field: &[Option<f64>], keep: impl Fn(usize) -> bool) -> Result<()> {
|
||||
for (_, e) in mesh.elements.iter().enumerate().filter(|(i, _)| keep(*i)) {
|
||||
let Some(th) = e.thermal_strain(field) else { continue };
|
||||
let forces = e.kernel()?.thermal_load(&e.d, &th).map_err(|err| Error::element(e.label, err))?;
|
||||
for (node, v) in e.nodes.iter().zip(forces) {
|
||||
add(f, &translations(mesh, *node)?, v);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The uniform force per volume a step's gravity and body forces put on each shell and beam
|
||||
/// element ([`Mesh::structural`] order): beam end forces are recovered with it.
|
||||
pub fn structural_body_forces(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step) -> Result<Vec<[f64; 3]>> {
|
||||
@@ -936,15 +1007,15 @@ fn body(f: &mut [f64], mesh: &Mesh, e: &Elem, b: [f64; 3]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The linear solver uses the elastic constants alone: a warning for each material in use whose
|
||||
/// plasticity, damage or erosion it leaves out.
|
||||
/// The linear solvers use the elastic constants alone: a warning for each material in use whose
|
||||
/// plasticity, damage or erosion they leave out.
|
||||
pub fn inelastic_warnings(model: &Model) -> Vec<String> {
|
||||
let used: BTreeSet<&str> = model.sections().values().map(|s| s.material.as_str()).collect();
|
||||
used.into_iter()
|
||||
.filter(|name| model.material(name).is_some_and(|m| m.plastic.is_some() || m.damage.is_some() || m.erosion.is_some()))
|
||||
.map(|name| {
|
||||
format!(
|
||||
"material `{name}`: the linear solver uses its elastic constants only (its plasticity, damage and erosion act in explicit dynamics)"
|
||||
"material `{name}`: the linear solver uses its elastic constants only (its plasticity acts in nonlinear static steps and explicit dynamics, its damage and erosion in explicit dynamics)"
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -130,7 +130,7 @@ fn nonlinear_patch_test() {
|
||||
};
|
||||
let (e, nu) = (100.0, 0.3);
|
||||
let neo = fieldcraft_fem::constitutive::MaterialModel::from_model(&iso(e, nu), Some(&Hyperelastic::NeoHookean {})).unwrap();
|
||||
let tau = neo.response(&f, &[], &mut []).unwrap().stress;
|
||||
let tau = neo.response(&f, None, &[], &mut []).unwrap().stress;
|
||||
let j = fieldcraft_fem::dense::det3(&f);
|
||||
let hybrid = Formulation { hybrid: true, ..Formulation::default() };
|
||||
let mut cases: Vec<(ElementKind, Formulation, &str)> = variants().into_iter().filter(|v| v.1 != incompatible()).collect();
|
||||
@@ -387,7 +387,7 @@ fn nonlinear_steps_fail_cleanly() {
|
||||
step.set_procedure(Procedure::NonlinearStatic(bad.clone()));
|
||||
assert!(capped.model.replace_step(step).is_err(), "{bad:?}");
|
||||
}
|
||||
// Refused by name: incompatible modes, a hybrid St Venant–Kirchhoff element, temperature.
|
||||
// Refused by name: incompatible modes, a hybrid St Venant–Kirchhoff element, centrifugal loads.
|
||||
let mut im = grid([2, 1, 1], ElementKind::Hex8, |q| q);
|
||||
im.define(iso(100.0, 0.3), None, incompatible(), nonlinear());
|
||||
im.fixed("root", |q| near(q[0], 0.0));
|
||||
@@ -402,10 +402,13 @@ fn nonlinear_steps_fail_cleanly() {
|
||||
hy.load("p", Load::Pressure { set: "end".into(), magnitude: 1.0 });
|
||||
let err = solve_nonlinear(&hy.model, None, &Limits::default()).unwrap_err().to_string();
|
||||
assert!(err.contains("hybrid elements of a St Venant–Kirchhoff material"), "{err}");
|
||||
hy.load("hot", Load::Temperature { target: Target::Set("root".into()), value: 50.0 });
|
||||
hy.load("spin", Load::Centrifugal { angular_velocity: 10.0, point: [0.0; 3], axis: [0.0, 0.0, 1.0], set: None });
|
||||
neo_hookean(&mut hy, 100.0, 0.3);
|
||||
let mut m = hy.model.material("Mat").unwrap().clone();
|
||||
m.density = Some(1.0);
|
||||
hy.model.replace_material("Mat", m).unwrap();
|
||||
let err = solve_nonlinear(&hy.model, None, &Limits::default()).unwrap_err().to_string();
|
||||
assert!(err.contains("temperature loads"), "{err}");
|
||||
assert!(err.contains("centrifugal loads"), "{err}");
|
||||
// A model that can move freely: the first tangent is singular, named like the linear solver.
|
||||
let mut free = grid([2, 1, 1], ElementKind::Hex8, |q| q);
|
||||
free.define(iso(100.0, 0.3), None, full(), nonlinear());
|
||||
@@ -433,3 +436,81 @@ fn nonlinear_results_are_deterministic() {
|
||||
assert_eq!(fieldcraft_parallel::with_threads(threads, || solve(&g)), a, "{threads} threads");
|
||||
}
|
||||
}
|
||||
|
||||
/// The principal Cauchy stresses of a decoupled polynomial energy at principal stretches `l`:
|
||||
/// σᵢ = (λᵢ/J) ∂W/∂λᵢ by central differences of W itself (independent of the solver's
|
||||
/// stress and tangent), W = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ + κ/2 (J − 1)².
|
||||
fn polynomial_stress(c: [f64; 4], kappa: f64, l: [f64; 3]) -> [f64; 3] {
|
||||
let w = |l: [f64; 3]| {
|
||||
let j = l[0] * l[1] * l[2];
|
||||
let b: Vec<f64> = l.iter().map(|x| x * x * j.powf(-2.0 / 3.0)).collect();
|
||||
let i1 = b[0] + b[1] + b[2];
|
||||
let i2 = b[0] * b[1] + b[1] * b[2] + b[2] * b[0];
|
||||
let x = i1 - 3.0;
|
||||
c[0] * x + c[1] * (i2 - 3.0) + c[2] * x * x + c[3] * x * x * x + 0.5 * kappa * (j - 1.0) * (j - 1.0)
|
||||
};
|
||||
let j = l[0] * l[1] * l[2];
|
||||
std::array::from_fn(|i| {
|
||||
let h = 1e-6 * l[i];
|
||||
let (mut p, mut m) = (l, l);
|
||||
p[i] += h;
|
||||
m[i] -= h;
|
||||
l[i] / j * (w(p) - w(m)) / (2.0 * h)
|
||||
})
|
||||
}
|
||||
|
||||
/// Mooney–Rivlin and Yeoh blocks in the homogeneous modes of rubber testing (L. R. G. Treloar,
|
||||
/// *The Physics of Rubber Elasticity*, 3rd ed., Oxford 1975, ch. 10): uniaxial and equibiaxial
|
||||
/// tension and pure shear, against the closed form of the compressible decoupled energy (the
|
||||
/// free stretches found by bisection so the free faces carry no stress).
|
||||
#[test]
|
||||
fn mooney_rivlin_and_yeoh_in_the_homogeneous_modes() {
|
||||
let (e, nu) = (3.0, 0.49);
|
||||
let (mu, kappa) = (e / (2.0 * (1.0 + nu)), e / (3.0 * (1.0 - 2.0 * nu)));
|
||||
let hybrid = Formulation { hybrid: true, ..Formulation::default() };
|
||||
let (c01, c20, c30) = (0.2, -0.02, 0.004);
|
||||
for option in [Hyperelastic::MooneyRivlin { c01 }, Hyperelastic::Yeoh { c20, c30 }] {
|
||||
let c = option.constants(mu);
|
||||
for (mode, lam) in [("uniaxial", 2.0), ("equibiaxial", 1.5), ("pure shear", 2.0)] {
|
||||
// The stretches with the free one(s) l from σ_free(l) = 0.
|
||||
let stretches = |l: f64| match mode {
|
||||
"uniaxial" => [lam, l, l],
|
||||
"equibiaxial" => [lam, lam, l],
|
||||
_ => [lam, 1.0, l],
|
||||
};
|
||||
let (mut lo, mut hi) = (0.05, 1.5);
|
||||
for _ in 0..200 {
|
||||
let mid = 0.5 * (lo + hi);
|
||||
if polynomial_stress(c, kappa, stretches(mid))[2] > 0.0 { hi = mid } else { lo = mid }
|
||||
}
|
||||
let want = polynomial_stress(c, kappa, stretches(0.5 * (lo + hi)));
|
||||
let mut g = grid([1, 1, 1], ElementKind::Hex20, |x| x);
|
||||
g.define(iso(e, nu), None, hybrid, nonlinear());
|
||||
g.model.replace_material("Mat", Material { elastic: Some(iso(e, nu)), hyperelastic: Some(option), ..Material::default() }).unwrap();
|
||||
g.symmetry("x0", Symmetry::X, |x| near(x[0], 0.0));
|
||||
g.symmetry("y0", Symmetry::Y, |x| near(x[1], 0.0));
|
||||
g.symmetry("z0", Symmetry::Z, |x| near(x[2], 0.0));
|
||||
g.node_set("x1", |x| near(x[0], 1.0));
|
||||
g.bc(
|
||||
"pull",
|
||||
BoundaryCondition::Displacement { target: Target::Set("x1".into()), values: [Some(lam - 1.0), None, None, None, None, None] },
|
||||
);
|
||||
g.node_set("y1", |x| near(x[1], 1.0));
|
||||
let y = match mode {
|
||||
"equibiaxial" => Some(lam - 1.0),
|
||||
"pure shear" => Some(0.0),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(v) = y {
|
||||
g.bc("y1", BoundaryCondition::Displacement { target: Target::Set("y1".into()), values: [None, Some(v), None, None, None, None] });
|
||||
}
|
||||
let r = solve(&g);
|
||||
for s in &r.stress {
|
||||
for k in 0..3 {
|
||||
assert!((s[k] - want[k]).abs() < 1e-7 * want[0], "{} {mode}: {s:?} vs {want:?}", option.name());
|
||||
}
|
||||
}
|
||||
eprintln!("{} {mode} λ = {lam}: σ = {:.6?} (closed form {want:.6?})", option.name(), &r.stress[0][..3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//! Thermal strain in nonlinear static steps against closed forms: free expansion and a fully
|
||||
//! constrained heated block at finite strain (the multiplicative split F = F_m F_θ), a heated
|
||||
//! bar held at its ends past yield at small strain (the additive split), the linear limit
|
||||
//! against the linear solver, and a step's own heat conduction feeding its temperatures.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use fieldcraft_model::*;
|
||||
use fieldcraft_solver::{Limits, StaticResult, solve_nonlinear, solve_static};
|
||||
|
||||
fn expanding(g: &mut Grid, e: f64, nu: f64, alpha: f64, hyperelastic: Option<Hyperelastic>, plastic: Option<Plastic>) {
|
||||
let thermal = Thermal {
|
||||
expansion: Some(Directional::Isotropic(alpha)),
|
||||
reference_temperature: 20.0,
|
||||
conductivity: Some(Directional::Isotropic(45.0)),
|
||||
..Thermal::default()
|
||||
};
|
||||
let m = Material { elastic: Some(iso(e, nu)), hyperelastic, plastic, thermal: Some(thermal), ..Material::default() };
|
||||
g.model.replace_material("Mat", m).unwrap();
|
||||
}
|
||||
|
||||
fn heat_all(g: &mut Grid, value: f64) {
|
||||
g.node_set("everything", |_| true);
|
||||
g.load("heat", Load::Temperature { target: Target::Set("everything".into()), value });
|
||||
}
|
||||
|
||||
fn solve(g: &Grid) -> StaticResult {
|
||||
solve_nonlinear(&g.model, Some("Step"), &Limits::default()).unwrap()
|
||||
}
|
||||
|
||||
fn symmetric(g: &mut Grid) {
|
||||
g.symmetry("x0", Symmetry::X, |x| near(x[0], 0.0));
|
||||
g.symmetry("y0", Symmetry::Y, |x| near(x[1], 0.0));
|
||||
g.symmetry("z0", Symmetry::Z, |x| near(x[2], 0.0));
|
||||
}
|
||||
|
||||
/// A free neo-Hookean cube heated by 200 with α = 10⁻³ grows by the thermal stretch 1.2 along
|
||||
/// every edge, stress-free; held on all six faces it takes the pressure of the mechanical
|
||||
/// volume ratio ϑ⁻³: σ = κ (ϑ⁻³ − 1), full and hybrid elements.
|
||||
#[test]
|
||||
fn finite_strain_free_and_constrained_expansion() {
|
||||
let (e, nu, alpha) = (10.0, 0.3, 1e-3);
|
||||
let kappa = e / (3.0 * (1.0 - 2.0 * nu));
|
||||
let theta: f64 = 1.0 + alpha * 200.0;
|
||||
for formulation in [Formulation::default(), Formulation { hybrid: true, ..Formulation::default() }] {
|
||||
let mut g = grid([2, 2, 2], ElementKind::Hex20, |x| x);
|
||||
g.define(iso(e, nu), None, formulation, Procedure::NonlinearStatic(NonlinearStatic::default()));
|
||||
expanding(&mut g, e, nu, alpha, Some(Hyperelastic::NeoHookean {}), None);
|
||||
symmetric(&mut g);
|
||||
heat_all(&mut g, 220.0);
|
||||
let r = solve(&g);
|
||||
for (label, u) in r.nodes.iter().zip(&r.displacement) {
|
||||
let x = g.model.node(*label).unwrap().xyz;
|
||||
assert!((0..3).all(|k| (u[k] - (theta - 1.0) * x[k]).abs() < 1e-9), "{formulation:?} node {label}: {u:?}");
|
||||
}
|
||||
assert!(r.stress.iter().flatten().all(|s| s.abs() < 1e-9 * kappa), "{formulation:?}: {:?}", r.stress[0]);
|
||||
assert!((r.elements[0].points[0].strain[0] - theta.ln()).abs() < 1e-9, "the total logarithmic strain");
|
||||
|
||||
g.symmetry("x1", Symmetry::X, |x| near(x[0], 1.0));
|
||||
g.symmetry("y1", Symmetry::Y, |x| near(x[1], 1.0));
|
||||
g.symmetry("z1", Symmetry::Z, |x| near(x[2], 1.0));
|
||||
let r = solve(&g);
|
||||
let want = kappa * (theta.powi(-3) - 1.0);
|
||||
for s in &r.stress {
|
||||
assert!((0..3).all(|k| (s[k] - want).abs() < 1e-9 * kappa) && s[3].abs() < 1e-9 * kappa, "{formulation:?}: {s:?} vs {want}");
|
||||
}
|
||||
eprintln!("{formulation:?}: constrained σ = {:.9} (closed form {want:.9})", r.stress[0][0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A steel bar held axially at both ends (lateral faces free) and heated at small strain: the
|
||||
/// mechanical strain is −αΔT, so the stress is −EαΔT until it yields, then
|
||||
/// −(σ_y + E_t (αΔT − σ_y/E)) with linear hardening.
|
||||
#[test]
|
||||
fn small_strain_heated_bar_held_at_its_ends_yields() {
|
||||
let (e, nu, alpha, y0, h) = (200_000.0, 0.3, 1.2e-5, 250.0, 2000.0);
|
||||
let small = NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() };
|
||||
let plastic = Plastic::Bilinear { yield_stress: y0, hardening_modulus: h, hardening: Hardening::Isotropic };
|
||||
let et = e * h / (e + h);
|
||||
for (dt, want) in [(80.0, -e * alpha * 80.0), (300.0, -(y0 + et * (alpha * 300.0 - y0 / e)))] {
|
||||
let mut g = grid([4, 1, 1], ElementKind::Hex8, |x| [4.0 * x[0], x[1], x[2]]);
|
||||
g.define(iso(e, nu), None, Formulation::default(), Procedure::NonlinearStatic(small.clone()));
|
||||
expanding(&mut g, e, nu, alpha, None, Some(plastic.clone()));
|
||||
symmetric(&mut g);
|
||||
g.symmetry("x1", Symmetry::X, |x| near(x[0], 1.0));
|
||||
heat_all(&mut g, 20.0 + dt);
|
||||
let r = solve(&g);
|
||||
for s in &r.stress {
|
||||
assert!((s[0] - want).abs() < 1e-8 * want.abs() && s[1].abs() < 1e-8 * want.abs(), "ΔT = {dt}: {s:?} vs {want}");
|
||||
}
|
||||
eprintln!("ΔT = {dt}: σ = {:.6} (closed form {want:.6})", r.stress[0][0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Without nonlinear geometry an elastic model is the linear solution, thermal strain
|
||||
/// included; a step's own temperature boundary conditions give its temperatures by conduction,
|
||||
/// as in the linear coupled step.
|
||||
#[test]
|
||||
fn thermal_linear_limit_and_conducted_temperatures() {
|
||||
let (e, nu, alpha) = (200_000.0, 0.3, 1.2e-5);
|
||||
let build = |procedure: Procedure, plastic: Option<Plastic>, conducted: bool| {
|
||||
let mut g = grid([6, 1, 2], ElementKind::Hex20, |q| [6.0 * q[0], q[1], q[2]]);
|
||||
g.define(iso(e, nu), None, Formulation::default(), procedure);
|
||||
expanding(&mut g, e, nu, alpha, None, plastic);
|
||||
g.fixed("root", |q| near(q[0], 0.0));
|
||||
if conducted {
|
||||
g.node_set("cold", |q| near(q[0], 0.0));
|
||||
g.bc("cold_t", BoundaryCondition::Temperature { target: Target::Set("cold".into()), value: 20.0 });
|
||||
g.node_set("hot", |q| near(q[0], 1.0));
|
||||
g.bc("hot_t", BoundaryCondition::Temperature { target: Target::Set("hot".into()), value: 320.0 });
|
||||
} else {
|
||||
// Hot on top, cold below: the bar bends.
|
||||
g.node_set("top", |q| near(q[2], 1.0));
|
||||
g.load("hot", Load::Temperature { target: Target::Set("top".into()), value: 120.0 });
|
||||
}
|
||||
g
|
||||
};
|
||||
let small = Procedure::NonlinearStatic(NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() });
|
||||
let linear = solve_static(&build(Procedure::Static {}, None, false).model, None, &Limits::default()).unwrap();
|
||||
let r = solve(&build(small.clone(), None, false));
|
||||
let scale = linear.displacement.iter().flatten().fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
for (a, b) in r.displacement.iter().zip(&linear.displacement) {
|
||||
assert!((0..3).all(|k| (a[k] - b[k]).abs() < 1e-9 * scale), "{a:?} vs {b:?}");
|
||||
}
|
||||
for (a, b) in r.stress.iter().zip(&linear.stress) {
|
||||
assert!((0..6).all(|k| (a[k] - b[k]).abs() < 1e-6 * e * alpha * 100.0), "{a:?} vs {b:?}");
|
||||
}
|
||||
// Conducted: T linear in x from 20 to 320 over a bar fixed at its cold end; the step's
|
||||
// temperature conditions are solved first, as the coupled linear step does, and a plastic
|
||||
// material that stays below yield gives that step's solution.
|
||||
let plastic = Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 0.0, hardening: Hardening::Isotropic };
|
||||
let r = solve(&build(small, Some(plastic), true));
|
||||
let coupled = solve_static(&build(Procedure::CoupledTemperatureDisplacement {}, None, true).model, None, &Limits::default()).unwrap();
|
||||
let largest = coupled.von_mises.iter().fold(0.0f64, |m, v| m.max(*v));
|
||||
assert!(largest < 250.0 && largest > 1.0, "{largest}");
|
||||
let scale = coupled.displacement.iter().flatten().fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
for (a, b) in r.displacement.iter().zip(&coupled.displacement) {
|
||||
assert!((0..3).all(|k| (a[k] - b[k]).abs() < 1e-8 * scale), "{a:?} vs {b:?}");
|
||||
}
|
||||
assert!(r.elements.iter().flat_map(|e| &e.plastic_strain).all(|p| *p == 0.0));
|
||||
// Both ways of giving temperatures at once is refused.
|
||||
let mut both = build(Procedure::NonlinearStatic(NonlinearStatic::default()), None, true);
|
||||
heat_all(&mut both, 50.0);
|
||||
let err = solve_nonlinear(&both.model, None, &Limits::default()).unwrap_err().to_string();
|
||||
assert!(err.contains("both by temperature loads and by heat conditions"), "{err}");
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
//! Implicit J2 plasticity in nonlinear static steps against closed forms: uniaxial tension at
|
||||
//! finite strain (Kirchhoff stress against the logarithmic strain) and at small strain in every
|
||||
//! element kind, the Bauschinger effect of linear kinematic hardening on a load path that comes
|
||||
//! back, Newton's quadratic rate with the consistent tangent, and what plastic steps refuse or
|
||||
//! stop at. Run with `--nocapture` to see the residuals.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use fieldcraft_model::*;
|
||||
use fieldcraft_solver::{Limits, StaticResult, solve_nonlinear};
|
||||
|
||||
const E: f64 = 200_000.0;
|
||||
const NU: f64 = 0.3;
|
||||
const Y0: f64 = 250.0;
|
||||
const H: f64 = 2000.0;
|
||||
|
||||
fn bilinear(hardening: Hardening) -> Plastic {
|
||||
Plastic::Bilinear { yield_stress: Y0, hardening_modulus: H, hardening }
|
||||
}
|
||||
|
||||
fn plastic(g: &mut Grid, p: Plastic) {
|
||||
let m = Material { elastic: Some(iso(E, NU)), plastic: Some(p), ..Material::default() };
|
||||
g.model.replace_material("Mat", m).unwrap();
|
||||
}
|
||||
|
||||
/// A unit cube on three symmetry planes, its x = 1 face pulled by `stretch` (uniaxial stress).
|
||||
fn cube(kind: ElementKind, formulation: Formulation, step: NonlinearStatic, p: Plastic, stretch: f64) -> Grid {
|
||||
let mut g = grid([2, 2, 2], kind, |x| x);
|
||||
g.define(iso(E, NU), None, formulation, Procedure::NonlinearStatic(step));
|
||||
plastic(&mut g, p);
|
||||
g.symmetry("x0", Symmetry::X, |x| near(x[0], 0.0));
|
||||
g.symmetry("y0", Symmetry::Y, |x| near(x[1], 0.0));
|
||||
g.symmetry("z0", Symmetry::Z, |x| near(x[2], 0.0));
|
||||
g.node_set("x1", |x| near(x[0], 1.0));
|
||||
g.bc("pull", BoundaryCondition::Displacement { target: Target::Set("x1".into()), values: [Some(stretch), None, None, None, None, None] });
|
||||
g
|
||||
}
|
||||
|
||||
fn solve(g: &Grid) -> StaticResult {
|
||||
solve_nonlinear(&g.model, Some("Step"), &Limits::default()).unwrap()
|
||||
}
|
||||
|
||||
/// The uniaxial stress of linear hardening at strain ε past yield: σ_y + E_t (ε − σ_y/E).
|
||||
fn uniaxial(strain: f64) -> f64 {
|
||||
let et = E * H / (E + H);
|
||||
Y0 + et * (strain - Y0 / E)
|
||||
}
|
||||
|
||||
fn hybrid() -> Formulation {
|
||||
Formulation { hybrid: true, ..Formulation::default() }
|
||||
}
|
||||
|
||||
/// Every element kind (and hybrid hex8, hex20, tet10) stretched to a logarithmic strain of 0.2:
|
||||
/// the Kirchhoff stress τ = J σ follows the small-strain curve in the logarithmic strain
|
||||
/// exactly (Simo's exponential map integrates a proportional path exactly), and the
|
||||
/// equivalent plastic strain is (τ − σ_y)/H.
|
||||
#[test]
|
||||
fn finite_strain_uniaxial_tension_follows_the_logarithmic_closed_form() {
|
||||
let strain: f64 = 0.2;
|
||||
let tau = uniaxial(strain);
|
||||
let mut cases: Vec<(ElementKind, Formulation, &str)> = variants().into_iter().filter(|v| v.1 != incompatible()).collect();
|
||||
cases.extend([
|
||||
(ElementKind::Hex8, hybrid(), "hex8 hybrid"),
|
||||
(ElementKind::Hex20, hybrid(), "hex20 hybrid"),
|
||||
(ElementKind::Tet10, hybrid(), "tet10 hybrid"),
|
||||
]);
|
||||
for (kind, formulation, name) in cases {
|
||||
let g = cube(kind, formulation, NonlinearStatic::default(), bilinear(Hardening::Isotropic), strain.exp() - 1.0);
|
||||
let r = solve(&g);
|
||||
for e in &r.elements {
|
||||
for (p, ep) in e.points.iter().zip(&e.plastic_strain) {
|
||||
// J from the logarithmic strains, σ the Cauchy stress.
|
||||
let j = (p.strain[0] + p.strain[1] + p.strain[2]).exp();
|
||||
assert!((p.stress[0] * j - tau).abs() < 1e-7 * tau, "{name}: τ = {} vs {tau}", p.stress[0] * j);
|
||||
assert!(p.stress[1].abs() < 1e-7 * tau && p.stress[3].abs() < 1e-7 * tau, "{name}: {:?}", p.stress);
|
||||
assert!((ep - (tau - Y0) / H).abs() < 1e-9, "{name}: ε_p = {ep}");
|
||||
assert!((p.strain[0] - strain).abs() < 1e-12, "{name}: {:?}", p.strain);
|
||||
}
|
||||
assert_eq!(e.plastic_strain.len(), e.points.len(), "{name}");
|
||||
}
|
||||
let h = r.nonlinear.as_ref().unwrap();
|
||||
eprintln!("{name}: {} increments, {} iterations", h.increments.len(), h.iterations);
|
||||
}
|
||||
}
|
||||
|
||||
/// Without nonlinear geometry: σ = σ_y + E_t (ε − σ_y/E) in the small strain, isotropic and
|
||||
/// kinematic hardening alike under monotonic loading, hybrid elements too.
|
||||
#[test]
|
||||
fn small_strain_uniaxial_tension_follows_the_closed_form() {
|
||||
let strain = 0.01;
|
||||
let sigma = uniaxial(strain);
|
||||
let small = NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() };
|
||||
for hardening in [Hardening::Isotropic, Hardening::Kinematic] {
|
||||
for (kind, formulation, name) in [
|
||||
(ElementKind::Hex8, full(), "hex8"),
|
||||
(ElementKind::Tet10, full(), "tet10"),
|
||||
(ElementKind::Hex20, reduced(), "hex20 reduced"),
|
||||
(ElementKind::Hex20, hybrid(), "hex20 hybrid"),
|
||||
] {
|
||||
let r = solve(&cube(kind, formulation, small.clone(), bilinear(hardening), strain));
|
||||
for e in &r.elements {
|
||||
for (p, ep) in e.points.iter().zip(&e.plastic_strain) {
|
||||
assert!((p.stress[0] - sigma).abs() < 1e-8 * sigma && p.stress[1].abs() < 1e-8 * sigma, "{name} {hardening:?}: {:?}", p.stress);
|
||||
assert!((ep - (sigma - Y0) / H).abs() < 1e-10, "{name}: {ep}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Bauschinger effect: pulled to ε_a and brought back to zero strain on a load path,
|
||||
/// linear kinematic hardening yields again in reverse at σ_a − 2σ_y and ends at the residual
|
||||
/// stress σ_a − 2σ_y − E_t (ε_a − 2σ_y/E); isotropic hardening yields again only at −σ_a and
|
||||
/// ends at −σ_a − E_t (ε_a − 2σ_a/E).
|
||||
#[test]
|
||||
fn kinematic_hardening_shows_the_bauschinger_effect_on_unloading() {
|
||||
let strain = 0.01;
|
||||
let sa = uniaxial(strain);
|
||||
let et = E * H / (E + H);
|
||||
let path = NonlinearStatic {
|
||||
nonlinear_geometry: false,
|
||||
initial_increment: 0.05,
|
||||
max_increment: 0.05,
|
||||
load_path: Some(vec![[0.0, 0.0], [0.5, 1.0], [1.0, 0.0]]),
|
||||
..NonlinearStatic::default()
|
||||
};
|
||||
for (hardening, want) in
|
||||
[(Hardening::Kinematic, sa - 2.0 * Y0 - et * (strain - 2.0 * Y0 / E)), (Hardening::Isotropic, -sa - et * (strain - 2.0 * sa / E))]
|
||||
{
|
||||
let r = solve(&cube(ElementKind::Hex8, full(), path.clone(), bilinear(hardening), strain));
|
||||
let s = r.elements[0].points[0].stress[0];
|
||||
assert!((s - want).abs() < 1e-8 * sa, "{hardening:?}: residual stress {s} vs {want}");
|
||||
assert!(r.displacement.iter().all(|u| u[0].abs() < 1e-12), "back at zero strain");
|
||||
let h = r.nonlinear.unwrap();
|
||||
assert!(h.increments.iter().any(|i| i.converged && i.load_factor == 1.0), "the path's turn is an increment's end");
|
||||
assert_eq!(h.frames.last().map(|f| f.load_factor), Some(0.0));
|
||||
eprintln!("{hardening:?}: residual stress {s:.6} (closed form {want:.6})");
|
||||
}
|
||||
}
|
||||
|
||||
/// Newton with the consistent tangent converges quadratically in a plastic structure: a notched
|
||||
/// bar pulled past general yield, and a cantilever bent into plastic flow at finite strain.
|
||||
#[test]
|
||||
fn plastic_newton_converges_quadratically() {
|
||||
for nonlinear_geometry in [false, true] {
|
||||
let mut step = NonlinearStatic { initial_increment: 0.5, nonlinear_geometry, ..NonlinearStatic::default() };
|
||||
step.tolerances.force = 1e-8;
|
||||
step.tolerances.displacement = 1e-8;
|
||||
step.tolerances.energy = 1e-16;
|
||||
let mut g = grid([8, 1, 2], ElementKind::Hex20, |q| [10.0 * q[0], q[1], q[2]]);
|
||||
g.define(iso(E, NU), None, full(), Procedure::NonlinearStatic(step));
|
||||
plastic(&mut g, bilinear(Hardening::Isotropic));
|
||||
g.fixed("root", |q| near(q[0], 0.0));
|
||||
g.node_set("tip", |q| near(q[0], 1.0));
|
||||
// A tip deflection of ten times the first yield's.
|
||||
let first_yield = 2.0 * Y0 * 100.0 / (3.0 * E * 2.0);
|
||||
g.bc(
|
||||
"push",
|
||||
BoundaryCondition::Displacement { target: Target::Set("tip".into()), values: [None, None, Some(-10.0 * first_yield), None, None, None] },
|
||||
);
|
||||
let r = solve(&g);
|
||||
assert!(r.elements.iter().flat_map(|e| &e.plastic_strain).any(|p| *p > 0.0), "plastic");
|
||||
let h = r.nonlinear.unwrap();
|
||||
let mut best = 0.0f64;
|
||||
for inc in h.increments.iter().filter(|i| i.converged) {
|
||||
let res: Vec<f64> = inc.iterations.iter().map(|i| i.force).collect();
|
||||
let orders: Vec<f64> = res.windows(3).filter(|w| w[0] < 1e-2 && w[2] > 1e-11).map(|w| (w[2] / w[1]).ln() / (w[1] / w[0]).ln()).collect();
|
||||
eprintln!("geometry {nonlinear_geometry}: increment {} (λ = {}): residuals {res:?}, orders {orders:?}", inc.number, inc.load_factor);
|
||||
best = orders.iter().fold(best, |m, o| m.max(*o));
|
||||
}
|
||||
assert!(best > 1.8, "nonlinear geometry {nonlinear_geometry}: no quadratic convergence (best order {best})");
|
||||
}
|
||||
}
|
||||
|
||||
/// What a static step cannot take is refused by name; a material that softens faster than it
|
||||
/// unloads makes the step cut back and stop cleanly; the results do not depend on the threads.
|
||||
#[test]
|
||||
fn plastic_steps_refuse_and_fail_cleanly() {
|
||||
let jc = |c: f64| Plastic::JohnsonCook {
|
||||
a: 350.0,
|
||||
b: 275.0,
|
||||
n: 0.36,
|
||||
c,
|
||||
m: 1.0,
|
||||
reference_strain_rate: 1.0,
|
||||
room_temperature: 293.0,
|
||||
melt_temperature: 1356.0,
|
||||
specific_heat: Some(4.5e8),
|
||||
taylor_quinney: 0.9,
|
||||
};
|
||||
let err = solve_nonlinear(&cube(ElementKind::Hex8, full(), NonlinearStatic::default(), jc(0.02), 0.01).model, None, &Limits::default())
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("strain-rate term") && err.contains("C = 0.02"), "{err}");
|
||||
// C = 0 is the rate-independent law: it solves, warning that a static step does not heat.
|
||||
let r = solve(&cube(ElementKind::Hex8, full(), NonlinearStatic::default(), jc(0.0), 0.01));
|
||||
assert!(r.warnings.iter().any(|w| w.contains("isothermal")), "{:?}", r.warnings);
|
||||
let err = solve_nonlinear(
|
||||
&cube(ElementKind::Hex8, full(), NonlinearStatic::default(), bilinear(Hardening::Kinematic), 0.01).model,
|
||||
None,
|
||||
&Limits::default(),
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("kinematic hardening") && err.contains("finite strain"), "{err}");
|
||||
let small = NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() };
|
||||
let err = solve_nonlinear(
|
||||
&cube(ElementKind::Hex8, incompatible(), small.clone(), bilinear(Hardening::Isotropic), 0.01).model,
|
||||
None,
|
||||
&Limits::default(),
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("incompatible modes with an inelastic material"), "{err}");
|
||||
// Softening steeper than −3G past a small plastic strain: the point localises, the step cuts
|
||||
// back to its minimum increment and stops, naming the element.
|
||||
let g_mod = E / (2.0 * (1.0 + NU));
|
||||
let softening =
|
||||
Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: Y0 }, HardeningPoint { strain: 1e-4, stress: Y0 - 4.0 * g_mod * 1e-4 }] };
|
||||
let soft = NonlinearStatic { min_increment: 1e-3, ..small.clone() };
|
||||
let err = solve_nonlinear(&cube(ElementKind::Hex8, full(), soft, softening, 0.01).model, None, &Limits::default()).unwrap_err().to_string();
|
||||
assert!(err.contains("stopped at load factor") && err.contains("element"), "{err}");
|
||||
// Damage is left out with a warning.
|
||||
let mut g = cube(ElementKind::Hex8, full(), small, bilinear(Hardening::Isotropic), 0.01);
|
||||
let mut m = g.model.material("Mat").unwrap().clone();
|
||||
m.damage = Some(Damage::JohnsonCook { d1: 0.5, d2: 0.0, d3: 0.0, d4: 0.0, d5: 0.0 });
|
||||
g.model.replace_material("Mat", m).unwrap();
|
||||
let r = solve(&g);
|
||||
assert!(r.warnings.iter().any(|w| w.contains("damage and erosion")), "{:?}", r.warnings);
|
||||
// Hostile plasticity never reaches the solver: non-finite or negative tables and yield.
|
||||
let mut g = cube(ElementKind::Hex8, full(), NonlinearStatic::default(), bilinear(Hardening::Isotropic), 0.01);
|
||||
for bad in [
|
||||
Plastic::Bilinear { yield_stress: -250.0, hardening_modulus: H, hardening: Hardening::Isotropic },
|
||||
Plastic::Bilinear { yield_stress: Y0, hardening_modulus: f64::NAN, hardening: Hardening::Isotropic },
|
||||
Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: f64::INFINITY }] },
|
||||
Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: Y0 }, HardeningPoint { strain: f64::NAN, stress: Y0 }] },
|
||||
Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: Y0 }, HardeningPoint { strain: 0.1, stress: -1.0 }] },
|
||||
] {
|
||||
let m = Material { elastic: Some(iso(E, NU)), plastic: Some(bad.clone()), ..Material::default() };
|
||||
assert!(g.model.replace_material("Mat", m).is_err(), "{bad:?}");
|
||||
}
|
||||
// Bit for bit on any number of threads.
|
||||
let g = cube(ElementKind::Tet10, full(), NonlinearStatic::default(), bilinear(Hardening::Isotropic), 0.1);
|
||||
let a = fieldcraft_parallel::with_threads(1, || solve(&g));
|
||||
for threads in [2, 7] {
|
||||
assert_eq!(fieldcraft_parallel::with_threads(threads, || solve(&g)), a, "{threads} threads");
|
||||
}
|
||||
}
|
||||
@@ -549,6 +549,16 @@ fn fixture_text(f: &FixtureCheck) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The fixture column of docs/oracle.md. A value blessed in this run matches its new fixture by
|
||||
/// construction, so the page reads the same whether or not the run blessed anything; otherwise
|
||||
/// a page written by a blessing run would differ from the one CI regenerates.
|
||||
fn page_fixture_text(f: &FixtureCheck) -> &'static str {
|
||||
match f {
|
||||
FixtureCheck::Blessed => fixture_text(&FixtureCheck::Matches),
|
||||
other => fixture_text(other),
|
||||
}
|
||||
}
|
||||
|
||||
fn cell(s: &str) -> String {
|
||||
s.replace('|', "\\|").replace('\n', " ")
|
||||
}
|
||||
@@ -609,7 +619,7 @@ benchmark, {} experiment; {} pending cases wait for capabilities still being bui
|
||||
error_of(r),
|
||||
r.quantity.tolerance.describe(),
|
||||
cell(&result),
|
||||
fixture_text(&r.fixture),
|
||||
page_fixture_text(&r.fixture),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+82
-10
@@ -171,6 +171,23 @@ converges, so a cut-back increment leaves no trace. The materials:
|
||||
|---|---|
|
||||
| Elastic (isotropic or orthotropic), no option | St Venant–Kirchhoff: S = D E, E = ½(FᵀF − I). Right for large rotations with small strains; it softens without bound in large compression. |
|
||||
| Isotropic elastic with `"hyperelastic": {"type": "neo_hookean"}` | Compressible neo-Hookean with the isochoric–volumetric split, W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)², μ and κ from E and ν (C₁₀ = μ/2, D₁ = 2/κ). Its small-strain limit is the linear material, which linear steps use. |
|
||||
| Isotropic elastic with `{"type": "mooney_rivlin", "c01": …}` or `{"type": "yeoh", "c20": …, "c30": …}` | Decoupled polynomials W̄ = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ with the same volume κ/2 (J − 1)²: Mooney–Rivlin with C₁₀ = μ/2 − C₀₁ (0 ≤ C₀₁ ≤ μ/2), Yeoh with C₁₀ = μ/2, so 2(C₁₀ + C₀₁) is the elastic μ and the small-strain material stays the elastic one. Stress and tangent from the fictitious τ̄ and c̄ (Holzapfel 2000, eqs. 6.168, 6.193). Abaqus `*HYPERELASTIC, MOONEY-RIVLIN` / `YEOH`; LS-DYNA refuses them by name. |
|
||||
|
||||
| Isotropic elastic with `"plastic"` (bilinear isotropic or kinematic, table, Johnson–Cook) | J2 plasticity (`fieldcraft_fem::elastoplastic`): at finite strain Simo's multiplicative model (CMAME 99, 1992) with a Hencky elastic energy, τ = K ln J I + 2G dev ½ ln bₑ, the history Cₚ⁻¹ and the equivalent plastic strain, integrated by the exponential return map on the principal logarithmic strains; at small strain the additive radial return with the back stress. Both call `J2::radial_return`, the update of explicit dynamics, and its consistent tangent `J2::tangent`. |
|
||||
|
||||
**Plasticity's tangent.** With Cₚ⁻¹ frozen over an iteration, L_v τ = 𝔻ᵉᵖ : ½ (∂ ln b/∂b) : (d b + b d) − (d τ + τ d), 𝔻ᵉᵖ the algorithmic tangent of the return
|
||||
(K I⊗I + 2Gθ ℙ − 2Gθ̄ n⊗n, Simo & Hughes 1998 Box 3.2) and the derivative of the logarithm the
|
||||
Daleckii–Krein divided differences θ_AB = artanh(r)/r, r = (λ²_A − λ²_B)/(λ²_A + λ²_B), in the
|
||||
principal frame of the trial bₑ: smooth through equal stretches, so uniaxial and undeformed
|
||||
states need no special case. The tangent is symmetric (isotropic hardening) and checked against
|
||||
central differences of the stress and of every element's forces from a plastic history. Hybrid
|
||||
elements take the deviatoric return and the volume K/2 (ln J)². Refused by name in nonlinear
|
||||
steps: kinematic hardening at finite strain (a back stress is not coaxial with bₑ, which the
|
||||
principal-frame return needs; it works with small strain), the Johnson–Cook strain-rate term (a
|
||||
static step has no time; C = 0 is the rate-independent law), and incompatible-mode elements of a
|
||||
plastic material. Damage, erosion and adiabatic heating act in explicit dynamics only (a warning
|
||||
says so). A hardening slope below −3G leaves the point without a tangent: the increment cuts
|
||||
back and the step stops at the minimum increment, naming the element.
|
||||
|
||||
"Isotropic elastic" keeps meaning the same thing everywhere: the neo-Hookean model is chosen
|
||||
only by the explicit option, and explicit dynamics refuses a hyperelastic material by name.
|
||||
@@ -190,9 +207,11 @@ differences of its forces.
|
||||
| plane stress | refused (σ_zz = 0 needs a local iteration for the thickness) |
|
||||
| trusses, shells, beams | refused (large rotations of structural elements come later in M15) |
|
||||
|
||||
Without nonlinear geometry (`"nonlinear_geometry": false`) the elements are the linear ones:
|
||||
every material is elastic today, so such a step is the linear solution reached by Newton (the
|
||||
path the next materials, plasticity first, take with small strains).
|
||||
Without nonlinear geometry (`"nonlinear_geometry": false`) an elastic element is the linear one
|
||||
(K u), and an element of a plastic material takes the same loops with small strain
|
||||
(`Kinematics::Small`: ε = sym ∇u, the reference gradients, f = ∫ Bᵀσ dV, K = ∫ Bᵀ𝔻B dV with the
|
||||
consistent tangent, no geometric stiffness; hybrid elements project 1 + tr ε and K tr ε), so an
|
||||
elastic model is still the linear solution reached by Newton.
|
||||
|
||||
**Follower pressure** (`fieldcraft_fem::face::follower_pressure`, `follower_edge_pressure`):
|
||||
fᵢ = −p ∫ Nᵢ (x,ξ × x,η) dξ dη on the current face, with its load stiffness
|
||||
@@ -207,6 +226,23 @@ the rate of convergence of open loaded surfaces; it is revisited with the symmet
|
||||
(LDLᵀ) factorization that arc-length and Taylor–Hood need. Without nonlinear geometry a pressure
|
||||
is a dead load.
|
||||
|
||||
**Load path.** A step's `load_path`, `[[fraction, factor], …]` from `[0, 0]` to fraction 1, makes
|
||||
the load factor piecewise linear in the step fraction (without one it equals the fraction), so
|
||||
loads and prescribed displacements can come down or reverse within a step: cyclic loading.
|
||||
Increments end at every point of the path, and the extrapolated predictor scales with the change
|
||||
of the load factor, so it turns with the path. LS-DYNA writes the path as the step's curve;
|
||||
Abaqus refuses it by name for now (it would be an amplitude on every load and boundary
|
||||
condition).
|
||||
|
||||
**Predictors.** An increment starts from the linear extrapolation of the last one; with
|
||||
path-dependent materials an increment without one (the first, after a turn of the path to zero
|
||||
change) starts from the tangent predictor: one solve with the converged state's tangent for the
|
||||
change of the loads and of the prescribed displacements, so a prescribed motion is spread into the
|
||||
interior before the material sees it (started from the boundary's jump alone, the elements next
|
||||
to it would flow plastically far beyond the solution). Its work is then the energy criterion's
|
||||
reference. A first tangent that is not positive definite is a cutback when the undeformed
|
||||
model's tangent is (the strain of the increment, not a model that can move freely).
|
||||
|
||||
**Newton–Raphson** with the consistent tangent refactored every iteration, an optional line
|
||||
search (off by default: on large elastic rotations the full step converges in fewer
|
||||
iterations), and convergence when the force residual (∞-norm over the largest force seen), the
|
||||
@@ -219,19 +255,35 @@ tangent is not positive definite, or the iterations run out; the step stops with
|
||||
and the reason at the minimum increment, the increment cap, or `Limits::max_newton_iterations`.
|
||||
|
||||
**Results**: the final displacements and reactions, the **Cauchy stress** and the
|
||||
**logarithmic strain** (ln V, engineering shears) at the integration points and averaged at the
|
||||
nodes, every increment's load factor, iterations and residuals, and a displacement frame per
|
||||
**logarithmic strain** (ln V, engineering shears; the small strain without nonlinear geometry) at
|
||||
the integration points and averaged at the nodes, the **equivalent plastic strain** at the
|
||||
integration points of plastic elements (`solve.probe` with `elements`, the largest in the step
|
||||
summary), every increment's load factor, iterations and residuals, and a displacement frame per
|
||||
converged increment (`results.frame`, the slider in the results panel).
|
||||
|
||||
**Couplings** stay the linear constraints of the linear solver (small rotations of the
|
||||
reference points), with a warning; springs act along their fixed directions. Temperature and
|
||||
centrifugal loads are refused by name in nonlinear steps.
|
||||
reference points), with a warning; springs act along their fixed directions. Centrifugal loads
|
||||
are refused by name in nonlinear steps.
|
||||
|
||||
**Thermal strain.** A nonlinear step's temperatures are its temperature loads, or the steady
|
||||
conduction of its temperature boundary conditions and heat loads solved first (one-way, as the
|
||||
linear coupled temperature–displacement step); they ramp with the load factor from each
|
||||
material's reference temperature. With nonlinear geometry F = F_m F_θ, F_θ = diag(1 + αᵢ ΔT) at
|
||||
each integration point (V. A. Lubarda, Appl. Mech. Rev. 57:95–108, 2004): the material sees F_m,
|
||||
the loops run on the expanded volume J_θ dV₀ (mixed elements project det F_m), and the reported
|
||||
strain is the total logarithmic one; the linear stretch 1 + α ΔT keeps α the engineering
|
||||
coefficient, so a free body grows by exactly α ΔT. Without nonlinear geometry the split is
|
||||
additive (ε_m = ε − α ΔT), and elastic elements on their linear stiffness take the equivalent
|
||||
nodal forces as a dead load. Johnson–Cook plasticity softens at the point's temperature. The
|
||||
expansion is constant in temperature (the model has no temperature-dependent coefficient yet).
|
||||
|
||||
**Decks**: Abaqus `*STEP, NLGEOM=YES|NO, INC=` with `*STATIC` (initial, period 1, minimum,
|
||||
maximum) and `*CONTROLS, PARAMETERS=LINE SEARCH`; the neo-Hookean option as `*HYPERELASTIC, NEO
|
||||
HOOKE`. LS-DYNA: the ramped implicit run with full Newton (`ilimit` 1, `maxref`) and
|
||||
HOOKE`; plasticity as for explicit dynamics (`*PLASTIC`); a load path, and a plastic deck of
|
||||
several general steps (Abaqus carries the plastic state from one to the next, every model step
|
||||
starts undeformed), are refused. LS-DYNA: the ramped implicit run with full Newton (`ilimit` 1, `maxref`) and
|
||||
`*CONTROL_IMPLICIT_AUTO`; hyperelastic materials in nonlinear steps and steps without nonlinear
|
||||
geometry are refused. Nastran refuses nonlinear steps (SOL 400) by name. Iteration caps and
|
||||
geometry are refused; a load path is the step's curve. Nastran refuses nonlinear steps (SOL 400) by name. Iteration caps and
|
||||
tolerances are left to each solver's own controls, whose norms differ from ours.
|
||||
|
||||
**Verification** (`crates/solver/tests/nonlinear.rs`, `crates/fem/src/{constitutive,finite,face}.rs`,
|
||||
@@ -242,4 +294,24 @@ test on a distorted mesh (all solids, hybrid ones included); a 90° rigid rotati
|
||||
the elastica (Bisshopp–Drucker) at P L²/(E I) = 1, 2, 5, 10 with hex20 (0.2–0.5 %); the inflation of a thick neo-Hookean tube by a follower pressure (Rivlin's closed form);
|
||||
Newton's quadratic convergence; the linear limit; a finite-strain Cook's membrane is pending
|
||||
its transcribed reference; and the failures (an element inverted by
|
||||
crushing stops at the minimum increment, naming it; the caps; what is refused).
|
||||
crushing stops at the minimum increment, naming it; the caps; what is refused). Plasticity
|
||||
(`crates/solver/tests/plasticity.rs`, `crates/fem/src/elastoplastic.rs`, `tests/oracle/`
|
||||
`nl-j2-uniaxial`, `j2-bauschinger`, `plastic-thick-cylinder`, `plastic-hinge-cantilever`): the
|
||||
consistent tangents against central differences from a plastic history (material point and
|
||||
every element, finite and small strain, full and hybrid); uniaxial tension to a logarithmic
|
||||
strain of 0.2 in every element kind, hybrid ones included (Kirchhoff stress exact to 10⁻⁷, and
|
||||
0.13 % from explicit dynamics' hypoelastic Cauchy stress, the elastic volume change between the
|
||||
two laws); the Bauschinger effect on a load path back to zero strain (exact); Hill's thick
|
||||
cylinder, the bore displacement at a plastic radius of 1.5a (0.009 %) and the collapse pressure
|
||||
(2·10⁻⁸); a cantilever's collapse 3.1 % above beam theory's hinge, converged under refinement;
|
||||
Newton's order with the consistent tangent 1.9 (small strain) and 2.3 (finite strain) in a
|
||||
plastic cantilever; and the refusals and the softening material that stops at the minimum
|
||||
increment. Mooney–Rivlin and Yeoh (`nl-mooney-rivlin-modes`, `nl-yeoh-uniaxial`,
|
||||
`nl-mooney-rivlin-tube`): tangents by central differences (material point and hybrid
|
||||
elements), Treloar's uniaxial, equibiaxial and pure-shear modes against the compressible closed
|
||||
form (exact), and a tube inflated by a follower pressure while stretched axially to λ_z = 1.5
|
||||
(Rivlin's incompressible solution, 0.6 % at ν = 0.4995). Thermal strain (`nl-thermal-expansion`,
|
||||
`j2-heated-bar`, `crates/solver/tests/nonlinear_thermal.rs`): free expansion and a fully held
|
||||
block at a thermal stretch of 1.2 (exact, full and hybrid), a bar held at its ends heated past
|
||||
yield (exact), the linear limit against the linear and coupled solvers, and tangents with a
|
||||
temperature field.
|
||||
+31
-2
@@ -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.
|
||||
|
||||
**142 of 148 quantities pass** (6 known failure(s)) in 40 active cases: 31 theory, 9 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built.
|
||||
**163 of 169 quantities pass** (6 known failure(s)) in 49 active cases: 40 theory, 9 published benchmark, 0 experiment; 9 pending cases wait for capabilities still being built.
|
||||
|
||||
## Theory: closed-form solutions, patch tests and mesh invariants
|
||||
|
||||
@@ -31,6 +31,10 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [free-beam-frequencies](../tests/oracle/free-beam-frequencies/) | sixth frequency (the last rigid-body mode) | 0 | ≈ 0 | ≈ 0 | ± 8.1264e-5 | pass | matches |
|
||||
| [free-beam-frequencies](../tests/oracle/free-beam-frequencies/) | first free–free bending frequency (mode 7) | 0.081264 | 0.080618 | -0.795% | 1.5% | pass | matches |
|
||||
| [free-beam-frequencies](../tests/oracle/free-beam-frequencies/) | total mass | 20 | 20 | < 1e-7% | 1e-12 rel. | pass | matches |
|
||||
| [j2-bauschinger](../tests/oracle/j2-bauschinger/) | axial stress back at zero strain after a pull to ε_a = 0.01 | -247.52 | -247.52 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [j2-bauschinger](../tests/oracle/j2-bauschinger/) | accumulated equivalent plastic strain | 0.016089 | 0.016089 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [j2-heated-bar](../tests/oracle/j2-heated-bar/) | axial stress, ΔT = 300 (α ΔT = 3.6·10⁻³, 2.9 times the yield strain) | -254.65 | -254.65 | < 1e-7% | 1e-8 rel. | pass | matches |
|
||||
| [j2-heated-bar](../tests/oracle/j2-heated-bar/) | equivalent plastic strain | 0.0023267 | 0.0023267 | < 1e-7% | 1e-7 rel. | pass | matches |
|
||||
| [j2-uniaxial-explicit](../tests/oracle/j2-uniaxial-explicit/) | axial Cauchy stress at a logarithmic strain of 0.2 | 643.56 | 643.57 | 0.000174% | 1% | pass | matches |
|
||||
| [j2-uniaxial-explicit](../tests/oracle/j2-uniaxial-explicit/) | equivalent plastic strain | 0.19678 | 0.19678 | -0.00000113% | 1% | pass | matches |
|
||||
| [kirsch-plate](../tests/oracle/kirsch-plate/) | σ_xx at the top of the hole (0, 1, 0) over the remote stress | 3 | 3.0121 | 0.402% | 2% | pass | matches |
|
||||
@@ -57,6 +61,15 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip shortening δ_h/L at P L²/(E I) = 5 | 0.38763 | 0.38955 | 0.495% | 2% | pass | matches |
|
||||
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip deflection δ_v/L at P L²/(E I) = 10 | 0.81061 | 0.81421 | 0.444% | 1% | pass | matches |
|
||||
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip shortening δ_h/L at P L²/(E I) = 10 | 0.555 | 0.558 | 0.541% | 2% | pass | matches |
|
||||
| [nl-j2-uniaxial](../tests/oracle/nl-j2-uniaxial/) | axial Cauchy stress at a logarithmic strain of 0.2 | 642.74 | 642.74 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-j2-uniaxial](../tests/oracle/nl-j2-uniaxial/) | equivalent plastic strain | 0.19678 | 0.19678 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | axial Cauchy stress, uniaxial stretch λ = 2 | 2.7366 | 2.7366 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | lateral displacement at (1, 1, 1), uniaxial: l − 1 | -0.28647 | -0.28647 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | in-plane Cauchy stress, equibiaxial stretch λ = 1.5 | 2.8427 | 2.8427 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | Cauchy stress along the stretch, pure shear λ = 2 | 3.5261 | 3.5261 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | Cauchy stress across, in the held direction, pure shear λ = 2 | 1.5067 | 1.5067 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-tube](../tests/oracle/nl-mooney-rivlin-tube/) | radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15 at λ_z = 1.5 | 5 | 5.0311 | 0.622% | 1% | pass | matches |
|
||||
| [nl-mooney-rivlin-tube](../tests/oracle/nl-mooney-rivlin-tube/) | radial displacement of the outer wall (20, 0, 0): b − B, a small difference of radii | 0.61553 | 0.64339 | 2.8e-2 | ± 0.04 | pass | matches |
|
||||
| [nl-neo-hookean-uniaxial](../tests/oracle/nl-neo-hookean-uniaxial/) | axial Cauchy stress at the corner (1, 1, 1), stretch λ = 2 | 8.0601 | 8.0601 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-neo-hookean-uniaxial](../tests/oracle/nl-neo-hookean-uniaxial/) | lateral displacement u_y at the corner (1, 1, 1), l − 1 | -0.18686 | -0.18686 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | u_x at inner node 22 | 0.13583 | 0.13583 | < 1e-7% | 1e-8 rel. | pass | matches |
|
||||
@@ -72,6 +85,11 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [nl-rigid-rotation](../tests/oracle/nl-rigid-rotation/) | u_y at inner node 22: the rotation's | 0.078591 | 0.078591 | -5.6e-17 | ± 1.0000e-9 | pass | matches |
|
||||
| [nl-rigid-rotation](../tests/oracle/nl-rigid-rotation/) | u_z at inner node 22: the rotation's | -0.067441 | -0.067441 | -1.2e-16 | ± 1.0000e-9 | pass | matches |
|
||||
| [nl-rigid-rotation](../tests/oracle/nl-rigid-rotation/) | von Mises stress at inner node 22 | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-8 | pass | matches |
|
||||
| [nl-thermal-expansion](../tests/oracle/nl-thermal-expansion/) | displacement of the corner (1, 1, 1) along x, free, α ΔT = 0.2 | 0.2 | 0.2 | < 1e-7% | 1e-8 rel. | pass | matches |
|
||||
| [nl-thermal-expansion](../tests/oracle/nl-thermal-expansion/) | stress of the free body (σ_xx at the corner) | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-8 | pass | matches |
|
||||
| [nl-thermal-expansion](../tests/oracle/nl-thermal-expansion/) | stress of the held block, σ_xx (hydrostatic) | -3.5108 | -3.5108 | < 1e-7% | 1e-8 rel. | pass | matches |
|
||||
| [nl-yeoh-uniaxial](../tests/oracle/nl-yeoh-uniaxial/) | axial Cauchy stress, stretch λ = 2 | 3.1743 | 3.1743 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-yeoh-uniaxial](../tests/oracle/nl-yeoh-uniaxial/) | lateral displacement at (1, 1, 1): l − 1 | -0.28545 | -0.28545 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [patch-hex20](../tests/oracle/patch-hex20/) | u_x at the corner (1, 1, 1) | -2.5000e-4 | -2.5000e-4 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [patch-hex20](../tests/oracle/patch-hex20/) | u_y at the corner (1, 1, 1) | 0.001 | 0.001 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [patch-hex20](../tests/oracle/patch-hex20/) | u_z at the corner (1, 1, 1) | 0.00225 | 0.00225 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
@@ -121,6 +139,9 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [patch-wedge6](../tests/oracle/patch-wedge6/) | σ_yy at the inner node (0.46, 0.55, 0.53) | 2 | 2 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [patch-wedge6](../tests/oracle/patch-wedge6/) | σ_zz at the inner node (0.46, 0.55, 0.53) | 3 | 3 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [patch-wedge6](../tests/oracle/patch-wedge6/) | σ_xy at the inner node | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-9 | pass | matches |
|
||||
| [plastic-hinge-cantilever](../tests/oracle/plastic-hinge-cantilever/) | tip force at a tip deflection of 15 (4.5 times the first yield's) | -312.5 | -322.61 | -3.23% | 5% | pass | matches |
|
||||
| [plastic-thick-cylinder](../tests/oracle/plastic-thick-cylinder/) | bore displacement at the pressure that takes the plastic zone to c = 15 (a = 10, b = 20, σ_y = 250) | 0.024355 | 0.024358 | 0.0089% | 0.2% | pass | matches |
|
||||
| [plastic-thick-cylinder](../tests/oracle/plastic-thick-cylinder/) | radial force on the bore (per radian times 2π, height 1) at a bore displacement of 0.3, 24 times the first yield's: the collapse pressure 2k ln 2 = 200.094 times 2πa | 12572 | 12572 | 0.00000178% | 0.1% | pass | matches |
|
||||
| [pure-bending-coupling](../tests/oracle/pure-bending-coupling/) | bending stress σ_xx at mid-span, top fibre (10, 0.5, 1) | 15 | 15 | -0.0000103% | 0.5% | pass | matches |
|
||||
| [pure-bending-coupling](../tests/oracle/pure-bending-coupling/) | rotation of the reference point about y | 0.3 | 0.30355 | 1.18% | 1% | known failure: 1.2% high: the distributing coupling weights every node of the end face equally, corner and mid-side nodes of hex20 alike, which is not the consistent linear traction; the end face deforms locally and the fitted rotation includes it. Area-consistent weights would remove it | — |
|
||||
| [rotating-cylinder-axisymmetric](../tests/oracle/rotating-cylinder-axisymmetric/) | radial stress σ_r on the axis (r = 0) | 0.38571 | 0.38608 | 0.0958% | 1% | pass | matches |
|
||||
@@ -173,6 +194,8 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
- **[cdt-cylinder](../tests/oracle/cdt-cylinder/)** (theory): Constrained Delaunay mesh of a cylinder: volume within the chordal bound, valid elements. Source: mesh invariants: an inscribed polygon of sagitta s loses at most 2s/R of a circle's area (elementary geometry); constrained Delaunay tetrahedralization after Diazzi et al., ACM TOG 42(6) 2023. Tolerances — volume: the tessellation inscribes the cylinder within the chordal tolerance s = 0.05: the polygon section loses between 0 and 2s/R = 0.5% of the area; the reference is the middle of that band; invalid: every element of a valid mesh has positive volume
|
||||
- **[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
|
||||
- **[free-beam-frequencies](../tests/oracle/free-beam-frequencies/)** (theory): Natural frequencies of a free beam: rigid-body modes and the first bending mode. Source: Euler–Bernoulli free–free beam, β₁L = 4.7300; Timoshenko, Young and Weaver, Vibration Problems in Engineering, 4th ed., Wiley 1974, §5.3. Tolerances — rigid_6: a free body has six zero frequencies; numerical rigid-body frequencies below 10⁻³ of the first elastic one (eigenvalues below 10⁻⁶ of it) are rounding of the shifted solve; f7: Euler–Bernoulli neglects shear and rotary inertia, about 0.8% at L/h = 20 for the free–free mode; mass: the mass matrix integrates the density exactly
|
||||
- **[j2-bauschinger](../tests/oracle/j2-bauschinger/)** (theory): Bauschinger effect of linear kinematic hardening: tension, then back to zero strain. Source: J2 plasticity with Prager's linear kinematic hardening in uniaxial stress (W. Prager, Ing.-Arch. 1955; Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3): from σ_a = σ_y + E_t (ε_a − σ_y/E) the material unloads elastically by 2σ_y and then hardens with E_t, so at zero strain σ = σ_a − 2σ_y − E_t (ε_a − 2σ_y/E). Tolerances — residual_stress: a homogeneous small-strain state the element represents exactly, with increments that end at the load path's turn; isotropic hardening would give −281.835 (it yields again only at −σ_a); plastic_strain: (σ_a − σ_y)/H forward plus E/(E + H) (ε_a − 2σ_y/E) in reverse
|
||||
- **[j2-heated-bar](../tests/oracle/j2-heated-bar/)** (theory): A steel bar held at its ends and heated past yield (small strain). Source: Uniaxial J2 plasticity with linear hardening (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3) with the additive thermal strain: the bar's total axial strain is held at zero, so its mechanical strain is −α ΔT and σ = −(σ_y + E_t (α ΔT − σ_y/E)) once α ΔT exceeds σ_y/E. Tolerances — sigma: a homogeneous uniaxial state the elements represent exactly; plastic_strain: (\|σ\| − σ_y)/H, as for the stress
|
||||
- **[j2-uniaxial-explicit](../tests/oracle/j2-uniaxial-explicit/)** (theory): Uniaxial tension of a J2 bilinear-hardening cube to large strain (explicit). Source: von Mises plasticity with linear isotropic hardening under uniaxial stress (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3); logarithmic strain for a hypoelastic–plastic law under coaxial stretching. Tolerances — sigma: uniaxial J2 with linear hardening: σ = σ_y + E_t (ε − σ_y/E) in logarithmic strain; the stretch takes 10⁴ wave transits, so inertia and the ringing of the start (ρcv ≈ 0.1% of σ) stay well under 1%; plastic_strain: ε_p = ε − σ/E, as for the stress
|
||||
- **[kirsch-plate](../tests/oracle/kirsch-plate/)** (theory): Plate with a circular hole in tension (Kirsch). Source: Kirsch, Zeitschrift des VDI 42 (1898) 797–807; Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §35. Tolerances — kt: the reference is the infinite plate; the finite plate (W = 60a, Heywood's width correction about +0.1%, and the loaded ends 30a away) raises the peak by a few tenths of a percent, and the stress extrapolated to the hole surface adds up to about 1%; side: infinite-plate value −S; finite width and extrapolation as for Kt, on a smaller value
|
||||
- **[kirsch-plate-plane-stress](../tests/oracle/kirsch-plate-plane-stress/)** (theory): Plate with a circular hole in tension, 2D plane stress (Kirsch). Source: Kirsch, Zeitschrift des VDI 42 (1898) 797–807; Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §35. Tolerances — kt: the reference is the infinite plate; the finite plate (W = 60a, loaded ends 30a away) raises the peak by a few tenths of a percent, and the stress extrapolated to the hole surface adds up to about 1%; side: infinite-plate value −S; finite width and extrapolation as for Kt, on a smaller value
|
||||
@@ -189,9 +212,14 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
- **[nafems-le10](../tests/oracle/nafems-le10/)** (benchmark): NAFEMS LE10 thick plate under pressure (hex20). Source: NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE10 (target σ_yy = −5.38 MPa at D); as reproduced in the Abaqus 2017 Benchmarks Guide, 'LE10: Thick plate under pressure'. Tolerances — syy_D: the target is published to three figures; at D the stress is extrapolated to a corner where the hole edge meets the loaded face, and published converged solutions scatter by 1–2% (Abaqus Benchmarks Guide fine meshes: −5.51 to −5.89)
|
||||
- **[nl-cylinder-inflation](../tests/oracle/nl-cylinder-inflation/)** (theory): Inflation of a thick neo-Hookean cylinder by a follower pressure (plane strain). Source: Finite inflation of an incompressible neo-Hookean thick-walled tube in plane strain: r² = R² + a² − A², σ_θ − σ_r = μ (λ² − λ⁻²) with λ = r/R, integrated through the wall: p = (μ/2)[(ln λ_a² − λ_a⁻²) − (ln λ_b² − λ_b⁻²)] (R. S. Rivlin, Large elastic deformations of isotropic materials VI, Phil. Trans. R. Soc. A 242:173–195, 1949; R. W. Ogden, Non-Linear Elastic Deformations, Dover 1997, §5.3.2). Tolerances — ua: the closed form is incompressible and our material nearly so (κ/μ = 1000, a volume change of order p/κ ≈ 4·10⁻⁴); hybrid hex20, 6 × 16, follow the wall's strain gradient well inside; ub: as at the bore
|
||||
- **[nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/)** (theory): Large-deflection cantilever under a dead tip load against the elastica (solids). Source: The elastica of a cantilever under a vertical end load: K. E. Bisshopp, D. C. Drucker, Large deflection of cantilever beams, Quarterly of Applied Mathematics 3(3):272–275, 1945 (elliptic-integral solution); values computed by integrating θ″ = −(P/EI) cos θ by shooting (our own RK4, 20000 steps), agreeing with the table of K. Mattiasson, Numerical results from large deflection beam and frame problems analysed by means of elliptic integrals, IJNME 17(1):145–153, 1981, to all six digits. Tolerances — v1: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u1: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's; v2: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u2: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's; v5: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u5: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's; v10: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u10: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's
|
||||
- **[nl-j2-uniaxial](../tests/oracle/nl-j2-uniaxial/)** (theory): Uniaxial tension of a J2 bilinear-hardening cube to large strain (implicit, finite strain). Source: J2 plasticity with linear isotropic hardening in uniaxial stress (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3) in the logarithmic strain, which Simo's exponential return map (CMAME 99:61–112, 1992) integrates exactly on a proportional path: Kirchhoff stress τ = σ_y + E_t (ε − σ_y/E), Cauchy stress σ = τ/J with ln J = τ/(3K) (Hencky's volume law). Tolerances — sigma: a homogeneous deformation the element represents exactly, reached on a proportional path: the tolerance only admits the Newton tolerance and rounding; plastic_strain: ε_p = (τ − σ_y)/H, as for the stress
|
||||
- **[nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/)** (theory): Mooney–Rivlin block in uniaxial, equibiaxial and pure-shear tension. Source: Treloar's homogeneous modes of rubber testing (L. R. G. Treloar, The Physics of Rubber Elasticity, 3rd ed., Oxford 1975, ch. 10) for the decoupled compressible energy W = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + κ/2 (J − 1)²: principal Cauchy stresses σᵢ = (λᵢ/J) ∂W/∂λᵢ with the free stretch found so the free faces carry no stress (bisection; E = 3, ν = 0.49, C₀₁ = 0.2, C₁₀ = μ/2 − C₀₁). Tolerances — uniaxial: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; uniaxial_lateral: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; equibiaxial: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; shear_xx: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; shear_yy: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding
|
||||
- **[nl-mooney-rivlin-tube](../tests/oracle/nl-mooney-rivlin-tube/)** (theory): Inflation of a stretched Mooney–Rivlin tube (follower pressure). Source: Finite inflation and extension of an incompressible Mooney–Rivlin tube (R. S. Rivlin, Large elastic deformations of isotropic materials VI, Phil. Trans. R. Soc. A 242:173–195, 1949; R. W. Ogden, Non-Linear Elastic Deformations, Dover 1997, §5.3): at axial stretch λ_z, r² = a² + (R² − A²)/λ_z, σ_θ − σ_r = 2 (λ_θ² − λ_r²)(C₁₀ + C₀₁ λ_z²) with λ_θ = r/R, λ_r = 1/(λ_θ λ_z), integrated through the wall: p = ∫ (σ_θ − σ_r) dr/r (Simpson, 2·10⁵ intervals). Tolerances — ua: 16 × 6 hybrid hex20 around the quarter; ν = 0.4995 stands in for incompressibility (a volume change of order p/κ ≈ 10⁻³ shifts the radii by about 0.1 %), and the tube is stretched axially by its ends, which a nearly incompressible solid takes with larger pressures than the plane-strain neo-Hookean tube; ub: 0.2 % of the outer radius b = 20.616: the same volume change of order 10⁻³ moves b by about 0.03, which is 4.5 % of b − B; the incompressible kinematics r² = a² + (R² − A²)/λ_z amplify it there
|
||||
- **[nl-neo-hookean-uniaxial](../tests/oracle/nl-neo-hookean-uniaxial/)** (theory): Uniaxial stretch of a neo-Hookean block to twice its length. Source: Closed form of the compressible neo-Hookean model W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)² in uniaxial stress (G. A. Holzapfel, Nonlinear Solid Mechanics, Wiley 2000, §6.5, eq. 6.148 with the volumetric term of §6.4): σ_yy = μ J^(−5/3)(l² − Ī/3) + κ(J − 1) = 0 fixes the lateral stretch l, then σ_xx = μ J^(−5/3)(λ² − l²). Tolerances — sigma: a homogeneous deformation every element represents exactly: the tolerance only admits rounding and the Newton tolerance; lateral: a homogeneous deformation every element represents exactly: the tolerance only admits rounding and the Newton tolerance
|
||||
- **[nl-patch-hex8](../tests/oracle/nl-patch-hex8/)** (theory): Nonlinear patch test, hex8: a homogeneous finite deformation of a distorted mesh. Source: The patch test (B. M. Irons, A. Razzaque, in A. K. Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, 557–587) at finite strain: a boundary moved by x = F X makes F the solution everywhere (J. Bonet, R. D. Wood, Nonlinear Continuum Mechanics for Finite Element Analysis, 2nd ed., Cambridge 2008, §8); the neo-Hookean Cauchy stress σ = μ J^(−5/3) dev b + κ (J − 1) I of that F. Tolerances — ux: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; uy: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; uz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; sxx: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; syy: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; szz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; sxy: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; sxz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; syz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance
|
||||
- **[nl-rigid-rotation](../tests/oracle/nl-rigid-rotation/)** (theory): A quarter turn strains and stresses nothing (objectivity). Source: Objectivity of the total Lagrangian formulation and of the hyperelastic stress: a rigid motion x = R X + c gives C = I and σ = 0 whatever the rotation (Bonet & Wood 2008, §3.4 and §5.2; T. Belytschko, W. K. Liu, B. Moran, Nonlinear Finite Elements for Continua and Structures, 2nd ed., Wiley 2014, §5.5). Tolerances — ux: the inner nodes follow the rigid rotation exactly; uy: the inner nodes follow the rigid rotation exactly; uz: the inner nodes follow the rigid rotation exactly; von_mises: zero up to rounding of stresses of order E = 1000: a 90° turn leaves no strain
|
||||
- **[nl-thermal-expansion](../tests/oracle/nl-thermal-expansion/)** (theory): Free and fully constrained thermal expansion at finite strain. Source: The multiplicative split F = F_m F_θ with F_θ = (1 + α ΔT) I (V. A. Lubarda, Appl. Mech. Rev. 57(2):95–108, 2004): a free body follows F = F_θ stress-free; held on every face, F = I and F_m = ϑ⁻¹ I, so the compressible neo-Hookean material carries the pressure of its volume ratio, σ = κ (ϑ⁻³ − 1). Tolerances — free_u: a homogeneous stretch every element represents exactly; free_stress: rounding only; held_stress: a homogeneous state; E = 10, ν = 0.3 (κ = 8.3333), ϑ = 1.2
|
||||
- **[nl-yeoh-uniaxial](../tests/oracle/nl-yeoh-uniaxial/)** (theory): Yeoh block in uniaxial tension. Source: Uniaxial tension of the decoupled compressible Yeoh energy W = C₁₀ (Ī₁ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ + κ/2 (J − 1)² (O. H. Yeoh, Rubber Chem. Technol. 66(5):754–771, 1993): σ = (λ/J) ∂W/∂λ with the lateral stretch found so σ_yy = 0 (bisection; E = 3, ν = 0.49, C₁₀ = μ/2, C₂₀ = −0.02, C₃₀ = 0.004). Tolerances — sigma: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; lateral: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding
|
||||
- **[patch-hex20](../tests/oracle/patch-hex20/)** (theory): Constant-stress patch test, hex20. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
- **[patch-hex20-reduced](../tests/oracle/patch-hex20-reduced/)** (theory): Constant-stress patch test, hex20-reduced. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
- **[patch-hex8](../tests/oracle/patch-hex8/)** (theory): Constant-stress patch test, hex8. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
@@ -199,6 +227,8 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
- **[patch-tet10](../tests/oracle/patch-tet10/)** (theory): Constant-stress patch test, tet10. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
- **[patch-tet4](../tests/oracle/patch-tet4/)** (theory): Constant-stress patch test, tet4. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
- **[patch-wedge6](../tests/oracle/patch-wedge6/)** (theory): Constant-stress patch test, wedge6. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
- **[plastic-hinge-cantilever](../tests/oracle/plastic-hinge-cantilever/)** (theory): Plastic collapse of a cantilever: the fully plastic moment at the root. Source: Limit analysis (B. G. Neal, The Plastic Methods of Structural Analysis, 3rd ed., Chapman & Hall 1977, ch. 1–2): beam theory's plastic hinge at the root, the fully plastic moment M_p = σ_y b h²/4 of a rectangular section, collapses a cantilever of length L at P_c = M_p/L = 312.5 (b = h = 10, L = 200, σ_y = 250); a bound for the solid, whose collapse lies above it (W. Prager, P. G. Hodge, Theory of Perfectly Plastic Solids, Wiley 1951: a continuum mechanism allows no normal velocity jump, which the beam's hinge rotation is). Tolerances — collapse_force: the solid's collapse lies above beam theory's hinge: refined through the depth (4 to 8 elements) and along the length (40 to 160) the force settles 3.1 % above M_p/L (322.3), so the band is that bound's gap; the root is a symmetry plane (no rotation, free lateral contraction) held on average through a coupling, the tip pushed through another; reduced hex20, 20 × 2 × 4
|
||||
- **[plastic-thick-cylinder](../tests/oracle/plastic-thick-cylinder/)** (theory): Elastic–perfectly plastic thick cylinder: plastic zone and collapse pressure. Source: Hill, The Mathematical Theory of Plasticity, Oxford 1950, ch. V §2: a von Mises cylinder in plane strain, elastically incompressible, with the plastic zone a ≤ r ≤ c under the pressure p = k (2 ln(c/a) + 1 − c²/b²), k = σ_y/√3, and the bore displacement u_a = k c²/(2 G a); collapse when c = b at p = 2k ln(b/a). Tolerances — u_bore: 20 reduced quad8 elements through the wall resolve the logarithmic plastic field and ν = 0.4999 stands in for Hill's incompressible elasticity (0.009 % apart); u_a ∝ c², so 0.2 % on u_a is 0.1 % on the plastic radius; collapse_force: once the wall is plastic through, perfect plasticity holds the force at the collapse value (σ_θ − σ_r = 2k everywhere, σ_z the mean of the others as plastic flow dominates); the band admits the quadrature of the logarithmic field
|
||||
- **[pure-bending-coupling](../tests/oracle/pure-bending-coupling/)** (theory): Pure bending through a distributing coupling. Source: Euler–Bernoulli pure bending, σ = M_y z/I, θ = ML/(EI) (exact in elasticity for ν = 0); Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §102. Tolerances — sigma_top: with ν = 0 pure bending is a quadratic displacement field that hex20 represents exactly; the coupling's nodal force pattern differs from the linear traction only within a depth or two of the end (Saint-Venant), five depths from the probe; rotation: the reference point follows the best-fit rigid motion of the end section, which the end zone's local deformation disturbs by well under 1%
|
||||
- **[rotating-cylinder-axisymmetric](../tests/oracle/rotating-cylinder-axisymmetric/)** (theory): Long rotating solid cylinder, axisymmetric. Source: Timoshenko and Goodier, Theory of Elasticity, 3rd ed., McGraw-Hill 1970, §32 (long rotating cylinder, ε_z = 0). Tolerances — radial_axis: stress extrapolated to the axis, where σ_r = σ_θ: under 1% with sixteen quadratic elements across; hoop_rim: stress extrapolated to the surface: under 1%
|
||||
- **[rotating-disc-plane-stress](../tests/oracle/rotating-disc-plane-stress/)** (theory): Rotating disc with a central hole, plane stress. Source: Timoshenko and Goodier, Theory of Elasticity, 3rd ed., McGraw-Hill 1970, §32 (rotating disc of uniform thickness with a hole). Tolerances — hoop_bore: stress extrapolated to the bore surface, where the a²b²/r² term is steepest: 16 radial quadratic elements graded towards the bore leave about 0.2% (8 uniform: −2.4%; 32 graded: −0.05%); radial_max: the nearest node to √(ab) is within half an element of it, where σ_r is stationary (an error of order (h/r)², under 0.5%), and nodal averaging adds a few tenths of a percent; hoop_rim: as at the bore
|
||||
@@ -218,6 +248,5 @@ Reference values already cited; each case gets its recipe when the capability it
|
||||
| [nl-cook-membrane](../tests/oracle/nl-cook-membrane/) | benchmark | Cook's membrane, nearly incompressible neo-Hookean, large load (plane strain) | the published finite-strain reference value, transcribed from the paper with its load and material constants | vertical displacement of the top corner C (48, 60) = to be transcribed | R. D. Cook, Improved two-dimensional finite element, J. Struct. Div. ASCE 100(ST9):1851–1863, 1974 (the tapered panel (0, 0), (48, 44), (48, 60), (0, 44), clamped on x = 0, shear on x = 48); finite-strain versions with a neo-Hookean material are published by J. C. Simo, F. Armero, IJNME 33:1413–1449, 1992; the values to compare with are to be transcribed from the paper |
|
||||
| [pinched-cylinder](../tests/oracle/pinched-cylinder/) | benchmark | Pinched cylinder with rigid diaphragms | shell elements (M8) | radial displacement under the load = -1.8248e-5 | Belytschko, Stolarski, Liu, Carpenter and Ong, 'Stress projection for membrane and shear locking in shell finite elements', Computer Methods in Applied Mechanics and Engineering 51 (1985) 221–258 (the shell obstacle course); reference 1.8248 × 10⁻⁵ under the load |
|
||||
| [pinched-hemisphere](../tests/oracle/pinched-hemisphere/) | benchmark | Hemispherical shell with an 18° hole under alternating point loads | shell elements (M8), or mapped blocks that follow doubly curved faces | displacement under the load along the load = 0.094 | MacNeal and Harder, 'A proposed standard set of problems to test finite element accuracy', Finite Elements in Analysis and Design 1 (1985) 3–20, Fig. 9 (p. 9), Table 5(b) (p. 11): 0.0940 (lower bound 0.0924 from their [7]) |
|
||||
| [plastic-thick-cylinder](../tests/oracle/plastic-thick-cylinder/) | theory | Elastic–perfectly plastic thick cylinder: collapse pressure | implicit (static) J2 plasticity: explicit J2 is in, a collapse load needs the static solver | collapse pressure for a = 10, b = 20, σ_y = 250 = 200.09 | Hill, The Mathematical Theory of Plasticity, Oxford 1950, ch. V §2: plane strain von Mises collapse pressure p = (2/√3)σ_y ln(b/a) |
|
||||
| [scordelis-lo-roof-shell](../tests/oracle/scordelis-lo-roof-shell/) | benchmark | Scordelis–Lo roof with shell elements | shell elements (MITC4, M8) | vertical displacement at the midpoint of the free edge = -0.3024 | MacNeal and Harder, 'A proposed standard set of problems to test finite element accuracy', Finite Elements in Analysis and Design 1 (1985) 3–20, Fig. 8 (p. 8), Table 5(a) (p. 11); Belytschko, Stolarski, Liu, Carpenter and Ong, 'Stress projection for membrane and shear locking in shell finite elements', Computer Methods in Applied Mechanics and Engineering 51 (1985) 221–258 (the shell obstacle course) |
|
||||
| [taylor-bar](../tests/oracle/taylor-bar/) | experiment | Taylor bar impact (copper) | the measured final shapes, to be transcribed from the paper (explicit Johnson–Cook plasticity is in) | final over initial length = to be transcribed | Wilkins and Guinan, 'Impact of cylinders on a rigid boundary', Journal of Applied Physics 44 (1973) 1200–1206 (final length and mushroom diameter vs impact speed) |
|
||||
+3
-3
@@ -44,7 +44,7 @@ products are run, no documentation text is copied). Status: **done**, **started*
|
||||
| Mesh | Mesh editing (move, merge, split, collapse, remesh region), imported orphan meshes | M6 | yes | started (node/element add/remove) |
|
||||
| Model | Sets and surfaces bound to geometry names | M3 | yes | done |
|
||||
| Model | Materials: elastic (isotropic, orthotropic), density, unit-system check | M3 | | done |
|
||||
| Model | Materials: plasticity, hyperelasticity, damage, thermal | M8 | | started (von Mises plasticity: bilinear, table, Johnson–Cook; Johnson–Cook damage, erosion; written to Abaqus `*PLASTIC`, Nastran `MATS1`, LS-DYNA plastic materials where expressible; compressible neo-Hookean for nonlinear steps, Abaqus `*HYPERELASTIC, NEO HOOKE`) |
|
||||
| Model | Materials: plasticity, hyperelasticity, damage, thermal | M8 | | started (von Mises plasticity: bilinear, table, Johnson–Cook; Johnson–Cook damage, erosion; plasticity in explicit and nonlinear static steps; written to Abaqus `*PLASTIC`, Nastran `MATS1`, LS-DYNA plastic materials where expressible; compressible neo-Hookean, Mooney–Rivlin and Yeoh for nonlinear steps, Abaqus `*HYPERELASTIC, NEO HOOKE`, `MOONEY-RIVLIN`, `YEOH`; thermal strain in nonlinear steps) |
|
||||
| Model | Sections: solid, shell, beam profiles; assignment by set | M3 | | done |
|
||||
| Model | 2D sections: plane stress, plane strain, axisymmetric (XY plane, Y the axis); element edges in sets; centrifugal load | M4 | | done |
|
||||
| Model | Composite layups, material orientations, continuum shells | M8 | yes | planned |
|
||||
@@ -53,7 +53,7 @@ products are run, no documentation text is copied). Status: **done**, **started*
|
||||
| Model | Loads: force and moment, pressure, gravity, body force | M3 | | done |
|
||||
| Model | Reference points, kinematic and distributing coupling | M3 | | done |
|
||||
| Model | Steps: linear static, frequency; output presets | M3 | | done |
|
||||
| Model | Steps: nonlinear static, explicit dynamics, heat transfer; amplitudes | M7–M8, M15 | | started (nonlinear static with nonlinear geometry, increments and tolerances, written to Abaqus `*STEP, NLGEOM` and LS-DYNA implicit, refused by Nastran; explicit dynamics; heat transfer; amplitudes planned) |
|
||||
| Model | Steps: nonlinear static, explicit dynamics, heat transfer; amplitudes | M7–M8, M15 | | started (nonlinear static with or without nonlinear geometry, increments, tolerances and a load path (cyclic loading), written to Abaqus `*STEP, NLGEOM` and LS-DYNA implicit (the load path as LS-DYNA's curve; refused by Abaqus for now), refused by Nastran; explicit dynamics; heat transfer; amplitudes per load planned) |
|
||||
| Interaction | Tie between non-matching meshes | M7 | | planned |
|
||||
| Interaction | Contact (general and pairs), friction, automatic pair detection | M7 | | planned |
|
||||
| Interaction | Springs and dashpots (ground or two-point; DOF, vector or line), point masses with rotary inertia | M4 | | done (implicit; explicit translational; all three decks with what each cannot take reported) |
|
||||
@@ -63,7 +63,7 @@ products are run, no documentation text is copied). Status: **done**, **started*
|
||||
| Decks | LS-DYNA keyword writer (implicit; explicit in M8) | M3 | | done (implicit static and eigenvalue; beam sections planned) |
|
||||
| Decks | Readers keeping unknown keywords | M9 | | planned |
|
||||
| Solve | Own linear static and modal solver, verified against analytic solutions | M4 | yes | started (solid elements: tet4, tet10, wedge6, hex8, hex20; 2D plane stress, plane strain and axisymmetric tri3, tri6, quad4 (full, incompatible modes), quad8 (full, reduced); quad4/tri3 MITC shells and Timoshenko beams with profiles, mixed with solids; reference points, kinematic and distributing couplings, moments; centrifugal loads) |
|
||||
| Solve | Own nonlinear implicit solver: Newton–Raphson with line search and automatic increments, total Lagrangian solids and 2D elements, mixed u–p at finite strain, follower pressure | M15 | yes | started (solids, plane strain, axisymmetric; St Venant–Kirchhoff and neo-Hookean; verified by the elastica, tube inflation, patch and objectivity tests; plasticity, shells, contact, arc-length next) |
|
||||
| Solve | Own nonlinear implicit solver: Newton–Raphson with line search and automatic increments, total Lagrangian solids and 2D elements, mixed u–p at finite strain, follower pressure | M15 | yes | started (solids, plane strain, axisymmetric; St Venant–Kirchhoff, neo-Hookean and J2 plasticity (Simo's exponential return map at finite strain, radial return at small strain, consistent tangents, the return shared with explicit dynamics); verified by the elastica, tube inflation, patch and objectivity tests, uniaxial tension to large strain, Hill's thick cylinder, a cantilever's collapse and the Bauschinger effect; Mooney–Rivlin and Yeoh; thermal strain at finite and small strain; shells, contact, arc-length next) |
|
||||
| Solve | Own explicit dynamics solver (central differences, one-point hex8 with hourglass control, nodal-pressure tet4, bulk viscosity, energy balance), verified against analytic wave and impact solutions | M10 | yes | started (X1 core and X2: large deformation, objective rates, J2 and Johnson–Cook plasticity, damage, erosion; contact planned) |
|
||||
| Results | Deformed shape, contours, mode shapes, probes, reaction sums | M4 | | started (results in the session, `solve.probe`, reaction sums; display planned) |
|
||||
| Results | XY and path plots, cuts, display groups, free-body forces, animation | M9 | | planned |
|
||||
|
||||
@@ -1186,3 +1186,45 @@ update are written from (cited again in the module docs):
|
||||
transcribed finite-strain values), R. D. Cook, J. Struct. Div. ASCE 100(ST9):1851–1863, 1974,
|
||||
and J. C. Simo, F. Armero, IJNME 33:1413–1449, 1992; tube inflation, R. S. Rivlin, Phil. Trans. R. Soc. A
|
||||
242:173–195, 1949, and R. W. Ogden, *Non-Linear Elastic Deformations*, Dover 1997.
|
||||
|
||||
## Part N: Implicit plasticity (finite and small strain), as implemented
|
||||
|
||||
- Multiplicative J2 plasticity with a Hencky elastic energy and the exponential return map in
|
||||
the principal logarithmic strains, which keeps the classical radial return of the
|
||||
infinitesimal theory: J. C. Simo, *Algorithms for static and dynamic multiplicative plasticity
|
||||
that preserve the classical return mapping schemes of the infinitesimal theory*, CMAME
|
||||
99(1):61–112, 1992, DOI [10.1016/0045-7825(92)90123-2](https://doi.org/10.1016/0045-7825(92)90123-2);
|
||||
J. C. Simo, T. J. R. Hughes, *Computational Inelasticity*, Springer 1998, ch. 9.
|
||||
- The radial return (shared with explicit dynamics) and its consistent tangent
|
||||
K I⊗I + 2Gθ ℙ − 2Gθ̄ n⊗n: Simo & Hughes 1998, §3.3, Box 3.2 (A4.1); J. C. Simo, R. L. Taylor,
|
||||
*Consistent tangent operators for rate-independent elastoplasticity*, CMAME 48(1):101–118,
|
||||
1985, DOI [10.1016/0045-7825(85)90070-2](https://doi.org/10.1016/0045-7825(85)90070-2).
|
||||
- The derivative of the matrix logarithm by divided differences (Daleckii–Krein), smooth through
|
||||
equal stretches: R. Bhatia, *Matrix Analysis*, Springer 1997, §V.3.
|
||||
- Kinematic hardening at finite strain (refused for now; the route when it comes): additive
|
||||
plasticity in the Lagrangian logarithmic strain space, C. Miehe, N. Apel, M. Lambrecht, CMAME
|
||||
191(47–48):5383–5425, 2002, DOI [10.1016/S0045-7825(02)00438-3](https://doi.org/10.1016/S0045-7825(02)00438-3).
|
||||
- Prager's linear kinematic hardening: W. Prager, *The theory of plasticity: a survey of recent
|
||||
achievements*, Proc. Inst. Mech. Eng. 169:41–57, 1955.
|
||||
- Verification references: the thick cylinder's plastic zone and collapse pressure, R. Hill,
|
||||
*The Mathematical Theory of Plasticity*, Oxford 1950, ch. V; the cantilever's plastic hinge,
|
||||
B. G. Neal, *The Plastic Methods of Structural Analysis*, 3rd ed., Chapman & Hall 1977, and the
|
||||
continuum's velocity fields, W. Prager, P. G. Hodge, *Theory of Perfectly Plastic Solids*,
|
||||
Wiley 1951.
|
||||
|
||||
## Part O: Polynomial hyperelasticity and thermal strain in nonlinear steps, as implemented
|
||||
|
||||
- Decoupled invariant-based hyperelasticity, the fictitious stress and elasticity and their
|
||||
projection: G. A. Holzapfel, *Nonlinear Solid Mechanics*, Wiley 2000, §6.4–6.6 (eqs. 6.168,
|
||||
6.193).
|
||||
- Mooney–Rivlin: M. Mooney, *A theory of large elastic deformation*, J. Appl. Phys.
|
||||
11(9):582–592, 1940; R. S. Rivlin, *Large elastic deformations of isotropic materials IV*,
|
||||
Phil. Trans. R. Soc. A 241:379–397, 1948. Yeoh: O. H. Yeoh, *Some forms of the strain energy
|
||||
function for rubber*, Rubber Chem. Technol. 66(5):754–771, 1993.
|
||||
- The homogeneous modes of rubber testing: L. R. G. Treloar, *The Physics of Rubber
|
||||
Elasticity*, 3rd ed., Oxford 1975, ch. 10; the inflation and extension of a tube: R. S. Rivlin,
|
||||
Phil. Trans. R. Soc. A 242:173–195, 1949, and R. W. Ogden, *Non-Linear Elastic Deformations*,
|
||||
Dover 1997, §5.3.
|
||||
- Thermal strain by the multiplicative split F = F_m F_θ: V. A. Lubarda, *Constitutive
|
||||
theories based on the multiplicative decomposition of deformation gradient: thermoelasticity,
|
||||
elastoplasticity, and biomechanics*, Appl. Mech. Rev. 57(2):95–108, 2004.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Bauschinger effect of linear kinematic hardening
|
||||
|
||||
One hex8 cube (E = 200000, ν = 0.3, σ_y = 250, H = 2000, Prager's linear kinematic hardening)
|
||||
held by symmetry on three faces, its fourth face pulled to a strain of 0.01 and brought back to
|
||||
zero within one nonlinear static step without nonlinear geometry, by the load path
|
||||
[[0, 0], [0.5, 1], [1, 0]] in increments of 0.05.
|
||||
|
||||
At ε_a the stress is σ_a = σ_y + E_t (ε_a − σ_y/E) = 267.327. The yield surface keeps its size
|
||||
and moves with the back stress, so unloading is elastic only until σ_a − 2σ_y and then hardens
|
||||
with E_t: back at zero strain σ = σ_a − 2σ_y − E_t (ε_a − 2σ_y/E) = −247.525. Isotropic hardening
|
||||
would yield again only at −σ_a and end at −281.835. The equivalent plastic strain accumulates
|
||||
(σ_a − σ_y)/H forward and E/(E + H)(ε_a − 2σ_y/E) in reverse, 0.0160891.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"title": "Bauschinger effect of linear kinematic hardening: tension, then back to zero strain",
|
||||
"kind": "theory",
|
||||
"source": "J2 plasticity with Prager's linear kinematic hardening in uniaxial stress (W. Prager, Ing.-Arch. 1955; Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3): from σ_a = σ_y + E_t (ε_a − σ_y/E) the material unloads elastically by 2σ_y and then hardens with E_t, so at zero strain σ = σ_a − 2σ_y − E_t (ε_a − 2σ_y/E)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "residual_stress",
|
||||
"quantity": "axial stress back at zero strain after a pull to ε_a = 0.01",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0"
|
||||
},
|
||||
"value": -247.52475247524754,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous small-strain state the element represents exactly, with increments that end at the load path's turn; isotropic hardening would give −281.835 (it yields again only at −σ_a)"
|
||||
},
|
||||
{
|
||||
"id": "plastic_strain",
|
||||
"quantity": "accumulated equivalent plastic strain",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/plastic_strain/0"
|
||||
},
|
||||
"value": 0.016089108910891083,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "(σ_a − σ_y)/H forward plus E/(E + H) (ε_a − 2σ_y/E) in reverse"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": {
|
||||
"plastic_strain": {
|
||||
"band": 1.6089108910891082e-11,
|
||||
"value": 0.01608910891089074
|
||||
},
|
||||
"residual_stress": {
|
||||
"band": 2.4752475247524754e-7,
|
||||
"value": -247.52475247484173
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "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": [1, 1, 1], "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": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.3}, "plastic": {"type": "bilinear", "yield_stress": 250.0, "hardening_modulus": 2000.0, "hardening": "kinematic"}}},
|
||||
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Steel", "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.01, null, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Pull", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "initial_increment": 0.05, "max_increment": 0.05, "load_path": [[0, 0], [0.5, 1], [1, 0]]}, "boundary_conditions": ["sx", "sy", "sz", "pull"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Pull"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
# A heated bar held at its ends past yield
|
||||
|
||||
A steel bar 4 × 1 × 1 of 4 hex8 elements (E = 200000, ν = 0.3, σ_y = 250, H = 2000 isotropic,
|
||||
α = 1.2·10⁻⁵ from 20 degrees) on symmetry planes at x = 0, y = 0, z = 0 and held at x = 4 (its
|
||||
axial strain is zero; the lateral faces are free), heated uniformly to 320 by a temperature load
|
||||
in a nonlinear static step without nonlinear geometry. The mechanical strain is −α ΔT = −0.0036,
|
||||
2.9 times the yield strain, so the bar yields in compression: σ = −(σ_y + E_t (α ΔT − σ_y/E)) =
|
||||
−254.653 with E_t = E H/(E + H), and ε_p = (|σ| − σ_y)/H.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"title": "A steel bar held at its ends and heated past yield (small strain)",
|
||||
"kind": "theory",
|
||||
"source": "Uniaxial J2 plasticity with linear hardening (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3) with the additive thermal strain: the bar's total axial strain is held at zero, so its mechanical strain is −α ΔT and σ = −(σ_y + E_t (α ΔT − σ_y/E)) once α ΔT exceeds σ_y/E",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "sigma",
|
||||
"quantity": "axial stress, ΔT = 300 (α ΔT = 3.6·10⁻³, 2.9 times the yield strain)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Heat",
|
||||
"elements": [
|
||||
2
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0"
|
||||
},
|
||||
"value": -254.65346534653466,
|
||||
"tolerance": {
|
||||
"relative": 1e-08
|
||||
},
|
||||
"reason": "a homogeneous uniaxial state the elements represent exactly"
|
||||
},
|
||||
{
|
||||
"id": "plastic_strain",
|
||||
"quantity": "equivalent plastic strain",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Heat",
|
||||
"elements": [
|
||||
2
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/plastic_strain/0"
|
||||
},
|
||||
"value": 0.0023267326732673297,
|
||||
"tolerance": {
|
||||
"relative": 1e-07
|
||||
},
|
||||
"reason": "(|σ| − σ_y)/H, as for the stress"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": {
|
||||
"plastic_strain": {
|
||||
"band": 2.3267326732675587e-12,
|
||||
"value": 0.0023267326732675587
|
||||
},
|
||||
"sigma": {
|
||||
"band": 2.5465346534653467e-7,
|
||||
"value": -254.6534653464418
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Bar", "kind": "hex8", "corners": [[0, 0, 0], [4, 0, 0], [4, 1, 0], [0, 1, 0], [0, 0, 1], [4, 0, 1], [4, 1, 1], [0, 1, 1]], "divisions": [4, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0", "4": "x1"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Bar"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Bar", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Bar", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Bar", "face": "z0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Bar", "face": "x1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.3}, "plastic": {"type": "bilinear", "yield_stress": 250.0, "hardening_modulus": 2000.0}, "thermal": {"expansion": 1.2e-05, "reference_temperature": 20.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "bar", "set": "all", "material": "Steel", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx0", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy0", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz0", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx1", "type": "symmetry", "target": {"set": "x1"}, "plane": "x"}},
|
||||
{"command": "model.load.create", "params": {"name": "heat", "type": "temperature", "target": {"set": "all"}, "value": 320.0}},
|
||||
{"command": "model.step.create", "params": {"name": "Heat", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false}, "boundary_conditions": ["sx0", "sy0", "sz0", "sx1"], "loads": ["heat"]}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Heat"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Uniaxial tension of a J2 bilinear-hardening cube to large strain (implicit)
|
||||
|
||||
The cube of `j2-uniaxial-explicit` solved by the implicit nonlinear solver: one fully integrated
|
||||
hex8, E = 200000, ν = 0.3, σ_y = 250, H = 2000 (isotropic), held by symmetry on three faces and
|
||||
stretched on the fourth to λ = e^0.2 in a nonlinear static step, lateral faces free.
|
||||
|
||||
Simo's exponential return map integrates this proportional path exactly in the logarithmic
|
||||
strain, so the Kirchhoff stress is the small-strain curve τ = σ_y + E_t (ε − σ_y/E) = 643.564 at
|
||||
ε = 0.2 and the equivalent plastic strain (τ − σ_y)/H = 0.196782. The Cauchy stress is
|
||||
σ = τ/J with Hencky's volume law ln J = τ/(3K): 642.737.
|
||||
|
||||
Cross-check with the explicit solver: `j2-uniaxial-explicit` reaches 643.57 for the Cauchy stress
|
||||
with the same plastic strain. Its hypoelastic update takes the logarithmic strain rate against
|
||||
the Cauchy stress, while the implicit model is hyperelastic in the Kirchhoff stress: the two
|
||||
laws differ by the factor J = 1.0013 (the elastic volume change), which is the 0.13 % between
|
||||
them; both meet the closed form of their own law.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"title": "Uniaxial tension of a J2 bilinear-hardening cube to large strain (implicit, finite strain)",
|
||||
"kind": "theory",
|
||||
"source": "J2 plasticity with linear isotropic hardening in uniaxial stress (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3) in the logarithmic strain, which Simo's exponential return map (CMAME 99:61–112, 1992) integrates exactly on a proportional path: Kirchhoff stress τ = σ_y + E_t (ε − σ_y/E), Cauchy stress σ = τ/J with ln J = τ/(3K) (Hencky's volume law)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "sigma",
|
||||
"quantity": "axial Cauchy stress at a logarithmic strain of 0.2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0"
|
||||
},
|
||||
"value": 642.7365391418857,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the element represents exactly, reached on a proportional path: the tolerance only admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "plastic_strain",
|
||||
"quantity": "equivalent plastic strain",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/plastic_strain/0"
|
||||
},
|
||||
"value": 0.19678217821782182,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "ε_p = (τ − σ_y)/H, as for the stress"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": {
|
||||
"plastic_strain": {
|
||||
"band": 1.9678217821795454e-10,
|
||||
"value": 0.19678217821795452
|
||||
},
|
||||
"sigma": {
|
||||
"band": 6.427365391418857e-7,
|
||||
"value": 642.7365390754569
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "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": [1, 1, 1], "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": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.3}, "plastic": {"type": "bilinear", "yield_stress": 250.0, "hardening_modulus": 2000.0, "hardening": "isotropic"}}},
|
||||
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Steel", "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.22140275816016985, null, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Pull", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Pull"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
# Mooney–Rivlin block in Treloar's three modes
|
||||
|
||||
One hybrid hex20 unit cube on three symmetry planes, E = 3, ν = 0.49 (μ = 1.006711,
|
||||
κ = 50), Mooney–Rivlin with C₀₁ = 0.2 (C₁₀ = μ/2 − C₀₁ = 0.303356), in three nonlinear
|
||||
static steps:
|
||||
|
||||
- **Uniaxial**: the face x = 1 pulled to λ = 2, the others free.
|
||||
- **Equibiaxial**: x = 1 and y = 1 pulled to λ = 1.5, z free.
|
||||
- **Pure shear**: x = 1 pulled to λ = 2, y = 1 held (λ_y = 1), z free.
|
||||
|
||||
The references are the compressible decoupled energy's principal Cauchy stresses
|
||||
σᵢ = (λᵢ/J) ∂W/∂λᵢ, by differences of W alone, with the free stretch found by bisection so the
|
||||
free faces carry no stress. The deformation is homogeneous, so the element represents it
|
||||
exactly.
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"title": "Mooney–Rivlin block in uniaxial, equibiaxial and pure-shear tension",
|
||||
"kind": "theory",
|
||||
"source": "Treloar's homogeneous modes of rubber testing (L. R. G. Treloar, The Physics of Rubber Elasticity, 3rd ed., Oxford 1975, ch. 10) for the decoupled compressible energy W = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + κ/2 (J − 1)²: principal Cauchy stresses σᵢ = (λᵢ/J) ∂W/∂λᵢ with the free stretch found so the free faces carry no stress (bisection; E = 3, ν = 0.49, C₀₁ = 0.2, C₁₀ = μ/2 − C₀₁)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "uniaxial",
|
||||
"quantity": "axial Cauchy stress, uniaxial stretch λ = 2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Uniaxial",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": 2.736633402084229,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "uniaxial_lateral",
|
||||
"quantity": "lateral displacement at (1, 1, 1), uniaxial: l − 1",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Uniaxial",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/1"
|
||||
},
|
||||
"value": -0.28647206688129256,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "equibiaxial",
|
||||
"quantity": "in-plane Cauchy stress, equibiaxial stretch λ = 1.5",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Equibiaxial",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": 2.842734818108988,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "shear_xx",
|
||||
"quantity": "Cauchy stress along the stretch, pure shear λ = 2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Shear",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": 3.526124960232999,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "shear_yy",
|
||||
"quantity": "Cauchy stress across, in the held direction, pure shear λ = 2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Shear",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/1"
|
||||
},
|
||||
"value": 1.506681177907714,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"equibiaxial": {
|
||||
"band": 2.8427348183579755e-9,
|
||||
"value": 2.8427348183579753
|
||||
},
|
||||
"shear_xx": {
|
||||
"band": 3.5261249603786212e-9,
|
||||
"value": 3.526124960378621
|
||||
},
|
||||
"shear_yy": {
|
||||
"band": 1.5066811779077142e-9,
|
||||
"value": 1.5066811778673217
|
||||
},
|
||||
"uniaxial": {
|
||||
"band": 2.736633402109491e-9,
|
||||
"value": 2.736633402109491
|
||||
},
|
||||
"uniaxial_lateral": {
|
||||
"band": 2.8647206688145424e-10,
|
||||
"value": -0.2864720668814542
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "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": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0", "4": "x1", "5": "y1"}}},
|
||||
{"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.set.create", "params": {"name": "y1", "geometry": [{"body": "Cube", "face": "y1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 3.0, "poisson": 0.49}, "hyperelastic": {"type": "mooney_rivlin", "c01": 0.2}}},
|
||||
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Rubber", "kind": {"type": "solid"}, "formulation": {"hybrid": true}}},
|
||||
{"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_u", "type": "displacement", "target": {"set": "x1"}, "values": [1.0, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "pull_e", "type": "displacement", "target": {"set": "x1"}, "values": [0.5, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "pull_ey", "type": "displacement", "target": {"set": "y1"}, "values": [null, 0.5, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "pull_s", "type": "displacement", "target": {"set": "x1"}, "values": [1.0, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "hold_y", "type": "displacement", "target": {"set": "y1"}, "values": [null, 0.0, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Uniaxial", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull_u"], "loads": []}},
|
||||
{"command": "model.step.create", "params": {"name": "Equibiaxial", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull_e", "pull_ey"], "loads": []}},
|
||||
{"command": "model.step.create", "params": {"name": "Shear", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull_s", "hold_y"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Uniaxial"}, "id": "uniaxial"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Equibiaxial"}, "id": "equibiaxial"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Shear"}, "id": "shear"}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
# Inflation of a stretched Mooney–Rivlin tube
|
||||
|
||||
The quarter tube of `nl-cylinder-inflation` (A = 10, B = 20, one unit long, 6 × 16 hybrid
|
||||
hex20), now Mooney–Rivlin (E = 3, ν = 0.4995, so μ = 1.000333; C₀₁ = 0.2, C₁₀ = μ/2 − C₀₁) and
|
||||
stretched axially by its end face (u_z = 0.5 at z = 1: λ_z = 1.5) while a follower pressure
|
||||
p = 0.540850 inflates the bore. In plane strain (λ_z = 1) Mooney–Rivlin is neo-Hookean with
|
||||
μ = 2(C₁₀ + C₀₁), since Ī₁ = Ī₂ there; the axial stretch makes the second invariant's share
|
||||
count: σ_θ − σ_r = 2(λ_θ² − λ_r²)(C₁₀ + C₀₁ λ_z²). Integrated through the incompressible wall,
|
||||
r² = a² + (R² − A²)/λ_z, that pressure puts the bore at a = 15 and the outer wall at
|
||||
b = 20.615528.
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"title": "Inflation of a stretched Mooney–Rivlin tube (follower pressure)",
|
||||
"kind": "theory",
|
||||
"source": "Finite inflation and extension of an incompressible Mooney–Rivlin tube (R. S. Rivlin, Large elastic deformations of isotropic materials VI, Phil. Trans. R. Soc. A 242:173–195, 1949; R. W. Ogden, Non-Linear Elastic Deformations, Dover 1997, §5.3): at axial stretch λ_z, r² = a² + (R² − A²)/λ_z, σ_θ − σ_r = 2 (λ_θ² − λ_r²)(C₁₀ + C₀₁ λ_z²) with λ_θ = r/R, λ_r = 1/(λ_θ λ_z), integrated through the wall: p = ∫ (σ_θ − σ_r) dr/r (Simpson, 2·10⁵ intervals)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "ua",
|
||||
"quantity": "radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15 at λ_z = 1.5",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Inflate",
|
||||
"at": [
|
||||
[
|
||||
10.0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/0"
|
||||
},
|
||||
"value": 5.0,
|
||||
"tolerance": {
|
||||
"relative": 0.01
|
||||
},
|
||||
"reason": "16 × 6 hybrid hex20 around the quarter; ν = 0.4995 stands in for incompressibility (a volume change of order p/κ ≈ 10⁻³ shifts the radii by about 0.1 %), and the tube is stretched axially by its ends, which a nearly incompressible solid takes with larger pressures than the plane-strain neo-Hookean tube"
|
||||
},
|
||||
{
|
||||
"id": "ub",
|
||||
"quantity": "radial displacement of the outer wall (20, 0, 0): b − B, a small difference of radii",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Inflate",
|
||||
"at": [
|
||||
[
|
||||
20.0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/0"
|
||||
},
|
||||
"value": 0.6155281280883038,
|
||||
"tolerance": {
|
||||
"absolute": 0.04
|
||||
},
|
||||
"reason": "0.2 % of the outer radius b = 20.616: the same volume change of order 10⁻³ moves b by about 0.03, which is 4.5 % of b − B; the incompressible kinematics r² = a² + (R² − A²)/λ_z amplify it there"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": {
|
||||
"ua": {
|
||||
"band": 5.031118164303424e-9,
|
||||
"value": 5.0311181643034235
|
||||
},
|
||||
"ub": {
|
||||
"band": 0.00004000064339064634,
|
||||
"value": 0.64339064633861
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Pipe", "kind": "hex20", "corners": [[10.0, 0, 0], [20.0, 0, 0], [0, 20.0, 0], [0, 10.0, 0], [10.0, 0, 1], [20.0, 0, 1], [0, 20.0, 1], [0, 10.0, 1]], "divisions": [6, 16, 1], "edges": [{"corners": [2, 3], "arc": {"center": [0, 0, 0]}}, {"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 1]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 1]}}], "faces": {"6": "bore", "3": "y0", "5": "x0", "1": "z0", "2": "z1"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Pipe"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "bore", "geometry": [{"body": "Pipe", "face": "bore"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Pipe", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Pipe", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Pipe", "face": "z0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Pipe", "face": "z1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 3.0, "poisson": 0.4995}, "hyperelastic": {"type": "mooney_rivlin", "c01": 0.2}}},
|
||||
{"command": "model.section.create", "params": {"name": "pipe", "set": "all", "material": "Rubber", "kind": {"type": "solid"}, "formulation": {"hybrid": true}}},
|
||||
{"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": "sz0", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "stretch", "type": "displacement", "target": {"set": "z1"}, "values": [null, null, 0.5, null, null, null]}},
|
||||
{"command": "model.load.create", "params": {"name": "p", "type": "pressure", "set": "bore", "magnitude": 0.5408503415684837}},
|
||||
{"command": "model.step.create", "params": {"name": "Inflate", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz0", "stretch"], "loads": ["p"]}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Inflate"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
# Free and constrained thermal expansion at finite strain
|
||||
|
||||
A unit cube of 2 × 2 × 2 hex20, compressible neo-Hookean (E = 10, ν = 0.3) with an expansion
|
||||
coefficient α = 10⁻³ from 20 degrees, heated to 220 by a temperature load in two nonlinear static
|
||||
steps: **Free** (three symmetry planes) and **Held** (symmetry planes on all six faces). The
|
||||
thermal stretch is ϑ = 1 + α ΔT = 1.2. Free, the cube grows to 1.2 along every edge without
|
||||
stress; held, F = I, the material's mechanical part is F_m = ϑ⁻¹ I and its stress the neo-Hookean
|
||||
pressure κ (J_m − 1) with J_m = ϑ⁻³: −3.510802.
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"title": "Free and fully constrained thermal expansion at finite strain",
|
||||
"kind": "theory",
|
||||
"source": "The multiplicative split F = F_m F_θ with F_θ = (1 + α ΔT) I (V. A. Lubarda, Appl. Mech. Rev. 57(2):95–108, 2004): a free body follows F = F_θ stress-free; held on every face, F = I and F_m = ϑ⁻¹ I, so the compressible neo-Hookean material carries the pressure of its volume ratio, σ = κ (ϑ⁻³ − 1)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "free_u",
|
||||
"quantity": "displacement of the corner (1, 1, 1) along x, free, α ΔT = 0.2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Free",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/0"
|
||||
},
|
||||
"value": 0.2,
|
||||
"tolerance": {
|
||||
"relative": 1e-08
|
||||
},
|
||||
"reason": "a homogeneous stretch every element represents exactly"
|
||||
},
|
||||
{
|
||||
"id": "free_stress",
|
||||
"quantity": "stress of the free body (σ_xx at the corner)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Free",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 1e-08
|
||||
},
|
||||
"reason": "rounding only"
|
||||
},
|
||||
{
|
||||
"id": "held_stress",
|
||||
"quantity": "stress of the held block, σ_xx (hydrostatic)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Held",
|
||||
"at": [
|
||||
[
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": -3.5108024691358017,
|
||||
"tolerance": {
|
||||
"relative": 1e-08
|
||||
},
|
||||
"reason": "a homogeneous state; E = 10, ν = 0.3 (κ = 8.3333), ϑ = 1.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"free_stress": {
|
||||
"band": 1e-8,
|
||||
"value": -1.0882345665298319e-14
|
||||
},
|
||||
"free_u": {
|
||||
"band": 2.0000000000000037e-10,
|
||||
"value": 0.20000000000000034
|
||||
},
|
||||
"held_stress": {
|
||||
"band": 3.510802469135803e-9,
|
||||
"value": -3.5108024691358026
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "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", "5": "y1", "2": "z1"}}},
|
||||
{"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.set.create", "params": {"name": "y1", "geometry": [{"body": "Cube", "face": "y1"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Cube", "face": "z1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 10.0, "poisson": 0.3}, "hyperelastic": {"type": "neo_hookean"}, "thermal": {"expansion": 0.001, "reference_temperature": 20.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Rubber", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx0", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy0", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz0", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx1", "type": "symmetry", "target": {"set": "x1"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy1", "type": "symmetry", "target": {"set": "y1"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz1", "type": "symmetry", "target": {"set": "z1"}, "plane": "z"}},
|
||||
{"command": "model.load.create", "params": {"name": "heat", "type": "temperature", "target": {"set": "all"}, "value": 220.0}},
|
||||
{"command": "model.step.create", "params": {"name": "Free", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx0", "sy0", "sz0"], "loads": ["heat"]}},
|
||||
{"command": "model.step.create", "params": {"name": "Held", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx0", "sy0", "sz0", "sx1", "sy1", "sz1"], "loads": ["heat"]}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Free"}, "id": "free"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Held"}, "id": "held"}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
# Yeoh block in uniaxial tension
|
||||
|
||||
One hybrid hex20 unit cube on three symmetry planes, E = 3, ν = 0.49, Yeoh with C₁₀ = μ/2,
|
||||
C₂₀ = −0.02 and C₃₀ = 0.004, pulled to λ = 2 in a nonlinear static step. The reference is the
|
||||
compressible decoupled energy's axial Cauchy stress σ = (λ/J) ∂W/∂λ with the lateral stretch
|
||||
found by bisection so σ_yy = 0.
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"title": "Yeoh block in uniaxial tension",
|
||||
"kind": "theory",
|
||||
"source": "Uniaxial tension of the decoupled compressible Yeoh energy W = C₁₀ (Ī₁ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ + κ/2 (J − 1)² (O. H. Yeoh, Rubber Chem. Technol. 66(5):754–771, 1993): σ = (λ/J) ∂W/∂λ with the lateral stretch found so σ_yy = 0 (bisection; E = 3, ν = 0.49, C₁₀ = μ/2, C₂₀ = −0.02, C₃₀ = 0.004)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "sigma",
|
||||
"quantity": "axial Cauchy stress, stretch λ = 2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Uniaxial",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": 3.174263919393508,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "lateral",
|
||||
"quantity": "lateral displacement at (1, 1, 1): l − 1",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Uniaxial",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/1"
|
||||
},
|
||||
"value": -0.2854505757255106,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": 2.854505757255106e-10,
|
||||
"value": -0.285450575725269
|
||||
},
|
||||
"sigma": {
|
||||
"band": 3.174263919404631e-9,
|
||||
"value": 3.1742639194046305
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "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": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0", "4": "x1", "5": "y1"}}},
|
||||
{"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": 3.0, "poisson": 0.49}, "hyperelastic": {"type": "yeoh", "c20": -0.02, "c30": 0.004}}},
|
||||
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Rubber", "kind": {"type": "solid"}, "formulation": {"hybrid": true}}},
|
||||
{"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": [1.0, null, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Uniaxial", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Uniaxial"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Plastic collapse of a cantilever
|
||||
|
||||
A steel bar 200 long with a 10 × 10 section (E = 200000, ν = 0.3, elastic–perfectly plastic at
|
||||
σ_y = 250), reduced hex20 20 × 2 × 4, in a nonlinear static step without nonlinear geometry. The
|
||||
root x = 0 is a plane of symmetry (no rotation, the section free to contract laterally), held in
|
||||
Y, Z and against twist on average through a distributing coupling; a coupling at the tip pushes
|
||||
the end down by 15, 4.5 times the deflection at first yield.
|
||||
|
||||
Beam theory's collapse is a plastic hinge at the root: M_p = σ_y b h²/4 = 62500 and
|
||||
P_c = M_p/L = 312.5. The solid's collapse lies above it, since a continuum cannot rotate about a
|
||||
hinge (that needs a jump of the normal velocity across the section): refined to 8 elements
|
||||
through the depth or 160 along the length, the force settles at 322.3, 3.1 % above. The case
|
||||
checks that the implicit plasticity reaches this plateau and holds it.
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "Plastic collapse of a cantilever: the fully plastic moment at the root",
|
||||
"kind": "theory",
|
||||
"source": "Limit analysis (B. G. Neal, The Plastic Methods of Structural Analysis, 3rd ed., Chapman & Hall 1977, ch. 1–2): beam theory's plastic hinge at the root, the fully plastic moment M_p = σ_y b h²/4 of a rectangular section, collapses a cantilever of length L at P_c = M_p/L = 312.5 (b = h = 10, L = 200, σ_y = 250); a bound for the solid, whose collapse lies above it (W. Prager, P. G. Hodge, Theory of Perfectly Plastic Solids, Wiley 1951: a continuum mechanism allows no normal velocity jump, which the beam's hinge rotation is)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "collapse_force",
|
||||
"quantity": "tip force at a tip deflection of 15 (4.5 times the first yield's)",
|
||||
"measure": {
|
||||
"from": "solve",
|
||||
"pointer": "/reference_points/1/reaction/2"
|
||||
},
|
||||
"value": -312.5,
|
||||
"tolerance": {
|
||||
"relative": 0.05
|
||||
},
|
||||
"reason": "the solid's collapse lies above beam theory's hinge: refined through the depth (4 to 8 elements) and along the length (40 to 160) the force settles 3.1 % above M_p/L (322.3), so the band is that bound's gap; the root is a symmetry plane (no rotation, free lateral contraction) held on average through a coupling, the tip pushed through another; reduced hex20, 20 × 2 × 4"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"collapse_force": {
|
||||
"band": 3.2260685847425373e-7,
|
||||
"value": -322.6068584742537
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Beam", "kind": "hex20", "corners": [[0, 0, -5.0], [200.0, 0, -5.0], [200.0, 10.0, -5.0], [0, 10.0, -5.0], [0, 0, 5.0], [200.0, 0, 5.0], [200.0, 10.0, 5.0], [0, 10.0, 5.0]], "divisions": [20, 2, 4], "faces": {"6": "root", "4": "tip"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Beam"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Beam", "face": "root"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "tip", "geometry": [{"body": "Beam", "face": "tip"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.3}, "plastic": {"type": "bilinear", "yield_stress": 250.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Steel", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "plane", "type": "symmetry", "target": {"set": "root"}, "plane": "x"}},
|
||||
{"command": "model.reference_point.create", "params": {"name": "Root", "xyz": [0, 5.0, 0]}},
|
||||
{"command": "model.coupling.create", "params": {"name": "support", "reference_point": "Root", "set": "root", "kind": "distributing"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "hold", "type": "displacement", "target": {"reference_point": "Root"}, "values": [null, 0, 0, 0, null, null]}},
|
||||
{"command": "model.reference_point.create", "params": {"name": "Tip", "xyz": [200.0, 5.0, 0]}},
|
||||
{"command": "model.coupling.create", "params": {"name": "end", "reference_point": "Tip", "set": "tip", "kind": "distributing"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "push", "type": "displacement", "target": {"reference_point": "Tip"}, "values": [null, null, -15.0, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Collapse", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false}, "boundary_conditions": ["plane", "hold", "push"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Collapse"}, "id": "solve"}
|
||||
]
|
||||
@@ -1,3 +1,14 @@
|
||||
# Elastic–perfectly plastic thick cylinder: collapse pressure
|
||||
# Elastic–perfectly plastic thick cylinder: plastic zone and collapse pressure
|
||||
|
||||
The Lamé cylinder of `lame-cylinder`, elastic–perfectly plastic, loaded to collapse.
|
||||
The Lamé cylinder of `lame-cylinder-axisymmetric` (a = 10, b = 20, an axisymmetric slice held
|
||||
axially on both faces, so plane strain), now elastic–perfectly plastic: E = 200000,
|
||||
ν = 0.4999 (Hill's solution takes the elasticity incompressible), σ_y = 250, one row of 20
|
||||
reduced quad8 elements through the wall, in two nonlinear static steps without nonlinear
|
||||
geometry:
|
||||
|
||||
- **Pressure**: the bore pressure p = k (2 ln(c/a) + 1 − c²/b²) = 180.195 (k = σ_y/√3) that
|
||||
takes the plastic zone to c = 15. The bore then moves u_a = k c²/(2 G a) = 0.024355: the plastic
|
||||
zone moves incompressibly (u r constant) and the elastic ring outside yields at its inner edge.
|
||||
- **Collapse**: the bore pushed out by 0.3 (24 times its first-yield displacement), past the
|
||||
pressure at which the wall is plastic through; the radial force on the bore stays at the
|
||||
collapse pressure 2k ln(b/a) = 200.094 times 2πa per unit height.
|
||||
@@ -1,14 +1,43 @@
|
||||
{
|
||||
"title": "Elastic–perfectly plastic thick cylinder: collapse pressure",
|
||||
"title": "Elastic–perfectly plastic thick cylinder: plastic zone and collapse pressure",
|
||||
"kind": "theory",
|
||||
"source": "Hill, The Mathematical Theory of Plasticity, Oxford 1950, ch. V §2: plane strain von Mises collapse pressure p = (2/√3)σ_y ln(b/a)",
|
||||
"status": "pending",
|
||||
"requires": "implicit (static) J2 plasticity: explicit J2 is in, a collapse load needs the static solver",
|
||||
"source": "Hill, The Mathematical Theory of Plasticity, Oxford 1950, ch. V §2: a von Mises cylinder in plane strain, elastically incompressible, with the plastic zone a ≤ r ≤ c under the pressure p = k (2 ln(c/a) + 1 − c²/b²), k = σ_y/√3, and the bore displacement u_a = k c²/(2 G a); collapse when c = b at p = 2k ln(b/a)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "p_collapse",
|
||||
"quantity": "collapse pressure for a = 10, b = 20, σ_y = 250",
|
||||
"value": 200.0944
|
||||
"id": "u_bore",
|
||||
"quantity": "bore displacement at the pressure that takes the plastic zone to c = 15 (a = 10, b = 20, σ_y = 250)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pressure",
|
||||
"at": [
|
||||
[
|
||||
10.0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/0"
|
||||
},
|
||||
"value": 0.02435534068380524,
|
||||
"tolerance": {
|
||||
"relative": 0.002
|
||||
},
|
||||
"reason": "20 reduced quad8 elements through the wall resolve the logarithmic plastic field and ν = 0.4999 stands in for Hill's incompressible elasticity (0.009 % apart); u_a ∝ c², so 0.2 % on u_a is 0.1 % on the plastic radius"
|
||||
},
|
||||
{
|
||||
"id": "collapse_force",
|
||||
"quantity": "radial force on the bore (per radian times 2π, height 1) at a bore displacement of 0.3, 24 times the first yield's: the collapse pressure 2k ln 2 = 200.094 times 2πa",
|
||||
"measure": {
|
||||
"from": "collapse",
|
||||
"pointer": "/reaction_total/0"
|
||||
},
|
||||
"value": 12572.299154203694,
|
||||
"tolerance": {
|
||||
"relative": 0.001
|
||||
},
|
||||
"reason": "once the wall is plastic through, perfect plasticity holds the force at the collapse value (σ_θ − σ_r = 2k everywhere, σ_z the mean of the others as plastic flow dominates); the band admits the quadrature of the logarithmic field"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
"collapse_force": {
|
||||
"band": 0.000012572299377396248,
|
||||
"value": 12572.299377396248
|
||||
},
|
||||
"u_bore": {
|
||||
"band": 2.4357507851220506e-11,
|
||||
"value": 0.024357507851220504
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Pipe", "kind": "quad8", "corners": [[10.0, 0, 0], [20.0, 0, 0], [20.0, 1, 0], [10.0, 1, 0]], "divisions": [20, 1], "bias": [1, 1], "edges": [{"corners": [1, 4], "name": "bore"}, {"corners": [1, 2], "name": "bottom"}, {"corners": [3, 4], "name": "top"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Pipe", "face": "area"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "bore", "geometry": [{"body": "Pipe", "edge": "bore"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "ends", "geometry": [{"body": "Pipe", "edge": "bottom"}, {"body": "Pipe", "edge": "top"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.4999}, "plastic": {"type": "bilinear", "yield_stress": 250.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "pipe", "set": "all", "material": "Steel", "kind": {"type": "axisymmetric"}, "formulation": {"integration": "reduced"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "ends", "type": "symmetry", "target": {"set": "ends"}, "plane": "y"}},
|
||||
{"command": "model.load.create", "params": {"name": "p", "type": "pressure", "set": "bore", "magnitude": 180.19538034924005}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "expand", "type": "displacement", "target": {"set": "bore"}, "values": [0.3, null, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Pressure", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false}, "boundary_conditions": ["ends"], "loads": ["p"]}},
|
||||
{"command": "model.step.create", "params": {"name": "Collapse", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false}, "boundary_conditions": ["ends", "expand"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Pressure"}, "id": "pressure"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Collapse"}, "id": "collapse"}
|
||||
]
|
||||
Reference in new issue
Block a user