Compare commits

...
4 Commits
Author SHA1 Message Date
jyvikoandClaude Opus 5.5 8ca2dddeec verification: nonlinear oracle cases and the nonlinear solver's documentation
Theory cases for the nonlinear solver, each frozen as a fixture: uniaxial
stretch of a neo-Hookean block to twice its length (closed form), the
nonlinear patch test on a distorted hex8 mesh, a quarter turn of the same
mesh (objectivity: zero stress), the elastica of a hex20 cantilever at
P L²/(E I) = 1, 2, 5, 10 against Bisshopp–Drucker (0.2–0.5 %), and the
inflation of a thick neo-Hookean tube by a follower pressure against
Rivlin's closed form (0.08 % at the bore). A finite-strain Cook's membrane
is pending its transcribed reference. docs/fem.md describes the
formulation, the material point interface, the follower-pressure choice and
what nonlinear steps refuse; the literature, parity and roadmap pages
follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:00:21 -04:00
jyvikoandClaude Opus 5.5 2183fdbe81 solver: nonlinear static analysis by Newton–Raphson with automatic increments
solve.nonlinear follows a nonlinear static step in load increments: dead
loads and prescribed displacements ramp with the load factor, a pressure
follows the deformed faces (its load stiffness symmetrised so the
supernodal Cholesky stays), the consistent tangent is refactored every
iteration with an optional line search, and an increment converges on
force, displacement and energy together. Increments start from the
extrapolated last one, grow after fast convergence and are cut back on
inversion, a non-finite or growing residual, an indefinite tangent or too
many iterations; the step stops cleanly at the minimum increment, the
increment cap or the Newton iteration limit. Elements are assembled in
parallel in label order (same bits on any thread count). Results carry
Cauchy stress, logarithmic strain, the increment history and a displacement
frame per increment, which results.frame and the results panel show.
Trusses, shells, beams, plane stress, incompatible modes, temperature and
centrifugal loads are refused by name; couplings warn that they stay
linear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:00:21 -04:00
jyvikoandClaude Opus 5.5 2321d75cc6 fem: finite-strain material point, total Lagrangian elements and follower pressure
The material point interface returns the Kirchhoff stress and the spatial
tangent of its Lie derivative from the deformation gradient, with per-point
history committed by the solver: St Venant–Kirchhoff (pushed forward from
S = D E) and the decoupled compressible neo-Hookean with its isochoric and
volumetric parts. Total Lagrangian solids (tet4, tet10, wedge6, hex8, hex20
full and reduced) and plane strain and axisymmetric elements give internal
forces and the consistent tangent with its geometric stiffness; hybrid
solids use the Simo–Taylor–Pister three-field formulation on the existing
pressure fields. Incompatible modes and plane stress are refused by name.
Follower pressure on faces and 2D edges with its load stiffness. Every
tangent is checked against central differences of its forces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:00:21 -04:00
jyvikoandClaude Opus 5.5 bc9fe2191a model: nonlinear static steps and neo-Hookean hyperelasticity, mapped by the deck writers
A nonlinear_static procedure (nonlinear geometry, initial/min/max increments,
increment and iteration caps, force/displacement/energy tolerances, line
search), validated with caps against hostile values. Materials gain an
explicit hyperelastic option (decoupled compressible neo-Hookean on the
isotropic constants). Abaqus writes *STEP, NLGEOM, INC / *STATIC with the
increments and *HYPERELASTIC, NEO HOOKE; LS-DYNA writes the ramped implicit
run with full Newton and *CONTROL_IMPLICIT_AUTO; Nastran refuses SOL 400 by
name, as LS-DYNA does hyperelastic materials in nonlinear steps. Explicit
dynamics refuses hyperelastic materials by name; solve.run names the solver
still to come.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:00:21 -04:00
61 changed files with 4586 additions and 83 deletions

No files matched your search

+5 -4
View File
@@ -41,7 +41,7 @@ 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 | 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 |
| 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 |
| 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 |
@@ -67,7 +67,8 @@ In progress, in this order of landing:
4. A Scordelis–Lo refinement study (32×32×2, 64×64×4, hex20 full and reduced) separating
discretisation error from the solid-versus-shell model difference (−0.33 % today).
5. The nonlinear implicit core (M15), the gate for contact (M7), elastomers and viscoelasticity
(M17), composite failure (M18), adhesion (M20) and tissues (M21).
(M17), composite failure (M18), adhesion (M20) and tissues (M21): solids, hyperelasticity and
follower pressure done; J2 plasticity and Mooney–Rivlin on the material point interface next.
## Gap review (2026-10-14)
@@ -75,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, frequency (Lanczos), steady and transient heat, coupled temperature–displacement, explicit dynamics, LBM flow | Nonlinear static and geometric nonlinearity, arc-length, buckling (M15); implicit, harmonic, random and spectrum dynamics (M16) |
| 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) |
| 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, Newtonian fluids | Hyperelastic and elastomers, 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 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) |
| 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) |
+50 -6
View File
@@ -30,7 +30,14 @@
//! - a frequency step is a perturbation of the last general step: its boundary conditions are
//! written against that base state and do not carry on to later steps; loads have no effect on
//! natural frequencies and are not written in it;
//! - every step restates its output request (`*OUTPUT, FIELD, OP=NEW`).
//! - every step restates its output request (`*OUTPUT, FIELD, OP=NEW`);
//! - a nonlinear static step is `*STEP, NLGEOM=YES` (or `NO` without nonlinear geometry) with
//! `INC=` its increment cap, `*STATIC` with its initial, minimum and maximum increments over a
//! step period of 1, and `*CONTROLS, PARAMETERS=LINE SEARCH` when it searches; its iteration cap
//! and tolerances are left to Abaqus's controls, whose residual norms are defined differently.
//! Abaqus starts each general step from the end of the previous one, while every model step
//! starts from the undeformed state; for elastic materials the end state depends on the final
//! loads alone, so the two agree.
//!
//! # Loads
//!
@@ -96,7 +103,10 @@
//!
//! # Materials
//!
//! Isotropic elasticity is `*ELASTIC, TYPE=ISOTROPIC`, engineering constants
//! Isotropic elasticity is `*ELASTIC, TYPE=ISOTROPIC` (or, for a neo-Hookean material in a deck
//! with nonlinear steps, `*HYPERELASTIC, NEO HOOKE` with C₁₀ = μ/2 and D₁ = 2/κ, the same strain
//! energy; a linear static step is then refused, since Abaqus's general static step would apply
//! the hyperelastic law), engineering constants
//! `TYPE=ENGINEERING CONSTANTS`, density `*DENSITY`. Von Mises plasticity is `*PLASTIC`: a
//! hardening table as its (yield stress, plastic strain) lines; bilinear isotropic hardening as
//! two lines, the second at a plastic strain of [`BILINEAR_REACH`] (Abaqus holds the last yield
@@ -219,6 +229,12 @@ pub fn element_type(kind: ElementKind, section: &Section) -> std::result::Result
Ok(if f.hybrid { format!("{plain}H") } else { plain.to_string() })
}
/// Whether the model has a nonlinear static step: hyperelastic materials are then written as
/// `*HYPERELASTIC` (in a deck of linear steps their elastic constants are the whole of it).
fn has_nonlinear_step(model: &Model) -> bool {
model.steps().iter().any(|s| matches!(s.procedure, Procedure::NonlinearStatic(_)))
}
/// Every element, grouped by element type (from its section) and sorted by label.
fn elements_by_type<'m>(model: &'m Model, r: &Resolved) -> Result<BTreeMap<String, Vec<(u32, &'m Element)>>> {
let mut by_type: BTreeMap<String, Vec<(u32, &Element)>> = BTreeMap::new();
@@ -287,6 +303,16 @@ fn check_expressible(model: &Model) -> Result<()> {
for step in model.steps().iter().filter(|s| matches!(s.procedure, Procedure::ExplicitDynamic(_))) {
bad.insert(format!("step `{}`: explicit dynamic steps (not written to Abaqus input yet)", step.name));
}
if has_nonlinear_step(model)
&& let Some(material) = model.hyperelastic_in_use().into_iter().next()
{
for step in model.steps().iter().filter(|s| matches!(s.procedure, Procedure::Static {})) {
bad.insert(format!(
"step `{}`: a linear static step beside nonlinear steps of hyperelastic material `{material}` (Abaqus's general static step applies the hyperelastic law; write the linear step to its own deck)",
step.name
));
}
}
for (name, m) in model.materials() {
if m.damage.is_some() || m.erosion.is_some() {
bad.insert(format!(
@@ -759,6 +785,12 @@ impl Deck {
for (name, m) in model.materials() {
self.line(&format!("*MATERIAL, NAME={name}"));
match m.elastic {
Some(Elastic::Isotropic { young, poisson }) if m.hyperelastic.is_some() && has_nonlinear_step(model) => {
// W = C10 (Ī1 − 3) + (J − 1)²/D1 with C10 = μ/2 and D1 = 2/κ.
let (shear, bulk) = fieldcraft_model::Hyperelastic::moduli(young, poisson);
self.line("*HYPERELASTIC, NEO HOOKE");
self.numbers_line("", &[shear / 2.0, 2.0 / bulk])?;
}
Some(Elastic::Isotropic { young, poisson }) => {
self.line("*ELASTIC, TYPE=ISOTROPIC");
self.numbers_line("", &[young, poisson])?;
@@ -1038,10 +1070,22 @@ impl Deck {
self.comment(&format!("Step {}", step.name));
match step.procedure {
Procedure::Fluid { .. } => return Err(crate::fluid_step(FORMAT, &step.name)),
Procedure::Static {} => {
self.line(&format!("*STEP, NAME={}, NLGEOM=NO", step.name));
self.line("*STATIC");
self.line("1.0, 1.0");
Procedure::Static {} | Procedure::NonlinearStatic(_) => {
if let Procedure::NonlinearStatic(n) = &step.procedure {
let nlgeom = if n.nonlinear_geometry { "YES" } else { "NO" };
self.line(&format!("*STEP, NAME={}, NLGEOM={nlgeom}, INC={}", step.name, n.max_increments));
self.line("*STATIC");
self.numbers_line("", &[n.initial_increment, 1.0, n.min_increment, n.max_increment])?;
if n.line_search {
self.line("*CONTROLS, PARAMETERS=LINE SEARCH");
self.line("5,");
}
self.comment("iterations and convergence: Abaqus's own controls (its residual norms differ from FieldCraft's)");
} else {
self.line(&format!("*STEP, NAME={}, NLGEOM=NO", step.name));
self.line("*STATIC");
self.line("1.0, 1.0");
}
self.step_boundaries(model, r, &base_bcs, &step.boundary_conditions)?;
if step.loads == base_loads {
self.comment("loads: as in the previous general step");
+37 -6
View File
@@ -24,7 +24,13 @@
//! the set's translations, equal weights);
//! - per step: `*CONTROL_IMPLICIT_GENERAL`, `*CONTROL_IMPLICIT_SOLUTION` (linear) or
//! `*CONTROL_IMPLICIT_EIGENVALUE`, `*CONTROL_TERMINATION` (a static step is one implicit step
//! from time 0 to 1 with every load ramped by one curve, `*DEFINE_CURVE`); boundary conditions
//! from time 0 to 1 with every load ramped by one curve, `*DEFINE_CURVE`); a nonlinear static
//! step runs over the same time with the same ramp, its first increment as `dt0`, the nonlinear
//! solver with a stiffness reformation every iteration (`ilimit` 1, full Newton) and its
//! iteration cap as `maxref`, and `*CONTROL_IMPLICIT_AUTO` with its minimum and maximum
//! increments (its increment cap and tolerances have no LS-DYNA counterpart and are left to
//! LS-DYNA's defaults; a step without nonlinear geometry and hyperelastic materials in nonlinear
//! steps are refused); boundary conditions
//! as `*BOUNDARY_SPC_*_ID` (named) and `*BOUNDARY_PRESCRIBED_MOTION_*_ID` for non-zero
//! displacements; forces, moments and tractions as `*LOAD_NODE_SET`/`_POINT` from the
//! resolved nodal values; pressure as `*LOAD_SEGMENT_SET`; gravity as `*LOAD_BODY_X/Y/Z`
@@ -267,7 +273,21 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
if matches!(step.procedure, Procedure::ExplicitDynamic(_)) {
refused.insert(format!("step `{}`: explicit dynamic steps (this writer writes implicit analyses so far)", step.name));
}
let is_static = matches!(step.procedure, Procedure::Static {});
if let Procedure::NonlinearStatic(n) = &step.procedure {
if !n.nonlinear_geometry {
refused.insert(format!(
"step `{}`: a nonlinear step without nonlinear geometry (LS-DYNA's nonlinear implicit analysis always follows the deformed shape)",
step.name
));
}
for material in model.hyperelastic_in_use() {
refused.insert(format!(
"step `{}`: hyperelastic material `{material}` in a nonlinear step (LS-DYNA's rubber materials define their volumetric energy differently; not written yet)",
step.name
));
}
}
let is_static = matches!(step.procedure, Procedure::Static {} | Procedure::NonlinearStatic(_));
for b in &step.boundary_conditions {
let Some(bc) = model.boundary_conditions().get(b) else { continue };
match bc.target() {
@@ -793,18 +813,29 @@ fn dof_code(dof: u8) -> i32 {
fn write_step(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan, step: &Step, n: u32, constraint_id: &mut u32) -> Result<()> {
d.line("*TITLE");
d.line(&step.name);
let is_static = matches!(step.procedure, Procedure::Static {});
let is_static = matches!(step.procedure, Procedure::Static {} | Procedure::NonlinearStatic(_));
let first_increment = if let Procedure::NonlinearStatic(n) = &step.procedure { n.initial_increment } else { 1.0 };
d.line("*CONTROL_IMPLICIT_GENERAL");
d.header(&["imflag", "dt0"]);
d.card([Field::from(1), Field::from(1.0)]);
match step.procedure {
d.card([Field::from(1), Field::from(first_increment)]);
match &step.procedure {
Procedure::Fluid { .. } => return Err(crate::fluid_step(FORMAT, &step.name)),
Procedure::Static {} => {
d.line("*CONTROL_IMPLICIT_SOLUTION");
d.header(&["nsolvr"]);
d.card([Field::from(1)]);
}
Procedure::Frequency { modes, min_hz, max_hz } => {
Procedure::NonlinearStatic(n) => {
// Nonlinear with a stiffness reformation every iteration (full Newton), at most
// `max_iterations` per increment; LS-DYNA's own convergence norms and line search.
d.line("*CONTROL_IMPLICIT_SOLUTION");
d.header(&["nsolvr", "ilimit", "maxref"]);
d.card([Field::from(2), Field::from(1), Field::from(n.max_iterations)]);
d.line("*CONTROL_IMPLICIT_AUTO");
d.header(&["iauto", "iteopt", "itewin", "dtmin", "dtmax"]);
d.card([Field::from(1), Field::Blank, Field::Blank, Field::from(n.min_increment), Field::from(n.max_increment)]);
}
&Procedure::Frequency { modes, min_hz, max_hz } => {
d.line("*CONTROL_IMPLICIT_EIGENVALUE");
d.header(&["neig", "center", "lflag", "lftend", "rflag", "rhtend"]);
let bound = |b: Option<f64>| match b {
+3
View File
@@ -280,6 +280,9 @@ fn solution(steps: &[&Step]) -> std::result::Result<u32, String> {
if let Some(s) = steps.iter().find(|s| matches!(s.procedure, Procedure::ExplicitDynamic(_))) {
return Err(format!("step `{}`: explicit dynamic steps (Nastran's linear solutions have none)", s.name));
}
if let Some(s) = steps.iter().find(|s| matches!(s.procedure, Procedure::NonlinearStatic(_))) {
return Err(format!("step `{}`: nonlinear static steps (Nastran's SOL 400; this writer writes SOL 101 and 103 so far)", s.name));
}
let statics = steps.iter().find(|s| matches!(s.procedure, Procedure::Static {}));
let modes = steps.iter().find(|s| matches!(s.procedure, Procedure::Frequency { .. }));
match (statics, modes) {
+42
View File
@@ -753,3 +753,45 @@ fn plasticity_is_written_and_failure_reported() {
let Err(Error::Unsupported { what, .. }) = abaqus::write(&failing) else { panic!("damage is not written") };
assert!(what.contains("material `Steel`: damage and erosion"), "{what}");
}
/// A nonlinear static step is a general step with NLGEOM, its increments and its line search; a
/// neo-Hookean material is then `*HYPERELASTIC` and a linear static step beside it is refused.
#[test]
fn nonlinear_steps_and_hyperelastic_materials_are_written() {
use fieldcraft_model::{Hyperelastic, NonlinearStatic};
let mut m = cantilever();
let controls = NonlinearStatic {
initial_increment: 0.05,
min_increment: 1e-4,
max_increment: 0.5,
max_increments: 40,
line_search: true,
..NonlinearStatic::default()
};
m.replace_step(step("Bend", Procedure::NonlinearStatic(controls.clone()), &["clamp"], &["end"])).unwrap();
let deck = abaqus::write(&m).unwrap();
let bend = block(&deck, "Bend");
assert_eq!(bend.first(), Some(&"*STEP, NAME=Bend, NLGEOM=YES, INC=40"), "{bend:?}");
let at = bend.iter().position(|l| *l == "*STATIC").unwrap();
assert_eq!(bend.get(at + 1..at + 4), Some(&["0.05, 1.0, 0.0001, 0.5", "*CONTROLS, PARAMETERS=LINE SEARCH", "5,"][..]), "{bend:?}");
assert!(deck.contains("*ELASTIC, TYPE=ISOTROPIC\n210000.0, 0.3\n"), "St Venant–Kirchhoff is the elastic material");
let small = NonlinearStatic { nonlinear_geometry: false, line_search: false, ..controls };
m.replace_step(step("Bend", Procedure::NonlinearStatic(small), &["clamp"], &["end"])).unwrap();
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::NeoHookean {}), ..steel() }).unwrap();
let deck = abaqus::write(&m).unwrap();
let bend = block(&deck, "Bend");
assert_eq!(bend.first(), Some(&"*STEP, NAME=Bend, NLGEOM=NO, INC=40"), "{bend:?}");
assert!(!bend.iter().any(|l| l.starts_with("*CONTROLS")), "{bend:?}");
// μ = E/(2(1 + ν)) = 80769.23…, κ = E/(3(1 − 2ν)) = 175000: C10 = μ/2, D1 = 2/κ.
let (c10, d1) = (210_000.0 / 2.6 / 2.0, 2.0 / 175_000.0);
let line = deck.split("*HYPERELASTIC, NEO HOOKE\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] - d1).abs() < 1e-9 * d1, "{line}");
assert!(!deck.contains("*ELASTIC"), "{deck}");
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");
}
+34
View File
@@ -737,3 +737,37 @@ fn plasticity_is_written_as_plastic_materials() {
let Err(Error::Unsupported { what, .. }) = lsdyna::write(&with(jc, None)) else { panic!() };
assert!(what.contains("Johnson–Cook"), "{what}");
}
/// A nonlinear static step: the ramped implicit run with full Newton and automatic increments;
/// what LS-DYNA cannot take is named.
#[test]
fn nonlinear_static_steps() {
let mut m = cantilever();
let controls =
NonlinearStatic { initial_increment: 0.05, min_increment: 1e-4, max_increment: 0.5, max_iterations: 12, ..NonlinearStatic::default() };
m.replace_step(step("Bend", Procedure::NonlinearStatic(controls.clone()), &["clamp"], &["tip_load"])).unwrap();
let deck = lsdyna::write(&m).unwrap().text;
let after = |keyword: &str| -> Vec<String> {
let mut lines = deck.lines().skip_while(|l| *l != keyword).skip(1).filter(|l| !l.starts_with('$'));
lines.next().unwrap().split_whitespace().map(str::to_string).collect()
};
let num = |s: &String| s.parse::<f64>().unwrap();
let general = after("*CONTROL_IMPLICIT_GENERAL");
assert_eq!((num(&general[0]), num(&general[1])), (1.0, 0.05), "{general:?}");
let solution = after("*CONTROL_IMPLICIT_SOLUTION");
assert_eq!(solution.iter().map(num).collect::<Vec<_>>(), [2.0, 1.0, 12.0], "{solution:?}");
let auto = after("*CONTROL_IMPLICIT_AUTO");
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}");
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic { nonlinear_geometry: false, ..controls }), &["clamp"], &["tip_load"]))
.unwrap();
unsupported(&m, "a nonlinear step without nonlinear geometry");
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic::default()), &["clamp"], &["tip_load"])).unwrap();
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::NeoHookean {}), ..steel() }).unwrap();
unsupported(&m, "hyperelastic material `Steel` in a nonlinear step");
// In a linear step the elastic constants are the whole of a hyperelastic material.
m.replace_step(step("Bend", Procedure::Static {}, &["clamp"], &["tip_load"])).unwrap();
assert!(lsdyna::write(&m).unwrap().text.contains("*MAT_ELASTIC_TITLE"));
}
+10
View File
@@ -782,3 +782,13 @@ fn plasticity_is_written_as_mats1() {
let Err(Error::Unsupported { what, .. }) = nastran::write(&m) else { panic!() };
assert!(what.contains("Johnson–Cook plasticity") && what.contains("damage and erosion"), "{what}");
}
#[test]
fn nonlinear_static_steps_are_refused_by_name() {
let mut m = cantilever();
let name = m.steps()[0].name.clone();
let mut s = m.steps()[0].clone();
s.set_procedure(Procedure::NonlinearStatic(fieldcraft_model::NonlinearStatic::default()));
m.replace_step(s).unwrap();
assert!(unsupported(&m).contains(&format!("step `{name}`: nonlinear static steps (Nastran's SOL 400")), "{}", unsupported(&m));
}
+4 -4
View File
@@ -301,7 +301,7 @@ pub static CATALOGUE: &[Capability] = &[
"Material",
Properties,
"Materials",
"Elastic (isotropic, orthotropic) material with density",
"Elastic (isotropic, orthotropic) material with density; neo-Hookean hyperelasticity for nonlinear steps",
&["model.material.edit", "model.material.delete"],
),
ready("model.material.plasticity", "Plasticity", Properties, "Materials", "Von Mises plasticity: bilinear, hardening table or Johnson–Cook"),
@@ -343,7 +343,7 @@ pub static CATALOGUE: &[Capability] = &[
"Step",
Steps,
"Steps",
"A linear static or frequency step with its output",
"A linear or nonlinear static, frequency, heat or explicit step with its output",
&["model.step.edit", "model.step.delete"],
),
ready("model.step.activate", "Activate", Steps, "Steps", "Choose which loads and boundary conditions act in a step"),
@@ -437,8 +437,8 @@ pub static CATALOGUE: &[Capability] = &[
"Static",
Solve,
"Implicit",
"Linear static (or coupled temperature–displacement) solve of a step with FieldCraft's own solver; heat transfer in the drop-down",
&["solve.heat"],
"Linear static (or coupled temperature–displacement) solve of a step with FieldCraft's own solver; nonlinear static and heat transfer in the drop-down",
&["solve.nonlinear", "solve.heat"],
),
ready("solve.frequency", "Frequency", Solve, "Implicit", "Natural frequencies and mode shapes of a frequency step"),
ready(
+2 -2
View File
@@ -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)} — 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"} (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": "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} (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)}"#,
kind: CommandKind::Model,
run: step_create,
};
+1
View File
@@ -95,6 +95,7 @@ pub static ALL: &[CommandSpec] = &[
deck::WRITE,
solve::RUN,
solve::STATIC,
solve::NONLINEAR,
solve::FREQUENCY,
solve::HEAT,
solve::EXPLICIT,
+9 -6
View File
@@ -43,7 +43,7 @@ pub const ANIMATE: CommandSpec = CommandSpec {
pub const FRAME: CommandSpec = CommandSpec {
id: "results.frame",
label: "Field Frame",
params: r#"{"step": "Impact", "frame": 3, "time": 0.002} — shows a field frame of a solved explicit step, by index from 0 or the frame nearest a time (neither: the last); the deformed shape and contours follow it"#,
params: r#"{"step": "Impact", "frame": 3, "time": 0.002} — shows a field frame of a solved explicit step, by index from 0 or the frame nearest a time (neither: the last), or of a nonlinear static step (one per converged increment, `time` its load factor; the deformed shape follows it, its contours show the final state); the deformed shape and contours follow it"#,
kind: CommandKind::Session,
run: frame,
};
@@ -79,21 +79,23 @@ fn solved_list(s: &Session) -> String {
enum Want {
Any,
Frequency,
Explicit,
/// Steps with frames: explicit dynamics and nonlinear static (one per converged increment).
Frames,
}
impl Want {
fn fits(self, s: &Session, step: &str) -> bool {
s.latest_result(step).is_some_and(|(r, _)| match (self, r.kind()) {
(Want::Any, _) | (Want::Frequency, Kind::Frequency(_)) | (Want::Explicit, Kind::Explicit(_)) => true,
(Want::Frequency | Want::Explicit, _) => false,
(Want::Any, _) | (Want::Frequency, Kind::Frequency(_)) | (Want::Frames, Kind::Explicit(_)) => true,
(Want::Frames, Kind::Static(r)) => r.nonlinear.is_some(),
(Want::Frequency | Want::Frames, _) => false,
})
}
fn name(self) -> &'static str {
match self {
Want::Any => "solved",
Want::Frequency => "frequency",
Want::Explicit => "explicit",
Want::Frames => "explicit or nonlinear static",
}
}
}
@@ -222,9 +224,10 @@ fn animate(s: &mut Session, p: &Value) -> Result<Value> {
}
fn frame(s: &mut Session, p: &Value) -> Result<Value> {
let step = step_for(s, p, Want::Explicit)?;
let step = step_for(s, p, Want::Frames)?;
let times: Vec<f64> = match s.latest_result(&step).map(|(r, _)| r.kind()) {
Some(Kind::Explicit(e)) => e.frames.iter().map(|f| f.time).collect(),
Some(Kind::Static(r)) => r.nonlinear.as_ref().map(|h| h.load_factors()).unwrap_or_default(),
_ => Vec::new(),
};
let last = times.len().checked_sub(1).ok_or_else(|| EngineError::Params(format!("step `{step}` has no field frames")))?;
+89 -1
View File
@@ -7,7 +7,8 @@ use fieldcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
use fieldcraft_model::FluidDomain;
use fieldcraft_parallel as parallel;
use fieldcraft_solver::{
FrequencyOptions, Limits, Location, MassKind, StaticResult, StepResult as ImplicitResult, solve_frequency, solve_heat, solve_static,
FrequencyOptions, Limits, Location, MassKind, StaticResult, StepResult as ImplicitResult, solve_frequency, solve_heat, solve_nonlinear,
solve_static,
};
use serde_json::{Value, json};
@@ -30,6 +31,14 @@ pub const STATIC: CommandSpec = CommandSpec {
run: run_static,
};
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"#,
kind: CommandKind::Model,
run: run_nonlinear,
};
pub const HEAT: CommandSpec = CommandSpec {
id: "solve.heat",
label: "Solve Heat Transfer Step",
@@ -117,6 +126,7 @@ fn limits(p: &Value) -> Result<Limits> {
max_dofs: cap(p, "max_dofs", Limits::MAX_DOFS)?,
max_factor_entries: cap(p, "max_factor_entries", Limits::MAX_FACTOR_ENTRIES)?,
max_lanczos_vectors: cap(p, "max_iterations", Limits::MAX_LANCZOS_VECTORS)?,
max_newton_iterations: cap(p, "max_newton_iterations", Limits::MAX_NEWTON_ITERATIONS)?,
})
}
@@ -142,6 +152,7 @@ fn run_all(s: &mut Session, _: &Value) -> Result<Value> {
Procedure::Frequency { .. } => run_frequency,
Procedure::ExplicitDynamic(_) => run_explicit,
Procedure::Fluid { .. } => run_fluid,
Procedure::NonlinearStatic(_) => run_nonlinear,
};
(st.name.clone(), st.procedure.name(), solve)
})
@@ -193,6 +204,36 @@ fn run_static(s: &mut Session, p: &Value) -> Result<Value> {
Ok(summary)
}
fn run_nonlinear(s: &mut Session, p: &Value) -> Result<Value> {
let limits = limits(p)?;
let step = opt::<String>(p, "step")?;
let model = &s.model;
let r = parallel::with_threads(threads(s, p)?, || solve_nonlinear(model, step.as_deref(), &limits))?;
let history = r.nonlinear.as_ref();
let summary = json!({
"step": r.step,
"dofs": r.stats.dofs,
"equations": r.stats.equations,
"factor_entries": r.stats.factor_entries,
"increments": history.map(|h| &h.increments),
"iterations": history.map(|h| h.iterations),
"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))),
"applied_total": r.applied_total,
"reaction_total": r.reaction_total,
"reference_points": r.reference_points.iter().map(|p| json!({
"name": p.name,
"label": p.label,
"displacement": p.displacement,
"reaction": p.reaction,
})).collect::<Vec<_>>(),
"warnings": r.warnings,
});
s.store_result(StepResult::Implicit(ImplicitResult::Static(r)));
Ok(summary)
}
fn run_heat(s: &mut Session, p: &Value) -> Result<Value> {
let limits = limits(p)?;
let step = opt::<String>(p, "step")?;
@@ -1354,4 +1395,51 @@ pub(crate) mod tests {
assert_eq!(fields, vec![crate::results::Field::Temperature, crate::results::Field::HeatFlux]);
assert!(crate::results::displacements(heat, crate::results::Pick::default()).is_err());
}
/// `solve.nonlinear`: a cantilever bent far by a dead tip load, with its increment history,
/// a frame per converged increment shown by `results.frame`, `solve.run` dispatching to it,
/// and the same bits on 1, 2 and 7 threads.
#[test]
fn nonlinear_steps_solve_with_their_history_and_frames() {
let solve = |threads: usize| -> (Value, u64, Session) {
let mut s = Session::default();
cantilever(&mut s);
let script = json!([
{"command": "model.section.edit", "params": {"name": "bar", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {}}},
{"command": "model.step.create", "params": {"name": "Large", "procedure": {"type": "nonlinear_static", "initial_increment": 0.25},
"boundary_conditions": ["clamp"], "loads": ["push"]}},
{"command": "model.load.edit", "params": {"name": "push", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, -15]}},
]);
s.run_script(&script).unwrap();
let r = s.execute("solve.nonlinear", &json!({"step": "Large", "threads": threads})).unwrap();
let hash = result_hash(&s, "Large");
(r, hash, s)
};
let (r, one, mut s) = solve(1);
for threads in [2, 7] {
assert_eq!(solve(threads).1, one, "{threads} threads");
}
let increments = r["increments"].as_array().unwrap();
assert!(increments.iter().any(|i| i["converged"] == true) && increments.last().unwrap()["load_factor"] == 1.0, "{r}");
assert!(increments[0]["iterations"].as_array().is_some_and(|it| !it.is_empty() && it[0]["force"].is_number()), "{r}");
let frames = r["frames"].as_array().unwrap().len();
assert_eq!(frames, 1 + increments.iter().filter(|i| i["converged"] == true).count());
// Far from linear: P L² / (E I) = 15·100/(1000/12) = 18, the tip swings well down and in.
let tip = r["max_displacement"]["value"].as_f64().unwrap();
assert!(tip > 8.0 && tip < 12.0, "{r}");
let view = s.execute("results.frame", &json!({"step": "Large", "frame": 1})).unwrap();
assert_eq!(view["view"]["frame"], 1, "{view}");
assert!(view["load_factor"].as_f64().is_some_and(|f| f > 0.0 && f < 1.0), "{view}");
assert!(s.execute("results.frame", &json!({"step": "Large", "frame": frames})).is_err());
let probe = s.execute("solve.probe", &json!({"step": "Large", "nodes": [11]})).unwrap();
assert!(probe["nodes"][0]["displacement"][2].as_f64().is_some_and(|w| w < -5.0), "{probe}");
// solve.run solves every step with its own solver; the hostile caps are refused.
assert!(s.execute("solve.nonlinear", &json!({"step": "Large", "max_newton_iterations": 0})).is_err());
assert!(s.execute("solve.nonlinear", &json!({"step": "Large", "max_newton_iterations": 1e300})).is_err());
let e = s.execute("solve.nonlinear", &json!({"step": "Large", "max_newton_iterations": 2})).unwrap_err().to_string();
assert!(e.contains("Newton iterations"), "{e}");
assert!(s.execute("solve.nonlinear", &json!({"step": "Bend"})).is_err(), "a linear step");
let all = s.execute("solve.run", &json!({})).unwrap();
assert_eq!(all["steps"].as_array().map(Vec::len), Some(3), "{all}");
}
}
+13 -2
View File
@@ -268,7 +268,12 @@ impl ResultsView {
/// of the picked mode or the picked frame's displacement, in [`StepResult::nodes`] order.
pub fn displacements(r: &StepResult, pick: Pick) -> Result<&[[f64; 3]]> {
match r.kind() {
Kind::Static(s) => Ok(&s.displacement),
Kind::Static(s) => match (pick.frame, &s.nonlinear) {
(Some(k), Some(h)) => h.frames.get(k).map(|f| f.displacement.as_slice()).ok_or_else(|| {
EngineError::Params(format!("step `{}` has frames 0 to {}; there is no frame {k}", s.step, h.frames.len().saturating_sub(1)))
}),
_ => Ok(&s.displacement),
},
Kind::Frequency(f) => pick
.mode
.checked_sub(1)
@@ -418,7 +423,13 @@ impl Session {
out["time"] = json!(t);
}
}
Kind::Static(_) => {}
Kind::Static(s) => {
if let Some(h) = &s.nonlinear {
out["frames"] = json!(h.load_factors());
let shown = v.frame.and_then(|k| h.frames.get(k)).or(h.frames.last());
out["load_factor"] = json!(shown.map(|f| f.load_factor));
}
}
}
out
}
+3
View File
@@ -139,6 +139,9 @@ fn invalid(m: impl Into<String>) -> Error {
fn material(model: &Model, name: &str) -> Result<Material> {
let m = model.material(name).ok_or_else(|| invalid(format!("material `{name}` is missing")))?;
let elastic = m.elastic.as_ref().ok_or_else(|| invalid(format!("material `{name}` has no elastic constants")))?;
if let Some(h) = m.hyperelastic {
return Err(Error::NotSupported(format!("{} hyperelastic material `{name}` in explicit dynamics", h.name())));
}
let density = m
.density
.filter(|d| *d > 0.0 && d.is_finite())
+356
View File
@@ -0,0 +1,356 @@
//! The material point of finite-strain analysis: from the deformation gradient F (and the
//! material's history) to the stress and its consistent tangent.
//!
//! **Stress measure.** A material returns the **Kirchhoff stress** τ = J σ (J = det F, σ the
//! Cauchy stress) and the **spatial tangent** c of the Lie derivative of τ, L_v τ = c : d, with
//! d the rate of deformation. Both are 6-vectors and 6 × 6 matrices in the order xx, yy, zz,
//! xy, xz, yz, c taking engineering shears like the small-strain elasticity matrices (so the
//! linear elasticity D is c at F = I). This is the total Lagrangian formulation written with
//! spatial quantities: τ = F S Fᵀ and c_ijkl = F_iI F_jJ F_kK F_lL C_IJKL push the second
//! Piola–Kirchhoff stress S and its material tangent C = ∂S/∂E forward, so the internal forces
//! ∫ Bₓᵀ τ dV₀ and the stiffness ∫ (Bₓᵀ c Bₓ + ∇ₓN·τ∇ₓN) dV₀, with the shape function gradients
//! of the current configuration and integrals over the reference volume, are exactly the total
//! Lagrangian ∫ B₀ᵀ S dV₀ and its linearization (J. Bonet, R. D. Wood, *Nonlinear Continuum
//! Mechanics for Finite Element Analysis*, 2nd ed., Cambridge 2008, §7.4 and §9.2; G. A.
//! Holzapfel, *Nonlinear Solid Mechanics*, Wiley 2000, §6.6). Finite-strain plasticity (the
//! exponential return map of J. C. Simo, CMAME 99:61–112, 1992) gives τ and c directly, which is
//! why the interface is spatial.
//!
//! **History.** A material keeps [`MaterialModel::history_len`] numbers per integration point.
//! An evaluation reads the history committed at the end of the last converged increment and
//! writes the updated one to a separate slice; the solver commits it when the increment
//! converges, so a rejected iteration or a cut-back increment leaves no trace. The elastic
//! materials here keep none.
//!
//! **Materials:**
//! - **St Venant–Kirchhoff**: S = D E with E = ½(FᵀF − I) the Green–Lagrange strain and D the
//! small-strain elasticity (isotropic or orthotropic in the global axes): an elastic material
//! in a nonlinear step. Exact for large rotations with small strains; it softens without bound
//! in large compression, so large strains want a hyperelastic model.
//! - **Neo-Hookean** (compressible, with the isochoric–volumetric split of P. J. Flory,
//! *Thermodynamic relations for high elastic materials*, Trans. Faraday Soc. 57:829–838, 1961):
//! W = μ/2 (Ī₁ − 3) + U(J), Ī₁ = J^(−2/3) tr b, U = κ/2 (J − 1)². Then
//! τ = μ dev b̄ + J U′ I with b̄ = J^(−2/3) b, and
//! c = ⅔ μ tr b̄ ℙ − ⅔ (τ_iso ⊗ I + I ⊗ τ_iso) + J (p̃ I ⊗ I − 2p 𝕀), with p = U′(J),
//! p̃ = p + J U″(J), 𝕀 the symmetric fourth-order identity and ℙ = 𝕀 − ⅓ I ⊗ I (Holzapfel 2000,
//! §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.
use fieldcraft_model::{Elastic, Hyperelastic};
use crate::dense::det3;
use crate::material::{Elasticity, elasticity, stress};
use crate::rotation::{IDENTITY, Mat3, mul, transpose, voigt};
use crate::{Error, Result};
/// A 6 × 6 tangent in Voigt order, engineering-shear columns.
pub type Tangent = [[f64; 6]; 6];
/// The (row, column) of each Voigt component.
pub const PAIRS: [(usize, usize); 6] = [(0, 0), (1, 1), (2, 2), (0, 1), (0, 2), (1, 2)];
/// The Kirchhoff stress and the spatial tangent of a material point.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Response {
pub stress: [f64; 6],
pub tangent: Tangent,
}
/// The constitutive model of a solid in a nonlinear step.
#[derive(Debug, Clone, PartialEq)]
pub enum MaterialModel {
StVenantKirchhoff(Box<Elasticity>),
NeoHookean { shear: f64, bulk: f64 },
}
/// The 6 × 6 matrix of the symmetric fourth-order identity 𝕀 (engineering-shear columns).
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 };
}
i4
}
/// The matrix T with (F A Fᵀ) = T a for a symmetric A of Voigt vector a (tensor components):
/// T[(ij)][(IJ)] = F_iI F_jJ + F_iJ F_jI for I ≠ J and F_iI F_jI for I = J. A tangent pushes
/// forward as T C Tᵀ.
pub fn push_forward(f: &Mat3) -> Tangent {
let mut t = [[0.0; 6]; 6];
for (a, &(i, j)) in PAIRS.iter().enumerate() {
for (b, &(ii, jj)) in PAIRS.iter().enumerate() {
t[a][b] = if ii == jj { f[i][ii] * f[j][ii] } else { f[i][ii] * f[j][jj] + f[i][jj] * f[j][ii] };
}
}
t
}
/// T C Tᵀ.
fn sandwich(t: &Tangent, c: &Tangent) -> Tangent {
let mut tc = [[0.0; 6]; 6];
for a in 0..6 {
for b in 0..6 {
tc[a][b] = (0..6).map(|k| t[a][k] * c[k][b]).sum();
}
}
let mut out = [[0.0; 6]; 6];
for a in 0..6 {
for b in 0..6 {
out[a][b] = (0..6).map(|k| tc[a][k] * t[b][k]).sum();
}
}
out
}
/// det F, refusing an inverted or collapsed point.
fn jacobian(f: &Mat3) -> Result<f64> {
let j = det3(f);
if j > 0.0 && j.is_finite() { Ok(j) } else { Err(Error::Degenerate(format!("the material is inverted (J = det F = {j:.3e} is not positive)"))) }
}
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
/// 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")?;
let (shear, bulk) = Hyperelastic::moduli(young, poisson);
Ok(MaterialModel::NeoHookean { 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.
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] },
}
}
pub fn name(&self) -> &'static str {
match self {
MaterialModel::StVenantKirchhoff(_) => "St Venant–Kirchhoff",
MaterialModel::NeoHookean { .. } => "neo-Hookean",
}
}
/// History variables per integration point.
pub fn history_len(&self) -> usize {
0
}
/// Whether the strain energy splits into isochoric and volumetric parts (what mixed
/// elements need).
pub fn is_decoupled(&self) -> bool {
matches!(self, MaterialModel::NeoHookean { .. })
}
/// The Kirchhoff stress and spatial tangent at deformation gradient `f`, from the history
/// `committed` (written, updated, to `updated`).
pub fn response(&self, f: &Mat3, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
updated.iter_mut().zip(committed).for_each(|(u, c)| *u = *c);
let j = jacobian(f)?;
match self {
MaterialModel::StVenantKirchhoff(d) => {
let c = mul(&transpose(f), f);
let e = [0.5 * (c[0][0] - 1.0), 0.5 * (c[1][1] - 1.0), 0.5 * (c[2][2] - 1.0), c[0][1], c[0][2], c[1][2]];
let s = stress(d, &e);
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()))?;
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())));
};
let j = jacobian(f)?;
let scale = j.powf(-2.0 / 3.0);
let b = mul(f, &transpose(f));
let bbar = voigt(&b).map(|v| scale * v);
let tr = bbar[0] + bbar[1] + bbar[2];
let mut tau = bbar.map(|v| mu * v);
for v in tau.iter_mut().take(3) {
*v -= mu * tr / 3.0;
}
let i4 = identity4();
let mut c = [[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 });
let projector = i4[a][b] - ia * ib / 3.0;
c[a][b] = 2.0 / 3.0 * mu * tr * projector - 2.0 / 3.0 * (tau[a] * ib + ia * tau[b]);
}
}
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)),
MaterialModel::StVenantKirchhoff(_) => None,
}
}
}
/// An isochoric response with a pressure p acting on dilatation J: τ + J p I, and the tangent
/// c + J (p̃ I ⊗ I − 2p 𝕀) with p̃ = p + `j_dp` (J dp/dJ, zero when p is held).
pub fn add_pressure(iso: &Response, j: f64, p: f64, j_dp: f64) -> Response {
let mut out = *iso;
let i4 = identity4();
for a in 0..3 {
out.stress[a] += j * p;
}
for a in 0..6 {
for b in 0..6 {
let ii = if a < 3 && b < 3 { 1.0 } else { 0.0 };
out.tangent[a][b] += j * ((p + j_dp) * ii - 2.0 * p * i4[a][b]);
}
}
out
}
/// The logarithmic (Hencky) strain ln V = ½ ln b of a deformation gradient, as a strain 6-vector
/// with engineering shears; `None` when F is not finite or inverted.
pub fn logarithmic_strain(f: &Mat3) -> Option<[f64; 6]> {
let (l, v) = crate::dense::symmetric_eigen(&mul(f, &transpose(f)))?;
if !l.iter().all(|x| *x > 0.0) {
return None;
}
let ln = l.map(|x| 0.5 * x.ln());
let e = |i: usize, j: usize| (0..3).map(|k| ln[k] * v[i][k] * v[j][k]).sum::<f64>();
Some([e(0, 0), e(1, 1), e(2, 2), 2.0 * e(0, 1), 2.0 * e(0, 2), 2.0 * e(1, 2)])
}
/// The deformation gradient F = I + Σ u_a ⊗ ∇₀N_a of nodal displacements and reference
/// gradients.
pub fn deformation_gradient(u: &[[f64; 3]], gradients: &[[f64; 3]]) -> Mat3 {
let mut f = IDENTITY;
for (ua, g) in u.iter().zip(gradients) {
for i in 0..3 {
for jj in 0..3 {
f[i][jj] += ua[i] * g[jj];
}
}
}
f
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dense::inverse3;
use crate::material::isotropic;
fn deformation() -> Mat3 {
[[1.2, 0.15, -0.05], [-0.1, 0.9, 0.2], [0.07, -0.12, 1.05]]
}
/// S = F⁻¹ τ F⁻ᵀ.
fn pull_back_stress(f: &Mat3, tau: &[f64; 6]) -> [f64; 6] {
crate::rotation::rotate(&inverse3(f).unwrap().0, tau)
}
/// 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 (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()));
let h = 1e-6;
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]));
pull_back_stress(&fp, &m.response(&fp, &[], &mut []).unwrap().stress)
};
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();
assert!((numeric - analytic).abs() < 1e-6 * scale, "{} {k} {a}: {numeric} vs {analytic}", m.name());
}
}
}
#[test]
fn tangents_are_consistent_with_the_stress() {
let d = isotropic(1000.0, 0.3).unwrap();
for m in [MaterialModel::StVenantKirchhoff(Box::new(d)), MaterialModel::NeoHookean { shear: 300.0, bulk: 5000.0 }] {
check_tangent(&m, &deformation());
check_tangent(&m, &IDENTITY);
// Symmetric (hyperelastic): major symmetry of c.
let c = m.response(&deformation(), &[], &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}");
}
}
}
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());
}
#[test]
fn small_strain_limit_is_linear_elasticity_and_rotations_do_not_stress() {
let (e, nu) = (1000.0, 0.3);
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;
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());
}
}
// A rigid rotation (90° about an inclined axis) leaves the stress at zero.
let (s, cth) = (1.0f64, 0.0f64);
let axis = [1.0 / 3f64.sqrt(); 3];
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;
assert!(tau.iter().all(|v| v.abs() < 1e-12 * e), "{}: {tau:?}", m.name());
}
}
#[test]
fn uniaxial_stretch_of_neo_hookean_and_log_strain() {
// Stretch λ along x with the lateral stretch μ chosen so that σ_yy = 0: check σ_xx
// against W directly, σ = (μ/J^(5/3)) (λ² − (λ² + 2 l²)/3) + κ (J − 1).
let (mu, kappa) = (300.0, 5000.0);
let m = MaterialModel::NeoHookean { shear: mu, bulk: kappa };
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 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());
}
}
+76
View File
@@ -113,6 +113,57 @@ pub fn positive_inverse(j: &[[f64; 3]; 3]) -> Option<([[f64; 3]; 3], f64)> {
(det > 1e-12 * norms).then_some((inv, det))
}
/// The eigenvalues and unit eigenvectors (the columns of the matrix returned) of a symmetric
/// 3 × 3 matrix, by cyclic Jacobi rotations (G. H. Golub, C. F. Van Loan, *Matrix
/// Computations*, 4th ed., Johns Hopkins 2013, §8.5): each rotation zeroes one off-diagonal
/// entry with the stable choice of angle, and the sweeps stop when the off-diagonal part is
/// round-off relative to the diagonal (at most 32 sweeps; three or four suffice). Accurate to a
/// few ulps of the largest eigenvalue; `None` for non-finite input.
pub fn symmetric_eigen(a: &[[f64; 3]; 3]) -> Option<([f64; 3], [[f64; 3]; 3])> {
if a.iter().flatten().any(|v| !v.is_finite()) {
return None;
}
let mut m = [[a[0][0], 0.5 * (a[0][1] + a[1][0]), 0.5 * (a[0][2] + a[2][0])], [0.0; 3], [0.0; 3]];
m[1] = [m[0][1], a[1][1], 0.5 * (a[1][2] + a[2][1])];
m[2] = [m[0][2], m[1][2], a[2][2]];
let mut v = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
for _ in 0..32 {
let off = m[0][1] * m[0][1] + m[0][2] * m[0][2] + m[1][2] * m[1][2];
let diag = m[0][0] * m[0][0] + m[1][1] * m[1][1] + m[2][2] * m[2][2];
if off <= 1e-36 * diag || off == 0.0 {
break;
}
for (p, q) in [(0, 1), (0, 2), (1, 2)] {
let apq = m[p][q];
if apq == 0.0 {
continue;
}
// tan of the rotation angle, the smaller root of t² + 2θt − 1 = 0.
let theta = (m[q][q] - m[p][p]) / (2.0 * apq);
let sign = if theta >= 0.0 { 1.0 } else { -1.0 };
let t = sign / (theta.abs() + (theta * theta + 1.0).sqrt());
let c = 1.0 / (t * t + 1.0).sqrt();
let s = t * c;
for k in 0..3 {
let (mkp, mkq) = (m[k][p], m[k][q]);
m[k][p] = c * mkp - s * mkq;
m[k][q] = s * mkp + c * mkq;
}
for k in 0..3 {
let (mpk, mqk) = (m[p][k], m[q][k]);
m[p][k] = c * mpk - s * mqk;
m[q][k] = s * mpk + c * mqk;
}
for row in &mut v {
let (vp, vq) = (row[p], row[q]);
row[p] = c * vp - s * vq;
row[q] = s * vp + c * vq;
}
}
}
Some(([m[0][0], m[1][1], m[2][2]], v))
}
pub fn det3(m: &[[f64; 3]; 3]) -> f64 {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
@@ -143,4 +194,29 @@ mod tests {
}
assert!(inverse3(&[[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [0.0, 0.0, 1.0]]).is_none());
}
#[test]
fn symmetric_eigen_decomposes() {
for a in [
[[4.0, 1.0, -2.0], [1.0, 3.0, 0.5], [-2.0, 0.5, 1.0]],
[[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 5.0]],
[[1.0, 1e-9, 0.0], [1e-9, 1.0, 0.0], [0.0, 0.0, 1.0]],
[[1e6, 3.0, 2.0], [3.0, 1e-3, 0.0], [2.0, 0.0, -7.0]],
] {
let (l, v) = symmetric_eigen(&a).unwrap();
let scale = l.iter().fold(0.0f64, |m, x| m.max(x.abs()));
for k in 0..3 {
// A v_k = λ_k v_k and the v_k are orthonormal.
for i in 0..3 {
let av: f64 = (0..3).map(|j| a[i][j] * v[j][k]).sum();
assert!((av - l[k] * v[i][k]).abs() < 1e-12 * scale, "{a:?}: {l:?}");
}
for m in 0..3 {
let d: f64 = (0..3).map(|i| v[i][k] * v[i][m]).sum();
assert!((d - if k == m { 1.0 } else { 0.0 }).abs() < 1e-13);
}
}
}
assert!(symmetric_eigen(&[[f64::NAN, 0.0, 0.0], [0.0; 3], [0.0; 3]]).is_none());
}
}
+152
View File
@@ -21,6 +21,113 @@ pub fn pressure(xyz: &[[f64; 3]], magnitude: f64) -> Result<Vec<[f64; 3]>> {
Ok(normal_weights(xyz)?.into_iter().map(|w| w.map(|c| -magnitude * c)).collect())
}
/// A pressure that follows the deformed face (positive pushes in): the nodal forces
/// fᵢ = −p ∫ Nᵢ (x,ξ × x,η) dξ dη on the current positions x = X + u (reference coordinates `xyz`,
/// displacements `u`, in the face's node order), and the load stiffness K = −∂f/∂u, 3n × 3n
/// row-major, from Δ(x,ξ × x,η) = [x,ξ]× Δx,η − [x,η]× Δx,ξ:
/// K_ab = p ∫ N_a (N_b,η [x,ξ]× − N_b,ξ [x,η]×) dξ dη (K. Schweizerhof, E. Ramm, *Displacement
/// dependent pressure loads in nonlinear finite element analyses*, Comput. Struct.
/// 18(6):1099–1114, 1984). K is not symmetric on its own; over a closed surface, or one whose
/// boundary is held, the assembled skew parts cancel. Same rule as [`pressure`], so at u = 0 the
/// forces are its forces.
pub fn follower_pressure(xyz: &[[f64; 3]], u: &[[f64; 3]], magnitude: f64) -> Result<(Vec<[f64; 3]>, Vec<f64>)> {
let n = xyz.len();
if u.len() != n {
return Err(Error::Invalid(format!("{} displacements for a {n}-node face", u.len())));
}
let x: Vec<[f64; 3]> = xyz.iter().zip(u).map(|(a, b)| [a[0] + b[0], a[1] + b[1], a[2] + b[2]]).collect();
if x.iter().flatten().any(|c| !c.is_finite()) || !magnitude.is_finite() {
return Err(Error::Invalid("face positions and pressure must be finite".into()));
}
let points = fieldcraft_model::nodal::face_points(n).map_err(|e| Error::Invalid(e.to_string()))?;
let mut f = vec![[0.0; 3]; n];
let mut k = vec![0.0; 9 * n * n];
for q in &points {
let (mut tx, mut te) = ([0.0; 3], [0.0; 3]);
for ((p, dx), de) in x.iter().zip(&q.dxi).zip(&q.deta) {
for c in 0..3 {
tx[c] += dx * p[c];
te[c] += de * p[c];
}
}
let normal = [tx[1] * te[2] - tx[2] * te[1], tx[2] * te[0] - tx[0] * te[2], tx[0] * te[1] - tx[1] * te[0]];
let (sx, se) = (crate::rotation::skew(&tx), crate::rotation::skew(&te));
for (a, na) in q.n.iter().enumerate() {
if let Some(fa) = f.get_mut(a) {
for c in 0..3 {
fa[c] -= magnitude * q.weight * na * normal[c];
}
}
for (b, (db_xi, db_eta)) in q.dxi.iter().zip(&q.deta).enumerate() {
for r in 0..3 {
for c in 0..3 {
let v = magnitude * q.weight * na * (db_eta * sx[r][c] - db_xi * se[r][c]);
if let Some(e) = k.get_mut((3 * a + r) * 3 * n + 3 * b + c) {
*e += v;
}
}
}
}
}
}
Ok((f, k))
}
/// A pressure that follows a deformed edge of a 2D element (positive pushes into a
/// counter-clockwise element): the nodal forces (x, y) fᵢ = −p ∫ Nᵢ e (t_y, −t_x) ds on the
/// current positions, with t the tangent and e the out-of-plane extent (a thickness, or 2π r at
/// the current radius when revolved), and the load stiffness K = −∂f/∂u, 2n × 2n row-major (the
/// revolved extent depends on the radial motion too).
pub fn follower_edge_pressure(
xyz: &[[f64; 3]],
u: &[[f64; 2]],
extent: fieldcraft_model::Extent,
magnitude: f64,
) -> Result<(Vec<[f64; 2]>, Vec<f64>)> {
let n = xyz.len();
if u.len() != n {
return Err(Error::Invalid(format!("{} displacements for a {n}-node edge", u.len())));
}
let x: Vec<[f64; 2]> = xyz.iter().zip(u).map(|(a, b)| [a[0] + b[0], a[1] + b[1]]).collect();
if x.iter().flatten().any(|c| !c.is_finite()) || !magnitude.is_finite() {
return Err(Error::Invalid("edge positions and pressure must be finite".into()));
}
let revolved = matches!(extent, fieldcraft_model::Extent::Revolved);
let points = fieldcraft_model::nodal::edge_points(n).map_err(|e| Error::Invalid(e.to_string()))?;
let mut f = vec![[0.0; 2]; n];
let mut k = vec![0.0; 4 * n * n];
let two_pi = 2.0 * std::f64::consts::PI;
for q in &points {
let (mut t, mut pos) = ([0.0; 2], [0.0; 2]);
for ((p, dn), ni) in x.iter().zip(&q.dxi).zip(&q.n) {
for c in 0..2 {
t[c] += dn * p[c];
pos[c] += ni * p[c];
}
}
let e = extent.at([pos[0], pos[1], 0.0]);
let w = magnitude * q.weight;
for (a, na) in q.n.iter().enumerate() {
if let Some(fa) = f.get_mut(a) {
fa[0] -= w * na * e * t[1];
fa[1] += w * na * e * t[0];
}
for (b, (nb, db)) in q.n.iter().zip(&q.dxi).enumerate() {
// ∂e/∂u_b (radial) = 2π N_b when revolved.
let de = if revolved { two_pi * nb } else { 0.0 };
// K = −∂f/∂u.
let entries = [(0, 0, w * na * de * t[1]), (0, 1, w * na * e * db), (1, 0, -w * na * (e * db + de * t[0])), (1, 1, 0.0)];
for (r, c, v) in entries {
if let Some(x) = k.get_mut((2 * a + r) * 2 * n + 2 * b + c) {
*x += v;
}
}
}
}
}
Ok((f, k))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -57,6 +164,51 @@ mod tests {
assert!(pressure(&tri[..2], 1.0).is_err());
}
/// Central differences of the follower forces against the load stiffness, on a distorted
/// quadratic face and a revolved quadratic edge; at u = 0 the forces are the dead ones.
#[test]
fn follower_pressure_stiffness_is_the_derivative_of_its_forces() {
let face = [[0., 0., 0.], [2., 0., 0.1], [2.2, 3., 0.], [0., 3., -0.2], [1., -0.1, 0.], [2.1, 1.5, 0.], [1., 3.1, 0.1], [0.1, 1.5, 0.]];
let u: Vec<[f64; 3]> = (0..8).map(|i| [0.1 * (i as f64).sin(), 0.05 * (i as f64).cos(), 0.2 * ((i * i) as f64).sin()]).collect();
let (f0, _) = follower_pressure(&face, &[[0.0; 3]; 8], 3.0).unwrap();
let dead = pressure(&face, 3.0).unwrap();
assert!(f0.iter().flatten().zip(dead.iter().flatten()).all(|(a, b)| (a - b).abs() < 1e-13), "{f0:?}");
let (_, k) = follower_pressure(&face, &u, 3.0).unwrap();
let h = 1e-6;
for j in 0..24 {
let shifted = |s: f64| {
let mut v = u.clone();
v[j / 3][j % 3] += s;
follower_pressure(&face, &v, 3.0).unwrap().0
};
let (p, m) = (shifted(h), shifted(-h));
for i in 0..24 {
let numeric = -(p[i / 3][i % 3] - m[i / 3][i % 3]) / (2.0 * h);
assert!((numeric - k[i * 24 + j]).abs() < 1e-7, "{i} {j}: {numeric} vs {}", k[i * 24 + j]);
}
}
let edge = [[1.0, 0.0, 0.0], [1.5, 2.0, 0.0], [1.3, 1.0, 0.0]];
let ue = [[0.1, 0.05], [-0.2, 0.1], [0.05, -0.03]];
for extent in [fieldcraft_model::Extent::Revolved, fieldcraft_model::Extent::Thickness(2.0)] {
let (f0, _) = follower_edge_pressure(&edge, &[[0.0; 2]; 3], extent, 2.0).unwrap();
let dead = fieldcraft_model::nodal::edge_pressure(&edge, extent, 2.0).unwrap();
assert!(f0.iter().zip(&dead).all(|(a, b)| (a[0] - b[0]).abs() < 1e-12 && (a[1] - b[1]).abs() < 1e-12));
let (_, k) = follower_edge_pressure(&edge, &ue, extent, 2.0).unwrap();
for j in 0..6 {
let shifted = |s: f64| {
let mut v = ue;
v[j / 2][j % 2] += s;
follower_edge_pressure(&edge, &v, extent, 2.0).unwrap().0
};
let (p, m) = (shifted(h), shifted(-h));
for i in 0..6 {
let numeric = -(p[i / 2][i % 2] - m[i / 2][i % 2]) / (2.0 * h);
assert!((numeric - k[i * 6 + j]).abs() < 1e-7, "{extent:?} {i} {j}: {numeric} vs {}", k[i * 6 + j]);
}
}
}
}
#[test]
fn pressure_on_a_curved_face_sums_to_the_projected_area() {
// A quadratic face on a quarter cylinder r = 2, z in 0..1, normal outward: the total
+561
View File
@@ -0,0 +1,561 @@
//! 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.
//!
//! **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
//! Kirchhoff stress τ and its spatial tangent c ([`crate::constitutive`]). With the shape
//! function gradients of the current configuration ∇ₓN_a = F⁻ᵀ ∇₀N_a,
//! - internal forces f_a = ∫ Bₓ,aᵀ τ dV₀,
//! - tangent K_ab = ∫ (Bₓ,aᵀ c Bₓ,b + (∇ₓN_a · τ ∇ₓN_b) I) dV₀ (material plus geometric, or
//! initial-stress, stiffness),
//!
//! with Bₓ the small-strain operator of the current gradients. This is the push-forward of the
//! textbook ∫ B₀ᵀ S dV₀ and ∫ (B₀ᵀ C B₀ + ∇₀N·S∇₀N I) dV₀, identical term by term (Bonet & Wood
//! 2008, §9.2–9.4; K.-J. Bathe, *Finite Element Procedures*, 2nd ed. 2014, §6.3). A rigid motion
//! leaves τ at zero exactly, whatever its size.
//!
//! **Hybrid (mixed u–p) solids** with a decoupled material W = W̄(C̄) + U(J): the three-field
//! formulation of J. C. Simo, R. L. Taylor, K. S. Pister, *Variational and projection methods
//! for the volume constraint in finite deformation elasto-plasticity*, CMAME 51:177–208, 1985,
//! with the element's discontinuous pressure functions N_p (the linear hybrid elements' fields,
//! [`crate::hybrid`]): the dilatation θ = N_p·θ̂ is the projection of J, M θ̂ = ∫ N_p J dV₀ with
//! M = ∫ N_p N_pᵀ dV₀; the pressure p = N_p·p̂ the projection of U′(θ), M p̂ = ∫ N_p U′(θ) dV₀.
//! Then τ = τ_iso(F) + J p I, and with G = ∫ N_p J (∇ₓN)ᵀ dV₀ and H = ∫ N_p U″(θ) N_pᵀ dV₀,
//! K = ∫ (Bₓᵀ c* Bₓ + ∇ₓN·τ∇ₓN I) dV₀ + Gᵀ M⁻¹ H M⁻¹ G with c* = c_iso + J p (I ⊗ I − 2𝕀),
//! 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.
//!
//! **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
//! radial motion; Bonet & Wood 2008, §9.5). Plane stress (whose thickness change needs a local
//! iteration for σ_zz = 0) is refused.
//!
//! **Incompatible modes** (hex8, quad4) are refused in nonlinear steps: their finite-strain form
//! (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.
use crate::constitutive::{MaterialModel, Response, add_pressure, logarithmic_strain};
use crate::dense::{Cholesky, Matrix, inverse3};
use crate::element::{Formulation, PointStress, Solid, accumulate_symmetric, b_block};
use crate::hybrid::pressure_basis;
use crate::plane::{Plane, Theory};
use crate::rotation::{IDENTITY, Mat3};
use crate::{Error, Result};
/// Internal forces (element degrees of freedom, node by node) and, when asked for, the tangent.
#[derive(Debug, Clone, PartialEq)]
pub struct Finite {
pub force: Vec<f64>,
pub tangent: Option<Matrix>,
}
/// What one integration point gives the element.
struct PointKinematics {
weight: f64,
f: Mat3,
j: f64,
/// ∇ₓN of every node.
gradients: Vec<[f64; 3]>,
/// Reference position.
x0: [f64; 3],
}
/// The checked history slice of one integration point.
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) {
for (a, g) in gradients.iter().enumerate() {
let b = b_block(g);
for c in 0..3 {
if let Some(x) = force.get_mut(3 * a + c) {
*x += w * (0..6).map(|s| b[s][c] * r.stress[s]).sum::<f64>();
}
}
}
let Some(k) = k else { return };
accumulate_symmetric(&mut k.data, gradients, &r.tangent, w);
let tau = crate::rotation::tensor(&r.stress);
let n = k.n;
for (a, ga) in gradients.iter().enumerate() {
let tg: [f64; 3] = std::array::from_fn(|i| (0..3).map(|jj| tau[i][jj] * ga[jj]).sum());
for (b, gb) in gradients.iter().enumerate() {
let s = w * (tg[0] * gb[0] + tg[1] * gb[1] + tg[2] * gb[2]);
for c in 0..3 {
if let Some(x) = k.data.get_mut((3 * a + c) * n + 3 * b + c) {
*x += s;
}
}
}
}
}
fn inverted(kind: &str, j: f64) -> Error {
Error::Degenerate(format!("a {kind} element is inverted at an integration point (J = det F = {j:.3e})"))
}
/// One displacement vector per node, all finite.
fn check_displacements<const N: usize>(u: &[[f64; N]], nodes: usize) -> Result<()> {
if u.len() != nodes {
return Err(Error::Invalid(format!("{} displacements for a {nodes}-node element", u.len())));
}
if u.iter().flatten().any(|v| !v.is_finite()) {
return Err(Error::Invalid("displacements must be finite".into()));
}
Ok(())
}
/// Check the element's history slices against the material and its points.
fn check_history(m: &MaterialModel, points: usize, committed: &[f64], updated: &[f64]) -> Result<usize> {
let len = m.history_len();
if committed.len() != points * len || updated.len() != points * len {
return Err(Error::Invalid(format!("{} history values for {points} points of {len}", committed.len())));
}
Ok(len)
}
impl Solid<'_> {
/// Refuse what has no finite-strain form here.
fn check_finite(&self, m: &MaterialModel, u: &[[f64; 3]]) -> 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::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()
))),
_ => Ok(()),
}
}
/// F, J and the current gradients at every stiffness point.
fn kinematics(&self, u: &[[f64; 3]]) -> Result<Vec<PointKinematics>> {
let n = self.ty.nodes;
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 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()) });
}
Ok(out)
}
/// The effective response (Kirchhoff stress and tangent) at every point, the forces, and the
/// tangent when `tangent`.
fn finite_points(
&self,
m: &MaterialModel,
u: &[[f64; 3]],
committed: &[f64],
updated: &mut [f64],
tangent: bool,
) -> Result<(Finite, Vec<(PointKinematics, Response)>)> {
self.check_finite(m, u)?;
let points = self.kinematics(u)?;
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())?
} 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
};
for (p, r) in points.iter().zip(&responses) {
add_point(&mut force, k.as_mut(), &p.gradients, r, p.weight);
}
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>> {
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);
let mut theta = vec![0.0; np];
for (p, nb) in points.iter().zip(&basis) {
for a in 0..np {
theta[a] += p.weight * nb[a] * p.j;
for b in 0..np {
mm.add(a, b, p.weight * nb[a] * nb[b]);
}
}
}
let chol = Cholesky::new(&mm)?;
chol.solve(&mut theta);
let mut pressure = vec![0.0; np];
let mut h = Matrix::zeros(np);
for (p, nb) in points.iter().zip(&basis) {
let th: f64 = (0..np).map(|a| nb[a] * theta[a]).sum();
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()))?;
for a in 0..np {
pressure[a] += p.weight * nb[a] * dp;
for b in 0..np {
h.add(a, b, p.weight * ddp * nb[a] * nb[b]);
}
}
}
chol.solve(&mut pressure);
let mut out = Vec::with_capacity(points.len());
for (p, nb) in points.iter().zip(&basis) {
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));
}
if let Some(k) = k {
// A = M⁻¹ G, np × ndof; K += Aᵀ H A.
let ndof = k.n;
let mut a_cols = vec![vec![0.0; np]; ndof];
for (p, nb) in points.iter().zip(&basis) {
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];
}
}
}
}
}
for col in &mut a_cols {
chol.solve(col);
}
let ha: Vec<Vec<f64>> = a_cols.iter().map(|col| h.mul_vec(col)).collect();
for i in 0..ndof {
for jj in i..ndof {
let v: f64 = a_cols.get(i).zip(ha.get(jj)).map_or(0.0, |(x, y)| x.iter().zip(y).map(|(p, q)| p * q).sum());
k.add(i, jj, v);
if i != jj {
k.add(jj, i, v);
}
}
}
}
Ok(out)
}
/// Internal forces and (with `tangent`) the tangent stiffness at nodal displacements `u`,
/// 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)
}
/// 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>> {
let mut scratch = committed.to_vec();
let (_, points) = self.finite_points(m, u, 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))?;
Ok(PointStress { xyz: p.x0, strain, stress: r.stress.map(|v| v / p.j) })
})
.collect()
}
}
impl Plane<'_> {
fn check_finite(&self, u: &[[f64; 2]]) -> 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(),
));
}
if self.formulation == Formulation::IncompatibleModes {
return Err(Error::NotSupported(format!(
"2D {} incompatible modes in nonlinear steps (their finite-strain form, enhanced assumed strain, is not implemented; use full integration or quad8)",
self.ty.kind.name()
)));
}
Ok(())
}
/// Per point: (dv, F, J, current columns (∂N/∂x, ∂N/∂y, N/r), reference position, response).
#[allow(clippy::type_complexity)]
fn finite_points(
&self,
m: &MaterialModel,
u: &[[f64; 2]],
committed: &[f64],
updated: &mut [f64],
tangent: bool,
) -> Result<(Finite, Vec<(Mat3, f64, [f64; 3], Response)>)> {
self.check_finite(u)?;
let n = self.ty.nodes;
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 mut out = Vec::with_capacity(rule.len());
for (i, p) in rule.iter().enumerate() {
let at = self.at(p)?;
let cols = at.g.get(..n).unwrap_or_default();
let mut f = IDENTITY;
for (ua, g) in u.iter().zip(cols) {
for r in 0..2 {
for c in 0..2 {
f[r][c] += ua[r] * g[c];
}
}
if axisymmetric {
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 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]);
}
if let Some(x) = force.get_mut(2 * a + 1) {
*x += at.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, &current, &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);
}
}
}
out.push((f, j, at.x, r));
}
Ok((Finite { force, tangent: k }, out))
}
/// 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)
}
/// The Cauchy stress and logarithmic 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>> {
let mut scratch = committed.to_vec();
let (_, points) = self.finite_points(m, u, 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) })
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::material::isotropic;
use fieldcraft_model::ElementKind;
fn hex20() -> Vec<[f64; 3]> {
let mut x: Vec<[f64; 3]> = crate::shape::HEX_CORNERS.iter().map(|c| [c[0] + 1.0, 0.5 * c[1] + 1.0, 0.8 * c[2]]).collect();
let edges = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)];
for (a, b) in edges {
x.push(std::array::from_fn(|k| 0.5 * (x[a][k] + x[b][k])));
}
// Distort a corner and a mid-side node.
x[6][0] += 0.2;
x[9][2] += 0.05;
x
}
fn tet10() -> Vec<[f64; 3]> {
let c = [[0.0, 0.0, 0.0], [1.2, 0.1, 0.0], [0.2, 1.0, 0.1], [0.1, 0.2, 0.9]];
let mut x = c.to_vec();
for (a, b) in [(0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)] {
x.push(std::array::from_fn(|k| 0.5 * (c[a][k] + c[b][k])));
}
x
}
fn displacement(n: usize, scale: f64) -> Vec<[f64; 3]> {
(0..n).map(|i| [scale * (i as f64 * 1.3).sin(), scale * (i as f64 * 0.7).cos(), scale * (i as f64 * 2.1).sin()]).collect()
}
/// 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();
let scale = k.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
let n = 3 * u.len();
let h = 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
};
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));
}
}
for i in 0..n {
for j in 0..n {
assert!((k.get(i, j) - k.get(j, i)).abs() <= 1e-12 * scale, "symmetric");
}
}
}
#[test]
fn solid_tangents_are_the_derivative_of_the_forces() {
let d = isotropic(1000.0, 0.3).unwrap();
let neo = MaterialModel::NeoHookean { shear: 300.0, bulk: 3000.0 };
let svk = MaterialModel::StVenantKirchhoff(Box::new(d));
let (x20, x10) = (hex20(), tet10());
let x8: Vec<[f64; 3]> = x20[..8].to_vec();
let x6 = vec![[0., 0., 0.], [1., 0., 0.], [0., 1., 0.1], [0., 0., 1.], [1.1, 0., 1.], [0., 1., 1.2]];
let x4 = vec![x10[0], x10[1], x10[2], x10[3]];
for (kind, x) in
[(ElementKind::Tet4, &x4), (ElementKind::Tet10, &x10), (ElementKind::Wedge6, &x6), (ElementKind::Hex8, &x8), (ElementKind::Hex20, &x20)]
{
for m in [&svk, &neo] {
let solid = Solid::new(kind, x, Formulation::Full).unwrap();
check_tangent(&solid, m, &displacement(x.len(), 0.08));
}
}
let reduced = Solid::new(ElementKind::Hex20, &x20, Formulation::Reduced).unwrap();
check_tangent(&reduced, &neo, &displacement(20, 0.08));
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));
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();
assert!(matches!(im.finite_strain(&svk, &displacement(8, 0.0), &[], &mut [], true), Err(Error::NotSupported(_))));
}
#[test]
fn at_zero_displacement_the_tangent_is_the_linear_stiffness() {
let (e, nu) = (1000.0, 0.45);
let d = isotropic(e, nu).unwrap();
let neo = MaterialModel::NeoHookean { shear: e / (2.0 * (1.0 + nu)), bulk: e / (3.0 * (1.0 - 2.0 * nu)) };
let x20 = hex20();
for (f, m) in [
(Formulation::Full, MaterialModel::StVenantKirchhoff(Box::new(d))),
(Formulation::Full, neo.clone()),
(Formulation::Hybrid { pressures: 4 }, neo),
] {
let solid = Solid::new(ElementKind::Hex20, &x20, f).unwrap();
let r = solid.finite_strain(&m, &vec![[0.0; 3]; 20], &[], &mut [], true).unwrap();
let linear = solid.stiffness(&d).unwrap();
let k = r.tangent.unwrap();
for (a, b) in k.data.iter().zip(&linear.data) {
assert!((a - b).abs() < 1e-9 * e, "{f:?}: {a} vs {b}");
}
assert!(r.force.iter().all(|v| v.abs() < 1e-12 * e));
}
}
#[test]
fn rigid_rotation_strains_and_stresses_nothing() {
let x20 = hex20();
let rot = [[0.0, -0.6, 0.8], [0.6, 0.64, 0.48], [-0.8, 0.48, 0.36]];
let u: Vec<[f64; 3]> = x20.iter().map(|p| std::array::from_fn(|i| (0..3).map(|jj| rot[i][jj] * p[jj]).sum::<f64>() + 5.0 - p[i])).collect();
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();
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));
}
// A rotation that inverts the element is refused.
let flip: Vec<[f64; 3]> = x20.iter().map(|p| [-2.0 * p[0], 0.0, 0.0]).collect();
let solid = Solid::new(ElementKind::Hex20, &x20, Formulation::Full).unwrap();
assert!(matches!(solid.finite_strain(&neo, &flip, &[], &mut [], false), Err(Error::Degenerate(_))));
}
#[test]
fn plane_strain_and_axisymmetric_tangents_are_the_derivative_of_the_forces() {
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],
];
let neo = MaterialModel::NeoHookean { shear: 300.0, bulk: 3000.0 };
let svk = MaterialModel::StVenantKirchhoff(Box::new(isotropic(1000.0, 0.3).unwrap()));
for theory in [Theory::PlaneStrain { thickness: 2.0 }, Theory::Axisymmetric] {
for (kind, x) in [(ElementKind::Quad8, &quad8[..]), (ElementKind::Quad4, &quad8[..4]), (ElementKind::Tri3, &quad8[..3])] {
let plane = Plane::new(kind, x, Formulation::Full, theory).unwrap();
for m in [&neo, &svk] {
let u: Vec<[f64; 2]> = displacement(x.len(), 0.06).iter().map(|v| [v[0], v[1]]).collect();
let k = plane.finite_strain(m, &u, &[], &mut [], true).unwrap().tangent.unwrap();
let scale = k.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
let h = 1e-6;
for j in 0..2 * x.len() {
let shifted = |s: f64| {
let mut v = u.clone();
v[j / 2][j % 2] += s;
plane.finite_strain(m, &v, &[], &mut [], false).unwrap().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:?} {i} {j}: {numeric} vs {}", k.get(i, j));
}
}
}
// At rest the tangent is the linear stiffness.
let d = isotropic(1000.0, 0.3).unwrap();
let k = plane.finite_strain(&svk, &vec![[0.0; 2]; x.len()], &[], &mut [], true).unwrap().tangent.unwrap();
for (a, b) in k.data.iter().zip(&plane.stiffness(&d).unwrap().data) {
assert!((a - b).abs() < 1e-9 * 1000.0);
}
}
}
let stress = Plane::new(ElementKind::Quad4, &quad8[..4], Formulation::Full, Theory::PlaneStress { thickness: 1.0 }).unwrap();
assert!(matches!(stress.finite_strain(&svk, &[[0.0; 2]; 4], &[], &mut [], true), Err(Error::NotSupported(_))));
}
}
+3 -3
View File
@@ -111,7 +111,7 @@ impl PressureField {
/// The pressure functions at a point: 1, then (x − c)/h, (y − c)/h, (z − c)/h for the linear
/// field (`np` = 4).
fn pressure_basis(np: usize, x: [f64; 3], frame: &([f64; 3], f64)) -> [f64; 4] {
pub(crate) fn pressure_basis(np: usize, x: [f64; 3], frame: &([f64; 3], f64)) -> [f64; 4] {
let (c, h) = frame;
let mut out = [1.0, 0.0, 0.0, 0.0];
if np == 4 && *h > 0.0 {
@@ -124,7 +124,7 @@ fn pressure_basis(np: usize, x: [f64; 3], frame: &([f64; 3], f64)) -> [f64; 4] {
impl Solid<'_> {
/// The pressure unknowns of this (hybrid) element.
fn np(&self) -> usize {
pub(crate) fn np(&self) -> usize {
match self.formulation {
Formulation::Hybrid { pressures } => pressures.min(4),
_ => 0,
@@ -133,7 +133,7 @@ impl Solid<'_> {
/// The element's centre (mean of its corners) and size (largest corner distance from it),
/// which scale the linear pressure functions.
fn frame(&self) -> ([f64; 3], f64) {
pub(crate) fn frame(&self) -> ([f64; 3], f64) {
let corners = self.xyz.get(..self.ty.corners).unwrap_or_default();
let n = corners.len().max(1) as f64;
let c = corners.iter().fold([0.0; 3], |a, x| [a[0] + x[0] / n, a[1] + x[1] / n, a[2] + x[2] / n]);
+7
View File
@@ -13,8 +13,13 @@
//! - [`shape`]: the hand-written first-order functions of tet4, tet10, wedge6, hex8, hex20 that
//! 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;
//! - [`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);
//! - [`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
@@ -44,10 +49,12 @@
pub mod basis;
pub mod beam;
pub mod constitutive;
pub mod dense;
pub mod dof;
pub mod element;
pub mod face;
pub mod finite;
pub mod frame;
pub mod heat;
pub mod hybrid;
+13 -13
View File
@@ -189,18 +189,18 @@ pub fn formulation(kind: ElementKind, f: &fieldcraft_model::Formulation) -> Resu
}
/// One column of the strain operator: (∂N/∂x, ∂N/∂y, hoop N/r), for a node or an internal mode.
type Column = [f64; 3];
pub(crate) type Column = [f64; 3];
/// What the kernels need at one integration point.
struct At {
shape: Shape,
pub(crate) struct At {
pub(crate) shape: Shape,
/// Strain columns of the nodes.
g: [Column; MAX_NODES],
det: f64,
x: [f64; 3],
pub(crate) g: [Column; MAX_NODES],
pub(crate) det: f64,
pub(crate) x: [f64; 3],
/// det J times the point's weight times the out-of-plane extent: the point's share of the
/// volume.
dv: f64,
pub(crate) dv: f64,
}
/// The incompatible modes' count of internal degrees of freedom: two modes, two directions.
@@ -210,14 +210,14 @@ const INCOMPATIBLE_DOFS: usize = 4;
#[derive(Debug, Clone, Copy)]
pub struct Plane<'a> {
pub ty: &'static ElementType,
xyz: &'a [[f64; 3]],
formulation: Formulation,
theory: Theory,
pub(crate) xyz: &'a [[f64; 3]],
pub(crate) formulation: Formulation,
pub(crate) theory: Theory,
}
/// out[2i + r][2j + c] += w (Bᵢᵀ D Bⱼ)[r][c] over every pair of columns (row-major, `cols` wide),
/// computed for j ≥ i and mirrored, so the result is exactly symmetric.
fn accumulate(out: &mut [f64], columns: &[Column], d: &Elasticity2, w: f64) {
pub(crate) fn accumulate(out: &mut [f64], columns: &[Column], d: &Elasticity2, w: f64) {
let cols = 2 * columns.len();
let block = |g: &Column| -> [[f64; 2]; 4] { [[g[0], 0.0], [0.0, g[1]], [g[2], 0.0], [g[1], g[0]]] };
for (j, gj) in columns.iter().enumerate() {
@@ -293,7 +293,7 @@ impl<'a> Plane<'a> {
}
}
fn degenerate(&self) -> Error {
pub(crate) fn degenerate(&self) -> Error {
Error::Degenerate(format!("a 2D {} element is inverted, clockwise or degenerate (its Jacobian is not positive)", self.ty.kind.name()))
}
@@ -310,7 +310,7 @@ impl<'a> Plane<'a> {
(j, j[0][0] * j[1][1] - j[0][1] * j[1][0])
}
fn at(&self, p: &Point) -> Result<At> {
pub(crate) fn at(&self, p: &Point) -> Result<At> {
let basis = Basis::of(self.ty.kind);
let shape = basis.eval(p.xi);
let grads = basis.gradients(&shape, self.xyz).map_err(|_| self.degenerate())?;
+2 -1
View File
@@ -322,6 +322,7 @@ impl Model {
Procedure::ExplicitDynamic(_) => "is dynamic",
Procedure::HeatTransfer { transient: Some(_) } => "is transient heat transfer",
Procedure::Static {}
| Procedure::NonlinearStatic(_)
| Procedure::Fluid { .. }
| Procedure::HeatTransfer { transient: None }
| Procedure::CoupledTemperatureDisplacement {} => {
@@ -469,7 +470,7 @@ impl Model {
}
}
match &s.procedure {
Procedure::Static {} => {
Procedure::Static {} | Procedure::NonlinearStatic(_) => {
if s.loads.is_empty() && !prescribes_motion {
d.warning(&subject, "nothing loads the model in this step");
}
+3 -2
View File
@@ -57,12 +57,13 @@ pub use discrete::{Direction, Mass, PointMass, ResolvedDirection, ResolvedMass,
pub use fluids::{BoxFace, BoxFaces, FaceCondition, FluidBoundary, FluidDomain, Profile, SurfaceBoundary, SurfaceCondition};
pub use geometry::{Classification, Entity, GeometryRef};
pub use loads::{Distribution, Load, centrifugal_acceleration};
pub use materials::{Damage, Directional, Elastic, Erosion, Fluid, Hardening, HardeningPoint, Material, Plastic, Thermal};
pub use materials::{Damage, Directional, Elastic, Erosion, Fluid, Hardening, HardeningPoint, Hyperelastic, Material, Plastic, Thermal};
pub use mesh::{Element, ElementEdge, ElementFace, ElementKind, Node, check_coordinates};
pub use sections::{BeamProfile, Extent, Formulation, Integration, Section, SectionKind};
pub use sets::Set;
pub use steps::{
BulkViscosity, ExplicitDynamic, Hourglass, HourglassKind, InitialVelocity, OutputVariable, Procedure, RigidWall, Step, StressRate, Transient,
BulkViscosity, ExplicitDynamic, Hourglass, HourglassKind, InitialVelocity, MAX_NONLINEAR_INCREMENTS, MAX_NONLINEAR_ITERATIONS, NonlinearStatic,
OutputVariable, Procedure, RigidWall, Step, StressRate, Tolerances, Transient,
};
pub use units::UnitSystem;
+66
View File
@@ -74,6 +74,37 @@ impl Elastic {
}
}
/// Hyperelastic behaviour of an isotropic material at large strain, in nonlinear steps.
///
/// The constants are the material's isotropic elastic ones: the shear modulus μ = E / (2(1 + ν))
/// and the bulk modulus κ = E / (3(1 − 2ν)), so at small strain the material is that linear
/// elastic one (linear steps use it so). Without this option an elastic material in a nonlinear
/// step is St Venant–Kirchhoff (the second Piola–Kirchhoff stress linear in the Green–Lagrange
/// strain), which is right for large rotations with small strains.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum Hyperelastic {
/// Compressible neo-Hookean with the isochoric–volumetric split: the strain energy per
/// reference volume W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)², with Ī₁ = J^(−2/3) tr C (C₁₀ = μ/2,
/// D₁ = 2/κ in the usual polynomial notation).
NeoHookean {},
}
impl Hyperelastic {
/// The shear and bulk moduli μ = E / (2(1 + ν)) and κ = E / (3(1 − 2ν)) the model takes from
/// the isotropic constants.
pub fn moduli(young: f64, poisson: f64) -> (f64, f64) {
(young / (2.0 * (1.0 + poisson)), young / (3.0 * (1.0 - 2.0 * poisson)))
}
/// The name of the model, for messages.
pub fn name(&self) -> &'static str {
match self {
Hyperelastic::NeoHookean {} => "neo-Hookean",
}
}
}
/// Fluid behaviour.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
@@ -332,6 +363,9 @@ pub struct Erosion {
pub struct Material {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub elastic: Option<Elastic>,
/// Large-strain behaviour in nonlinear steps, built on the isotropic elastic constants.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hyperelastic: Option<Hyperelastic>,
/// Mass per volume, needed by frequency steps, gravity and transient heat transfer.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub density: Option<f64>,
@@ -366,6 +400,14 @@ impl Material {
if let Some(e) = &self.elastic {
e.validate()?;
}
if let Some(h) = &self.hyperelastic {
if !matches!(self.elastic, Some(Elastic::Isotropic { .. })) {
return Err(Error::Invalid(format!("{} hyperelasticity needs isotropic elastic constants", h.name())));
}
if self.plastic.is_some() {
return Err(Error::Invalid(format!("plasticity of a {} hyperelastic material is not supported yet", h.name())));
}
}
if let Some(d) = self.density {
positive("density", d)?;
}
@@ -412,6 +454,11 @@ impl Model {
self.materials.get(name)
}
/// The hyperelastic materials some section uses, by name.
pub fn hyperelastic_in_use(&self) -> std::collections::BTreeSet<&str> {
self.sections.values().map(|s| s.material.as_str()).filter(|m| self.material(m).is_some_and(|m| m.hyperelastic.is_some())).collect()
}
pub fn add_material(&mut self, name: &str, material: Material) -> Result<()> {
material.validate().map_err(|e| context("material", name, e))?;
insert_new(&mut self.materials, "material", name, material)
@@ -544,6 +591,25 @@ mod tests {
assert_eq!(serde_json::from_str::<Material>(&text).unwrap(), Material { plastic: Some(jc()), damage: Some(damage), ..steel() });
}
#[test]
fn hyperelasticity_builds_on_isotropic_constants() {
let m: Material = serde_json::from_value(serde_json::json!({
"elastic": {"type": "isotropic", "young": 3.0, "poisson": 0.49}, "hyperelastic": {"type": "neo_hookean"}
}))
.unwrap();
m.validate().unwrap();
assert_eq!(m.hyperelastic, Some(Hyperelastic::NeoHookean {}));
let back: Material = serde_json::from_value(serde_json::to_value(&m).unwrap()).unwrap();
assert_eq!(back, m);
assert!(serde_json::from_value::<Material>(serde_json::json!({"hyperelastic": {"type": "neo_hookean", "c10": 1}})).is_err());
let alone = Material { hyperelastic: Some(Hyperelastic::NeoHookean {}), density: Some(1.0), ..Material::default() };
assert!(alone.validate().is_err(), "needs isotropic constants");
let ortho = Elastic::EngineeringConstants { young: [1.0; 3], poisson: [0.0; 3], shear: [1.0; 3] };
assert!(Material { elastic: Some(ortho), ..alone }.validate().is_err());
let plastic = Plastic::Bilinear { yield_stress: 1.0, hardening_modulus: 0.0, hardening: Hardening::Isotropic };
assert!(Material { plastic: Some(plastic), ..m }.validate().is_err());
}
#[test]
fn thermal_properties() {
let m: Material = serde_json::from_value(serde_json::json!({
+49
View File
@@ -124,6 +124,55 @@ pub fn integrate_face(xyz: &[[f64; 3]], mut visit: impl FnMut(&[f64], [f64; 3]))
Ok(())
}
/// A quadrature point of a reference face or edge: the shape function values, their
/// derivatives along the face's natural coordinates ξ and η (an edge has ξ only; its `deta` is
/// empty) and the weight.
#[derive(Debug, Clone, PartialEq)]
pub struct QuadraturePoint {
pub n: Vec<f64>,
pub dxi: Vec<f64>,
pub deta: Vec<f64>,
pub weight: f64,
}
/// The quadrature points of a face of `nodes` nodes (3, 4, 6 or 8), with the rule and shape
/// functions of [`integrate_face`]: what a load on the deformed face (follower pressure)
/// integrates with.
pub fn face_points(nodes: usize) -> Result<Vec<QuadraturePoint>> {
let (corners, shape): (usize, fn(usize, f64, f64) -> Shape) = match nodes {
3 | 6 => (3, triangle),
4 | 8 => (4, quadrilateral),
n => return Err(Error::Invalid(format!("a face has 3, 4, 6 or 8 nodes, not {n}"))),
};
Ok(rule(corners)
.into_iter()
.map(|(xi, eta, weight)| {
let s = shape(nodes, xi, eta);
QuadraturePoint { n: s.n, dxi: s.dxi, deta: s.deta, weight }
})
.collect())
}
/// The quadrature points of an element edge of `nodes` nodes (2 or 3), with the rule and shape
/// functions of [`edge_normal_weights`].
pub fn edge_points(nodes: usize) -> Result<Vec<QuadraturePoint>> {
if !matches!(nodes, 2 | 3) {
return Err(Error::Invalid(format!("an edge has 2 or 3 nodes, not {nodes}")));
}
let g = (0.6f64).sqrt();
Ok([(-g, 5.0 / 9.0), (0.0, 8.0 / 9.0), (g, 5.0 / 9.0)]
.into_iter()
.map(|(s, weight)| {
let (n, dn) = if nodes == 2 {
(vec![(1.0 - s) / 2.0, (1.0 + s) / 2.0], vec![-0.5, 0.5])
} else {
(vec![s * (s - 1.0) / 2.0, s * (s + 1.0) / 2.0, 1.0 - s * s], vec![s - 0.5, s + 0.5, -2.0 * s])
};
QuadraturePoint { n, dxi: dn, deta: Vec::new(), weight }
})
.collect())
}
/// ∫ Nᵢ n dA for every node of a face: the consistent nodal forces of a unit pressure pulling
/// along the face's right-hand (outward, for solids) normal.
pub fn face_normal_weights(xyz: &[[f64; 3]]) -> Result<Vec<[f64; 3]>> {
+151 -2
View File
@@ -86,6 +86,114 @@ pub enum Procedure {
/// Steady heat conduction and the static equilibrium of the thermal strains it causes,
/// together: every boundary condition and load of the step acts.
CoupledTemperatureDisplacement {},
/// Static equilibrium followed in load increments with Newton–Raphson iterations: large
/// displacements, rotations and strains (`nonlinear_geometry`), hyperelastic materials and
/// follower pressure. Loads and prescribed displacements grow in proportion to the step's
/// load factor, from 0 to 1.
NonlinearStatic(NonlinearStatic),
}
/// Most load increments a nonlinear static step may take.
pub const MAX_NONLINEAR_INCREMENTS: u32 = 100_000;
/// Most Newton iterations a nonlinear static step may take in one increment.
pub const MAX_NONLINEAR_ITERATIONS: u32 = 1000;
/// The settings of a nonlinear static step.
///
/// The step's load factor λ runs from 0 to 1: every load is λ times its value and every
/// prescribed displacement λ times its value (a linear ramp). Increments are fractions of the
/// step: the first is `initial_increment`; an increment that converges quickly lets the next
/// grow (up to `max_increment`), one that diverges, fails to converge in `max_iterations` or
/// inverts an element is retried with a quarter of its size, and the step fails, naming the
/// load factor and the reason, when an increment would fall below `min_increment` or the step
/// has taken `max_increments` increments.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct NonlinearStatic {
/// Large displacements, rotations and strains (total Lagrangian elements, follower
/// pressure); `false` keeps the kinematics of small displacements (only the materials are
/// nonlinear).
pub nonlinear_geometry: bool,
/// The first increment, as a fraction of the step.
pub initial_increment: f64,
/// The smallest increment a cutback may reach.
pub min_increment: f64,
/// The largest increment.
pub max_increment: f64,
/// Cap on the increments (cutbacks included).
pub max_increments: u32,
/// Cap on the Newton iterations of one increment.
pub max_iterations: u32,
/// Convergence tolerances.
pub tolerances: Tolerances,
/// A line search along each Newton correction (off by default: full Newton steps converge
/// best for elastic large-rotation problems; a search helps where the material softens).
pub line_search: bool,
}
/// When an increment's Newton iterations have converged: all three hold.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Tolerances {
/// The largest residual force component over the largest force (applied, internal or
/// reaction) the step has seen.
pub force: f64,
/// The largest displacement correction over the largest displacement of the increment.
pub displacement: f64,
/// The work of the last correction against the residual over that of the first.
pub energy: f64,
}
impl Default for Tolerances {
fn default() -> Tolerances {
Tolerances { force: 1e-6, displacement: 1e-5, energy: 1e-10 }
}
}
impl Default for NonlinearStatic {
fn default() -> NonlinearStatic {
NonlinearStatic {
nonlinear_geometry: true,
initial_increment: 0.1,
min_increment: 1e-5,
max_increment: 1.0,
max_increments: 100,
max_iterations: 16,
tolerances: Tolerances::default(),
line_search: false,
}
}
}
impl NonlinearStatic {
pub fn validate(&self) -> Result<()> {
let fraction = |what: &str, v: f64| {
if v > 0.0 && v <= 1.0 { Ok(v) } else { Err(Error::Invalid(format!("the {what} is a fraction of the step in (0, 1], got {v}"))) }
};
let initial = fraction("initial increment", self.initial_increment)?;
let min = fraction("minimum increment", self.min_increment)?;
let max = fraction("maximum increment", self.max_increment)?;
if !(min <= initial && initial <= max) {
return Err(Error::Invalid(format!("the increments must satisfy minimum ≤ initial ≤ maximum, got {min}, {initial} and {max}")));
}
if !(1..=MAX_NONLINEAR_INCREMENTS).contains(&self.max_increments) {
return Err(Error::Invalid(format!("a nonlinear step takes 1 to {MAX_NONLINEAR_INCREMENTS} increments, got {}", self.max_increments)));
}
if !(1..=MAX_NONLINEAR_ITERATIONS).contains(&self.max_iterations) {
return Err(Error::Invalid(format!(
"a nonlinear step takes 1 to {MAX_NONLINEAR_ITERATIONS} iterations per increment, got {}",
self.max_iterations
)));
}
let t = self.tolerances;
for (what, v) in [("force", t.force), ("displacement", t.displacement), ("energy", t.energy)] {
if !(v > 0.0 && v < 1.0) {
return Err(Error::Invalid(format!("the {what} tolerance is in (0, 1), got {v}")));
}
}
Ok(())
}
}
/// Most time increments of a transient heat transfer step.
@@ -386,7 +494,7 @@ impl BulkViscosity {
}
impl Procedure {
/// `static` or `frequency`.
/// The procedure's type, as the model file names it.
pub fn name(&self) -> &'static str {
match self {
Procedure::Static {} => "static",
@@ -395,6 +503,7 @@ impl Procedure {
Procedure::HeatTransfer { .. } => "heat_transfer",
Procedure::CoupledTemperatureDisplacement {} => "coupled_temperature_displacement",
Procedure::Fluid { .. } => "fluid",
Procedure::NonlinearStatic(_) => "nonlinear_static",
}
}
@@ -435,6 +544,9 @@ impl Procedure {
if let Procedure::ExplicitDynamic(e) = self {
return e.validate();
}
if let Procedure::NonlinearStatic(n) = self {
return n.validate();
}
if let Procedure::Fluid { end_time, steps, time_step, frames, magic, .. } = *self {
match (end_time, steps) {
(Some(t), None) => {
@@ -480,7 +592,7 @@ impl Procedure {
/// The output a step of this procedure requests unless told otherwise.
pub fn default_output(&self) -> BTreeSet<OutputVariable> {
match self {
Procedure::Static {} => OutputVariable::ALL.into_iter().collect(),
Procedure::Static {} | Procedure::NonlinearStatic(_) => OutputVariable::ALL.into_iter().collect(),
Procedure::Frequency { .. } => [OutputVariable::Displacement].into_iter().collect(),
Procedure::ExplicitDynamic(_) => [OutputVariable::Displacement, OutputVariable::Stress].into_iter().collect(),
// Velocity, pressure and wall forces, always.
@@ -753,6 +865,43 @@ mod tests {
assert!(serde_json::from_value::<Procedure>(serde_json::json!({"type": "explicit_dynamic", "end_time": 1, "dt": 2})).is_err());
}
#[test]
fn nonlinear_static_steps_validate_their_controls() {
let p: Procedure = serde_json::from_value(serde_json::json!({"type": "nonlinear_static"})).unwrap();
assert_eq!(p, Procedure::NonlinearStatic(NonlinearStatic::default()));
assert_eq!(p.name(), "nonlinear_static");
assert!(p.validate().is_ok() && p.is_structural() && !p.is_dynamic());
assert_eq!(p.default_output(), Procedure::Static {}.default_output());
let p: Procedure = serde_json::from_value(serde_json::json!({
"type": "nonlinear_static", "nonlinear_geometry": false, "initial_increment": 0.5, "min_increment": 0.01,
"max_increments": 20, "max_iterations": 8, "tolerances": {"force": 1e-4}, "line_search": true
}))
.unwrap();
let Procedure::NonlinearStatic(n) = &p else { panic!("{p:?}") };
assert_eq!((n.nonlinear_geometry, n.max_increment, n.tolerances.force, n.tolerances.energy), (false, 1.0, 1e-4, 1e-10));
let back: Procedure = serde_json::from_value(serde_json::to_value(&p).unwrap()).unwrap();
assert_eq!(back, p);
let base = NonlinearStatic::default();
let bad = [
NonlinearStatic { initial_increment: 0.0, ..base.clone() },
NonlinearStatic { initial_increment: f64::NAN, ..base.clone() },
NonlinearStatic { initial_increment: 2.0, max_increment: 2.0, ..base.clone() },
NonlinearStatic { min_increment: 0.5, initial_increment: 0.1, ..base.clone() },
NonlinearStatic { max_increment: 0.05, ..base.clone() },
NonlinearStatic { max_increments: 0, ..base.clone() },
NonlinearStatic { max_increments: MAX_NONLINEAR_INCREMENTS + 1, ..base.clone() },
NonlinearStatic { max_iterations: 0, ..base.clone() },
NonlinearStatic { max_iterations: u32::MAX, ..base.clone() },
NonlinearStatic { tolerances: Tolerances { force: 0.0, ..Tolerances::default() }, ..base.clone() },
NonlinearStatic { tolerances: Tolerances { energy: f64::INFINITY, ..Tolerances::default() }, ..base.clone() },
NonlinearStatic { tolerances: Tolerances { displacement: 1.0, ..Tolerances::default() }, ..base.clone() },
];
for b in bad {
assert!(Procedure::NonlinearStatic(b.clone()).validate().is_err(), "{b:?}");
}
assert!(serde_json::from_value::<Procedure>(serde_json::json!({"type": "nonlinear_static", "nlgeom": true})).is_err());
}
#[test]
fn heat_transfer_procedures() {
let steady: Procedure = serde_json::from_value(serde_json::json!({"type": "heat_transfer"})).unwrap();
+18 -14
View File
@@ -106,6 +106,9 @@ pub struct StaticResult {
pub warnings: Vec<String>,
/// The temperatures of a coupled temperature–displacement step, per node.
pub temperature: Option<Vec<f64>>,
/// The increments of a nonlinear static step (its stresses are Cauchy stresses and its
/// strains logarithmic strains).
pub nonlinear: Option<crate::nonlinear::History>,
}
/// One natural mode.
@@ -167,10 +170,10 @@ struct Assembled {
}
/// Elements whose matrices are computed together before they are added.
const ROUND: usize = 4096;
pub(crate) const ROUND: usize = 4096;
/// Elements per parallel task.
const GRAIN: usize = 16;
pub(crate) const GRAIN: usize = 16;
/// Assemble the stiffness (and the mass when `mass` is set) on the mesh's node graph.
fn assemble(mesh: &Mesh, graph: &NodeGraph, mass: Option<MassKind>) -> Result<Assembled> {
@@ -240,13 +243,13 @@ fn assemble(mesh: &Mesh, graph: &NodeGraph, mass: Option<MassKind>) -> Result<As
}
/// Factor a free-dof matrix; a singular pivot is reported as the free-dof column it hit.
fn factor_raw(a: &Csr, limits: &Limits) -> std::result::Result<Factor, FactorError> {
pub(crate) fn factor_raw(a: &Csr, limits: &Limits) -> std::result::Result<Factor, FactorError> {
Factor::new(&UpperColumns::from_symmetric(a), limits.max_factor_entries)
}
/// A factorization failure in the user's terms: the node (or reference point) and direction of
/// a singular pivot.
fn factor_error(e: FactorError, mesh: &Mesh, reduction: &Reduction) -> Error {
pub(crate) fn factor_error(e: FactorError, mesh: &Mesh, reduction: &Reduction) -> Error {
match e {
FactorError::TooLarge { entries, limit } => {
Error::Limit(format!("the factorization needs {entries} entries; the limit is {limit} (refine less or raise the limit)"))
@@ -270,7 +273,7 @@ fn directions<const N: usize>(full: &[f64], layout: &DofLayout, slot: usize) ->
/// A full vector split into the mesh nodes' translations and rotations (zero where a node has
/// none), per node, and the reference points' translations and rotations, per point.
fn split(full: &[f64], mesh: &Mesh) -> (Vec<[f64; 3]>, Vec<[f64; 3]>, Vec<[f64; 6]>) {
pub(crate) fn split(full: &[f64], mesh: &Mesh) -> (Vec<[f64; 3]>, Vec<[f64; 3]>, Vec<[f64; 6]>) {
let l = &mesh.layout;
let (translation, rotation) = (0..l.nodes())
.map(|n| {
@@ -283,7 +286,7 @@ fn split(full: &[f64], mesh: &Mesh) -> (Vec<[f64; 3]>, Vec<[f64; 3]>, Vec<[f64;
/// The matrix pattern of the mesh (node blocks of its elements, trusses and discrete elements)
/// and the order of its nodes for the factorization.
fn pattern(mesh: &Mesh) -> (NodeGraph, Vec<usize>) {
pub(crate) fn pattern(mesh: &Mesh) -> (NodeGraph, Vec<usize>) {
let graph = NodeGraph::new(mesh.block_count(), mesh.blocks());
// The order covers the mesh nodes; reference point equations follow them.
let n = mesh.labels.len();
@@ -297,7 +300,7 @@ fn pattern(mesh: &Mesh) -> (NodeGraph, Vec<usize>) {
}
/// Warnings for what a step without velocities ignores.
fn ignored_dashpots(mesh: &Mesh, warnings: &mut Vec<String>) {
pub(crate) fn ignored_dashpots(mesh: &Mesh, warnings: &mut Vec<String>) {
let names = mesh.discrete.dashpots();
if !names.is_empty() {
warnings.push(format!(
@@ -309,20 +312,20 @@ fn ignored_dashpots(mesh: &Mesh, warnings: &mut Vec<String>) {
/// Constraint rows, prescribed values and their elimination for a step.
/// The model, its resolved view, the mesh and the step being solved.
struct Setup<'a> {
model: &'a Model,
resolved: &'a Resolved,
mesh: &'a Mesh,
step: &'a Step,
pub(crate) struct Setup<'a> {
pub(crate) model: &'a Model,
pub(crate) resolved: &'a Resolved,
pub(crate) mesh: &'a Mesh,
pub(crate) step: &'a Step,
}
fn reduction(s: &Setup, values: bool, order: &[usize], budget: Budget, warnings: &mut Vec<String>) -> Result<Reduction> {
pub(crate) fn reduction(s: &Setup, values: bool, order: &[usize], budget: Budget, warnings: &mut Vec<String>) -> Result<Reduction> {
let rows = coupling_rows(s.model, s.resolved, s.mesh)?;
Reduction::new(s.mesh, order, &rows, |active| prescribed(s.model, s.resolved, s.mesh, s.step, values, active, warnings), budget)
}
/// The reduced matrices' memory budget: the factorization's.
fn budget(limits: &Limits, mesh: &Mesh, graph: &NodeGraph, matrices: usize) -> Budget {
pub(crate) fn budget(limits: &Limits, mesh: &Mesh, graph: &NodeGraph, matrices: usize) -> Budget {
Budget { max_bytes: limits.max_factor_entries.saturating_mul(8), pattern_entries: graph.entries(&mesh.layout), matrices }
}
@@ -499,6 +502,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
stats: Stats { dofs: mesh.dofs(), equations: red.neq(), factor_entries: chol.entries(), lanczos_vectors: 0 },
warnings,
temperature,
nonlinear: None,
})
}
+44
View File
@@ -650,6 +650,50 @@ impl Reduction {
.collect()
}
/// T_fᵀ r: a vector over every degree of freedom (a residual) restricted to the free
/// equations.
pub fn restrict(&self, r: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; self.neq()];
for (dof, v) in r.iter().enumerate() {
for (e, c) in self.free.row(dof) {
if let Some(x) = out.get_mut(e) {
*x += c * v;
}
}
}
out
}
/// Every degree of freedom from the free equations' values `q` with the prescribed values
/// scaled by `scale` (a load factor): T_f q + scale T_p u_p.
pub fn expand_scaled(&self, q: &[f64], scale: f64) -> Vec<f64> {
(0..self.prescribed_motion.len())
.map(|d| {
let free: f64 = self.free.row(d).map(|(e, c)| c * q.get(e).copied().unwrap_or(0.0)).sum();
free + scale * self.prescribed_motion.get(d).copied().unwrap_or(0.0)
})
.collect()
}
/// The largest prescribed motion, |T_p u_p|∞.
pub fn largest_prescribed(&self) -> f64 {
self.prescribed_motion.iter().fold(0.0, |m, v| m.max(v.abs()))
}
/// T_pᵀ g for a vector g over every degree of freedom (the reactions of the out-of-balance
/// forces g = f_int − f_ext), zero where nothing is prescribed.
pub fn reactions_of(&self, g: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; self.prescribed_motion.len()];
for (dof, v) in g.iter().enumerate() {
for (p, c) in self.fixed.row(dof) {
if let Some(x) = out.get_mut(p) {
*x += c * v;
}
}
}
out
}
/// The reactions at the prescribed degrees of freedom, T_pᵀ (K u − f), over every degree of
/// freedom (zero where nothing is prescribed).
pub fn reactions(&self, k: &Csr, u: &[f64], f: &[f64]) -> Vec<f64> {
+11 -1
View File
@@ -31,6 +31,7 @@ pub mod constraints;
pub mod discrete;
pub mod eigen;
pub mod heat;
pub mod nonlinear;
pub mod ordering;
pub mod sparse;
pub mod system;
@@ -40,6 +41,7 @@ pub use analysis::{
solve_static,
};
pub use heat::{HeatResult, solve_heat};
pub use nonlinear::solve_nonlinear;
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum Error {
@@ -82,16 +84,24 @@ pub struct Limits {
pub max_factor_entries: usize,
/// Lanczos vectors (iterations × block size).
pub max_lanczos_vectors: usize,
/// Newton iterations of a nonlinear step, over all its increments.
pub max_newton_iterations: usize,
}
impl Limits {
pub const MAX_DOFS: usize = 6_000_000;
pub const MAX_FACTOR_ENTRIES: usize = 400_000_000;
pub const MAX_LANCZOS_VECTORS: usize = 3_000;
pub const MAX_NEWTON_ITERATIONS: usize = 100_000;
}
impl Default for Limits {
fn default() -> Limits {
Limits { max_dofs: Limits::MAX_DOFS, max_factor_entries: Limits::MAX_FACTOR_ENTRIES, max_lanczos_vectors: Limits::MAX_LANCZOS_VECTORS }
Limits {
max_dofs: Limits::MAX_DOFS,
max_factor_entries: Limits::MAX_FACTOR_ENTRIES,
max_lanczos_vectors: Limits::MAX_LANCZOS_VECTORS,
max_newton_iterations: Limits::MAX_NEWTON_ITERATIONS,
}
}
}
+897
View File
@@ -0,0 +1,897 @@
//! Nonlinear static analysis: equilibrium followed in load increments with Newton–Raphson
//! iterations.
//!
//! **Load path.** The step's load factor λ runs from 0 to 1. The dead loads (forces, tractions,
//! gravity, body forces; pressure too without nonlinear geometry) are λ times their values, a
//! follower pressure λ p on the deformed faces, and every prescribed displacement λ times its
//! value (u = T_f q + λ T_p u_p, the constraint elimination of the linear solver,
//! [`crate::constraints`]).
//!
//! **Newton–Raphson** with the full consistent tangent, refactored every iteration (Bathe
//! 2014, §8.4; M. A. Crisfield, *Non-linear Finite Element Analysis of Solids and Structures*,
//! vol. 1, Wiley 1991, ch. 9): K_T δq = r with r = T_fᵀ (λ f_ext(u) − f_int(u)), then, when the
//! step asks for it (it is off by default: on large elastic rotations the full Newton step
//! converges in fewer iterations), a line search on the work of the residual along δq, G(s) = δq · r(q + s δq): s = 1 is taken when
//! |G(1)| ≤ 0.8 |G(0)| (always near the solution, so quadratic convergence is kept), otherwise
//! s is moved to the root of the line through (0, G(0)) and (s, G(s)), within [0.1, 1], at most
//! five times (Crisfield 1991, §9.3). An increment has converged when, at once,
//! - **force**: the largest residual component is at most `force` times the largest force the
//! step has seen (applied, internal, reaction), never less than a round-off floor of 10⁻⁸ of a
//! typical diagonal stiffness times the model's size;
//! - **displacement**: the largest correction is at most `displacement` times the largest
//! displacement of the increment;
//! - **energy**: the work of the correction against the residual, s δq · r, is at most `energy`
//! times that of the first iteration (K.-J. Bathe, A. P. Cimento, *Some practical procedures
//! 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).
//!
//! **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
//! inner nodes in step with the prescribed ones; from the last converged state when the
//! extrapolation inverts an element. The first increment is the step's `initial_increment`; two
//! increments in a row that converge in at most four iterations let the next grow by half (up
//! to `max_increment`), one that needs more than eight shrinks it by a quarter. An increment is
//! cut back to a quarter of its size when it fails: an element inverts, the residual is not
//! finite, the residual grows in two successive iterations, the tangent is not positive definite
//! (a limit point or an unstable material), or it does not converge in `max_iterations`. The
//! step fails cleanly, naming the load factor and the last reason, when an increment falls below
//! `min_increment`, when it has taken `max_increments` increments, or when the Newton iterations
//! of the whole step reach [`crate::Limits::max_newton_iterations`].
//!
//! **Follower pressure** and the Cholesky factorization: the load stiffness of a pressure on the
//! deformed surface is not symmetric (Schweizerhof & Ramm 1984); its skew part is left out of the
//! tangent (the symmetric part is kept), so the factorization stays the supernodal Cholesky of the
//! linear solver. The residual is always exact, so the converged solution is unchanged; only the
//! rate of convergence can drop where the skew part matters. Over a closed surface, or one whose
//! boundary is held, the skew parts of neighbouring faces cancel and nothing is left out; the
//! result reports the skew part's size relative to the load stiffness when it is not small.
//!
//! **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).
//! Springs act along their fixed directions. Trusses, shells and beams (large rotations), plane
//! stress, incompatible modes, temperature and centrifugal loads are refused by name.
//! 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
//! the same bit for bit on any number of threads.
use fieldcraft_fem::constitutive::MaterialModel;
use fieldcraft_fem::dense::Matrix;
use fieldcraft_fem::element::{Extrapolation, extrapolation};
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_parallel as parallel;
use serde::Serialize;
use crate::analysis::{
ElementStress, GRAIN, PointResult, ROUND, Setup, StaticResult, Stats, budget, factor_error, factor_raw, ignored_dashpots, pattern, reduction,
split,
};
use crate::cholesky::FactorError;
use crate::constraints::Reduction;
use crate::sparse::{Csr, add_block};
use crate::system::{Mesh, inelastic_warnings, loads, translations};
use crate::{Error, Limits, Result};
/// One Newton iteration of an increment.
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct Iteration {
/// Largest residual component over the force scale.
pub force: f64,
/// Largest correction over the largest displacement of the increment.
pub displacement: f64,
/// Work of the correction against the residual over the first iteration's.
pub energy: f64,
/// The line search's step along the correction (1: the full Newton step).
pub line_search: f64,
}
/// One increment (converged, or cut back).
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Increment {
/// From 1, attempts included.
pub number: usize,
/// The load factor reached (when converged) or attempted.
pub load_factor: f64,
/// Its size, as a fraction of the step.
pub size: f64,
pub iterations: Vec<Iteration>,
pub converged: bool,
/// Why it was cut back.
#[serde(skip_serializing_if = "Option::is_none")]
pub cutback: Option<String>,
}
/// The displacements at the end of a converged increment.
#[derive(Debug, Clone, PartialEq)]
pub struct Frame {
pub load_factor: f64,
/// Per node, in [`StaticResult::nodes`] order.
pub displacement: Vec<[f64; 3]>,
}
/// The increments of a nonlinear static step and the displacements after each converged one.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct History {
pub increments: Vec<Increment>,
/// The undeformed state (load factor 0), then one per converged increment while they fit
/// [`MAX_FRAME_VALUES`].
pub frames: Vec<Frame>,
/// Newton iterations over the step.
pub iterations: usize,
}
impl History {
/// The load factor of every frame.
pub fn load_factors(&self) -> Vec<f64> {
self.frames.iter().map(|f| f.load_factor).collect()
}
}
/// Most displacement values the frames of one step keep (3 per node per frame).
pub const MAX_FRAME_VALUES: usize = 30_000_000;
/// Line search: accept a step whose residual work is at most this fraction of the first.
const LINE_SEARCH_ACCEPT: f64 = 0.8;
/// Line search: most trial steps, and the range of the step.
const LINE_SEARCH_TRIALS: usize = 5;
const LINE_SEARCH_MIN: f64 = 0.1;
/// Cutback factor of a failed increment, growth after fast ones and shrinking after slow ones.
const CUTBACK: f64 = 0.25;
const GROWTH: f64 = 1.5;
const SHRINK: f64 = 0.75;
const FAST_ITERATIONS: usize = 4;
const SLOW_ITERATIONS: usize = 8;
/// A force residual this fraction of its tolerance is round-off.
const ROUND_OFF: f64 = 1e-3;
/// A pressure on element faces (or 2D edges) that follows the deformation.
struct Follower {
element: u32,
/// Layout slots of the face's nodes, and their reference positions.
nodes: Vec<usize>,
xyz: Vec<[f64; 3]>,
magnitude: f64,
/// The out-of-plane extent of a 2D edge; `None` for a solid face.
extent: Option<Extent>,
/// The translations of its nodes (x, y, z, or x, y per node).
dofs: Vec<usize>,
}
/// Everything an evaluation of the out-of-balance forces needs.
struct Problem<'a> {
mesh: &'a Mesh,
red: &'a Reduction,
/// Dead loads at λ = 1.
dead: Vec<f64>,
followers: Vec<Follower>,
/// Springs (linear along their fixed directions), on the pattern.
springs: Option<Csr>,
/// Large-deformation kinematics.
finite: bool,
/// Each element's global degrees of freedom, in its matrices' order, and the kinds per node.
element_dofs: Vec<Vec<usize>>,
element_kinds: Vec<DofSet>,
/// Each element's material model.
materials: Vec<MaterialModel>,
/// The tangent's pattern, zero.
pattern: Csr,
}
/// The internal and external forces at a state, and the tangent when asked for.
struct Evaluation {
internal: Vec<f64>,
external: Vec<f64>,
tangent: Option<Csr>,
/// Every element's material history at this state (committed when the increment
/// converges).
history: Vec<Vec<f64>>,
}
/// Why an attempt at an increment failed, when that calls for a cutback rather than an error.
enum Failure {
Cutback(String),
Fatal(Error),
}
impl From<Error> for Failure {
fn from(e: Error) -> Failure {
match e {
// An inverted element or a material that cannot take the deformation: a smaller
// increment may avoid it.
Error::Element { label, source: fieldcraft_fem::Error::Degenerate(m) } => Failure::Cutback(format!("element {label}: {m}")),
Error::NotConverged(m) => Failure::Cutback(m),
e => Failure::Fatal(e),
}
}
}
fn norm_inf(v: &[f64]) -> f64 {
v.iter().fold(0.0, |m, x| m.max(x.abs()))
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
/// ½(K + sign Kᵀ) of a row-major m × m matrix: its symmetric (sign 1) or skew (−1) part.
fn half(k: &[f64], m: usize, sign: f64) -> Vec<f64> {
(0..m * m).map(|i| 0.5 * (k.get(i).copied().unwrap_or(0.0) + sign * k.get((i % m) * m + i / m).copied().unwrap_or(0.0))).collect()
}
/// The out-of-balance forces over the free equations, T_fᵀ (f_ext − f_int).
fn free_residual(red: &Reduction, e: &Evaluation) -> Vec<f64> {
let r: Vec<f64> = e.external.iter().zip(&e.internal).map(|(a, b)| a - b).collect();
red.restrict(&r)
}
impl Problem<'_> {
/// Nodal displacements of an element (3 per node for solids, 2 for 2D elements).
fn element_vector(&self, e: usize, u: &[f64]) -> Vec<f64> {
self.element_dofs.get(e).map(|dofs| dofs.iter().map(|d| u.get(*d).copied().unwrap_or(0.0)).collect()).unwrap_or_default()
}
/// 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>)> {
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 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)?;
Ok((r.force, r.tangent, updated))
}
/// The forces of a follower pressure at λ (on [`Follower::dofs`]) and its load stiffness
/// (full, not symmetric).
fn follower(&self, f: &Follower, u: &[f64], lambda: f64) -> Result<(Vec<f64>, Vec<f64>)> {
let at = |err| Error::element(f.element, err);
let ue: Vec<f64> = f.dofs.iter().map(|d| u.get(*d).copied().unwrap_or(0.0)).collect();
let p = lambda * f.magnitude;
let (force, k) = match f.extent {
None => {
let (force, k) = face::follower_pressure(&f.xyz, ue.as_chunks::<3>().0, p).map_err(at)?;
(force.into_iter().flatten().collect(), k)
}
Some(extent) => {
let (force, k) = face::follower_edge_pressure(&f.xyz, ue.as_chunks::<2>().0, extent, p).map_err(at)?;
(force.into_iter().flatten().collect(), k)
}
};
Ok((force, k))
}
/// The forces at displacements `u` and load factor λ, and the tangent (symmetric part of the
/// load stiffness included) when `tangent`.
fn evaluate(&self, u: &[f64], lambda: f64, tangent: bool, committed: &[Vec<f64>]) -> Result<Evaluation> {
let layout = &self.mesh.layout;
let n = u.len();
let mut internal = vec![0.0; n];
let mut k = tangent.then(|| self.pattern.clone());
let mut history = Vec::with_capacity(self.mesh.elements.len());
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())
});
for (i, c) in computed.into_iter().enumerate() {
let (f, ke, updated) = c?;
history.push(updated);
let e = start + i;
let el = self.mesh.elements.get(e).ok_or_else(|| Error::Internal("element index".into()))?;
for (d, v) in self.element_dofs.get(e).into_iter().flatten().zip(&f) {
if let Some(x) = internal.get_mut(*d) {
*x += v;
}
}
if let (Some(k), Some(ke)) = (k.as_mut(), ke) {
let kinds = self.element_kinds.get(e).copied().ok_or_else(|| Error::Internal("element kinds".into()))?;
add_block(k, layout, &el.nodes, kinds, &ke.data)?;
}
}
}
if let Some(s) = &self.springs {
let mut ku = vec![0.0; s.n];
s.mul(u.get(..s.n).unwrap_or_default(), &mut ku);
for (x, v) in internal.iter_mut().zip(&ku) {
*x += v;
}
// Both are on the mesh's pattern.
if let Some(k) = k.as_mut() {
k.val.iter_mut().zip(&s.val).for_each(|(x, v)| *x += v);
}
}
let mut external: Vec<f64> = self.dead.iter().map(|f| lambda * f).collect();
for f in &self.followers {
let (force, kl) = self.follower(f, u, lambda)?;
for (d, v) in f.dofs.iter().zip(&force) {
if let Some(x) = external.get_mut(*d) {
*x += v;
}
}
if let Some(k) = k.as_mut() {
add_block(k, layout, &f.nodes, self.mesh.translation_kinds(), &half(&kl, f.dofs.len(), 1.0))?;
}
}
if internal.iter().chain(&external).any(|v| !v.is_finite()) {
return Err(Error::NotConverged("the forces are not finite".into()));
}
Ok(Evaluation { internal, external, tangent: k, history })
}
/// The size of the follower load stiffness's skew part relative to the whole, over the free
/// degrees of freedom (Frobenius norms), at `u` and λ; `None` without followers.
fn follower_asymmetry(&self, u: &[f64], lambda: f64) -> Result<Option<f64>> {
if self.followers.is_empty() {
return Ok(None);
}
let layout = &self.mesh.layout;
let (mut skew, mut whole) = (self.pattern.clone(), self.pattern.clone());
for f in &self.followers {
let (_, kl) = self.follower(f, u, lambda)?;
add_block(&mut skew, layout, &f.nodes, self.mesh.translation_kinds(), &half(&kl, f.dofs.len(), -1.0))?;
add_block(&mut whole, layout, &f.nodes, self.mesh.translation_kinds(), &kl)?;
}
let mut free = vec![false; skew.n];
for d in &self.red.eq_dof {
if let Some(x) = free.get_mut(*d) {
*x = true;
}
}
let norm = |a: &Csr| -> f64 {
let mut sum = 0.0;
for r in (0..a.n).filter(|r| free.get(*r).copied().unwrap_or(false)) {
let (cols, vals) = a.row(r);
for (c, v) in cols.iter().zip(vals) {
if free.get(*c).copied().unwrap_or(false) {
sum += v * v;
}
}
}
sum.sqrt()
};
let total = norm(&whole);
Ok((total > 0.0).then(|| norm(&skew) / total))
}
}
/// Mutable state of the Newton loop over the step.
struct Run<'a> {
problem: &'a Problem<'a>,
settings: &'a NonlinearStatic,
limits: &'a Limits,
/// The largest force seen so far (∞-norm), and the round-off floor of the force scale.
force_scale: f64,
force_floor: Option<f64>,
length: f64,
iterations: usize,
factor_entries: usize,
/// Every element's material history at the end of the last converged increment
/// (integration points × the material's history length).
committed: Vec<Vec<f64>>,
}
/// The outcome of a converged increment.
struct Converged {
q: Vec<f64>,
evaluation: Evaluation,
}
impl Run<'_> {
/// The residual over the free equations, T_fᵀ (f_ext − f_int), and the force scale.
fn residual(&mut self, e: &Evaluation) -> Vec<f64> {
self.force_scale = self.force_scale.max(norm_inf(&e.external)).max(norm_inf(&e.internal));
free_residual(self.problem.red, e)
}
fn scale(&self) -> f64 {
self.force_scale.max(self.force_floor.unwrap_or(0.0)).max(f64::MIN_POSITIVE)
}
fn state(&self, q: &[f64], lambda: f64, tangent: bool) -> std::result::Result<Evaluation, Failure> {
Ok(self.problem.evaluate(&self.problem.red.expand_scaled(q, lambda), lambda, tangent, &self.committed)?)
}
/// Newton iterations from the converged `q0` (at the previous load factor) to load factor
/// `lambda`.
fn increment(
&mut self,
q0: &[f64],
predictor: Option<&[f64]>,
lambda: f64,
size: f64,
iterations: &mut Vec<Iteration>,
) -> std::result::Result<Converged, Failure> {
let red = self.problem.red;
let tol = self.settings.tolerances;
// Start from the extrapolated predictor when it is a valid state, else from q0.
let start = predictor.map(|p| self.state(p, lambda, true).map(|e| (p.to_vec(), e)));
let (mut q, mut eval) = match start {
Some(Ok(s)) => s,
Some(Err(Failure::Fatal(e))) => return Err(Failure::Fatal(e)),
Some(Err(Failure::Cutback(_))) | None => (q0.to_vec(), self.state(q0, lambda, true)?),
};
let mut r = self.residual(&eval);
let prescribed = size * red.largest_prescribed();
let mut first_energy: Option<f64> = None;
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 {
// The round-off rule: nothing left to correct.
if norm_inf(&r) <= ROUND_OFF * tol.force * self.scale() && self.force_floor.is_some() {
return Ok(Converged { q, evaluation: eval });
}
if self.iterations >= self.limits.max_newton_iterations {
return Err(Failure::Fatal(Error::Limit(format!(
"the step took {} Newton iterations, the limit (raise max_newton_iterations or loosen the tolerances)",
self.iterations
))));
}
self.iterations += 1;
let k = eval.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
let [kff] = red.reduce([&k])?;
// The first tangent of the step: a singular one is a modelling error, not a cutback.
let first = self.force_floor.is_none();
if first {
let diagonal = kff.diagonal();
let mean = diagonal.iter().map(|d| d.abs()).sum::<f64>() / diagonal.len().max(1) as f64;
self.force_floor = Some(1e-8 * mean * self.length);
}
drop(k);
let chol = match factor_raw(&kff, self.limits) {
Ok(c) => c,
Err(FactorError::NotPositiveDefinite { .. }) if !first => {
return Err(Failure::Cutback(
"the tangent stiffness is not positive definite (a limit point, a bifurcation or an unstable material)".into(),
));
}
Err(e) => return Err(Failure::Fatal(factor_error(e, self.problem.mesh, red))),
};
self.factor_entries = chol.entries();
drop(kff);
let mut dq = r.clone();
chol.solve(&mut dq)?;
if dq.iter().any(|v| !v.is_finite()) {
return Err(Failure::Cutback("the Newton correction is not finite".into()));
}
let g0 = dot(&dq, &r);
let (s, next) = self.line_search(&q, &dq, lambda, g0)?;
for (x, d) in q.iter_mut().zip(&dq) {
*x += s * d;
}
eval = next;
let r_old = std::mem::replace(&mut r, self.residual(&eval));
let energy = (s * dot(&dq, &r_old)).abs();
let first = *first_energy.get_or_insert(energy);
let moved = q.iter().zip(q0).fold(0.0f64, |m, (a, b)| m.max((a - b).abs()));
let reference = moved.max(prescribed).max(1e-12 * self.length);
let it = Iteration {
force: norm_inf(&r) / self.scale(),
displacement: s * norm_inf(&dq) / reference,
energy: if first > 0.0 { energy / first } else { 0.0 },
line_search: s,
};
iterations.push(it);
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 {
return Ok(Converged { q, evaluation: eval });
}
history.push(it.force);
if let [.., a, b, c] = history[..]
&& c > b
&& b > a
{
return Err(Failure::Cutback(format!("the residual grows ({a:.2e}, {b:.2e}, {c:.2e} of the force scale)")));
}
}
Err(Failure::Cutback(format!("no convergence in {} iterations", self.settings.max_iterations)))
}
/// The line search along `dq` from `q`: the accepted step and the state there (with the
/// tangent).
fn line_search(&mut self, q: &[f64], dq: &[f64], lambda: f64, g0: f64) -> std::result::Result<(f64, Evaluation), Failure> {
let at = |s: f64| -> Vec<f64> { q.iter().zip(dq).map(|(a, b)| a + s * b).collect() };
// The full step, with its tangent (taken in most iterations).
let full = self.state(&at(1.0), lambda, true);
if !self.settings.line_search {
return full.map(|e| (1.0, e));
}
let red = self.problem.red;
let work = |e: &Evaluation| dot(dq, &free_residual(red, e));
let mut s = 1.0;
let mut g = match &full {
Ok(e) => {
let g1 = work(e);
if g1.abs() <= LINE_SEARCH_ACCEPT * g0.abs() {
return full.map(|e| (1.0, e));
}
Some(g1)
}
Err(Failure::Cutback(_)) => None,
Err(Failure::Fatal(_)) => return full.map(|e| (1.0, e)),
};
let mut best: Option<(f64, f64)> = g.map(|g| (1.0, g.abs()));
for _ in 0..LINE_SEARCH_TRIALS {
// Root of the line through (0, g0) and (s, g), or half the step after a failure.
let next = match g {
Some(gs) if (g0 - gs).abs() > 0.0 => (s * g0 / (g0 - gs)).clamp(LINE_SEARCH_MIN, 1.0),
Some(_) => break,
None => (0.5 * s).max(LINE_SEARCH_MIN),
};
if next == s {
break;
}
s = next;
g = match self.state(&at(s), lambda, false) {
Ok(e) => Some(work(&e)),
Err(Failure::Cutback(_)) => None,
Err(f) => return Err(f),
};
if let Some(gs) = g {
if best.is_none_or(|(_, b)| gs.abs() < b) {
best = Some((s, gs.abs()));
}
if gs.abs() <= LINE_SEARCH_ACCEPT * g0.abs() {
break;
}
}
}
let Some((s, _)) = best else {
return Err(Failure::Cutback("every step along the Newton correction inverts an element".into()));
};
if s == 1.0 {
return full.map(|e| (1.0, e));
}
self.state(&at(s), lambda, true).map(|e| (s, e))
}
}
/// Refuse what nonlinear steps cannot take yet, and warn about what they simplify.
fn check_supported(model: &Model, mesh: &Mesh, step: &Step, settings: &NonlinearStatic, warnings: &mut Vec<String>) -> Result<()> {
let at = |what: String| Error::NotSupported(format!("{what} in nonlinear steps (step `{}`)", step.name));
if let Some(t) = mesh.trusses.first() {
return Err(at(format!("trusses (element {})", t.label)));
}
if let Some(s) = mesh.structural.first() {
return Err(at(format!("shells and beams, whose large rotations are not implemented yet (element {})", s.label)));
}
for e in &mesh.elements {
if !settings.nonlinear_geometry && e.hyperelastic.is_some() {
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 settings.nonlinear_geometry
&& let Some(name) = model.couplings().keys().next()
{
warnings.push(format!(
"coupling `{name}` and the others are linear constraints (small rotations of their reference points) in this nonlinear step"
));
}
Ok(())
}
/// 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)> {
let mut out = Vec::new();
let mut dead = step.clone();
for name in &step.loads {
let Some(Load::Pressure { set, magnitude }) = model.load(name) else { continue };
dead.loads.remove(name);
let slots = |labels: Vec<u32>| -> Result<(Vec<usize>, Vec<[f64; 3]>, Vec<usize>)> {
let idx: Vec<usize> = labels
.iter()
.map(|l| mesh.node(*l).ok_or_else(|| Error::Invalid(format!("load `{name}`: node {l} belongs to no solid element"))))
.collect::<Result<_>>()?;
let xyz = idx.iter().filter_map(|i| mesh.xyz.get(*i).copied()).collect();
let mut dofs = Vec::with_capacity(3 * idx.len());
for n in &idx {
dofs.extend(translations(mesh, *n)?);
}
Ok((idx, xyz, dofs))
};
if let Some(theory) = mesh.theory {
if resolved.sets.get(set).is_some_and(|s| !s.faces.is_empty()) {
return Err(Error::Invalid(format!("load `{name}`: a 2D model's pressures act on element edges, and set `{set}` has element faces")));
}
for g in resolved.planar_edges(set) {
let labels =
model.edge_nodes(g).ok_or_else(|| Error::Invalid(format!("load `{name}`: element {} edge {} is missing", g.element, g.edge)))?;
let (nodes, xyz, dofs) = slots(labels)?;
out.push(Follower { element: g.element, nodes, xyz, magnitude: *magnitude, extent: Some(theory.extent()), dofs });
}
} else {
for fc in resolved.sets.get(set).map(|s| s.faces.as_slice()).unwrap_or_default() {
let labels = model
.face_nodes(*fc)
.ok_or_else(|| Error::Invalid(format!("load `{name}`: element {} face {} is missing", fc.element, fc.face)))?;
let (nodes, xyz, dofs) = slots(labels)?;
out.push(Follower { element: fc.element, nodes, xyz, magnitude: *magnitude, extent: None, dofs });
}
}
}
Ok((out, dead))
}
/// Solve a nonlinear static step: the step named `step`, or the model's only nonlinear static
/// step.
pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Result<StaticResult> {
let resolved = model.resolve()?;
let step = model.select_step(step, |p| matches!(p, Procedure::NonlinearStatic(_)), "nonlinear static")?;
let Procedure::NonlinearStatic(settings) = &step.procedure else {
return Err(Error::Internal("nonlinear static step".into()));
};
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);
let setup = Setup { model, resolved: &resolved, mesh: &mesh, step };
let red = reduction(&setup, true, &order, budget(limits, &mesh, &graph, 1), &mut warnings)?;
if red.neq() == 0 {
return Err(Error::Invalid("every degree of freedom is prescribed; there is nothing to solve".into()));
}
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 springs = if mesh.discrete.springs.iter().any(|s| !s.dashpot) {
let mut s = graph.zero_matrix(&mesh.layout);
mesh.discrete.add_stiffness(&mut s)?;
Some(s)
} else {
None
};
let element_kinds = mesh.elements.iter().map(|e| Ok(e.kernel()?.dofs().node)).collect::<Result<Vec<_>>>()?;
let element_dofs = mesh
.elements
.iter()
.zip(&element_kinds)
.map(|(e, kinds)| {
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 {
mesh: &mesh,
red: &red,
dead,
followers: follower_loads,
springs,
finite: settings.nonlinear_geometry,
element_dofs,
element_kinds,
materials: mesh.elements.iter().map(|e| MaterialModel::from_elasticity(&e.d, e.hyperelastic.as_ref())).collect(),
pattern: graph.zero_matrix(&mesh.layout),
};
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])))
});
let length = (0..3).map(|k| (hi[k] - lo[k]).powi(2)).sum::<f64>().sqrt().max(f64::MIN_POSITIVE);
let committed = mesh
.elements
.iter()
.zip(&problem.materials)
.map(|(e, m)| Ok(vec![0.0; e.kernel()?.rule().points().len() * m.history_len()]))
.collect::<Result<Vec<_>>>()?;
// 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 mut history = History::default();
let (mut frame_values, mut frames_full) = (0usize, false);
let values = 3 * mesh.labels.len();
let mut keep_frame = |history: &mut History, lambda: f64, u: &[f64], warnings: &mut Vec<String>| {
if frames_full {
return;
}
if frame_values.saturating_add(values) > MAX_FRAME_VALUES {
warnings.push(format!("the displacement frames stop at load factor {lambda}: the step keeps at most {MAX_FRAME_VALUES} values"));
frames_full = true;
return;
}
frame_values += values;
history.frames.push(Frame { load_factor: lambda, displacement: split(u, &mesh).0 });
};
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;
let mut size = settings.initial_increment;
let mut fast = 0usize;
let mut last: Option<Evaluation> = None;
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 {
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) };
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) {
Ok(c) => {
previous = Some((c.q.iter().zip(&q).map(|(a, b)| a - b).collect(), this));
let count = iterations.len();
history.increments.push(Increment { number, load_factor: target, size: this, iterations, converged: true, cutback: None });
lambda = target;
q = c.q;
keep_frame(&mut history, lambda, &red.expand_scaled(&q, lambda), &mut warnings);
run.committed.clone_from(&c.evaluation.history);
last = Some(c.evaluation);
fast = if count <= FAST_ITERATIONS { fast + 1 } else { 0 };
if fast >= 2 {
size *= GROWTH;
} else if count > SLOW_ITERATIONS {
size *= SHRINK;
}
size = size.clamp(settings.min_increment, settings.max_increment);
}
Err(Failure::Fatal(e)) => return Err(e),
Err(Failure::Cutback(reason)) => {
history.increments.push(Increment {
number,
load_factor: target,
size: this,
iterations,
converged: false,
cutback: Some(reason.clone()),
});
fast = 0;
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.name, settings.min_increment
)));
}
}
}
}
history.iterations = run.iterations;
let u = red.expand_scaled(&q, lambda);
let evaluation = match last {
Some(e) => e,
None => problem.evaluate(&u, lambda, false, &run.committed)?,
};
if let Some(a) = problem.follower_asymmetry(&u, lambda)?
&& a > 1e-6
{
warnings.push(format!(
"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)
}
/// The final state as a static result: displacements, reactions, Cauchy stresses and
/// logarithmic strains.
#[allow(clippy::too_many_arguments)]
fn finish(
mesh: &Mesh,
step: &Step,
red: &Reduction,
problem: &Problem,
u: Vec<f64>,
e: Evaluation,
committed: &[Vec<f64>],
history: History,
factor_entries: usize,
mut warnings: Vec<String>,
) -> Result<StaticResult> {
let (displacement, rotation, point_motion) = split(&u, mesh);
let out_of_balance: Vec<f64> = e.internal.iter().zip(&e.external).map(|(a, b)| a - b).collect();
let reactions = red.reactions_of(&out_of_balance);
let (reaction, reaction_moment, point_reaction) = split(&reactions, mesh);
let (mut applied_total, mut reaction_total) = ([0.0; 3], [0.0; 3]);
for (dof, (fv, rv)) in e.external.iter().zip(&reactions).enumerate() {
let Some(d) = mesh.kind(dof).and_then(DofKind::direction) else { continue };
if let (Some(a), Some(r)) = (applied_total.get_mut(d), reaction_total.get_mut(d)) {
*a += fv;
*r += rv;
}
}
let reference_points = mesh
.points
.iter()
.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)> {
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)
}
.map_err(at)?;
Ok((points, 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 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,
None => {
extrapolations.push((ty, rule, extrapolation(ty, rule).map_err(|err| Error::element(el.label, err))?));
extrapolations.len() - 1
}
};
let nodal = extrapolations.get(index).map(|(_, _, x)| x.apply(&values)).unwrap_or_default();
for (n, s) in el.nodes.iter().zip(&nodal) {
if let (Some(acc), Some(c)) = (sum.get_mut(*n), count.get_mut(*n)) {
for k in 0..6 {
acc[k] += s[k];
}
*c += 1;
}
}
elements.push(ElementStress { label: el.label, kind: el.kind, points, nodal });
}
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();
let scale = applied_total.iter().chain(&reaction_total).fold(0.0f64, |m, v| m.max(v.abs()));
let ground = mesh.discrete.ground_force(&u, mesh);
let imbalance = (0..3).map(|c| (applied_total[c] + reaction_total[c] + ground[c]).abs()).fold(0.0, f64::max);
if scale > 0.0 && imbalance > 1e-6 * scale {
warnings.push(format!("loads and reactions differ by {imbalance:.3e} (relative {:.1e})", imbalance / scale));
}
Ok(StaticResult {
step: step.name.clone(),
nodes: mesh.labels.clone(),
displacement,
rotation,
reaction,
reaction_moment,
stress,
von_mises,
elements,
shells: Vec::new(),
beams: Vec::new(),
reference_points,
applied_total,
reaction_total,
stats: Stats { dofs: mesh.dofs(), equations: red.neq(), factor_entries, lanczos_vectors: 0 },
warnings,
temperature: None,
nonlinear: Some(history),
})
}
+32 -5
View File
@@ -8,6 +8,7 @@
//! name): translations along X, Y, Z and rotations about them.
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::plane::{Plane, Theory};
@@ -17,7 +18,7 @@ use fieldcraft_fem::{DofKind, DofLayout, DofSet, Elasticity, ElementDofs, PointS
use fieldcraft_model::nodal::edge_pressure;
use std::collections::BTreeSet;
use fieldcraft_model::{Elastic, ElementKind, Integration, Load, Model, Resolved, SectionKind, Step, Target, centrifugal_acceleration};
use fieldcraft_model::{Elastic, ElementKind, Hyperelastic, Integration, Load, Model, Resolved, SectionKind, Step, Target, centrifugal_acceleration};
use crate::analysis::MassKind;
use crate::{Error, Result};
@@ -38,6 +39,8 @@ pub struct Elem {
/// The expansion coefficients along X, Y, Z and the reference temperature, when the
/// material expands.
pub expansion: Option<([f64; 3], f64)>,
/// The large-strain option of the element's material (nonlinear steps).
pub hyperelastic: Option<Hyperelastic>,
}
/// An element's kernel: a solid or a 2D element.
@@ -110,6 +113,30 @@ 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(
&self,
m: &MaterialModel,
u: &[f64],
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),
}
}
/// 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>> {
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),
}
}
pub fn rule(&self) -> fieldcraft_fem::quadrature::Rule {
match self {
Kernel::Solid(s) => s.rule(),
@@ -449,7 +476,7 @@ 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));
elements.push((label, el.kind, el.nodes.clone(), formulation, d, material.density, theory, dofs, expansion, material.hyperelastic));
}
}
if let (Some(_), Some(name)) = (planar, model.couplings().keys().next()) {
@@ -515,10 +542,10 @@ impl Mesh {
warnings,
discrete: crate::discrete::Discrete::default(),
};
for (label, kind, nodes, formulation, d, density, theory, _, expansion) in elements {
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();
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 });
mesh.elements.push(Elem { label, kind, nodes: idx, xyz: exyz, formulation, d, density, theory, expansion, hyperelastic });
}
for (label, kind, nodes, young, area, density, expansion) in trusses {
let idx: Vec<usize> = nodes.iter().filter_map(|n| mesh.node(*n)).collect();
@@ -648,7 +675,7 @@ fn vector_dofs(mesh: &Mesh, slot: usize, rotation: bool) -> Option<Vec<usize>> {
}
/// The translations of mesh node `node`.
fn translations(mesh: &Mesh, node: usize) -> Result<Vec<usize>> {
pub(crate) fn translations(mesh: &Mesh, node: usize) -> Result<Vec<usize>> {
vector_dofs(mesh, node, false).ok_or_else(|| Error::Internal(format!("mesh node {node} has no translations")))
}
+435
View File
@@ -0,0 +1,435 @@
//! Nonlinear static analysis against closed forms: uniaxial stretch of neo-Hookean and
//! St Venant–Kirchhoff blocks (3D, plane strain, axisymmetric), the nonlinear patch test, a
//! large rigid rotation, Newton's quadratic convergence, the linear limit, and what nonlinear
//! 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, solve_static};
fn nonlinear() -> Procedure {
Procedure::NonlinearStatic(NonlinearStatic::default())
}
fn neo_hookean(g: &mut Grid, e: f64, nu: f64) {
let m = Material { elastic: Some(iso(e, nu)), hyperelastic: Some(Hyperelastic::NeoHookean {}), ..Material::default() };
g.model.replace_material("Mat", m).unwrap();
}
/// Prescribe every displacement component of each node where `f` holds from its position:
/// one set and one boundary condition per node.
fn prescribe(g: &mut Grid, f: impl Fn([f64; 3]) -> bool, u: impl Fn([f64; 3]) -> [f64; 3]) {
for l in g.nodes_where(f) {
let x = g.model.node(l).unwrap().xyz;
let v = u(x);
let name = format!("n{l}");
g.model.add_set(&name, Set { nodes: vec![l], ..Set::default() }).unwrap();
let values = [Some(v[0]), Some(v[1]), Some(v[2]), None, None, None];
g.bc(&name, BoundaryCondition::Displacement { target: Target::Set(name.clone()), values });
}
}
fn at(r: &StaticResult, label: u32) -> usize {
r.nodes.binary_search(&label).unwrap()
}
fn solve(g: &Grid) -> StaticResult {
solve_nonlinear(&g.model, Some("Step"), &Limits::default()).unwrap()
}
/// The lateral stretch of a neo-Hookean bar stretched by λ in uniaxial stress (σ_yy = σ_zz = 0)
/// and its axial Cauchy stress, by bisection on σ_yy(l) = μ J^(−5/3) (l² − I₁/3) + κ (J − 1).
fn neo_hookean_uniaxial(mu: f64, kappa: f64, lam: f64) -> (f64, f64) {
let stress = |l: f64| {
let (j, i1) = (lam * l * l, lam * lam + 2.0 * l * l);
let c = mu * j.powf(-5.0 / 3.0);
(c * (lam * lam - i1 / 3.0) + kappa * (j - 1.0), c * (l * l - i1 / 3.0) + kappa * (j - 1.0))
};
let (mut lo, mut hi) = (0.1, 1.5);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if stress(mid).1 > 0.0 { hi = mid } else { lo = mid }
}
let l = 0.5 * (lo + hi);
(l, stress(l).0)
}
/// A unit block stretched to twice its length (x), free laterally (symmetry planes), in each
/// element kind: the homogeneous closed form at every node, and the logarithmic strain ln 2.
#[test]
fn neo_hookean_block_in_uniaxial_stretch() {
let (e, nu, lam) = (10.0, 0.3, 2.0);
let (mu, kappa) = (e / (2.0 * (1.0 + nu)), e / (3.0 * (1.0 - 2.0 * nu)));
let (l, sigma) = neo_hookean_uniaxial(mu, kappa, lam);
for (kind, formulation, name) in variants().into_iter().filter(|v| v.1 != incompatible()) {
let mut g = grid([2, 2, 2], kind, |x| x);
g.define(iso(e, nu), None, formulation, nonlinear());
neo_hookean(&mut g, e, nu);
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] });
let r = solve(&g);
for (i, label) in r.nodes.iter().enumerate() {
let x = g.model.node(*label).unwrap().xyz;
let want = [(lam - 1.0) * x[0], (l - 1.0) * x[1], (l - 1.0) * x[2]];
for k in 0..3 {
assert!((r.displacement[i][k] - want[k]).abs() < 1e-8, "{name} node {label}: {:?} vs {want:?}", r.displacement[i]);
}
}
for s in &r.stress {
assert!((s[0] - sigma).abs() < 1e-8 * sigma && s[1].abs() < 1e-8 * sigma && s[3].abs() < 1e-8 * sigma, "{name}: {s:?} vs {sigma}");
}
let strain = r.elements[0].points[0].strain;
assert!((strain[0] - lam.ln()).abs() < 1e-9 && (strain[1] - l.ln()).abs() < 1e-9, "{name}: {strain:?}");
// The reactions on the pulled face balance the axial force σ times the deformed area l².
let pulled: f64 = g.nodes_where(|x| near(x[0], 1.0)).iter().map(|n| r.reaction[at(&r, *n)][0]).sum();
assert!((pulled - sigma * l * l).abs() < 1e-8 * sigma, "{name}: {pulled} vs {}", sigma * l * l);
let h = r.nonlinear.as_ref().unwrap();
assert!(h.increments.last().is_some_and(|i| i.converged && i.load_factor == 1.0), "{name}: {:?}", h.increments);
assert_eq!(h.frames.first().map(|f| f.load_factor), Some(0.0));
assert_eq!(h.frames.last().map(|f| f.load_factor), Some(1.0));
eprintln!("{name}: {} increments, {} iterations", h.increments.len(), h.iterations);
}
}
/// St Venant–Kirchhoff in uniaxial stress: S₁₁ = E E₁₁ with E₁₁ = (λ² − 1)/2, lateral
/// E₂₂ = −ν E₁₁; the Cauchy stress σ = λ² S₁₁ / J.
#[test]
fn st_venant_kirchhoff_uniaxial_stretch() {
let (e, nu, lam): (f64, f64, f64) = (1000.0, 0.25, 1.3);
let e11 = 0.5 * (lam * lam - 1.0);
let l = (1.0 - 2.0 * nu * e11).sqrt();
let sigma = lam * lam * e * e11 / (lam * l * l);
let mut g = grid([1, 1, 1], ElementKind::Hex20, |x| x);
g.define(iso(e, nu), None, full(), nonlinear());
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] });
let r = solve(&g);
let corner = g.nodes_where(|x| near(x[0], 1.0) && near(x[1], 1.0) && near(x[2], 1.0))[0];
assert!((r.displacement[at(&r, corner)][1] - (l - 1.0)).abs() < 1e-9, "{:?}", r.displacement[at(&r, corner)]);
assert!(r.stress.iter().all(|s| (s[0] - sigma).abs() < 1e-8 * sigma), "{:?} vs {sigma}", r.stress[0]);
}
/// The nonlinear patch test: a distorted mesh whose boundary nodes follow a homogeneous finite
/// deformation x = F X (stretch, shear and rotation) reproduces it at the inner nodes and gives
/// the uniform stress of F, for every element kind.
#[test]
fn nonlinear_patch_test() {
let f = [[1.3, 0.2, -0.1], [-0.15, 0.9, 0.25], [0.05, -0.2, 1.1]];
let affine = |x: [f64; 3]| -> [f64; 3] { std::array::from_fn(|i| (0..3).map(|j| f[i][j] * x[j]).sum::<f64>() - x[i]) };
let distort = |q: [f64; 3]| {
let bump = (std::f64::consts::PI * q[0]).sin() * (std::f64::consts::PI * q[1]).sin() * (std::f64::consts::PI * q[2]).sin();
[q[0] + 0.12 * bump, q[1] - 0.08 * bump, q[2] + 0.1 * bump]
};
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 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();
cases.extend([
(ElementKind::Hex8, hybrid, "hex8 hybrid"),
(ElementKind::Hex20, hybrid, "hex20 hybrid"),
(ElementKind::Tet10, hybrid, "tet10 hybrid"),
]);
for (kind, formulation, name) in cases {
let mut g = grid([2, 2, 2], kind, distort);
g.define(iso(e, nu), None, formulation, nonlinear());
neo_hookean(&mut g, e, nu);
let boundary = |q: [f64; 3]| q.iter().any(|c| near(*c, 0.0) || near(*c, 1.0));
prescribe(&mut g, boundary, affine);
let r = solve(&g);
for (i, label) in r.nodes.iter().enumerate() {
let want = affine(g.model.node(*label).unwrap().xyz);
assert!((0..3).all(|k| (r.displacement[i][k] - want[k]).abs() < 1e-9), "{name} node {label}: {:?} vs {want:?}", r.displacement[i]);
}
for el in &r.elements {
for p in &el.points {
assert!((0..6).all(|k| (p.stress[k] - tau[k] / j).abs() < 1e-8 * e), "{name}: {:?} vs {:?}", p.stress, tau.map(|t| t / j));
}
}
}
}
/// A block turned rigidly by 90° about an inclined axis, its boundary prescribed: no strain, no
/// stress, no reaction, whatever the path (objectivity of the formulation).
#[test]
fn rigid_rotation_strains_and_stresses_nothing() {
let axis = [1.0 / 3f64.sqrt(); 3];
let k = fieldcraft_fem::rotation::skew(&axis);
let k2 = fieldcraft_fem::rotation::mul(&k, &k);
let rot: [[f64; 3]; 3] = std::array::from_fn(|i| std::array::from_fn(|j| if i == j { 1.0 } else { 0.0 } + k[i][j] + k2[i][j]));
for (kind, formulation, name) in
[(ElementKind::Hex20, full(), "hex20"), (ElementKind::Tet10, full(), "tet10"), (ElementKind::Hex8, full(), "hex8")]
{
let mut g = grid([2, 2, 2], kind, |x| x);
g.define(iso(1000.0, 0.3), None, formulation, nonlinear());
neo_hookean(&mut g, 1000.0, 0.3);
let boundary = |q: [f64; 3]| q.iter().any(|c| near(*c, 0.0) || near(*c, 1.0));
prescribe(&mut g, boundary, |x| std::array::from_fn(|i| (0..3).map(|j| rot[i][j] * x[j]).sum::<f64>() - x[i]));
let r = solve(&g);
let worst = r.elements.iter().flat_map(|e| &e.points).flat_map(|p| p.stress.iter().chain(&p.strain)).fold(0.0f64, |m, v| m.max(v.abs()));
assert!(worst < 1e-10 * 1000.0, "{name}: largest stress or strain {worst:e}");
assert!(r.reaction.iter().flatten().all(|v| v.abs() < 1e-9), "{name}");
}
}
/// Plane strain and axisymmetric 2D elements reproduce the closed forms: a plane strain block
/// stretched in x (σ_yy = 0, λ_z = 1) and a solid cylinder stretched along its axis (the 3D
/// uniaxial case).
#[test]
fn plane_strain_and_axisymmetric_stretch() {
let (e, nu, lam) = (10.0, 0.3, 1.6);
let (mu, kappa) = (e / (2.0 * (1.0 + nu)), e / (3.0 * (1.0 - 2.0 * nu)));
// Plane strain: σ_yy(l) = μ J^(−5/3) (l² − I₁/3) + κ (J − 1) = 0 with J = λ l, I₁ = λ² + l² + 1.
let syy = |l: f64| {
let (j, i1) = (lam * l, lam * lam + l * l + 1.0);
mu * j.powf(-5.0 / 3.0) * (l * l - i1 / 3.0) + kappa * (j - 1.0)
};
let (mut lo, mut hi) = (0.1, 1.5);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if syy(mid) > 0.0 { hi = mid } else { lo = mid }
}
let l_plane = 0.5 * (lo + hi);
let (l_axi, sigma_axi) = neo_hookean_uniaxial(mu, kappa, lam);
for (section, kind) in [
(SectionKind::PlaneStrain { thickness: 1.0 }, ElementKind::Quad8),
(SectionKind::PlaneStrain { thickness: 1.0 }, ElementKind::Tri3),
(SectionKind::Axisymmetric {}, ElementKind::Quad8),
(SectionKind::Axisymmetric {}, ElementKind::Quad4),
] {
let mut m = Model::default();
let n = 3usize;
let label = |i: usize, j: usize| (1 + i + (2 * n + 1) * j) as u32;
for j in 0..=2 * n {
for i in 0..=2 * n {
m.set_node(label(i, j), [i as f64 / (2 * n) as f64, j as f64 / (2 * n) as f64, 0.0]).unwrap();
}
}
let mut next = 1;
for cj in 0..n {
for ci in 0..n {
let (i, j) = (2 * ci, 2 * cj);
let c = [label(i, j), label(i + 2, j), label(i + 2, j + 2), label(i, j + 2)];
match kind {
ElementKind::Quad8 => {
let mid = [label(i + 1, j), label(i + 2, j + 1), label(i + 1, j + 2), label(i, j + 1)];
m.set_element(next, kind, c.into_iter().chain(mid).collect()).unwrap();
next += 1;
}
ElementKind::Quad4 => {
m.set_element(next, kind, c.to_vec()).unwrap();
next += 1;
}
_ => {
m.set_element(next, kind, vec![c[0], c[1], c[2]]).unwrap();
m.set_element(next + 1, kind, vec![c[0], c[2], c[3]]).unwrap();
next += 2;
}
}
}
}
let all: Vec<u32> = m.elements().keys().copied().collect();
m.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
let mat = Material { elastic: Some(iso(e, nu)), hyperelastic: Some(Hyperelastic::NeoHookean {}), ..Material::default() };
m.add_material("Mat", mat).unwrap();
m.add_section("s", Section { set: "all".into(), material: "Mat".into(), kind: section, formulation: Formulation::default() }).unwrap();
let axisymmetric = matches!(section, SectionKind::Axisymmetric {});
// Stretch along x (plane strain) or along the axis y (axisymmetric).
let (pulled, axis) = if axisymmetric { (1, 1) } else { (0, 0) };
let on = |k: usize, v: f64| -> Vec<u32> { m.nodes().iter().filter(|(_, n)| near(n.xyz[k], v)).map(|(l, _)| *l).collect() };
let (low, high, side) = (on(pulled, 0.0), on(pulled, 1.0), on(1 - pulled, 0.0));
m.add_set("low", Set { nodes: low, ..Set::default() }).unwrap();
m.add_set("high", Set { nodes: high, ..Set::default() }).unwrap();
m.add_set("side", Set { nodes: side, ..Set::default() }).unwrap();
let hold = |k: usize, v: Option<f64>| {
let mut values = [None; 6];
values[k] = v;
values
};
m.add_boundary_condition("low", BoundaryCondition::Displacement { target: Target::Set("low".into()), values: hold(axis, Some(0.0)) })
.unwrap();
m.add_boundary_condition("high", BoundaryCondition::Displacement { target: Target::Set("high".into()), values: hold(axis, Some(lam - 1.0)) })
.unwrap();
m.add_boundary_condition("side", BoundaryCondition::Displacement { target: Target::Set("side".into()), values: hold(1 - axis, Some(0.0)) })
.unwrap();
let mut step = Step::new("Step", nonlinear());
step.boundary_conditions = ["low", "high", "side"].iter().map(|s| s.to_string()).collect();
m.add_step(step, None).unwrap();
let r = solve_nonlinear(&m, None, &Limits::default()).unwrap();
let lateral = if axisymmetric { l_axi } else { l_plane };
for (i, label) in r.nodes.iter().enumerate() {
let x = m.node(*label).unwrap().xyz;
let want = [(lateral - 1.0) * x[1 - pulled], (lam - 1.0) * x[pulled]];
let (got_axial, got_lateral) = (r.displacement[i][pulled], r.displacement[i][1 - pulled]);
assert!(
(got_axial - want[1]).abs() < 1e-8 && (got_lateral - want[0]).abs() < 1e-8,
"{section:?} {kind:?} node {label}: {:?}",
r.displacement[i]
);
}
if axisymmetric {
assert!(r.stress.iter().all(|s| (s[1] - sigma_axi).abs() < 1e-7 * sigma_axi && s[0].abs() < 1e-7 * sigma_axi), "{:?}", r.stress[0]);
}
}
}
/// Newton converges quadratically near the solution: in the last iterations of each increment
/// the residual falls at least like the square of the previous one.
#[test]
fn newton_converges_quadratically() {
let mut g = grid([8, 1, 2], ElementKind::Hex20, |q| [10.0 * q[0], q[1], q[2]]);
let mut step = NonlinearStatic { initial_increment: 0.5, line_search: false, ..NonlinearStatic::default() };
step.tolerances.force = 1e-12;
step.tolerances.displacement = 1e-12;
step.tolerances.energy = 1e-20;
g.define(iso(1000.0, 0.0), None, full(), Procedure::NonlinearStatic(step));
g.fixed("root", |q| near(q[0], 0.0));
g.face_set("tip", |q| near(q[0], 1.0));
// P L² / (E I) = 1: a tip rotation of about 0.46 rad.
let p = 1000.0 * (2.0f64.powi(3) / 12.0) / 100.0;
g.load("push", Load::Force { target: Target::Set("tip".into()), vector: [0.0, 0.0, -p], distribution: Distribution::Total });
let r = solve(&g);
let h = r.nonlinear.unwrap();
// The order of convergence ln(r₂/r₁) / ln(r₁/r₀) over successive residuals in the
// asymptotic range (below 10⁻³, above round-off): 2 for Newton with the consistent tangent.
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-3 && w[2] > 1e-15).map(|w| (w[2] / w[1]).ln() / (w[1] / w[0]).ln()).collect();
eprintln!("increment {} (λ = {}): residuals {res:?}, orders {orders:?}", inc.number, inc.load_factor);
best = orders.iter().fold(best, |m, o| m.max(*o));
}
assert!(best > 1.8, "no quadratic convergence (best order {best})");
}
/// Without nonlinear geometry, and with a small load with it, the step is the linear solution.
#[test]
fn small_loads_and_small_kinematics_give_the_linear_solution() {
let build = |procedure: Procedure, scale: f64| {
let mut g = grid([6, 1, 2], ElementKind::Hex20, |q| [6.0 * q[0], q[1], q[2]]);
g.define(iso(1000.0, 0.3), None, full(), procedure);
g.fixed("root", |q| near(q[0], 0.0));
g.face_set("tip", |q| near(q[0], 1.0));
g.load("push", Load::Force { target: Target::Set("tip".into()), vector: [0.0, 0.0, -scale], distribution: Distribution::Total });
g.face_set("top", |q| near(q[2], 1.0));
g.load("press", Load::Pressure { set: "top".into(), magnitude: 0.1 * scale });
g
};
let linear = solve_static(&build(Procedure::Static {}, 1e-3).model, None, &Limits::default()).unwrap();
let small = NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() };
let r = solve(&build(Procedure::NonlinearStatic(small), 1e-3));
let tip = linear.displacement.iter().fold(0.0f64, |m, u| m.max(u[2].abs()));
for (a, b) in r.displacement.iter().zip(&linear.displacement) {
assert!((0..3).all(|k| (a[k] - b[k]).abs() < 1e-9 * tip), "{a:?} vs {b:?}");
}
let r = solve(&build(nonlinear(), 1e-3));
for (a, b) in r.displacement.iter().zip(&linear.displacement) {
assert!((0..3).all(|k| (a[k] - b[k]).abs() < 1e-3 * tip), "geometric effects of order u/L: {a:?} vs {b:?}");
}
}
/// Hostile and impossible input: the step's controls are refused when defined; an element
/// that inverts makes the step cut back and stop at the minimum increment with the element
/// named; the caps stop a step; unsupported elements and loads are refused by name.
#[test]
fn nonlinear_steps_fail_cleanly() {
let base = || {
let mut g = grid([2, 1, 1], ElementKind::Hex8, |q| q);
let step = NonlinearStatic { min_increment: 1e-3, ..NonlinearStatic::default() };
g.define(iso(100.0, 0.3), None, full(), Procedure::NonlinearStatic(step));
neo_hookean(&mut g, 100.0, 0.3);
g.fixed("root", |q| near(q[0], 0.0));
g
};
// Squash the bar through itself: past −100 % every element inverts.
let mut g = base();
g.node_set("x1", |q| near(q[0], 1.0));
g.bc("crush", BoundaryCondition::Displacement { target: Target::Set("x1".into()), values: [Some(-1.5), Some(0.0), Some(0.0), None, None, None] });
let err = solve_nonlinear(&g.model, None, &Limits::default()).unwrap_err().to_string();
assert!(err.contains("stopped at load factor") && err.contains("element"), "{err}");
// A tight cap on the Newton iterations.
let err = solve_nonlinear(&g.model, None, &Limits { max_newton_iterations: 3, ..Limits::default() }).unwrap_err().to_string();
assert!(err.contains("Newton iterations"), "{err}");
// The increment cap.
let mut capped = base();
capped.node_set("x1", |q| near(q[0], 1.0));
capped.bc("pull", BoundaryCondition::Displacement { target: Target::Set("x1".into()), values: [Some(0.5), None, None, None, None, None] });
let mut step = capped.model.step("Step").unwrap().clone();
step.set_procedure(Procedure::NonlinearStatic(NonlinearStatic {
initial_increment: 0.01,
max_increment: 0.01,
min_increment: 0.01,
max_increments: 3,
..NonlinearStatic::default()
}));
capped.model.replace_step(step).unwrap();
let err = solve_nonlinear(&capped.model, None, &Limits::default()).unwrap_err().to_string();
assert!(err.contains("increments reached the step's cap"), "{err}");
// Hostile controls never reach the solver.
let d = NonlinearStatic::default();
for bad in [
NonlinearStatic { initial_increment: f64::NAN, ..d.clone() },
NonlinearStatic { initial_increment: 0.0, ..d.clone() },
NonlinearStatic { max_iterations: u32::MAX, ..d.clone() },
NonlinearStatic { max_increments: 0, ..d.clone() },
NonlinearStatic { tolerances: Tolerances { force: -1.0, ..Tolerances::default() }, ..d.clone() },
] {
let mut step = capped.model.step("Step").unwrap().clone();
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.
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));
im.face_set("end", |q| near(q[0], 1.0));
im.load("p", Load::Pressure { set: "end".into(), magnitude: 1.0 });
let err = solve_nonlinear(&im.model, None, &Limits::default()).unwrap_err().to_string();
assert!(err.contains("incompatible modes in nonlinear steps"), "{err}");
let mut hy = grid([2, 1, 1], ElementKind::Hex8, |q| q);
hy.define(iso(100.0, 0.3), None, Formulation { hybrid: true, ..Formulation::default() }, nonlinear());
hy.fixed("root", |q| near(q[0], 0.0));
hy.face_set("end", |q| near(q[0], 1.0));
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 });
neo_hookean(&mut hy, 100.0, 0.3);
let err = solve_nonlinear(&hy.model, None, &Limits::default()).unwrap_err().to_string();
assert!(err.contains("temperature 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());
free.face_set("end", |q| near(q[0], 1.0));
free.load("p", Load::Pressure { set: "end".into(), magnitude: 1.0 });
let err = solve_nonlinear(&free.model, None, &Limits::default()).unwrap_err().to_string();
assert!(err.contains("singular at node"), "{err}");
// A linear static step is not a nonlinear one.
let mut lin = grid([1, 1, 1], ElementKind::Hex8, |q| q);
lin.define(iso(100.0, 0.3), None, full(), Procedure::Static {});
assert!(solve_nonlinear(&lin.model, None, &Limits::default()).is_err());
}
/// The same model gives the same numbers bit for bit, whatever the thread count.
#[test]
fn nonlinear_results_are_deterministic() {
let mut g = grid([3, 1, 1], ElementKind::Tet10, |q| [4.0 * q[0], q[1], q[2]]);
g.define(iso(100.0, 0.3), None, full(), nonlinear());
neo_hookean(&mut g, 100.0, 0.3);
g.fixed("root", |q| near(q[0], 0.0));
g.face_set("top", |q| near(q[2], 1.0));
g.load("press", Load::Pressure { set: "top".into(), magnitude: 0.2 });
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");
}
}
+15 -5
View File
@@ -73,10 +73,15 @@ impl Plot {
}
}
/// An explicit or transient heat step's frame shown, its time and the number of frames.
/// An explicit or transient heat step's frame shown, its time (a nonlinear static step's:
/// its load factor) and the number of frames.
pub fn frame(&self) -> Option<(usize, f64, usize)> {
let (n, time): (usize, &dyn Fn(usize) -> Option<f64>) = match self.result.kind() {
Kind::Explicit(e) => (e.frames.len(), &|k| e.frames.get(k).map(|f| f.time)),
Kind::Static(r) if r.nonlinear.is_some() => {
let frames = r.nonlinear.as_ref().map(|h| h.frames.as_slice()).unwrap_or_default();
(frames.len(), &|k| frames.get(k).map(|f| f.load_factor))
}
Kind::Heat(h) if h.times.len() > 1 => (h.times.len(), &|k| h.times.get(k).copied()),
_ => return None,
};
@@ -109,6 +114,8 @@ pub fn swing(result: &StepResult, frame: Option<u64>) -> f64 {
let a = std::f64::consts::TAU * k as f64 / FRAMES as f64;
match result.kind() {
Kind::Frequency(_) => a.sin(),
// A nonlinear step plays its increments' frames.
Kind::Static(r) if r.nonlinear.is_some() => 1.0,
Kind::Static(_) => 0.5 * (1.0 - a.cos()),
Kind::Explicit(_) | Kind::Fluid(_) | Kind::Heat(_) => 1.0,
}
@@ -118,10 +125,13 @@ pub fn swing(result: &StepResult, frame: Option<u64>) -> f64 {
/// step running through its field frames once per cycle.
pub fn pick(view: &ResultsView, result: &StepResult, frame: Option<u64>) -> Pick {
let mut p = view.pick();
if let (Some(k), Kind::Explicit(e)) = (frame, result.kind())
&& let Some(last) = e.frames.len().checked_sub(1)
{
p.frame = Some((k as usize * e.frames.len() / FRAMES as usize).min(last));
let frames = match result.kind() {
Kind::Explicit(e) => e.frames.len(),
Kind::Static(r) => r.nonlinear.as_ref().map_or(0, |h| h.frames.len()),
_ => 0,
};
if let (Some(k), Some(last)) = (frame, frames.checked_sub(1)) {
p.frame = Some((k as usize * frames / FRAMES as usize).min(last));
}
p
}
+11
View File
@@ -162,6 +162,17 @@ pub fn controls(app: &mut ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens)
if let Kind::Explicit(e) = plot.result.kind() {
ui.separator();
explicit(ui, e, plot, t, &mut run);
} else if let Some((k, factor, n)) = plot.frame().filter(|_| matches!(plot.result.kind(), Kind::Static(_))) {
// A nonlinear static step: its increments, the deformed shape following the slider.
ui.separator();
ui.horizontal(|ui| {
ui.label("Increment");
let mut frame = k;
if ui.add(egui::Slider::new(&mut frame, 0..=n.saturating_sub(1))).changed() {
run.push(("results.frame", json!({"frame": frame})));
}
});
ui.label(RichText::new(format!("load factor {}", number(factor))).color(t.text_dim));
} else if let Kind::Frequency(f) = plot.result.kind() {
ui.separator();
ui.label(RichText::new("Modes").color(t.text_dim));
+3 -3
View File
@@ -56,7 +56,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
| Panel | Capability | Status | Command |
|---|---|---|---|
| Materials | Material — Elastic (isotropic, orthotropic) material with density | ready | `model.material.create`, `model.material.edit`, `model.material.delete` |
| Materials | Material — Elastic (isotropic, orthotropic) material with density; neo-Hookean hyperelasticity for nonlinear steps | ready | `model.material.create`, `model.material.edit`, `model.material.delete` |
| Materials | Plasticity — Von Mises plasticity: bilinear, hardening table or Johnson–Cook | ready | `model.material.plasticity` |
| Materials | Hyperelastic — Hyperelastic material models | coming soon (M8) | |
| Materials | Damage — Johnson–Cook damage and element erosion | ready | `model.material.damage` |
@@ -78,7 +78,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
| Panel | Capability | Status | Command |
|---|---|---|---|
| Steps | Step — A linear static or frequency step with its output | ready | `model.step.create`, `model.step.edit`, `model.step.delete` |
| Steps | Step — A linear or nonlinear static, frequency, heat or explicit step with its output | ready | `model.step.create`, `model.step.edit`, `model.step.delete` |
| Steps | Activate — Choose which loads and boundary conditions act in a step | ready | `model.step.activate` |
| Steps | Step Table — The step by load and boundary condition table | ready | `model.activity` |
| Procedures | Amplitude — Time and frequency amplitudes | coming soon (M7) | |
@@ -122,7 +122,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
| Decks | Nastran Deck — Write Nastran bulk data (SOL 101, 103) | ready | `deck.write {"format":"nastran"}` |
| Decks | LS-DYNA Deck — Write an LS-DYNA keyword file | ready | `deck.write {"format":"lsdyna"}` |
| Decks | Read Deck — Read a deck, keeping keywords FieldCraft does not know | coming soon (M9) | |
| Implicit | Static — Linear static (or coupled temperature–displacement) solve of a step with FieldCraft's own solver; heat transfer in the drop-down | ready | `solve.static`, `solve.heat` |
| Implicit | Static — Linear static (or coupled temperature–displacement) solve of a step with FieldCraft's own solver; nonlinear static and heat transfer in the drop-down | ready | `solve.static`, `solve.nonlinear`, `solve.heat` |
| Implicit | Frequency — Natural frequencies and mode shapes of a frequency step | ready | `solve.frequency` |
| Explicit | Explicit — Transient dynamics of an explicit step by central differences (contact and erosion coming in M10) | ready | `solve.explicit` |
| Explicit | Blast & Impact — EOS-driven fluids with fluid–structure contact (ALE or SPH) | coming soon (M14) | |
+94
View File
@@ -149,3 +149,97 @@ K G M⁻¹ ∫ N_p tr ε_th, so a heated nearly incompressible solid takes its b
when confined. A continuous
nodal pressure (`DofKind::P`, Taylor–Hood) needs a symmetric indefinite factorization and is
planned.
## Nonlinear static analysis (finite strain)
A step `{"type": "nonlinear_static", …}` (`Procedure::NonlinearStatic`, solved by
`solve.nonlinear`, `fieldcraft_solver::nonlinear`) follows equilibrium in load increments with
Newton–Raphson iterations. The step's load factor λ runs from 0 to 1: dead loads are λ times
their values, a pressure follows the deformed faces with λ times its magnitude, and prescribed
displacements are λ times theirs (a linear ramp).
**Material point** (`fieldcraft_fem::constitutive`). Given the deformation gradient F and the
history committed at the end of the last converged increment, a material returns the
**Kirchhoff stress** τ = J σ and the **spatial tangent** c of its Lie derivative (L_v τ = c : d),
in Voigt order with engineering-shear columns, so c = D at F = I. This is the push-forward of
the second Piola–Kirchhoff stress and its tangent: τ = F S Fᵀ, c_ijkl = F_iI F_jJ F_kK F_lL
C_IJKL. History variables (`MaterialModel::history_len` per integration point) are read from the
committed slice and written to a separate one, which the solver commits when the increment
converges, so a cut-back increment leaves no trace. The materials:
| Material in a nonlinear step | Model |
|---|---|
| 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" 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.
**Elements** (`fieldcraft_fem::finite`), total Lagrangian over the reference element with its
own quadrature: f_a = ∫ Bₓ,aᵀ τ dV₀ and K_ab = ∫ (Bₓ,aᵀ c Bₓ,b + ∇ₓN_a·τ∇ₓN_b I) dV₀ with the
current gradients ∇ₓN = F⁻ᵀ∇₀N (material plus geometric stiffness), identical term by term to
∫ B₀ᵀ S dV₀ and its linearisation. Every element's tangent is checked against central
differences of its forces.
| Element | Nonlinear steps |
|---|---|
| tet4, tet10, wedge6, hex8, hex20 (full), hex20 (reduced) | total Lagrangian |
| hybrid hex8, hex20, tet10 | Simo–Taylor–Pister three-field form on the linear hybrid elements' pressure fields (θ the projection of J, p the projection of U′(θ)); needs the neo-Hookean option (St Venant–Kirchhoff has no volumetric split) |
| hex8 and quad4 incompatible modes | refused: their finite-strain form (enhanced assumed strain, Simo–Armero 1992) needs the modes as unknowns carried between iterations and hourglasses in large compression; use full integration, hybrid or quadratic elements |
| plane strain, axisymmetric (tri3, tri6, quad4, quad8) | total Lagrangian with F₃₃ = 1 or r/R (hoop column N/r, geometric term τ_θθ N_a N_b/r²) |
| 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).
**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
K = p ∫ N_a (N_b,η [x,ξ]× − N_b,ξ [x,η]×). The load stiffness is not symmetric. The solver keeps
its symmetric part and leaves the skew part out, so the tangent stays symmetric for the
supernodal Cholesky factorization: the residual is exact (the converged answer does not
change), and over a closed surface or one whose boundary is held the skew parts of neighbouring
faces cancel (Schweizerhof & Ramm 1984), so nothing is lost there; elsewhere Newton may converge
linearly instead of quadratically, and the result says how large the left-out part is. An LU
factorization would double the factor's memory and lose the symmetric solver for a gain only in
the rate of convergence of open loaded surfaces; it is revisited with the symmetric indefinite
(LDLᵀ) factorization that arc-length and Taylor–Hood need. Without nonlinear geometry a pressure
is a dead load.
**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
displacement correction (over the increment's largest displacement) and the energy (over the
first iteration's) are all below their tolerances, or the residual is round-off. Each increment
starts from the linear extrapolation of the last one. Automatic incrementation grows an
increment by half after two fast ones (≤ 4 iterations), shrinks it after a slow one (> 8), and
cuts it to a quarter when an element inverts, the residual is not finite or grows twice, the
tangent is not positive definite, or the iterations run out; the step stops with the load factor
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
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.
**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
`*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
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`,
and `tests/oracle/nl-*`): consistent tangents of both materials and of every element (central
differences); uniaxial stretch to λ = 2 of a neo-Hookean block in every element kind, plane
strain and axisymmetric (closed form); St Venant–Kirchhoff uniaxial stretch; the nonlinear patch
test on a distorted mesh (all solids, hybrid ones included); a 90° rigid rotation (zero stress);
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).
+32 -1
View File
@@ -2,7 +2,7 @@
Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the policy is [verification.md](verification.md). Every case is our own model, built with FieldCraft commands from the problem's published description, and compared with the reference value its `expected.json` cites, within the stated tolerance and for the stated reason. Accepted results are frozen in each case's `fixture.json`: a later change that moves one beyond its band (10⁻⁹ of its size) fails CI even while the reference still passes. No reference value was produced with a commercial product.
**117 of 123 quantities pass** (6 known failure(s)) in 35 active cases: 26 theory, 9 published benchmark, 0 experiment; 9 pending cases wait for capabilities still being built.
**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.
## Theory: closed-form solutions, patch tests and mesh invariants
@@ -47,6 +47,31 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
| [lame-cylinder-plane-strain](../tests/oracle/lame-cylinder-plane-strain/) | radial displacement at the bore (10, 0) | 0.0090794 | 0.0090793 | -0.000267% | 0.1% | pass | matches |
| [lame-cylinder-plane-strain](../tests/oracle/lame-cylinder-plane-strain/) | hoop stress σ_θ = σ_yy at the bore (10, 0) | 166.67 | 166.36 | -0.183% | 1% | pass | matches |
| [lame-cylinder-plane-strain](../tests/oracle/lame-cylinder-plane-strain/) | axial stress σ_zz = 2νA at the bore (plane strain) | 20 | 20.083 | 0.415% | 1% | pass | matches |
| [nl-cylinder-inflation](../tests/oracle/nl-cylinder-inflation/) | radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15 | 5 | 5.004 | 0.0793% | 0.5% | pass | matches |
| [nl-cylinder-inflation](../tests/oracle/nl-cylinder-inflation/) | radial displacement of the outer wall (20, 0, 0) | 2.9129 | 2.9173 | 0.152% | 0.5% | pass | matches |
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip deflection δ_v/L at P L²/(E I) = 1 | 0.30172 | 0.30226 | 0.179% | 1% | pass | matches |
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip shortening δ_h/L at P L²/(E I) = 1 | 0.056433 | 0.056633 | 0.354% | 2% | pass | matches |
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip deflection δ_v/L at P L²/(E I) = 2 | 0.49346 | 0.49455 | 0.221% | 1% | pass | matches |
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip shortening δ_h/L at P L²/(E I) = 2 | 0.16064 | 0.16132 | 0.422% | 2% | pass | matches |
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip deflection δ_v/L at P L²/(E I) = 5 | 0.71379 | 0.71598 | 0.306% | 1% | pass | matches |
| [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-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 |
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | u_y at inner node 22 | 3.4875e-4 | 3.4875e-4 | -0.000000665% | 1e-8 rel. | pass | matches |
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | u_z at inner node 22 | -0.0023985 | -0.0023985 | -0.000000102% | 1e-8 rel. | pass | matches |
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | Cauchy σ_xx at inner node 22 | 42.296 | 42.296 | < 1e-7% | 1e-7 rel. | pass | matches |
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | Cauchy σ_yy at inner node 22 | 23.501 | 23.501 | < 1e-7% | 1e-7 rel. | pass | matches |
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | Cauchy σ_zz at inner node 22 | 31.453 | 31.453 | < 1e-7% | 1e-7 rel. | pass | matches |
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | Cauchy σ_xy at inner node 22 | -0.88971 | -0.88971 | < 1e-7% | 1e-7 rel. | pass | matches |
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | Cauchy σ_xz at inner node 22 | -1.8906 | -1.8906 | < 1e-7% | 1e-7 rel. | pass | matches |
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | Cauchy σ_yz at inner node 22 | 1.9462 | 1.9462 | < 1e-7% | 1e-7 rel. | pass | matches |
| [nl-rigid-rotation](../tests/oracle/nl-rigid-rotation/) | u_x at inner node 22: the rotation's | -0.011149 | -0.011149 | -1.2e-16 | ± 1.0000e-9 | pass | matches |
| [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 |
| [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 |
@@ -162,6 +187,11 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
- **[nafems-le1](../tests/oracle/nafems-le1/)** (benchmark): NAFEMS LE1 elliptic membrane (hex20 plane stress layer). Source: NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE1 (target σ_yy = 92.7 MPa at D); as reproduced in the Abaqus 2017 Benchmarks Guide, 'LE1: Plane stress elements—elliptic membrane'. Tolerances — syy_D: the target is published to three figures; plane stress modelled by a thin solid layer and the stress extrapolated to the edge add a few tenths of a percent
- **[nafems-le1-plane-stress](../tests/oracle/nafems-le1-plane-stress/)** (benchmark): NAFEMS LE1 elliptic membrane with 2D plane stress elements (quad8). Source: NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE1 (target σ_yy = 92.7 MPa at D); as reproduced in the Abaqus 2017 Benchmarks Guide, 'LE1: Plane stress elements—elliptic membrane'. Tolerances — syy_D: the target is published to three figures; the stress extrapolated to the edge on a converged mesh adds a few tenths of a percent
- **[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-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
- **[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)
@@ -185,6 +215,7 @@ Reference values already cited; each case gets its recipe when the capability it
| [lid-driven-cavity-re100](../tests/oracle/lid-driven-cavity-re100/) | benchmark | Lid-driven cavity, Re = 100 | lattice Boltzmann CFD (M12) | u/U at y = 0.4531 on the vertical centreline = -0.2109 | Ghia, Ghia and Shin, Journal of Computational Physics 48 (1982) 387–411, Table I (u along the vertical centreline) |
| [mh-twisted-beam](../tests/oracle/mh-twisted-beam/) | benchmark | MacNeal–Harder twisted beam | shell elements (M8) or mapped blocks with helical edges | tip displacement along the in-plane load = 0.005424; tip displacement along the out-of-plane load = 0.001754 | 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. 6 (p. 8), Table 3 (p. 10) |
| [nafems-le11](../tests/oracle/nafems-le11/) | benchmark | NAFEMS LE11 solid cylinder/taper/sphere under a temperature field | thermal loads (M8 heat transfer) | σ_zz at A = -105 | NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE11 (target σ_zz = −105 MPa at A) |
| [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) |
+3 -2
View File
@@ -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) |
| 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 | 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 | | planned |
| 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) |
| 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,6 +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 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 |
+28
View File
@@ -1158,3 +1158,31 @@ update are written from (cited again in the module docs):
ν = 0.4999 (exact), Lamé's thick cylinder in plane strain at ν = 0.4999 (hex20 1e-5,
tet10 1e-3, hex8 3e-3 relative error in the bore displacement, where the displacement hex8
is 80 % too stiff). Cook's membrane is a published benchmark, not part of the analytic gate.
## Part M: Nonlinear static analysis (finite strain), as implemented
- Total Lagrangian formulation written with spatial quantities (Kirchhoff stress, spatial
tangent, current gradients): J. Bonet, R. D. Wood, *Nonlinear Continuum Mechanics for Finite
Element Analysis*, 2nd ed., Cambridge 2008, ch. 5–9; G. A. Holzapfel, *Nonlinear Solid
Mechanics*, Wiley 2000, §6.6 (the isochoric–volumetric split and its tangents); K.-J. Bathe,
*Finite Element Procedures*, 2nd ed., 2014, §6.3; T. Belytschko, W. K. Liu, B. Moran,
K. Elkhodary, *Nonlinear Finite Elements for Continua and Structures*, 2nd ed., Wiley 2014,
ch. 4–6.
- Decoupled neo-Hookean energy: P. J. Flory, Trans. Faraday Soc. 57:829–838, 1961.
- Mixed u–p at finite strain: J. C. Simo, R. L. Taylor, K. S. Pister, CMAME 51:177–208, 1985;
mean dilatation: J. C. Nagtegaal, D. M. Parks, J. R. Rice, CMAME 4:153–177, 1974.
- Enhanced assumed strain at finite strain (why incompatible modes are refused for now): J. C.
Simo, F. Armero, IJNME 33:1413–1449, 1992; their hourglassing in compression: P. Wriggers,
S. Reese, CMAME 135:201–209, 1996.
- Follower pressure and its load stiffness: K. Schweizerhof, E. Ramm, Comput. Struct.
18(6):1099–1114, 1984.
- Newton–Raphson, line search and convergence criteria: M. A. Crisfield, *Non-linear Finite
Element Analysis of Solids and Structures*, vol. 1, Wiley 1991, ch. 9; K.-J. Bathe,
A. P. Cimento, CMAME 22:59–85, 1980.
- Symmetric eigen-decomposition for the logarithmic strain: G. H. Golub, C. F. Van Loan,
*Matrix Computations*, 4th ed., Johns Hopkins 2013, §8.5 (cyclic Jacobi).
- Verification references: the elastica, K. E. Bisshopp, D. C. Drucker, Q. Appl. Math.
3(3):272–275, 1945, with K. Mattiasson, IJNME 17(1):145–153, 1981; Cook's membrane (pending its
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.
+7
View File
@@ -0,0 +1,7 @@
# Cook's membrane at finite strain (pending)
Waits for its reference value: the finite-strain Cook's membrane is defined (load, μ, κ) and
answered in papers we have not transcribed, and no value is entered from memory. FieldCraft
solves the model today (hybrid hex20 one element thick in plane strain, or plane strain quad8 for
compressible materials); the case becomes active when the published numbers are copied in with
their table.
@@ -0,0 +1,14 @@
{
"title": "Cook's membrane, nearly incompressible neo-Hookean, large load (plane strain)",
"kind": "benchmark",
"source": "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",
"status": "pending",
"requires": "the published finite-strain reference value, transcribed from the paper with its load and material constants",
"quantities": [
{
"id": "vC",
"quantity": "vertical displacement of the top corner C (48, 60)",
"value": null
}
]
}
@@ -0,0 +1,7 @@
# Inflation of a thick neo-Hookean cylinder (follower pressure)
A quarter of a tube A = 10, B = 20, one unit long, in plane strain (symmetry planes x = 0,
y = 0 and both ends), 6 × 16 hybrid hex20 elements, neo-Hookean with E = 3, ν = 0.4995
(μ = 1.00033, κ = 1000). The bore pressure p = 0.428371199 follows the deforming bore; the incompressible
closed form puts the bore at a = 15 and the outer wall at b = 22.912878475. This is the follower-pressure
case: the pressure turns with the bore and grows with its area.
@@ -0,0 +1,53 @@
{
"title": "Inflation of a thick neo-Hookean cylinder by a follower pressure (plane strain)",
"kind": "theory",
"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)",
"quantities": [
{
"id": "ua",
"quantity": "radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15",
"measure": {
"command": "solve.probe",
"params": {
"step": "Inflate",
"at": [
[
10.0,
0,
0
]
]
},
"pointer": "/nodes/0/displacement/0"
},
"value": 5.0,
"tolerance": {
"relative": 0.005
},
"reason": "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"
},
{
"id": "ub",
"quantity": "radial displacement of the outer wall (20, 0, 0)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Inflate",
"at": [
[
20.0,
0,
0
]
]
},
"pointer": "/nodes/0/displacement/0"
},
"value": 2.9128784747791983,
"tolerance": {
"relative": 0.005
},
"reason": "as at the bore"
}
]
}
@@ -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.003966508393597e-9,
"value": 5.003966508393597
},
"ub": {
"band": 2.917320022569369e-9,
"value": 2.9173200225693687
}
}
}
@@ -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": "neo_hookean"}}},
{"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": "sz1", "type": "symmetry", "target": {"set": "z1"}, "plane": "z"}},
{"command": "model.load.create", "params": {"name": "p", "type": "pressure", "set": "bore", "magnitude": 0.42837119949633445}},
{"command": "model.step.create", "params": {"name": "Inflate", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz0", "sz1"], "loads": ["p"]}},
{"command": "solve.nonlinear", "params": {"step": "Inflate"}, "id": "solve"}
]
@@ -0,0 +1,8 @@
# Large-deflection cantilever (elastica)
A cantilever L = 10, b = 1, h = 0.5 (L/h = 20) of 40 × 1 × 2 hex20 elements, clamped at x = 0,
E = 10⁴, ν = 0 (an elastic material in a nonlinear step: St Venant–Kirchhoff, exact for large
rotations with these small strains), under a dead vertical tip force P spread over the end face.
Four steps at P L²/(E I) = 1, 2, 5 and 10 (rotations of the tip 0.46 to 1.43 rad). The tip's
neutral-axis node (10, 0, 0.25) is compared with the elastica (Bisshopp–Drucker), whose values
were computed by shooting and agree with Mattiasson's table.
@@ -0,0 +1,199 @@
{
"title": "Large-deflection cantilever under a dead tip load against the elastica (solids)",
"kind": "theory",
"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",
"quantities": [
{
"id": "v1",
"quantity": "tip deflection δ_v/L at P L²/(E I) = 1",
"measure": {
"command": "solve.probe",
"params": {
"step": "K1",
"at": [
[
10.0,
0,
0.25
]
]
},
"pointer": "/nodes/0/displacement/2",
"scale": -0.1
},
"value": 0.301721,
"tolerance": {
"relative": 0.01
},
"reason": "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"
},
{
"id": "u1",
"quantity": "tip shortening δ_h/L at P L²/(E I) = 1",
"measure": {
"command": "solve.probe",
"params": {
"step": "K1",
"at": [
[
10.0,
0,
0.25
]
]
},
"pointer": "/nodes/0/displacement/0",
"scale": -0.1
},
"value": 0.056433,
"tolerance": {
"relative": 0.02
},
"reason": "as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's"
},
{
"id": "v2",
"quantity": "tip deflection δ_v/L at P L²/(E I) = 2",
"measure": {
"command": "solve.probe",
"params": {
"step": "K2",
"at": [
[
10.0,
0,
0.25
]
]
},
"pointer": "/nodes/0/displacement/2",
"scale": -0.1
},
"value": 0.493457,
"tolerance": {
"relative": 0.01
},
"reason": "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"
},
{
"id": "u2",
"quantity": "tip shortening δ_h/L at P L²/(E I) = 2",
"measure": {
"command": "solve.probe",
"params": {
"step": "K2",
"at": [
[
10.0,
0,
0.25
]
]
},
"pointer": "/nodes/0/displacement/0",
"scale": -0.1
},
"value": 0.160642,
"tolerance": {
"relative": 0.02
},
"reason": "as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's"
},
{
"id": "v5",
"quantity": "tip deflection δ_v/L at P L²/(E I) = 5",
"measure": {
"command": "solve.probe",
"params": {
"step": "K5",
"at": [
[
10.0,
0,
0.25
]
]
},
"pointer": "/nodes/0/displacement/2",
"scale": -0.1
},
"value": 0.713792,
"tolerance": {
"relative": 0.01
},
"reason": "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"
},
{
"id": "u5",
"quantity": "tip shortening δ_h/L at P L²/(E I) = 5",
"measure": {
"command": "solve.probe",
"params": {
"step": "K5",
"at": [
[
10.0,
0,
0.25
]
]
},
"pointer": "/nodes/0/displacement/0",
"scale": -0.1
},
"value": 0.387628,
"tolerance": {
"relative": 0.02
},
"reason": "as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's"
},
{
"id": "v10",
"quantity": "tip deflection δ_v/L at P L²/(E I) = 10",
"measure": {
"command": "solve.probe",
"params": {
"step": "K10",
"at": [
[
10.0,
0,
0.25
]
]
},
"pointer": "/nodes/0/displacement/2",
"scale": -0.1
},
"value": 0.810609,
"tolerance": {
"relative": 0.01
},
"reason": "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"
},
{
"id": "u10",
"quantity": "tip shortening δ_h/L at P L²/(E I) = 10",
"measure": {
"command": "solve.probe",
"params": {
"step": "K10",
"at": [
[
10.0,
0,
0.25
]
]
},
"pointer": "/nodes/0/displacement/0",
"scale": -0.1
},
"value": 0.554996,
"tolerance": {
"relative": 0.02
},
"reason": "as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's"
}
]
}
@@ -0,0 +1,37 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"u1": {
"band": 5.6632559447764666e-11,
"value": 0.056632559447764665
},
"u10": {
"band": 5.579973985797526e-10,
"value": 0.5579973985797526
},
"u2": {
"band": 1.6132061646961728e-10,
"value": 0.16132061646961726
},
"u5": {
"band": 3.895481979257076e-10,
"value": 0.3895481979257076
},
"v1": {
"band": 3.02260079032045e-10,
"value": 0.30226007903204494
},
"v10": {
"band": 8.142088468220408e-10,
"value": 0.8142088468220408
},
"v2": {
"band": 4.945495713426414e-10,
"value": 0.4945495713426413
},
"v5": {
"band": 7.159758984663589e-10,
"value": 0.7159758984663589
}
}
}
@@ -0,0 +1,21 @@
[
{"command": "mesh.block", "params": {"body": "Beam", "kind": "hex20", "corners": [[0, 0, 0], [10.0, 0, 0], [10.0, 1.0, 0], [0, 1.0, 0], [0, 0, 0.5], [10.0, 0, 0.5], [10.0, 1.0, 0.5], [0, 1.0, 0.5]], "divisions": [40, 1, 2], "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": 10000.0, "poisson": 0.0}}},
{"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Steel", "kind": {"type": "solid"}}},
{"command": "model.boundary_condition.create", "params": {"name": "clamp", "type": "fixed", "target": {"set": "root"}}},
{"command": "model.load.create", "params": {"name": "P1", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, -1.0416666666666667]}},
{"command": "model.step.create", "params": {"name": "K1", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["clamp"], "loads": ["P1"]}},
{"command": "model.load.create", "params": {"name": "P2", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, -2.0833333333333335]}},
{"command": "model.step.create", "params": {"name": "K2", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["clamp"], "loads": ["P2"]}},
{"command": "model.load.create", "params": {"name": "P5", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, -5.208333333333334]}},
{"command": "model.step.create", "params": {"name": "K5", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["clamp"], "loads": ["P5"]}},
{"command": "model.load.create", "params": {"name": "P10", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, -10.416666666666668]}},
{"command": "model.step.create", "params": {"name": "K10", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["clamp"], "loads": ["P10"]}},
{"command": "solve.nonlinear", "params": {"step": "K1"}, "id": "solve1"},
{"command": "solve.nonlinear", "params": {"step": "K2"}, "id": "solve2"},
{"command": "solve.nonlinear", "params": {"step": "K5"}, "id": "solve5"},
{"command": "solve.nonlinear", "params": {"step": "K10"}, "id": "solve10"}
]
@@ -0,0 +1,7 @@
# Uniaxial stretch of a neo-Hookean block
A unit cube of 2 × 2 × 2 hex20 elements, symmetry planes x = 0, y = 0, z = 0, the face x = 1
pulled to x = 2 (λ = 2) in a nonlinear static step; E = 10, ν = 0.3 with the neo-Hookean
option (μ = 3.84615, κ = 8.33333). The lateral stretch l = 0.813143719 solves σ_yy = 0 (bisection
on the closed form), and σ_xx = 8.060135422. The deformation is homogeneous, so every element
reproduces it exactly.
@@ -0,0 +1,53 @@
{
"title": "Uniaxial stretch of a neo-Hookean block to twice its length",
"kind": "theory",
"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²)",
"quantities": [
{
"id": "sigma",
"quantity": "axial Cauchy stress at the corner (1, 1, 1), stretch λ = 2",
"measure": {
"command": "solve.probe",
"params": {
"step": "Stretch",
"at": [
[
1,
1,
1
]
]
},
"pointer": "/nodes/0/stress/0"
},
"value": 8.060135422196785,
"tolerance": {
"relative": 1e-06
},
"reason": "a homogeneous deformation every element represents exactly: the tolerance only admits rounding and the Newton tolerance"
},
{
"id": "lateral",
"quantity": "lateral displacement u_y at the corner (1, 1, 1), l − 1",
"measure": {
"command": "solve.probe",
"params": {
"step": "Stretch",
"at": [
[
1,
1,
1
]
]
},
"pointer": "/nodes/0/displacement/1"
},
"value": -0.1868562805727787,
"tolerance": {
"relative": 1e-06
},
"reason": "a homogeneous deformation every element represents exactly: the tolerance only admits rounding and the Newton tolerance"
}
]
}
@@ -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": 1.8685628057277872e-10,
"value": -0.18685628056417355
},
"sigma": {
"band": 8.060135422217968e-9,
"value": 8.060135422217968
}
}
}
@@ -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": [2, 2, 2], "faces": {"6": "x0", "3": "y0", "1": "z0", "4": "x1"}}},
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}},
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}},
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}},
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}},
{"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}},
{"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 10.0, "poisson": 0.3}, "hyperelastic": {"type": "neo_hookean"}}},
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Rubber", "kind": {"type": "solid"}}},
{"command": "model.boundary_condition.create", "params": {"name": "sx", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}},
{"command": "model.boundary_condition.create", "params": {"name": "sy", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}},
{"command": "model.boundary_condition.create", "params": {"name": "sz", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
{"command": "model.boundary_condition.create", "params": {"name": "pull", "type": "displacement", "target": {"set": "x1"}, "values": [1.0, null, null, null, null, null]}},
{"command": "model.step.create", "params": {"name": "Stretch", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull"], "loads": []}},
{"command": "solve.nonlinear", "params": {"step": "Stretch"}, "id": "solve"}
]
+7
View File
@@ -0,0 +1,7 @@
# Nonlinear patch test, hex8
A unit cube of 3 × 3 × 3 hex8 elements whose non-corner nodes are moved off the lattice, so no
element is a parallelepiped. Every boundary node is prescribed u = (F − I) X with
F = [[1.3, 0.2, -0.1], [-0.15, 0.9, 0.25], [0.05, -0.2, 1.1]] (stretch, shear and rotation, J = 1.389000); the eight inner nodes are free. The neo-Hookean
material (E = 100, ν = 0.3) must take the homogeneous deformation: the inner node 22 moves by
(F − I) X and the Cauchy stress is uniform.
+178
View File
@@ -0,0 +1,178 @@
{
"title": "Nonlinear patch test, hex8: a homogeneous finite deformation of a distorted mesh",
"kind": "theory",
"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",
"quantities": [
{
"id": "ux",
"quantity": "u_x at inner node 22",
"measure": {
"command": "solve.probe",
"params": {
"step": "Deform",
"nodes": [
22
]
},
"pointer": "/nodes/0/displacement/0"
},
"value": 0.13583340405268285,
"tolerance": {
"relative": 1e-08
},
"reason": "a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance"
},
{
"id": "uy",
"quantity": "u_y at inner node 22",
"measure": {
"command": "solve.probe",
"params": {
"step": "Deform",
"nodes": [
22
]
},
"pointer": "/nodes/0/displacement/1"
},
"value": 0.0003487491373645457,
"tolerance": {
"relative": 1e-08
},
"reason": "a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance"
},
{
"id": "uz",
"quantity": "u_z at inner node 22",
"measure": {
"command": "solve.probe",
"params": {
"step": "Deform",
"nodes": [
22
]
},
"pointer": "/nodes/0/displacement/2"
},
"value": -0.002398508020922785,
"tolerance": {
"relative": 1e-08
},
"reason": "a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance"
},
{
"id": "sxx",
"quantity": "Cauchy σ_xx at inner node 22",
"measure": {
"command": "solve.probe",
"params": {
"step": "Deform",
"nodes": [
22
]
},
"pointer": "/nodes/0/stress/0"
},
"value": 42.29617970634737,
"tolerance": {
"relative": 1e-07
},
"reason": "a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance"
},
{
"id": "syy",
"quantity": "Cauchy σ_yy at inner node 22",
"measure": {
"command": "solve.probe",
"params": {
"step": "Deform",
"nodes": [
22
]
},
"pointer": "/nodes/0/stress/1"
},
"value": 23.501008557686546,
"tolerance": {
"relative": 1e-07
},
"reason": "a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance"
},
{
"id": "szz",
"quantity": "Cauchy σ_zz at inner node 22",
"measure": {
"command": "solve.probe",
"params": {
"step": "Deform",
"nodes": [
22
]
},
"pointer": "/nodes/0/stress/2"
},
"value": 31.452811735966126,
"tolerance": {
"relative": 1e-07
},
"reason": "a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance"
},
{
"id": "sxy",
"quantity": "Cauchy σ_xy at inner node 22",
"measure": {
"command": "solve.probe",
"params": {
"step": "Deform",
"nodes": [
22
]
},
"pointer": "/nodes/0/stress/3"
},
"value": -0.8897122437235885,
"tolerance": {
"relative": 1e-07
},
"reason": "a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance"
},
{
"id": "sxz",
"quantity": "Cauchy σ_xz at inner node 22",
"measure": {
"command": "solve.probe",
"params": {
"step": "Deform",
"nodes": [
22
]
},
"pointer": "/nodes/0/stress/4"
},
"value": -1.8906385179126266,
"tolerance": {
"relative": 1e-07
},
"reason": "a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance"
},
{
"id": "syz",
"quantity": "Cauchy σ_yz at inner node 22",
"measure": {
"command": "solve.probe",
"params": {
"step": "Deform",
"nodes": [
22
]
},
"pointer": "/nodes/0/stress/5"
},
"value": 1.9462455331453503,
"tolerance": {
"relative": 1e-07
},
"reason": "a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance"
}
]
}
+41
View File
@@ -0,0 +1,41 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"sxx": {
"band": 4.2296179707574264e-8,
"value": 42.29617970757426
},
"sxy": {
"band": 8.89712243927903e-10,
"value": -0.8897122439279029
},
"sxz": {
"band": 1.8906385180297377e-9,
"value": -1.8906385180297376
},
"syy": {
"band": 2.3501008558807953e-8,
"value": 23.501008558807953
},
"syz": {
"band": 1.9462455331453503e-9,
"value": 1.9462455329577204
},
"szz": {
"band": 3.14528117370872e-8,
"value": 31.452811737087195
},
"ux": {
"band": 1.3583340405308903e-10,
"value": 0.13583340405308902
},
"uy": {
"band": 3.4874913736454575e-13,
"value": 0.0003487491350470007
},
"uz": {
"band": 2.3985080233668645e-12,
"value": -0.002398508023366864
}
}
}
+210
View File
@@ -0,0 +1,210 @@
[
{"command": "mesh.node.add", "params": {"label": 1, "xyz": [0.0, 0.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 2, "xyz": [0.2962451065991113, 0.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 3, "xyz": [0.6111177857270027, 0.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 4, "xyz": [1.0, 0.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 5, "xyz": [0.0, 0.2910009137991098, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 6, "xyz": [0.3571377677211701, 0.2737737705050953, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 7, "xyz": [0.714575893424076, 0.2779668080365649, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 8, "xyz": [1.0, 0.2966218598767702, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 9, "xyz": [0.0, 0.6967279180541397, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 10, "xyz": [0.3333333333333333, 0.7184592286655991, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 11, "xyz": [0.6295784399324447, 0.7261031080083589, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 12, "xyz": [1.0, 0.7142867384976158, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 13, "xyz": [0.0, 1.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 14, "xyz": [0.31800086721172344, 1.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 15, "xyz": [0.6904711010545035, 1.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 16, "xyz": [1.0, 1.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 17, "xyz": [0.0, 0.0, 0.31040904830229277]}},
{"command": "mesh.node.add", "params": {"label": 18, "xyz": [0.2777844523936694, 0.0, 0.340800598743757]}},
{"command": "mesh.node.add", "params": {"label": 19, "xyz": [0.6089828171538932, 0.0, 0.37600300670769227]}},
{"command": "mesh.node.add", "params": {"label": 20, "xyz": [1.0, 0.0, 0.39127279992629]}},
{"command": "mesh.node.add", "params": {"label": 21, "xyz": [0.0, 0.2737737705050953, 0.3696657255165094]}},
{"command": "mesh.node.add", "params": {"label": 22, "xyz": [0.38124256009074275, 0.2779668080365649, 0.34132725581852985]}},
{"command": "mesh.node.add", "params": {"label": 23, "xyz": [0.7261665552938148, 0.2966218598767702, 0.31090073344351915]}},
{"command": "mesh.node.add", "params": {"label": 24, "xyz": [1.0, 0.33381112436048954, 0.28130120256419844]}},
{"command": "mesh.node.add", "params": {"label": 25, "xyz": [0.0, 0.7184592286655991, 0.28103878698851803]}},
{"command": "mesh.node.add", "params": {"label": 26, "xyz": [0.2962451065991113, 0.7261031080083589, 0.31040904830229277]}},
{"command": "mesh.node.add", "params": {"label": 27, "xyz": [0.6111177857270027, 0.7142867384976158, 0.340800598743757]}},
{"command": "mesh.node.add", "params": {"label": 28, "xyz": [1.0, 0.6815367191456442, 0.37600300670769227]}},
{"command": "mesh.node.add", "params": {"label": 29, "xyz": [0.0, 1.0, 0.3914085136552225]}},
{"command": "mesh.node.add", "params": {"label": 30, "xyz": [0.3571377677211701, 1.0, 0.3696657255165094]}},
{"command": "mesh.node.add", "params": {"label": 31, "xyz": [0.714575893424076, 1.0, 0.34132725581852985]}},
{"command": "mesh.node.add", "params": {"label": 32, "xyz": [1.0, 1.0, 0.31090073344351915]}},
{"command": "mesh.node.add", "params": {"label": 33, "xyz": [0.0, 0.0, 0.6741339320770904]}},
{"command": "mesh.node.add", "params": {"label": 34, "xyz": [0.2756494838205599, 0.0, 0.7093363400410255]}},
{"command": "mesh.node.add", "params": {"label": 35, "xyz": [0.6246741854110541, 0.0, 0.7246061332596233]}},
{"command": "mesh.node.add", "params": {"label": 36, "xyz": [1.0, 0.0, 0.7247418469885558]}},
{"command": "mesh.node.add", "params": {"label": 37, "xyz": [0.0, 0.2779668080365649, 0.6746605891518631]}},
{"command": "mesh.node.add", "params": {"label": 38, "xyz": [0.3928332219604814, 0.2966218598767702, 0.6442340667768525]}},
{"command": "mesh.node.add", "params": {"label": 39, "xyz": [0.7182163755580564, 0.33381112436048954, 0.6146345358975318]}},
{"command": "mesh.node.add", "params": {"label": 40, "xyz": [1.0, 0.3633945847208064, 0.6066672542736244]}},
{"command": "mesh.node.add", "params": {"label": 41, "xyz": [0.0, 0.7261031080083589, 0.6437423816356261]}},
{"command": "mesh.node.add", "params": {"label": 42, "xyz": [0.2777844523936694, 0.7142867384976158, 0.6741339320770904]}},
{"command": "mesh.node.add", "params": {"label": 43, "xyz": [0.6089828171538932, 0.6815367191456442, 0.7093363400410255]}},
{"command": "mesh.node.add", "params": {"label": 44, "xyz": [1.0, 0.6508727591847184, 0.7246061332596233]}},
{"command": "mesh.node.add", "params": {"label": 45, "xyz": [0.0, 1.0, 0.7029990588498427]}},
{"command": "mesh.node.add", "params": {"label": 46, "xyz": [0.38124256009074275, 1.0, 0.6746605891518631]}},
{"command": "mesh.node.add", "params": {"label": 47, "xyz": [0.7261665552938148, 1.0, 0.6442340667768525]}},
{"command": "mesh.node.add", "params": {"label": 48, "xyz": [1.0, 1.0, 0.6146345358975318]}},
{"command": "mesh.node.add", "params": {"label": 49, "xyz": [0.0, 0.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 50, "xyz": [0.29134085207772076, 0.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 51, "xyz": [0.6513342005450568, 0.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 52, "xyz": [1.0, 0.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 53, "xyz": [0.0, 0.2966218598767702, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 54, "xyz": [0.3848830422247231, 0.33381112436048954, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 55, "xyz": [0.6963134677349831, 0.3633945847208064, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 56, "xyz": [1.0, 0.3851258953322657, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 57, "xyz": [0.0, 0.7142867384976158, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 58, "xyz": [0.2756494838205599, 0.6815367191456442, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 59, "xyz": [0.6246741854110541, 0.6508727591847184, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 60, "xyz": [1.0, 0.6243342471324431, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 61, "xyz": [0.0, 1.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 62, "xyz": [0.3928332219604814, 1.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 63, "xyz": [0.7182163755580564, 1.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 64, "xyz": [1.0, 1.0, 1.0]}},
{"command": "mesh.element.add", "params": {"label": 1, "kind": "hex8", "nodes": [1, 2, 6, 5, 17, 18, 22, 21]}},
{"command": "mesh.element.add", "params": {"label": 2, "kind": "hex8", "nodes": [2, 3, 7, 6, 18, 19, 23, 22]}},
{"command": "mesh.element.add", "params": {"label": 3, "kind": "hex8", "nodes": [3, 4, 8, 7, 19, 20, 24, 23]}},
{"command": "mesh.element.add", "params": {"label": 4, "kind": "hex8", "nodes": [5, 6, 10, 9, 21, 22, 26, 25]}},
{"command": "mesh.element.add", "params": {"label": 5, "kind": "hex8", "nodes": [6, 7, 11, 10, 22, 23, 27, 26]}},
{"command": "mesh.element.add", "params": {"label": 6, "kind": "hex8", "nodes": [7, 8, 12, 11, 23, 24, 28, 27]}},
{"command": "mesh.element.add", "params": {"label": 7, "kind": "hex8", "nodes": [9, 10, 14, 13, 25, 26, 30, 29]}},
{"command": "mesh.element.add", "params": {"label": 8, "kind": "hex8", "nodes": [10, 11, 15, 14, 26, 27, 31, 30]}},
{"command": "mesh.element.add", "params": {"label": 9, "kind": "hex8", "nodes": [11, 12, 16, 15, 27, 28, 32, 31]}},
{"command": "mesh.element.add", "params": {"label": 10, "kind": "hex8", "nodes": [17, 18, 22, 21, 33, 34, 38, 37]}},
{"command": "mesh.element.add", "params": {"label": 11, "kind": "hex8", "nodes": [18, 19, 23, 22, 34, 35, 39, 38]}},
{"command": "mesh.element.add", "params": {"label": 12, "kind": "hex8", "nodes": [19, 20, 24, 23, 35, 36, 40, 39]}},
{"command": "mesh.element.add", "params": {"label": 13, "kind": "hex8", "nodes": [21, 22, 26, 25, 37, 38, 42, 41]}},
{"command": "mesh.element.add", "params": {"label": 14, "kind": "hex8", "nodes": [22, 23, 27, 26, 38, 39, 43, 42]}},
{"command": "mesh.element.add", "params": {"label": 15, "kind": "hex8", "nodes": [23, 24, 28, 27, 39, 40, 44, 43]}},
{"command": "mesh.element.add", "params": {"label": 16, "kind": "hex8", "nodes": [25, 26, 30, 29, 41, 42, 46, 45]}},
{"command": "mesh.element.add", "params": {"label": 17, "kind": "hex8", "nodes": [26, 27, 31, 30, 42, 43, 47, 46]}},
{"command": "mesh.element.add", "params": {"label": 18, "kind": "hex8", "nodes": [27, 28, 32, 31, 43, 44, 48, 47]}},
{"command": "mesh.element.add", "params": {"label": 19, "kind": "hex8", "nodes": [33, 34, 38, 37, 49, 50, 54, 53]}},
{"command": "mesh.element.add", "params": {"label": 20, "kind": "hex8", "nodes": [34, 35, 39, 38, 50, 51, 55, 54]}},
{"command": "mesh.element.add", "params": {"label": 21, "kind": "hex8", "nodes": [35, 36, 40, 39, 51, 52, 56, 55]}},
{"command": "mesh.element.add", "params": {"label": 22, "kind": "hex8", "nodes": [37, 38, 42, 41, 53, 54, 58, 57]}},
{"command": "mesh.element.add", "params": {"label": 23, "kind": "hex8", "nodes": [38, 39, 43, 42, 54, 55, 59, 58]}},
{"command": "mesh.element.add", "params": {"label": 24, "kind": "hex8", "nodes": [39, 40, 44, 43, 55, 56, 60, 59]}},
{"command": "mesh.element.add", "params": {"label": 25, "kind": "hex8", "nodes": [41, 42, 46, 45, 57, 58, 62, 61]}},
{"command": "mesh.element.add", "params": {"label": 26, "kind": "hex8", "nodes": [42, 43, 47, 46, 58, 59, 63, 62]}},
{"command": "mesh.element.add", "params": {"label": 27, "kind": "hex8", "nodes": [43, 44, 48, 47, 59, 60, 64, 63]}},
{"command": "model.set.create", "params": {"name": "all", "elements": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]}},
{"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 100.0, "poisson": 0.3}, "hyperelastic": {"type": "neo_hookean"}}},
{"command": "model.section.create", "params": {"name": "s", "set": "all", "material": "Rubber", "kind": {"type": "solid"}}},
{"command": "model.set.create", "params": {"name": "n1", "nodes": [1]}},
{"command": "model.boundary_condition.create", "params": {"name": "n1", "type": "displacement", "target": {"set": "n1"}, "values": [0.0, 0.0, 0.0, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n2", "nodes": [2]}},
{"command": "model.boundary_condition.create", "params": {"name": "n2", "type": "displacement", "target": {"set": "n2"}, "values": [0.08887353197973341, -0.04443676598986669, 0.014812255329955566, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n3", "nodes": [3]}},
{"command": "model.boundary_condition.create", "params": {"name": "n3", "type": "displacement", "target": {"set": "n3"}, "values": [0.1833353357181008, -0.09166766785905041, 0.030555889286350137, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n4", "nodes": [4]}},
{"command": "model.boundary_condition.create", "params": {"name": "n4", "type": "displacement", "target": {"set": "n4"}, "values": [0.30000000000000004, -0.15, 0.05, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n5", "nodes": [5]}},
{"command": "model.boundary_condition.create", "params": {"name": "n5", "type": "displacement", "target": {"set": "n5"}, "values": [0.058200182759821965, -0.029100091379910986, -0.058200182759821965, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n6", "nodes": [6]}},
{"command": "model.boundary_condition.create", "params": {"name": "n6", "type": "displacement", "target": {"set": "n6"}, "values": [0.16189608441737013, -0.08094804220868504, -0.036897865714960565, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n7", "nodes": [7]}},
{"command": "model.boundary_condition.create", "params": {"name": "n7", "type": "displacement", "target": {"set": "n7"}, "values": [0.2699661296345358, -0.1349830648172679, -0.019864566936109183, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n8", "nodes": [8]}},
{"command": "model.boundary_condition.create", "params": {"name": "n8", "type": "displacement", "target": {"set": "n8"}, "values": [0.35932437197535405, -0.179662185987677, -0.009324371975354039, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n9", "nodes": [9]}},
{"command": "model.boundary_condition.create", "params": {"name": "n9", "type": "displacement", "target": {"set": "n9"}, "values": [0.13934558361082794, -0.06967279180541397, -0.13934558361082794, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n10", "nodes": [10]}},
{"command": "model.boundary_condition.create", "params": {"name": "n10", "type": "displacement", "target": {"set": "n10"}, "values": [0.2436918457331198, -0.12184592286655993, -0.12702517906645316, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n11", "nodes": [11]}},
{"command": "model.boundary_condition.create", "params": {"name": "n11", "type": "displacement", "target": {"set": "n11"}, "values": [0.33409415358140526, -0.16704707679070263, -0.11374169960504955, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n12", "nodes": [12]}},
{"command": "model.boundary_condition.create", "params": {"name": "n12", "type": "displacement", "target": {"set": "n12"}, "values": [0.44285734769952323, -0.22142867384976161, -0.09285734769952315, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n13", "nodes": [13]}},
{"command": "model.boundary_condition.create", "params": {"name": "n13", "type": "displacement", "target": {"set": "n13"}, "values": [0.2, -0.09999999999999998, -0.2, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n14", "nodes": [14]}},
{"command": "model.boundary_condition.create", "params": {"name": "n14", "type": "displacement", "target": {"set": "n14"}, "values": [0.295400260163517, -0.14770013008175853, -0.18409995663941384, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n15", "nodes": [15]}},
{"command": "model.boundary_condition.create", "params": {"name": "n15", "type": "displacement", "target": {"set": "n15"}, "values": [0.40714133031635114, -0.20357066515817546, -0.16547644494727484, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n16", "nodes": [16]}},
{"command": "model.boundary_condition.create", "params": {"name": "n16", "type": "displacement", "target": {"set": "n16"}, "values": [0.5, -0.25, -0.15000000000000002, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n17", "nodes": [17]}},
{"command": "model.boundary_condition.create", "params": {"name": "n17", "type": "displacement", "target": {"set": "n17"}, "values": [-0.03104090483022928, 0.07760226207557319, 0.03104090483022931, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n18", "nodes": [18]}},
{"command": "model.boundary_condition.create", "params": {"name": "n18", "type": "displacement", "target": {"set": "n18"}, "values": [0.04925527584372513, 0.04353248182688885, 0.047969282494059184, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n19", "nodes": [19]}},
{"command": "model.boundary_condition.create", "params": {"name": "n19", "type": "displacement", "target": {"set": "n19"}, "values": [0.1450945444753987, 0.002653329103839086, 0.06804944152846393, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n20", "nodes": [20]}},
{"command": "model.boundary_condition.create", "params": {"name": "n20", "type": "displacement", "target": {"set": "n20"}, "values": [0.26087272000737105, -0.052181800018427496, 0.08912727999262904, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n21", "nodes": [21]}},
{"command": "model.boundary_condition.create", "params": {"name": "n21", "type": "displacement", "target": {"set": "n21"}, "values": [0.017788181549368128, 0.06503905432861784, -0.017788181549368065, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n24", "nodes": [24]}},
{"command": "model.boundary_condition.create", "params": {"name": "n24", "type": "displacement", "target": {"set": "n24"}, "values": [0.33863210461567816, -0.11305581179499935, 0.011367895384321924, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n25", "nodes": [25]}},
{"command": "model.boundary_condition.create", "params": {"name": "n25", "type": "displacement", "target": {"set": "n25"}, "values": [0.11558796703426802, -0.0015862261194303784, -0.11558796703426799, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n28", "nodes": [28]}},
{"command": "model.boundary_condition.create", "params": {"name": "n28", "type": "displacement", "target": {"set": "n28"}, "values": [0.3987070431583597, -0.12415292023764135, -0.04870704315835961, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n29", "nodes": [29]}},
{"command": "model.boundary_condition.create", "params": {"name": "n29", "type": "displacement", "target": {"set": "n29"}, "values": [0.16085914863447776, -0.0021478715861943387, -0.1608591486344777, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n30", "nodes": [30]}},
{"command": "model.boundary_condition.create", "params": {"name": "n30", "type": "displacement", "target": {"set": "n30"}, "values": [0.2701747577647001, -0.06115423377904816, -0.14517653906229053, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n31", "nodes": [31]}},
{"command": "model.boundary_condition.create", "params": {"name": "n31", "type": "displacement", "target": {"set": "n31"}, "values": [0.3802400424453698, -0.12185457005897893, -0.13013847974694318, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n32", "nodes": [32]}},
{"command": "model.boundary_condition.create", "params": {"name": "n32", "type": "displacement", "target": {"set": "n32"}, "values": [0.46890992665564823, -0.17227481663912014, -0.11890992665564809, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n33", "nodes": [33]}},
{"command": "model.boundary_condition.create", "params": {"name": "n33", "type": "displacement", "target": {"set": "n33"}, "values": [-0.06741339320770905, 0.1685334830192726, 0.06741339320770912, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n34", "nodes": [34]}},
{"command": "model.boundary_condition.create", "params": {"name": "n34", "type": "displacement", "target": {"set": "n34"}, "values": [0.011761211142065409, 0.1359866624371724, 0.08471610819513054, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n35", "nodes": [35]}},
{"command": "model.boundary_condition.create", "params": {"name": "n35", "type": "displacement", "target": {"set": "n35"}, "values": [0.11494164229735393, 0.08745040550324772, 0.10369432259651512, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n36", "nodes": [36]}},
{"command": "model.boundary_condition.create", "params": {"name": "n36", "type": "displacement", "target": {"set": "n36"}, "values": [0.22752581530114435, 0.03118546174713896, 0.12247418469885574, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n37", "nodes": [37]}},
{"command": "model.boundary_condition.create", "params": {"name": "n37", "type": "displacement", "target": {"set": "n37"}, "values": [-0.011872697307873326, 0.14086846648430928, 0.011872697307873437, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n40", "nodes": [40]}},
{"command": "model.boundary_condition.create", "params": {"name": "n40", "type": "displacement", "target": {"set": "n40"}, "values": [0.3120121915167988, -0.034672644903674554, 0.03798780848320127, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n41", "nodes": [41]}},
{"command": "model.boundary_condition.create", "params": {"name": "n41", "type": "displacement", "target": {"set": "n41"}, "values": [0.08084638343810917, 0.08832528460807065, -0.08084638343810913, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n44", "nodes": [44]}},
{"command": "model.boundary_condition.create", "params": {"name": "n44", "type": "displacement", "target": {"set": "n44"}, "values": [0.3577139385109813, -0.03393574260356602, -0.007713938510981233, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n45", "nodes": [45]}},
{"command": "model.boundary_condition.create", "params": {"name": "n45", "type": "displacement", "target": {"set": "n45"}, "values": [0.12970009411501573, 0.07574976471246075, -0.1297000941150157, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n46", "nodes": [46]}},
{"command": "model.boundary_condition.create", "params": {"name": "n46", "type": "displacement", "target": {"set": "n46"}, "values": [0.2469067091120365, 0.011478763274354487, -0.11347181308027643, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n47", "nodes": [47]}},
{"command": "model.boundary_condition.create", "params": {"name": "n47", "type": "displacement", "target": {"set": "n47"}, "values": [0.35342655991045935, -0.04786646659985905, -0.09926826555762391, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n48", "nodes": [48]}},
{"command": "model.boundary_condition.create", "params": {"name": "n48", "type": "displacement", "target": {"set": "n48"}, "values": [0.43853654641024686, -0.09634136602561705, -0.08853654641024677, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n49", "nodes": [49]}},
{"command": "model.boundary_condition.create", "params": {"name": "n49", "type": "displacement", "target": {"set": "n49"}, "values": [-0.1, 0.25, 0.10000000000000009, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n50", "nodes": [50]}},
{"command": "model.boundary_condition.create", "params": {"name": "n50", "type": "displacement", "target": {"set": "n50"}, "values": [-0.012597744376683817, 0.2062988721883419, 0.11456704260388606, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n51", "nodes": [51]}},
{"command": "model.boundary_condition.create", "params": {"name": "n51", "type": "displacement", "target": {"set": "n51"}, "values": [0.0954002601635171, 0.1522998699182415, 0.13256671002725295, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n52", "nodes": [52]}},
{"command": "model.boundary_condition.create", "params": {"name": "n52", "type": "displacement", "target": {"set": "n52"}, "values": [0.19999999999999996, 0.1, 0.15000000000000013, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n53", "nodes": [53]}},
{"command": "model.boundary_condition.create", "params": {"name": "n53", "type": "displacement", "target": {"set": "n53"}, "values": [-0.040675628024645964, 0.22033781401232294, 0.04067562802464608, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n54", "nodes": [54]}},
{"command": "model.boundary_condition.create", "params": {"name": "n54", "type": "displacement", "target": {"set": "n54"}, "values": [0.08222713753951483, 0.1588864312302426, 0.05248192723913836, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n55", "nodes": [55]}},
{"command": "model.boundary_condition.create", "params": {"name": "n55", "type": "displacement", "target": {"set": "n55"}, "values": [0.18157295726465628, 0.10921352136767187, 0.06213675644258787, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n56", "nodes": [56]}},
{"command": "model.boundary_condition.create", "params": {"name": "n56", "type": "displacement", "target": {"set": "n56"}, "values": [0.2770251790664533, 0.06148741046677342, 0.07297482093354701, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n57", "nodes": [57]}},
{"command": "model.boundary_condition.create", "params": {"name": "n57", "type": "displacement", "target": {"set": "n57"}, "values": [0.04285734769952315, 0.1785713261502384, -0.042857347699523096, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n58", "nodes": [58]}},
{"command": "model.boundary_condition.create", "params": {"name": "n58", "type": "displacement", "target": {"set": "n58"}, "values": [0.11900218897529685, 0.14049890551235156, -0.022524869638100742, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n59", "nodes": [59]}},
{"command": "model.boundary_condition.create", "params": {"name": "n59", "type": "displacement", "target": {"set": "n59"}, "values": [0.21757680746025987, 0.09121159626987008, 0.0010591574336091814, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n60", "nodes": [60]}},
{"command": "model.boundary_condition.create", "params": {"name": "n60", "type": "displacement", "target": {"set": "n60"}, "values": [0.3248668494264886, 0.037566575286755666, 0.02513315057351151, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n61", "nodes": [61]}},
{"command": "model.boundary_condition.create", "params": {"name": "n61", "type": "displacement", "target": {"set": "n61"}, "values": [0.1, 0.1499999999999999, -0.09999999999999987, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n62", "nodes": [62]}},
{"command": "model.boundary_condition.create", "params": {"name": "n62", "type": "displacement", "target": {"set": "n62"}, "values": [0.21784996658814437, 0.09107501670592777, -0.08035833890197586, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n63", "nodes": [63]}},
{"command": "model.boundary_condition.create", "params": {"name": "n63", "type": "displacement", "target": {"set": "n63"}, "values": [0.31546491266741694, 0.042267543666291596, -0.06408918122209706, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n64", "nodes": [64]}},
{"command": "model.boundary_condition.create", "params": {"name": "n64", "type": "displacement", "target": {"set": "n64"}, "values": [0.40000000000000013, 0.0, -0.04999999999999993, null, null, null]}},
{"command": "model.step.create", "params": {"name": "Deform", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["n1", "n2", "n3", "n4", "n5", "n6", "n7", "n8", "n9", "n10", "n11", "n12", "n13", "n14", "n15", "n16", "n17", "n18", "n19", "n20", "n21", "n24", "n25", "n28", "n29", "n30", "n31", "n32", "n33", "n34", "n35", "n36", "n37", "n40", "n41", "n44", "n45", "n46", "n47", "n48", "n49", "n50", "n51", "n52", "n53", "n54", "n55", "n56", "n57", "n58", "n59", "n60", "n61", "n62", "n63", "n64"], "loads": []}},
{"command": "solve.nonlinear", "params": {"step": "Deform"}, "id": "solve"}
]
+6
View File
@@ -0,0 +1,6 @@
# Rigid rotation: objectivity
The distorted 3 × 3 × 3 hex8 cube of nl-patch-hex8 (neo-Hookean, E = 1000, ν = 0.3) with every
boundary node prescribed the motion of a 90° rotation about the axis (1, 1, 1)/√3, applied in
one ramp (the intermediate states are not rigid; the end state is). The inner nodes must follow
the rotation and the stress must vanish.
@@ -0,0 +1,83 @@
{
"title": "A quarter turn strains and stresses nothing (objectivity)",
"kind": "theory",
"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)",
"quantities": [
{
"id": "ux",
"quantity": "u_x at inner node 22: the rotation's",
"measure": {
"command": "solve.probe",
"params": {
"step": "Turn",
"nodes": [
22
]
},
"pointer": "/nodes/0/displacement/0"
},
"value": -0.011149180525904245,
"tolerance": {
"absolute": 1e-09
},
"reason": "the inner nodes follow the rigid rotation exactly"
},
{
"id": "uy",
"quantity": "u_y at inner node 22: the rotation's",
"measure": {
"command": "solve.probe",
"params": {
"step": "Turn",
"nodes": [
22
]
},
"pointer": "/nodes/0/displacement/1"
},
"value": 0.07859051161172886,
"tolerance": {
"absolute": 1e-09
},
"reason": "the inner nodes follow the rigid rotation exactly"
},
{
"id": "uz",
"quantity": "u_z at inner node 22: the rotation's",
"measure": {
"command": "solve.probe",
"params": {
"step": "Turn",
"nodes": [
22
]
},
"pointer": "/nodes/0/displacement/2"
},
"value": -0.06744133108582467,
"tolerance": {
"absolute": 1e-09
},
"reason": "the inner nodes follow the rigid rotation exactly"
},
{
"id": "von_mises",
"quantity": "von Mises stress at inner node 22",
"measure": {
"command": "solve.probe",
"params": {
"step": "Turn",
"nodes": [
22
]
},
"pointer": "/nodes/0/von_mises"
},
"value": 0.0,
"tolerance": {
"absolute": 1e-08
},
"reason": "zero up to rounding of stresses of order E = 1000: a 90° turn leaves no strain"
}
]
}
@@ -0,0 +1,21 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"ux": {
"band": 1.2149180525904365e-11,
"value": -0.011149180525904363
},
"uy": {
"band": 7.959051161172887e-11,
"value": 0.07859051161172881
},
"uz": {
"band": 6.84413310858248e-11,
"value": -0.0674413310858248
},
"von_mises": {
"band": 1e-8,
"value": 4.868377314671631e-13
}
}
}
+210
View File
@@ -0,0 +1,210 @@
[
{"command": "mesh.node.add", "params": {"label": 1, "xyz": [0.0, 0.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 2, "xyz": [0.2962451065991113, 0.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 3, "xyz": [0.6111177857270027, 0.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 4, "xyz": [1.0, 0.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 5, "xyz": [0.0, 0.2910009137991098, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 6, "xyz": [0.3571377677211701, 0.2737737705050953, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 7, "xyz": [0.714575893424076, 0.2779668080365649, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 8, "xyz": [1.0, 0.2966218598767702, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 9, "xyz": [0.0, 0.6967279180541397, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 10, "xyz": [0.3333333333333333, 0.7184592286655991, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 11, "xyz": [0.6295784399324447, 0.7261031080083589, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 12, "xyz": [1.0, 0.7142867384976158, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 13, "xyz": [0.0, 1.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 14, "xyz": [0.31800086721172344, 1.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 15, "xyz": [0.6904711010545035, 1.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 16, "xyz": [1.0, 1.0, 0.0]}},
{"command": "mesh.node.add", "params": {"label": 17, "xyz": [0.0, 0.0, 0.31040904830229277]}},
{"command": "mesh.node.add", "params": {"label": 18, "xyz": [0.2777844523936694, 0.0, 0.340800598743757]}},
{"command": "mesh.node.add", "params": {"label": 19, "xyz": [0.6089828171538932, 0.0, 0.37600300670769227]}},
{"command": "mesh.node.add", "params": {"label": 20, "xyz": [1.0, 0.0, 0.39127279992629]}},
{"command": "mesh.node.add", "params": {"label": 21, "xyz": [0.0, 0.2737737705050953, 0.3696657255165094]}},
{"command": "mesh.node.add", "params": {"label": 22, "xyz": [0.38124256009074275, 0.2779668080365649, 0.34132725581852985]}},
{"command": "mesh.node.add", "params": {"label": 23, "xyz": [0.7261665552938148, 0.2966218598767702, 0.31090073344351915]}},
{"command": "mesh.node.add", "params": {"label": 24, "xyz": [1.0, 0.33381112436048954, 0.28130120256419844]}},
{"command": "mesh.node.add", "params": {"label": 25, "xyz": [0.0, 0.7184592286655991, 0.28103878698851803]}},
{"command": "mesh.node.add", "params": {"label": 26, "xyz": [0.2962451065991113, 0.7261031080083589, 0.31040904830229277]}},
{"command": "mesh.node.add", "params": {"label": 27, "xyz": [0.6111177857270027, 0.7142867384976158, 0.340800598743757]}},
{"command": "mesh.node.add", "params": {"label": 28, "xyz": [1.0, 0.6815367191456442, 0.37600300670769227]}},
{"command": "mesh.node.add", "params": {"label": 29, "xyz": [0.0, 1.0, 0.3914085136552225]}},
{"command": "mesh.node.add", "params": {"label": 30, "xyz": [0.3571377677211701, 1.0, 0.3696657255165094]}},
{"command": "mesh.node.add", "params": {"label": 31, "xyz": [0.714575893424076, 1.0, 0.34132725581852985]}},
{"command": "mesh.node.add", "params": {"label": 32, "xyz": [1.0, 1.0, 0.31090073344351915]}},
{"command": "mesh.node.add", "params": {"label": 33, "xyz": [0.0, 0.0, 0.6741339320770904]}},
{"command": "mesh.node.add", "params": {"label": 34, "xyz": [0.2756494838205599, 0.0, 0.7093363400410255]}},
{"command": "mesh.node.add", "params": {"label": 35, "xyz": [0.6246741854110541, 0.0, 0.7246061332596233]}},
{"command": "mesh.node.add", "params": {"label": 36, "xyz": [1.0, 0.0, 0.7247418469885558]}},
{"command": "mesh.node.add", "params": {"label": 37, "xyz": [0.0, 0.2779668080365649, 0.6746605891518631]}},
{"command": "mesh.node.add", "params": {"label": 38, "xyz": [0.3928332219604814, 0.2966218598767702, 0.6442340667768525]}},
{"command": "mesh.node.add", "params": {"label": 39, "xyz": [0.7182163755580564, 0.33381112436048954, 0.6146345358975318]}},
{"command": "mesh.node.add", "params": {"label": 40, "xyz": [1.0, 0.3633945847208064, 0.6066672542736244]}},
{"command": "mesh.node.add", "params": {"label": 41, "xyz": [0.0, 0.7261031080083589, 0.6437423816356261]}},
{"command": "mesh.node.add", "params": {"label": 42, "xyz": [0.2777844523936694, 0.7142867384976158, 0.6741339320770904]}},
{"command": "mesh.node.add", "params": {"label": 43, "xyz": [0.6089828171538932, 0.6815367191456442, 0.7093363400410255]}},
{"command": "mesh.node.add", "params": {"label": 44, "xyz": [1.0, 0.6508727591847184, 0.7246061332596233]}},
{"command": "mesh.node.add", "params": {"label": 45, "xyz": [0.0, 1.0, 0.7029990588498427]}},
{"command": "mesh.node.add", "params": {"label": 46, "xyz": [0.38124256009074275, 1.0, 0.6746605891518631]}},
{"command": "mesh.node.add", "params": {"label": 47, "xyz": [0.7261665552938148, 1.0, 0.6442340667768525]}},
{"command": "mesh.node.add", "params": {"label": 48, "xyz": [1.0, 1.0, 0.6146345358975318]}},
{"command": "mesh.node.add", "params": {"label": 49, "xyz": [0.0, 0.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 50, "xyz": [0.29134085207772076, 0.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 51, "xyz": [0.6513342005450568, 0.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 52, "xyz": [1.0, 0.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 53, "xyz": [0.0, 0.2966218598767702, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 54, "xyz": [0.3848830422247231, 0.33381112436048954, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 55, "xyz": [0.6963134677349831, 0.3633945847208064, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 56, "xyz": [1.0, 0.3851258953322657, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 57, "xyz": [0.0, 0.7142867384976158, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 58, "xyz": [0.2756494838205599, 0.6815367191456442, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 59, "xyz": [0.6246741854110541, 0.6508727591847184, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 60, "xyz": [1.0, 0.6243342471324431, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 61, "xyz": [0.0, 1.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 62, "xyz": [0.3928332219604814, 1.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 63, "xyz": [0.7182163755580564, 1.0, 1.0]}},
{"command": "mesh.node.add", "params": {"label": 64, "xyz": [1.0, 1.0, 1.0]}},
{"command": "mesh.element.add", "params": {"label": 1, "kind": "hex8", "nodes": [1, 2, 6, 5, 17, 18, 22, 21]}},
{"command": "mesh.element.add", "params": {"label": 2, "kind": "hex8", "nodes": [2, 3, 7, 6, 18, 19, 23, 22]}},
{"command": "mesh.element.add", "params": {"label": 3, "kind": "hex8", "nodes": [3, 4, 8, 7, 19, 20, 24, 23]}},
{"command": "mesh.element.add", "params": {"label": 4, "kind": "hex8", "nodes": [5, 6, 10, 9, 21, 22, 26, 25]}},
{"command": "mesh.element.add", "params": {"label": 5, "kind": "hex8", "nodes": [6, 7, 11, 10, 22, 23, 27, 26]}},
{"command": "mesh.element.add", "params": {"label": 6, "kind": "hex8", "nodes": [7, 8, 12, 11, 23, 24, 28, 27]}},
{"command": "mesh.element.add", "params": {"label": 7, "kind": "hex8", "nodes": [9, 10, 14, 13, 25, 26, 30, 29]}},
{"command": "mesh.element.add", "params": {"label": 8, "kind": "hex8", "nodes": [10, 11, 15, 14, 26, 27, 31, 30]}},
{"command": "mesh.element.add", "params": {"label": 9, "kind": "hex8", "nodes": [11, 12, 16, 15, 27, 28, 32, 31]}},
{"command": "mesh.element.add", "params": {"label": 10, "kind": "hex8", "nodes": [17, 18, 22, 21, 33, 34, 38, 37]}},
{"command": "mesh.element.add", "params": {"label": 11, "kind": "hex8", "nodes": [18, 19, 23, 22, 34, 35, 39, 38]}},
{"command": "mesh.element.add", "params": {"label": 12, "kind": "hex8", "nodes": [19, 20, 24, 23, 35, 36, 40, 39]}},
{"command": "mesh.element.add", "params": {"label": 13, "kind": "hex8", "nodes": [21, 22, 26, 25, 37, 38, 42, 41]}},
{"command": "mesh.element.add", "params": {"label": 14, "kind": "hex8", "nodes": [22, 23, 27, 26, 38, 39, 43, 42]}},
{"command": "mesh.element.add", "params": {"label": 15, "kind": "hex8", "nodes": [23, 24, 28, 27, 39, 40, 44, 43]}},
{"command": "mesh.element.add", "params": {"label": 16, "kind": "hex8", "nodes": [25, 26, 30, 29, 41, 42, 46, 45]}},
{"command": "mesh.element.add", "params": {"label": 17, "kind": "hex8", "nodes": [26, 27, 31, 30, 42, 43, 47, 46]}},
{"command": "mesh.element.add", "params": {"label": 18, "kind": "hex8", "nodes": [27, 28, 32, 31, 43, 44, 48, 47]}},
{"command": "mesh.element.add", "params": {"label": 19, "kind": "hex8", "nodes": [33, 34, 38, 37, 49, 50, 54, 53]}},
{"command": "mesh.element.add", "params": {"label": 20, "kind": "hex8", "nodes": [34, 35, 39, 38, 50, 51, 55, 54]}},
{"command": "mesh.element.add", "params": {"label": 21, "kind": "hex8", "nodes": [35, 36, 40, 39, 51, 52, 56, 55]}},
{"command": "mesh.element.add", "params": {"label": 22, "kind": "hex8", "nodes": [37, 38, 42, 41, 53, 54, 58, 57]}},
{"command": "mesh.element.add", "params": {"label": 23, "kind": "hex8", "nodes": [38, 39, 43, 42, 54, 55, 59, 58]}},
{"command": "mesh.element.add", "params": {"label": 24, "kind": "hex8", "nodes": [39, 40, 44, 43, 55, 56, 60, 59]}},
{"command": "mesh.element.add", "params": {"label": 25, "kind": "hex8", "nodes": [41, 42, 46, 45, 57, 58, 62, 61]}},
{"command": "mesh.element.add", "params": {"label": 26, "kind": "hex8", "nodes": [42, 43, 47, 46, 58, 59, 63, 62]}},
{"command": "mesh.element.add", "params": {"label": 27, "kind": "hex8", "nodes": [43, 44, 48, 47, 59, 60, 64, 63]}},
{"command": "model.set.create", "params": {"name": "all", "elements": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]}},
{"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}, "hyperelastic": {"type": "neo_hookean"}}},
{"command": "model.section.create", "params": {"name": "s", "set": "all", "material": "Rubber", "kind": {"type": "solid"}}},
{"command": "model.set.create", "params": {"name": "n1", "nodes": [1]}},
{"command": "model.boundary_condition.create", "params": {"name": "n1", "type": "displacement", "target": {"set": "n1"}, "values": [0.0, 0.0, 0.0, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n2", "nodes": [2]}},
{"command": "model.boundary_condition.create", "params": {"name": "n2", "type": "displacement", "target": {"set": "n2"}, "values": [-0.1974967377327409, 0.26978556090747674, -0.07228882317473585, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n3", "nodes": [3]}},
{"command": "model.boundary_condition.create", "params": {"name": "n3", "type": "displacement", "target": {"set": "n3"}, "values": [-0.4074118571513352, 0.5565349466717207, -0.14912308952038547, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n4", "nodes": [4]}},
{"command": "model.boundary_condition.create", "params": {"name": "n4", "type": "displacement", "target": {"set": "n4"}, "values": [-0.6666666666666669, 0.9106836025229592, -0.24401693585629242, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n5", "nodes": [5]}},
{"command": "model.boundary_condition.create", "params": {"name": "n5", "type": "displacement", "target": {"set": "n5"}, "values": [-0.07100915131663986, -0.1940006091994066, 0.2650097605160464, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n6", "nodes": [6]}},
{"command": "model.boundary_condition.create", "params": {"name": "n6", "type": "displacement", "target": {"set": "n6"}, "values": [-0.304897281743924, 0.1427236619019261, 0.16217361984199785, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n7", "nodes": [7]}},
{"command": "model.boundary_condition.create", "params": {"name": "n7", "type": "displacement", "target": {"set": "n7"}, "values": [-0.544212537716221, 0.4654413435417897, 0.07877119417443118, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n8", "nodes": [8]}},
{"command": "model.boundary_condition.create", "params": {"name": "n8", "type": "displacement", "target": {"set": "n8"}, "values": [-0.7390474240217908, 0.7129356959384456, 0.026111728083345054, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n9", "nodes": [9]}},
{"command": "model.boundary_condition.create", "params": {"name": "n9", "type": "displacement", "target": {"set": "n9"}, "values": [-0.17001341168910516, -0.4644852787027599, 0.634498690391865, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n10", "nodes": [10]}},
{"command": "model.boundary_condition.create", "params": {"name": "n10", "type": "displacement", "target": {"set": "n10"}, "values": [-0.3975384417388771, -0.1754116182694132, 0.5729500600082902, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n11", "nodes": [11]}},
{"command": "model.boundary_condition.create", "params": {"name": "n11", "type": "displacement", "target": {"set": "n11"}, "values": [-0.5969004154868935, 0.08927802307622379, 0.5076223924106696, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n12", "nodes": [12]}},
{"command": "model.boundary_condition.create", "params": {"name": "n12", "type": "displacement", "target": {"set": "n12"}, "values": [-0.8409647279176399, 0.43449244352454863, 0.4064722843930913, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n13", "nodes": [13]}},
{"command": "model.boundary_condition.create", "params": {"name": "n13", "type": "displacement", "target": {"set": "n13"}, "values": [-0.24401693585629242, -0.6666666666666669, 0.9106836025229592, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n14", "nodes": [14]}},
{"command": "model.boundary_condition.create", "params": {"name": "n14", "type": "displacement", "target": {"set": "n14"}, "values": [-0.45601751399744145, -0.3770684913088693, 0.8330860053063107, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n15", "nodes": [15]}},
{"command": "model.boundary_condition.create", "params": {"name": "n15", "type": "displacement", "target": {"set": "n15"}, "values": [-0.7043310032259615, -0.037865956920357435, 0.7421969601463189, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n16", "nodes": [16]}},
{"command": "model.boundary_condition.create", "params": {"name": "n16", "type": "displacement", "target": {"set": "n16"}, "values": [-0.9106836025229592, 0.24401693585629225, 0.6666666666666667, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n17", "nodes": [17]}},
{"command": "model.boundary_condition.create", "params": {"name": "n17", "type": "displacement", "target": {"set": "n17"}, "values": [0.28268443036365526, -0.07574506482879335, -0.20693936553486192, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n18", "nodes": [18]}},
{"command": "model.boundary_condition.create", "params": {"name": "n18", "type": "displacement", "target": {"set": "n18"}, "values": [0.1251718820768331, 0.16981262798729288, -0.29498451006412607, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n19", "nodes": [19]}},
{"command": "model.boundary_condition.create", "params": {"name": "n19", "type": "displacement", "target": {"set": "n19"}, "values": [-0.06356877206123668, 0.46283956423072403, -0.3992707921694874, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n20", "nodes": [20]}},
{"command": "model.boundary_condition.create", "params": {"name": "n20", "type": "displacement", "target": {"set": "n20"}, "values": [-0.310340943660548, 0.8152064128010338, -0.5048654691404858, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n21", "nodes": [21]}},
{"command": "model.boundary_condition.create", "params": {"name": "n21", "type": "displacement", "target": {"set": "n21"}, "values": [0.26984307804616103, -0.2727205446350288, 0.002877466588867783, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n24", "nodes": [24]}},
{"command": "model.boundary_condition.create", "params": {"name": "n24", "type": "displacement", "target": {"set": "n24"}, "values": [-0.49194584184265233, 0.6195005954468935, -0.12755475360424126, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n25", "nodes": [25]}},
{"command": "model.boundary_condition.create", "params": {"name": "n25", "type": "displacement", "target": {"set": "n25"}, "values": [0.08062119546673135, -0.547551042768107, 0.46692984730137566, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n28", "nodes": [28]}},
{"command": "model.boundary_condition.create", "params": {"name": "n28", "type": "displacement", "target": {"set": "n28"}, "values": [-0.49055339583811186, 0.36457468818963223, 0.12597870764847952, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n29", "nodes": [29]}},
{"command": "model.boundary_condition.create", "params": {"name": "n29", "type": "displacement", "target": {"set": "n29"}, "values": [0.11243237941740247, -0.76217697283688, 0.6497445934194774, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n30", "nodes": [30]}},
{"command": "model.boundary_condition.create", "params": {"name": "n30", "type": "displacement", "target": {"set": "n30"}, "values": [-0.14546026636110104, -0.43163185539297577, 0.5770921217540765, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n31", "nodes": [31]}},
{"command": "model.boundary_condition.create", "params": {"name": "n31", "type": "displacement", "target": {"set": "n31"}, "values": [-0.4095597298375821, -0.09920374885624161, 0.5087634786938237, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n32", "nodes": [32]}},
{"command": "model.boundary_condition.create", "params": {"name": "n32", "type": "displacement", "target": {"set": "n32"}, "values": [-0.627551402563585, 0.1681518915259308, 0.4593995110376539, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n33", "nodes": [33]}},
{"command": "model.boundary_condition.create", "params": {"name": "n33", "type": "displacement", "target": {"set": "n33"}, "values": [0.6139227178469325, -0.16450009646220556, -0.44942262138472705, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n34", "nodes": [34]}},
{"command": "model.boundary_condition.create", "params": {"name": "n34", "type": "displacement", "target": {"set": "n34"}, "values": [0.46221465100197195, 0.07793938477097354, -0.5401540357729455, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n35", "nodes": [35]}},
{"command": "model.boundary_condition.create", "params": {"name": "n35", "type": "displacement", "target": {"set": "n35"}, "values": [0.24343746690640233, 0.3920643692325442, -0.6355018361389466, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n36", "nodes": [36]}},
{"command": "model.boundary_condition.create", "params": {"name": "n36", "type": "displacement", "target": {"set": "n36"}, "values": [-0.00665615055198554, 0.7338343177339819, -0.7271781671819965, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n37", "nodes": [37]}},
{"command": "model.boundary_condition.create", "params": {"name": "n37", "type": "displacement", "target": {"set": "n37"}, "values": [0.5465737270422439, -0.3499398150655486, -0.19663391197669544, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n40", "nodes": [40]}},
{"command": "model.boundary_condition.create", "params": {"name": "n40", "type": "displacement", "target": {"set": "n40"}, "values": [-0.20285917908239148, 0.5203834615702214, -0.31752428248783016, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n41", "nodes": [41]}},
{"command": "model.boundary_condition.create", "params": {"name": "n41", "type": "displacement", "target": {"set": "n41"}, "values": [0.4090641756727114, -0.6411527821197969, 0.2320886064470854, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n44", "nodes": [44]}},
{"command": "model.boundary_condition.create", "params": {"name": "n44", "type": "displacement", "target": {"set": "n44"}, "values": [-0.1656037191481471, 0.29995226139245723, -0.13434854224431025, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n45", "nodes": [45]}},
{"command": "model.boundary_condition.create", "params": {"name": "n45", "type": "displacement", "target": {"set": "n45"}, "values": [0.3961927796273322, -0.8382103429170629, 0.4420175632897305, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n46", "nodes": [46]}},
{"command": "model.boundary_condition.create", "params": {"name": "n46", "type": "displacement", "target": {"set": "n46"}, "values": [0.11622369322562642, -0.48410392831599214, 0.3678802350903656, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n47", "nodes": [47]}},
{"command": "model.boundary_condition.create", "params": {"name": "n47", "type": "displacement", "target": {"set": "n47"}, "values": [-0.14143457191847508, -0.16256271500913355, 0.3039972869276085, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n48", "nodes": [48]}},
{"command": "model.boundary_condition.create", "params": {"name": "n48", "type": "displacement", "target": {"set": "n48"}, "values": [-0.350946009136768, 0.09403569973512238, 0.2569103094016455, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n49", "nodes": [49]}},
{"command": "model.boundary_condition.create", "params": {"name": "n49", "type": "displacement", "target": {"set": "n49"}, "values": [0.9106836025229592, -0.24401693585629242, -0.6666666666666669, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n50", "nodes": [50]}},
{"command": "model.boundary_condition.create", "params": {"name": "n50", "type": "displacement", "target": {"set": "n50"}, "values": [0.7164563678044786, 0.021302400875954863, -0.7377587686804337, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n51", "nodes": [51]}},
{"command": "model.boundary_condition.create", "params": {"name": "n51", "type": "displacement", "target": {"set": "n51"}, "values": [0.47646080215958786, 0.3491424403424915, -0.8256032425020795, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n52", "nodes": [52]}},
{"command": "model.boundary_condition.create", "params": {"name": "n52", "type": "displacement", "target": {"set": "n52"}, "values": [0.24401693585629225, 0.6666666666666667, -0.9106836025229592, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n53", "nodes": [53]}},
{"command": "model.boundary_condition.create", "params": {"name": "n53", "type": "displacement", "target": {"set": "n53"}, "values": [0.8383028451678353, -0.44176484244080594, -0.3965380027270293, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n54", "nodes": [54]}},
{"command": "model.boundary_condition.create", "params": {"name": "n54", "type": "displacement", "target": {"set": "n54"}, "values": [0.5726393399852867, -0.11605100998674517, -0.4565883299985416, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n55", "nodes": [55]}},
{"command": "model.boundary_condition.create", "params": {"name": "n55", "type": "displacement", "target": {"set": "n55"}, "values": [0.3578001909626294, 0.14784126494531863, -0.5056414559079481, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n56", "nodes": [56]}},
{"command": "model.boundary_condition.create", "params": {"name": "n56", "type": "displacement", "target": {"set": "n56"}, "values": [0.15003969495840175, 0.4099160697784896, -0.5599557647368913, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n57", "nodes": [57]}},
{"command": "model.boundary_condition.create", "params": {"name": "n57", "type": "displacement", "target": {"set": "n57"}, "values": [0.7363855412719862, -0.720208094854703, -0.016177446417283137, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n58", "nodes": [58]}},
{"command": "model.boundary_condition.create", "params": {"name": "n58", "type": "displacement", "target": {"set": "n58"}, "values": [0.5606107780964487, -0.44734528366075366, -0.11326549443569511, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n59", "nodes": [59]}},
{"command": "model.boundary_condition.create", "params": {"name": "n59", "type": "displacement", "target": {"set": "n59"}, "values": [0.33541016925367084, -0.10905157107287111, -0.22635859818079984, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n60", "nodes": [60]}},
{"command": "model.boundary_condition.create", "params": {"name": "n60", "type": "displacement", "target": {"set": "n60"}, "values": [0.09166880592088833, 0.250443835245038, -0.34211264116592655, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n61", "nodes": [61]}},
{"command": "model.boundary_condition.create", "params": {"name": "n61", "type": "displacement", "target": {"set": "n61"}, "values": [0.6666666666666667, -0.9106836025229592, 0.24401693585629225, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n62", "nodes": [62]}},
{"command": "model.boundary_condition.create", "params": {"name": "n62", "type": "displacement", "target": {"set": "n62"}, "values": [0.40477785202634575, -0.5529368287572868, 0.1481589767309408, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n63", "nodes": [63]}},
{"command": "model.boundary_condition.create", "params": {"name": "n63", "type": "displacement", "target": {"set": "n63"}, "values": [0.18785574962796237, -0.25661572623876583, 0.06875997661080335, null, null, null]}},
{"command": "model.set.create", "params": {"name": "n64", "nodes": [64]}},
{"command": "model.boundary_condition.create", "params": {"name": "n64", "type": "displacement", "target": {"set": "n64"}, "values": [0.0, 0.0, 0.0, null, null, null]}},
{"command": "model.step.create", "params": {"name": "Turn", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["n1", "n2", "n3", "n4", "n5", "n6", "n7", "n8", "n9", "n10", "n11", "n12", "n13", "n14", "n15", "n16", "n17", "n18", "n19", "n20", "n21", "n24", "n25", "n28", "n29", "n30", "n31", "n32", "n33", "n34", "n35", "n36", "n37", "n40", "n41", "n44", "n45", "n46", "n47", "n48", "n49", "n50", "n51", "n52", "n53", "n54", "n55", "n56", "n57", "n58", "n59", "n60", "n61", "n62", "n63", "n64"], "loads": []}},
{"command": "solve.nonlinear", "params": {"step": "Turn"}, "id": "solve"}
]