forked from 1b-agent/fieldcraft-upstream
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ca2dddeec | ||
|
|
2183fdbe81 | ||
|
|
2321d75cc6 | ||
|
|
bc9fe2191a |
No files matched your search
+5
-4
@@ -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) |
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -95,6 +95,7 @@ pub static ALL: &[CommandSpec] = &[
|
||||
deck::WRITE,
|
||||
solve::RUN,
|
||||
solve::STATIC,
|
||||
solve::NONLINEAR,
|
||||
solve::FREQUENCY,
|
||||
solve::HEAT,
|
||||
solve::EXPLICIT,
|
||||
|
||||
@@ -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")))?;
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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, ¤t, &c4, at.dv);
|
||||
for (a, ga) in current.iter().enumerate() {
|
||||
for (b, gb) in current.iter().enumerate() {
|
||||
let s = ga[0] * gb[0] * tau[0] + ga[1] * gb[1] * tau[1] + (ga[0] * gb[1] + ga[1] * gb[0]) * tau[3];
|
||||
k.add(2 * a, 2 * b, at.dv * (s + tau[2] * ga[2] * gb[2]));
|
||||
k.add(2 * a + 1, 2 * b + 1, at.dv * s);
|
||||
}
|
||||
}
|
||||
}
|
||||
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(_))));
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
@@ -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())?;
|
||||
|
||||
@@ -322,6 +322,7 @@ impl Model {
|
||||
Procedure::ExplicitDynamic(_) => "is dynamic",
|
||||
Procedure::HeatTransfer { transient: Some(_) } => "is transient heat transfer",
|
||||
Procedure::Static {}
|
||||
| Procedure::NonlinearStatic(_)
|
||||
| Procedure::Fluid { .. }
|
||||
| Procedure::HeatTransfer { transient: None }
|
||||
| Procedure::CoupledTemperatureDisplacement {} => {
|
||||
@@ -469,7 +470,7 @@ impl Model {
|
||||
}
|
||||
}
|
||||
match &s.procedure {
|
||||
Procedure::Static {} => {
|
||||
Procedure::Static {} | Procedure::NonlinearStatic(_) => {
|
||||
if s.loads.is_empty() && !prescribes_motion {
|
||||
d.warning(&subject, "nothing loads the model in this step");
|
||||
}
|
||||
|
||||
@@ -57,12 +57,13 @@ pub use discrete::{Direction, Mass, PointMass, ResolvedDirection, ResolvedMass,
|
||||
pub use fluids::{BoxFace, BoxFaces, FaceCondition, FluidBoundary, FluidDomain, Profile, SurfaceBoundary, SurfaceCondition};
|
||||
pub use geometry::{Classification, Entity, GeometryRef};
|
||||
pub use loads::{Distribution, Load, centrifugal_acceleration};
|
||||
pub use materials::{Damage, Directional, Elastic, Erosion, Fluid, Hardening, HardeningPoint, Material, Plastic, Thermal};
|
||||
pub use materials::{Damage, Directional, Elastic, Erosion, Fluid, Hardening, HardeningPoint, Hyperelastic, Material, Plastic, Thermal};
|
||||
pub use mesh::{Element, ElementEdge, ElementFace, ElementKind, Node, check_coordinates};
|
||||
pub use sections::{BeamProfile, Extent, Formulation, Integration, Section, SectionKind};
|
||||
pub use sets::Set;
|
||||
pub use steps::{
|
||||
BulkViscosity, ExplicitDynamic, Hourglass, HourglassKind, InitialVelocity, OutputVariable, Procedure, RigidWall, Step, StressRate, Transient,
|
||||
BulkViscosity, ExplicitDynamic, Hourglass, HourglassKind, InitialVelocity, MAX_NONLINEAR_INCREMENTS, MAX_NONLINEAR_ITERATIONS, NonlinearStatic,
|
||||
OutputVariable, Procedure, RigidWall, Step, StressRate, Tolerances, Transient,
|
||||
};
|
||||
pub use units::UnitSystem;
|
||||
|
||||
|
||||
@@ -74,6 +74,37 @@ impl Elastic {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hyperelastic behaviour of an isotropic material at large strain, in nonlinear steps.
|
||||
///
|
||||
/// The constants are the material's isotropic elastic ones: the shear modulus μ = E / (2(1 + ν))
|
||||
/// and the bulk modulus κ = E / (3(1 − 2ν)), so at small strain the material is that linear
|
||||
/// elastic one (linear steps use it so). Without this option an elastic material in a nonlinear
|
||||
/// step is St Venant–Kirchhoff (the second Piola–Kirchhoff stress linear in the Green–Lagrange
|
||||
/// strain), which is right for large rotations with small strains.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Hyperelastic {
|
||||
/// Compressible neo-Hookean with the isochoric–volumetric split: the strain energy per
|
||||
/// reference volume W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)², with Ī₁ = J^(−2/3) tr C (C₁₀ = μ/2,
|
||||
/// D₁ = 2/κ in the usual polynomial notation).
|
||||
NeoHookean {},
|
||||
}
|
||||
|
||||
impl Hyperelastic {
|
||||
/// The shear and bulk moduli μ = E / (2(1 + ν)) and κ = E / (3(1 − 2ν)) the model takes from
|
||||
/// the isotropic constants.
|
||||
pub fn moduli(young: f64, poisson: f64) -> (f64, f64) {
|
||||
(young / (2.0 * (1.0 + poisson)), young / (3.0 * (1.0 - 2.0 * poisson)))
|
||||
}
|
||||
|
||||
/// The name of the model, for messages.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Hyperelastic::NeoHookean {} => "neo-Hookean",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fluid behaviour.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
@@ -332,6 +363,9 @@ pub struct Erosion {
|
||||
pub struct Material {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub elastic: Option<Elastic>,
|
||||
/// Large-strain behaviour in nonlinear steps, built on the isotropic elastic constants.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub hyperelastic: Option<Hyperelastic>,
|
||||
/// Mass per volume, needed by frequency steps, gravity and transient heat transfer.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub density: Option<f64>,
|
||||
@@ -366,6 +400,14 @@ impl Material {
|
||||
if let Some(e) = &self.elastic {
|
||||
e.validate()?;
|
||||
}
|
||||
if let Some(h) = &self.hyperelastic {
|
||||
if !matches!(self.elastic, Some(Elastic::Isotropic { .. })) {
|
||||
return Err(Error::Invalid(format!("{} hyperelasticity needs isotropic elastic constants", h.name())));
|
||||
}
|
||||
if self.plastic.is_some() {
|
||||
return Err(Error::Invalid(format!("plasticity of a {} hyperelastic material is not supported yet", h.name())));
|
||||
}
|
||||
}
|
||||
if let Some(d) = self.density {
|
||||
positive("density", d)?;
|
||||
}
|
||||
@@ -412,6 +454,11 @@ impl Model {
|
||||
self.materials.get(name)
|
||||
}
|
||||
|
||||
/// The hyperelastic materials some section uses, by name.
|
||||
pub fn hyperelastic_in_use(&self) -> std::collections::BTreeSet<&str> {
|
||||
self.sections.values().map(|s| s.material.as_str()).filter(|m| self.material(m).is_some_and(|m| m.hyperelastic.is_some())).collect()
|
||||
}
|
||||
|
||||
pub fn add_material(&mut self, name: &str, material: Material) -> Result<()> {
|
||||
material.validate().map_err(|e| context("material", name, e))?;
|
||||
insert_new(&mut self.materials, "material", name, material)
|
||||
@@ -544,6 +591,25 @@ mod tests {
|
||||
assert_eq!(serde_json::from_str::<Material>(&text).unwrap(), Material { plastic: Some(jc()), damage: Some(damage), ..steel() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyperelasticity_builds_on_isotropic_constants() {
|
||||
let m: Material = serde_json::from_value(serde_json::json!({
|
||||
"elastic": {"type": "isotropic", "young": 3.0, "poisson": 0.49}, "hyperelastic": {"type": "neo_hookean"}
|
||||
}))
|
||||
.unwrap();
|
||||
m.validate().unwrap();
|
||||
assert_eq!(m.hyperelastic, Some(Hyperelastic::NeoHookean {}));
|
||||
let back: Material = serde_json::from_value(serde_json::to_value(&m).unwrap()).unwrap();
|
||||
assert_eq!(back, m);
|
||||
assert!(serde_json::from_value::<Material>(serde_json::json!({"hyperelastic": {"type": "neo_hookean", "c10": 1}})).is_err());
|
||||
let alone = Material { hyperelastic: Some(Hyperelastic::NeoHookean {}), density: Some(1.0), ..Material::default() };
|
||||
assert!(alone.validate().is_err(), "needs isotropic constants");
|
||||
let ortho = Elastic::EngineeringConstants { young: [1.0; 3], poisson: [0.0; 3], shear: [1.0; 3] };
|
||||
assert!(Material { elastic: Some(ortho), ..alone }.validate().is_err());
|
||||
let plastic = Plastic::Bilinear { yield_stress: 1.0, hardening_modulus: 0.0, hardening: Hardening::Isotropic };
|
||||
assert!(Material { plastic: Some(plastic), ..m }.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thermal_properties() {
|
||||
let m: Material = serde_json::from_value(serde_json::json!({
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -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")))
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
@@ -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"}
|
||||
]
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
Reference in new issue
Block a user