forked from 1b-agent/fieldcraft-upstream
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ecb7c6883 | ||
|
|
98d2f55407 | ||
|
|
7f5399c7e9 | ||
|
|
b6ce7aee69 | ||
|
|
8ca2dddeec | ||
|
|
2183fdbe81 | ||
|
|
2321d75cc6 | ||
|
|
bc9fe2191a |
No files matched your search
@@ -48,6 +48,7 @@ is committed and listed here with its exports.
|
||||
| assets/icons/mesh.element.add.svg, assets/icons/mesh.element.add.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.element.add.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.hex.decompose.svg, assets/icons/mesh.hex.decompose.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.decompose.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.hex.dominant.svg, assets/icons/mesh.hex.dominant.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.dominant.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.hex.blocks.svg, assets/icons/mesh.hex.blocks.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.blocks.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.hex.mapped.svg, assets/icons/mesh.hex.mapped.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.mapped.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.hex.octree.svg, assets/icons/mesh.hex.octree.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.octree.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/mesh.hex.polycube.svg, assets/icons/mesh.hex.polycube.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.polycube.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
|
||||
+6
-5
@@ -31,7 +31,7 @@ pressure) is a degree of freedom of the existing DOF framework, not a separate s
|
||||
| M2 Tet meshing and size field | Size field (global, per face and edge seeds, curvature and proximity; the `fieldcraft_mesh::size::SizeField` trait and edge-division helpers are in); Bowyer–Watson Delaunay (exact, symbolic perturbation, verified; `fieldcraft-mesh::delaunay`, done); robust CDT boundary recovery (Diazzi et al. 2023: implicit LNC Steiner points on segments, gift-wrapping facet recovery, inside/outside labelling, B-rep face tags on boundary tet faces, few-ulp rounding repair checked by the validity gate; `fieldcraft-mesh::cdt` and `mesh.tet.generate`, done: all 20 valid corpus parts); Delaunay refinement, ODT smoothing, sliver removal; tet4 and tet10 with mid-side nodes on the B-rep; mesh verify per set | in progress |
|
||||
| M3 First deck | Sets and surfaces bound to SolidCraft face, edge, vertex and body names, resolved after meshing; materials (isotropic and orthotropic elastic, density) with a unit-system label and consistency check; sections (solid, shell, beam profiles) assigned by set; element formulation per region mapped to each solver; BCs (fixed, displacement, symmetry); loads (force and moment on reference points, pressure, gravity); reference points with kinematic and distributing coupling; steps (linear static, frequency) with explicit per-step load and BC tables and output presets; `model.check` naming every missing or bad item; `model.*` and `mesh.tag` commands (model definition: done); writers for Abaqus input (done: `deck.write`), Nastran SOL 101/103 (done: `deck.write`) and LS-DYNA implicit (done); the `.fieldcraft` file (text definition, binary mesh, geometry references reopened on load, journal; atomic saves, autosave and recovery; undo/redo on the journal: done) | in progress |
|
||||
| M4 Own solver and first results | Element library per docs/elements.md (1D and 2D first); Linear static and modal solver (tet4/10, wedge6, hex8, hex20; sparse direct, Lanczos), verified against analytic solutions (solver: done — `fieldcraft-fem` kernels, `fieldcraft-solver` with nested dissection, sparse Cholesky and block Lanczos, `solve.static`/`solve.frequency`/`solve.probe`, patch, cantilever, Lamé, Kirsch and beam-frequency tests; reference points with kinematic and distributing couplings and moments by master–slave constraint elimination, verified by rigid motion, clamping, Saint-Venant tension, bending and tube torsion; 1D elements I: rod2/rod3 trusses, point masses, rotary inertia, springs and dashpots in both solvers and all three decks, verified by the method of joints, the three-bar truss, spring–mass, two-mass chain and torsional pendulum frequencies, and damped decay; 2D elements: plane stress, plane strain and axisymmetric tri3, tri6, quad4 (incompatible modes QM6), quad8 with two degrees of freedom per node, centrifugal loads, `mesh.planar` 2D meshes of polygons and planar faces, verified by patch tests, Kirsch, Lamé (plane strain and axisymmetric), rotating discs and cylinders, and a plane stress cantilever; deck mapping to Abaqus CPS/CPE/CAX, Nastran membranes, LS-DYNA ELFORM 12/13); shells and beams (done: quad4 MITC4 and tri3 MITC3 flat shells with a Hughes–Brezzi drilling rotation, two-node Timoshenko beams with profile properties and orientations, six degrees of freedom at shell and beam nodes shared with solids and trusses, section forces and surface stresses, verified by shell patch tests, Navier's plate, cantilever and portal-frame beams, and the Scordelis–Lo and pinched-cylinder benchmarks); results display (done: `results.*` commands, deformed shape to scale, banded contours of displacement, von Mises, principal and component stresses with a legend, mode shapes with their frequencies and animation, probing a node by clicking it, reaction force and moment sums per set, stale results marked after an edit; explicit steps: field frames by slider or animation with contours per frame, energy histories plotted) | started |
|
||||
| M5 Structured hex | 2D quad meshing of faces (`fieldcraft-mesh::quad`: transfinite mapped meshes, free all-quad meshes of polygons with holes on prescribed boundary nodes, exact planar quad validity, shared `SizeField` and even edge divisions; done for planar faces); mapped hex of six-sided regions (trivariate TFI, curved faces) and sweep meshing (multi-level extrusions with imprinted cross-sections, revolutions, one-to-one sweeps through linking faces; interval assignment, hex8/hex20 on the geometry, B-rep face tags, `mesh.hex.generate`/`sweep`/`mapped`: done, 16 of 21 corpus parts all-hex); conforming interfaces and tet–hex transition; automatic decomposition into sweepable and mappable regions, proposed as SolidCraft splits | started |
|
||||
| M5 Structured hex | 2D quad meshing of faces (`fieldcraft-mesh::quad`: transfinite mapped meshes, free all-quad meshes of polygons with holes on prescribed boundary nodes, exact planar quad validity, shared `SizeField` and even edge divisions; done for planar faces); mapped hex of six-sided regions (trivariate TFI, curved faces) and sweep meshing (multi-level extrusions with imprinted cross-sections, revolutions, one-to-one sweeps through linking faces; interval assignment, hex8/hex20 on the geometry, B-rep face tags, `mesh.hex.generate`/`sweep`/`mapped`: done, 16 of 21 corpus parts all-hex); conforming interfaces between cells (done: `mesh.hex.blocks` meshes a part split with SolidCraft's Split Body into mapped blocks as one conforming mesh — composite faces for seams and tangent faces, interval assignment over all cells, shared faces meshed once, grading off holes and fillets, free-internal-face check; the filleted bracket in 30 cells, hex20 within 0.1 % of a fine tet10 tip deflection); sweepable cells in a conforming mesh and tet–hex transition; automatic decomposition into sweepable and mappable regions, proposed as SolidCraft splits | started |
|
||||
| M6 Automatic hex | Octree/grid-dual all-hex for any part; hex-dominant by tet recombination; polycube for blocky parts; smoothing, untangling, boundary padding, layers through thin walls; mesh editing and orphan-mesh import | planned |
|
||||
| M7 Assemblies, contact and friction | Needs M15. Part instances from SolidCraft occurrences; tie between non-matching meshes (mortar); implicit contact: surface-to-surface mortar and node-to-surface, penalty and augmented Lagrange, self-contact, automatic pair detection with a bounding-volume search shared with explicit contact; friction: Coulomb with stick–slip, static/kinetic, anisotropic, pressure-, rate- and temperature-dependent coefficients; contact output (pressure, slip, status); thermal contact conductance and frictional heating; wear (Archard); bolt pretension, connectors with behaviour (elastic, damped, stops, locks); amplitudes, multiple steps, restart; nonlinear steps in all three decks. Verified by Hertz (sphere and cylinder), the Cattaneo–Mindlin partial-slip problem, the sliding block, NAFEMS contact benchmarks | planned |
|
||||
| M8 Materials, explicit, shells, composites | Plasticity, hyperelasticity, damage data per solver; explicit dynamics decks (Abaqus/Explicit, LS-DYNA); midsurface shells; composite layups and material orientations, continuum shells on swept hex; hybrid (mixed u–p) hex8/hex20/tet10 for nearly incompressible materials (done); heat transfer (solids done: steady and θ-method transient conduction with flux, convection and sources, thermal strain in static steps (solids and 2D) and explicit dynamics (with plasticity), coupled temperature–displacement, `solve.heat`; deck output, 2D/shell/beam conduction planned) | started |
|
||||
@@ -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) |
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
|
||||
<title>mesh.hex.blocks</title>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<path d="M12 3 L20 7 L20 16 L12 20 L4 16 L4 7 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 7 L12 11 L20 7 M12 11 L12 20" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 8 L6 17 M10 10 L10 19 M4 9.25 L12 13.25 M4 13.75 L12 17.75 M14 10 L14 19 M18 8 L18 17 M12 13.25 L20 9.25 M12 17.75 L20 13.75 M6 6 L14 10 M10 4 L18 8 M14 4 L6 8 M18 6 L10 10" fill="none" stroke="#4f9ce8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 5 L16 9 M16 5 L8 9 M8 9 L8 18 M4 11.5 L12 15.5 M16 9 L16 18 M12 15.5 L20 11.5" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1008 B |
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"shapes": [{"d": "M12 3 L20 7 L20 16 L12 20 L4 16 L4 7 Z", "fill": "soft", "stroke": "line"}, {"d": "M4 7 L12 11 L20 7 M12 11 L12 20", "stroke": "line"}, {"d": "M6 8 L6 17 M10 10 L10 19 M4 9.25 L12 13.25 M4 13.75 L12 17.75 M14 10 L14 19 M18 8 L18 17 M12 13.25 L20 9.25 M12 17.75 L20 13.75 M6 6 L14 10 M10 4 L18 8 M14 4 L6 8 M18 6 L10 10", "stroke": "accent", "width": 1}, {"d": "M8 5 L16 9 M16 5 L8 9 M8 9 L8 18 M4 11.5 L12 15.5 M16 9 L16 18 M12 15.5 L20 11.5", "stroke": "line", "width": 1.5}]}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -27,3 +27,5 @@ log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
zip = { workspace = true }
|
||||
# Multi-body designs from SolidCraft scripts in tests.
|
||||
fieldcraft-geometry = { workspace = true, features = ["scripts"] }
|
||||
@@ -272,6 +272,13 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
"Hex mesh swept along extrusions (several levels), around revolutions, or from a source face to a target",
|
||||
&["mesh.hex.generate"],
|
||||
),
|
||||
ready(
|
||||
"mesh.hex.blocks",
|
||||
"Block Hex",
|
||||
Mesh,
|
||||
"Structured Hex",
|
||||
"One conforming hex mesh of a part split into six-sided blocks: shared faces meshed once",
|
||||
),
|
||||
soon("mesh.hex.decompose", "Decompose", Mesh, "Structured Hex", "M5", "Find sweepable and mappable regions and propose the splits"),
|
||||
soon("mesh.transition", "Transition", Mesh, "Structured Hex", "M5", "Conforming tet–hex interfaces with pyramids"),
|
||||
soon("mesh.hex.octree", "All-Hex", Mesh, "Automatic Hex", "M6", "Octree dual all-hex mesh of any part"),
|
||||
@@ -301,7 +308,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 +350,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 +444,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,
|
||||
};
|
||||
|
||||
+224
-22
@@ -3,7 +3,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use fieldcraft_geometry::{FaceSurface, Shape, SurfaceMesh};
|
||||
use fieldcraft_mesh::hex::{HexMesh, HexMethod, HexOptions, Surface, hex_mesh};
|
||||
use fieldcraft_mesh::hex::{Cell, HexMesh, HexMethod, HexOptions, MAX_CELLS, Surface, hex_mesh, hex_mesh_cells};
|
||||
use fieldcraft_mesh::size::UniformSize;
|
||||
use fieldcraft_mesh::surface::BoundarySurface;
|
||||
use fieldcraft_model::{GeometryRef, Model};
|
||||
@@ -129,6 +129,134 @@ pub const HEX_MAPPED: CommandSpec = CommandSpec {
|
||||
run: hex_mapped,
|
||||
};
|
||||
|
||||
pub const HEX_BLOCKS: CommandSpec = CommandSpec {
|
||||
id: "mesh.hex.blocks",
|
||||
label: "Block Hex Mesh",
|
||||
params: r#"{"bodies": ["Cell1", "Cell2"] (default: every body of the open design), "size": 2.0 (element size, mm; default 1/25 of the bounding-box diagonal of all of them), "order": 1 (hex8) or 2 (hex20, mid-edge nodes on the geometry), "tolerance": 0.05 (chordal tolerance of the tessellation, mm; default 1e-3 of that diagonal)} — replaces the mesh with one conforming all-hex mesh of a part partitioned into cells (bodies that meet face to face, such as the pieces of solid.split_body in SolidCraft): every cell a mapped block (six four-sided faces; faces that meet smoothly, such as the two halves of a hole, count as one), division counts assigned over all cells together, each shared face meshed once and its nodes merged, radial lines graded off holes and fillets. Elements are tagged with their cell's body and boundary faces with its B-rep faces (`weld`, `weld_distance` and `feature_angle` as in mesh.tet.generate). Reports the cells, the shared faces, counts and quality; a cell that is not a block, or cells that touch without sharing whole faces, is an error that says which"#,
|
||||
kind: CommandKind::Model,
|
||||
run: hex_blocks,
|
||||
};
|
||||
|
||||
fn hex_blocks(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let size = number(p, "size")?;
|
||||
let quadratic = order(p)?;
|
||||
let design = s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first".into()))?;
|
||||
let shapes: Vec<&Shape> = match p.get("bodies") {
|
||||
None => design.bodies.iter().collect(),
|
||||
Some(v) => {
|
||||
let names = v.as_array().ok_or_else(|| EngineError::Params("`bodies` must be a list of body names".into()))?;
|
||||
// Bounded before the duplicate check, which is quadratic in the list.
|
||||
if names.len() > MAX_CELLS {
|
||||
return Err(EngineError::Params(format!("`bodies` lists {} bodies; at most {MAX_CELLS} are meshed together", names.len())));
|
||||
}
|
||||
let mut out: Vec<&Shape> = Vec::with_capacity(names.len());
|
||||
for n in names {
|
||||
let name = n.as_str().ok_or_else(|| EngineError::Params("`bodies` must be a list of body names".into()))?;
|
||||
let shape = design.body(name).ok_or_else(|| EngineError::Params(format!("no body `{name}`")))?;
|
||||
if out.iter().any(|b| b.name == shape.name) {
|
||||
return Err(EngineError::Params(format!("body `{name}` is listed twice")));
|
||||
}
|
||||
out.push(shape);
|
||||
}
|
||||
out
|
||||
}
|
||||
};
|
||||
if shapes.is_empty() || shapes.len() > MAX_CELLS {
|
||||
return Err(EngineError::Params(format!("{} bodies; mesh.hex.blocks meshes 1 to {MAX_CELLS}", shapes.len())));
|
||||
}
|
||||
// One size and one tessellation tolerance for all cells, from their joint extent, so shared
|
||||
// curves are divided alike.
|
||||
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
|
||||
for b in &shapes {
|
||||
let m = b.measure()?;
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(m.bbox_min[k]);
|
||||
hi[k] = hi[k].max(m.bbox_max[k]);
|
||||
}
|
||||
}
|
||||
let diag = diagonal(lo, hi);
|
||||
if !(diag > 0.0 && diag.is_finite()) {
|
||||
return Err(EngineError::Params("the bodies have no extent".into()));
|
||||
}
|
||||
let tolerance = number(p, "tolerance")?.unwrap_or(DEFAULT_RELATIVE_TOLERANCE * diag);
|
||||
if tolerance < MIN_RELATIVE_TOLERANCE * diag {
|
||||
return Err(EngineError::Params(format!("`tolerance` must be at least {} mm", MIN_RELATIVE_TOLERANCE * diag)));
|
||||
}
|
||||
let h = size.unwrap_or(DEFAULT_RELATIVE_SIZE * diag);
|
||||
if h < MIN_RELATIVE_SIZE * diag {
|
||||
return Err(EngineError::Params(format!("`size` must be at least {} mm", MIN_RELATIVE_SIZE * diag)));
|
||||
}
|
||||
let inputs: Vec<Input> = shapes.iter().map(|b| shape_input(b, Some(tolerance))).collect::<Result<_>>()?;
|
||||
let mut surfaces = Vec::with_capacity(inputs.len());
|
||||
for b in &inputs {
|
||||
let options = super::mesh::surface_options(p, &b.mesh)?;
|
||||
surfaces.push(
|
||||
BoundarySurface::new(&b.mesh.positions, &b.mesh.triangles, &b.mesh.tri_face, &options)
|
||||
.map_err(|e| EngineError::Params(format!("body `{}`: {e}", b.name)))?,
|
||||
);
|
||||
}
|
||||
let cells: Vec<Cell> = surfaces.iter().zip(&inputs).map(|(surface, b)| Cell { name: &b.name, surface, faces: &b.surfaces }).collect();
|
||||
let options = HexOptions {
|
||||
method: HexMethod::Mapped,
|
||||
quadratic,
|
||||
chordal_tolerance: tolerance,
|
||||
max_elements: fieldcraft_model::MAX_ELEMENTS / 8,
|
||||
..HexOptions::default()
|
||||
};
|
||||
let cm = hex_mesh_cells(&cells, &UniformSize(h), &options).map_err(|e| EngineError::Params(e.to_string()))?;
|
||||
let m = &cm.mesh;
|
||||
// Replace the mesh: definitions stay, nodes, elements and geometry tags are new.
|
||||
let mut model = s.model.without_mesh();
|
||||
for (i, xyz) in m.nodes.iter().enumerate() {
|
||||
model.set_node(label(i)?, *xyz)?;
|
||||
}
|
||||
let mut body_tags = Vec::with_capacity(inputs.len());
|
||||
for b in &inputs {
|
||||
body_tags.push(model.geometry_tag(&GeometryRef::body(b.name.as_str())?)?);
|
||||
}
|
||||
for e in 0..m.element_count() {
|
||||
let cell = cm.element_cell.get(e).map_or(0, |&c| c as usize);
|
||||
let nodes: Vec<u32> = m.element(e).unwrap_or(&[]).iter().map(|&i| i + 1).collect();
|
||||
model.set_element(label(e)?, m.kind, nodes)?;
|
||||
model.tag_element(label(e)?, body_tags.get(cell).copied().ok_or_else(|| EngineError::Params("element without a cell".into()))?)?;
|
||||
}
|
||||
tag_faces(&mut model, &inputs, |e| cm.element_cell.get(e as usize).map_or(0, |&c| c as usize), m, 1)?;
|
||||
s.model = model;
|
||||
let blocks: Vec<Value> =
|
||||
inputs.iter().zip(&cm.blocks).map(|(b, d)| json!({"body": b.name, "divisions": d, "elements": d.iter().product::<usize>()})).collect();
|
||||
Ok(json!({
|
||||
"cells": inputs.len(),
|
||||
"interfaces": cm.interfaces,
|
||||
"description": m.report.description,
|
||||
"kind": m.kind.name(),
|
||||
"elements": m.element_count(),
|
||||
"nodes": m.nodes.len(),
|
||||
"boundary_faces": m.boundary.len(),
|
||||
"quality": {"min_scaled_jacobian": m.report.min_scaled_jacobian, "mean_scaled_jacobian": m.report.mean_scaled_jacobian},
|
||||
"volume": m.report.volume,
|
||||
"surface_volume": m.report.surface_volume,
|
||||
"size": h,
|
||||
"blocks": blocks,
|
||||
}))
|
||||
}
|
||||
|
||||
/// A 1-based model label for 0-based index `i`.
|
||||
fn label(i: usize) -> Result<u32> {
|
||||
u32::try_from(i).ok().and_then(|x| x.checked_add(1)).ok_or_else(|| EngineError::Params("too many nodes or elements".into()))
|
||||
}
|
||||
|
||||
/// The `order` parameter: false for hex8, true for hex20.
|
||||
fn order(p: &Value) -> Result<bool> {
|
||||
match p.get("order") {
|
||||
None => Ok(false),
|
||||
Some(v) => match v.as_u64() {
|
||||
Some(1) => Ok(false),
|
||||
Some(2) => Ok(true),
|
||||
_ => Err(EngineError::Params("`order` must be 1 (hex8) or 2 (hex20)".into())),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_generate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let all = [HexMethod::Auto, HexMethod::Swept, HexMethod::Extrude, HexMethod::Sweep, HexMethod::Revolve, HexMethod::Mapped];
|
||||
generate(s, p, HexMethod::Auto, &all)
|
||||
@@ -145,14 +273,7 @@ fn hex_mapped(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// Mesh with `default` or the `method` parameter, which must be one of `allowed`.
|
||||
fn generate(s: &mut Session, p: &Value, default: HexMethod, allowed: &[HexMethod]) -> Result<Value> {
|
||||
let size = number(p, "size")?;
|
||||
let quadratic = match p.get("order") {
|
||||
None => false,
|
||||
Some(v) => match v.as_u64() {
|
||||
Some(1) => false,
|
||||
Some(2) => true,
|
||||
_ => return Err(EngineError::Params("`order` must be 1 (hex8) or 2 (hex20)".into())),
|
||||
},
|
||||
};
|
||||
let quadratic = order(p)?;
|
||||
let method = match p.get("method") {
|
||||
None => default,
|
||||
Some(v) => v.as_str().and_then(HexMethod::from_name).filter(|m| allowed.contains(m)).ok_or_else(|| {
|
||||
@@ -197,7 +318,7 @@ fn generate(s: &mut Session, p: &Value, default: HexMethod, allowed: &[HexMethod
|
||||
model.tag_element(element_label, body_tag)?;
|
||||
element_label = element_label.checked_add(1).ok_or_else(|| EngineError::Params("too many elements".into()))?;
|
||||
}
|
||||
tag_faces(&mut model, b, m, first_element)?;
|
||||
tag_faces(&mut model, std::slice::from_ref(b), |_| 0, m, first_element)?;
|
||||
report.push(json!({
|
||||
"body": b.name,
|
||||
"method": m.report.method.name(),
|
||||
@@ -215,22 +336,20 @@ fn generate(s: &mut Session, p: &Value, default: HexMethod, allowed: &[HexMethod
|
||||
Ok(json!({"bodies": report, "nodes": node_label - 1, "elements": element_label - 1}))
|
||||
}
|
||||
|
||||
/// Tag every boundary element face with its B-rep face.
|
||||
fn tag_faces(model: &mut Model, b: &Input<'_>, m: &HexMesh, first_element: u32) -> Result<()> {
|
||||
let mut tags: Vec<Option<u32>> = Vec::new();
|
||||
/// Tag every boundary element face with the B-rep face it lies on, of the body
|
||||
/// (`bodies[cell(element)]`) its element meshes.
|
||||
fn tag_faces(model: &mut Model, bodies: &[Input<'_>], cell: impl Fn(u32) -> usize, m: &HexMesh, first_element: u32) -> Result<()> {
|
||||
let mut tags: std::collections::BTreeMap<(usize, u32), u32> = std::collections::BTreeMap::new();
|
||||
for f in &m.boundary {
|
||||
let i = f.brep_face as usize;
|
||||
if tags.len() <= i {
|
||||
tags.resize(i + 1, None);
|
||||
}
|
||||
let tag = match tags.get(i).copied().flatten() {
|
||||
Some(t) => t,
|
||||
let c = cell(f.element);
|
||||
let tag = match tags.get(&(c, f.brep_face)) {
|
||||
Some(&t) => t,
|
||||
None => {
|
||||
let b = bodies.get(c).ok_or_else(|| EngineError::Params("a boundary face without its body".into()))?;
|
||||
let i = f.brep_face as usize;
|
||||
let name = b.face_names.get(i).cloned().unwrap_or_else(|| i.to_string());
|
||||
let t = model.geometry_tag(&GeometryRef::face(b.name.as_str(), name)?)?;
|
||||
if let Some(slot) = tags.get_mut(i) {
|
||||
*slot = Some(t);
|
||||
}
|
||||
tags.insert((c, f.brep_face), t);
|
||||
t
|
||||
}
|
||||
};
|
||||
@@ -291,6 +410,89 @@ mod tests {
|
||||
assert_eq!(s.model.element_counts(), vec![(fieldcraft_model::ElementKind::Hex8, n)]);
|
||||
}
|
||||
|
||||
/// A 20 × 10 × 10 box split by SolidCraft into four cells (x = 10, then y = 5 on each half),
|
||||
/// opened as a design; with `t_junction` the second half is not split, so the first half's
|
||||
/// two cells meet it on one face.
|
||||
fn split_box(t_junction: bool) -> (crate::tests::TempDir, Session) {
|
||||
let mut commands = vec![
|
||||
json!({"command": "solid.box", "params": {"length": 20, "width": 10, "height": 10, "body_name": "Box"}}),
|
||||
json!({"command": "solid.split_body", "params": {"body": "Box", "plane": {"origin": [10, 0, 0], "normal": [1, 0, 0]}}}),
|
||||
json!({"command": "solid.split_body", "params": {"body": "Box (1)", "plane": {"origin": [0, 5, 0], "normal": [0, 1, 0]}}}),
|
||||
];
|
||||
if !t_junction {
|
||||
commands.push(json!({"command": "solid.split_body", "params": {"body": "Box", "plane": {"origin": [0, 5, 0], "normal": [0, 1, 0]}}}));
|
||||
}
|
||||
let bytes = fieldcraft_geometry::script_design(&json!({"commands": commands}).to_string()).unwrap();
|
||||
let (dir, path) = crate::tests::design_file(&bytes);
|
||||
let mut s = Session::default();
|
||||
s.execute("geometry.open", &json!({"path": path})).unwrap();
|
||||
(dir, s)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_cells_mesh_into_one_conforming_hex_mesh() {
|
||||
let (_dir, mut s) = split_box(false);
|
||||
let r = s.execute("mesh.hex.blocks", &json!({"size": 2.5})).unwrap();
|
||||
assert_eq!(r["cells"], 4, "{r}");
|
||||
assert_eq!(r["interfaces"], 4, "{r}");
|
||||
assert_eq!(r["elements"], 8 * 4 * 4, "{r}");
|
||||
// One 9 × 5 × 5 grid of nodes: the cells' nodes are merged on the interfaces.
|
||||
assert_eq!(r["nodes"], 9 * 5 * 5, "{r}");
|
||||
assert_eq!(s.model.element_counts(), vec![(fieldcraft_model::ElementKind::Hex8, 128)]);
|
||||
assert!((r["volume"].as_f64().unwrap() - 2000.0).abs() < 1e-6, "{r}");
|
||||
// Each cell's elements carry its body, and the outer faces their B-rep faces: a set of
|
||||
// all four bodies skins the box with no internal faces.
|
||||
let bodies = s.execute("geometry.bodies", &json!({})).unwrap();
|
||||
let names: Vec<Value> = bodies.as_array().unwrap().iter().map(|b| json!({"body": b["name"]})).collect();
|
||||
s.execute("model.set.create", &json!({"name": "all", "geometry": names})).unwrap();
|
||||
let all = s.execute("model.set.resolve", &json!({"name": "all"})).unwrap();
|
||||
assert_eq!(all["elements"]["count"], 128, "{all}");
|
||||
assert_eq!(all["faces"]["count"], r["boundary_faces"], "{all}");
|
||||
assert_eq!(r["boundary_faces"], 2 * (8 * 4 + 8 * 4 + 4 * 4), "{r}");
|
||||
let first = bodies[0]["name"].as_str().unwrap();
|
||||
s.execute("model.set.create", &json!({"name": "one", "geometry": [{"body": first}]})).unwrap();
|
||||
assert_eq!(s.execute("model.set.resolve", &json!({"name": "one"})).unwrap()["elements"]["count"], 32);
|
||||
// Hex20 shares mid-edge nodes across the interfaces as well.
|
||||
let q = s.execute("mesh.hex.blocks", &json!({"size": 2.5, "order": 2})).unwrap();
|
||||
assert_eq!(q["kind"], "hex20");
|
||||
assert_eq!(q["nodes"], 9 * 5 * 5 + 8 * 5 * 5 + 9 * 4 * 5 + 9 * 5 * 4, "{q}");
|
||||
// A list of bodies meshes those only.
|
||||
let two = s.execute("mesh.hex.blocks", &json!({"bodies": [bodies[0]["name"], bodies[1]["name"]], "size": 2.5})).unwrap();
|
||||
assert_eq!(two["cells"], 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cells_that_do_not_conform_and_hostile_params_are_errors() {
|
||||
let (_dir, mut s) = split_box(true);
|
||||
let before = s.model.clone();
|
||||
let e = s.execute("mesh.hex.blocks", &json!({"size": 2.5})).unwrap_err().to_string();
|
||||
assert!(e.contains("do not conform") && e.contains("`Box`"), "{e}");
|
||||
assert_eq!(s.model, before);
|
||||
for p in [
|
||||
json!({"size": 0}),
|
||||
json!({"size": -1}),
|
||||
json!({"size": "big"}),
|
||||
json!({"size": 1e-300}),
|
||||
json!({"order": 3}),
|
||||
json!({"tolerance": -1}),
|
||||
json!({"tolerance": 1e-300}),
|
||||
json!({"bodies": "Box"}),
|
||||
json!({"bodies": []}),
|
||||
json!({"bodies": ["Nope"]}),
|
||||
json!({"bodies": ["Box", "Box"]}),
|
||||
json!({"bodies": [7]}),
|
||||
json!({"size": 1e-3}),
|
||||
] {
|
||||
assert!(s.execute("mesh.hex.blocks", &p).is_err(), "{p}");
|
||||
assert_eq!(s.model, before, "{p}");
|
||||
}
|
||||
assert!(Session::default().execute("mesh.hex.blocks", &json!({})).is_err());
|
||||
// One cell that is no block says which.
|
||||
let (_dir, mut plate) = opened();
|
||||
let e = plate.execute("mesh.hex.blocks", &json!({})).unwrap_err().to_string();
|
||||
assert!(e.contains("`Body1`") && e.contains("not a logical cube"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_params_are_errors_and_change_nothing() {
|
||||
let (_dir, mut s) = opened();
|
||||
|
||||
@@ -40,6 +40,7 @@ pub static ALL: &[CommandSpec] = &[
|
||||
hex::HEX_GENERATE,
|
||||
hex::HEX_SWEEP,
|
||||
hex::HEX_MAPPED,
|
||||
hex::HEX_BLOCKS,
|
||||
tags::TAG,
|
||||
tags::TAG_CLEAR,
|
||||
model::UNITS,
|
||||
@@ -95,6 +96,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}");
|
||||
}
|
||||
}
|
||||
@@ -530,9 +530,14 @@ mod tests {
|
||||
|
||||
/// A design file in a fresh temporary directory (removed when the guard drops).
|
||||
pub(crate) fn sample_design_file(plate: &Plate) -> (TempDir, String) {
|
||||
design_file(&plate_design(plate).unwrap())
|
||||
}
|
||||
|
||||
/// Design file bytes written to a temporary directory.
|
||||
pub(crate) fn design_file(bytes: &[u8]) -> (TempDir, String) {
|
||||
let dir = TempDir::new();
|
||||
let path = dir.0.join("plate.solidcraft");
|
||||
std::fs::write(&path, plate_design(plate).unwrap()).unwrap();
|
||||
let path = dir.0.join("design.solidcraft");
|
||||
std::fs::write(&path, bytes).unwrap();
|
||||
(dir, path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ pub struct Palette {
|
||||
pub mesh_edge: Rgb,
|
||||
pub geometry: Rgb,
|
||||
pub geometry_edge: Rgb,
|
||||
/// The bodies of a design with several, in turn.
|
||||
pub bodies: [Rgb; 8],
|
||||
/// Faces of nodes without a value in a contour plot.
|
||||
pub no_result: Rgb,
|
||||
/// The contour colour scale, low to high.
|
||||
@@ -52,6 +54,16 @@ impl Palette {
|
||||
mesh_edge: Rgb(0x1e, 0x23, 0x2a),
|
||||
geometry: Rgb(0xb4, 0xba, 0xc4),
|
||||
geometry_edge: Rgb(0x2a, 0x30, 0x3a),
|
||||
bodies: [
|
||||
Rgb(0x8f, 0xb4, 0xd9),
|
||||
Rgb(0xe0, 0xa4, 0x6c),
|
||||
Rgb(0x8c, 0xc4, 0x8a),
|
||||
Rgb(0xd6, 0x8c, 0x96),
|
||||
Rgb(0xb0, 0x9c, 0xd8),
|
||||
Rgb(0xd8, 0xc8, 0x70),
|
||||
Rgb(0x7c, 0xc4, 0xc0),
|
||||
Rgb(0xc0, 0xa0, 0x84),
|
||||
],
|
||||
no_result: Rgb(0x9a, 0xa0, 0xaa),
|
||||
contour: [Rgb(0x2b, 0x4f, 0xd8), Rgb(0x2f, 0xb8, 0xe0), Rgb(0x3c, 0xc8, 0x64), Rgb(0xf0, 0xd2, 0x3c), Rgb(0xe0, 0x40, 0x2e)],
|
||||
glyphs: GlyphColors {
|
||||
@@ -125,7 +137,7 @@ fn model_scene(d: &DisplayMesh, p: &Palette) -> Scene {
|
||||
s.tris = d.tris.iter().map(|t| SceneTri { p: t.p, normal: t.normal, color: p.element(t.kind) }).collect();
|
||||
let faces_drawn = d.tris.is_empty();
|
||||
if faces_drawn {
|
||||
s.tris.extend(d.surface_tris.iter().map(|t| SceneTri { p: t.p, normal: t.normal, color: p.geometry }));
|
||||
s.tris.extend(d.surface_tris.iter().map(|t| SceneTri { p: t.p, normal: t.normal, color: d.body_color(t, p.geometry, &p.bodies) }));
|
||||
}
|
||||
s.lines = d.edges.iter().map(|e| SceneLine { a: e.p[0], b: e.p[1], color: p.mesh_edge, width: EDGE_WIDTH }).collect();
|
||||
let edge = if faces_drawn { p.mesh_edge } else { p.geometry_edge };
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -18,6 +18,7 @@ const MESHERS: &[(&str, &str, &str)] = &[
|
||||
("hex", "mesh.hex.generate", "all-hex mesh of a body"),
|
||||
("hex_sweep", "mesh.hex.sweep", "swept all-hex mesh"),
|
||||
("hex_mapped", "mesh.hex.mapped", "mapped all-hex mesh"),
|
||||
("hex_blocks", "mesh.hex.blocks", "one conforming all-hex mesh of a design split into six-sided blocks {bodies, size, order}"),
|
||||
("block", "mesh.block", "structured hex block"),
|
||||
("planar", "mesh.planar", "2D elements of a polygon or a planar face"),
|
||||
];
|
||||
|
||||
+117
-5
@@ -6,7 +6,16 @@
|
||||
//! The hex meshers reason on this topology the way B-rep sweepers do (Blacker, *The Cooper
|
||||
//! tool*, Proc. 5th International Meshing Roundtable, 1996): which faces are sources, targets and
|
||||
//! linking faces, and which edges bound them.
|
||||
//!
|
||||
//! **Virtual topology.** [`Brep::composite`] joins faces that meet smoothly along all of their
|
||||
//! common boundary (the two halves of a cylinder the kernel splits at a seam, a plane running
|
||||
//! tangentially into a fillet, coplanar pieces) into one composite face, so their seam is no edge
|
||||
//! of the mesher's topology: the composite surfaces of virtual topology (A. Sheffer, M. Bercovier,
|
||||
//! T. Blacker, J. Clements, *Virtual topology operators for meshing*, IJCGA 10(3):309–331, 2000).
|
||||
//! A composite face keeps an analytic surface when every member lies on the first member's;
|
||||
//! otherwise nodes go onto its triangles.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::geom::{Surface, add, dist, scale, unit};
|
||||
@@ -59,6 +68,9 @@ impl Edge {
|
||||
|
||||
pub(crate) struct Brep<'a> {
|
||||
pub surface: &'a BoundarySurface,
|
||||
/// The face tag of each triangle: the surface's own tags, or for composite faces the smallest
|
||||
/// tag of the composite.
|
||||
tags: Cow<'a, [u32]>,
|
||||
pub faces: Vec<Face>,
|
||||
pub edges: Vec<Edge>,
|
||||
/// Whether each surface vertex is a B-rep vertex.
|
||||
@@ -72,9 +84,29 @@ pub(crate) struct Brep<'a> {
|
||||
/// Relative geometric tolerance (of the bounding-box diagonal).
|
||||
pub(crate) const RELATIVE_EPS: f64 = 1e-7;
|
||||
|
||||
/// Largest angle (radians) between the surface normals of two faces along their common boundary
|
||||
/// for them to join into a composite face: 1°.
|
||||
const SMOOTH_ANGLE: f64 = 0.017_453_292_519_943_295;
|
||||
/// Distance from a member's vertices to the first member's analytic surface (relative to the
|
||||
/// bounding-box diagonal) within which a composite face keeps that surface.
|
||||
const RELATIVE_ON_SURFACE: f64 = 1e-6;
|
||||
|
||||
impl<'a> Brep<'a> {
|
||||
/// Recover the topology. `surfaces[tag]` is the analytic surface of B-rep face `tag`, if any.
|
||||
pub(crate) fn new(surface: &'a BoundarySurface, surfaces: &[Option<Surface>]) -> Result<Self> {
|
||||
Self::build(surface, surfaces, Cow::Borrowed(surface.tri_face()), false)
|
||||
}
|
||||
|
||||
/// [`Brep::new`] with smoothly meeting faces joined into composite faces (virtual topology).
|
||||
pub(crate) fn composite(surface: &'a BoundarySurface, surfaces: &[Option<Surface>]) -> Result<Self> {
|
||||
let tags = composite_tags(surface, surfaces);
|
||||
Self::build(surface, surfaces, Cow::Owned(tags), true)
|
||||
}
|
||||
|
||||
fn build(surface: &'a BoundarySurface, surfaces: &[Option<Surface>], tags: Cow<'a, [u32]>, composite: bool) -> Result<Self> {
|
||||
if tags.len() != surface.triangles().len() {
|
||||
return Err(Error::Failed("a face tag per triangle is needed".into()));
|
||||
}
|
||||
let pts = surface.points();
|
||||
let mut lo = [f64::INFINITY; 3];
|
||||
let mut hi = [f64::NEG_INFINITY; 3];
|
||||
@@ -91,7 +123,7 @@ impl<'a> Brep<'a> {
|
||||
let eps = RELATIVE_EPS * diagonal;
|
||||
// Faces in tag order.
|
||||
let mut by_tag: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
|
||||
for (t, &f) in surface.tri_face().iter().enumerate() {
|
||||
for (t, &f) in tags.iter().enumerate() {
|
||||
by_tag.entry(f).or_default().push(t as u32);
|
||||
}
|
||||
let index: BTreeMap<u32, usize> = by_tag.keys().enumerate().map(|(i, &f)| (f, i)).collect();
|
||||
@@ -109,12 +141,13 @@ impl<'a> Brep<'a> {
|
||||
}
|
||||
verts.sort_unstable();
|
||||
verts.dedup();
|
||||
let analytic = surfaces.get(id as usize).copied().flatten().and_then(|s| s.checked());
|
||||
let analytic =
|
||||
if composite { composite_surface(surface, surfaces, tris, RELATIVE_ON_SURFACE * diagonal) } else { own_surface(surfaces, id) };
|
||||
let normal = unit(nsum).unwrap_or([0.0; 3]);
|
||||
let plane = planarity(pts, &verts, normal, analytic, eps);
|
||||
faces.push(Face { id, triangles: tris.clone(), vertices: verts, area, surface: analytic, plane, loops: Vec::new() });
|
||||
}
|
||||
let face_of = |t: u32| surface.tri_face().get(t as usize).and_then(|f| index.get(f)).copied();
|
||||
let face_of = |t: u32| tags.get(t as usize).and_then(|f| index.get(f)).copied();
|
||||
// Oriented boundary segments: `from → to` with face `left` traversing it.
|
||||
struct Seg {
|
||||
from: u32,
|
||||
@@ -128,6 +161,10 @@ impl<'a> Brep<'a> {
|
||||
let (Some(left), Some(right)) = (face_of(fe.triangles[0]), face_of(fe.triangles[1])) else {
|
||||
return Err(Error::Failed("feature edge without faces".into()));
|
||||
};
|
||||
// The seam inside a composite face is no edge.
|
||||
if left == right {
|
||||
continue;
|
||||
}
|
||||
// Orient each segment as the lower-indexed face traverses it, so that the segments of
|
||||
// one edge chain head to tail.
|
||||
let [lo, hi] = fe.vertices;
|
||||
@@ -236,7 +273,7 @@ impl<'a> Brep<'a> {
|
||||
face.loops = loops;
|
||||
}
|
||||
}
|
||||
Ok(Brep { surface, faces, edges, is_vertex, edge_triangles, diagonal })
|
||||
Ok(Brep { surface, tags, faces, edges, is_vertex, edge_triangles, diagonal })
|
||||
}
|
||||
|
||||
pub(crate) fn point(&self, v: u32) -> P {
|
||||
@@ -276,9 +313,14 @@ impl<'a> Brep<'a> {
|
||||
|
||||
/// The faces that use the triangle's tag (index into `faces`).
|
||||
pub(crate) fn face_of_triangle(&self, t: u32) -> Option<usize> {
|
||||
let tag = *self.surface.tri_face().get(t as usize)?;
|
||||
let tag = *self.tags.get(t as usize)?;
|
||||
self.faces.binary_search_by_key(&tag, |f| f.id).ok()
|
||||
}
|
||||
|
||||
/// The face tag of triangle `t` in this topology (a composite face's tag for its members).
|
||||
pub(crate) fn tag(&self, t: u32) -> Option<u32> {
|
||||
self.tags.get(t as usize).copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// A point and outward normal of a planar face: the analytic plane, or the plane fitted through
|
||||
@@ -300,3 +342,73 @@ fn planarity(pts: &[P], verts: &[u32], normal: P, analytic: Option<Surface>, eps
|
||||
// tolerance, not to ours.
|
||||
(flat || matches!(analytic, Some(Surface::Plane { .. }))).then_some((point, n))
|
||||
}
|
||||
|
||||
/// The analytic surface of face `tag` from the geometry, checked.
|
||||
fn own_surface(surfaces: &[Option<Surface>], tag: u32) -> Option<Surface> {
|
||||
surfaces.get(tag as usize).copied().flatten().and_then(|s| s.checked())
|
||||
}
|
||||
|
||||
/// The analytic surface of a composite face (its triangles `tris`): the first member's, when the
|
||||
/// vertices of every member lie on it within `tol`.
|
||||
fn composite_surface(surface: &BoundarySurface, surfaces: &[Option<Surface>], tris: &[u32], tol: f64) -> Option<Surface> {
|
||||
let tri_face = surface.tri_face();
|
||||
let first = tris.iter().filter_map(|&t| tri_face.get(t as usize)).min()?;
|
||||
let s = own_surface(surfaces, *first)?;
|
||||
let pts = surface.points();
|
||||
let on = |v: u32| pts.get(v as usize).is_some_and(|&p| s.project(p).is_some_and(|q| dist(p, q) <= tol));
|
||||
tris.iter().all(|&t| surface.triangles().get(t as usize).is_some_and(|tri| tri.iter().all(|&v| on(v)))).then_some(s)
|
||||
}
|
||||
|
||||
/// Face tags with smoothly meeting faces joined: two faces join when, at every boundary segment
|
||||
/// between them, their analytic surfaces' normals are within [`SMOOTH_ANGLE`] of parallel and the
|
||||
/// two triangles there face the same side. Each composite face takes its smallest member tag.
|
||||
fn composite_tags(surface: &BoundarySurface, surfaces: &[Option<Surface>]) -> Vec<u32> {
|
||||
let tri_face = surface.tri_face();
|
||||
let pts = surface.points();
|
||||
let point = |v: u32| pts.get(v as usize).copied().unwrap_or([0.0; 3]);
|
||||
let tri_normal = |t: u32| {
|
||||
surface.triangles().get(t as usize).and_then(|tri| {
|
||||
let [a, b, c] = tri.map(point);
|
||||
unit(cross(sub(b, a), sub(c, a)))
|
||||
})
|
||||
};
|
||||
let cos_min = SMOOTH_ANGLE.cos();
|
||||
// Per pair of faces (low tag, high tag): whether every segment between them is smooth.
|
||||
let mut pairs: BTreeMap<(u32, u32), bool> = BTreeMap::new();
|
||||
for fe in surface.feature_edges().iter().filter(|e| e.kind == FeatureKind::FaceBoundary) {
|
||||
let (Some(&a), Some(&b)) = (tri_face.get(fe.triangles[0] as usize), tri_face.get(fe.triangles[1] as usize)) else { continue };
|
||||
if a == b {
|
||||
continue;
|
||||
}
|
||||
let m = scale(add(point(fe.vertices[0]), point(fe.vertices[1])), 0.5);
|
||||
let normals = (own_surface(surfaces, a).and_then(|s| s.normal(m)), own_surface(surfaces, b).and_then(|s| s.normal(m)));
|
||||
let same_side = match (tri_normal(fe.triangles[0]), tri_normal(fe.triangles[1])) {
|
||||
(Some(x), Some(y)) => dot(x, y) > 0.0,
|
||||
_ => false,
|
||||
};
|
||||
let smooth = same_side && matches!(normals, (Some(x), Some(y)) if dot(x, y).abs() >= cos_min);
|
||||
let entry = pairs.entry((a.min(b), a.max(b))).or_insert(true);
|
||||
*entry = *entry && smooth;
|
||||
}
|
||||
// Union–find over tags, the root always the smaller tag.
|
||||
let mut parent: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
fn root(parent: &BTreeMap<u32, u32>, mut x: u32) -> u32 {
|
||||
// Each step goes to a smaller tag, so this ends.
|
||||
while let Some(&p) = parent.get(&x) {
|
||||
if p == x {
|
||||
break;
|
||||
}
|
||||
x = p;
|
||||
}
|
||||
x
|
||||
}
|
||||
for (&(a, b), &smooth) in &pairs {
|
||||
if smooth {
|
||||
let (ra, rb) = (root(&parent, a), root(&parent, b));
|
||||
if ra != rb {
|
||||
parent.insert(ra.max(rb), ra.min(rb));
|
||||
}
|
||||
}
|
||||
}
|
||||
tri_face.iter().map(|&f| root(&parent, f)).collect()
|
||||
}
|
||||
@@ -0,0 +1,796 @@
|
||||
//! Conforming structured hex meshes of a part partitioned into cells (multi-block meshing).
|
||||
//!
|
||||
//! An analyst meshes a part no single sweep fits by splitting it into cells that each are a
|
||||
//! logical cube (six four-sided faces, possibly composite), meshing every cell as a mapped block
|
||||
//! and joining the blocks so that they conform: the multi-block grids of J. F. Thompson, B. K.
|
||||
//! Soni, N. P. Weatherill (eds.), *Handbook of Grid Generation*, CRC Press 1999, ch. 3 and 13.
|
||||
//! [`hex_mesh_cells`] does it in five steps:
|
||||
//!
|
||||
//! 1. **Topology.** Each cell's B-rep is read with smoothly meeting faces joined
|
||||
//! ([`super::brep::Brep::composite`]: the halves of a hole, a plane running into a fillet) and
|
||||
//! recognized as a logical cube ([`super::mapped::recognize`]). Corners of different cells at
|
||||
//! the same point (within the merge tolerance, the one geometric decision) are one vertex;
|
||||
//! cell edges between the same two vertices along the same curve are one edge; cell faces
|
||||
//! bounded by the same four edges are one face, shared by at most two cells (an interface).
|
||||
//! Everything after this works on these identities, never on coordinates.
|
||||
//! 2. **Interval assignment** over the edges of all cells together ([`super::interval`]): the
|
||||
//! four edges along each direction of every cell are equal, so a count is the same on both
|
||||
//! sides of every interface. Edges that run away from a curved face (radial lines off a hole,
|
||||
//! the diagonal off a fillet) are then graded: the spacing at that end matches the spacing
|
||||
//! along the curved face there and grows by at most [`GROWTH`] per element towards the target
|
||||
//! size; their counts are reassigned with the grading's goals.
|
||||
//! 3. **Edges** are divided once, on the geometry, and their nodes belong to the edge.
|
||||
//! 4. **Faces** get one transfinite grid each (Gordon and Hall), from their four edges and
|
||||
//! projected onto the face; an interface's grid is made once and both cells use its nodes.
|
||||
//! 5. **Cells** fill their interior by trivariate transfinite interpolation (Gordon and Thiel)
|
||||
//! of their six face grids, blended by the edges' arc-length distribution.
|
||||
//!
|
||||
//! Nodes are merged by identity (a vertex, an edge's k-th node, a face grid's node), so the
|
||||
//! cells conform exactly: no node is duplicated on an interface and none hangs. The result goes
|
||||
//! through the same finishing as a single body ([`super::finish`]): the certified validity gate,
|
||||
//! the tiling and fidelity checks, a closed 2-manifold boundary, and no boundary face lying on
|
||||
//! another cell (a free internal face, where cells touch without sharing a face).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::brep::Brep;
|
||||
use super::geom::{TriangleTree, add, dist, scale};
|
||||
use super::mapped::{self, Cube, face_ring, other};
|
||||
use super::{Cells, Ctx, HexMesh, HexMethod, HexOptions, RELATIVE_TOLERANCE, Raw, Surface, finish, project_to_face, project_to_faces};
|
||||
use crate::quad::{lattice_index, transfinite_grid, transfinite_volume};
|
||||
use crate::size::{SizeField, divide_polyline, ideal_divisions};
|
||||
use crate::surface::BoundarySurface;
|
||||
use crate::vec3::{P, cross, dot, norm, sub};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// One cell: its name (for messages), its boundary surface (face-tagged) and the analytic
|
||||
/// surfaces of its faces by tag.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Cell<'a> {
|
||||
pub name: &'a str,
|
||||
pub surface: &'a BoundarySurface,
|
||||
pub faces: &'a [Option<Surface>],
|
||||
}
|
||||
|
||||
/// A conforming mesh of several cells.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CellsMesh {
|
||||
/// The mesh; boundary faces are tagged with the B-rep faces of their element's cell.
|
||||
pub mesh: HexMesh,
|
||||
/// The cell of each element (index into the cells given).
|
||||
pub element_cell: Vec<u32>,
|
||||
/// Each cell's block: divisions along its three logical directions.
|
||||
pub blocks: Vec<[usize; 3]>,
|
||||
/// Faces shared by two cells.
|
||||
pub interfaces: usize,
|
||||
}
|
||||
|
||||
/// The most cells meshed together.
|
||||
pub const MAX_CELLS: usize = 4096;
|
||||
/// Spacing growth from one element to the next along a graded edge (1.2: each at most 20 %
|
||||
/// longer than the one before).
|
||||
pub const GROWTH: f64 = 1.2;
|
||||
/// Segments a graded edge's polyline is cut into for the integration of its grading (the size
|
||||
/// helpers take the size constant along a sample).
|
||||
const GRADED_SEGMENTS: usize = 64;
|
||||
|
||||
/// An edge shared by cells: its end vertices (`a < b`), the cell that divides it and how.
|
||||
struct Edge {
|
||||
a: usize,
|
||||
b: usize,
|
||||
/// The owning cell, its edge and whether that edge runs from `a` to `b`.
|
||||
owner: (usize, usize, bool),
|
||||
length: f64,
|
||||
}
|
||||
|
||||
/// A face shared by one or two cells.
|
||||
struct Face {
|
||||
/// The owner's ring of vertices (the order of [`face_ring`]).
|
||||
ring: [usize; 4],
|
||||
/// The cells, with the direction and side of the face in each; the first owns the grid.
|
||||
uses: Vec<(usize, usize, usize)>,
|
||||
}
|
||||
|
||||
/// Mesh `cells` (bodies that together make the part, meeting face to face) into one conforming
|
||||
/// all-hex mesh, every cell a mapped block. `opts.method` must be [`HexMethod::Auto`] or
|
||||
/// [`HexMethod::Mapped`]; a cell that is not a logical cube, or cells that meet without sharing
|
||||
/// whole faces, are [`Error::Unsupported`] with the reason.
|
||||
pub fn hex_mesh_cells(cells: &[Cell], field: &dyn SizeField, opts: &HexOptions) -> Result<CellsMesh> {
|
||||
if !matches!(opts.method, HexMethod::Auto | HexMethod::Mapped) {
|
||||
return Err(Error::Invalid(format!("conforming cells are meshed as mapped blocks; method `{}` does not apply", opts.method.name())));
|
||||
}
|
||||
if !(opts.chordal_tolerance >= 0.0 && opts.chordal_tolerance.is_finite()) {
|
||||
return Err(Error::Invalid(format!("the chordal tolerance must be a non-negative number, got {}", opts.chordal_tolerance)));
|
||||
}
|
||||
if cells.is_empty() || cells.len() > MAX_CELLS {
|
||||
return Err(Error::Invalid(format!("{} cells; 1 to {MAX_CELLS} are meshed together", cells.len())));
|
||||
}
|
||||
if let Some(c) = cells.iter().find(|c| c.surface.shells().len() != 1) {
|
||||
return Err(Error::Unsupported(format!("cell `{}` has {} shells; a cell is one solid without voids", c.name, c.surface.shells().len())));
|
||||
}
|
||||
let surfaces: Vec<&BoundarySurface> = cells.iter().map(|c| c.surface).collect();
|
||||
super::check_estimate(&surfaces, field, opts)?;
|
||||
let breps: Vec<Brep> = cells.iter().map(|c| Brep::composite(c.surface, c.faces)).collect::<Result<_>>()?;
|
||||
let trees: Vec<TriangleTree> = cells.iter().map(|c| TriangleTree::new(c.surface)).collect::<Result<_>>()?;
|
||||
let ctxs: Vec<Ctx> =
|
||||
breps.iter().zip(&trees).map(|(brep, tree)| Ctx { brep, tree, field, opts, tol: RELATIVE_TOLERANCE * brep.diagonal }).collect();
|
||||
let names: Vec<&str> = cells.iter().map(|c| c.name).collect();
|
||||
let cubes: Vec<Cube> = breps
|
||||
.iter()
|
||||
.zip(&names)
|
||||
.map(|(b, name)| mapped::recognize(b).map_err(|e| Error::Unsupported(format!("cell `{name}`: {}", super::plain(&e)))))
|
||||
.collect::<Result<_>>()?;
|
||||
let topo = Topology::new(&breps, &cubes, &names, opts.chordal_tolerance)?;
|
||||
let divisions = topo.intervals(&ctxs, &cubes, field)?;
|
||||
let mut builder =
|
||||
Builder { topo: &topo, ctxs: &ctxs, cubes: &cubes, divisions: &divisions, field, opts, nodes: Vec::new(), edge_nodes: Vec::new() };
|
||||
let (raw, element_cell, blocks) = builder.build()?;
|
||||
let shared = topo.interfaces();
|
||||
let mesh = finish(&Cells { ctxs: &ctxs, of: &element_cell, names: &names }, raw, HexMethod::Mapped)?;
|
||||
Ok(CellsMesh { mesh, element_cell, blocks, interfaces: shared })
|
||||
}
|
||||
|
||||
/// Vertices, edges and faces shared by the cells.
|
||||
struct Topology {
|
||||
/// Shared vertices: where they are.
|
||||
vertices: Vec<P>,
|
||||
edges: Vec<Edge>,
|
||||
faces: Vec<Face>,
|
||||
/// Per cell: the vertex at each corner, `corner[i][j][k]`.
|
||||
corners: Vec<[[[usize; 2]; 2]; 2]>,
|
||||
/// Per cell, per direction: the four edges (in the order of [`Cube::groups`]).
|
||||
cell_edges: Vec<[[usize; 4]; 3]>,
|
||||
/// Per cell: the face on each side of each direction.
|
||||
cell_faces: Vec<[[usize; 2]; 3]>,
|
||||
}
|
||||
|
||||
/// Points at most this far apart (relative to the diagonal of all cells) are one vertex.
|
||||
const RELATIVE_MERGE: f64 = 1e-6;
|
||||
|
||||
impl Topology {
|
||||
fn new(breps: &[Brep], cubes: &[Cube], names: &[&str], chordal: f64) -> Result<Topology> {
|
||||
let name = |c: usize| names.get(c).copied().unwrap_or("?");
|
||||
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
|
||||
for b in breps {
|
||||
for p in b.surface.points() {
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(p[k]);
|
||||
hi[k] = hi[k].max(p[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let merge = RELATIVE_MERGE * dist(lo, hi);
|
||||
if !(merge > 0.0 && merge.is_finite()) {
|
||||
return Err(Error::Invalid("the cells have no extent".into()));
|
||||
}
|
||||
// Vertices: corners within `merge` of each other, found through a grid of that spacing.
|
||||
let mut vertices: Vec<P> = Vec::new();
|
||||
let mut grid: BTreeMap<[i64; 3], Vec<usize>> = BTreeMap::new();
|
||||
let cellkey = |p: P| p.map(|x| (x / merge).floor() as i64);
|
||||
let mut corners = Vec::with_capacity(cubes.len());
|
||||
for (c, (brep, cube)) in breps.iter().zip(cubes).enumerate() {
|
||||
let mut ids = [[[0usize; 2]; 2]; 2];
|
||||
let mut seen: Vec<usize> = Vec::with_capacity(8);
|
||||
for (i, plane) in ids.iter_mut().enumerate() {
|
||||
for (j, row) in plane.iter_mut().enumerate() {
|
||||
for (k, slot) in row.iter_mut().enumerate() {
|
||||
let p = brep.point(cube.at(i, j, k));
|
||||
let key = cellkey(p);
|
||||
let mut found: Option<usize> = None;
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
let near = [key[0] + dx, key[1] + dy, key[2] + dz];
|
||||
for &v in grid.get(&near).map(Vec::as_slice).unwrap_or(&[]) {
|
||||
if vertices.get(v).is_some_and(|&x| dist(x, p) <= merge) {
|
||||
if found.is_some_and(|f| f != v) {
|
||||
return Err(Error::Unsupported(format!(
|
||||
"cell `{}`: a corner at {p:?} is within the merge distance of two vertices",
|
||||
name(c)
|
||||
)));
|
||||
}
|
||||
found = Some(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let v = match found {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
vertices.push(p);
|
||||
grid.entry(key).or_default().push(vertices.len() - 1);
|
||||
vertices.len() - 1
|
||||
}
|
||||
};
|
||||
if seen.contains(&v) {
|
||||
return Err(Error::Unsupported(format!("cell `{}`: two corners at the same point {p:?}", name(c))));
|
||||
}
|
||||
seen.push(v);
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
corners.push(ids);
|
||||
}
|
||||
// Edges: the same two vertices and the same curve (midpoints within the merge distance
|
||||
// and the tessellation's chordal deviation).
|
||||
let edge_tol = merge + 4.0 * chordal;
|
||||
let mut edges: Vec<Edge> = Vec::new();
|
||||
let mut by_ends: BTreeMap<(usize, usize), Vec<(usize, P)>> = BTreeMap::new();
|
||||
let mut cell_edges = Vec::with_capacity(cubes.len());
|
||||
for (c, (brep, cube)) in breps.iter().zip(cubes).enumerate() {
|
||||
let ids = corners.get(c).copied().ok_or_else(|| Error::Failed("cell corners".into()))?;
|
||||
let mut out = [[0usize; 4]; 3];
|
||||
for (d, row) in out.iter_mut().enumerate() {
|
||||
for (m, slot) in row.iter_mut().enumerate() {
|
||||
let (u, v) = edge_ends(&ids, d, m);
|
||||
let (e, fwd) = cube.groups.get(d).and_then(|g| g.get(m)).copied().ok_or_else(|| Error::Failed("cube groups".into()))?;
|
||||
let pts = brep.edge_points(e, fwd);
|
||||
let (mid, length) = midpoint(&pts);
|
||||
let (a, b) = (u.min(v), u.max(v));
|
||||
let list = by_ends.entry((a, b)).or_default();
|
||||
let id = match list.iter().find(|(_, q)| dist(*q, mid) <= edge_tol) {
|
||||
Some(&(id, _)) => id,
|
||||
None => {
|
||||
edges.push(Edge { a, b, owner: (c, e, fwd == (u == a)), length });
|
||||
list.push((edges.len() - 1, mid));
|
||||
edges.len() - 1
|
||||
}
|
||||
};
|
||||
*slot = id;
|
||||
}
|
||||
}
|
||||
cell_edges.push(out);
|
||||
}
|
||||
// Faces: the same four edges, on the same surface, with the cells on opposite sides.
|
||||
let mut faces: Vec<Face> = Vec::new();
|
||||
let mut by_edges: BTreeMap<[usize; 4], usize> = BTreeMap::new();
|
||||
let mut cell_faces = Vec::with_capacity(cubes.len());
|
||||
for (c, (brep, cube)) in breps.iter().zip(cubes).enumerate() {
|
||||
let ids = corners.get(c).copied().ok_or_else(|| Error::Failed("cell corners".into()))?;
|
||||
let mut out = [[0usize; 2]; 3];
|
||||
for (d, row) in out.iter_mut().enumerate() {
|
||||
for (side, slot) in row.iter_mut().enumerate() {
|
||||
let ring = face_ring(d, side).map(|[i, j, k]| ids[i][j][k]);
|
||||
let mut key = [0usize; 4];
|
||||
for (m, k) in key.iter_mut().enumerate() {
|
||||
let (u, v) = (ring[m], ring[(m + 1) % 4]);
|
||||
let groups = cell_edges.get(c).ok_or_else(|| Error::Failed("cell edges".into()))?;
|
||||
*k = edge_between(&edges, groups, u, v).ok_or_else(|| Error::Failed("a face side is no cell edge".into()))?;
|
||||
}
|
||||
key.sort_unstable();
|
||||
let face = cube.faces.get(d).and_then(|r| r.get(side)).copied().ok_or_else(|| Error::Failed("cube faces".into()))?;
|
||||
let id = match by_edges.get(&key) {
|
||||
Some(&id) => {
|
||||
let f = faces.get_mut(id).ok_or_else(|| Error::Failed("face index".into()))?;
|
||||
if f.uses.len() >= 2 {
|
||||
return Err(Error::Unsupported(format!("cell `{}`: a face is shared by more than two cells", name(c))));
|
||||
}
|
||||
if let Some(&(c0, d0, s0)) = f.uses.first() {
|
||||
if c0 == c {
|
||||
return Err(Error::Unsupported(format!("cell `{}`: two faces of the cell have the same four edges", name(c))));
|
||||
}
|
||||
let f0 = cubes.get(c0).and_then(|q| q.faces.get(d0)).and_then(|r| r.get(s0)).copied().unwrap_or(0);
|
||||
let b0 = breps.get(c0).ok_or_else(|| Error::Failed("cell index".into()))?;
|
||||
let (centre0, area0) = face_centre(b0, f0);
|
||||
let (centre1, area1) = face_centre(brep, face);
|
||||
if dist(centre0, centre1) > edge_tol + 1e-3 * norm(area0).sqrt() || dot(area0, area1) >= 0.0 {
|
||||
return Err(Error::Unsupported(format!(
|
||||
"cells `{}` and `{}` share the four edges of a face but not the face (different surfaces, or on the same side)",
|
||||
name(c0),
|
||||
name(c)
|
||||
)));
|
||||
}
|
||||
}
|
||||
f.uses.push((c, d, side));
|
||||
id
|
||||
}
|
||||
None => {
|
||||
faces.push(Face { ring, uses: vec![(c, d, side)] });
|
||||
by_edges.insert(key, faces.len() - 1);
|
||||
faces.len() - 1
|
||||
}
|
||||
};
|
||||
*slot = id;
|
||||
}
|
||||
}
|
||||
cell_faces.push(out);
|
||||
}
|
||||
Ok(Topology { vertices, edges, faces, corners, cell_edges, cell_faces })
|
||||
}
|
||||
|
||||
/// Faces shared by two cells.
|
||||
fn interfaces(&self) -> usize {
|
||||
self.faces.iter().filter(|f| f.uses.len() == 2).count()
|
||||
}
|
||||
|
||||
/// The edge between vertices `u` and `v` among cell `c`'s edges, and whether it runs from
|
||||
/// `u` to `v`.
|
||||
fn edge(&self, c: usize, u: usize, v: usize) -> Option<(usize, bool)> {
|
||||
let e = edge_between(&self.edges, self.cell_edges.get(c)?, u, v)?;
|
||||
Some((e, self.edges.get(e)?.a == u))
|
||||
}
|
||||
|
||||
/// How every edge is divided, all cells together; graded edges get the counts their
|
||||
/// grading asks for.
|
||||
fn intervals(&self, ctxs: &[Ctx], cubes: &[Cube], field: &dyn SizeField) -> Result<Vec<Division>> {
|
||||
let polylines: Vec<Vec<P>> = self.edges.iter().map(|e| self.polyline(ctxs, e)).collect::<Result<_>>()?;
|
||||
let goals: Vec<f64> = polylines.iter().map(|p| ideal_divisions(field, p)).collect::<Result<_>>()?;
|
||||
let first = self.solve(&goals)?;
|
||||
// Ends of edges that leave a curved face: the spacing along that face there.
|
||||
let ends = self.graded_ends(ctxs, cubes, &first);
|
||||
let mut graded = goals;
|
||||
let mut divisions = Vec::with_capacity(polylines.len());
|
||||
for ((poly, ends), goal) in polylines.into_iter().zip(ends).zip(graded.iter_mut()) {
|
||||
let poly = if ends.iter().any(Option::is_some) {
|
||||
let dense = densify(&poly);
|
||||
*goal = goal.max(ideal_divisions(&Grading { field, ends }, &dense)?);
|
||||
dense
|
||||
} else {
|
||||
poly
|
||||
};
|
||||
divisions.push(Division { count: 1, ends, polyline: poly });
|
||||
}
|
||||
for (d, n) in divisions.iter_mut().zip(self.solve(&graded)?) {
|
||||
d.count = n;
|
||||
}
|
||||
Ok(divisions)
|
||||
}
|
||||
|
||||
fn solve(&self, goals: &[f64]) -> Result<Vec<usize>> {
|
||||
let mut ia = super::interval::Intervals::default();
|
||||
for &g in goals {
|
||||
ia.var(g, 1);
|
||||
}
|
||||
for groups in &self.cell_edges {
|
||||
for g in groups {
|
||||
for &e in g.iter().skip(1) {
|
||||
ia.equal.push((vec![g[0]], vec![e]));
|
||||
}
|
||||
}
|
||||
}
|
||||
ia.solve()
|
||||
}
|
||||
|
||||
/// For every edge, at each end (`a`, `b`): the point and the spacing to start from when the
|
||||
/// edge leaves a curved face there (ends on it without lying on it) — the smallest spacing,
|
||||
/// by `counts`, of that face's edges at the end — or `None`.
|
||||
fn graded_ends(&self, ctxs: &[Ctx], cubes: &[Cube], counts: &[usize]) -> Vec<GradedEnds> {
|
||||
// The edges of every curved face, by the vertices they meet.
|
||||
let mut curved: Vec<[usize; 4]> = Vec::new();
|
||||
let mut curved_at: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
|
||||
for f in &self.faces {
|
||||
let Some(&(c, d, side)) = f.uses.first() else { continue };
|
||||
let planar = cubes
|
||||
.get(c)
|
||||
.and_then(|q| q.faces.get(d))
|
||||
.and_then(|r| r.get(side))
|
||||
.and_then(|&bf| ctxs.get(c).and_then(|x| x.brep.faces.get(bf)))
|
||||
.is_none_or(|bf| bf.plane.is_some());
|
||||
if planar {
|
||||
continue;
|
||||
}
|
||||
let mut es = [usize::MAX; 4];
|
||||
for (m, slot) in es.iter_mut().enumerate() {
|
||||
if let Some((e, _)) = self.edge(c, f.ring[m], f.ring[(m + 1) % 4]) {
|
||||
*slot = e;
|
||||
}
|
||||
}
|
||||
for &v in &f.ring {
|
||||
curved_at.entry(v).or_default().push(curved.len());
|
||||
}
|
||||
curved.push(es);
|
||||
}
|
||||
let spacing = |e: usize| -> f64 { self.edges.get(e).map_or(f64::INFINITY, |x| x.length / counts.get(e).copied().unwrap_or(1).max(1) as f64) };
|
||||
self.edges
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(e, edge)| {
|
||||
[edge.a, edge.b].map(|v| {
|
||||
let mut best: Option<f64> = None;
|
||||
for face in curved_at.get(&v).map(Vec::as_slice).unwrap_or(&[]).iter().filter_map(|&k| curved.get(k)) {
|
||||
if face.contains(&e) {
|
||||
continue;
|
||||
}
|
||||
for &o in face {
|
||||
if self.edges.get(o).is_some_and(|x| x.a == v || x.b == v) {
|
||||
let s = spacing(o);
|
||||
best = Some(best.map_or(s, |b| b.min(s)));
|
||||
}
|
||||
}
|
||||
}
|
||||
best.and_then(|s| self.vertices.get(v).map(|&x| (x, s)))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The owner's polyline of an edge, from `a` to `b`.
|
||||
fn polyline(&self, ctxs: &[Ctx], e: &Edge) -> Result<Vec<P>> {
|
||||
let (c, be, fwd) = e.owner;
|
||||
let ctx = ctxs.get(c).ok_or_else(|| Error::Failed("edge owner".into()))?;
|
||||
let mut pts = ctx.brep.edge_points(be, fwd);
|
||||
// The ends are the shared vertices.
|
||||
if let (Some(first), Some(a)) = (pts.first_mut(), self.vertices.get(e.a)) {
|
||||
*first = *a;
|
||||
}
|
||||
if let (Some(last), Some(b)) = (pts.last_mut(), self.vertices.get(e.b)) {
|
||||
*last = *b;
|
||||
}
|
||||
if pts.len() < 2 {
|
||||
return Err(Error::Failed("an edge without points".into()));
|
||||
}
|
||||
Ok(pts)
|
||||
}
|
||||
}
|
||||
|
||||
/// The edge among a cell's edges (`groups`) between vertices `u` and `v`.
|
||||
fn edge_between(edges: &[Edge], groups: &[[usize; 4]; 3], u: usize, v: usize) -> Option<usize> {
|
||||
let key = (u.min(v), u.max(v));
|
||||
groups.iter().flatten().copied().find(|&e| edges.get(e).is_some_and(|x| (x.a, x.b) == key))
|
||||
}
|
||||
|
||||
/// The corners (low, high) of edge `m` of direction `d` of a cell whose corners are `ids`, in
|
||||
/// the order of [`Cube::groups`].
|
||||
fn edge_ends(ids: &[[[usize; 2]; 2]; 2], d: usize, m: usize) -> (usize, usize) {
|
||||
let (s, t) = ((m / 2) & 1, m & 1);
|
||||
match d {
|
||||
0 => (ids[0][s][t], ids[1][s][t]),
|
||||
1 => (ids[s][0][t], ids[s][1][t]),
|
||||
_ => (ids[s][t][0], ids[s][t][1]),
|
||||
}
|
||||
}
|
||||
|
||||
/// The point halfway along a polyline (by arc length) and its length.
|
||||
fn midpoint(pts: &[P]) -> (P, f64) {
|
||||
let length: f64 = pts.windows(2).map(|w| dist(w[0], w[1])).sum();
|
||||
let mut walked = 0.0;
|
||||
for w in pts.windows(2) {
|
||||
let l = dist(w[0], w[1]);
|
||||
if walked + l >= 0.5 * length && l > 0.0 {
|
||||
let t = (0.5 * length - walked) / l;
|
||||
return (add(w[0], scale(sub(w[1], w[0]), t)), length);
|
||||
}
|
||||
walked += l;
|
||||
}
|
||||
(pts.first().copied().unwrap_or([0.0; 3]), length)
|
||||
}
|
||||
|
||||
/// A B-rep face's area-weighted centre and its area vector (outward).
|
||||
fn face_centre(brep: &Brep, f: usize) -> (P, P) {
|
||||
let (mut c, mut n, mut total) = ([0.0; 3], [0.0; 3], 0.0);
|
||||
for &t in brep.faces.get(f).map(|x| x.triangles.as_slice()).unwrap_or(&[]) {
|
||||
let Some(tri) = brep.surface.triangles().get(t as usize) else { continue };
|
||||
let [a, b, d] = tri.map(|v| brep.point(v));
|
||||
let area = cross(sub(b, a), sub(d, a));
|
||||
let w = 0.5 * norm(area);
|
||||
c = add(c, scale(add(add(a, b), d), w / 3.0));
|
||||
n = add(n, scale(area, 0.5));
|
||||
total += w;
|
||||
}
|
||||
(if total > 0.0 { scale(c, 1.0 / total) } else { c }, n)
|
||||
}
|
||||
|
||||
/// A polyline cut into at least [`GRADED_SEGMENTS`] pieces (for the integration of a grading).
|
||||
fn densify(pts: &[P]) -> Vec<P> {
|
||||
let length: f64 = pts.windows(2).map(|w| dist(w[0], w[1])).sum();
|
||||
let step = length / GRADED_SEGMENTS as f64;
|
||||
let mut out = Vec::with_capacity(pts.len() + GRADED_SEGMENTS);
|
||||
for w in pts.windows(2) {
|
||||
let l = dist(w[0], w[1]);
|
||||
let k = if step > 0.0 { ((l / step).ceil() as usize).clamp(1, GRADED_SEGMENTS) } else { 1 };
|
||||
for s in 0..k {
|
||||
out.push(add(w[0], scale(sub(w[1], w[0]), s as f64 / k as f64)));
|
||||
}
|
||||
}
|
||||
if let Some(&last) = pts.last() {
|
||||
out.push(last);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Each end of an edge (`a`, `b`): where grading starts from and the spacing there.
|
||||
type GradedEnds = [Option<(P, f64)>; 2];
|
||||
|
||||
/// How an edge is divided: its count, its graded ends and the polyline (from `a` to `b`; cut
|
||||
/// finer when graded) it is divided along.
|
||||
struct Division {
|
||||
count: usize,
|
||||
ends: GradedEnds,
|
||||
polyline: Vec<P>,
|
||||
}
|
||||
|
||||
/// The size along a graded edge: the field's, capped near each graded end by the spacing there
|
||||
/// growing by [`GROWTH`] per element (linearly with the distance from that end).
|
||||
struct Grading<'a> {
|
||||
field: &'a dyn SizeField,
|
||||
ends: GradedEnds,
|
||||
}
|
||||
|
||||
impl SizeField for Grading<'_> {
|
||||
fn size(&self, p: [f64; 3]) -> f64 {
|
||||
let mut h = self.field.size(p);
|
||||
for (q, s) in self.ends.iter().flatten() {
|
||||
h = h.min(s + (GROWTH - 1.0) * dist(p, *q));
|
||||
}
|
||||
h
|
||||
}
|
||||
}
|
||||
|
||||
/// The nodes of a cell's lattice, or of a face's grid, by identity.
|
||||
struct Builder<'t, 'c, 'a> {
|
||||
topo: &'t Topology,
|
||||
ctxs: &'c [Ctx<'a>],
|
||||
cubes: &'c [Cube],
|
||||
divisions: &'c [Division],
|
||||
field: &'c dyn SizeField,
|
||||
opts: &'c HexOptions,
|
||||
nodes: Vec<P>,
|
||||
/// Per edge: its node ids from `a` to `b`.
|
||||
edge_nodes: Vec<Vec<u32>>,
|
||||
}
|
||||
|
||||
impl Builder<'_, '_, '_> {
|
||||
fn count(&self, e: usize) -> usize {
|
||||
self.divisions.get(e).map_or(1, |d| d.count)
|
||||
}
|
||||
|
||||
fn push(&mut self, p: P) -> Result<u32> {
|
||||
let id = u32::try_from(self.nodes.len()).map_err(|_| Error::Limit("too many nodes".into()))?;
|
||||
self.nodes.push(p);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn build(&mut self) -> Result<(Raw, Vec<u32>, Vec<[usize; 3]>)> {
|
||||
let (topo, field, max_elements) = (self.topo, self.field, self.opts.max_elements);
|
||||
// Block sizes and the element budget before any node is made.
|
||||
let mut blocks = Vec::with_capacity(topo.cell_edges.len());
|
||||
let mut total = 0usize;
|
||||
for groups in &topo.cell_edges {
|
||||
let n = groups.map(|g| self.count(g[0]));
|
||||
let nodes = (n[0] + 1).checked_mul(n[1] + 1).and_then(|x| x.checked_mul(n[2] + 1)).filter(|&x| x <= mapped::MAX_NODES);
|
||||
if nodes.is_none() {
|
||||
return Err(Error::Limit(format!("a {} × {} × {} block exceeds {} nodes", n[0], n[1], n[2], mapped::MAX_NODES)));
|
||||
}
|
||||
total = total.saturating_add(n[0] * n[1] * n[2]);
|
||||
blocks.push(n);
|
||||
}
|
||||
if total > max_elements {
|
||||
return Err(Error::Limit(format!("the mesh would have {total} hexahedra; the limit is {max_elements}")));
|
||||
}
|
||||
// Vertices.
|
||||
for &v in &topo.vertices {
|
||||
self.push(v)?;
|
||||
}
|
||||
// Edges: divided once, on the geometry.
|
||||
for (edge, division) in topo.edges.iter().zip(self.divisions) {
|
||||
let n = division.count;
|
||||
let pts = if division.ends.iter().any(Option::is_some) {
|
||||
divide_polyline(&Grading { field, ends: division.ends }, &division.polyline, n)?
|
||||
} else {
|
||||
divide_polyline(field, &division.polyline, n)?
|
||||
};
|
||||
let (c, be, _) = edge.owner;
|
||||
let ctx = self.ctxs.get(c).ok_or_else(|| Error::Failed("edge owner".into()))?;
|
||||
let faces = ctx.brep.edges.get(be).map(|x| x.faces.to_vec()).unwrap_or_default();
|
||||
let mut ids = Vec::with_capacity(n + 1);
|
||||
ids.push(edge.a as u32);
|
||||
for q in pts.iter().take(n).skip(1) {
|
||||
let p = project_to_faces(ctx, &faces, *q);
|
||||
ids.push(self.push(p)?);
|
||||
}
|
||||
ids.push(edge.b as u32);
|
||||
self.edge_nodes.push(ids);
|
||||
}
|
||||
// Faces: one grid each, from the owner's side.
|
||||
let mut face_grids: Vec<FaceGrid> = Vec::with_capacity(topo.faces.len());
|
||||
for face in &topo.faces {
|
||||
face_grids.push(self.face_grid(face)?);
|
||||
}
|
||||
// Cells.
|
||||
let mut hexes: Vec<[u32; 8]> = Vec::with_capacity(total);
|
||||
let mut element_cell: Vec<u32> = Vec::with_capacity(total);
|
||||
for (c, &n) in blocks.iter().enumerate() {
|
||||
let ids = self.cell_lattice(c, n, &face_grids)?;
|
||||
let [ni, nj, nk] = n;
|
||||
let id = |i: usize, j: usize, k: usize| ids.get(lattice_index(n, [i, j, k])).copied().unwrap_or(0);
|
||||
for k in 0..nk {
|
||||
for j in 0..nj {
|
||||
for i in 0..ni {
|
||||
hexes.push([
|
||||
id(i, j, k),
|
||||
id(i + 1, j, k),
|
||||
id(i + 1, j + 1, k),
|
||||
id(i, j + 1, k),
|
||||
id(i, j, k + 1),
|
||||
id(i + 1, j, k + 1),
|
||||
id(i + 1, j + 1, k + 1),
|
||||
id(i, j + 1, k + 1),
|
||||
]);
|
||||
element_cell.push(c as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let shared = topo.interfaces();
|
||||
let description = format!(
|
||||
"{} cell{} as conforming mapped blocks, {} shared face{}",
|
||||
blocks.len(),
|
||||
if blocks.len() == 1 { "" } else { "s" },
|
||||
shared,
|
||||
if shared == 1 { "" } else { "s" }
|
||||
);
|
||||
let nodes = std::mem::take(&mut self.nodes);
|
||||
Ok((Raw { nodes, hexes, description }, element_cell, blocks))
|
||||
}
|
||||
|
||||
/// The nodes of edge `e` from vertex `u`.
|
||||
fn edge_from(&self, e: usize, u: usize) -> Result<Vec<u32>> {
|
||||
let edge = self.topo.edges.get(e).ok_or_else(|| Error::Failed("edge index".into()))?;
|
||||
let mut ids = self.edge_nodes.get(e).cloned().ok_or_else(|| Error::Failed("edge nodes".into()))?;
|
||||
if edge.b == u {
|
||||
ids.reverse();
|
||||
} else if edge.a != u {
|
||||
return Err(Error::Failed("an edge does not start at the vertex asked".into()));
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// A face's transfinite grid in its owner's frame.
|
||||
fn face_grid(&mut self, face: &Face) -> Result<FaceGrid> {
|
||||
let &(c, d, side) = face.uses.first().ok_or_else(|| Error::Failed("a face without a cell".into()))?;
|
||||
let r = face.ring;
|
||||
let side_nodes = |b: &Self, u: usize, v: usize| -> Result<Vec<u32>> {
|
||||
let (e, _) = b.topo.edge(c, u, v).ok_or_else(|| Error::Failed("a face side is no edge".into()))?;
|
||||
b.edge_from(e, u)
|
||||
};
|
||||
let bottom = side_nodes(self, r[0], r[1])?;
|
||||
let right = side_nodes(self, r[1], r[2])?;
|
||||
let top = side_nodes(self, r[3], r[2])?;
|
||||
let left = side_nodes(self, r[0], r[3])?;
|
||||
let (na, nb) = (bottom.len() - 1, left.len() - 1);
|
||||
if top.len() != na + 1 || right.len() != nb + 1 {
|
||||
return Err(Error::Failed("opposite sides of a face have different counts".into()));
|
||||
}
|
||||
let at = |b: &Self, ids: &[u32]| -> Vec<P> { ids.iter().map(|&i| b.nodes.get(i as usize).copied().unwrap_or([0.0; 3])).collect() };
|
||||
let grid = transfinite_grid(&at(self, &bottom), &at(self, &right), &at(self, &top), &at(self, &left))?;
|
||||
let ctx = self.ctxs.get(c).ok_or_else(|| Error::Failed("face owner".into()))?;
|
||||
let brep_face = self.cubes.get(c).and_then(|q| q.faces.get(d)).and_then(|row| row.get(side)).copied();
|
||||
let mut ids = vec![0u32; (na + 1) * (nb + 1)];
|
||||
for b in 0..=nb {
|
||||
for a in 0..=na {
|
||||
let id = if b == 0 {
|
||||
bottom.get(a).copied()
|
||||
} else if b == nb {
|
||||
top.get(a).copied()
|
||||
} else if a == 0 {
|
||||
left.get(b).copied()
|
||||
} else if a == na {
|
||||
right.get(b).copied()
|
||||
} else {
|
||||
let q = grid.get(b * (na + 1) + a).copied().ok_or_else(|| Error::Failed("face grid".into()))?;
|
||||
let q = brep_face.and_then(|f| project_to_face(ctx, f, q)).unwrap_or(q);
|
||||
Some(self.push(q)?)
|
||||
};
|
||||
if let Some(slot) = ids.get_mut(b * (na + 1) + a) {
|
||||
*slot = id.ok_or_else(|| Error::Failed("face side node".into()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(FaceGrid { ring: r, na, nb, ids })
|
||||
}
|
||||
|
||||
/// Cell `c`'s lattice of node ids: boundary from the shared face grids, interior new.
|
||||
fn cell_lattice(&mut self, c: usize, n: [usize; 3], face_grids: &[FaceGrid]) -> Result<Vec<u32>> {
|
||||
let topo = self.topo;
|
||||
let corners = topo.corners.get(c).copied().ok_or_else(|| Error::Failed("cell corners".into()))?;
|
||||
let faces = topo.cell_faces.get(c).copied().ok_or_else(|| Error::Failed("cell faces".into()))?;
|
||||
let total = (n[0] + 1) * (n[1] + 1) * (n[2] + 1);
|
||||
let mut ids: Vec<Option<u32>> = vec![None; total];
|
||||
for d in 0..3 {
|
||||
let (da, db) = other(d);
|
||||
for side in 0..2 {
|
||||
let gf = faces.get(d).and_then(|r| r.get(side)).copied().ok_or_else(|| Error::Failed("cell face".into()))?;
|
||||
let grid = face_grids.get(gf).ok_or_else(|| Error::Failed("face grid".into()))?;
|
||||
let ring = face_ring(d, side).map(|[i, j, k]| corners[i][j][k]);
|
||||
let map = grid.map_from(ring, n[da], n[db])?;
|
||||
for b in 0..=n[db] {
|
||||
for a in 0..=n[da] {
|
||||
let mut l = [0usize; 3];
|
||||
l[d] = side * n[d];
|
||||
l[da] = a;
|
||||
l[db] = b;
|
||||
if let Some(slot) = ids.get_mut(lattice_index(n, l)) {
|
||||
*slot = Some(map(a, b)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Interior: trivariate interpolation, blended by the edges' arc-length distribution.
|
||||
let mut x: Vec<Option<P>> = ids.iter().map(|i| i.and_then(|i| self.nodes.get(i as usize).copied())).collect();
|
||||
let groups = topo.cell_edges.get(c).copied().ok_or_else(|| Error::Failed("cell edges".into()))?;
|
||||
let mut blend: [Vec<f64>; 3] = [Vec::new(), Vec::new(), Vec::new()];
|
||||
for (d, g) in groups.iter().enumerate() {
|
||||
let mut sum = vec![0.0; n[d] + 1];
|
||||
for (m, &e) in g.iter().enumerate() {
|
||||
let pts: Vec<P> = self
|
||||
.edge_from(e, edge_ends(&corners, d, m).0)?
|
||||
.iter()
|
||||
.map(|&i| self.nodes.get(i as usize).copied().unwrap_or([0.0; 3]))
|
||||
.collect();
|
||||
let params = crate::quad::tfi::params(&pts, "edge")?;
|
||||
if params.len() != sum.len() {
|
||||
return Err(Error::Failed("an edge's count differs from its block's".into()));
|
||||
}
|
||||
for (acc, p) in sum.iter_mut().zip(params) {
|
||||
*acc += 0.25 * p;
|
||||
}
|
||||
}
|
||||
if let Some(first) = sum.first_mut() {
|
||||
*first = 0.0;
|
||||
}
|
||||
if let Some(last) = sum.last_mut() {
|
||||
*last = 1.0;
|
||||
}
|
||||
blend[d] = sum;
|
||||
}
|
||||
transfinite_volume(&mut x, n, [&blend[0], &blend[1], &blend[2]])?;
|
||||
let mut out = Vec::with_capacity(total);
|
||||
for (slot, p) in ids.iter().zip(&x) {
|
||||
out.push(match (slot, p) {
|
||||
(Some(id), _) => *id,
|
||||
(None, Some(p)) => self.push(*p)?,
|
||||
(None, None) => return Err(Error::Failed("a block node is missing".into())),
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// A face's grid of node ids in its owner's frame: `(na + 1) × (nb + 1)`, node `(a, b)` at
|
||||
/// `b·(na + 1) + a`, corners at the owner's ring.
|
||||
struct FaceGrid {
|
||||
ring: [usize; 4],
|
||||
na: usize,
|
||||
nb: usize,
|
||||
ids: Vec<u32>,
|
||||
}
|
||||
|
||||
impl FaceGrid {
|
||||
/// The lookup of node `(a, b)` of a cell's view of this face, whose ring (the order of
|
||||
/// [`face_ring`]) is `ring` and counts `na × nb`.
|
||||
fn map_from(&self, ring: [usize; 4], na: usize, nb: usize) -> Result<impl Fn(usize, usize) -> Result<u32> + '_> {
|
||||
// The owner's lattice position of each of the cell's corners.
|
||||
let corner = |v: usize| -> Option<[i64; 2]> {
|
||||
let k = self.ring.iter().position(|&x| x == v)?;
|
||||
let (a, b) = [(0, 0), (self.na, 0), (self.na, self.nb), (0, self.nb)].get(k).copied()?;
|
||||
Some([a as i64, b as i64])
|
||||
};
|
||||
let (Some(o), Some(pa), Some(pb)) = (corner(ring[0]), corner(ring[1]), corner(ring[3])) else {
|
||||
return Err(Error::Failed("a cell's face does not have the shared face's corners".into()));
|
||||
};
|
||||
let step = |p: [i64; 2], n: usize| -> Option<[i64; 2]> {
|
||||
let d = [p[0] - o[0], p[1] - o[1]];
|
||||
let n = n as i64;
|
||||
// One axis of the owner's lattice, `n` long.
|
||||
(n > 0 && d.iter().filter(|&&x| x != 0).count() == 1 && d.iter().all(|&x| x == 0 || x.abs() == n)).then(|| d.map(|x| x / n))
|
||||
};
|
||||
let (Some(du), Some(dv)) = (step(pa, na), step(pb, nb)) else {
|
||||
return Err(Error::Failed("a cell's view of a shared face does not match its grid".into()));
|
||||
};
|
||||
let (na_owner, ids) = (self.na, &self.ids);
|
||||
Ok(move |a: usize, b: usize| -> Result<u32> {
|
||||
let (a, b) = (a as i64, b as i64);
|
||||
let u = o[0] + a * du[0] + b * dv[0];
|
||||
let v = o[1] + a * du[1] + b * dv[1];
|
||||
usize::try_from(v)
|
||||
.ok()
|
||||
.zip(usize::try_from(u).ok())
|
||||
.and_then(|(v, u)| ids.get(v * (na_owner + 1) + u))
|
||||
.copied()
|
||||
.ok_or_else(|| Error::Failed("shared face grid index".into()))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,29 @@ impl Surface {
|
||||
};
|
||||
q.iter().all(|x| x.is_finite()).then_some(q)
|
||||
}
|
||||
|
||||
/// The unit normal of the surface at its point closest to `p`, up to sign, or `None` where
|
||||
/// the closest point is not unique.
|
||||
pub(crate) fn normal(&self, p: P) -> Option<P> {
|
||||
match *self {
|
||||
Surface::Plane { normal, .. } => unit(normal),
|
||||
Surface::Cylinder { origin, axis, .. } => {
|
||||
let w = sub(p, origin);
|
||||
unit(sub(w, scale(axis, dot(w, axis))))
|
||||
}
|
||||
Surface::Cone { apex, axis, half_angle } => {
|
||||
let w = sub(p, apex);
|
||||
let radial = unit(sub(w, scale(axis, dot(w, axis))))?;
|
||||
unit(sub(scale(radial, half_angle.cos()), scale(axis, half_angle.sin())))
|
||||
}
|
||||
Surface::Sphere { center, .. } => unit(sub(p, center)),
|
||||
Surface::Torus { center, axis, major, .. } => {
|
||||
let w = sub(p, center);
|
||||
let ring = add(center, scale(unit(sub(w, scale(axis, dot(w, axis))))?, major));
|
||||
unit(sub(p, ring))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The closest point to `p` on triangle `abc`.
|
||||
@@ -274,8 +297,18 @@ impl<'a> TriangleTree<'a> {
|
||||
|
||||
/// The closest triangle to `p` among those `accept` admits (by triangle index).
|
||||
pub(crate) fn closest(&self, p: P, accept: &dyn Fn(u32) -> bool) -> Option<Hit> {
|
||||
self.search(p, f64::INFINITY, accept)
|
||||
}
|
||||
|
||||
/// [`TriangleTree::closest`] among the triangles within distance `r` of `p` (the search
|
||||
/// skips everything farther).
|
||||
pub(crate) fn closest_within(&self, p: P, r: f64, accept: &dyn Fn(u32) -> bool) -> Option<Hit> {
|
||||
self.search(p, r * r, accept).filter(|h| h.dist <= r)
|
||||
}
|
||||
|
||||
fn search(&self, p: P, limit2: f64, accept: &dyn Fn(u32) -> bool) -> Option<Hit> {
|
||||
let mut best: Option<Hit> = None;
|
||||
let mut best2 = f64::INFINITY;
|
||||
let mut best2 = limit2;
|
||||
let mut stack = vec![0usize];
|
||||
let mut visits = 0usize;
|
||||
while let Some(id) = stack.pop() {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::brep::Brep;
|
||||
use super::interval::Intervals;
|
||||
use super::{Ctx, Raw, project_to_face, project_to_faces};
|
||||
use crate::quad::{lattice_index, transfinite_faces, transfinite_volume};
|
||||
@@ -20,10 +21,30 @@ use crate::vec3::{P, det, sub};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// The most nodes of a mapped block.
|
||||
const MAX_NODES: usize = 1 << 26;
|
||||
pub(crate) const MAX_NODES: usize = 1 << 26;
|
||||
|
||||
pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
|
||||
let brep = ctx.brep;
|
||||
/// A logical cube found in a B-rep: its corners, its edges by logical direction and its faces.
|
||||
pub(crate) struct Cube {
|
||||
/// The surface vertex at each corner, `corner[i][j][k]`, right-handed.
|
||||
pub corner: [[[u32; 2]; 2]; 2],
|
||||
/// The four edges along each logical direction `d`, oriented from the low corner to the high
|
||||
/// one, at the other two coordinates `(s, t) ∈ {0, 1}²` in the order (0, 0), (0, 1), (1, 0),
|
||||
/// (1, 1).
|
||||
pub groups: [[(usize, bool); 4]; 3],
|
||||
/// The B-rep face (index into `faces`) on each side of each direction: `faces[d][side]`.
|
||||
pub faces: [[usize; 2]; 3],
|
||||
}
|
||||
|
||||
impl Cube {
|
||||
/// The corner at logical coordinates `(i, j, k)` (each 0 or 1).
|
||||
pub(crate) fn at(&self, i: usize, j: usize, k: usize) -> u32 {
|
||||
self.corner[i & 1][j & 1][k & 1]
|
||||
}
|
||||
}
|
||||
|
||||
/// Recognize a logical cube: six faces with one loop of four edges each, three edges at each of
|
||||
/// eight corners. Anything else is [`Error::Unsupported`] with the reason.
|
||||
pub(crate) fn recognize(brep: &Brep) -> Result<Cube> {
|
||||
if brep.faces.len() != 6 || brep.edges.len() != 12 {
|
||||
return Err(Error::Unsupported(format!("not a logical cube: {} faces and {} edges (6 and 12 needed)", brep.faces.len(), brep.edges.len())));
|
||||
}
|
||||
@@ -61,7 +82,6 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
|
||||
};
|
||||
let c111 = common(c110, c101, a).filter(|&x| neighbours(c011).contains(&x));
|
||||
let Some(c111) = c111 else { return Err(Error::Unsupported("not a logical cube: the corner graph is not a cube".into())) };
|
||||
// corner[i][j][k]
|
||||
let corner = [[[c000, c], [b, c011]], [[a, c101], [c110, c111]]];
|
||||
let at = |i: usize, j: usize, k: usize| -> u32 { corner[i][j][k] };
|
||||
let mut distinct: Vec<u32> = corner.iter().flatten().flatten().copied().collect();
|
||||
@@ -74,20 +94,65 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
|
||||
let edge_between = |u: u32, v: u32| -> Option<(usize, bool)> {
|
||||
adj.get(&u)?.iter().find(|x| x.0 == v).map(|x| (x.1, brep.edges.get(x.1).is_some_and(|e| e.first() == u)))
|
||||
};
|
||||
// The four edges along each direction, from the low corner to the high one.
|
||||
let mut groups: [Vec<(usize, bool)>; 3] = [Vec::new(), Vec::new(), Vec::new()];
|
||||
let mut groups = [[(0usize, true); 4]; 3];
|
||||
for (d, group) in groups.iter_mut().enumerate() {
|
||||
for s in 0..2 {
|
||||
for t in 0..2 {
|
||||
let (lo, hi) = match d {
|
||||
0 => (at(0, s, t), at(1, s, t)),
|
||||
1 => (at(s, 0, t), at(s, 1, t)),
|
||||
_ => (at(s, t, 0), at(s, t, 1)),
|
||||
};
|
||||
group.push(edge_between(lo, hi).ok_or_else(|| Error::Unsupported("not a logical cube: a corner pair has no edge".into()))?);
|
||||
}
|
||||
for (m, slot) in group.iter_mut().enumerate() {
|
||||
let (s, t) = (m / 2, m % 2);
|
||||
let (lo, hi) = match d {
|
||||
0 => (at(0, s, t), at(1, s, t)),
|
||||
1 => (at(s, 0, t), at(s, 1, t)),
|
||||
_ => (at(s, t, 0), at(s, t, 1)),
|
||||
};
|
||||
*slot = edge_between(lo, hi).ok_or_else(|| Error::Unsupported("not a logical cube: a corner pair has no edge".into()))?;
|
||||
}
|
||||
}
|
||||
// The face bounded by the four edges of each side.
|
||||
let mut faces = [[0usize; 2]; 3];
|
||||
for (d, row) in faces.iter_mut().enumerate() {
|
||||
for (side, slot) in row.iter_mut().enumerate() {
|
||||
let ring = face_ring(d, side).map(|[i, j, k]| at(i, j, k));
|
||||
let mut bounding: Vec<usize> = (0..4).filter_map(|m| edge_between(ring[m], ring[(m + 1) % 4]).map(|x| x.0)).collect();
|
||||
bounding.sort_unstable();
|
||||
*slot = brep
|
||||
.faces
|
||||
.iter()
|
||||
.position(|f| {
|
||||
let mut es: Vec<usize> = f.loops.iter().flatten().map(|x| x.0).collect();
|
||||
es.sort_unstable();
|
||||
es == bounding
|
||||
})
|
||||
.ok_or_else(|| Error::Unsupported("not a logical cube: a side's four edges bound no face".into()))?;
|
||||
}
|
||||
}
|
||||
Ok(Cube { corner, groups, faces })
|
||||
}
|
||||
|
||||
/// The corners of side `side` of direction `d` in the order (0, 0), (1, 0), (1, 1), (0, 1) of its
|
||||
/// two other directions (lower first).
|
||||
pub(crate) fn face_ring(d: usize, side: usize) -> [[usize; 3]; 4] {
|
||||
let (da, db) = other(d);
|
||||
[(0, 0), (1, 0), (1, 1), (0, 1)].map(|(a, b)| {
|
||||
let mut c = [0usize; 3];
|
||||
c[d] = side;
|
||||
c[da] = a;
|
||||
c[db] = b;
|
||||
c
|
||||
})
|
||||
}
|
||||
|
||||
/// The two directions other than `d`, lower first.
|
||||
pub(crate) fn other(d: usize) -> (usize, usize) {
|
||||
match d {
|
||||
0 => (1, 2),
|
||||
1 => (0, 2),
|
||||
_ => (0, 1),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
|
||||
let brep = ctx.brep;
|
||||
let cube = recognize(brep)?;
|
||||
let groups = cube.groups;
|
||||
let mut ia = Intervals::default();
|
||||
for e in 0..brep.edges.len() {
|
||||
ia.var(ideal_divisions(ctx.field, &brep.edge_points(e, true))?, 1);
|
||||
@@ -99,7 +164,7 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
|
||||
}
|
||||
}
|
||||
let counts = ia.solve()?;
|
||||
let count = |g: &Vec<(usize, bool)>| g.first().and_then(|x| counts.get(x.0)).copied().unwrap_or(1);
|
||||
let count = |g: &[(usize, bool); 4]| g.first().and_then(|x| counts.get(x.0)).copied().unwrap_or(1);
|
||||
let (ni, nj, nk) = (count(&groups[0]), count(&groups[1]), count(&groups[2]));
|
||||
let total = (ni + 1).checked_mul(nj + 1).and_then(|x| x.checked_mul(nk + 1)).filter(|&x| x <= MAX_NODES);
|
||||
let total = total.ok_or_else(|| Error::Limit(format!("a {ni} × {nj} × {nk} block exceeds {MAX_NODES} nodes")))?;
|
||||
@@ -135,26 +200,8 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
|
||||
// Faces: grids from their edges, projected onto the B-rep face their four edges bound.
|
||||
let n = [ni, nj, nk];
|
||||
transfinite_faces(&mut x, n, None, |d, side, grid| {
|
||||
let (da, db) = match d {
|
||||
0 => (1, 2),
|
||||
1 => (0, 2),
|
||||
_ => (0, 1),
|
||||
};
|
||||
let logical = |a: usize, b: usize| {
|
||||
let mut c = [0usize; 3];
|
||||
c[d] = side;
|
||||
c[da] = a;
|
||||
c[db] = b;
|
||||
at(c[0], c[1], c[2])
|
||||
};
|
||||
let ring = [logical(0, 0), logical(1, 0), logical(1, 1), logical(0, 1)];
|
||||
let mut bounding: Vec<usize> = (0..4).filter_map(|m| edge_between(ring[m], ring[(m + 1) % 4]).map(|x| x.0)).collect();
|
||||
bounding.sort_unstable();
|
||||
let face = brep.faces.iter().position(|f| {
|
||||
let mut es: Vec<usize> = f.loops.iter().flatten().map(|x| x.0).collect();
|
||||
es.sort_unstable();
|
||||
es == bounding
|
||||
});
|
||||
let (da, db) = other(d);
|
||||
let face = cube.faces.get(d).and_then(|r| r.get(side)).copied();
|
||||
let (na, nb) = (n[da], n[db]);
|
||||
for b in 1..nb {
|
||||
for a in 1..na {
|
||||
|
||||
+208
-36
@@ -19,6 +19,11 @@
|
||||
//! A body none of these fits is [`Error::Unsupported`] with each method's reason: the input for
|
||||
//! decomposition into sweepable and mappable regions.
|
||||
//!
|
||||
//! **Partitioned parts.** [`hex_mesh_cells`] ([`cells`]) meshes a part split into cells that
|
||||
//! meet face to face (SolidCraft split bodies) as one conforming mesh: every cell a mapped block,
|
||||
//! interval assignment over all cells together, each shared face meshed once and its nodes
|
||||
//! merged by identity.
|
||||
//!
|
||||
//! **Common steps.** Interval assignment ([`interval`]) picks the division counts of shared curves
|
||||
//! together. Boundary nodes on analytic faces are placed on the exact surface; quadratic hexahedra
|
||||
//! (hex20) get their mid-edge nodes projected onto the faces (and onto both faces of a B-rep
|
||||
@@ -31,6 +36,7 @@
|
||||
|
||||
mod arrangement;
|
||||
mod brep;
|
||||
mod cells;
|
||||
mod geom;
|
||||
mod interval;
|
||||
mod layered;
|
||||
@@ -43,6 +49,7 @@ mod tests;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use cells::{Cell, CellsMesh, GROWTH, MAX_CELLS, hex_mesh_cells};
|
||||
pub use geom::Surface;
|
||||
|
||||
use brep::Brep;
|
||||
@@ -198,16 +205,7 @@ pub fn hex_mesh(surface: &BoundarySurface, faces: &[Option<Surface>], field: &dy
|
||||
surface.shells().len()
|
||||
)));
|
||||
}
|
||||
// A mesh far beyond the element limit is refused before any work: the solid's volume over
|
||||
// the cube of the smallest size the field asks for at the boundary.
|
||||
let mut h_min = f64::INFINITY;
|
||||
for p in surface.points() {
|
||||
h_min = h_min.min(crate::size::checked_size(field, *p)?);
|
||||
}
|
||||
let estimate = surface.volume().abs() / h_min.powi(3);
|
||||
if !(estimate <= 8.0 * opts.max_elements as f64) {
|
||||
return Err(Error::Limit(format!("about {estimate:.3e} hexahedra at this size; the limit is {}", opts.max_elements)));
|
||||
}
|
||||
check_estimate(&[surface], field, opts)?;
|
||||
let brep = Brep::new(surface, faces)?;
|
||||
let tree = TriangleTree::new(surface)?;
|
||||
let ctx = Ctx { brep: &brep, tree: &tree, field, opts, tol: RELATIVE_TOLERANCE * brep.diagonal };
|
||||
@@ -253,7 +251,7 @@ pub fn hex_mesh(surface: &BoundarySurface, faces: &[Option<Surface>], field: &dy
|
||||
Err(e @ Error::Limit(_)) => return Err(e),
|
||||
Err(e) => last = Some(e),
|
||||
},
|
||||
_ => match mapped::mesh(&ctx).and_then(|raw| finish(&ctx, raw, method)) {
|
||||
_ => match mapped::mesh(&ctx).and_then(|raw| finish(&Cells::one(&ctx), raw, method)) {
|
||||
Ok(mesh) => return Ok(mesh),
|
||||
Err(e @ Error::Limit(_)) => return Err(e),
|
||||
Err(e) => last = Some(e),
|
||||
@@ -266,6 +264,23 @@ pub fn hex_mesh(surface: &BoundarySurface, faces: &[Option<Surface>], field: &dy
|
||||
Err(Error::Unsupported(format!("no structured hex method fits this body ({})", reasons.join("; "))))
|
||||
}
|
||||
|
||||
/// Refuse a mesh far beyond the element limit before any work: the solids' volume over the cube
|
||||
/// of the smallest size the field asks for on their boundaries.
|
||||
fn check_estimate(surfaces: &[&BoundarySurface], field: &dyn SizeField, opts: &HexOptions) -> Result<()> {
|
||||
let (mut h_min, mut volume) = (f64::INFINITY, 0.0);
|
||||
for s in surfaces {
|
||||
for p in s.points() {
|
||||
h_min = h_min.min(crate::size::checked_size(field, *p)?);
|
||||
}
|
||||
volume += s.volume().abs();
|
||||
}
|
||||
let estimate = volume / h_min.powi(3);
|
||||
if !(estimate <= 8.0 * opts.max_elements as f64) {
|
||||
return Err(Error::Limit(format!("about {estimate:.3e} hexahedra at this size; the limit is {}", opts.max_elements)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The frames a layered method can try, fewest levels first, or the first reason none fits.
|
||||
fn candidate_setups(ctx: &Ctx, method: HexMethod) -> std::result::Result<Vec<layered::Setup>, String> {
|
||||
let frames = if method == HexMethod::Extrude { layered::translations(ctx.brep) } else { layered::rotations(ctx.brep) };
|
||||
@@ -292,7 +307,7 @@ fn candidate_setups(ctx: &Ctx, method: HexMethod) -> std::result::Result<Vec<lay
|
||||
fn with_refinement(ctx: &Ctx, method: HexMethod, run: impl Fn(u32) -> Result<Raw>) -> Result<HexMesh> {
|
||||
let mut last = Error::Failed("no attempt".into());
|
||||
for refine in 0..=MAX_REFINE {
|
||||
match run(refine).and_then(|raw| finish(ctx, raw, method)) {
|
||||
match run(refine).and_then(|raw| finish(&Cells::one(ctx), raw, method)) {
|
||||
Ok(mesh) => return Ok(mesh),
|
||||
Err(e @ Error::Failed(_)) => last = e,
|
||||
Err(e) => return Err(e),
|
||||
@@ -314,14 +329,130 @@ fn hex_face(k: usize) -> [usize; 4] {
|
||||
ElementKind::Hex8.faces().get(k).and_then(|f| <[usize; 4]>::try_from(*f).ok()).unwrap_or([0, 1, 2, 3])
|
||||
}
|
||||
|
||||
/// Tag, check, upgrade to hex20 when asked, gate and measure.
|
||||
fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
/// The cells a mesh spans, for [`finish`]: one context per cell and the cell of each element.
|
||||
pub(crate) struct Cells<'c, 'a> {
|
||||
pub ctxs: &'c [Ctx<'a>],
|
||||
/// The cell of each hexahedron (index into `ctxs`); empty: all in the first.
|
||||
pub of: &'c [u32],
|
||||
/// The cells' names, for messages.
|
||||
pub names: &'c [&'c str],
|
||||
}
|
||||
|
||||
impl<'c, 'a> Cells<'c, 'a> {
|
||||
pub(crate) fn one(ctx: &'c Ctx<'a>) -> Self {
|
||||
Cells { ctxs: std::slice::from_ref(ctx), of: &[], names: &[] }
|
||||
}
|
||||
|
||||
fn name(&self, c: usize) -> String {
|
||||
self.names.get(c).map_or_else(|| c.to_string(), |n| format!("`{n}`"))
|
||||
}
|
||||
|
||||
fn cell(&self, e: usize) -> usize {
|
||||
self.of.get(e).map_or(0, |&c| c as usize)
|
||||
}
|
||||
|
||||
fn ctx(&self, e: usize) -> Result<&Ctx<'a>> {
|
||||
self.ctxs.get(self.cell(e)).ok_or_else(|| Error::Failed(format!("element {e} has no cell")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Buckets of a uniform grid over the cells' bounding boxes: which cells may be near a point.
|
||||
struct CellGrid {
|
||||
lo: P,
|
||||
step: f64,
|
||||
dims: [usize; 3],
|
||||
buckets: Vec<Vec<usize>>,
|
||||
}
|
||||
|
||||
/// Cosine of the largest angle between a boundary face's normal and the reversed normal of
|
||||
/// another cell's triangle for the face to lie on that triangle (about 25°): a face on another
|
||||
/// cell faces it, while faces that merely meet at an edge (a fillet beside a split) do not.
|
||||
const FACING: f64 = 0.9;
|
||||
|
||||
/// Buckets per axis of a [`CellGrid`] at most.
|
||||
const MAX_GRID_DIM: usize = 64;
|
||||
|
||||
impl CellGrid {
|
||||
/// The cells' boxes grown by `gap`.
|
||||
fn new(ctxs: &[Ctx], gap: f64) -> CellGrid {
|
||||
let boxes: Vec<(P, P)> = ctxs
|
||||
.iter()
|
||||
.map(|c| {
|
||||
c.brep.surface.points().iter().fold(([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]), |(mut lo, mut hi), p| {
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(p[k] - gap);
|
||||
hi[k] = hi[k].max(p[k] + gap);
|
||||
}
|
||||
(lo, hi)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let (lo, hi) = boxes.iter().fold(([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]), |(mut lo, mut hi), (a, b)| {
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(a[k]);
|
||||
hi[k] = hi[k].max(b[k]);
|
||||
}
|
||||
(lo, hi)
|
||||
});
|
||||
let per_axis = ((ctxs.len() as f64).cbrt().ceil() as usize * 2).clamp(1, MAX_GRID_DIM);
|
||||
let extent = (0..3).map(|k| hi[k] - lo[k]).fold(0.0, f64::max);
|
||||
let step = if extent > 0.0 && extent.is_finite() { extent / per_axis as f64 } else { 1.0 };
|
||||
let dim = |k: usize| (((hi[k] - lo[k]) / step).ceil() as usize).clamp(1, MAX_GRID_DIM);
|
||||
let dims = [dim(0), dim(1), dim(2)];
|
||||
let mut grid = CellGrid { lo, step, dims, buckets: vec![Vec::new(); dims[0] * dims[1] * dims[2]] };
|
||||
for (c, (a, b)) in boxes.iter().enumerate() {
|
||||
let (i0, i1) = (grid.index(*a), grid.index(*b));
|
||||
for i in i0[0]..=i1[0] {
|
||||
for j in i0[1]..=i1[1] {
|
||||
for k in i0[2]..=i1[2] {
|
||||
if let Some(bucket) = grid.buckets.get_mut((k * dims[1] + j) * dims[0] + i) {
|
||||
bucket.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
grid
|
||||
}
|
||||
|
||||
fn index(&self, p: P) -> [usize; 3] {
|
||||
[0, 1, 2].map(|k| {
|
||||
let x = ((p[k] - self.lo[k]) / self.step).floor();
|
||||
if x.is_finite() && x > 0.0 { (x as usize).min(self.dims[k] - 1) } else { 0 }
|
||||
})
|
||||
}
|
||||
|
||||
fn near(&self, p: P) -> &[usize] {
|
||||
let [i, j, k] = self.index(p);
|
||||
self.buckets.get((k * self.dims[1] + j) * self.dims[0] + i).map(Vec::as_slice).unwrap_or(&[])
|
||||
}
|
||||
}
|
||||
|
||||
/// A boundary quadrilateral of a mesh being finished: the element face, its cell and the B-rep
|
||||
/// face (index into the cell's faces) it lies on.
|
||||
struct Skin {
|
||||
element: u32,
|
||||
face: u8,
|
||||
cell: usize,
|
||||
brep: usize,
|
||||
}
|
||||
|
||||
/// Tag, check, upgrade to hex20 when asked, gate and measure. Besides the gate, the tiling and
|
||||
/// the fidelity checks, no boundary face of a mesh of several cells may lie on another cell's
|
||||
/// boundary (a free internal face: cells that do not conform there), and the mesh boundary must
|
||||
/// be a closed 2-manifold of quadrilaterals (every edge on exactly two).
|
||||
fn finish(cells: &Cells, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
let Raw { mut nodes, hexes, description } = raw;
|
||||
let first = cells.ctxs.first().ok_or_else(|| Error::Failed("no cell to finish".into()))?;
|
||||
let opts = first.opts;
|
||||
if hexes.is_empty() {
|
||||
return Err(Error::Failed("the method produced no elements".into()));
|
||||
}
|
||||
if hexes.len() > ctx.opts.max_elements {
|
||||
return Err(Error::Limit(format!("the mesh has {} hexahedra; the limit is {}", hexes.len(), ctx.opts.max_elements)));
|
||||
if hexes.len() > opts.max_elements {
|
||||
return Err(Error::Limit(format!("the mesh has {} hexahedra; the limit is {}", hexes.len(), opts.max_elements)));
|
||||
}
|
||||
if !cells.of.is_empty() && cells.of.len() != hexes.len() {
|
||||
return Err(Error::Failed("a cell is needed for every element".into()));
|
||||
}
|
||||
fieldcraft_model::check_coordinates(nodes.as_flattened())?;
|
||||
let at = |nodes: &[P], i: u32| nodes.get(i as usize).copied().ok_or_else(|| Error::Failed(format!("node {i} out of range")));
|
||||
@@ -337,8 +468,14 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
if let Some(n) = uses.values().copied().max().filter(|&n| n > 2) {
|
||||
return Err(Error::Failed(format!("a hexahedron face is shared by {n} elements (the mesh is not a manifold)")));
|
||||
}
|
||||
// A boundary face this close to another cell's boundary lies on it.
|
||||
let gap = 2.0 * opts.chordal_tolerance + cells.ctxs.iter().map(|c| c.tol).fold(0.0, f64::max);
|
||||
let others = (cells.ctxs.len() > 1).then(|| CellGrid::new(cells.ctxs, gap));
|
||||
let mut skin: Vec<Skin> = Vec::new();
|
||||
let mut boundary = Vec::new();
|
||||
let mut fidelity = 0.0;
|
||||
// Boundary edges of the quadrilaterals: each must be on exactly two of them.
|
||||
let mut rim: HashMap<(u32, u32), u32> = HashMap::new();
|
||||
for (e, h) in hexes.iter().enumerate() {
|
||||
for k in 0..6 {
|
||||
let corners = hex_face(k).map(|i| h.get(i).copied().unwrap_or(0));
|
||||
@@ -347,6 +484,11 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
if uses.get(&key) != Some(&1) {
|
||||
continue;
|
||||
}
|
||||
for m in 0..4 {
|
||||
let (a, b) = (corners[m], corners[(m + 1) % 4]);
|
||||
*rim.entry((a.min(b), a.max(b))).or_insert(0) += 1;
|
||||
}
|
||||
let ctx = cells.ctx(e)?;
|
||||
let [a, b, c, d] = [at(&nodes, corners[0])?, at(&nodes, corners[1])?, at(&nodes, corners[2])?, at(&nodes, corners[3])?];
|
||||
let centre = scale(add(add(a, b), add(c, d)), 0.25);
|
||||
let n = cross(sub(c, a), sub(d, b));
|
||||
@@ -357,14 +499,35 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
.closest(centre, &|t| dot(ctx.tree.normal(t), n) > 0.0)
|
||||
.ok_or_else(|| Error::Failed(format!("no boundary face faces the same way as element {e}'s face {}", k + 1)))?;
|
||||
let f = ctx.brep.face_of_triangle(hit.triangle).ok_or_else(|| Error::Failed("triangle without a face".into()))?;
|
||||
fidelity += area * (hit.dist + ctx.opts.chordal_tolerance);
|
||||
boundary.push(BoundaryFace { element: e as u32, face: k as u8 + 1, brep_face: ctx.brep.face_tag(f) });
|
||||
let cell = cells.cell(e);
|
||||
if let Some(grid) = &others {
|
||||
for &o in grid.near(centre) {
|
||||
let Some(other) = cells.ctxs.get(o).filter(|_| o != cell) else { continue };
|
||||
if other.tree.closest_within(centre, gap, &|t| dot(other.tree.normal(t), n) < -FACING).is_some() {
|
||||
return Err(Error::Failed(format!(
|
||||
"element {e}'s face {} lies on cell {}'s boundary without a neighbour there: cells {} and {} do not conform (split them so that they meet face to face)",
|
||||
k + 1,
|
||||
cells.name(o),
|
||||
cells.name(cell),
|
||||
cells.name(o)
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// The geometry's own face tag (a member of a composite face keeps its own).
|
||||
let tag = ctx.brep.surface.tri_face().get(hit.triangle as usize).copied().unwrap_or_else(|| ctx.brep.face_tag(f));
|
||||
fidelity += area * (hit.dist + opts.chordal_tolerance);
|
||||
boundary.push(BoundaryFace { element: e as u32, face: k as u8 + 1, brep_face: tag });
|
||||
skin.push(Skin { element: e as u32, face: k as u8 + 1, cell, brep: f });
|
||||
}
|
||||
}
|
||||
let kind = if ctx.opts.quadratic { ElementKind::Hex20 } else { ElementKind::Hex8 };
|
||||
if let Some((&(a, b), &n)) = rim.iter().filter(|(_, n)| **n != 2).min_by_key(|(k, _)| **k) {
|
||||
return Err(Error::Failed(format!("the mesh boundary is not closed: the edge between nodes {a} and {b} is on {n} boundary faces")));
|
||||
}
|
||||
let kind = if opts.quadratic { ElementKind::Hex20 } else { ElementKind::Hex8 };
|
||||
let mut connectivity: Vec<u32> = Vec::with_capacity(hexes.len() * kind.node_count());
|
||||
if ctx.opts.quadratic {
|
||||
let mids = mid_edge_nodes(ctx, &mut nodes, &hexes, &boundary)?;
|
||||
if opts.quadratic {
|
||||
let mids = mid_edge_nodes(cells, &mut nodes, &hexes, &skin)?;
|
||||
for (h, m) in hexes.iter().zip(&mids) {
|
||||
connectivity.extend_from_slice(h);
|
||||
connectivity.extend_from_slice(m);
|
||||
@@ -398,7 +561,7 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
let gate = validity_gate_checks(checks.iter().map(|(l, c)| (*l, c)), &GateOptions { expected_volume: Some(enclosed), volume_tolerance: 1e-9 })?
|
||||
.into_result()
|
||||
.map_err(|e| Error::Failed(format!("{} ({description})", plain(&e))))?;
|
||||
let surface_volume = ctx.brep.surface.volume();
|
||||
let surface_volume: f64 = cells.ctxs.iter().map(|c| c.brep.surface.volume()).sum();
|
||||
let slack = 2.0 * fidelity + 1e-9 * surface_volume.abs();
|
||||
if (gate.volume - surface_volume).abs() > slack {
|
||||
return Err(Error::Failed(format!(
|
||||
@@ -478,46 +641,52 @@ pub(crate) fn project_to_face(ctx: &Ctx, f: usize, p: P) -> Option<P> {
|
||||
return Some(q);
|
||||
}
|
||||
let tag = face.id;
|
||||
let tri_face = ctx.brep.surface.tri_face();
|
||||
ctx.tree.closest(p, &|t| tri_face.get(t as usize) == Some(&tag)).map(|h| h.point)
|
||||
ctx.tree.closest(p, &|t| ctx.brep.tag(t) == Some(tag)).map(|h| h.point)
|
||||
}
|
||||
|
||||
/// The point on every listed face (their common curve or corner) nearest `p`, by alternating
|
||||
/// projections.
|
||||
pub(crate) fn project_to_faces(ctx: &Ctx, faces: &[usize], p: P) -> P {
|
||||
let pairs: Vec<(&Ctx, usize)> = faces.iter().map(|&f| (ctx, f)).collect();
|
||||
project_alternating(&pairs, ctx.tol, p)
|
||||
}
|
||||
|
||||
/// The point on every listed face of the listed cells nearest `p`, by alternating projections
|
||||
/// until a round moves it less than a thousandth of `tol`.
|
||||
fn project_alternating(faces: &[(&Ctx, usize)], tol: f64, p: P) -> P {
|
||||
let mut x = p;
|
||||
for _ in 0..PROJECTION_ROUNDS {
|
||||
let before = x;
|
||||
for &f in faces {
|
||||
for &(ctx, f) in faces {
|
||||
if let Some(q) = project_to_face(ctx, f, x) {
|
||||
x = q;
|
||||
}
|
||||
}
|
||||
if faces.len() < 2 || dist(before, x) <= ctx.tol * 1e-3 {
|
||||
if faces.len() < 2 || dist(before, x) <= tol * 1e-3 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
/// Mid-edge nodes of every hexahedron (Hex20 order: positions 8..20), shared between elements;
|
||||
/// boundary edges get theirs on the B-rep faces of the boundary quads around them.
|
||||
fn mid_edge_nodes(ctx: &Ctx, nodes: &mut Vec<P>, hexes: &[[u32; 8]], boundary: &[BoundaryFace]) -> Result<Vec<[u32; 12]>> {
|
||||
// B-rep faces at each boundary edge.
|
||||
let mut edge_faces: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
|
||||
for b in boundary {
|
||||
/// Mid-edge nodes of every hexahedron (Hex20 order: positions 8..20), shared between elements
|
||||
/// (of any cell); boundary edges get theirs on the B-rep faces of the boundary quads around them.
|
||||
fn mid_edge_nodes(cells: &Cells, nodes: &mut Vec<P>, hexes: &[[u32; 8]], skin: &[Skin]) -> Result<Vec<[u32; 12]>> {
|
||||
// B-rep faces (cell, face) at each boundary edge.
|
||||
let mut edge_faces: HashMap<(u32, u32), Vec<(usize, usize)>> = HashMap::new();
|
||||
for b in skin {
|
||||
let Some(h) = hexes.get(b.element as usize) else { continue };
|
||||
let c = hex_face(b.face as usize - 1).map(|i| h.get(i).copied().unwrap_or(0));
|
||||
let f = ctx.brep.faces.binary_search_by_key(&b.brep_face, |x| x.id).map_err(|_| Error::Failed("boundary tag".into()))?;
|
||||
for k in 0..4 {
|
||||
let (a, z) = (c[k], c[(k + 1) % 4]);
|
||||
let list = edge_faces.entry((a.min(z), a.max(z))).or_default();
|
||||
if !list.contains(&f) {
|
||||
list.push(f);
|
||||
if !list.contains(&(b.cell, b.brep)) {
|
||||
list.push((b.cell, b.brep));
|
||||
}
|
||||
}
|
||||
}
|
||||
let edges = ElementKind::Hex20.edges();
|
||||
let tol = cells.ctxs.iter().map(|c| c.tol).fold(f64::INFINITY, f64::min);
|
||||
let mut mid: HashMap<(u32, u32), u32> = HashMap::new();
|
||||
let mut out = Vec::with_capacity(hexes.len());
|
||||
for h in hexes {
|
||||
@@ -533,7 +702,10 @@ fn mid_edge_nodes(ctx: &Ctx, nodes: &mut Vec<P>, hexes: &[[u32; 8]], boundary: &
|
||||
let (a, b) = (nodes.get(i as usize).copied().unwrap_or([0.0; 3]), nodes.get(j as usize).copied().unwrap_or([0.0; 3]));
|
||||
let p = scale(add(a, b), 0.5);
|
||||
let p = match edge_faces.get(&key) {
|
||||
Some(fs) => project_to_faces(ctx, fs, p),
|
||||
Some(fs) => {
|
||||
let pairs: Vec<(&Ctx, usize)> = fs.iter().filter_map(|&(c, f)| cells.ctxs.get(c).map(|ctx| (ctx, f))).collect();
|
||||
project_alternating(&pairs, tol, p)
|
||||
}
|
||||
None => p,
|
||||
};
|
||||
nodes.push(p);
|
||||
|
||||
@@ -292,3 +292,191 @@ fn unsweepable_bodies_say_why_and_hostile_input_is_an_error() {
|
||||
let m = hex_mesh(&s, &garbage, &UniformSize(1.0), &HexOptions::default()).unwrap();
|
||||
assert_eq!(m.element_count(), 24);
|
||||
}
|
||||
|
||||
// ---- Conforming cells ----
|
||||
|
||||
/// A prism over a counter-clockwise polygon from z0 to z1 whose side from vertex k to k + 1 gets
|
||||
/// tag `side_tags[k]` (bottom 0, top 1); quads of one tag make one face.
|
||||
fn tagged_prism(poly: &[[f64; 2]], side_tags: &[u32], z0: f64, z1: f64) -> BoundarySurface {
|
||||
let n = poly.len();
|
||||
let mut pts: Vec<[f64; 3]> = poly.iter().map(|p| [p[0], p[1], z0]).collect();
|
||||
pts.extend(poly.iter().map(|p| [p[0], p[1], z1]));
|
||||
let mut tris = Vec::new();
|
||||
let mut tags = Vec::new();
|
||||
for t in ears(&pts, &(0..n as u32).rev().collect::<Vec<_>>()) {
|
||||
tris.push(t);
|
||||
tags.push(0);
|
||||
}
|
||||
for t in ears(&pts, &(n as u32..2 * n as u32).collect::<Vec<_>>()) {
|
||||
tris.push(t);
|
||||
tags.push(1);
|
||||
}
|
||||
for (k, &tag) in side_tags.iter().enumerate().take(n) {
|
||||
let (a, b) = (k as u32, ((k + 1) % n) as u32);
|
||||
let (c, d) = (b + n as u32, a + n as u32);
|
||||
tris.push([a, b, c]);
|
||||
tris.push([a, c, d]);
|
||||
tags.extend([tag, tag]);
|
||||
}
|
||||
BoundarySurface::new(&pts, &tris, &tags, &SurfaceOptions::default()).unwrap()
|
||||
}
|
||||
|
||||
fn cells_of<'a>(surfaces: &'a [BoundarySurface], faces: &'a [Vec<Option<Surface>>]) -> Vec<Cell<'a>> {
|
||||
surfaces.iter().zip(faces).map(|(surface, faces)| Cell { name: "cell", surface, faces }).collect()
|
||||
}
|
||||
|
||||
/// Boxes `[x0, x1] × [y0, y1] × [0, h]` as prisms (faces 0 bottom, 1 top, 2..6 the sides).
|
||||
fn boxes(spans: &[([f64; 2], [f64; 2])], h: f64) -> Vec<BoundarySurface> {
|
||||
spans.iter().map(|(x, y)| prism(&[[x[0], y[0]], [x[1], y[0]], [x[1], y[1]], [x[0], y[1]]], 0.0, h)).collect()
|
||||
}
|
||||
|
||||
/// Every node of the mesh at a distinct position: the cells' nodes were merged.
|
||||
fn distinct_positions(m: &HexMesh) -> bool {
|
||||
let mut keys: Vec<[u64; 3]> = m.nodes.iter().map(|p| p.map(|x| (x + 0.0).to_bits())).collect();
|
||||
keys.sort_unstable();
|
||||
keys.windows(2).all(|w| w[0] != w[1])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boxes_side_by_side_conform() {
|
||||
// Two boxes sharing x = 2, then a 2 × 2 arrangement whose middle edge four cells share.
|
||||
let pairs = boxes(&[([0.0, 2.0], [0.0, 1.0]), ([2.0, 3.0], [0.0, 1.0])], 1.0);
|
||||
let none = vec![Vec::new(); 2];
|
||||
let m = hex_mesh_cells(&cells_of(&pairs, &none), &UniformSize(0.5), &HexOptions::default()).unwrap();
|
||||
assert_eq!(m.interfaces, 1);
|
||||
assert_eq!(m.blocks.iter().map(|b| b[0] * b[1] * b[2]).sum::<usize>(), m.mesh.element_count());
|
||||
assert_eq!(m.mesh.element_count(), 6 * 2 * 2);
|
||||
// One structured grid: 7 × 3 × 3 nodes, none duplicated on the interface.
|
||||
assert_eq!(m.mesh.nodes.len(), 7 * 3 * 3);
|
||||
assert!(distinct_positions(&m.mesh));
|
||||
assert!((m.mesh.report.volume - 3.0).abs() < 1e-12);
|
||||
assert!(m.mesh.report.gate.passed());
|
||||
// The boundary is the outside only: 2·(6·2 + 6·2 + 2·2) quads.
|
||||
assert_eq!(m.mesh.boundary.len(), 56);
|
||||
assert_eq!(m.element_cell.iter().filter(|&&c| c == 0).count(), 16);
|
||||
|
||||
let grid = boxes(&[([0.0, 1.0], [0.0, 1.0]), ([1.0, 2.0], [0.0, 1.0]), ([0.0, 1.0], [1.0, 2.0]), ([1.0, 2.0], [1.0, 2.0])], 1.0);
|
||||
let none = vec![Vec::new(); 4];
|
||||
let a = hex_mesh_cells(&cells_of(&grid, &none), &UniformSize(0.25), &HexOptions::default()).unwrap();
|
||||
assert_eq!(a.interfaces, 4);
|
||||
assert_eq!(a.mesh.nodes.len(), 9 * 9 * 5);
|
||||
assert!(distinct_positions(&a.mesh));
|
||||
// Deterministic, and hex20 shares its mid-edge nodes across the interfaces too.
|
||||
let b = hex_mesh_cells(&cells_of(&grid, &none), &UniformSize(0.25), &HexOptions::default()).unwrap();
|
||||
assert_eq!(a, b);
|
||||
let q = hex_mesh_cells(&cells_of(&grid, &none), &UniformSize(0.25), &HexOptions { quadratic: true, ..HexOptions::default() }).unwrap();
|
||||
assert_eq!(q.mesh.kind, ElementKind::Hex20);
|
||||
// Corner nodes plus one node per lattice edge: 9·9·5 + (8·9·5)·2 + 9·9·4.
|
||||
assert_eq!(q.mesh.nodes.len(), 405 + 720 + 324);
|
||||
assert!(distinct_positions(&q.mesh));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cells_that_do_not_meet_face_to_face_are_refused() {
|
||||
// One box against two half-height boxes: its face at x = 1 meets two faces (a T-junction).
|
||||
let t = vec![
|
||||
prism(&[[0.0, 0.0], [1.0, 0.0], [1.0, 2.0], [0.0, 2.0]], 0.0, 1.0),
|
||||
prism(&[[1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]], 0.0, 1.0),
|
||||
prism(&[[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0]], 0.0, 1.0),
|
||||
];
|
||||
let none = vec![Vec::new(); 3];
|
||||
match hex_mesh_cells(&cells_of(&t, &none), &UniformSize(0.5), &HexOptions::default()) {
|
||||
Err(Error::Failed(why)) => assert!(why.contains("interface") || why.contains("conform"), "{why}"),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
// A cell that is no logical cube says which.
|
||||
let l = vec![prism(&[[0.0, 0.0], [6.0, 0.0], [6.0, 2.0], [2.0, 2.0], [2.0, 6.0], [0.0, 6.0]], 0.0, 2.0)];
|
||||
match hex_mesh_cells(&cells_of(&l, &[Vec::new()]), &UniformSize(1.0), &HexOptions::default()) {
|
||||
Err(Error::Unsupported(why)) => assert!(why.starts_with("cell `cell`: not a logical cube"), "{why}"),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_cell_input_is_an_error() {
|
||||
let two = boxes(&[([0.0, 1.0], [0.0, 1.0]), ([1.0, 2.0], [0.0, 1.0])], 1.0);
|
||||
let none = vec![Vec::new(); 2];
|
||||
let cells = cells_of(&two, &none);
|
||||
assert!(hex_mesh_cells(&[], &UniformSize(1.0), &HexOptions::default()).is_err());
|
||||
for method in [HexMethod::Sweep, HexMethod::Extrude, HexMethod::Revolve, HexMethod::Swept] {
|
||||
assert!(matches!(hex_mesh_cells(&cells, &UniformSize(1.0), &HexOptions { method, ..HexOptions::default() }), Err(Error::Invalid(_))));
|
||||
}
|
||||
let bad_sizes: [&dyn crate::size::SizeField; 3] = [&UniformSize(f64::NAN), &UniformSize(0.0), &UniformSize(-1.0)];
|
||||
for f in bad_sizes {
|
||||
assert!(hex_mesh_cells(&cells, f, &HexOptions::default()).is_err());
|
||||
}
|
||||
assert!(matches!(hex_mesh_cells(&cells, &UniformSize(1e-4), &HexOptions::default()), Err(Error::Limit(_))));
|
||||
assert!(matches!(hex_mesh_cells(&cells, &UniformSize(0.1), &HexOptions { max_elements: 100, ..HexOptions::default() }), Err(Error::Limit(_))));
|
||||
assert!(hex_mesh_cells(&cells, &UniformSize(1.0), &HexOptions { chordal_tolerance: -1.0, ..HexOptions::default() }).is_err());
|
||||
// The same cell twice: two cells on the same side of every face.
|
||||
let same = [two[0].clone(), two[0].clone()];
|
||||
assert!(hex_mesh_cells(&cells_of(&same, &none), &UniformSize(0.5), &HexOptions::default()).is_err());
|
||||
// Garbage analytic surfaces are not trusted.
|
||||
let garbage = vec![vec![Some(Surface::Cylinder { origin: [f64::NAN; 3], axis: [0.0; 3], radius: -1.0 }); 6]; 2];
|
||||
let m = hex_mesh_cells(&cells_of(&two, &garbage), &UniformSize(0.5), &HexOptions::default()).unwrap();
|
||||
assert_eq!(m.mesh.element_count(), 16);
|
||||
}
|
||||
|
||||
/// The four cells of an O-grid round a hole of radius 1 in the square [−2, 2]², height `h`: each
|
||||
/// cell the hole's quarter between two diagonals and a side of the square. The hole's quarter is
|
||||
/// one face (tag 5) or two halves of the cylinder (tags 5 and 6: virtual topology joins them).
|
||||
fn o_grid(h: f64, arc_segments: usize, split_arcs: bool) -> (Vec<BoundarySurface>, Vec<Vec<Option<Surface>>>) {
|
||||
use std::f64::consts::FRAC_PI_4;
|
||||
let mut surfaces = Vec::new();
|
||||
let mut faces = Vec::new();
|
||||
for q in 0..4 {
|
||||
let turn = q as f64 * 2.0 * FRAC_PI_4;
|
||||
let rot = |p: [f64; 2]| [p[0] * turn.cos() - p[1] * turn.sin(), p[0] * turn.sin() + p[1] * turn.cos()];
|
||||
let mut poly = vec![rot([FRAC_PI_4.cos(), -FRAC_PI_4.sin()]), rot([2.0, -2.0]), rot([2.0, 2.0])];
|
||||
let mut tags = vec![2, 3, 4];
|
||||
for k in 0..arc_segments {
|
||||
let t = FRAC_PI_4 - 2.0 * FRAC_PI_4 * k as f64 / arc_segments as f64;
|
||||
poly.push(rot([t.cos(), t.sin()]));
|
||||
tags.push(if split_arcs && 2 * k >= arc_segments { 6 } else { 5 });
|
||||
}
|
||||
// Exact corners where the cells meet.
|
||||
let snap = |p: [f64; 2]| p.map(|x| if (x.abs() - 2.0).abs() < 1e-12 { 2.0 * x.signum() } else { x });
|
||||
let poly: Vec<[f64; 2]> = poly.into_iter().map(snap).collect();
|
||||
surfaces.push(tagged_prism(&poly, &tags, 0.0, h));
|
||||
let cyl = Some(Surface::Cylinder { origin: [0.0; 3], axis: [0.0, 0.0, 1.0], radius: 1.0 });
|
||||
faces.push(vec![None, None, None, None, None, cyl, cyl]);
|
||||
}
|
||||
(surfaces, faces)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_o_grid_round_a_hole_conforms_with_graded_radial_lines() {
|
||||
for split_arcs in [false, true] {
|
||||
let (surfaces, faces) = o_grid(0.5, 24, split_arcs);
|
||||
let opts = HexOptions { chordal_tolerance: 1.0 - (std::f64::consts::PI / 48.0).cos(), ..HexOptions::default() };
|
||||
let m = hex_mesh_cells(&cells_of(&surfaces, &faces), &UniformSize(0.5), &opts).unwrap();
|
||||
assert_eq!(m.interfaces, 4, "{split_arcs}");
|
||||
assert!(m.mesh.report.gate.passed());
|
||||
assert!(m.mesh.report.min_scaled_jacobian > 0.3, "{}", m.mesh.report.min_scaled_jacobian);
|
||||
assert!(distinct_positions(&m.mesh));
|
||||
// A ring of quads round the hole: every block is (around) × (radial) × (through).
|
||||
let sorted = |b: [usize; 3]| {
|
||||
let mut b = b;
|
||||
b.sort_unstable();
|
||||
b
|
||||
};
|
||||
assert!(m.blocks.iter().all(|&b| sorted(b) == sorted(m.blocks[0])), "{:?}", m.blocks);
|
||||
let per_block = m.blocks[0].iter().product::<usize>();
|
||||
assert_eq!(m.mesh.element_count(), 4 * per_block);
|
||||
// Nodes on the hole lie on it.
|
||||
let hole: Vec<u32> = if split_arcs { vec![5, 6] } else { vec![5] };
|
||||
assert!(on_radius(&m.mesh, &hole, 1.0), "{split_arcs}");
|
||||
let on_hole = m.mesh.boundary.iter().filter(|b| hole.contains(&b.brep_face)).count();
|
||||
assert!(on_hole > 0);
|
||||
// Radial grading: the first spacing off the hole is smaller than the last at the square.
|
||||
let radii: std::collections::BTreeSet<u64> =
|
||||
m.mesh.nodes.iter().filter(|p| p[2] == 0.0 && p[1].abs() < 1e-12 && p[0] > 0.0).map(|p| p[0].to_bits()).collect();
|
||||
let r: Vec<f64> = radii.into_iter().map(f64::from_bits).collect();
|
||||
assert!(r.len() >= 3, "{r:?}");
|
||||
let first = r[1] - r[0];
|
||||
let last = r[r.len() - 1] - r[r.len() - 2];
|
||||
assert!(first < 0.75 * last, "{r:?}");
|
||||
// Spacing grows by at most the growth rate plus the rounding of the count.
|
||||
assert!(r.windows(3).all(|w| (w[2] - w[1]) <= 1.35 * (w[1] - w[0])), "{r:?}");
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ fn dist<const D: usize>(a: &[f64; D], b: &[f64; D]) -> f64 {
|
||||
}
|
||||
|
||||
/// Normalized cumulative arc length of a side, from 0 to 1.
|
||||
fn params<const D: usize>(side: &[[f64; D]], name: &str) -> Result<Vec<f64>> {
|
||||
pub(crate) fn params<const D: usize>(side: &[[f64; D]], name: &str) -> Result<Vec<f64>> {
|
||||
let mut acc = Vec::with_capacity(side.len());
|
||||
let mut s = 0.0;
|
||||
acc.push(0.0);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -55,6 +55,8 @@ pub struct DisplayNode {
|
||||
pub struct SurfaceTri {
|
||||
pub p: [Vec3; 3],
|
||||
pub normal: Vec3,
|
||||
/// Which surface (body) it belongs to: the order [`DisplayMesh::add_surface`] got them in.
|
||||
pub body: u32,
|
||||
}
|
||||
|
||||
/// Display data for a model and its geometry.
|
||||
@@ -72,6 +74,8 @@ pub struct DisplayMesh {
|
||||
pub surface_tris: Vec<SurfaceTri>,
|
||||
/// Geometry edges: where two faces meet, and open boundaries.
|
||||
pub surface_edges: Vec<[Vec3; 2]>,
|
||||
/// Geometry surfaces (bodies) added.
|
||||
pub bodies: u32,
|
||||
pub bounds: Aabb3,
|
||||
}
|
||||
|
||||
@@ -225,7 +229,7 @@ impl DisplayMesh {
|
||||
for p in [a, b, c] {
|
||||
self.bounds.add(p);
|
||||
}
|
||||
self.surface_tris.push(SurfaceTri { p: [a, b, c], normal });
|
||||
self.surface_tris.push(SurfaceTri { p: [a, b, c], normal, body: self.bodies });
|
||||
let face = tri_face.get(t).copied().unwrap_or(u32::MAX);
|
||||
let [v0, v1, v2] = [vertex(i0), vertex(i1), vertex(i2)];
|
||||
for (u, v) in [(v0, v1), (v1, v2), (v2, v0)] {
|
||||
@@ -241,6 +245,17 @@ impl DisplayMesh {
|
||||
self.surface_edges.push([a, b]);
|
||||
}
|
||||
}
|
||||
self.bodies = self.bodies.saturating_add(1);
|
||||
}
|
||||
|
||||
/// The colour of a geometry triangle: `single` when there is one body, else body `k`'s
|
||||
/// colour from `palette` (cycling), so the bodies of a design, such as the cells of a
|
||||
/// partitioned part, tell apart.
|
||||
pub fn body_color<C: Copy>(&self, tri: &SurfaceTri, single: C, palette: &[C]) -> C {
|
||||
if self.bodies <= 1 || palette.is_empty() {
|
||||
return single;
|
||||
}
|
||||
palette.get(tri.body as usize % palette.len()).copied().unwrap_or(single)
|
||||
}
|
||||
|
||||
/// Where node `label` is drawn, if it is (nodes are kept in label order).
|
||||
@@ -401,6 +416,13 @@ mod tests {
|
||||
welded.add_surface(&soup, &[[0, 1, 2], [3, 4, 5]], &[0, 0]);
|
||||
assert_eq!(welded.surface_edges.len(), 4);
|
||||
let mut hostile = DisplayMesh::default();
|
||||
let mut two = DisplayMesh::default();
|
||||
two.add_surface(&p, &[[0, 1, 2]], &[0]);
|
||||
two.add_surface(&p, &[[0, 2, 3]], &[0]);
|
||||
assert_eq!(two.bodies, 2);
|
||||
let colors: Vec<u8> = two.surface_tris.iter().map(|t| two.body_color(t, 0u8, &[7, 8])).collect();
|
||||
assert_eq!(colors, vec![7, 8]);
|
||||
assert_eq!(one_face.body_color(&one_face.surface_tris[0], 0u8, &[7, 8]), 0);
|
||||
hostile.add_surface(&[[f64::NAN, 0., 0.], [1., 0., 0.], [0., 1., 0.]], &[[0, 1, 2], [u32::MAX, 0, 1]], &[]);
|
||||
assert!(hostile.surface_tris.is_empty() && hostile.surface_edges.is_empty());
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ pub const CATEGORIES: &[(&str, &str)] = &[
|
||||
("blocky", "axis-aligned blocks (polycube-friendly)"),
|
||||
("organic", "free-form, lofted or revolved spline surfaces"),
|
||||
("assembly", "several bodies placed by component transforms"),
|
||||
("partitioned", "split into cells that each mesh as a mapped block (conforming multi-block hex)"),
|
||||
("analytic", "closed-form geometry for solver verification"),
|
||||
];
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use std::time::Instant;
|
||||
use crate::corpus::{self, CATEGORIES, Part};
|
||||
use fieldcraft_geometry::{Design, FaceSurface, Measure, SurfaceMesh};
|
||||
use fieldcraft_mesh::cdt::{CdtOptions, tetrahedralize};
|
||||
use fieldcraft_mesh::hex::{HexOptions, Surface, hex_mesh};
|
||||
use fieldcraft_mesh::hex::{Cell, HexOptions, Surface, hex_mesh, hex_mesh_cells};
|
||||
use fieldcraft_mesh::size::UniformSize;
|
||||
use fieldcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
|
||||
|
||||
@@ -123,7 +123,8 @@ const REASON_CHARS: usize = 240;
|
||||
|
||||
/// Structured all-hex meshing (`fieldcraft_mesh::hex`): the method that meshed each body, the
|
||||
/// element count and the smallest scaled Jacobian of the hex8 mesh and of the hex20 mesh (mid-edge
|
||||
/// nodes on the geometry), or why no structured method fits.
|
||||
/// nodes on the geometry), or why no structured method fits. A `partitioned` part's bodies are
|
||||
/// the cells of one conforming mesh (`blocks`).
|
||||
struct StructuredHex;
|
||||
|
||||
impl Stage for StructuredHex {
|
||||
@@ -144,6 +145,12 @@ impl Stage for StructuredHex {
|
||||
let size = UniformSize(HEX_RELATIVE_SIZE * diagonal);
|
||||
let linear = HexOptions { chordal_tolerance: l.tolerance, ..HexOptions::default() };
|
||||
let quadratic = HexOptions { quadratic: true, ..linear.clone() };
|
||||
if l.part.categories.iter().any(|c| c == "partitioned") {
|
||||
return blocks(l, &size, &linear, &quadratic).map(|mut cells| {
|
||||
cells.push(format!("{:.0}", start.elapsed().as_secs_f64() * 1e3));
|
||||
cells
|
||||
});
|
||||
}
|
||||
let (mut methods, mut elements, mut sj8, mut sj20) = (Vec::new(), 0usize, f64::INFINITY, Ok(f64::INFINITY));
|
||||
for (body, mesh) in l.design.bodies.iter().zip(&l.surfaces) {
|
||||
let surface_options = SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() };
|
||||
@@ -180,6 +187,39 @@ impl Stage for StructuredHex {
|
||||
}
|
||||
}
|
||||
|
||||
/// The bodies of a partitioned part as the cells of one conforming mesh: method, elements and
|
||||
/// the hex8 and hex20 smallest scaled Jacobians.
|
||||
fn blocks(l: &Loaded, size: &UniformSize, linear: &HexOptions, quadratic: &HexOptions) -> Result<Vec<String>, String> {
|
||||
let mut surfaces = Vec::with_capacity(l.surfaces.len());
|
||||
let mut faces: Vec<Vec<Option<Surface>>> = Vec::with_capacity(l.surfaces.len());
|
||||
for (body, mesh) in l.design.bodies.iter().zip(&l.surfaces) {
|
||||
let options = SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() };
|
||||
surfaces.push(BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &options).map_err(|e| format!("{}: {e}", body.name))?);
|
||||
faces.push(body.face_surfaces().map_err(|e| format!("{}: {e}", body.name))?.into_iter().map(|f| f.map(analytic)).collect());
|
||||
}
|
||||
let cells: Vec<Cell> =
|
||||
l.design.bodies.iter().zip(&surfaces).zip(&faces).map(|((b, surface), faces)| Cell { name: &b.name, surface, faces }).collect();
|
||||
let m = match hex_mesh_cells(&cells, size, linear) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
return Ok(vec![
|
||||
format!("no: {}", e.to_string().chars().take(REASON_CHARS).collect::<String>()),
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
]);
|
||||
}
|
||||
};
|
||||
let sj20 =
|
||||
hex_mesh_cells(&cells, size, quadratic).map_or_else(|e| format!("error: {e}"), |q| format!("{:.3}", q.mesh.report.min_scaled_jacobian));
|
||||
Ok(vec![
|
||||
format!("blocks ({} cells, {} shared faces)", cells.len(), m.interfaces),
|
||||
m.mesh.element_count().to_string(),
|
||||
format!("{:.3}", m.mesh.report.min_scaled_jacobian),
|
||||
sj20,
|
||||
])
|
||||
}
|
||||
|
||||
/// The geometry's analytic face surface in the mesher's terms.
|
||||
pub fn analytic(s: FaceSurface) -> Surface {
|
||||
match s {
|
||||
@@ -330,6 +370,17 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The partitioned bracket's 30 cells mesh into one conforming hex mesh.
|
||||
#[test]
|
||||
fn partitioned_bracket_meshes_as_blocks() {
|
||||
let part = corpus::read_recipe(&corpus::corpus_dir().join("filleted-bracket-blocks")).unwrap();
|
||||
let (loaded, _) = load(&part).unwrap();
|
||||
assert_eq!(loaded.design.bodies.len(), 30);
|
||||
let cells = StructuredHex.run(&loaded).unwrap();
|
||||
assert!(cells[0].starts_with("blocks (30 cells"), "{cells:?}");
|
||||
assert!(cells[2].parse::<f64>().unwrap() > 0.5, "{cells:?}");
|
||||
}
|
||||
|
||||
/// The constrained Delaunay tetrahedralization passes the gate on parts that exercise each
|
||||
/// of its stages: an assembly whose rounding needs a few-ulp repair, cavities refilled by
|
||||
/// gift-wrapping, and a holed plate.
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//! The filleted bracket meshed all-hex by partitioning (`tests/corpus/filleted-bracket-blocks`):
|
||||
//! the upright's end clamped, the foot's end pushed down by 1 kN (steel). The conforming hex8 and
|
||||
//! hex20 meshes converge: the tip deflection, the von Mises stress at the middle of the fillet and
|
||||
//! the peak (at the upright's hole) agree between hex20 at 3 mm and 2 mm, and hex8 approaches them.
|
||||
//! For reference, a 48 016-element tet10 mesh of the unpartitioned part (curvature-refined, from
|
||||
//! the tet quality pipeline) gives a tip deflection of 0.5093 mm, 133.2 MPa at the fillet and a
|
||||
//! peak of 291.8 MPa; hex20 at 2 mm gives 0.5089 mm, 137.4 MPa and 297.0 MPa.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use fieldcraft_engine::Session;
|
||||
use fieldcraft_testkit::corpus;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn open(part: &str) -> (Session, std::path::PathBuf) {
|
||||
let parts = corpus::parts().unwrap();
|
||||
let p = parts.iter().find(|p| p.name == part).unwrap();
|
||||
let dir = std::env::temp_dir().join(format!("fieldcraft-bracket-{}-{part}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("part.solidcraft");
|
||||
std::fs::write(&path, corpus::design_bytes(p).unwrap()).unwrap();
|
||||
let mut s = Session::default();
|
||||
s.execute("geometry.open", &json!({"path": path})).unwrap();
|
||||
(s, dir)
|
||||
}
|
||||
|
||||
/// Every (body, face) whose name is `face` (a face split into cells keeps its name in each).
|
||||
fn faces_named(s: &mut Session, face: &str) -> Vec<Value> {
|
||||
let bodies = s.execute("geometry.bodies", &json!({})).unwrap();
|
||||
let mut out = Vec::new();
|
||||
for b in bodies.as_array().unwrap() {
|
||||
for f in b["faces"].as_array().unwrap() {
|
||||
if f["name"] == face {
|
||||
out.push(json!({"body": b["name"], "face": face}));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Clamp the upright's top (y = 50), push the foot's end (x = 60) down by 1 kN; tip deflection,
|
||||
/// von Mises at the middle of the fillet and the largest von Mises.
|
||||
fn analyse(s: &mut Session) -> (f64, f64, f64) {
|
||||
let top = faces_named(s, "F2:side:l5");
|
||||
let tip = faces_named(s, "F2:side:l2");
|
||||
assert!(!top.is_empty() && !tip.is_empty());
|
||||
for (name, geometry) in [
|
||||
(
|
||||
"all",
|
||||
json!(
|
||||
s.execute("geometry.bodies", &json!({})).unwrap().as_array().unwrap().iter().map(|b| json!({"body": b["name"]})).collect::<Vec<_>>()
|
||||
),
|
||||
),
|
||||
("top", json!(top)),
|
||||
("tip", json!(tip)),
|
||||
] {
|
||||
s.execute("model.set.create", &json!({"name": name, "geometry": geometry})).unwrap();
|
||||
}
|
||||
s.execute("model.material.create", &json!({"name": "Steel", "elastic": {"type": "isotropic", "young": 210000.0, "poisson": 0.3}})).unwrap();
|
||||
s.execute("model.section.create", &json!({"name": "solid", "set": "all", "material": "Steel", "kind": {"type": "solid"}})).unwrap();
|
||||
s.execute("model.boundary_condition.create", &json!({"name": "clamp", "type": "fixed", "target": {"set": "top"}})).unwrap();
|
||||
s.execute("model.load.create", &json!({"name": "push", "type": "force", "target": {"set": "tip"}, "vector": [0.0, -1000.0, 0.0]})).unwrap();
|
||||
s.execute("model.step.create", &json!({"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["push"]}))
|
||||
.unwrap();
|
||||
let r = s.execute("solve.static", &json!({})).unwrap();
|
||||
let probe = s.execute("solve.probe", &json!({"step": "Load", "at": [[60.0, 4.0, 20.0], [9.757359312880714, 9.757359312880714, 20.0]]})).unwrap();
|
||||
let nodes = probe["nodes"].as_array().unwrap();
|
||||
let tip = nodes[0]["displacement"][1].as_f64().unwrap();
|
||||
let fillet = nodes[1]["von_mises"].as_f64().unwrap();
|
||||
(tip, fillet, r["max_von_mises"]["value"].as_f64().unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn partitioned_bracket_converges_with_hex8_and_hex20() {
|
||||
let mut results = Vec::new();
|
||||
for (order, size) in [(1, 2.0), (2, 3.0), (2, 2.0)] {
|
||||
let (mut s, dir) = open("filleted-bracket-blocks");
|
||||
let m = s.execute("mesh.hex.blocks", &json!({"size": size, "order": order})).unwrap();
|
||||
assert_eq!(m["cells"], 30, "{m}");
|
||||
assert!(m["quality"]["min_scaled_jacobian"].as_f64().unwrap() > 0.6, "{m}");
|
||||
let r = analyse(&mut s);
|
||||
eprintln!("order {order}, size {size}: {} elements; tip {:.5} mm, fillet {:.2} MPa, peak {:.2} MPa", m["elements"], r.0, r.1, r.2);
|
||||
results.push(r);
|
||||
std::fs::remove_dir_all(dir).ok();
|
||||
}
|
||||
let close = |a: f64, b: f64, rel: f64| (a - b).abs() <= rel * b.abs();
|
||||
let [hex8, coarse, fine] = [results[0], results[1], results[2]];
|
||||
// The tip moves down; hex20 has converged on it, hex8 is within a few percent (stiffer).
|
||||
assert!(fine.0 < 0.0);
|
||||
assert!(close(coarse.0, fine.0, 0.005), "{coarse:?} {fine:?}");
|
||||
assert!(close(hex8.0, fine.0, 0.04) && hex8.0.abs() < fine.0.abs(), "{hex8:?} {fine:?}");
|
||||
// Stresses at the fillet and the peak.
|
||||
assert!(close(coarse.1, fine.1, 0.06), "{coarse:?} {fine:?}");
|
||||
assert!(close(coarse.2, fine.2, 0.02), "{coarse:?} {fine:?}");
|
||||
assert!(close(hex8.1, fine.1, 0.15) && close(hex8.2, fine.2, 0.10), "{hex8:?} {fine:?}");
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -79,7 +79,11 @@ pub fn build(display: &DisplayMesh, style: Style, show: Show, t: &Tokens) -> Sce
|
||||
s.tris = display.tris.iter().map(|d| SceneTri { p: d.p, normal: d.normal, color: rgb(t.element(d.kind)) }).collect();
|
||||
}
|
||||
if !mesh_faces && show.geometry {
|
||||
s.tris.extend(display.surface_tris.iter().map(|d| SceneTri { p: d.p, normal: d.normal, color: rgb(t.geometry) }));
|
||||
s.tris.extend(display.surface_tris.iter().map(|d| SceneTri {
|
||||
p: d.p,
|
||||
normal: d.normal,
|
||||
color: rgb(display.body_color(d, t.geometry, &t.bodies)),
|
||||
}));
|
||||
}
|
||||
}
|
||||
if style != Style::Shaded && show.mesh {
|
||||
|
||||
@@ -66,6 +66,8 @@ pub struct Tokens {
|
||||
pub geometry: Color32,
|
||||
/// Their face boundaries.
|
||||
pub geometry_edge: Color32,
|
||||
/// The bodies of a design with several (the cells of a partitioned part), in turn.
|
||||
pub bodies: [Color32; 8],
|
||||
// ---- results ----
|
||||
/// The contour colour scale, low to high (the same in both themes).
|
||||
pub contour: [Color32; 5],
|
||||
@@ -137,6 +139,16 @@ impl Tokens {
|
||||
node: Color32::from_rgb(0xb4, 0x3c, 0x8c),
|
||||
geometry: Color32::from_rgb(176, 186, 198),
|
||||
geometry_edge: Color32::from_rgb(40, 44, 52),
|
||||
bodies: [
|
||||
Color32::from_rgb(0x8f, 0xb4, 0xd9),
|
||||
Color32::from_rgb(0xe0, 0xa4, 0x6c),
|
||||
Color32::from_rgb(0x8c, 0xc4, 0x8a),
|
||||
Color32::from_rgb(0xd6, 0x8c, 0x96),
|
||||
Color32::from_rgb(0xb0, 0x9c, 0xd8),
|
||||
Color32::from_rgb(0xd8, 0xc8, 0x70),
|
||||
Color32::from_rgb(0x7c, 0xc4, 0xc0),
|
||||
Color32::from_rgb(0xc0, 0xa0, 0x84),
|
||||
],
|
||||
contour: CONTOUR,
|
||||
no_result: Color32::from_rgb(0x9a, 0xa0, 0xaa),
|
||||
glyph_constraint: Color32::from_rgb(0x00, 0x8a, 0xa8),
|
||||
@@ -188,6 +200,16 @@ impl Tokens {
|
||||
t.node = Color32::from_rgb(0xf2, 0xe6, 0x6e);
|
||||
t.geometry = Color32::from_rgb(152, 156, 162);
|
||||
t.geometry_edge = Color32::from_rgb(16, 16, 18);
|
||||
t.bodies = [
|
||||
Color32::from_rgb(0x6c, 0x92, 0xb8),
|
||||
Color32::from_rgb(0xbc, 0x84, 0x50),
|
||||
Color32::from_rgb(0x6c, 0xa2, 0x6a),
|
||||
Color32::from_rgb(0xb4, 0x6e, 0x78),
|
||||
Color32::from_rgb(0x90, 0x7e, 0xb6),
|
||||
Color32::from_rgb(0xb6, 0xa6, 0x54),
|
||||
Color32::from_rgb(0x5c, 0xa2, 0x9e),
|
||||
Color32::from_rgb(0xa0, 0x82, 0x66),
|
||||
];
|
||||
t.no_result = Color32::from_rgb(0x70, 0x76, 0x80);
|
||||
t.glyph_constraint = Color32::from_rgb(0x3c, 0xdc, 0xf0);
|
||||
t.glyph_load = Color32::from_rgb(0xff, 0x5a, 0x4c);
|
||||
|
||||
+5
-4
@@ -6,7 +6,7 @@ Every entry is a toolbar button. **Ready** entries run their command; **coming s
|
||||
shown greyed with the roadmap milestone that brings them (ROADMAP.md). Scripts, the command line,
|
||||
the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
||||
|
||||
54 ready, 49 coming soon.
|
||||
55 ready, 49 coming soon.
|
||||
|
||||
## Geometry
|
||||
|
||||
@@ -35,6 +35,7 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
||||
| Structured Hex | Quad Faces — All-quad meshes of faces on shared edge nodes | coming soon (M5) | |
|
||||
| Structured Hex | Mapped Hex — Mapped hex mesh of a six-sided block (curved faces allowed) | ready | `mesh.hex.mapped`, `mesh.hex.generate` |
|
||||
| Structured Hex | Sweep Hex — Hex mesh swept along extrusions (several levels), around revolutions, or from a source face to a target | ready | `mesh.hex.sweep`, `mesh.hex.generate` |
|
||||
| Structured Hex | Block Hex — One conforming hex mesh of a part split into six-sided blocks: shared faces meshed once | ready | `mesh.hex.blocks` |
|
||||
| Structured Hex | Decompose — Find sweepable and mappable regions and propose the splits | coming soon (M5) | |
|
||||
| Structured Hex | Transition — Conforming tet–hex interfaces with pyramids | coming soon (M5) | |
|
||||
| Automatic Hex | All-Hex — Octree dual all-hex mesh of any part | coming soon (M6) | |
|
||||
@@ -56,7 +57,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 +79,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 +123,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).
|
||||
+27
-24
@@ -10,28 +10,29 @@ clock on the machine that ran it; every other column is deterministic.
|
||||
|
||||
| part | categories | bodies | faces | triangles | tol. mm | build ms | enclosed mm³ | SolidCraft mm³ | rel. diff | exact mm³ | boundary | genus | feature edges | check ms | CDT tets | Steiner | cavities | tet gate | CDT ms | hex | hex elements | hex8 min SJ | hex20 min SJ | hex ms |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| bent-tube | sweepable, thin-wall, genus | 1 | 18 | 4296 | 0.121 | 2726 | 4213.1 | 4159.4 | 1.3e-2 | 4159.8 | ok, 2148 vertices | 1 | 268 | 19.8 | 15550 | 2338 | 0 | pass | 4070 | sweep | 192 | 0.859 | 0.997 | 535 |
|
||||
| bolted-assembly | assembly, blocky | 3 | 22 | 284 | 0.117 | 1141 | 33004.7 | 32999.0 | 1.7e-4 | 32998.9 | ok, 144 vertices | 0, 1, 1 | 104 | 1.6 | 690 | 181 | 0 | pass | 211 | extrude | 357 | 0.398 | 0.222 | 139 |
|
||||
| cantilever-i-beam | sweepable, thin-wall | 1 | 14 | 44 | 0.208 | 483 | 61600.0 | 61600.0 | 0.0e0 | 61600.0 | ok, 24 vertices | 0 | 36 | 0.2 | 90 | 24 | 0 | pass | 5 | extrude | 336 | 0.707 | 0.707 | 47 |
|
||||
| cross-cylinders | boolean, sharp | 1 | 10 | 1395 | 0.087 | 59348 | 32197.4 | 32357.8 | -5.0e-3 | 32365.8 | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | |
|
||||
| filleted-bracket | fillet, genus, sweepable | 1 | 13 | 540 | 0.088 | 3706 | 32162.3 | 32145.0 | 5.4e-4 | 32144.8 | ok, 268 vertices | 2 | 135 | 2.1 | 5582 | 1613 | 0 | pass | 1769 | no: Bracket: not supported yet: no structured hex method fits this body (extrude: cylindrical face 8 is not parallel to the direction; sweep: from face 1: linking face 6 has 2 boundary loops (periodic or holed linking faces are not swept); from face 6: | | | | 10 |
|
||||
| i-beam | sweepable, thin-wall | 1 | 14 | 44 | 0.208 | 280 | 61600.0 | 61600.0 | 0.0e0 | 61600.0 | ok, 24 vertices | 0 | 36 | 0.3 | 94 | 24 | 0 | pass | 7 | extrude | 336 | 0.707 | 0.707 | 52 |
|
||||
| loft-transition | organic | 1 | 6 | 292 | 0.075 | 397 | 56390.2 | 56450.6 | -1.1e-3 | – | ok, 148 vertices | 0 | 40 | 1.0 | 1329 | 256 | 1 | pass | 172 | sweep | 2873 | 0.189 | 0.189 | 2056 |
|
||||
| perforated-plate | genus, thin-wall, sweepable | 1 | 54 | 1644 | 0.144 | 2864 | 26822.2 | 26764.6 | 2.2e-3 | 26764.2 | ok, 776 vertices | 24 | 828 | 10.6 | 33611 | 9798 | 0 | pass | 6513 | extrude | 467 | 0.304 | 0.033 | 480 |
|
||||
| pinhole-block | small-feature, genus | 1 | 38 | 4044 | 0.073 | 2053 | 49919.4 | 49909.6 | 2.0e-4 | 49909.5 | ok, 1992 vertices | 16 | 1548 | 41.8 | 96411 | 30221 | 0 | pass | 28180 | extrude | 5236 | 0.408 | 0.392 | 3838 |
|
||||
| rounded-box | fillet | 1 | 26 | 2540 | 0.054 | 1069 | 23325.7 | 23340.5 | -6.3e-4 | 23340.8 | ok, 1272 vertices | 0 | 216 | 11.5 | 11653 | 1914 | 0 | pass | 2265 | no: Box: not supported yet: no structured hex method fits this body (extrude: cylindrical face 8 is not parallel to the direction; sweep: from face 0: 12 faces are not on the sweep path; from face 5: 12 faces are not on the sweep path; revolve: plan | | | | 67 |
|
||||
| shallow-groove | near-tangent, boolean | 1 | 8 | 62 | 0.075 | 218 | 47795.2 | 47782.3 | 2.7e-4 | 47781.5 | ok, 33 vertices | 0 | 24 | 0.2 | 267 | 87 | 0 | pass | 24 | extrude | 1960 | 0.991 | 0.986 | 1078 |
|
||||
| sharp-wedge | sharp, thin-wall, sweepable | 1 | 5 | 8 | 0.083 | 72 | 3200.0 | 3200.0 | 0.0e0 | 3200.0 | ok, 6 vertices | 0 | 9 | 0.0 | 11 | 3 | 0 | pass | 1 | extrude | 294 | 0.050 | 0.050 | 87 |
|
||||
| sphere-on-plate | near-tangent, boolean | 1 | 12 | 2826 | 0.098 | 3775 | 69353.3 | 69509.0 | -2.2e-3 | 69509.7 | ok, 1415 vertices | 0 | 124 | 8.3 | 4898 | 309 | 9 | pass | 1265 | no: Plate: not supported yet: no structured hex method fits this body (extrude: face 6 is curved across the direction; sweep: from face 0: 6 faces are not on the sweep path; from face 5: linking face 11 has 2 boundary loops (periodic or holed linking | | | | 75 |
|
||||
| spline-vase | organic | 1 | 11 | 3890 | 0.106 | 1097 | 94891.2 | 94948.5 | -6.0e-4 | – | ok, 1947 vertices | 0 | 243 | 23.5 | 12461 | 1512 | 0 | pass | 2487 | sweep | 4554 | 0.649 | 0.649 | 3030 |
|
||||
| stepped-block | blocky, sharp | 1 | 14 | 96 | 0.078 | 438 | 47000.0 | 47000.0 | 0.0e0 | 47000.0 | ok, 48 vertices | 1 | 60 | 0.6 | 223 | 41 | 0 | pass | 11 | extrude | 1416 | 0.999 | 0.999 | 906 |
|
||||
| thin-ribs | thin-wall, blocky | 1 | 26 | 92 | 0.119 | 1085 | 32000.0 | 32000.0 | -4.5e-16 | 32000.0 | ok, 48 vertices | 0 | 72 | 0.7 | 171 | 33 | 0 | pass | 10 | extrude | 777 | 0.844 | 0.844 | 236 |
|
||||
| thin-wall-enclosure | thin-wall, blocky | 1 | 11 | 36 | 0.108 | 189 | 15564.0 | 15564.0 | 0.0e0 | 15564.0 | ok, 20 vertices | 0 | 28 | 0.2 | 191 | 47 | 2 | pass | 8 | extrude | 894 | 0.297 | 0.297 | 349 |
|
||||
| analytic-sphere | analytic | 1 | 6 | 2784 | 0.069 | 1340 | 33353.4 | 33507.9 | -4.6e-3 | 33510.3 | ok, 1394 vertices | 0 | 96 | 8.9 | 4211 | 58 | 0 | pass | 1430 | no: Sphere: not supported yet: no structured hex method fits this body (extrude: no direction to try; sweep: no planar face to sweep from; revolve: no axis to try; mapped: meshing failed: meshing failed: mesh validity gate: 8 inverted or degenerate ele | | | | 159 |
|
||||
| analytic-torus | analytic, genus | 1 | 6 | 7230 | 0.109 | 1725 | 37644.1 | 37893.7 | -6.6e-3 | 37899.3 | ok, 3615 vertices | 1 | 180 | 31.7 | 19790 | 466 | 0 | pass | 4946 | revolve | 1134 | 0.706 | 0.706 | 1155 |
|
||||
| analytic-cylinder | analytic, sweepable | 1 | 4 | 1004 | 0.049 | 368 | 12533.1 | 12564.7 | -2.5e-3 | 12566.4 | ok, 504 vertices | 0 | 106 | 9.3 | 7437 | 1568 | 0 | pass | 1797 | extrude | 3200 | 0.707 | 0.707 | 2316 |
|
||||
| analytic-pipe | analytic, genus, sweepable | 1 | 6 | 1448 | 0.104 | 506 | 94150.9 | 94238.0 | -9.3e-4 | 94247.8 | ok, 724 vertices | 1 | 220 | 9.5 | 6238 | 1340 | 0 | pass | 1734 | extrude | 1288 | 0.998 | 0.998 | 1059 |
|
||||
| analytic-plate-hole | analytic, genus, sweepable | 1 | 8 | 112 | 0.224 | 524 | 98449.1 | 98429.5 | 2.0e-4 | 98429.2 | ok, 56 vertices | 1 | 62 | 0.6 | 1868 | 543 | 0 | pass | 78 | extrude | 321 | 0.320 | 0.142 | 248 |
|
||||
| bent-tube | sweepable, thin-wall, genus | 1 | 18 | 4296 | 0.121 | 428 | 4213.1 | 4159.4 | 1.3e-2 | 4159.8 | ok, 2148 vertices | 1 | 268 | 11.8 | 15550 | 2338 | 0 | pass | 598 | sweep | 192 | 0.859 | 0.997 | 62 |
|
||||
| bolted-assembly | assembly, blocky | 3 | 22 | 284 | 0.117 | 53 | 33004.7 | 32999.0 | 1.7e-4 | 32998.9 | ok, 144 vertices | 0, 1, 1 | 104 | 0.7 | 690 | 181 | 0 | pass | 33 | extrude | 357 | 0.398 | 0.222 | 40 |
|
||||
| cantilever-i-beam | sweepable, thin-wall | 1 | 14 | 44 | 0.208 | 4 | 61600.0 | 61600.0 | 0.0e0 | 61600.0 | ok, 24 vertices | 0 | 36 | 0.1 | 90 | 24 | 0 | pass | 3 | extrude | 336 | 0.707 | 0.707 | 33 |
|
||||
| cross-cylinders | boolean, sharp | 1 | 10 | 1395 | 0.087 | 10640 | 32197.4 | 32357.8 | -5.0e-3 | 32365.8 | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | |
|
||||
| filleted-bracket | fillet, genus, sweepable | 1 | 13 | 540 | 0.088 | 494 | 32162.3 | 32145.0 | 5.4e-4 | 32144.8 | ok, 268 vertices | 2 | 135 | 1.7 | 5582 | 1613 | 0 | pass | 233 | no: Bracket: not supported yet: no structured hex method fits this body (extrude: cylindrical face 8 is not parallel to the direction; sweep: from face 1: linking face 6 has 2 boundary loops (periodic or holed linking faces are not swept); from face 6: | | | | 5 |
|
||||
| filleted-bracket-blocks | fillet, genus, partitioned | 30 | 190 | 712 | 0.088 | 1057 | 32169.8 | 32145.1 | 7.7e-4 | 32144.8 | ok, 416 vertices | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 | 474 | 1.7 | 1423 | 349 | 0 | pass | 64 | blocks (30 cells, 45 shared faces) | 4986 | 0.705 | 0.705 | 1440 |
|
||||
| i-beam | sweepable, thin-wall | 1 | 14 | 44 | 0.208 | 234 | 61600.0 | 61600.0 | 0.0e0 | 61600.0 | ok, 24 vertices | 0 | 36 | 0.3 | 94 | 24 | 0 | pass | 8 | extrude | 336 | 0.707 | 0.707 | 74 |
|
||||
| loft-transition | organic | 1 | 6 | 292 | 0.075 | 493 | 56390.2 | 56450.6 | -1.1e-3 | – | ok, 148 vertices | 0 | 40 | 1.5 | 1329 | 256 | 1 | pass | 80 | sweep | 2873 | 0.189 | 0.189 | 892 |
|
||||
| perforated-plate | genus, thin-wall, sweepable | 1 | 54 | 1644 | 0.144 | 2861 | 26822.2 | 26764.6 | 2.2e-3 | 26764.2 | ok, 776 vertices | 24 | 828 | 10.6 | 33611 | 9798 | 0 | pass | 7636 | extrude | 467 | 0.304 | 0.033 | 555 |
|
||||
| pinhole-block | small-feature, genus | 1 | 38 | 4044 | 0.073 | 4821 | 49919.4 | 49909.6 | 2.0e-4 | 49909.5 | ok, 1992 vertices | 16 | 1548 | 29.3 | 96411 | 30221 | 0 | pass | 23460 | extrude | 5236 | 0.408 | 0.392 | 1853 |
|
||||
| rounded-box | fillet | 1 | 26 | 2540 | 0.054 | 1822 | 23325.7 | 23340.5 | -6.3e-4 | 23340.8 | ok, 1272 vertices | 0 | 216 | 15.1 | 11687 | 1921 | 0 | pass | 1081 | no: Box: not supported yet: no structured hex method fits this body (extrude: cylindrical face 6 is not parallel to the direction; sweep: from face 0: 12 faces are not on the sweep path; from face 5: 12 faces are not on the sweep path; revolve: plan | | | | 37 |
|
||||
| shallow-groove | near-tangent, boolean | 1 | 8 | 62 | 0.075 | 266 | 47795.2 | 47782.3 | 2.7e-4 | 47781.5 | ok, 33 vertices | 0 | 24 | 0.4 | 267 | 87 | 0 | pass | 27 | extrude | 1960 | 0.991 | 0.986 | 500 |
|
||||
| sharp-wedge | sharp, thin-wall, sweepable | 1 | 5 | 8 | 0.083 | 143 | 3200.0 | 3200.0 | 0.0e0 | 3200.0 | ok, 6 vertices | 0 | 9 | 0.0 | 11 | 3 | 0 | pass | 1 | extrude | 294 | 0.050 | 0.050 | 67 |
|
||||
| sphere-on-plate | near-tangent, boolean | 1 | 12 | 2826 | 0.098 | 3307 | 69353.3 | 69509.0 | -2.2e-3 | 69509.7 | ok, 1415 vertices | 0 | 124 | 15.4 | 4898 | 309 | 9 | pass | 736 | no: Plate: not supported yet: no structured hex method fits this body (extrude: face 6 is curved across the direction; sweep: from face 0: 6 faces are not on the sweep path; from face 5: linking face 11 has 2 boundary loops (periodic or holed linking | | | | 55 |
|
||||
| spline-vase | organic | 1 | 11 | 3890 | 0.106 | 1175 | 94891.2 | 94948.5 | -6.0e-4 | – | ok, 1947 vertices | 0 | 243 | 20.5 | 12461 | 1512 | 0 | pass | 2093 | sweep | 4554 | 0.649 | 0.649 | 846 |
|
||||
| stepped-block | blocky, sharp | 1 | 14 | 96 | 0.078 | 32 | 47000.0 | 47000.0 | 0.0e0 | 47000.0 | ok, 48 vertices | 1 | 60 | 0.5 | 223 | 41 | 0 | pass | 8 | extrude | 1416 | 0.999 | 0.999 | 158 |
|
||||
| thin-ribs | thin-wall, blocky | 1 | 26 | 92 | 0.119 | 68 | 32000.0 | 32000.0 | -4.5e-16 | 32000.0 | ok, 48 vertices | 0 | 72 | 0.6 | 171 | 33 | 0 | pass | 8 | extrude | 777 | 0.844 | 0.844 | 86 |
|
||||
| thin-wall-enclosure | thin-wall, blocky | 1 | 11 | 36 | 0.108 | 9 | 15564.0 | 15564.0 | 0.0e0 | 15564.0 | ok, 20 vertices | 0 | 28 | 0.1 | 191 | 47 | 2 | pass | 6 | extrude | 894 | 0.297 | 0.297 | 100 |
|
||||
| analytic-sphere | analytic | 1 | 6 | 2784 | 0.069 | 284 | 33353.4 | 33507.9 | -4.6e-3 | 33510.3 | ok, 1394 vertices | 0 | 96 | 7.0 | 4211 | 58 | 0 | pass | 358 | no: Sphere: not supported yet: no structured hex method fits this body (extrude: no direction to try; sweep: no planar face to sweep from; revolve: no axis to try; mapped: meshing failed: meshing failed: mesh validity gate: 8 inverted or degenerate ele | | | | 35 |
|
||||
| analytic-torus | analytic, genus | 1 | 6 | 7230 | 0.109 | 627 | 37644.1 | 37893.7 | -6.6e-3 | 37899.3 | ok, 3615 vertices | 1 | 180 | 30.0 | 19790 | 466 | 0 | pass | 1091 | revolve | 1134 | 0.706 | 0.706 | 210 |
|
||||
| analytic-cylinder | analytic, sweepable | 1 | 4 | 1004 | 0.049 | 32 | 12533.1 | 12564.7 | -2.5e-3 | 12566.4 | ok, 504 vertices | 0 | 106 | 3.4 | 7437 | 1568 | 0 | pass | 344 | extrude | 3200 | 0.707 | 0.707 | 409 |
|
||||
| analytic-pipe | analytic, genus, sweepable | 1 | 6 | 1448 | 0.104 | 51 | 94150.9 | 94238.0 | -9.3e-4 | 94247.8 | ok, 724 vertices | 1 | 220 | 4.5 | 6238 | 1340 | 0 | pass | 327 | extrude | 1288 | 0.998 | 0.998 | 176 |
|
||||
| analytic-plate-hole | analytic, genus, sweepable | 1 | 8 | 112 | 0.224 | 13 | 98449.1 | 98429.5 | 2.0e-4 | 98429.2 | ok, 56 vertices | 1 | 62 | 0.5 | 1868 | 543 | 0 | pass | 69 | extrude | 321 | 0.320 | 0.142 | 49 |
|
||||
|
||||
## Categories
|
||||
|
||||
@@ -39,8 +40,8 @@ clock on the machine that ran it; every other column is deterministic.
|
||||
|---|---|---|
|
||||
| thin-wall | walls thin against the part's size | 7 |
|
||||
| small-feature | features much smaller than the part | 1 |
|
||||
| fillet | blends between faces | 2 |
|
||||
| genus | through holes (genus > 0) | 7 |
|
||||
| fillet | blends between faces | 3 |
|
||||
| genus | through holes (genus > 0) | 8 |
|
||||
| sharp | sharp edges and acute dihedral angles | 3 |
|
||||
| near-tangent | faces meeting at a shallow angle | 2 |
|
||||
| boolean | intersection curves from booleans | 3 |
|
||||
@@ -48,6 +49,7 @@ clock on the machine that ran it; every other column is deterministic.
|
||||
| blocky | axis-aligned blocks (polycube-friendly) | 4 |
|
||||
| organic | free-form, lofted or revolved spline surfaces | 2 |
|
||||
| assembly | several bodies placed by component transforms | 1 |
|
||||
| partitioned | split into cells that each mesh as a mapped block (conforming multi-block hex) | 1 |
|
||||
| analytic | closed-form geometry for solver verification | 5 |
|
||||
|
||||
## Parts
|
||||
@@ -57,6 +59,7 @@ clock on the machine that ran it; every other column is deterministic.
|
||||
- `cantilever-i-beam`: The 200 mm I-beam of `i-beam` lying along X with its web upright (Z up): a sweep along a horizontal axis, and the cantilever of the README demo.
|
||||
- `cross-cylinders`: Two orthogonal R10 cylinders joined: equal radii, so the intersection curves cross at two singular points.
|
||||
- `filleted-bracket`: An 8 mm L-bracket with an R6 inside fillet and a hole through each leg: a blend beside holes.
|
||||
- `filleted-bracket-blocks`: The filleted bracket split into 30 cells with SolidCraft's Split Body (three slabs across its width, the corner halved along its diagonal, an O-grid of four cells round each hole) so that every cell is a mapped block: conforming multi-block hex meshing.
|
||||
- `i-beam`: A 200 mm I-beam with 3 mm flanges and a 2 mm web: a long sweepable prism with thin walls.
|
||||
- `loft-transition`: A 40 mm square lofted to an R15 circle 50 mm above: a ruled free-form surface between unlike sections.
|
||||
- `perforated-plate`: A 3 mm plate perforated by a 6 x 4 grid of 6 mm holes: genus 24 in a thin sheet.
|
||||
|
||||
@@ -880,7 +880,7 @@ input points), Steiner points kept half the local spacing from the boundary.
|
||||
|
||||
### B8. Mapped and swept hexahedral meshing (implemented in `fieldcraft-mesh::hex`)
|
||||
|
||||
`mesh.hex.generate` (and `mesh.hex.sweep`, `mesh.hex.mapped`). Every method below is implemented from
|
||||
`mesh.hex.generate` (and `mesh.hex.sweep`, `mesh.hex.mapped`, `mesh.hex.blocks`). Every method below is implemented from
|
||||
its paper; no code was read. DOIs marked "checked" were confirmed on 2026-10-13 through a repository
|
||||
record or the publisher; the others are cited by venue only.
|
||||
|
||||
@@ -932,7 +932,17 @@ record or the publisher; the others are cited by venue only.
|
||||
Applied Mathematics and Computation 10–11:171–233, 1982.
|
||||
- **Use.** Interior nodes of a logical cube from its six face grids (`hex::mapped`).
|
||||
|
||||
**B8.8 Support.** Generalized winding number for the inside test (A. Jacobson, L. Kavan, O. Sorkine-Hornung,
|
||||
**B8.8 Multi-block meshing of partitioned parts.** **Verdict: BUILD ON (conforming cells).**
|
||||
- **Citations.** J. F. Thompson, B. K. Soni, N. P. Weatherill (eds.). *Handbook of Grid Generation.* CRC Press,
|
||||
1999, ch. 3 (transfinite and algebraic grids) and ch. 13 (multiblock grids). A. Sheffer, M. Bercovier,
|
||||
T. Blacker, J. Clements. *Virtual topology operators for meshing.* IJCGA 10(3):309–331, 2000.
|
||||
- **Use.** `hex::cells` (`mesh.hex.blocks`): cells that meet face to face, each a logical cube after smoothly
|
||||
meeting faces are joined into composite faces (virtual topology); vertices, edges and faces shared by
|
||||
identity; interval assignment over all cells (B8.5); each edge divided once, each face's transfinite grid
|
||||
made once and used by both cells; edges that leave a curved face graded from the spacing along it with a
|
||||
growth of 1.2 per element; interiors by B8.7. The merged mesh is checked for free internal faces.
|
||||
|
||||
**B8.9 Support.** Generalized winding number for the inside test (A. Jacobson, L. Kavan, O. Sorkine-Hornung,
|
||||
*Robust inside-outside segmentation using generalized winding numbers*, ACM TOG 32(4):33, 2013; solid angle
|
||||
of a triangle: Van Oosterom and Strackee 1983); closest point on a triangle (C. Ericson, *Real-Time
|
||||
Collision Detection*, 2005, §5.1.5).
|
||||
|
||||
+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 @@
|
||||
The filleted bracket split into 30 cells with SolidCraft's Split Body (three slabs across its width, the corner halved along its diagonal, an O-grid of four cells round each hole) so that every cell is a mapped block: conforming multi-block hex meshing.
|
||||
@@ -0,0 +1,39 @@
|
||||
{"corpus": {"categories": ["fillet", "genus", "partitioned"], "volume_mm3": 32144.778925},
|
||||
"commands": [
|
||||
{"command": "sketch.create", "params": {"plane": "XY", "name": "Profile"}},
|
||||
{"command": "sketch.line", "params": {"points": [[0, 0], [60, 0], [60, 8], [8, 8], [8, 50], [0, 50]], "closed": true}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"distance": 40, "body_name": "Bracket"}},
|
||||
{"command": "solid.fillet", "params": {"edges": [[8, 8, 20]], "radius": 6}},
|
||||
{"command": "solid.hole", "params": {"position": [40, 8, 20], "diameter": 8}},
|
||||
{"command": "solid.hole", "params": {"position": [8, 32, 20], "diameter": 8}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket", "plane": {"origin": [0, 0, 30], "normal": [0, 0, 1]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1)", "plane": {"origin": [0, 0, 10], "normal": [0, 0, 1]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)", "plane": {"origin": [16, 0, 0], "normal": [1, 0, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1) (1)", "plane": {"origin": [0, 16, 0], "normal": [0, 1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1) (1) (1)", "plane": {"origin": [0, 0, 0], "normal": [1, -1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)", "plane": {"origin": [30, 0, 0], "normal": [1, 0, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)", "plane": {"origin": [50, 0, 0], "normal": [1, 0, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1) (1)", "plane": {"origin": [0, 22, 0], "normal": [0, 1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1) (1)", "plane": {"origin": [0, 42, 0], "normal": [0, 1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1)", "plane": {"origin": [16, 0, 0], "normal": [1, 0, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)'", "plane": {"origin": [0, 16, 0], "normal": [0, 1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)' (1)", "plane": {"origin": [0, 0, 0], "normal": [1, -1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1)", "plane": {"origin": [30, 0, 0], "normal": [1, 0, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1)", "plane": {"origin": [50, 0, 0], "normal": [1, 0, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)'", "plane": {"origin": [0, 22, 0], "normal": [0, 1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)'", "plane": {"origin": [0, 42, 0], "normal": [0, 1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket", "plane": {"origin": [16, 0, 0], "normal": [1, 0, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1)'", "plane": {"origin": [0, 16, 0], "normal": [0, 1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1)' (1)", "plane": {"origin": [0, 0, 0], "normal": [1, -1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket", "plane": {"origin": [30, 0, 0], "normal": [1, 0, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket", "plane": {"origin": [50, 0, 0], "normal": [1, 0, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1)'", "plane": {"origin": [0, 22, 0], "normal": [0, 1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1)'", "plane": {"origin": [0, 42, 0], "normal": [0, 1, 0]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)'''", "plane": {"origin": [40, 0, 20], "normal": [1, 0, 1]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)'''", "plane": {"origin": [40, 0, 20], "normal": [1, 0, -1]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)''' (1)", "plane": {"origin": [40, 0, 20], "normal": [1, 0, -1]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)' (1)''", "plane": {"origin": [0, 32, 20], "normal": [0, 1, 1]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)' (1)''", "plane": {"origin": [0, 32, 20], "normal": [0, 1, -1]}}},
|
||||
{"command": "solid.split_body", "params": {"body": "Bracket (1) (1)' (1)'' (1)", "plane": {"origin": [0, 32, 20], "normal": [0, 1, -1]}}}
|
||||
]}
|
||||
@@ -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