forked from 1b-agent/fieldcraft-upstream
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
741a803785 | ||
|
|
d586d84859 | ||
|
|
3f9e9e30d8 | ||
|
|
53adab54c2 | ||
|
|
7a69fe5e2f | ||
|
|
17b2c837f2 | ||
|
|
569b69c061 | ||
|
|
72940fe78f | ||
|
|
3ecb7c6883 | ||
|
|
98d2f55407 | ||
|
|
7f5399c7e9 | ||
|
|
b6ce7aee69 |
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 |
|
||||
|
||||
+5
-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,9 +41,9 @@ pressure) is a degree of freedom of the existing DOF framework, not a separate s
|
||||
| M12 CFD: lattice Boltzmann | D3Q19/D3Q27 on grids voxelised from the face-tagged surface; TRT, regularised and cumulant collision; interpolated walls, inlets and outlets; LES (Smagorinsky, WALE) with wall models; thermal LBM and conjugate heat transfer with the structure's thermal solver; non-Newtonian viscosity (power-law, Carreau–Yasuda, Cross, Herschel–Bulkley, Bingham) and temperature-dependent viscosity; porous media (Brinkman, Darcy–Forchheimer); multiphase and free surface (M13); validation by the lid-driven cavity, backward-facing step, cylinder wake (Strouhal) and turbulent channel; octree refinement shared with the hex mesher; wgpu backend (deterministic per device, checked against the CPU) (CPU core: done — `fieldcraft-lbm` with D3Q19 TRT and Guo forcing, halfway bounce-back and moving walls, slip and periodic faces, Zou–He velocity and pressure faces, exact voxelisation of obstacle bodies, momentum-exchange forces per face; fluid materials, fluid domains and fluid steps in the model; `model.fluid_domain.*`, `solve.fluid`; verified by Poiseuille channel and pipe, Couette, Taylor–Green decay, sphere-array Stokes drag and mass conservation; Schäfer–Turek 2D-1 as a benchmark) | started |
|
||||
| M13 Fluid–structure interaction | Immersed-boundary coupling of the lattice Boltzmann fluid with the explicit or implicit structure; interface force transfer; sub-cycling; Aitken then interface quasi-Newton coupling; free-surface flow | planned |
|
||||
| M14 Blast and impact fluids | EOS-driven fluids in the explicit solver: ALE multi-material or SPH, with fluid–structure contact | planned |
|
||||
| M15 Nonlinear implicit core | Newton–Raphson with line search, automatic increments and cutbacks, convergence on force, displacement and energy; geometric nonlinearity (total and updated Lagrangian, large rotations for shells and beams, follower pressure); every `fem` material behind one interface with its consistent tangent (plasticity, damage and thermal strain from the explicit solver reused implicitly); arc-length (Riks) for snap-through; linear buckling (eigen) with preload stress stiffening, and imperfections from buckling modes; nonlinear stabilisation; restart. Verified by the elastica, Lee's frame, the snap-through arch, the hinged cylindrical shell and plastic collapse loads (solids core: done — `nonlinear_static` steps with automatic increments and cutbacks, Newton–Raphson with the consistent tangent, force, displacement and energy convergence and an optional line search; a material point interface (Kirchhoff stress and spatial tangent from F, with history) with St Venant–Kirchhoff and compressible neo-Hookean; total Lagrangian tet4, tet10, wedge6, hex8, hex20 (full and reduced), hybrid hex8/hex20/tet10 by the three-field mixed form, plane strain and axisymmetric elements; follower pressure with its symmetrised load stiffness; Cauchy stress and logarithmic strain, increment history and frames, `solve.nonlinear`; Abaqus and LS-DYNA decks; verified by the patch and objectivity tests, uniaxial stretch, the elastica and tube inflation by a follower pressure (finite-strain Cook's membrane pending its transcribed reference). Next: J2 plasticity and Mooney–Rivlin on the interface, thermal strain, incompatible modes (EAS), shells and beams, arc-length) | started |
|
||||
| M15 Nonlinear implicit core | Newton–Raphson with line search, automatic increments and cutbacks, convergence on force, displacement and energy; geometric nonlinearity (total and updated Lagrangian, large rotations for shells and beams, follower pressure); every `fem` material behind one interface with its consistent tangent (plasticity, damage and thermal strain from the explicit solver reused implicitly); arc-length (Riks) for snap-through; linear buckling (eigen) with preload stress stiffening, and imperfections from buckling modes; nonlinear stabilisation; restart. Verified by the elastica, Lee's frame, the snap-through arch, the hinged cylindrical shell and plastic collapse loads (solids core: done — `nonlinear_static` steps with automatic increments and cutbacks, Newton–Raphson with the consistent tangent, force, displacement and energy convergence and an optional line search; a material point interface (Kirchhoff stress and spatial tangent from F, with history) with St Venant–Kirchhoff and compressible neo-Hookean; total Lagrangian tet4, tet10, wedge6, hex8, hex20 (full and reduced), hybrid hex8/hex20/tet10 by the three-field mixed form, plane strain and axisymmetric elements; follower pressure with its symmetrised load stiffness; Cauchy stress and logarithmic strain, increment history and frames, `solve.nonlinear`; Abaqus and LS-DYNA decks; verified by the patch and objectivity tests, uniaxial stretch, the elastica and tube inflation by a follower pressure (finite-strain Cook's membrane pending its transcribed reference; J2 plasticity on the interface: Simo's exponential return map at finite strain and the radial return at small strain, both with consistent tangents and the return shared with explicit dynamics, bilinear isotropic and kinematic (small strain), tabulated and Johnson–Cook (rate-independent) hardening, hybrid elements included, load paths for cyclic loading, a tangent predictor for path-dependent materials, verified by uniaxial tension to large strain, Hill's thick cylinder (plastic zone and collapse pressure), a cantilever's collapse and the Bauschinger effect, with quadratic Newton convergence; Mooney–Rivlin and Yeoh options (decoupled, hybrid elements), verified in Treloar's three modes and by a stretched tube's inflation; thermal strain by the multiplicative split at finite strain and the additive one at small strain, with temperatures from temperature loads or the step's own conduction, verified by free and constrained expansion and a heated bar held past yield. Next: incompatible modes (EAS), kinematic hardening at finite strain, shells and beams, linear buckling, arc-length with an indefinite factorization) | started |
|
||||
| M16 Structural dynamics | Implicit transient (Newmark, HHT-α, generalised-α); modal superposition transient; steady-state harmonic (direct and modal), with Rayleigh, modal, structural and material damping; random vibration from PSDs and response spectrum (SRSS, CQC); complex eigenvalues with damping; rotating machinery (Campbell diagrams, gyroscopic terms); substructures (Craig–Bampton) | planned |
|
||||
| M17 Constitutive library and material library | Elastic: transversely isotropic, fully anisotropic (21 constants), temperature-dependent. Elastomers: hyperelastic neo-Hookean, Mooney–Rivlin, Yeoh, Ogden, Arruda–Boyce, Gent, Marlow, with Mullins softening and finite-strain viscoelasticity (Prony series; parallel networks after Bergström–Boyce), foams. Viscoelasticity: small- and finite-strain Prony series, time–temperature superposition (WLF, Arrhenius), storage and loss moduli for harmonic steps. Viscoplasticity and creep: Norton power law, time and strain hardening, Perzyna, Anand, Chaboche combined hardening. Plasticity: Hill anisotropic, Drucker–Prager, Mohr–Coulomb, Cam-clay, crushable foam, concrete damaged plasticity, Gurson. Fluids: Newtonian and non-Newtonian viscosity shared with M12. Material library: curated, cited data sets (steels, aluminium and titanium alloys, polymers, elastomers, composites, tissues) with units and temperature dependence; calibration from test data (uniaxial, biaxial, shear, relaxation, DMA) by least squares with Drucker stability checks; user materials as a safe Rust trait | planned |
|
||||
| M17 Constitutive library and material library | Elastic: transversely isotropic, fully anisotropic (21 constants), temperature-dependent. Elastomers: hyperelastic neo-Hookean, Mooney–Rivlin, Yeoh (these three done, in nonlinear steps), Ogden, Arruda–Boyce, Gent, Marlow, with Mullins softening and finite-strain viscoelasticity (Prony series; parallel networks after Bergström–Boyce), foams. Viscoelasticity: small- and finite-strain Prony series, time–temperature superposition (WLF, Arrhenius), storage and loss moduli for harmonic steps. Viscoplasticity and creep: Norton power law, time and strain hardening, Perzyna, Anand, Chaboche combined hardening. Plasticity: Hill anisotropic, Drucker–Prager, Mohr–Coulomb, Cam-clay, crushable foam, concrete damaged plasticity, Gurson. Fluids: Newtonian and non-Newtonian viscosity shared with M12. Material library: curated, cited data sets (steels, aluminium and titanium alloys, polymers, elastomers, composites, tissues) with units and temperature dependence; calibration from test data (uniaxial, biaxial, shear, relaxation, DMA) by least squares with Drucker stability checks; user materials as a safe Rust trait | planned |
|
||||
| M18 Composites and anisotropy | Material orientations (rectangular, cylindrical, spherical, from geometry edges and faces, per-element fields, draping of fibres over curved parts); layups on shells and continuum shells, ply stacks on swept hex; classical lamination theory checks; ply stresses and strains through the thickness; failure indices (Tsai–Wu, Tsai–Hill, Hashin, Puck, LaRC) and progressive damage; homogenisation (rule of mixtures, Mori–Tanaka, RVEs with periodic boundary conditions); delamination through M20's cohesive zones. Verified by laminate closed forms and the NAFEMS composite benchmarks | planned |
|
||||
| M19 Coupled fields | On the DOF framework, each a degree of freedom beside the displacements: piezoelectricity (electric potential, d/e coupling and permittivity matrices, voltage and charge conditions, resonance and impedance through M16 harmonic steps); poroelasticity (Biot, u–p, Darcy flow, consolidation; Terzaghi and Mandel verification; soils and biphasic tissue); electro-thermal (Joule heating) and thermo-electro-mechanical coupling; acoustics (Helmholtz elements, structure–acoustic coupling, absorbing boundaries); low-frequency electromagnetics later | planned |
|
||||
| M20 Damage, fracture, fatigue and adhesion | Continuum damage beyond Johnson–Cook (Lemaitre, ductile and shear criteria) for implicit and explicit; fracture mechanics (J-integral and stress intensity factors, crack-front meshes from the hex mesher; XFEM later); fatigue (S–N and ε–N with mean-stress corrections, rainflow counting, Paris crack growth); adhesion and debonding by cohesive zones (bilinear and exponential traction–separation, mixed mode with Benzeggagh–Kenane) as cohesive elements and cohesive contact; adhesive joints. Verified by DCB, ENF and MMB delamination tests and published fatigue data | planned |
|
||||
@@ -76,9 +76,9 @@ What exists and what is missing, by area; each gap is in a milestone above.
|
||||
|
||||
| Area | Today | Missing |
|
||||
|---|---|---|
|
||||
| Solution procedures | Linear static, nonlinear static (solids with geometric nonlinearity), frequency (Lanczos), steady and transient heat, coupled temperature–displacement, explicit dynamics, LBM flow | Nonlinear shells and beams, plasticity in implicit steps, arc-length, buckling (M15); implicit, harmonic, random and spectrum dynamics (M16) |
|
||||
| Solution procedures | Linear static, nonlinear static (solids with geometric and material nonlinearity: hyperelasticity, J2 plasticity), frequency (Lanczos), steady and transient heat, coupled temperature–displacement, explicit dynamics, LBM flow | Nonlinear shells and beams, arc-length, buckling (M15); implicit, harmonic, random and spectrum dynamics (M16) |
|
||||
| Contact, friction, adhesion | Explicit planar rigid walls (frictionless); couplings, springs, dashpots | Implicit and explicit deformable contact, tie, friction laws, thermal contact, wear (M7); cohesive adhesion and debonding (M20) |
|
||||
| Materials | Isotropic and orthotropic elasticity, density, thermal, J2 plasticity (bilinear, table, Johnson–Cook), Johnson–Cook damage and erosion, compressible neo-Hookean hyperelasticity, Newtonian fluids | Elastomers beyond neo-Hookean (Mooney–Rivlin and the rest), viscoelasticity, viscoplasticity and creep, anisotropic and pressure-dependent plasticity, non-Newtonian viscosity, a cited material library and calibration (M17) |
|
||||
| Materials | Isotropic and orthotropic elasticity, density, thermal, J2 plasticity (bilinear, table, Johnson–Cook), Johnson–Cook damage and erosion, compressible neo-Hookean, Mooney–Rivlin and Yeoh hyperelasticity, Newtonian fluids | Elastomers beyond Yeoh (Ogden, Arruda–Boyce and the rest), viscoelasticity, viscoplasticity and creep, anisotropic and pressure-dependent plasticity, non-Newtonian viscosity, a cited material library and calibration (M17) |
|
||||
| Composites and anisotropy | Orthotropic constants | Orientations, layups, ply results, failure criteria, homogenisation (M18) |
|
||||
| Coupled fields | Thermal–structural (one way and coupled) | Piezoelectric, poroelastic, electro-thermal, acoustics (M19) |
|
||||
| Failure | Johnson–Cook damage and element erosion (explicit) | Fracture mechanics, fatigue, cohesive zones, composite progressive damage (M18, M20) |
|
||||
|
||||
@@ -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}]}
|
||||
@@ -37,7 +37,10 @@
|
||||
//! and tolerances are left to Abaqus's controls, whose residual norms are defined differently.
|
||||
//! Abaqus starts each general step from the end of the previous one, while every model step
|
||||
//! starts from the undeformed state; for elastic materials the end state depends on the final
|
||||
//! loads alone, so the two agree.
|
||||
//! loads alone, so the two agree. A plastic material's state depends on the path, so a deck
|
||||
//! with nonlinear steps, a plastic material and more than one general step is refused, as is
|
||||
//! a nonlinear step's load path (an amplitude on every load and boundary condition, not
|
||||
//! written yet).
|
||||
//!
|
||||
//! # Loads
|
||||
//!
|
||||
@@ -105,7 +108,8 @@
|
||||
//!
|
||||
//! Isotropic elasticity is `*ELASTIC, TYPE=ISOTROPIC` (or, for a neo-Hookean material in a deck
|
||||
//! with nonlinear steps, `*HYPERELASTIC, NEO HOOKE` with C₁₀ = μ/2 and D₁ = 2/κ, the same strain
|
||||
//! energy; a linear static step is then refused, since Abaqus's general static step would apply
|
||||
//! energy, and likewise `MOONEY-RIVLIN` (C₁₀, C₀₁, D₁) and `YEOH` (C₁₀, C₂₀, C₃₀, D₁, the higher
|
||||
//! volumetric terms zero); a linear static step is then refused, since Abaqus's general static step would apply
|
||||
//! the hyperelastic law), engineering constants
|
||||
//! `TYPE=ENGINEERING CONSTANTS`, density `*DENSITY`. Von Mises plasticity is `*PLASTIC`: a
|
||||
//! hardening table as its (yield stress, plastic strain) lines; bilinear isotropic hardening as
|
||||
@@ -126,8 +130,8 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use fieldcraft_model::{
|
||||
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Hardening, Integration, Load, Model, OutputVariable, Plastic,
|
||||
Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
||||
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Hardening, Hyperelastic, Integration, Load, Model, OutputVariable,
|
||||
Plastic, Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
||||
};
|
||||
|
||||
use crate::{Error, Result, unsupported};
|
||||
@@ -313,6 +317,27 @@ fn check_expressible(model: &Model) -> Result<()> {
|
||||
));
|
||||
}
|
||||
}
|
||||
for step in model.steps() {
|
||||
if let Procedure::NonlinearStatic(n) = &step.procedure
|
||||
&& n.load_path.is_some()
|
||||
{
|
||||
bad.insert(format!(
|
||||
"step `{}`: a load path (it would be an amplitude on every load and boundary condition of the step; not written yet)",
|
||||
step.name
|
||||
));
|
||||
}
|
||||
}
|
||||
let general = model.steps().iter().filter(|s| matches!(s.procedure, Procedure::Static {} | Procedure::NonlinearStatic(_))).count();
|
||||
let plastic_in_use = model.sections().values().find(|s| model.material(&s.material).is_some_and(|m| m.plastic.is_some()));
|
||||
if has_nonlinear_step(model)
|
||||
&& general > 1
|
||||
&& let Some(section) = plastic_in_use
|
||||
{
|
||||
bad.insert(format!(
|
||||
"plastic material `{}` in a deck of {general} general steps (Abaqus carries the plastic state from one general step to the next, while every model step starts from the undeformed state; write each step to its own deck)",
|
||||
section.material
|
||||
));
|
||||
}
|
||||
for (name, m) in model.materials() {
|
||||
if m.damage.is_some() || m.erosion.is_some() {
|
||||
bad.insert(format!(
|
||||
@@ -785,11 +810,20 @@ impl Deck {
|
||||
for (name, m) in model.materials() {
|
||||
self.line(&format!("*MATERIAL, NAME={name}"));
|
||||
match m.elastic {
|
||||
Some(Elastic::Isotropic { young, poisson }) if m.hyperelastic.is_some() && has_nonlinear_step(model) => {
|
||||
// W = C10 (Ī1 − 3) + (J − 1)²/D1 with C10 = μ/2 and D1 = 2/κ.
|
||||
let (shear, bulk) = fieldcraft_model::Hyperelastic::moduli(young, poisson);
|
||||
self.line("*HYPERELASTIC, NEO HOOKE");
|
||||
self.numbers_line("", &[shear / 2.0, 2.0 / bulk])?;
|
||||
Some(Elastic::Isotropic { young, poisson })
|
||||
if has_nonlinear_step(model)
|
||||
&& let Some(h) = m.hyperelastic =>
|
||||
{
|
||||
// The polynomial constants of the option and U = (J − 1)²/D1 with D1 = 2/κ.
|
||||
let (shear, bulk) = Hyperelastic::moduli(young, poisson);
|
||||
let [c10, c01, c20, c30] = h.constants(shear);
|
||||
let (keyword, values) = match h {
|
||||
Hyperelastic::NeoHookean {} => ("NEO HOOKE", vec![c10, 2.0 / bulk]),
|
||||
Hyperelastic::MooneyRivlin { .. } => ("MOONEY-RIVLIN", vec![c10, c01, 2.0 / bulk]),
|
||||
Hyperelastic::Yeoh { .. } => ("YEOH", vec![c10, c20, c30, 2.0 / bulk]),
|
||||
};
|
||||
self.line(&format!("*HYPERELASTIC, {keyword}"));
|
||||
self.numbers_line("", &values)?;
|
||||
}
|
||||
Some(Elastic::Isotropic { young, poisson }) => {
|
||||
self.line("*ELASTIC, TYPE=ISOTROPIC");
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
//! - per step: `*CONTROL_IMPLICIT_GENERAL`, `*CONTROL_IMPLICIT_SOLUTION` (linear) or
|
||||
//! `*CONTROL_IMPLICIT_EIGENVALUE`, `*CONTROL_TERMINATION` (a static step is one implicit step
|
||||
//! from time 0 to 1 with every load ramped by one curve, `*DEFINE_CURVE`); a nonlinear static
|
||||
//! step runs over the same time with the same ramp, its first increment as `dt0`, the nonlinear
|
||||
//! step runs over the same time with the same ramp (or its load path as the curve), its first
|
||||
//! increment as `dt0`, the nonlinear
|
||||
//! solver with a stiffness reformation every iteration (`ilimit` 1, full Newton) and its
|
||||
//! iteration cap as `maxref`, and `*CONTROL_IMPLICIT_AUTO` with its minimum and maximum
|
||||
//! increments (its increment cap and tolerances have no LS-DYNA counterpart and are left to
|
||||
@@ -872,8 +873,13 @@ fn write_step(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan, step: &Ste
|
||||
d.header(&["lcid"]);
|
||||
d.card([Field::from(n)]);
|
||||
d.header(&["a1", "o1"]);
|
||||
d.card([Field::from(0.0), Field::from(0.0)]);
|
||||
d.card([Field::from(1.0), Field::from(1.0)]);
|
||||
let path = match &step.procedure {
|
||||
Procedure::NonlinearStatic(s) => s.load_path.clone(),
|
||||
_ => None,
|
||||
};
|
||||
for [t, f] in path.unwrap_or_else(|| vec![[0.0, 0.0], [1.0, 1.0]]) {
|
||||
d.card([Field::from(t), Field::from(f)]);
|
||||
}
|
||||
}
|
||||
|
||||
for b in &step.boundary_conditions {
|
||||
|
||||
@@ -790,8 +790,41 @@ fn nonlinear_steps_and_hyperelastic_materials_are_written() {
|
||||
assert!((values[0] - c10).abs() < 1e-9 * c10 && (values[1] - d1).abs() < 1e-9 * d1, "{line}");
|
||||
assert!(!deck.contains("*ELASTIC"), "{deck}");
|
||||
|
||||
// Mooney–Rivlin and Yeoh: C10 = μ/2 − C01, C01, D1 and C10, C20, C30, D1.
|
||||
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::MooneyRivlin { c01: 10000.0 }), ..steel() }).unwrap();
|
||||
let deck = abaqus::write(&m).unwrap();
|
||||
let line = deck.split("*HYPERELASTIC, MOONEY-RIVLIN\n").nth(1).and_then(|s| s.lines().next()).unwrap();
|
||||
let values: Vec<f64> = line.split(", ").map(|v| v.parse().unwrap()).collect();
|
||||
assert!((values[0] - (c10 - 10000.0)).abs() < 1e-9 * c10 && values[1] == 10000.0 && (values[2] - d1).abs() < 1e-9 * d1, "{line}");
|
||||
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::Yeoh { c20: -50.0, c30: 2.0 }), ..steel() }).unwrap();
|
||||
let deck = abaqus::write(&m).unwrap();
|
||||
let line = deck.split("*HYPERELASTIC, YEOH\n").nth(1).and_then(|s| s.lines().next()).unwrap();
|
||||
let values: Vec<f64> = line.split(", ").map(|v| v.parse().unwrap()).collect();
|
||||
assert!((values[0] - c10).abs() < 1e-9 * c10 && values[1..3] == [-50.0, 2.0] && (values[3] - d1).abs() < 1e-9 * d1, "{line}");
|
||||
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::NeoHookean {}), ..steel() }).unwrap();
|
||||
|
||||
m.add_step(step("Linear", Procedure::Static {}, &["clamp"], &["end"]), None).unwrap();
|
||||
assert!(unsupported(&m).contains("step `Linear`: a linear static step beside nonlinear steps of hyperelastic material `Steel`"));
|
||||
m.remove_step("Bend").unwrap();
|
||||
assert!(abaqus::write(&m).unwrap().contains("*ELASTIC, TYPE=ISOTROPIC\n"), "with linear steps only, the elastic constants are the whole of it");
|
||||
}
|
||||
|
||||
/// A plastic material in nonlinear steps is `*PLASTIC` in a general step; what Abaqus would
|
||||
/// read differently is refused: a load path, and several general steps (Abaqus carries the
|
||||
/// plastic state between them).
|
||||
#[test]
|
||||
fn nonlinear_plastic_steps_and_what_is_refused() {
|
||||
use fieldcraft_model::{Hardening, NonlinearStatic, Plastic};
|
||||
let mut m = cantilever();
|
||||
let plastic = Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 2000.0, hardening: Hardening::Isotropic };
|
||||
m.replace_material("Steel", Material { plastic: Some(plastic), ..steel() }).unwrap();
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic::default()), &["clamp"], &["end"])).unwrap();
|
||||
let deck = abaqus::write(&m).unwrap();
|
||||
assert!(deck.contains("*PLASTIC\n250.0, 0.0\n") && deck.contains("NLGEOM=YES"), "{deck}");
|
||||
let path = NonlinearStatic { load_path: Some(vec![[0.0, 0.0], [0.5, 1.0], [1.0, 0.0]]), ..NonlinearStatic::default() };
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(path), &["clamp"], &["end"])).unwrap();
|
||||
assert!(unsupported(&m).contains("step `Bend`: a load path"));
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic::default()), &["clamp"], &["end"])).unwrap();
|
||||
m.add_step(step("Again", Procedure::NonlinearStatic(NonlinearStatic::default()), &["clamp"], &["end"]), None).unwrap();
|
||||
assert!(unsupported(&m).contains("plastic material `Steel` in a deck of 2 general steps"));
|
||||
}
|
||||
@@ -760,6 +760,19 @@ fn nonlinear_static_steps() {
|
||||
assert_eq!(auto.iter().map(num).collect::<Vec<_>>(), [1.0, 1e-4, 0.5], "blank fields are skipped: {auto:?}");
|
||||
assert!(deck.contains("*DEFINE_CURVE_TITLE\nBend ramp\n"), "the loads follow the ramp");
|
||||
assert!(deck.contains("*LOAD_NODE"), "{deck}");
|
||||
// A load path is the curve.
|
||||
let path = NonlinearStatic { load_path: Some(vec![[0.0, 0.0], [0.5, 1.0], [1.0, -1.0]]), ..controls.clone() };
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(path), &["clamp"], &["tip_load"])).unwrap();
|
||||
let deck = lsdyna::write(&m).unwrap().text;
|
||||
let curve: Vec<Vec<f64>> = deck
|
||||
.lines()
|
||||
.skip_while(|l| *l != "Bend ramp")
|
||||
.filter(|l| !l.starts_with('$'))
|
||||
.skip(2)
|
||||
.take(3)
|
||||
.map(|l| l.split_whitespace().map(|v| v.parse().unwrap()).collect())
|
||||
.collect();
|
||||
assert_eq!(curve, [vec![0.0, 0.0], vec![0.5, 1.0], vec![1.0, -1.0]], "{deck}");
|
||||
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic { nonlinear_geometry: false, ..controls }), &["clamp"], &["tip_load"]))
|
||||
.unwrap();
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -81,7 +81,7 @@ definition_commands!(
|
||||
Material,
|
||||
"material",
|
||||
"Material",
|
||||
r#"{"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3} | {"type": "engineering_constants", "young": [E1, E2, E3], "poisson": [ν12, ν13, ν23], "shear": [G12, G13, G23]}, "density": 7.85e-9, "fluid": {"type": "newtonian", "viscosity": 1.0e-9} (dynamic viscosity; a fluid needs a density), "plastic": {…} (see model.material.plasticity), "damage": {…}, "erosion": {…} (see model.material.damage), "hyperelastic": {"type": "neo_hookean"} (large strain in nonlinear steps, from the isotropic constants: μ = E/(2(1 + ν)), κ = E/(3(1 − 2ν)); without it an elastic material is St Venant–Kirchhoff there)} — in the model's unit system (model.units), never converted"#,
|
||||
r#"{"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3} | {"type": "engineering_constants", "young": [E1, E2, E3], "poisson": [ν12, ν13, ν23], "shear": [G12, G13, G23]}, "density": 7.85e-9, "fluid": {"type": "newtonian", "viscosity": 1.0e-9} (dynamic viscosity; a fluid needs a density), "plastic": {…} (see model.material.plasticity), "damage": {…}, "erosion": {…} (see model.material.damage), "hyperelastic": {"type": "neo_hookean"} | {"type": "mooney_rivlin", "c01": 0.1} (C10 = μ/2 − C01) | {"type": "yeoh", "c20": -0.01, "c30": 0.001} (C10 = μ/2) (large strain in nonlinear steps, from the isotropic constants: μ = E/(2(1 + ν)), κ = E/(3(1 − 2ν)); without it an elastic material is St Venant–Kirchhoff there)} — in the model's unit system (model.units), never converted"#,
|
||||
add_material,
|
||||
replace_material,
|
||||
remove_material,
|
||||
@@ -266,7 +266,7 @@ fn fluid_boundary(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
pub const STEP_CREATE: CommandSpec = CommandSpec {
|
||||
id: "model.step.create",
|
||||
label: "New Step",
|
||||
params: r#"{"name": "Bend", "procedure": {"type": "static"} | {"type": "nonlinear_static", "nonlinear_geometry": true, "initial_increment": 0.1, "min_increment": 1e-5, "max_increment": 1.0 (fractions of the step), "max_increments": 100, "max_iterations": 16 (per increment), "tolerances": {"force": 1e-6, "displacement": 1e-5, "energy": 1e-10}, "line_search": false} (loads and prescribed displacements ramp with the load factor from 0 to 1) | {"type": "frequency", "modes": 10, "min_hz": 0, "max_hz": 500} | {"type": "fluid", "domain": "Flow", "end_time": 1.0 | "steps": 1000, "time_step": (default: chosen for Mach 0.087 and τ ≤ 1), "frames": 10, "magic": 0.1875 (the two-relaxation-time Λ)} | {"type": "explicit_dynamic", "end_time": 0.001, "dt_scale": 0.9, "max_increments": 10000000, "output_interval", "history_interval", "history_nodes": "set", "initial_velocities": [{"set", "velocity": [vx, vy, vz]}], "rigid_walls": [{"set", "point", "normal"}], "hourglass": {"type": "viscous|stiffness", "coefficient": 0.1}, "bulk_viscosity": {"linear": 0.06, "quadratic": 1.5}}, "output": ["displacement", "reaction_force", "stress", "strain"] (default by procedure), "boundary_conditions": [names], "loads": [names] (active in this step; nothing carries over between steps), "before": "step" (default: last)}"#,
|
||||
params: r#"{"name": "Bend", "procedure": {"type": "static"} | {"type": "nonlinear_static", "nonlinear_geometry": true, "initial_increment": 0.1, "min_increment": 1e-5, "max_increment": 1.0 (fractions of the step), "max_increments": 100, "max_iterations": 16 (per increment), "tolerances": {"force": 1e-6, "displacement": 1e-5, "energy": 1e-10}, "line_search": false, "load_path": [[0, 0], [0.5, 1], [1, 0]] (optional: the load factor against the step fraction, piecewise linear from [0, 0] to fraction 1; increments end at its points)} (loads and prescribed displacements are the load factor times their values; it ramps from 0 to 1 without a load path) | {"type": "frequency", "modes": 10, "min_hz": 0, "max_hz": 500} | {"type": "fluid", "domain": "Flow", "end_time": 1.0 | "steps": 1000, "time_step": (default: chosen for Mach 0.087 and τ ≤ 1), "frames": 10, "magic": 0.1875 (the two-relaxation-time Λ)} | {"type": "explicit_dynamic", "end_time": 0.001, "dt_scale": 0.9, "max_increments": 10000000, "output_interval", "history_interval", "history_nodes": "set", "initial_velocities": [{"set", "velocity": [vx, vy, vz]}], "rigid_walls": [{"set", "point", "normal"}], "hourglass": {"type": "viscous|stiffness", "coefficient": 0.1}, "bulk_viscosity": {"linear": 0.06, "quadratic": 1.5}}, "output": ["displacement", "reaction_force", "stress", "strain"] (default by procedure), "boundary_conditions": [names], "loads": [names] (active in this step; nothing carries over between steps), "before": "step" (default: last)}"#,
|
||||
kind: CommandKind::Model,
|
||||
run: step_create,
|
||||
};
|
||||
|
||||
+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,
|
||||
|
||||
@@ -34,7 +34,7 @@ pub const STATIC: CommandSpec = CommandSpec {
|
||||
pub const NONLINEAR: CommandSpec = CommandSpec {
|
||||
id: "solve.nonlinear",
|
||||
label: "Solve Nonlinear Static Step",
|
||||
params: r#"{"step": "Inflate", "max_dofs": 6000000, "max_factor_entries": 400000000, "max_newton_iterations": 100000, "threads": 8} — solves a nonlinear static step (the only one when `step` is left out) in load increments with Newton–Raphson iterations and keeps the result in the session: the final displacements, reactions, Cauchy stresses and logarithmic strains, every increment's load factor, iterations and residuals (force, displacement and energy, relative to their tolerances' scales), and a displacement frame per converged increment; the caps may only be lowered (`max_newton_iterations` over the whole step); `threads` as for solve.static"#,
|
||||
params: r#"{"step": "Inflate", "max_dofs": 6000000, "max_factor_entries": 400000000, "max_newton_iterations": 100000, "threads": 8} — solves a nonlinear static step (the only one when `step` is left out) in load increments with Newton–Raphson iterations and keeps the result in the session: the final displacements, reactions, Cauchy stresses and logarithmic strains (small strains without nonlinear geometry), the equivalent plastic strain of plastic materials (the largest in the summary), every increment's load factor, iterations and residuals (force, displacement and energy, relative to their tolerances' scales), and a displacement frame per converged increment; the caps may only be lowered (`max_newton_iterations` over the whole step); `threads` as for solve.static"#,
|
||||
kind: CommandKind::Model,
|
||||
run: run_nonlinear,
|
||||
};
|
||||
@@ -82,7 +82,7 @@ pub const FLUID: CommandSpec = CommandSpec {
|
||||
pub const PROBE: CommandSpec = CommandSpec {
|
||||
id: "solve.probe",
|
||||
label: "Probe Results",
|
||||
params: r#"{"step": "Load", "nodes": [1, 2], "mode": 1, "frame": 0} — the stored result at nodes: displacement, rotation, reaction, reaction moment, stress and von Mises (static, with `elements` each element's results: solid, 2D and truss nodal stresses, shell section forces (N, M, Q per length in the element's axes) and bottom, middle and top stresses, beam end forces (N, V_y, V_z, T, M_y, M_z in its axes)), the shape of mode `mode` with its rotations (frequency, from 1), or displacement and velocity in field frame `frame` (explicit, from 0; the last by default) at `nodes` (or at the nodes nearest to points: "at": [[x, y, z], ...], lowest label on a tie, each entry then giving its `xyz` and `distance`) and, with `elements` (explicit), each element's stress, von Mises stress, strain, equivalent plastic strain, damage, temperature and whether it has eroded; a coupled reference point's label gives its six displacements (translations, rotations) and reactions (forces, moments). Fluid steps: {"step": "Flow", "points": [[x, y, z]], "frame": 10 (default the last)} gives velocity and pressure in the cells at the points; {"step": "Flow", "forces": ["x_min", "F2:top", "Body1/F2:top"]} gives those walls' force histories"#,
|
||||
params: r#"{"step": "Load", "nodes": [1, 2], "mode": 1, "frame": 0} — the stored result at nodes: displacement, rotation, reaction, reaction moment, stress and von Mises (static, with `elements` each element's results: solid, 2D and truss nodal stresses (and, in a nonlinear step, the equivalent plastic strain at the integration points of a plastic element), shell section forces (N, M, Q per length in the element's axes) and bottom, middle and top stresses, beam end forces (N, V_y, V_z, T, M_y, M_z in its axes)), the shape of mode `mode` with its rotations (frequency, from 1), or displacement and velocity in field frame `frame` (explicit, from 0; the last by default) at `nodes` (or at the nodes nearest to points: "at": [[x, y, z], ...], lowest label on a tie, each entry then giving its `xyz` and `distance`) and, with `elements` (explicit), each element's stress, von Mises stress, strain, equivalent plastic strain, damage, temperature and whether it has eroded; a coupled reference point's label gives its six displacements (translations, rotations) and reactions (forces, moments). Fluid steps: {"step": "Flow", "points": [[x, y, z]], "frame": 10 (default the last)} gives velocity and pressure in the cells at the points; {"step": "Flow", "forces": ["x_min", "F2:top", "Body1/F2:top"]} gives those walls' force histories"#,
|
||||
kind: CommandKind::Model,
|
||||
run: probe,
|
||||
};
|
||||
@@ -220,6 +220,7 @@ fn run_nonlinear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"frames": history.map(|h| h.load_factors()),
|
||||
"max_displacement": largest(r.nodes.iter().zip(&r.displacement).map(|(l, u)| (*l, crate::results::magnitude(*u)))),
|
||||
"max_von_mises": largest(r.nodes.iter().zip(&r.von_mises).map(|(l, v)| (*l, *v))),
|
||||
"max_plastic_strain": largest(r.elements.iter().flat_map(|e| e.plastic_strain.iter().map(|v| (e.label, *v)))),
|
||||
"applied_total": r.applied_total,
|
||||
"reaction_total": r.reaction_total,
|
||||
"reference_points": r.reference_points.iter().map(|p| json!({
|
||||
@@ -613,7 +614,11 @@ fn by_label<T>(list: &[T], label: u32, key: impl Fn(&T) -> u32) -> Option<&T> {
|
||||
/// The stored results of one element of a static step.
|
||||
fn element_result(r: &StaticResult, label: u32) -> Option<Value> {
|
||||
if let Some(e) = by_label(&r.elements, label, |e| e.label) {
|
||||
return Some(json!({"element": label, "kind": e.kind.name(), "stress": e.nodal}));
|
||||
let mut v = json!({"element": label, "kind": e.kind.name(), "stress": e.nodal});
|
||||
if !e.plastic_strain.is_empty() {
|
||||
v["plastic_strain"] = json!(e.plastic_strain);
|
||||
}
|
||||
return Some(v);
|
||||
}
|
||||
if let Some(e) = by_label(&r.shells, label, |e| e.label) {
|
||||
let s = &e.stress;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
+168
-33
@@ -36,11 +36,22 @@
|
||||
//! §6.6, eqs. 6.165–6.169 and 6.193, in Kirchhoff form). The split is what the mixed (hybrid)
|
||||
//! elements need: they take the isochoric part from [`MaterialModel::isochoric`] and the
|
||||
//! volumetric one from [`MaterialModel::volumetric`] with the element's own dilatation.
|
||||
//! - **Mooney–Rivlin and Yeoh**: decoupled polynomials
|
||||
//! W̄ = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ with the neo-Hookean volume; their fictitious stress and
|
||||
//! elasticity from Holzapfel 2000, eqs. 6.193 and 6.168 ([`MaterialModel::Polynomial`]).
|
||||
//! - **J2 plasticity** ([`crate::elastoplastic`]): Simo's multiplicative plasticity with the
|
||||
//! exponential return map in the principal logarithmic strains (finite strain), or the
|
||||
//! additive radial return (small strain, [`MaterialModel::small_strain`]); decoupled too, the
|
||||
//! return deviatoric and the Hencky volume K/2 (ln J)².
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use fieldcraft_model::{Elastic, Hyperelastic};
|
||||
|
||||
use crate::dense::det3;
|
||||
use crate::elastoplastic;
|
||||
use crate::material::{Elasticity, elasticity, stress};
|
||||
use crate::plasticity::J2;
|
||||
use crate::rotation::{IDENTITY, Mat3, mul, transpose, voigt};
|
||||
use crate::{Error, Result};
|
||||
|
||||
@@ -61,11 +72,24 @@ pub struct Response {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum MaterialModel {
|
||||
StVenantKirchhoff(Box<Elasticity>),
|
||||
NeoHookean { shear: f64, bulk: f64 },
|
||||
NeoHookean {
|
||||
shear: f64,
|
||||
bulk: f64,
|
||||
},
|
||||
/// A decoupled polynomial in the isochoric invariants,
|
||||
/// W̄ = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ with `c` = (C₁₀, C₀₁, C₂₀, C₃₀), and U = κ/2 (J − 1)²:
|
||||
/// Mooney–Rivlin and Yeoh.
|
||||
Polynomial {
|
||||
name: &'static str,
|
||||
c: [f64; 4],
|
||||
bulk: f64,
|
||||
},
|
||||
/// Von Mises plasticity (shared: one per material, whatever the element count).
|
||||
Plastic(Arc<J2>),
|
||||
}
|
||||
|
||||
/// The 6 × 6 matrix of the symmetric fourth-order identity 𝕀 (engineering-shear columns).
|
||||
fn identity4() -> Tangent {
|
||||
pub(crate) fn identity4() -> Tangent {
|
||||
let mut i4 = [[0.0; 6]; 6];
|
||||
for k in 0..6 {
|
||||
i4[k][k] = if k < 3 { 1.0 } else { 0.5 };
|
||||
@@ -110,27 +134,35 @@ fn jacobian(f: &Mat3) -> Result<f64> {
|
||||
}
|
||||
|
||||
impl MaterialModel {
|
||||
/// The model of a material's elastic behaviour in a nonlinear step: neo-Hookean when the
|
||||
/// material asks for it (its isotropic constants give μ and κ), St Venant–Kirchhoff
|
||||
/// The model of a material's elastic behaviour in a nonlinear step: the hyperelastic option
|
||||
/// when the material has one (its isotropic constants give μ and κ), St Venant–Kirchhoff
|
||||
/// otherwise.
|
||||
pub fn from_model(elastic: &Elastic, hyperelastic: Option<&Hyperelastic>) -> Result<MaterialModel> {
|
||||
match hyperelastic {
|
||||
None => Ok(MaterialModel::StVenantKirchhoff(Box::new(elasticity(elastic)?))),
|
||||
Some(Hyperelastic::NeoHookean {}) => {
|
||||
let (young, poisson) = crate::material::isotropic_constants(elastic, "neo-Hookean")?;
|
||||
Some(h) => {
|
||||
let (young, poisson) = crate::material::isotropic_constants(elastic, h.name())?;
|
||||
let (shear, bulk) = Hyperelastic::moduli(young, poisson);
|
||||
Ok(MaterialModel::NeoHookean { shear, bulk })
|
||||
Ok(Self::hyperelastic(h, shear, bulk))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The hyperelastic model of an option with small-strain moduli μ and κ.
|
||||
fn hyperelastic(h: &Hyperelastic, shear: f64, bulk: f64) -> MaterialModel {
|
||||
match h {
|
||||
Hyperelastic::NeoHookean {} => MaterialModel::NeoHookean { shear, bulk },
|
||||
_ => MaterialModel::Polynomial { name: h.name(), c: h.constants(shear), bulk },
|
||||
}
|
||||
}
|
||||
|
||||
/// The model of an element whose elasticity is `d` (the small-strain one) and whose material
|
||||
/// has the large-strain option `hyperelastic`: neo-Hookean takes μ = D₄₄ and κ = D₁₂ + ⅔ μ of
|
||||
/// an isotropic `d`, St Venant–Kirchhoff `d` itself.
|
||||
/// has the large-strain option `hyperelastic`: the hyperelastic models take μ = D₄₄ and
|
||||
/// κ = D₁₂ + ⅔ μ of an isotropic `d`, St Venant–Kirchhoff `d` itself.
|
||||
pub fn from_elasticity(d: &Elasticity, hyperelastic: Option<&Hyperelastic>) -> MaterialModel {
|
||||
match hyperelastic {
|
||||
None => MaterialModel::StVenantKirchhoff(Box::new(*d)),
|
||||
Some(Hyperelastic::NeoHookean {}) => MaterialModel::NeoHookean { shear: d[3][3], bulk: d[0][1] + 2.0 / 3.0 * d[3][3] },
|
||||
Some(h) => Self::hyperelastic(h, d[3][3], d[0][1] + 2.0 / 3.0 * d[3][3]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,23 +170,43 @@ impl MaterialModel {
|
||||
match self {
|
||||
MaterialModel::StVenantKirchhoff(_) => "St Venant–Kirchhoff",
|
||||
MaterialModel::NeoHookean { .. } => "neo-Hookean",
|
||||
MaterialModel::Polynomial { name, .. } => name,
|
||||
MaterialModel::Plastic(_) => "J2 plastic",
|
||||
}
|
||||
}
|
||||
|
||||
/// History variables per integration point.
|
||||
pub fn history_len(&self) -> usize {
|
||||
0
|
||||
match self {
|
||||
MaterialModel::Plastic(_) => elastoplastic::HISTORY,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the response depends on the path (the material keeps history), so the solver
|
||||
/// treats it with the inelastic kinematics and predictors.
|
||||
pub fn is_path_dependent(&self) -> bool {
|
||||
self.history_len() > 0
|
||||
}
|
||||
|
||||
/// The equivalent plastic strain in one point's history; `None` for an elastic material.
|
||||
pub fn plastic_strain(&self, history: &[f64]) -> Option<f64> {
|
||||
match self {
|
||||
MaterialModel::Plastic(_) => Some(elastoplastic::value(history, elastoplastic::PLASTIC_STRAIN)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the strain energy splits into isochoric and volumetric parts (what mixed
|
||||
/// elements need).
|
||||
pub fn is_decoupled(&self) -> bool {
|
||||
matches!(self, MaterialModel::NeoHookean { .. })
|
||||
!matches!(self, MaterialModel::StVenantKirchhoff(_))
|
||||
}
|
||||
|
||||
/// The Kirchhoff stress and spatial tangent at deformation gradient `f`, from the history
|
||||
/// The Kirchhoff stress and spatial tangent at deformation gradient `f` and (for
|
||||
/// temperature-dependent plasticity) temperature `temperature`, from the history
|
||||
/// `committed` (written, updated, to `updated`).
|
||||
pub fn response(&self, f: &Mat3, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
pub fn response(&self, f: &Mat3, temperature: Option<f64>, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
updated.iter_mut().zip(committed).for_each(|(u, c)| *u = *c);
|
||||
let j = jacobian(f)?;
|
||||
match self {
|
||||
@@ -165,18 +217,25 @@ impl MaterialModel {
|
||||
let t = push_forward(f);
|
||||
Ok(Response { stress: std::array::from_fn(|a| (0..6).map(|b| t[a][b] * s[b]).sum()), tangent: sandwich(&t, d) })
|
||||
}
|
||||
MaterialModel::NeoHookean { .. } => {
|
||||
let iso = self.isochoric(f)?;
|
||||
let (p, dp) = self.volumetric(j).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
|
||||
MaterialModel::NeoHookean { .. } | MaterialModel::Polynomial { .. } | MaterialModel::Plastic(_) => {
|
||||
let iso = self.isochoric(f, temperature, committed, updated)?;
|
||||
let (p, dp) = self.volumetric(j, false).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
|
||||
Ok(add_pressure(&iso, j, p, j * dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The isochoric part of a decoupled material (τ_iso = dev τ̄ and its tangent).
|
||||
pub fn isochoric(&self, f: &Mat3) -> Result<Response> {
|
||||
let &MaterialModel::NeoHookean { shear: mu, .. } = self else {
|
||||
return Err(Error::NotSupported(format!("the isochoric–volumetric split of a {} material", self.name())));
|
||||
/// The isochoric part of a decoupled material (τ_iso = dev τ̄ and its tangent), from the
|
||||
/// history `committed`; the history that changes is written to `updated`, which the caller
|
||||
/// starts as a copy of `committed`.
|
||||
pub fn isochoric(&self, f: &Mat3, temperature: Option<f64>, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
let mu = match self {
|
||||
MaterialModel::NeoHookean { shear, .. } => *shear,
|
||||
MaterialModel::Plastic(j2) => return elastoplastic::finite_isochoric(j2, f, temperature, committed, updated),
|
||||
MaterialModel::Polynomial { c, .. } => return polynomial_isochoric(c, f),
|
||||
MaterialModel::StVenantKirchhoff(_) => {
|
||||
return Err(Error::NotSupported(format!("the isochoric–volumetric split of a {} material", self.name())));
|
||||
}
|
||||
};
|
||||
let j = jacobian(f)?;
|
||||
let scale = j.powf(-2.0 / 3.0);
|
||||
@@ -199,13 +258,74 @@ impl MaterialModel {
|
||||
Ok(Response { stress: tau, tangent: c })
|
||||
}
|
||||
|
||||
/// U′(J) and U″(J) of a decoupled material's volumetric energy.
|
||||
pub fn volumetric(&self, j: f64) -> Option<(f64, f64)> {
|
||||
match *self {
|
||||
MaterialModel::NeoHookean { bulk, .. } => Some((bulk * (j - 1.0), bulk)),
|
||||
/// U′(J) and U″(J) of a decoupled material's volumetric energy at dilatation J (with
|
||||
/// `small`, J = 1 + tr ε of small strain, where every material's is K/2 (J − 1)²).
|
||||
pub fn volumetric(&self, j: f64, small: bool) -> Option<(f64, f64)> {
|
||||
match self {
|
||||
MaterialModel::NeoHookean { bulk, .. } | MaterialModel::Polynomial { bulk, .. } => Some((bulk * (j - 1.0), *bulk)),
|
||||
MaterialModel::Plastic(j2) => elastoplastic::volumetric(j2, j, small),
|
||||
MaterialModel::StVenantKirchhoff(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Small strain: the stress and its tangent at the (mechanical) strain `strain`
|
||||
/// (engineering shears), from the history `committed` (written, updated, to `updated`);
|
||||
/// `bulk` false keeps the deviatoric part only (what a mixed element takes). St
|
||||
/// Venant–Kirchhoff is linear elasticity here; hyperelastic materials have no small-strain
|
||||
/// form.
|
||||
pub fn small_strain(&self, strain: &[f64; 6], temperature: Option<f64>, committed: &[f64], updated: &mut [f64], bulk: bool) -> Result<Response> {
|
||||
updated.iter_mut().zip(committed).for_each(|(u, c)| *u = *c);
|
||||
match self {
|
||||
MaterialModel::Plastic(j2) => elastoplastic::small(j2, strain, temperature, committed, updated, bulk),
|
||||
MaterialModel::StVenantKirchhoff(d) if bulk => Ok(Response { stress: stress(d, strain), tangent: **d }),
|
||||
_ => Err(Error::NotSupported(format!("the {} material at small strain{}", self.name(), if bulk { "" } else { " in mixed elements" }))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The isochoric response of the polynomial energy W̄(Ī₁, Ī₂) with `c` = (C₁₀, C₀₁, C₂₀, C₃₀)
|
||||
/// (Holzapfel 2000, §6.5–6.6, in Kirchhoff form): the fictitious stress
|
||||
/// τ̄ = 2 (Ψ₁ + Ī₁ Ψ₂) b̄ − 2 Ψ₂ b̄², τ_iso = dev τ̄, and with the fictitious elasticity
|
||||
/// c̄ = 4 (Ψ₁₁ + Ψ₂) b̄ ⊗ b̄ − 4 Ψ₂ b̄ ⊙ b̄ (eq. 6.193 pushed forward, Ψ₁₂ = Ψ₂₂ = 0 here),
|
||||
/// c_iso = ℙ : c̄ : ℙ + ⅔ tr τ̄ ℙ − ⅔ (τ_iso ⊗ I + I ⊗ τ_iso) (eq. 6.168). With C₁₀ alone it is
|
||||
/// the neo-Hookean response.
|
||||
fn polynomial_isochoric(c: &[f64; 4], f: &Mat3) -> Result<Response> {
|
||||
let [c10, c01, c20, c30] = *c;
|
||||
let j = jacobian(f)?;
|
||||
let scale = j.powf(-2.0 / 3.0);
|
||||
let b = mul(f, &transpose(f)).map(|row| row.map(|v| scale * v));
|
||||
let b2 = mul(&b, &b);
|
||||
let i1 = b[0][0] + b[1][1] + b[2][2];
|
||||
let x = i1 - 3.0;
|
||||
let (psi1, psi2, psi11) = (c10 + 2.0 * c20 * x + 3.0 * c30 * x * x, c01, 2.0 * c20 + 6.0 * c30 * x);
|
||||
let (bv, b2v) = (voigt(&b), voigt(&b2));
|
||||
let fict: [f64; 6] = std::array::from_fn(|a| 2.0 * (psi1 + i1 * psi2) * bv[a] - 2.0 * psi2 * b2v[a]);
|
||||
let tr = fict[0] + fict[1] + fict[2];
|
||||
let mut tau = fict;
|
||||
for v in tau.iter_mut().take(3) {
|
||||
*v -= tr / 3.0;
|
||||
}
|
||||
let mut cbar = [[0.0; 6]; 6];
|
||||
for (a, &(i, jj)) in PAIRS.iter().enumerate() {
|
||||
for (bb, &(k, l)) in PAIRS.iter().enumerate() {
|
||||
cbar[a][bb] = 4.0 * (psi11 + psi2) * bv[a] * bv[bb] - 2.0 * psi2 * (b[i][k] * b[jj][l] + b[i][l] * b[jj][k]);
|
||||
}
|
||||
}
|
||||
// ℙ : c̄ : ℙ = c̄ − ⅓ I ⊗ (I : c̄) − ⅓ (c̄ : I) ⊗ I + ⅑ (I : c̄ : I) I ⊗ I.
|
||||
let row: [f64; 6] = std::array::from_fn(|a| (0..3).map(|bb| cbar[a][bb]).sum());
|
||||
let col: [f64; 6] = std::array::from_fn(|bb| (0..3).map(|a| cbar[a][bb]).sum());
|
||||
let all: f64 = (0..3).map(|a| row[a]).sum();
|
||||
let i4 = identity4();
|
||||
let mut out = [[0.0; 6]; 6];
|
||||
for a in 0..6 {
|
||||
for bb in 0..6 {
|
||||
let (ia, ib) = (if a < 3 { 1.0 } else { 0.0 }, if bb < 3 { 1.0 } else { 0.0 });
|
||||
let projected = cbar[a][bb] - ia * col[bb] / 3.0 - row[a] * ib / 3.0 + ia * ib * all / 9.0;
|
||||
let projector = i4[a][bb] - ia * ib / 3.0;
|
||||
out[a][bb] = projected + 2.0 / 3.0 * tr * projector - 2.0 / 3.0 * (tau[a] * ib + ia * tau[bb]);
|
||||
}
|
||||
}
|
||||
Ok(Response { stress: tau, tangent: out })
|
||||
}
|
||||
|
||||
/// An isochoric response with a pressure p acting on dilatation J: τ + J p I, and the tangent
|
||||
@@ -269,7 +389,7 @@ mod tests {
|
||||
/// The tangent's consistency: dS = C : dE with C the pull-back of c, checked by central
|
||||
/// differences of S along several directions dF (dE = sym(Fᵀ dF), engineering shears).
|
||||
fn check_tangent(m: &MaterialModel, f: &Mat3) {
|
||||
let r = m.response(f, &[], &mut []).unwrap();
|
||||
let r = m.response(f, None, &[], &mut []).unwrap();
|
||||
let (inv, _) = inverse3(f).unwrap();
|
||||
let back = sandwich(&push_forward(&inv), &r.tangent);
|
||||
let scale = back.iter().flatten().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
@@ -280,7 +400,7 @@ mod tests {
|
||||
df[(k + 1) % 3][(k + 2) % 3] += 0.3;
|
||||
let at = |s: f64| {
|
||||
let fp: Mat3 = std::array::from_fn(|i| std::array::from_fn(|j| f[i][j] + s * df[i][j]));
|
||||
pull_back_stress(&fp, &m.response(&fp, &[], &mut []).unwrap().stress)
|
||||
pull_back_stress(&fp, &m.response(&fp, None, &[], &mut []).unwrap().stress)
|
||||
};
|
||||
let (sp, sm) = (at(h), at(-h));
|
||||
let ftdf = mul(&transpose(f), &df);
|
||||
@@ -300,13 +420,28 @@ mod tests {
|
||||
check_tangent(&m, &deformation());
|
||||
check_tangent(&m, &IDENTITY);
|
||||
// Symmetric (hyperelastic): major symmetry of c.
|
||||
let c = m.response(&deformation(), &[], &mut []).unwrap().tangent;
|
||||
let c = m.response(&deformation(), None, &[], &mut []).unwrap().tangent;
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
assert!((c[a][b] - c[b][a]).abs() < 1e-9 * 5000.0, "{a} {b}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Mooney–Rivlin and Yeoh: consistent and symmetric; with C₁₀ alone the neo-Hookean model.
|
||||
for m in [
|
||||
MaterialModel::Polynomial { name: "Mooney–Rivlin", c: [120.0, 30.0, 0.0, 0.0], bulk: 5000.0 },
|
||||
MaterialModel::Polynomial { name: "Yeoh", c: [150.0, 0.0, -12.0, 1.5], bulk: 5000.0 },
|
||||
] {
|
||||
check_tangent(&m, &deformation());
|
||||
check_tangent(&m, &IDENTITY);
|
||||
let c = m.response(&deformation(), None, &[], &mut []).unwrap().tangent;
|
||||
assert!((0..36).all(|k| (c[k / 6][k % 6] - c[k % 6][k / 6]).abs() < 1e-9 * 5000.0), "{} symmetric", m.name());
|
||||
}
|
||||
let neo = MaterialModel::NeoHookean { shear: 300.0, bulk: 5000.0 };
|
||||
let poly = MaterialModel::Polynomial { name: "neo", c: [150.0, 0.0, 0.0, 0.0], bulk: 5000.0 };
|
||||
let (a, b) = (neo.response(&deformation(), None, &[], &mut []).unwrap(), poly.response(&deformation(), None, &[], &mut []).unwrap());
|
||||
assert!(a.stress.iter().zip(&b.stress).all(|(x, y)| (x - y).abs() < 1e-11 * 300.0));
|
||||
assert!(a.tangent.iter().flatten().zip(b.tangent.iter().flatten()).all(|(x, y)| (x - y).abs() < 1e-10 * 5000.0));
|
||||
let ortho = crate::material::orthotropic([1000.0, 300.0, 500.0], [0.3, 0.2, 0.25], [100.0, 120.0, 90.0]).unwrap();
|
||||
check_tangent(&MaterialModel::StVenantKirchhoff(Box::new(ortho)), &deformation());
|
||||
}
|
||||
@@ -317,7 +452,7 @@ mod tests {
|
||||
let d = isotropic(e, nu).unwrap();
|
||||
let neo = MaterialModel::from_model(&Elastic::Isotropic { young: e, poisson: nu }, Some(&Hyperelastic::NeoHookean {})).unwrap();
|
||||
for m in [MaterialModel::StVenantKirchhoff(Box::new(d)), neo] {
|
||||
let c = m.response(&IDENTITY, &[], &mut []).unwrap().tangent;
|
||||
let c = m.response(&IDENTITY, None, &[], &mut []).unwrap().tangent;
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
assert!((c[a][b] - d[a][b]).abs() < 1e-9 * e, "{}: {a} {b}", m.name());
|
||||
@@ -329,7 +464,7 @@ mod tests {
|
||||
let k = crate::rotation::skew(&axis);
|
||||
let k2 = mul(&k, &k);
|
||||
let rot: Mat3 = std::array::from_fn(|i| std::array::from_fn(|j| IDENTITY[i][j] + s * k[i][j] + (1.0 - cth) * k2[i][j]));
|
||||
let tau = m.response(&rot, &[], &mut []).unwrap().stress;
|
||||
let tau = m.response(&rot, None, &[], &mut []).unwrap().stress;
|
||||
assert!(tau.iter().all(|v| v.abs() < 1e-12 * e), "{}: {tau:?}", m.name());
|
||||
}
|
||||
}
|
||||
@@ -343,14 +478,14 @@ mod tests {
|
||||
let (lam, l) = (1.5f64, 0.85f64);
|
||||
let f = [[lam, 0.0, 0.0], [0.0, l, 0.0], [0.0, 0.0, l]];
|
||||
let j = lam * l * l;
|
||||
let tau = m.response(&f, &[], &mut []).unwrap().stress;
|
||||
let tau = m.response(&f, None, &[], &mut []).unwrap().stress;
|
||||
let i1 = lam * lam + 2.0 * l * l;
|
||||
let sxx = mu / j.powf(5.0 / 3.0) * (lam * lam - i1 / 3.0) + kappa * (j - 1.0);
|
||||
let syy = mu / j.powf(5.0 / 3.0) * (l * l - i1 / 3.0) + kappa * (j - 1.0);
|
||||
assert!((tau[0] / j - sxx).abs() < 1e-10 * mu && (tau[1] / j - syy).abs() < 1e-10 * mu, "{tau:?}");
|
||||
let e = logarithmic_strain(&f).unwrap();
|
||||
assert!((e[0] - lam.ln()).abs() < 1e-14 && (e[1] - l.ln()).abs() < 1e-14 && e[3].abs() < 1e-14, "{e:?}");
|
||||
assert!(m.response(&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]], &[], &mut []).is_err(), "inverted");
|
||||
assert!(MaterialModel::StVenantKirchhoff(Box::new(isotropic(1.0, 0.3).unwrap())).isochoric(&IDENTITY).is_err());
|
||||
assert!(m.response(&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]], None, &[], &mut []).is_err(), "inverted");
|
||||
assert!(MaterialModel::StVenantKirchhoff(Box::new(isotropic(1.0, 0.3).unwrap())).isochoric(&IDENTITY, None, &[], &mut []).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
//! Von Mises (J2) elastoplasticity at a material point of implicit (Newton) analysis: the
|
||||
//! stress after an increment and its consistent tangent, at finite strain and at small strain.
|
||||
//! Both kinematics call the radial return of [`crate::plasticity`], the update explicit
|
||||
//! dynamics uses, so the yield surface, the hardening (bilinear isotropic or linear kinematic,
|
||||
//! tabulated, Johnson–Cook) and the plastic strain increment of a trial state are one
|
||||
//! implementation for both solvers.
|
||||
//!
|
||||
//! **Finite strain** (J. C. Simo, *Algorithms for static and dynamic multiplicative plasticity
|
||||
//! that preserve the classical return mapping schemes of the infinitesimal theory*, CMAME
|
||||
//! 99:61–112, 1992; Simo & Hughes 1998, ch. 9): the multiplicative split F = Fₑ Fₚ with the
|
||||
//! elastic left Cauchy–Green tensor bₑ = F Cₚ⁻¹ Fᵀ and a Hencky (quadratic in the logarithmic
|
||||
//! strain) elastic energy, so the Kirchhoff stress is τ = K ln J I + 2G dev εₑ with
|
||||
//! εₑ = ½ ln bₑ. The history is Cₚ⁻¹ (stored as Cₚ⁻¹ − I, so the virgin state is zeros) and the
|
||||
//! equivalent plastic strain. An increment freezes Cₚ⁻¹ at its committed value: the trial
|
||||
//! bₑᵗʳ = F Cₚ⁻¹ Fᵀ, its principal stretches and directions, the trial logarithmic strains
|
||||
//! ε_A = ½ ln λ_A², and then the **return map of the infinitesimal theory on the principal
|
||||
//! values** (the exponential map integrates the flow rule exactly in the principal frame, which
|
||||
//! the isotropy of the model keeps fixed during the return). The update writes back
|
||||
//! bₑ = Σ exp(2 εₑ,A) n_A ⊗ n_A and Cₚ⁻¹ = F⁻¹ bₑ F⁻ᵀ, which keeps det Cₚ = 1 (plastic flow
|
||||
//! is isochoric).
|
||||
//!
|
||||
//! **Its tangent.** With Cₚ⁻¹ frozen, the Lie derivative of bₑᵗʳ vanishes, so for a rate of
|
||||
//! deformation d the trial strain changes by dεᵗʳ = ½ (∂ ln b/∂b) : (d b + b d), and
|
||||
//! L_v τ = 𝔻ᵉᵖ : dεᵗʳ − (d τ + τ d), with 𝔻ᵉᵖ the algorithmic tangent of the return map
|
||||
//! ([`crate::plasticity::J2::tangent`]). In the principal frame of b the derivative of the
|
||||
//! logarithm is the Daleckii–Krein divided difference, so dεᵗʳ_AB = θ_AB d_AB with θ_AA = 1 and
|
||||
//! θ_AB = ½ (λ²_A + λ²_B) ln(λ²_A/λ²_B)/(λ²_A − λ²_B) = artanh(r)/r, r = (λ²_A − λ²_B)/(λ²_A + λ²_B),
|
||||
//! smooth through equal stretches (its series 1 + r²/3 + r⁴/5 + … near r = 0), so uniaxial and
|
||||
//! undeformed states need no special case (Simo 1992, §3.3, gives the same tangent in spectral
|
||||
//! form; R. Bhatia, *Matrix Analysis*, Springer 1997, §V.3, for the divided differences). The
|
||||
//! tangent is symmetric for isotropic hardening: 𝔻ᵉᵖ and the θ scaling share the principal
|
||||
//! frame.
|
||||
//!
|
||||
//! **Kinematic hardening at finite strain** is refused: a back stress is not coaxial with bₑ, so
|
||||
//! the principal-frame return no longer holds, and it needs a formulation of its own (for
|
||||
//! example the Lagrangian logarithmic strain space of C. Miehe, N. Apel, M. Lambrecht, CMAME
|
||||
//! 191:5383–5425, 2002). It works with small strain.
|
||||
//!
|
||||
//! **Small strain** (Simo & Hughes 1998, §3.3): ε = εₑ + εₚ additively, the history the plastic
|
||||
//! strain εₚ, the back stress α and the equivalent plastic strain; the trial εₑ = ε − εₚ,ₙ goes
|
||||
//! through the same return, and εₚ advances by √(3/2) Δε̄ₚ n.
|
||||
//!
|
||||
//! **Mixed (hybrid) elements** take the isochoric part (the deviatoric return, without K) and the
|
||||
//! volumetric energy U(J) = K/2 (ln J)², U′ = K ln J / J (finite strain), or U = K/2 (J − 1)² with
|
||||
//! J the 1 + tr ε of small strain: J2 plasticity is pressure insensitive and isochoric, so the
|
||||
//! split is exact.
|
||||
//!
|
||||
//! **History layout** per integration point ([`HISTORY`] values): 0–5 Cₚ⁻¹ − I (finite strain,
|
||||
//! tensor components) or εₚ (small strain, engineering shears), 6–11 the back stress α, 12 the
|
||||
//! equivalent plastic strain.
|
||||
|
||||
use crate::constitutive::{Response, Tangent};
|
||||
use crate::dense::{inverse3, symmetric_eigen};
|
||||
use crate::plasticity::{J2, PointState, Return};
|
||||
use crate::rotation::{IDENTITY, Mat3, mul, tensor, transpose, voigt};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// History values per integration point.
|
||||
pub const HISTORY: usize = 13;
|
||||
|
||||
/// Where the equivalent plastic strain is in a point's history.
|
||||
pub const PLASTIC_STRAIN: usize = 12;
|
||||
|
||||
/// History value `i` (0 when the slice is short).
|
||||
pub(crate) fn value(h: &[f64], i: usize) -> f64 {
|
||||
h.get(i).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Cₚ⁻¹ of a finite-strain history (stored as Cₚ⁻¹ − I).
|
||||
fn plastic_stretch(h: &[f64]) -> Mat3 {
|
||||
let stored = tensor(&std::array::from_fn(|k| value(h, k)));
|
||||
std::array::from_fn(|i| std::array::from_fn(|j| IDENTITY[i][j] + stored[i][j]))
|
||||
}
|
||||
|
||||
/// Store Cₚ⁻¹ as Cₚ⁻¹ − I in the first six history values.
|
||||
fn store_plastic_stretch(cp: &Mat3, h: &mut [f64]) {
|
||||
let v = voigt(cp);
|
||||
for (k, slot) in h.iter_mut().take(6).enumerate() {
|
||||
*slot = v[k] - if k < 3 { 1.0 } else { 0.0 };
|
||||
}
|
||||
}
|
||||
|
||||
/// artanh(r)/r, smooth through r = 0.
|
||||
fn log_divided_difference(r: f64) -> f64 {
|
||||
if r.abs() < 1e-4 {
|
||||
let r2 = r * r;
|
||||
1.0 + r2 * (1.0 / 3.0 + r2 * (1.0 / 5.0 + r2 / 7.0))
|
||||
} else {
|
||||
r.atanh() / r
|
||||
}
|
||||
}
|
||||
|
||||
/// The state a return starts from: the committed back stress and plastic strain, at the point's
|
||||
/// temperature (Johnson–Cook softening; room temperature without one).
|
||||
fn start(j2: &J2, committed: &[f64], temperature: Option<f64>) -> PointState {
|
||||
let mut s = PointState::new(temperature.unwrap_or_else(|| j2.initial_temperature()));
|
||||
s.back = std::array::from_fn(|k| value(committed, 6 + k));
|
||||
s.plastic_strain = value(committed, PLASTIC_STRAIN);
|
||||
s
|
||||
}
|
||||
|
||||
fn singular() -> Error {
|
||||
Error::Degenerate("the plastic material point has no tangent: its hardening falls faster than −3G (a softening material localises)".into())
|
||||
}
|
||||
|
||||
/// The deviatoric trial stress 2G dev ε of a strain 6-vector (engineering shears), as tensor
|
||||
/// components.
|
||||
fn trial_deviator(j2: &J2, e: &[f64; 6]) -> [f64; 6] {
|
||||
let mean = (e[0] + e[1] + e[2]) / 3.0;
|
||||
let g2 = 2.0 * j2.shear;
|
||||
[g2 * (e[0] - mean), g2 * (e[1] - mean), g2 * (e[2] - mean), j2.shear * e[3], j2.shear * e[4], j2.shear * e[5]]
|
||||
}
|
||||
|
||||
/// Finite strain: the isochoric (deviatoric) Kirchhoff stress and its spatial tangent at
|
||||
/// deformation gradient `f`, from the history `committed` (written, updated, to `updated`). Add
|
||||
/// the volumetric part with [`volumetric`] (or the mixed element's pressure) for the whole
|
||||
/// response.
|
||||
pub fn finite_isochoric(j2: &J2, f: &Mat3, temperature: Option<f64>, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
if let Some(crate::plasticity::Flow::Linear { kinematic: true, .. }) = j2.flow {
|
||||
return Err(Error::NotSupported(
|
||||
"kinematic hardening at finite strain (its back stress is not coaxial with the elastic stretch; use nonlinear_geometry false or isotropic hardening)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
let b = mul(&mul(f, &plastic_stretch(committed)), &transpose(f));
|
||||
let (l, q) = symmetric_eigen(&b)
|
||||
.filter(|(l, _)| l.iter().all(|x| *x > 0.0))
|
||||
.ok_or_else(|| Error::Degenerate("the elastic stretch of a plastic material point is not positive (the material is inverted)".into()))?;
|
||||
let eps = l.map(|x| 0.5 * x.ln());
|
||||
let mut state = start(j2, committed, temperature);
|
||||
let mut trial = trial_deviator(j2, &[eps[0], eps[1], eps[2], 0.0, 0.0, 0.0]);
|
||||
let ret: Option<Return> = j2.radial_return(&mut state, &mut trial, 0.0);
|
||||
// Back to the global frame: τ = Σ τ_A q_A ⊗ q_A, n likewise.
|
||||
let spectral = |v: [f64; 3]| -> Mat3 { std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|a| v[a] * q[i][a] * q[j][a]).sum())) };
|
||||
let tau = voigt(&spectral([trial[0], trial[1], trial[2]]));
|
||||
let global = ret.map(|r| Return { normal: voigt(&spectral([r.normal[0], r.normal[1], r.normal[2]])), ..r });
|
||||
let d = j2.tangent(global.as_ref(), false).ok_or_else(singular)?;
|
||||
// θ_AB of the logarithm's derivative.
|
||||
let theta: Mat3 =
|
||||
std::array::from_fn(|a| std::array::from_fn(|bb| if a == bb { 1.0 } else { log_divided_difference((l[a] - l[bb]) / (l[a] + l[bb])) }));
|
||||
let tau_m = tensor(&tau);
|
||||
let qt = transpose(&q);
|
||||
let mut c: Tangent = [[0.0; 6]; 6];
|
||||
for (col, &(i, j)) in crate::constitutive::PAIRS.iter().enumerate() {
|
||||
// The unit rate of deformation of this column (engineering shear 1: d_ij = d_ji = ½).
|
||||
let mut dm = [[0.0; 3]; 3];
|
||||
let half = if i == j { 1.0 } else { 0.5 };
|
||||
dm[i][j] = half;
|
||||
dm[j][i] = half;
|
||||
let mut dp = mul(&mul(&qt, &dm), &q);
|
||||
for a in 0..3 {
|
||||
for bb in 0..3 {
|
||||
dp[a][bb] *= theta[a][bb];
|
||||
}
|
||||
}
|
||||
let de = voigt(&mul(&mul(&q, &dp), &qt));
|
||||
let de = [de[0], de[1], de[2], 2.0 * de[3], 2.0 * de[4], 2.0 * de[5]];
|
||||
let spin = voigt(&{
|
||||
let (x, y) = (mul(&dm, &tau_m), mul(&tau_m, &dm));
|
||||
std::array::from_fn(|r| std::array::from_fn(|s| x[r][s] + y[r][s]))
|
||||
});
|
||||
for row in 0..6 {
|
||||
c[row][col] = (0..6).map(|k| d[row][k] * de[k]).sum::<f64>() - spin[row];
|
||||
}
|
||||
}
|
||||
if let Some(r) = ret {
|
||||
// bₑ = Σ exp(2 εₑ,A) q_A ⊗ q_A with εₑ = ε − √(3/2) Δε̄ₚ n, and Cₚ⁻¹ = F⁻¹ bₑ F⁻ᵀ.
|
||||
let dg = (1.5f64).sqrt() * r.plastic_strain;
|
||||
let be = spectral(std::array::from_fn(|a| (2.0 * (eps[a] - dg * r.normal[a])).exp()));
|
||||
let (inv, _) = inverse3(f).ok_or_else(|| Error::Degenerate("the deformation gradient is singular".into()))?;
|
||||
store_plastic_stretch(&mul(&mul(&inv, &be), &transpose(&inv)), updated);
|
||||
if let Some(slot) = updated.get_mut(PLASTIC_STRAIN) {
|
||||
*slot = state.plastic_strain;
|
||||
}
|
||||
}
|
||||
Ok(Response { stress: tau, tangent: c })
|
||||
}
|
||||
|
||||
/// U′(J) and U″(J) of the Hencky volumetric energy K/2 (ln J)² (finite strain), or of
|
||||
/// K/2 (J − 1)² for the dilatation J = 1 + tr ε of small strain.
|
||||
pub fn volumetric(j2: &J2, j: f64, small: bool) -> Option<(f64, f64)> {
|
||||
if !(j > 0.0 && j.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let k = j2.bulk;
|
||||
Some(if small { (k * (j - 1.0), k) } else { (k * j.ln() / j, k * (1.0 - j.ln()) / (j * j)) })
|
||||
}
|
||||
|
||||
/// Small strain: the stress and the consistent tangent at total (mechanical) strain `strain`
|
||||
/// (engineering shears), from the history `committed` (written, updated, to `updated`); with
|
||||
/// `bulk` false only the deviatoric part (what a mixed element takes).
|
||||
pub fn small(j2: &J2, strain: &[f64; 6], temperature: Option<f64>, committed: &[f64], updated: &mut [f64], bulk: bool) -> Result<Response> {
|
||||
let elastic: [f64; 6] = std::array::from_fn(|k| strain[k] - value(committed, k));
|
||||
let mut state = start(j2, committed, temperature);
|
||||
let mut stress = trial_deviator(j2, &elastic);
|
||||
let ret = j2.radial_return(&mut state, &mut stress, 0.0);
|
||||
if bulk {
|
||||
let p = j2.bulk * (elastic[0] + elastic[1] + elastic[2]);
|
||||
for s in stress.iter_mut().take(3) {
|
||||
*s += p;
|
||||
}
|
||||
}
|
||||
let tangent = j2.tangent(ret.as_ref(), bulk).ok_or_else(singular)?;
|
||||
if let Some(r) = ret {
|
||||
let dg = (1.5f64).sqrt() * r.plastic_strain;
|
||||
for (k, slot) in updated.iter_mut().take(6).enumerate() {
|
||||
*slot = value(committed, k) + dg * r.normal[k] * if k < 3 { 1.0 } else { 2.0 };
|
||||
}
|
||||
for (k, slot) in updated.iter_mut().skip(6).take(6).enumerate() {
|
||||
*slot = state.back[k];
|
||||
}
|
||||
if let Some(slot) = updated.get_mut(PLASTIC_STRAIN) {
|
||||
*slot = state.plastic_strain;
|
||||
}
|
||||
}
|
||||
Ok(Response { stress, tangent })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::constitutive::{add_pressure, push_forward};
|
||||
use fieldcraft_model::{Elastic, Hardening, HardeningPoint, Plastic};
|
||||
|
||||
const E: f64 = 200_000.0;
|
||||
const NU: f64 = 0.3;
|
||||
|
||||
fn j2(plastic: Plastic) -> J2 {
|
||||
J2::from_model(&Elastic::Isotropic { young: E, poisson: NU }, Some(&plastic), None).unwrap()
|
||||
}
|
||||
|
||||
fn bilinear(h: f64, hardening: Hardening) -> J2 {
|
||||
j2(Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: h, hardening })
|
||||
}
|
||||
|
||||
/// The whole finite-strain response, isochoric plus the Hencky volume.
|
||||
fn finite(m: &J2, f: &Mat3, committed: &[f64], updated: &mut [f64]) -> Response {
|
||||
let iso = finite_isochoric(m, f, None, committed, updated).unwrap();
|
||||
let j = crate::dense::det3(f);
|
||||
let (p, dp) = volumetric(m, j, false).unwrap();
|
||||
add_pressure(&iso, j, p, j * dp)
|
||||
}
|
||||
|
||||
/// The pulled-back tangent C (dS = C : dE) against central differences of S along several
|
||||
/// directions of F, as `constitutive::tests::check_tangent` does, from fixed history.
|
||||
fn check_finite_tangent(m: &J2, f: &Mat3, committed: &[f64]) -> f64 {
|
||||
let mut scratch = vec![0.0; HISTORY];
|
||||
let r = finite(m, f, committed, &mut scratch);
|
||||
let (inv, _) = inverse3(f).unwrap();
|
||||
let t = push_forward(&inv);
|
||||
let back: Tangent =
|
||||
std::array::from_fn(|a| std::array::from_fn(|b| (0..6).map(|k| (0..6).map(|l| t[a][k] * r.tangent[k][l] * t[b][l]).sum::<f64>()).sum()));
|
||||
let scale = back.iter().flatten().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let h = 1e-7;
|
||||
let mut worst: f64 = 0.0;
|
||||
for k in 0..9 {
|
||||
let mut df = [[0.0; 3]; 3];
|
||||
df[k / 3][k % 3] = 1.0;
|
||||
df[(k + 1) % 3][(k + 2) % 3] += 0.3;
|
||||
let at = |s: f64| {
|
||||
let fp: Mat3 = std::array::from_fn(|i| std::array::from_fn(|j| f[i][j] + s * df[i][j]));
|
||||
let mut scratch = vec![0.0; HISTORY];
|
||||
let tau = finite(m, &fp, committed, &mut scratch).stress;
|
||||
crate::rotation::rotate(&inverse3(&fp).unwrap().0, &tau)
|
||||
};
|
||||
let (sp, sm) = (at(h), at(-h));
|
||||
let ftdf = mul(&transpose(f), &df);
|
||||
let de = [ftdf[0][0], ftdf[1][1], ftdf[2][2], ftdf[0][1] + ftdf[1][0], ftdf[0][2] + ftdf[2][0], ftdf[1][2] + ftdf[2][1]];
|
||||
for a in 0..6 {
|
||||
let numeric = (sp[a] - sm[a]) / (2.0 * h);
|
||||
let analytic: f64 = (0..6).map(|b| back[a][b] * de[b]).sum();
|
||||
worst = worst.max((numeric - analytic).abs() / scale);
|
||||
}
|
||||
}
|
||||
worst
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finite_strain_tangent_is_consistent_elastic_and_plastic() {
|
||||
let deformations: [Mat3; 3] = [
|
||||
[[1.02, 0.015, -0.005], [-0.01, 0.99, 0.02], [0.007, -0.012, 1.005]],
|
||||
// Large: well into plasticity.
|
||||
[[1.3, 0.15, -0.05], [-0.1, 0.85, 0.2], [0.07, -0.12, 1.05]],
|
||||
// Uniaxial (two equal principal stretches).
|
||||
[[1.2, 0.0, 0.0], [0.0, 0.92, 0.0], [0.0, 0.0, 0.92]],
|
||||
];
|
||||
let tables = Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: 250.0 }, HardeningPoint { strain: 0.5, stress: 600.0 }] };
|
||||
let jc = Plastic::JohnsonCook {
|
||||
a: 350.0,
|
||||
b: 275.0,
|
||||
n: 0.36,
|
||||
c: 0.0,
|
||||
m: 1.0,
|
||||
reference_strain_rate: 1.0,
|
||||
room_temperature: 293.0,
|
||||
melt_temperature: 1356.0,
|
||||
specific_heat: None,
|
||||
taylor_quinney: 0.9,
|
||||
};
|
||||
// A plastic history to start from: a prior stretch committed.
|
||||
let mut prior = vec![0.0; HISTORY];
|
||||
finite(&bilinear(2000.0, Hardening::Isotropic), &[[1.1, 0.02, 0.0], [0.0, 0.95, 0.03], [0.0, 0.0, 0.96]], &[0.0; HISTORY], &mut prior);
|
||||
assert!(prior[PLASTIC_STRAIN] > 0.0);
|
||||
for m in [bilinear(2000.0, Hardening::Isotropic), bilinear(0.0, Hardening::Isotropic), j2(tables), j2(jc)] {
|
||||
for f in &deformations {
|
||||
for committed in [&[0.0; HISTORY][..], &prior[..]] {
|
||||
let worst = check_finite_tangent(&m, f, committed);
|
||||
assert!(worst < 1e-7, "{:?} at {f:?}: {worst}", m.flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finite_strain_tangent_is_symmetric_and_elastic_at_rest() {
|
||||
let m = bilinear(2000.0, Hardening::Isotropic);
|
||||
let mut scratch = vec![0.0; HISTORY];
|
||||
let r = finite(&m, &[[1.3, 0.15, -0.05], [-0.1, 0.85, 0.2], [0.07, -0.12, 1.05]], &[0.0; HISTORY], &mut scratch);
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
assert!((r.tangent[a][b] - r.tangent[b][a]).abs() < 1e-9 * E, "{a} {b}");
|
||||
}
|
||||
}
|
||||
let rest = finite(&m, &IDENTITY, &[0.0; HISTORY], &mut scratch);
|
||||
let d = crate::material::isotropic(E, NU).unwrap();
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
assert!((rest.tangent[a][b] - d[a][b]).abs() < 1e-9 * E);
|
||||
}
|
||||
}
|
||||
assert!(rest.stress.iter().all(|v| *v == 0.0));
|
||||
}
|
||||
|
||||
/// Uniaxial stress along x at finite strain, from a virgin point to a logarithmic strain ε
|
||||
/// in one increment (the lateral stretch found so σ_yy = 0): the Kirchhoff stress is the
|
||||
/// small-strain curve in the logarithmic strain, τ = σ_y + E_t (ε − σ_y/E), because the
|
||||
/// exponential map integrates a proportional path exactly.
|
||||
#[test]
|
||||
fn finite_uniaxial_matches_the_logarithmic_closed_form() {
|
||||
let (y0, h) = (250.0, 2000.0);
|
||||
let m = bilinear(h, Hardening::Isotropic);
|
||||
let strain: f64 = 0.2;
|
||||
let lam = strain.exp();
|
||||
let sigma = |l: f64| {
|
||||
let f = [[lam, 0.0, 0.0], [0.0, l, 0.0], [0.0, 0.0, l]];
|
||||
let mut u = vec![0.0; HISTORY];
|
||||
let r = finite(&m, &f, &[0.0; HISTORY], &mut u);
|
||||
let j = lam * l * l;
|
||||
(r.stress[0] / j, r.stress[1] / j, u)
|
||||
};
|
||||
// Secant on the lateral stretch.
|
||||
let (mut a, mut b) = (1.0, 0.9);
|
||||
for _ in 0..60 {
|
||||
let (fa, fb) = (sigma(a).1, sigma(b).1);
|
||||
if fb.abs() < 1e-11 || fb == fa {
|
||||
break;
|
||||
}
|
||||
(a, b) = (b, b - fb * (b - a) / (fb - fa));
|
||||
}
|
||||
let (sxx, _, u) = sigma(b);
|
||||
let j = lam * b * b;
|
||||
// In Kirchhoff stress τ = J σ: τ = y0 + E_t (ε − y0/E) along the log strain path.
|
||||
let et = E * h / (E + h);
|
||||
let exact = y0 + et * (strain - y0 / E);
|
||||
assert!((sxx * j - exact).abs() < 1e-8 * exact, "{} vs {exact}", sxx * j);
|
||||
assert!((u[PLASTIC_STRAIN] - (exact - y0) / h).abs() < 1e-10);
|
||||
// The plastic deformation is isochoric: det Cₚ⁻¹ = 1.
|
||||
let cp = plastic_stretch(&u);
|
||||
assert!((crate::dense::det3(&cp) - 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_strain_tangent_is_consistent_and_cyclic_kinematic_shows_bauschinger() {
|
||||
let m = bilinear(10_000.0, Hardening::Kinematic);
|
||||
let strain = [3e-3, -1e-3, 5e-4, 2e-3, -1e-3, 7e-4];
|
||||
let mut prior = vec![0.0; HISTORY];
|
||||
small(&m, &[2e-3, -1e-3, -1e-3, 0.0, 0.0, 0.0], None, &[0.0; HISTORY], &mut prior, true).unwrap();
|
||||
for committed in [&[0.0; HISTORY][..], &prior[..]] {
|
||||
let mut scratch = vec![0.0; HISTORY];
|
||||
let r = small(&m, &strain, None, committed, &mut scratch, true).unwrap();
|
||||
let h = 1e-9;
|
||||
for b in 0..6 {
|
||||
let at = |s: f64| {
|
||||
let mut e = strain;
|
||||
e[b] += s;
|
||||
small(&m, &e, None, committed, &mut [0.0; HISTORY], true).unwrap().stress
|
||||
};
|
||||
let (p, q) = (at(h), at(-h));
|
||||
for a in 0..6 {
|
||||
let numeric = (p[a] - q[a]) / (2.0 * h);
|
||||
assert!((numeric - r.tangent[a][b]).abs() < 1e-5 * E, "{a} {b}: {numeric} vs {}", r.tangent[a][b]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Kinematic hardening is refused at finite strain, named.
|
||||
assert!(matches!(finite_isochoric(&m, &IDENTITY, None, &[0.0; HISTORY], &mut [0.0; HISTORY]), Err(Error::NotSupported(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softening_steeper_than_3g_has_no_tangent() {
|
||||
let g = E / (2.0 * (1.0 + NU));
|
||||
let points = vec![HardeningPoint { strain: 0.0, stress: 250.0 }, HardeningPoint { strain: 1e-4, stress: 250.0 - 4.0 * g * 1e-4 }];
|
||||
let m = j2(Plastic::Table { points });
|
||||
let r = small(&m, &[5e-3, -2.5e-3, -2.5e-3, 0.0, 0.0, 0.0], None, &[0.0; HISTORY], &mut [0.0; HISTORY], true);
|
||||
assert!(matches!(r, Err(Error::Degenerate(_))), "{r:?}");
|
||||
}
|
||||
}
|
||||
+483
-83
@@ -1,6 +1,6 @@
|
||||
//! Finite-strain (total Lagrangian) solid and 2D elements: internal forces and the consistent
|
||||
//! tangent stiffness for nodal displacements u, and the Cauchy stress and logarithmic strain at
|
||||
//! the integration points.
|
||||
//! Solid and 2D elements of nonlinear steps: total Lagrangian (finite strain) or with small
|
||||
//! strain and an inelastic material; internal forces and the consistent tangent stiffness for
|
||||
//! nodal displacements u, and the Cauchy stress and the strain at the integration points.
|
||||
//!
|
||||
//! **Formulation.** Total Lagrangian: the integrals run over the reference (undeformed) element
|
||||
//! with its own quadrature, F = I + Σ u_a ⊗ ∇₀N_a at each point, and the material returns the
|
||||
@@ -26,7 +26,26 @@
|
||||
//! symmetric; at u = 0 it is the linear hybrid stiffness K_dev + κ G M⁻¹ Gᵀ. The constant
|
||||
//! pressure case is the mean dilatation method of J. C. Nagtegaal, D. M. Parks, J. R. Rice
|
||||
//! (CMAME 4:153–177, 1974) at finite strain. St Venant–Kirchhoff has no such split, so hybrid
|
||||
//! elements of an elastic material are refused in nonlinear steps.
|
||||
//! elements of an elastic material are refused in nonlinear steps; J2 plasticity has it exactly
|
||||
//! (the return is deviatoric, the volume Hencky's K/2 (ln J)², [`crate::elastoplastic`]).
|
||||
//!
|
||||
//! **Thermal strain** ([`Temperature`]): at finite strain the multiplicative split F = F_m F_θ
|
||||
//! with the thermal stretches F_θ = diag(1 + αᵢ (T − T_ref)) at each point (T interpolated from
|
||||
//! the nodes; V. A. Lubarda, *Constitutive theories based on the multiplicative decomposition of
|
||||
//! deformation gradient: thermoelasticity, elastoplasticity, and biomechanics*, Appl. Mech. Rev.
|
||||
//! 57(2):95–108, 2004): the material sees
|
||||
//! F_m, the stress per reference volume is J_θ τ_m(F_m), and since F_θ is fixed over an
|
||||
//! iteration the spatial tangent is J_θ c_m, so the loops run unchanged on the thermally
|
||||
//! expanded volume J_θ dV₀ (the mixed elements project det F_m on it). The linear stretch
|
||||
//! 1 + α ΔT is the engineering definition of α, so a free body grows by exactly α ΔT. At small
|
||||
//! strain the split is additive, ε_m = ε − α ΔT. Reported strains are the total ones.
|
||||
//!
|
||||
//! **Small strain** ([`Kinematics::Small`], a step without nonlinear geometry with an inelastic
|
||||
//! material): the same loops on the linear kinematics, ε = sym ∇u with the reference gradients,
|
||||
//! f = ∫ Bᵀ σ dV₀ and K = ∫ Bᵀ 𝔻 B dV₀ with the material's consistent tangent 𝔻, and no
|
||||
//! geometric stiffness. Mixed elements project the dilatation 1 + tr ε and the pressure K tr ε
|
||||
//! the same way, which is the linear hybrid element of [`crate::hybrid`] while the material is
|
||||
//! elastic.
|
||||
//!
|
||||
//! **2D elements**: plane strain (F₃₃ = 1) and axisymmetric (F₃₃ = r/R, the hoop stretch, with
|
||||
//! the hoop column N/r of the current radius in Bₓ and a geometric term τ_θθ N_a N_b / r² on the
|
||||
@@ -37,7 +56,8 @@
|
||||
//! (enhanced assumed strain, J. C. Simo, F. Armero, IJNME 33:1413–1449, 1992) needs the internal
|
||||
//! modes as element unknowns carried between iterations, and it develops spurious hourglass
|
||||
//! modes in large compression (P. Wriggers, S. Reese, CMAME 135:201–209, 1996), which want a
|
||||
//! stabilised variant of their own.
|
||||
//! stabilised variant of their own. With small strain they are refused for inelastic materials
|
||||
//! too: their internal modes would carry the plastic history between iterations.
|
||||
|
||||
use crate::constitutive::{MaterialModel, Response, add_pressure, logarithmic_strain};
|
||||
use crate::dense::{Cholesky, Matrix, inverse3};
|
||||
@@ -54,12 +74,87 @@ pub struct Finite {
|
||||
pub tangent: Option<Matrix>,
|
||||
}
|
||||
|
||||
/// The kinematics of an evaluation.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Kinematics {
|
||||
/// Total Lagrangian, large displacements, rotations and strains.
|
||||
Finite,
|
||||
/// Small displacements and strains (ε = sym ∇u, the reference gradients, no geometric
|
||||
/// stiffness): only the material is nonlinear.
|
||||
Small,
|
||||
}
|
||||
|
||||
/// The temperatures of an element in a nonlinear step: the linear expansion coefficients along
|
||||
/// X, Y, Z (zero for a material that does not expand), the reference temperature at which the
|
||||
/// thermal strain is zero, and the temperature at each node.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Temperature {
|
||||
pub alpha: [f64; 3],
|
||||
pub reference: f64,
|
||||
pub nodal: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Temperature {
|
||||
fn check(&self, nodes: usize) -> Result<()> {
|
||||
if self.nodal.len() != nodes {
|
||||
return Err(Error::Invalid(format!("{} temperatures for a {nodes}-node element", self.nodal.len())));
|
||||
}
|
||||
if self.alpha.iter().chain(&self.nodal).chain([&self.reference]).any(|v| !v.is_finite()) {
|
||||
return Err(Error::Invalid("temperatures and expansion coefficients must be finite".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The temperature at a point and its thermal stretches ϑᵢ = 1 + αᵢ (T − T_ref).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Heat {
|
||||
temperature: Option<f64>,
|
||||
stretch: [f64; 3],
|
||||
}
|
||||
|
||||
impl Heat {
|
||||
/// At a point of shape function values `n`.
|
||||
fn at(t: Option<&Temperature>, n: &[f64]) -> Result<Heat> {
|
||||
let Some(t) = t else { return Ok(Heat { temperature: None, stretch: [1.0; 3] }) };
|
||||
let temperature: f64 = t.nodal.iter().zip(n).map(|(a, b)| a * b).sum();
|
||||
let stretch = t.alpha.map(|a| 1.0 + a * (temperature - t.reference));
|
||||
if !stretch.iter().all(|v| *v > 0.0 && v.is_finite()) {
|
||||
return Err(Error::Degenerate(format!("the thermal stretches {stretch:?} at T = {temperature} are not positive")));
|
||||
}
|
||||
Ok(Heat { temperature: Some(temperature), stretch })
|
||||
}
|
||||
|
||||
/// J_θ = ϑ₁ ϑ₂ ϑ₃.
|
||||
fn volume(&self) -> f64 {
|
||||
self.stretch.iter().product()
|
||||
}
|
||||
|
||||
/// The mechanical part F_m = F F_θ⁻¹ of a deformation gradient.
|
||||
fn mechanical(&self, f: &Mat3) -> Mat3 {
|
||||
std::array::from_fn(|i| std::array::from_fn(|j| f[i][j] / self.stretch[j]))
|
||||
}
|
||||
|
||||
/// The mechanical part ε − α (T − T_ref) of a small strain.
|
||||
fn mechanical_strain(&self, e: &[f64; 6]) -> [f64; 6] {
|
||||
std::array::from_fn(|k| if k < 3 { e[k] - (self.stretch[k] - 1.0) } else { e[k] })
|
||||
}
|
||||
}
|
||||
|
||||
/// What one integration point gives the element.
|
||||
struct PointKinematics {
|
||||
/// The point's share of the reference volume (of the thermally expanded volume J_θ dV₀ at
|
||||
/// finite strain).
|
||||
weight: f64,
|
||||
/// The deformation gradient, and its mechanical part F_m = F F_θ⁻¹ the material sees.
|
||||
total: Mat3,
|
||||
f: Mat3,
|
||||
/// det F_m, or the dilatation 1 + tr ε_m of small strain (what a mixed element projects).
|
||||
j: f64,
|
||||
/// ∇ₓN of every node.
|
||||
/// The mechanical small strain ε − ε_th (engineering shears); zero at finite strain.
|
||||
strain: [f64; 6],
|
||||
temperature: Option<f64>,
|
||||
/// ∇ₓN of every node (∇₀N at small strain).
|
||||
gradients: Vec<[f64; 3]>,
|
||||
/// Reference position.
|
||||
x0: [f64; 3],
|
||||
@@ -70,8 +165,22 @@ fn point_history(history: &[f64], len: usize, point: usize) -> &[f64] {
|
||||
history.get(point * len..(point + 1) * len).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Add f_a += w Bₓ,aᵀ τ and, with `k`, the material and geometric tangent.
|
||||
fn add_point(force: &mut [f64], k: Option<&mut Matrix>, gradients: &[[f64; 3]], r: &Response, w: f64) {
|
||||
/// The small strain of a displacement gradient F − I (engineering shears).
|
||||
fn small_strain(f: &Mat3) -> [f64; 6] {
|
||||
[f[0][0] - 1.0, f[1][1] - 1.0, f[2][2] - 1.0, f[0][1] + f[1][0], f[0][2] + f[2][0], f[1][2] + f[2][1]]
|
||||
}
|
||||
|
||||
/// The response of one point's material with its history slices.
|
||||
fn point_response(m: &MaterialModel, kin: Kinematics, p: &PointKinematics, committed: &[f64], updated: &mut [f64]) -> Result<Response> {
|
||||
match kin {
|
||||
Kinematics::Finite => m.response(&p.f, p.temperature, committed, updated),
|
||||
Kinematics::Small => m.small_strain(&p.strain, p.temperature, committed, updated, true),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add f_a += w Bₓ,aᵀ τ and, with `k`, the material tangent and (with `geometric`) the
|
||||
/// geometric one.
|
||||
fn add_point(force: &mut [f64], k: Option<&mut Matrix>, gradients: &[[f64; 3]], r: &Response, w: f64, geometric: bool) {
|
||||
for (a, g) in gradients.iter().enumerate() {
|
||||
let b = b_block(g);
|
||||
for c in 0..3 {
|
||||
@@ -82,6 +191,9 @@ fn add_point(force: &mut [f64], k: Option<&mut Matrix>, gradients: &[[f64; 3]],
|
||||
}
|
||||
let Some(k) = k else { return };
|
||||
accumulate_symmetric(&mut k.data, gradients, &r.tangent, w);
|
||||
if !geometric {
|
||||
return;
|
||||
}
|
||||
let tau = crate::rotation::tensor(&r.stress);
|
||||
let n = k.n;
|
||||
for (a, ga) in gradients.iter().enumerate() {
|
||||
@@ -122,14 +234,17 @@ fn check_history(m: &MaterialModel, points: usize, committed: &[f64], updated: &
|
||||
}
|
||||
|
||||
impl Solid<'_> {
|
||||
/// Refuse what has no finite-strain form here.
|
||||
fn check_finite(&self, m: &MaterialModel, u: &[[f64; 3]]) -> Result<()> {
|
||||
/// Refuse what has no form here.
|
||||
fn check_nonlinear(&self, m: &MaterialModel, u: &[[f64; 3]], kin: Kinematics) -> Result<()> {
|
||||
check_displacements(u, self.ty.nodes)?;
|
||||
match self.formulation {
|
||||
Formulation::IncompatibleModes => Err(Error::NotSupported(format!(
|
||||
"{} incompatible modes in nonlinear steps (their finite-strain form, enhanced assumed strain, is not implemented; use full integration, hybrid or hex20)",
|
||||
self.ty.kind.name()
|
||||
))),
|
||||
Formulation::IncompatibleModes => {
|
||||
let why = match kin {
|
||||
Kinematics::Finite => "in nonlinear steps (their finite-strain form, enhanced assumed strain, is not implemented",
|
||||
Kinematics::Small => "with an inelastic material (their internal modes would carry the plastic history",
|
||||
};
|
||||
Err(Error::NotSupported(format!("{} incompatible modes {why}; use full integration, hybrid or hex20)", self.ty.kind.name())))
|
||||
}
|
||||
Formulation::Hybrid { .. } if !m.is_decoupled() => Err(Error::NotSupported(format!(
|
||||
"hybrid elements of a {} material in nonlinear steps (the mixed formulation needs a volumetric–isochoric split of the strain energy; give the material the neo-Hookean option)",
|
||||
m.name()
|
||||
@@ -138,59 +253,87 @@ impl Solid<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// F, J and the current gradients at every stiffness point.
|
||||
fn kinematics(&self, u: &[[f64; 3]]) -> Result<Vec<PointKinematics>> {
|
||||
/// F, J and the gradients at every stiffness point: the current ones at finite strain, the
|
||||
/// reference ones at small strain.
|
||||
fn kinematics(&self, u: &[[f64; 3]], kin: Kinematics, thermal: Option<&Temperature>) -> Result<Vec<PointKinematics>> {
|
||||
let n = self.ty.nodes;
|
||||
if let Some(t) = thermal {
|
||||
t.check(n)?;
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for p in self.rule().points() {
|
||||
let g = self.gradients(p.xi)?;
|
||||
let reference = g.dx.get(..n).unwrap_or_default();
|
||||
let f = crate::constitutive::deformation_gradient(u, reference);
|
||||
let (inv, j) = inverse3(&f).ok_or_else(|| inverted(self.ty.kind.name(), 0.0))?;
|
||||
if !(j > 0.0 && j.is_finite()) {
|
||||
return Err(inverted(self.ty.kind.name(), j));
|
||||
let total = crate::constitutive::deformation_gradient(u, reference);
|
||||
let (weight, x0) = (g.det * p.weight, self.position_of(g.shape.values()));
|
||||
let heat = Heat::at(thermal, g.shape.values())?;
|
||||
if kin == Kinematics::Small {
|
||||
let strain = heat.mechanical_strain(&small_strain(&total));
|
||||
let j = 1.0 + strain[0] + strain[1] + strain[2];
|
||||
let gradients = reference.to_vec();
|
||||
out.push(PointKinematics { weight, total, f: total, j, strain, temperature: heat.temperature, gradients, x0 });
|
||||
continue;
|
||||
}
|
||||
let (inv, det) = inverse3(&total).ok_or_else(|| inverted(self.ty.kind.name(), 0.0))?;
|
||||
let j = det / heat.volume();
|
||||
if !(det > 0.0 && j.is_finite()) {
|
||||
return Err(inverted(self.ty.kind.name(), det));
|
||||
}
|
||||
let gradients = reference.iter().map(|d| std::array::from_fn(|i| (0..3).map(|jj| d[jj] * inv[jj][i]).sum())).collect();
|
||||
out.push(PointKinematics { weight: g.det * p.weight, f, j, gradients, x0: self.position_of(g.shape.values()) });
|
||||
let f = heat.mechanical(&total);
|
||||
out.push(PointKinematics { weight: weight * heat.volume(), total, f, j, strain: [0.0; 6], temperature: heat.temperature, gradients, x0 });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The effective response (Kirchhoff stress and tangent) at every point, the forces, and the
|
||||
/// tangent when `tangent`.
|
||||
fn finite_points(
|
||||
/// The effective response (Kirchhoff stress and tangent, or the stress of small strain) at
|
||||
/// every point, the forces, and the tangent when `tangent`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn nonlinear_points(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 3]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> Result<(Finite, Vec<(PointKinematics, Response)>)> {
|
||||
self.check_finite(m, u)?;
|
||||
let points = self.kinematics(u)?;
|
||||
self.check_nonlinear(m, u, kin)?;
|
||||
let points = self.kinematics(u, kin, thermal)?;
|
||||
let len = check_history(m, points.len(), committed, updated)?;
|
||||
let ndof = 3 * self.ty.nodes;
|
||||
let mut force = vec![0.0; ndof];
|
||||
let mut k = tangent.then(|| Matrix::zeros(ndof));
|
||||
let responses: Vec<Response> = if let Formulation::Hybrid { .. } = self.formulation {
|
||||
self.mixed_responses(m, &points, k.as_mut())?
|
||||
self.mixed_responses(m, &points, kin, committed, updated, k.as_mut())?
|
||||
} else {
|
||||
let mut out = Vec::with_capacity(points.len());
|
||||
for (i, p) in points.iter().enumerate() {
|
||||
let updated_point = updated.get_mut(i * len..(i + 1) * len).unwrap_or_default();
|
||||
out.push(m.response(&p.f, point_history(committed, len, i), updated_point)?);
|
||||
out.push(point_response(m, kin, p, point_history(committed, len, i), updated_point)?);
|
||||
}
|
||||
out
|
||||
};
|
||||
for (p, r) in points.iter().zip(&responses) {
|
||||
add_point(&mut force, k.as_mut(), &p.gradients, r, p.weight);
|
||||
add_point(&mut force, k.as_mut(), &p.gradients, r, p.weight, kin == Kinematics::Finite);
|
||||
}
|
||||
Ok((Finite { force, tangent: k }, points.into_iter().zip(responses).collect()))
|
||||
}
|
||||
|
||||
/// The three-field mixed responses τ_iso + J p I with their tangents, and the dilatation
|
||||
/// stiffness Gᵀ M⁻¹ H M⁻¹ G added to `k`.
|
||||
fn mixed_responses(&self, m: &MaterialModel, points: &[PointKinematics], k: Option<&mut Matrix>) -> Result<Vec<Response>> {
|
||||
/// The three-field mixed responses τ_iso + J p I with their tangents (small strain:
|
||||
/// s + p I), and the dilatation stiffness Gᵀ M⁻¹ H M⁻¹ G added to `k`.
|
||||
fn mixed_responses(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
points: &[PointKinematics],
|
||||
kin: Kinematics,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
k: Option<&mut Matrix>,
|
||||
) -> Result<Vec<Response>> {
|
||||
let small = kin == Kinematics::Small;
|
||||
let len = m.history_len();
|
||||
let (np, frame) = (self.np(), self.frame());
|
||||
let basis: Vec<[f64; 4]> = points.iter().map(|p| pressure_basis(np, p.x0, &frame)).collect();
|
||||
let mut mm = Matrix::zeros(np);
|
||||
@@ -212,7 +355,7 @@ impl Solid<'_> {
|
||||
if !(th > 0.0 && th.is_finite()) {
|
||||
return Err(inverted(self.ty.kind.name(), th));
|
||||
}
|
||||
let (dp, ddp) = m.volumetric(th).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
|
||||
let (dp, ddp) = m.volumetric(th, small).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
|
||||
for a in 0..np {
|
||||
pressure[a] += p.weight * nb[a] * dp;
|
||||
for b in 0..np {
|
||||
@@ -222,20 +365,28 @@ impl Solid<'_> {
|
||||
}
|
||||
chol.solve(&mut pressure);
|
||||
let mut out = Vec::with_capacity(points.len());
|
||||
for (p, nb) in points.iter().zip(&basis) {
|
||||
for (i, (p, nb)) in points.iter().zip(&basis).enumerate() {
|
||||
let pr: f64 = (0..np).map(|a| nb[a] * pressure[a]).sum();
|
||||
out.push(add_pressure(&m.isochoric(&p.f)?, p.j, pr, 0.0));
|
||||
let (c, u) = (point_history(committed, len, i), updated.get_mut(i * len..(i + 1) * len).unwrap_or_default());
|
||||
out.push(if small {
|
||||
let mut r = m.small_strain(&p.strain, p.temperature, c, u, false)?;
|
||||
r.stress.iter_mut().take(3).for_each(|s| *s += pr);
|
||||
r
|
||||
} else {
|
||||
add_pressure(&m.isochoric(&p.f, p.temperature, c, u)?, p.j, pr, 0.0)
|
||||
});
|
||||
}
|
||||
if let Some(k) = k {
|
||||
// A = M⁻¹ G, np × ndof; K += Aᵀ H A.
|
||||
// A = M⁻¹ G, np × ndof; K += Aᵀ H A. At small strain G = ∫ N_p ∇₀N dV₀.
|
||||
let ndof = k.n;
|
||||
let mut a_cols = vec![vec![0.0; np]; ndof];
|
||||
for (p, nb) in points.iter().zip(&basis) {
|
||||
let jw = p.weight * if small { 1.0 } else { p.j };
|
||||
for (node, g) in p.gradients.iter().enumerate() {
|
||||
for c in 0..3 {
|
||||
if let Some(col) = a_cols.get_mut(3 * node + c) {
|
||||
for (x, n) in col.iter_mut().zip(nb) {
|
||||
*x += p.weight * n * p.j * g[c];
|
||||
*x += jw * n * g[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,32 +413,71 @@ impl Solid<'_> {
|
||||
/// with the material history `committed` at the start of the increment (points × the
|
||||
/// material's history length) written, updated, to `updated`.
|
||||
pub fn finite_strain(&self, m: &MaterialModel, u: &[[f64; 3]], committed: &[f64], updated: &mut [f64], tangent: bool) -> Result<Finite> {
|
||||
Ok(self.finite_points(m, u, committed, updated, tangent)?.0)
|
||||
Ok(self.nonlinear_points(m, u, Kinematics::Finite, None, committed, updated, tangent)?.0)
|
||||
}
|
||||
|
||||
/// The Cauchy stress σ = τ/J and the logarithmic strain at every integration point (at its
|
||||
/// reference position).
|
||||
pub fn finite_strain_stresses(&self, m: &MaterialModel, u: &[[f64; 3]], committed: &[f64]) -> Result<Vec<PointStress>> {
|
||||
/// As [`Self::finite_strain`] with either kinematics and the element's temperatures when
|
||||
/// it has some; with small strain an inelastic material on the linear kinematics,
|
||||
/// f = ∫ Bᵀ σ dV and K = ∫ Bᵀ 𝔻 B dV with the material's consistent tangent 𝔻.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn nonlinear(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 3]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> Result<Finite> {
|
||||
Ok(self.nonlinear_points(m, u, kin, thermal, committed, updated, tangent)?.0)
|
||||
}
|
||||
|
||||
/// The Cauchy stress and the total strain (logarithmic at finite strain, ε at small strain)
|
||||
/// at every integration point (at its reference position).
|
||||
pub fn nonlinear_stresses(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 3]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
) -> Result<Vec<PointStress>> {
|
||||
let mut scratch = committed.to_vec();
|
||||
let (_, points) = self.finite_points(m, u, committed, &mut scratch, false)?;
|
||||
let (_, points) = self.nonlinear_points(m, u, kin, thermal, committed, &mut scratch, false)?;
|
||||
points
|
||||
.iter()
|
||||
.map(|(p, r)| {
|
||||
let strain = logarithmic_strain(&p.f).ok_or_else(|| inverted(self.ty.kind.name(), p.j))?;
|
||||
if kin == Kinematics::Small {
|
||||
return Ok(PointStress { xyz: p.x0, strain: small_strain(&p.total), stress: r.stress });
|
||||
}
|
||||
let strain = logarithmic_strain(&p.total).ok_or_else(|| inverted(self.ty.kind.name(), p.j))?;
|
||||
Ok(PointStress { xyz: p.x0, strain, stress: r.stress.map(|v| v / p.j) })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// One integration point of a 2D element in a nonlinear step: the total deformation gradient,
|
||||
/// det F_m, the reference position and the response.
|
||||
struct PlanePoint {
|
||||
f: Mat3,
|
||||
j: f64,
|
||||
x: [f64; 3],
|
||||
response: Response,
|
||||
}
|
||||
|
||||
impl Plane<'_> {
|
||||
fn check_finite(&self, u: &[[f64; 2]]) -> Result<()> {
|
||||
fn check_nonlinear(&self, u: &[[f64; 2]], kin: Kinematics) -> Result<()> {
|
||||
check_displacements(u, self.ty.nodes)?;
|
||||
if let Theory::PlaneStress { .. } = self.theory {
|
||||
return Err(Error::NotSupported(
|
||||
"plane stress elements in nonlinear steps (the thickness change needs a local iteration; use plane strain, axisymmetric or solid elements)"
|
||||
.into(),
|
||||
));
|
||||
let why = match kin {
|
||||
Kinematics::Finite => "the thickness change needs a local iteration",
|
||||
Kinematics::Small => "σ_zz = 0 needs a local iteration of the inelastic material",
|
||||
};
|
||||
return Err(Error::NotSupported(format!(
|
||||
"plane stress elements in nonlinear steps ({why}; use plane strain, axisymmetric or solid elements)"
|
||||
)));
|
||||
}
|
||||
if self.formulation == Formulation::IncompatibleModes {
|
||||
return Err(Error::NotSupported(format!(
|
||||
@@ -298,24 +488,31 @@ impl Plane<'_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Per point: (dv, F, J, current columns (∂N/∂x, ∂N/∂y, N/r), reference position, response).
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn finite_points(
|
||||
/// Per point: F, J, the reference position, the small strain and the response; the forces
|
||||
/// and the tangent.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn nonlinear_points(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 2]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> Result<(Finite, Vec<(Mat3, f64, [f64; 3], Response)>)> {
|
||||
self.check_finite(u)?;
|
||||
) -> Result<(Finite, Vec<PlanePoint>)> {
|
||||
self.check_nonlinear(u, kin)?;
|
||||
let n = self.ty.nodes;
|
||||
if let Some(t) = thermal {
|
||||
t.check(n)?;
|
||||
}
|
||||
let rule = self.rule().points();
|
||||
let len = check_history(m, rule.len(), committed, updated)?;
|
||||
let ndof = 2 * n;
|
||||
let mut force = vec![0.0; ndof];
|
||||
let mut k = tangent.then(|| Matrix::zeros(ndof));
|
||||
let axisymmetric = self.theory == Theory::Axisymmetric;
|
||||
let small = kin == Kinematics::Small;
|
||||
let mut out = Vec::with_capacity(rule.len());
|
||||
for (i, p) in rule.iter().enumerate() {
|
||||
let at = self.at(p)?;
|
||||
@@ -331,37 +528,47 @@ impl Plane<'_> {
|
||||
f[2][2] += ua[0] * g[2];
|
||||
}
|
||||
}
|
||||
let det2 = f[0][0] * f[1][1] - f[0][1] * f[1][0];
|
||||
let j = det2 * f[2][2];
|
||||
if !(j > 0.0 && j.is_finite() && f[2][2] > 0.0) {
|
||||
return Err(inverted(&format!("2D {}", self.ty.kind.name()), j));
|
||||
}
|
||||
let inv = [[f[1][1] / det2, -f[0][1] / det2], [-f[1][0] / det2, f[0][0] / det2]];
|
||||
let current: Vec<[f64; 3]> =
|
||||
cols.iter().map(|g| [g[0] * inv[0][0] + g[1] * inv[1][0], g[0] * inv[0][1] + g[1] * inv[1][1], g[2] / f[2][2]]).collect();
|
||||
let updated_point = updated.get_mut(i * len..(i + 1) * len).unwrap_or_default();
|
||||
let r = m.response(&f, point_history(committed, len, i), updated_point)?;
|
||||
let heat = Heat::at(thermal, at.shape.values())?;
|
||||
let (j, dv, current, r) = if small {
|
||||
let strain = heat.mechanical_strain(&small_strain(&f));
|
||||
let r = m.small_strain(&strain, heat.temperature, point_history(committed, len, i), updated_point, true)?;
|
||||
(1.0, at.dv, cols.to_vec(), r)
|
||||
} else {
|
||||
let det2 = f[0][0] * f[1][1] - f[0][1] * f[1][0];
|
||||
let j = det2 * f[2][2];
|
||||
if !(j > 0.0 && j.is_finite() && f[2][2] > 0.0) {
|
||||
return Err(inverted(&format!("2D {}", self.ty.kind.name()), j));
|
||||
}
|
||||
let inv = [[f[1][1] / det2, -f[0][1] / det2], [-f[1][0] / det2, f[0][0] / det2]];
|
||||
let current: Vec<[f64; 3]> =
|
||||
cols.iter().map(|g| [g[0] * inv[0][0] + g[1] * inv[1][0], g[0] * inv[0][1] + g[1] * inv[1][1], g[2] / f[2][2]]).collect();
|
||||
let r = m.response(&heat.mechanical(&f), heat.temperature, point_history(committed, len, i), updated_point)?;
|
||||
(j / heat.volume(), at.dv * heat.volume(), current, r)
|
||||
};
|
||||
let tau = [r.stress[0], r.stress[1], r.stress[2], r.stress[3]];
|
||||
for (a, g) in current.iter().enumerate() {
|
||||
if let Some(x) = force.get_mut(2 * a) {
|
||||
*x += at.dv * (g[0] * tau[0] + g[2] * tau[2] + g[1] * tau[3]);
|
||||
*x += dv * (g[0] * tau[0] + g[2] * tau[2] + g[1] * tau[3]);
|
||||
}
|
||||
if let Some(x) = force.get_mut(2 * a + 1) {
|
||||
*x += at.dv * (g[1] * tau[1] + g[0] * tau[3]);
|
||||
*x += dv * (g[1] * tau[1] + g[0] * tau[3]);
|
||||
}
|
||||
}
|
||||
if let Some(k) = k.as_mut() {
|
||||
let c4: [[f64; 4]; 4] = std::array::from_fn(|a| std::array::from_fn(|b| r.tangent[a][b]));
|
||||
crate::plane::accumulate(&mut k.data, ¤t, &c4, at.dv);
|
||||
for (a, ga) in current.iter().enumerate() {
|
||||
for (b, gb) in current.iter().enumerate() {
|
||||
let s = ga[0] * gb[0] * tau[0] + ga[1] * gb[1] * tau[1] + (ga[0] * gb[1] + ga[1] * gb[0]) * tau[3];
|
||||
k.add(2 * a, 2 * b, at.dv * (s + tau[2] * ga[2] * gb[2]));
|
||||
k.add(2 * a + 1, 2 * b + 1, at.dv * s);
|
||||
crate::plane::accumulate(&mut k.data, ¤t, &c4, dv);
|
||||
if !small {
|
||||
for (a, ga) in current.iter().enumerate() {
|
||||
for (b, gb) in current.iter().enumerate() {
|
||||
let s = ga[0] * gb[0] * tau[0] + ga[1] * gb[1] * tau[1] + (ga[0] * gb[1] + ga[1] * gb[0]) * tau[3];
|
||||
k.add(2 * a, 2 * b, dv * (s + tau[2] * ga[2] * gb[2]));
|
||||
k.add(2 * a + 1, 2 * b + 1, dv * s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push((f, j, at.x, r));
|
||||
out.push(PlanePoint { f, j, x: at.x, response: r });
|
||||
}
|
||||
Ok((Finite { force, tangent: k }, out))
|
||||
}
|
||||
@@ -369,19 +576,44 @@ impl Plane<'_> {
|
||||
/// Internal forces (u, v per node) and (with `tangent`) the tangent stiffness at nodal
|
||||
/// displacements `u`, as for [`Solid::finite_strain`].
|
||||
pub fn finite_strain(&self, m: &MaterialModel, u: &[[f64; 2]], committed: &[f64], updated: &mut [f64], tangent: bool) -> Result<Finite> {
|
||||
Ok(self.finite_points(m, u, committed, updated, tangent)?.0)
|
||||
Ok(self.nonlinear_points(m, u, Kinematics::Finite, None, committed, updated, tangent)?.0)
|
||||
}
|
||||
|
||||
/// The Cauchy stress and logarithmic strain at every integration point, as the solid
|
||||
/// As [`Solid::nonlinear`], on the 2D element.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn nonlinear(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 2]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> Result<Finite> {
|
||||
Ok(self.nonlinear_points(m, u, kin, thermal, committed, updated, tangent)?.0)
|
||||
}
|
||||
|
||||
/// The Cauchy stress and the total strain at every integration point, as the solid
|
||||
/// elements' 6-vectors (axisymmetric: rr, axial, hoop, r-axial).
|
||||
pub fn finite_strain_stresses(&self, m: &MaterialModel, u: &[[f64; 2]], committed: &[f64]) -> Result<Vec<PointStress>> {
|
||||
pub fn nonlinear_stresses(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[[f64; 2]],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
) -> Result<Vec<PointStress>> {
|
||||
let mut scratch = committed.to_vec();
|
||||
let (_, points) = self.finite_points(m, u, committed, &mut scratch, false)?;
|
||||
let (_, points) = self.nonlinear_points(m, u, kin, thermal, committed, &mut scratch, false)?;
|
||||
points
|
||||
.iter()
|
||||
.map(|(f, j, x, r)| {
|
||||
let strain = logarithmic_strain(f).ok_or_else(|| inverted(&format!("2D {}", self.ty.kind.name()), *j))?;
|
||||
Ok(PointStress { xyz: *x, strain, stress: r.stress.map(|v| v / j) })
|
||||
.map(|p| {
|
||||
if kin == Kinematics::Small {
|
||||
return Ok(PointStress { xyz: p.x, strain: small_strain(&p.f), stress: p.response.stress });
|
||||
}
|
||||
let strain = logarithmic_strain(&p.f).ok_or_else(|| inverted(&format!("2D {}", self.ty.kind.name()), p.j))?;
|
||||
Ok(PointStress { xyz: p.x, strain, stress: p.response.stress.map(|v| v / p.j) })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -420,20 +652,26 @@ mod tests {
|
||||
|
||||
/// Central differences of the internal forces against the tangent.
|
||||
fn check_tangent(solid: &Solid, m: &MaterialModel, u: &[[f64; 3]]) {
|
||||
let k = solid.finite_strain(m, u, &[], &mut [], true).unwrap().tangent.unwrap();
|
||||
check_tangent_from(solid, m, u, Kinematics::Finite, &[], 1e-6);
|
||||
}
|
||||
|
||||
/// The same from a committed history, with either kinematics.
|
||||
fn check_tangent_from(solid: &Solid, m: &MaterialModel, u: &[[f64; 3]], kin: Kinematics, committed: &[f64], tol: f64) {
|
||||
let mut scratch = committed.to_vec();
|
||||
let k = solid.nonlinear_points(m, u, kin, None, committed, &mut scratch, true).unwrap().0.tangent.unwrap();
|
||||
let scale = k.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let n = 3 * u.len();
|
||||
let h = 1e-6;
|
||||
let h = if kin == Kinematics::Small { 1e-9 } else { 1e-6 };
|
||||
for j in 0..n {
|
||||
let shifted = |s: f64| {
|
||||
let mut v = u.to_vec();
|
||||
v[j / 3][j % 3] += s;
|
||||
solid.finite_strain(m, &v, &[], &mut [], false).unwrap().force
|
||||
solid.nonlinear_points(m, &v, kin, None, committed, &mut committed.to_vec(), false).unwrap().0.force
|
||||
};
|
||||
let (p, q) = (shifted(h), shifted(-h));
|
||||
for i in 0..n {
|
||||
let numeric = (p[i] - q[i]) / (2.0 * h);
|
||||
assert!((numeric - k.get(i, j)).abs() < 1e-6 * scale, "{:?} {i} {j}: {numeric} vs {}", solid.ty.kind, k.get(i, j));
|
||||
assert!((numeric - k.get(i, j)).abs() < tol * scale, "{:?} {kin:?} {i} {j}: {numeric} vs {}", solid.ty.kind, k.get(i, j));
|
||||
}
|
||||
}
|
||||
for i in 0..n {
|
||||
@@ -465,6 +703,10 @@ mod tests {
|
||||
for (kind, x, np) in [(ElementKind::Hex8, &x8, 1), (ElementKind::Hex20, &x20, 4), (ElementKind::Tet10, &x10, 1)] {
|
||||
let hybrid = Solid::new(kind, x, Formulation::Hybrid { pressures: np }).unwrap();
|
||||
check_tangent(&hybrid, &neo, &displacement(x.len(), 0.08));
|
||||
let mr = MaterialModel::Polynomial { name: "Mooney–Rivlin", c: [100.0, 50.0, 0.0, 0.0], bulk: 3000.0 };
|
||||
let yeoh = MaterialModel::Polynomial { name: "Yeoh", c: [150.0, 0.0, -10.0, 2.0], bulk: 3000.0 };
|
||||
check_tangent(&hybrid, &mr, &displacement(x.len(), 0.08));
|
||||
check_tangent(&hybrid, &yeoh, &displacement(x.len(), 0.08));
|
||||
assert!(hybrid.finite_strain(&svk, &displacement(x.len(), 0.0), &[], &mut [], false).is_err(), "SVK has no split");
|
||||
}
|
||||
let im = Solid::new(ElementKind::Hex8, &x8, Formulation::IncompatibleModes).unwrap();
|
||||
@@ -493,6 +735,164 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// J2 plasticity in full and hybrid elements, at finite and small strain: the tangent is
|
||||
/// the derivative of the forces from a plastic history too, and the plastic points are
|
||||
/// really plastic.
|
||||
#[test]
|
||||
fn plastic_tangents_are_the_derivative_of_the_forces() {
|
||||
use crate::elastoplastic::{HISTORY, PLASTIC_STRAIN};
|
||||
use fieldcraft_model::{Elastic, Hardening, Plastic};
|
||||
let j2 = crate::plasticity::J2::from_model(
|
||||
&Elastic::Isotropic { young: 1000.0, poisson: 0.3 },
|
||||
Some(&Plastic::Bilinear { yield_stress: 5.0, hardening_modulus: 50.0, hardening: Hardening::Isotropic }),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let m = MaterialModel::Plastic(std::sync::Arc::new(j2));
|
||||
let (x20, x10) = (hex20(), tet10());
|
||||
let x8: Vec<[f64; 3]> = x20[..8].to_vec();
|
||||
for (kind, x, f) in [
|
||||
(ElementKind::Hex8, &x8, Formulation::Full),
|
||||
(ElementKind::Tet10, &x10, Formulation::Full),
|
||||
(ElementKind::Hex20, &x20, Formulation::Reduced),
|
||||
(ElementKind::Hex8, &x8, Formulation::Hybrid { pressures: 1 }),
|
||||
(ElementKind::Hex20, &x20, Formulation::Hybrid { pressures: 4 }),
|
||||
] {
|
||||
let solid = Solid::new(kind, x, f).unwrap();
|
||||
let points = solid.rule().points().len();
|
||||
for (kin, scale) in [(Kinematics::Finite, 0.05), (Kinematics::Small, 0.05)] {
|
||||
// A first increment from the virgin state, committed, then a second from it.
|
||||
let mut committed = vec![0.0; points * HISTORY];
|
||||
let first = displacement(x.len(), 0.5 * scale);
|
||||
solid.nonlinear_points(&m, &first, kin, None, &vec![0.0; points * HISTORY], &mut committed, false).unwrap();
|
||||
assert!((0..points).all(|p| committed[p * HISTORY + PLASTIC_STRAIN] > 0.0), "{kind:?} {f:?} {kin:?} yields everywhere");
|
||||
check_tangent_from(&solid, &m, &displacement(x.len(), scale), kin, &committed, 1e-6);
|
||||
}
|
||||
}
|
||||
// Incompatible modes have no inelastic form.
|
||||
let im = Solid::new(ElementKind::Hex8, &x8, Formulation::IncompatibleModes).unwrap();
|
||||
assert!(matches!(
|
||||
im.nonlinear(&m, &displacement(8, 0.0), Kinematics::Small, None, &[0.0; 8 * HISTORY], &mut [0.0; 8 * HISTORY], true),
|
||||
Err(Error::NotSupported(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plastic_plane_strain_and_axisymmetric_tangents_are_the_derivative_of_the_forces() {
|
||||
use crate::elastoplastic::{HISTORY, PLASTIC_STRAIN};
|
||||
use fieldcraft_model::{Elastic, Hardening, Plastic};
|
||||
let j2 = crate::plasticity::J2::from_model(
|
||||
&Elastic::Isotropic { young: 1000.0, poisson: 0.3 },
|
||||
Some(&Plastic::Bilinear { yield_stress: 5.0, hardening_modulus: 50.0, hardening: Hardening::Isotropic }),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let m = MaterialModel::Plastic(std::sync::Arc::new(j2));
|
||||
let quad8 = [
|
||||
[1.0, 0.0, 0.0],
|
||||
[2.0, 0.1, 0.0],
|
||||
[2.2, 1.0, 0.0],
|
||||
[0.9, 1.1, 0.0],
|
||||
[1.5, 0.0, 0.0],
|
||||
[2.1, 0.55, 0.0],
|
||||
[1.5, 1.1, 0.0],
|
||||
[0.95, 0.5, 0.0],
|
||||
];
|
||||
for theory in [Theory::PlaneStrain { thickness: 2.0 }, Theory::Axisymmetric] {
|
||||
for (kind, x) in [(ElementKind::Quad8, &quad8[..]), (ElementKind::Tri3, &quad8[..3])] {
|
||||
let plane = Plane::new(kind, x, Formulation::Full, theory).unwrap();
|
||||
let points = plane.rule().points().len();
|
||||
for kin in [Kinematics::Finite, Kinematics::Small] {
|
||||
let u = |scale: f64| -> Vec<[f64; 2]> { displacement(x.len(), scale).iter().map(|v| [v[0], v[1]]).collect() };
|
||||
let mut committed = vec![0.0; points * HISTORY];
|
||||
plane.nonlinear_points(&m, &u(0.03), kin, None, &vec![0.0; points * HISTORY], &mut committed, false).unwrap();
|
||||
assert!((0..points).all(|p| committed[p * HISTORY + PLASTIC_STRAIN] > 0.0), "{theory:?} {kind:?} {kin:?}");
|
||||
let at = u(0.06);
|
||||
let k = plane.nonlinear_points(&m, &at, kin, None, &committed, &mut committed.clone(), true).unwrap().0.tangent.unwrap();
|
||||
let scale = k.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let h = 1e-8;
|
||||
for j in 0..2 * x.len() {
|
||||
let shifted = |s: f64| {
|
||||
let mut v = at.clone();
|
||||
v[j / 2][j % 2] += s;
|
||||
plane.nonlinear_points(&m, &v, kin, None, &committed, &mut committed.clone(), false).unwrap().0.force
|
||||
};
|
||||
let (p, q) = (shifted(h), shifted(-h));
|
||||
for i in 0..2 * x.len() {
|
||||
let numeric = (p[i] - q[i]) / (2.0 * h);
|
||||
assert!(
|
||||
(numeric - k.get(i, j)).abs() < 1e-6 * scale,
|
||||
"{theory:?} {kind:?} {kin:?} {i} {j}: {numeric} vs {}",
|
||||
k.get(i, j)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let stress = Plane::new(ElementKind::Quad4, &quad8[..4], Formulation::Full, Theory::PlaneStress { thickness: 1.0 }).unwrap();
|
||||
assert!(matches!(
|
||||
stress.nonlinear(&m, &[[0.0; 2]; 4], Kinematics::Small, None, &[0.0; 4 * HISTORY], &mut [0.0; 4 * HISTORY], true),
|
||||
Err(Error::NotSupported(_))
|
||||
));
|
||||
}
|
||||
|
||||
/// A temperature field: the tangent is the derivative of the forces (finite and small strain,
|
||||
/// full and hybrid, elastic and plastic), and a free expansion u = (F_θ − I) X of a uniform
|
||||
/// temperature stresses nothing.
|
||||
#[test]
|
||||
fn thermal_tangents_and_free_expansion() {
|
||||
use crate::elastoplastic::HISTORY;
|
||||
use fieldcraft_model::{Elastic, Hardening, Plastic};
|
||||
let j2 = crate::plasticity::J2::from_model(
|
||||
&Elastic::Isotropic { young: 1000.0, poisson: 0.3 },
|
||||
Some(&Plastic::Bilinear { yield_stress: 5.0, hardening_modulus: 50.0, hardening: Hardening::Isotropic }),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let plastic = MaterialModel::Plastic(std::sync::Arc::new(j2));
|
||||
let neo = MaterialModel::NeoHookean { shear: 300.0, bulk: 3000.0 };
|
||||
let x20 = hex20();
|
||||
let field = |n: usize| Temperature {
|
||||
alpha: [1e-3, 2e-3, 1.5e-3],
|
||||
reference: 20.0,
|
||||
nodal: (0..n).map(|i| 20.0 + 40.0 * (i as f64 * 0.37).sin()).collect(),
|
||||
};
|
||||
for f in [Formulation::Full, Formulation::Hybrid { pressures: 4 }] {
|
||||
let solid = Solid::new(ElementKind::Hex20, &x20, f).unwrap();
|
||||
let points = solid.rule().points().len();
|
||||
let t = field(20);
|
||||
for (m, kin, len) in [(&neo, Kinematics::Finite, 0), (&plastic, Kinematics::Finite, HISTORY), (&plastic, Kinematics::Small, HISTORY)] {
|
||||
let u = displacement(20, 0.03);
|
||||
let committed = vec![0.0; points * len];
|
||||
let k = solid.nonlinear(m, &u, kin, Some(&t), &committed, &mut committed.clone(), true).unwrap().tangent.unwrap();
|
||||
let scale = k.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let h = if kin == Kinematics::Small { 1e-9 } else { 1e-7 };
|
||||
for j in 0..60 {
|
||||
let shifted = |s: f64| {
|
||||
let mut v = u.clone();
|
||||
v[j / 3][j % 3] += s;
|
||||
solid.nonlinear(m, &v, kin, Some(&t), &committed, &mut committed.clone(), false).unwrap().force
|
||||
};
|
||||
let (p, q) = (shifted(h), shifted(-h));
|
||||
for i in 0..60 {
|
||||
let numeric = (p[i] - q[i]) / (2.0 * h);
|
||||
assert!((numeric - k.get(i, j)).abs() < 1e-6 * scale, "{f:?} {} {kin:?} {i} {j}: {numeric} vs {}", m.name(), k.get(i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Free expansion by a uniform 150 degrees: no force, no stress.
|
||||
let uniform = Temperature { alpha: [1e-3, 2e-3, 1.5e-3], reference: 20.0, nodal: vec![170.0; 20] };
|
||||
let stretch = uniform.alpha.map(|a| 1.0 + 150.0 * a);
|
||||
let u: Vec<[f64; 3]> = x20.iter().map(|x| std::array::from_fn(|k| (stretch[k] - 1.0) * x[k])).collect();
|
||||
let r = solid.nonlinear(&neo, &u, Kinematics::Finite, Some(&uniform), &[], &mut [], false).unwrap();
|
||||
assert!(r.force.iter().all(|v| v.abs() < 1e-10 * 3000.0), "{f:?}: {:?}", r.force);
|
||||
let s = solid.nonlinear_stresses(&neo, &u, Kinematics::Finite, Some(&uniform), &[]).unwrap();
|
||||
assert!(s.iter().all(|p| p.stress.iter().all(|v| v.abs() < 1e-10 * 3000.0)), "{f:?}");
|
||||
assert!((s[0].strain[1] - stretch[1].ln()).abs() < 1e-12, "total logarithmic strain: {:?}", s[0].strain);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rigid_rotation_strains_and_stresses_nothing() {
|
||||
let x20 = hex20();
|
||||
@@ -501,7 +901,7 @@ mod tests {
|
||||
let neo = MaterialModel::NeoHookean { shear: 300.0, bulk: 3000.0 };
|
||||
for f in [Formulation::Full, Formulation::Hybrid { pressures: 4 }] {
|
||||
let solid = Solid::new(ElementKind::Hex20, &x20, f).unwrap();
|
||||
let s = solid.finite_strain_stresses(&neo, &u, &[]).unwrap();
|
||||
let s = solid.nonlinear_stresses(&neo, &u, Kinematics::Finite, None, &[]).unwrap();
|
||||
assert!(s.iter().all(|p| p.stress.iter().chain(&p.strain).all(|v| v.abs() < 1e-12 * 3000.0)), "{f:?}: {:?}", s[0]);
|
||||
let r = solid.finite_strain(&neo, &u, &[], &mut [], false).unwrap();
|
||||
assert!(r.force.iter().all(|v| v.abs() < 1e-11 * 3000.0));
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
//! the solid kernels evaluate;
|
||||
//! - [`material`]: isotropic and orthotropic elasticity matrices, von Mises stress;
|
||||
//! - [`constitutive`]: the material point of finite-strain analysis (Kirchhoff stress and
|
||||
//! spatial tangent from the deformation gradient, with history): St Venant–Kirchhoff and
|
||||
//! neo-Hookean;
|
||||
//! spatial tangent from the deformation gradient, with history): St Venant–Kirchhoff,
|
||||
//! neo-Hookean and J2 plasticity;
|
||||
//! - [`element`]: the element table, stiffness (full, reduced, incompatible modes), consistent
|
||||
//! and lumped mass, body forces, integration-point stresses and their extrapolation to nodes;
|
||||
//! - [`finite`]: total Lagrangian solid and 2D elements (internal forces, the consistent tangent
|
||||
//! with its geometric part, mixed u–p at finite strain, Cauchy stress and logarithmic strain);
|
||||
//! with its geometric part, mixed u–p at finite strain, Cauchy stress and logarithmic strain),
|
||||
//! and the same with small strain for inelastic materials;
|
||||
//! - [`plane`]: 2D elements (plane stress, plane strain, axisymmetric) on tri3, tri6, quad4 and
|
||||
//! quad8, with two displacement components per node;
|
||||
//! - [`shell`]: flat MITC4 quadrilateral and MITC3 triangle shells with a drilling rotation, six
|
||||
@@ -33,6 +34,9 @@
|
||||
//! shape vectors and element frequency bounds for explicit dynamics;
|
||||
//! - [`rotation`]: incrementally objective stress updates (Hughes–Winget Jaumann, Flanagan–Taylor
|
||||
//! Green–Naghdi);
|
||||
//! - [`elastoplastic`]: J2 plasticity at a material point of implicit analysis, at finite strain
|
||||
//! (Simo's exponential map in the principal logarithmic strains) and at small strain, with the
|
||||
//! consistent tangents, on the radial return of [`plasticity`];
|
||||
//! - [`plasticity`]: J2 radial return with isotropic, kinematic and Johnson–Cook hardening,
|
||||
//! Johnson–Cook damage;
|
||||
//! - [`truss`]: two- and three-node trusses (straight or curved), their stiffness, mass, body
|
||||
@@ -52,6 +56,7 @@ pub mod beam;
|
||||
pub mod constitutive;
|
||||
pub mod dense;
|
||||
pub mod dof;
|
||||
pub mod elastoplastic;
|
||||
pub mod element;
|
||||
pub mod face;
|
||||
pub mod finite;
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
//! [`MIN_FRACTURE_STRAIN`] counts as that (some published sets give negative values at high
|
||||
//! triaxiality, where the point fails at once). The point has failed at D ≥ 1.
|
||||
//!
|
||||
//! **One return map for both solvers.** [`J2::radial_return`] is the return above; explicit
|
||||
//! dynamics calls it through [`J2::update`] on its hypoelastic stress, the implicit material
|
||||
//! point ([`crate::elastoplastic`]) on its trial elastic strain, small or logarithmic, and adds
|
||||
//! the consistent tangent [`J2::tangent`].
|
||||
//!
|
||||
//! The strain increment the update takes is the **mechanical** one: a caller with initial or
|
||||
//! thermal strains subtracts their increment first.
|
||||
//!
|
||||
@@ -98,6 +103,21 @@ impl PointState {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a radial return did: what the consistent tangent needs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Return {
|
||||
/// The equivalent plastic strain increment Δε_p.
|
||||
pub plastic_strain: f64,
|
||||
/// The trial von Mises stress of ξ* = s* − α, q* = √(3/2)‖ξ*‖.
|
||||
pub trial: f64,
|
||||
/// The flow direction ξ*/‖ξ*‖ (tensor components).
|
||||
pub normal: [f64; 6],
|
||||
/// The kinematic hardening modulus H_k.
|
||||
pub kinematic: f64,
|
||||
/// The slope of the flow stress against the plastic strain at the end of the increment.
|
||||
pub slope: f64,
|
||||
}
|
||||
|
||||
/// What one update did.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct Increment {
|
||||
@@ -209,6 +229,11 @@ impl J2 {
|
||||
Ok(J2 { shear, bulk, flow, damage, heating })
|
||||
}
|
||||
|
||||
/// Whether the flow stress depends on the temperature (Johnson–Cook softening).
|
||||
pub fn is_temperature_dependent(&self) -> bool {
|
||||
matches!(self.flow, Some(Flow::JohnsonCook { .. }))
|
||||
}
|
||||
|
||||
/// The temperature a point starts at.
|
||||
pub fn initial_temperature(&self) -> f64 {
|
||||
match self.flow {
|
||||
@@ -232,27 +257,11 @@ impl J2 {
|
||||
}
|
||||
let pressure = mean + self.bulk * vol;
|
||||
let mut out = Increment::default();
|
||||
if let Some(flow) = &self.flow {
|
||||
let xi: [f64; 6] = std::array::from_fn(|k| trial[k] - state.back[k]);
|
||||
let norm = norm2(&xi).sqrt();
|
||||
let q = (1.5f64).sqrt() * norm;
|
||||
let (yield_now, _) = flow.stress(state.plastic_strain, 0.0, state.temperature, dt);
|
||||
if q > yield_now && norm > 0.0 {
|
||||
let hk = flow.kinematic();
|
||||
let de = self.plastic_increment(flow, state, q, yield_now, hk, dt);
|
||||
let n: [f64; 6] = xi.map(|x| x / norm);
|
||||
let radial = g2 * (1.5f64).sqrt() * de;
|
||||
let shift = (2.0f64 / 3.0).sqrt() * hk * de;
|
||||
for k in 0..6 {
|
||||
trial[k] -= radial * n[k];
|
||||
state.back[k] += shift * n[k];
|
||||
}
|
||||
state.plastic_strain += de;
|
||||
let q_new = q - (3.0 * self.shear + hk) * de;
|
||||
out = Increment { plastic_strain: de, plastic_work: q_new * de };
|
||||
if self.heating > 0.0 && density > 0.0 {
|
||||
state.temperature += self.heating * out.plastic_work / density;
|
||||
}
|
||||
if let Some(r) = self.radial_return(state, &mut trial, dt) {
|
||||
let q_new = r.trial - (3.0 * self.shear + r.kinematic) * r.plastic_strain;
|
||||
out = Increment { plastic_strain: r.plastic_strain, plastic_work: q_new * r.plastic_strain };
|
||||
if self.heating > 0.0 && density > 0.0 {
|
||||
state.temperature += self.heating * out.plastic_work / density;
|
||||
}
|
||||
}
|
||||
for k in 0..3 {
|
||||
@@ -273,6 +282,68 @@ impl J2 {
|
||||
out
|
||||
}
|
||||
|
||||
/// The radial return of a trial deviator `trial` (tensor components) from `state`: when the
|
||||
/// trial von Mises stress exceeds the yield stress, `trial` is moved back to the yield
|
||||
/// surface, the back stress and the equivalent plastic strain of `state` are advanced, and
|
||||
/// what the consistent tangent needs is returned; `None` when the point stays elastic.
|
||||
pub fn radial_return(&self, state: &mut PointState, trial: &mut [f64; 6], dt: f64) -> Option<Return> {
|
||||
let flow = self.flow.as_ref()?;
|
||||
let xi: [f64; 6] = std::array::from_fn(|k| trial[k] - state.back[k]);
|
||||
let norm = norm2(&xi).sqrt();
|
||||
let q = (1.5f64).sqrt() * norm;
|
||||
let (yield_now, _) = flow.stress(state.plastic_strain, 0.0, state.temperature, dt);
|
||||
if !(q > yield_now && norm > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let hk = flow.kinematic();
|
||||
let de = self.plastic_increment(flow, state, q, yield_now, hk, dt);
|
||||
let n: [f64; 6] = xi.map(|x| x / norm);
|
||||
let radial = 2.0 * self.shear * (1.5f64).sqrt() * de;
|
||||
let shift = (2.0f64 / 3.0).sqrt() * hk * de;
|
||||
for k in 0..6 {
|
||||
trial[k] -= radial * n[k];
|
||||
state.back[k] += shift * n[k];
|
||||
}
|
||||
let rate = if dt > 0.0 { de / dt } else { 0.0 };
|
||||
let (_, slope) = flow.stress(state.plastic_strain + de, rate, state.temperature, dt);
|
||||
state.plastic_strain += de;
|
||||
Some(Return { plastic_strain: de, trial: q, normal: n, kinematic: hk, slope })
|
||||
}
|
||||
|
||||
/// The consistent (algorithmic) tangent of the return map, dσ/dε of the stress after the
|
||||
/// update against the total strain of the increment (engineering-shear columns): the
|
||||
/// elasticity K I ⊗ I + 2G ℙ while elastic, and after a return
|
||||
/// K I ⊗ I + 2G θ ℙ − 2G θ̄ n ⊗ n with θ = 1 − 3G Δε_p / q*, θ̄ = 1/(1 + H/3G) − (1 − θ) and
|
||||
/// H the slope of the flow stress plus the kinematic modulus at the end of the increment
|
||||
/// (Simo & Hughes 1998, Box 3.2, eq. 3.3.10, in the equivalent plastic strain). `bulk`
|
||||
/// leaves out the volumetric part K I ⊗ I when false (the isochoric part a mixed element
|
||||
/// takes). `None` when 3G + H is not positive: the point softens faster than it unloads
|
||||
/// elastically and has no tangent.
|
||||
pub fn tangent(&self, r: Option<&Return>, bulk: bool) -> Option<[[f64; 6]; 6]> {
|
||||
let g = self.shear;
|
||||
let (theta, theta_bar, n) = match r {
|
||||
None => (1.0, 0.0, [0.0; 6]),
|
||||
Some(r) => {
|
||||
let stiff = 3.0 * g + r.slope + r.kinematic;
|
||||
if !(stiff > 0.0 && stiff.is_finite() && r.trial > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let theta = 1.0 - 3.0 * g * r.plastic_strain / r.trial;
|
||||
(theta, 3.0 * g / stiff - (1.0 - theta), r.normal)
|
||||
}
|
||||
};
|
||||
let k = if bulk { self.bulk } else { 0.0 };
|
||||
let identity = crate::constitutive::identity4();
|
||||
let mut d = [[0.0; 6]; 6];
|
||||
for a in 0..6 {
|
||||
for b in 0..6 {
|
||||
let (ia, ib) = (if a < 3 { 1.0 } else { 0.0 }, if b < 3 { 1.0 } else { 0.0 });
|
||||
d[a][b] = k * ia * ib + 2.0 * g * theta * (identity[a][b] - ia * ib / 3.0) - 2.0 * g * theta_bar * n[a] * n[b];
|
||||
}
|
||||
}
|
||||
Some(d)
|
||||
}
|
||||
|
||||
/// The plastic strain increment of a trial state that yields.
|
||||
fn plastic_increment(&self, flow: &Flow, state: &PointState, q: f64, yield_now: f64, hk: f64, dt: f64) -> f64 {
|
||||
let g3 = 3.0 * self.shear;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -62,8 +62,8 @@ pub use mesh::{Element, ElementEdge, ElementFace, ElementKind, Node, check_coord
|
||||
pub use sections::{BeamProfile, Extent, Formulation, Integration, Section, SectionKind};
|
||||
pub use sets::Set;
|
||||
pub use steps::{
|
||||
BulkViscosity, ExplicitDynamic, Hourglass, HourglassKind, InitialVelocity, MAX_NONLINEAR_INCREMENTS, MAX_NONLINEAR_ITERATIONS, NonlinearStatic,
|
||||
OutputVariable, Procedure, RigidWall, Step, StressRate, Tolerances, Transient,
|
||||
BulkViscosity, ExplicitDynamic, Hourglass, HourglassKind, InitialVelocity, MAX_LOAD_PATH_POINTS, MAX_NONLINEAR_INCREMENTS,
|
||||
MAX_NONLINEAR_ITERATIONS, NonlinearStatic, OutputVariable, Procedure, RigidWall, Step, StressRate, Tolerances, Transient,
|
||||
};
|
||||
pub use units::UnitSystem;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Materials: linear elasticity, mass density, fluid behaviour, thermal properties (conductivity,
|
||||
//! specific heat, expansion), and the inelastic behaviours of
|
||||
//! explicit dynamics: von Mises (J2) plasticity with a bilinear, tabulated or Johnson–Cook flow stress,
|
||||
//! Johnson–Cook damage, and element erosion criteria.
|
||||
//! specific heat, expansion), and the inelastic behaviours: von Mises (J2) plasticity with a
|
||||
//! bilinear, tabulated or Johnson–Cook flow stress (explicit dynamics and nonlinear static
|
||||
//! steps), Johnson–Cook damage and element erosion criteria (explicit dynamics).
|
||||
//!
|
||||
//! The records are solver-neutral and named for what they describe, never after a product's
|
||||
//! keyword or material number; the deck writers map them to each format.
|
||||
@@ -88,6 +88,16 @@ pub enum Hyperelastic {
|
||||
/// reference volume W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)², with Ī₁ = J^(−2/3) tr C (C₁₀ = μ/2,
|
||||
/// D₁ = 2/κ in the usual polynomial notation).
|
||||
NeoHookean {},
|
||||
/// Mooney–Rivlin (M. Mooney, J. Appl. Phys. 11:582–592, 1940; R. S. Rivlin, Phil. Trans. R.
|
||||
/// Soc. A 241:379–397, 1948), decoupled: W = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + κ/2 (J − 1)². The
|
||||
/// small-strain shear modulus 2 (C₁₀ + C₀₁) is the elastic μ, so `c01` sets the second
|
||||
/// invariant's share and C₁₀ = μ/2 − C₀₁ (0 ≤ C₀₁ ≤ μ/2, both constants non-negative).
|
||||
MooneyRivlin { c01: f64 },
|
||||
/// Yeoh (O. H. Yeoh, Rubber Chem. Technol. 66(5):754–771, 1993), decoupled:
|
||||
/// W = C₁₀ (Ī₁ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ + κ/2 (J − 1)², with C₁₀ = μ/2 from the
|
||||
/// elastic constants (the small-strain shear modulus); `c20` (often negative) and `c30`
|
||||
/// shape the large-strain response.
|
||||
Yeoh { c20: f64, c30: f64 },
|
||||
}
|
||||
|
||||
impl Hyperelastic {
|
||||
@@ -101,8 +111,39 @@ impl Hyperelastic {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Hyperelastic::NeoHookean {} => "neo-Hookean",
|
||||
Hyperelastic::MooneyRivlin { .. } => "Mooney–Rivlin",
|
||||
Hyperelastic::Yeoh { .. } => "Yeoh",
|
||||
}
|
||||
}
|
||||
|
||||
/// The polynomial constants (C₁₀, C₀₁, C₂₀, C₃₀) for the shear modulus μ.
|
||||
pub fn constants(&self, shear: f64) -> [f64; 4] {
|
||||
match *self {
|
||||
Hyperelastic::NeoHookean {} => [shear / 2.0, 0.0, 0.0, 0.0],
|
||||
Hyperelastic::MooneyRivlin { c01 } => [shear / 2.0 - c01, c01, 0.0, 0.0],
|
||||
Hyperelastic::Yeoh { c20, c30 } => [shear / 2.0, 0.0, c20, c30],
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the option's constants against the elastic shear modulus μ.
|
||||
pub fn validate(&self, shear: f64) -> Result<()> {
|
||||
match *self {
|
||||
Hyperelastic::NeoHookean {} => {}
|
||||
Hyperelastic::MooneyRivlin { c01 } => {
|
||||
if !(c01 >= 0.0 && c01 <= shear / 2.0) {
|
||||
return Err(Error::Invalid(format!(
|
||||
"the Mooney–Rivlin C01 = {c01} must be within [0, μ/2 = {}] (C10 = μ/2 − C01 and C01 both non-negative)",
|
||||
shear / 2.0
|
||||
)));
|
||||
}
|
||||
}
|
||||
Hyperelastic::Yeoh { c20, c30 } => {
|
||||
finite("Yeoh C20", c20)?;
|
||||
finite("Yeoh C30", c30)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fluid behaviour.
|
||||
@@ -372,8 +413,8 @@ pub struct Material {
|
||||
/// How it flows, for fluid domains (needs a density).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fluid: Option<Fluid>,
|
||||
/// Von Mises plasticity (explicit dynamics; the linear implicit solver uses the elastic
|
||||
/// constants alone).
|
||||
/// Von Mises plasticity (explicit dynamics and nonlinear static steps; the linear solvers use
|
||||
/// the elastic constants alone).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub plastic: Option<Plastic>,
|
||||
/// Damage to failure (explicit dynamics: a failed element erodes).
|
||||
@@ -407,6 +448,9 @@ impl Material {
|
||||
if self.plastic.is_some() {
|
||||
return Err(Error::Invalid(format!("plasticity of a {} hyperelastic material is not supported yet", h.name())));
|
||||
}
|
||||
if let Some(Elastic::Isotropic { young, poisson }) = self.elastic {
|
||||
h.validate(Hyperelastic::moduli(young, poisson).0)?;
|
||||
}
|
||||
}
|
||||
if let Some(d) = self.density {
|
||||
positive("density", d)?;
|
||||
@@ -607,7 +651,28 @@ mod tests {
|
||||
let ortho = Elastic::EngineeringConstants { young: [1.0; 3], poisson: [0.0; 3], shear: [1.0; 3] };
|
||||
assert!(Material { elastic: Some(ortho), ..alone }.validate().is_err());
|
||||
let plastic = Plastic::Bilinear { yield_stress: 1.0, hardening_modulus: 0.0, hardening: Hardening::Isotropic };
|
||||
assert!(Material { plastic: Some(plastic), ..m }.validate().is_err());
|
||||
assert!(Material { plastic: Some(plastic), ..m.clone() }.validate().is_err());
|
||||
// Mooney–Rivlin and Yeoh: μ = E/(2(1 + ν)) = 1.00671 here, so C01 is within [0, μ/2].
|
||||
let mr: Material = serde_json::from_value(serde_json::json!({
|
||||
"elastic": {"type": "isotropic", "young": 3.0, "poisson": 0.49}, "hyperelastic": {"type": "mooney_rivlin", "c01": 0.1}
|
||||
}))
|
||||
.unwrap();
|
||||
mr.validate().unwrap();
|
||||
let mu = Hyperelastic::moduli(3.0, 0.49).0;
|
||||
let c = Hyperelastic::MooneyRivlin { c01: 0.1 }.constants(mu);
|
||||
assert!((2.0 * (c[0] + c[1]) - mu).abs() < 1e-15 && c[1] == 0.1);
|
||||
let yeoh = Hyperelastic::Yeoh { c20: -0.01, c30: 0.001 };
|
||||
assert_eq!(yeoh.constants(mu), [mu / 2.0, 0.0, -0.01, 0.001]);
|
||||
assert!(Material { hyperelastic: Some(yeoh), ..m.clone() }.validate().is_ok());
|
||||
for bad in [
|
||||
Hyperelastic::MooneyRivlin { c01: -0.1 },
|
||||
Hyperelastic::MooneyRivlin { c01: 0.6 },
|
||||
Hyperelastic::MooneyRivlin { c01: f64::NAN },
|
||||
Hyperelastic::Yeoh { c20: f64::INFINITY, c30: 0.0 },
|
||||
Hyperelastic::Yeoh { c20: 0.0, c30: f64::NAN },
|
||||
] {
|
||||
assert!(Material { hyperelastic: Some(bad), ..m.clone() }.validate().is_err(), "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -87,9 +87,9 @@ pub enum Procedure {
|
||||
/// together: every boundary condition and load of the step acts.
|
||||
CoupledTemperatureDisplacement {},
|
||||
/// Static equilibrium followed in load increments with Newton–Raphson iterations: large
|
||||
/// displacements, rotations and strains (`nonlinear_geometry`), hyperelastic materials and
|
||||
/// follower pressure. Loads and prescribed displacements grow in proportion to the step's
|
||||
/// load factor, from 0 to 1.
|
||||
/// displacements, rotations and strains (`nonlinear_geometry`), hyperelastic and plastic
|
||||
/// materials and follower pressure. Loads and prescribed displacements are the step's load
|
||||
/// factor times their values: a ramp from 0 to 1, or the step's load path.
|
||||
NonlinearStatic(NonlinearStatic),
|
||||
}
|
||||
|
||||
@@ -130,8 +130,18 @@ pub struct NonlinearStatic {
|
||||
/// A line search along each Newton correction (off by default: full Newton steps converge
|
||||
/// best for elastic large-rotation problems; a search helps where the material softens).
|
||||
pub line_search: bool,
|
||||
/// The load factor against the step fraction: piecewise linear through these
|
||||
/// `[fraction, factor]` points, from `[0, 0]` to fraction 1 (`None`: the linear ramp, the
|
||||
/// factor equal to the fraction). Loads and prescribed displacements are the factor times
|
||||
/// their values, so a path that comes down unloads and one that turns negative reverses
|
||||
/// them (cyclic loading). Increments end at every point of the path.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub load_path: Option<Vec<[f64; 2]>>,
|
||||
}
|
||||
|
||||
/// Most points of a nonlinear step's load path.
|
||||
pub const MAX_LOAD_PATH_POINTS: usize = 1000;
|
||||
|
||||
/// When an increment's Newton iterations have converged: all three hold.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
@@ -162,6 +172,7 @@ impl Default for NonlinearStatic {
|
||||
max_iterations: 16,
|
||||
tolerances: Tolerances::default(),
|
||||
line_search: false,
|
||||
load_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,8 +203,41 @@ impl NonlinearStatic {
|
||||
return Err(Error::Invalid(format!("the {what} tolerance is in (0, 1), got {v}")));
|
||||
}
|
||||
}
|
||||
if let Some(path) = &self.load_path {
|
||||
if !(2..=MAX_LOAD_PATH_POINTS).contains(&path.len()) {
|
||||
return Err(Error::Invalid(format!("a load path has 2 to {MAX_LOAD_PATH_POINTS} points, got {}", path.len())));
|
||||
}
|
||||
if path.first() != Some(&[0.0, 0.0]) || path.last().map(|p| p[0]) != Some(1.0) {
|
||||
return Err(Error::Invalid("a load path starts at [0, 0] (the unloaded start of the step) and ends at fraction 1".into()));
|
||||
}
|
||||
if path.iter().flatten().any(|v| !v.is_finite()) {
|
||||
return Err(Error::Invalid("the points of a load path must be finite".into()));
|
||||
}
|
||||
if path.windows(2).any(|w| w.get(1).map(|b| b[0]) <= w.first().map(|a| a[0])) {
|
||||
return Err(Error::Invalid("the fractions of a load path must increase".into()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The load factor at step fraction `t` (in [0, 1]).
|
||||
pub fn load_factor(&self, t: f64) -> f64 {
|
||||
let Some(path) = &self.load_path else { return t };
|
||||
for w in path.windows(2) {
|
||||
if let [[t0, f0], [t1, f1]] = *w
|
||||
&& t <= t1
|
||||
{
|
||||
return if t1 > t0 { f0 + (f1 - f0) * ((t - t0) / (t1 - t0)).clamp(0.0, 1.0) } else { f1 };
|
||||
}
|
||||
}
|
||||
path.last().map_or(t, |p| p[1])
|
||||
}
|
||||
|
||||
/// The first point of the load path after fraction `t` (1 without a path), where an
|
||||
/// increment from `t` must end.
|
||||
pub fn next_turn(&self, t: f64) -> f64 {
|
||||
self.load_path.as_ref().and_then(|path| path.iter().map(|p| p[0]).find(|f| *f > t * (1.0 + 1e-12) + 1e-12)).unwrap_or(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Most time increments of a transient heat transfer step.
|
||||
@@ -900,6 +944,24 @@ mod tests {
|
||||
assert!(Procedure::NonlinearStatic(b.clone()).validate().is_err(), "{b:?}");
|
||||
}
|
||||
assert!(serde_json::from_value::<Procedure>(serde_json::json!({"type": "nonlinear_static", "nlgeom": true})).is_err());
|
||||
// A load path: up, down through zero, back to zero.
|
||||
let cyclic = NonlinearStatic { load_path: Some(vec![[0.0, 0.0], [0.25, 1.0], [0.75, -1.0], [1.0, 0.0]]), ..base.clone() };
|
||||
assert!(Procedure::NonlinearStatic(cyclic.clone()).validate().is_ok());
|
||||
for (t, f) in [(0.0, 0.0), (0.125, 0.5), (0.25, 1.0), (0.5, 0.0), (0.75, -1.0), (0.875, -0.5), (1.0, 0.0)] {
|
||||
assert!((cyclic.load_factor(t) - f).abs() < 1e-15, "{t}: {}", cyclic.load_factor(t));
|
||||
}
|
||||
assert_eq!((cyclic.next_turn(0.0), cyclic.next_turn(0.25), cyclic.next_turn(0.3), cyclic.next_turn(0.75)), (0.25, 0.75, 0.75, 1.0));
|
||||
assert_eq!((base.load_factor(0.3), base.next_turn(0.3)), (0.3, 1.0));
|
||||
for path in [
|
||||
vec![[0.0, 0.0]],
|
||||
vec![[0.0, 0.5], [1.0, 1.0]],
|
||||
vec![[0.0, 0.0], [0.9, 1.0]],
|
||||
vec![[0.0, 0.0], [0.5, 1.0], [0.5, 2.0], [1.0, 0.0]],
|
||||
vec![[0.0, 0.0], [0.5, f64::NAN], [1.0, 0.0]],
|
||||
vec![[0.0, 0.0]; MAX_LOAD_PATH_POINTS + 1],
|
||||
] {
|
||||
assert!(Procedure::NonlinearStatic(NonlinearStatic { load_path: Some(path.clone()), ..base.clone() }).validate().is_err(), "{path:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ pub struct ElementStress {
|
||||
pub points: Vec<PointStress>,
|
||||
/// Extrapolated to the element's nodes, in its node order.
|
||||
pub nodal: Vec<[f64; 6]>,
|
||||
/// The equivalent plastic strain at the integration points (nonlinear steps with a plastic
|
||||
/// material; empty otherwise).
|
||||
pub plastic_strain: Vec<f64>,
|
||||
}
|
||||
|
||||
/// The motion and reaction of a coupled reference point: translations along X, Y, Z, then
|
||||
@@ -350,9 +353,9 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
// loads.
|
||||
let (field, temperature) = match step.procedure {
|
||||
Procedure::CoupledTemperatureDisplacement {} => {
|
||||
let (labels, t) = crate::heat::temperatures(model, &resolved, step, limits)?;
|
||||
let at = |l: &u32| labels.binary_search(l).ok().and_then(|i| t.get(i)).copied();
|
||||
(Some(mesh.labels.iter().map(at).collect::<Vec<_>>()), Some(mesh.labels.iter().map(|l| at(l).unwrap_or(0.0)).collect()))
|
||||
let field = crate::heat::temperatures_at(model, &resolved, step, limits, &mesh.labels)?;
|
||||
let temperature = field.iter().map(|t| t.unwrap_or(0.0)).collect();
|
||||
(Some(field), Some(temperature))
|
||||
}
|
||||
_ => (temperature_loads(model, &resolved, &mesh, step)?, None),
|
||||
};
|
||||
@@ -431,7 +434,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
elements.push(ElementStress { label: e.label, kind: e.kind, points, nodal });
|
||||
elements.push(ElementStress { label: e.label, kind: e.kind, points, nodal, plastic_strain: Vec::new() });
|
||||
}
|
||||
for t in &mesh.trusses {
|
||||
let truss = t.truss()?;
|
||||
@@ -448,7 +451,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
elements.push(ElementStress { label: t.label, kind: t.kind, points, nodal });
|
||||
elements.push(ElementStress { label: t.label, kind: t.kind, points, nodal, plastic_strain: Vec::new() });
|
||||
}
|
||||
elements.sort_by_key(|e| e.label);
|
||||
let (mut shells, mut beams) = (Vec::new(), Vec::new());
|
||||
|
||||
@@ -414,6 +414,13 @@ pub(crate) fn temperatures(model: &Model, resolved: &Resolved, step: &Step, limi
|
||||
Ok((s.mesh.labels, t))
|
||||
}
|
||||
|
||||
/// The steady temperatures of a step's thermal conditions at the nodes `labels` (sorted), `None`
|
||||
/// at the nodes conduction does not reach.
|
||||
pub(crate) fn temperatures_at(model: &Model, resolved: &Resolved, step: &Step, limits: &Limits, labels: &[u32]) -> Result<Vec<Option<f64>>> {
|
||||
let (solved, t) = temperatures(model, resolved, step, limits)?;
|
||||
Ok(labels.iter().map(|l| solved.binary_search(l).ok().and_then(|i| t.get(i)).copied()).collect())
|
||||
}
|
||||
|
||||
/// Solve a heat transfer step: the step named `step`, or the model's only one.
|
||||
pub fn solve_heat(model: &Model, step: Option<&str>, limits: &Limits) -> Result<HeatResult> {
|
||||
let resolved = model.resolve()?;
|
||||
|
||||
+262
-50
@@ -25,7 +25,9 @@
|
||||
//! for the solution of nonlinear finite element equations*, CMAME 22:59–85, 1980);
|
||||
//!
|
||||
//! or when the force residual is a thousandth of its tolerance (round-off: no correction can
|
||||
//! improve it).
|
||||
//! improve it). With path-dependent materials, an increment that starts within the force
|
||||
//! tolerance (a predictor that already solved it) waives the energy criterion, whose reference
|
||||
//! would be round-off.
|
||||
//!
|
||||
//! **Automatic incrementation.** Each increment starts from the linear extrapolation of the
|
||||
//! last converged one (the free motion scaled by the ratio of their sizes), which keeps the
|
||||
@@ -50,10 +52,31 @@
|
||||
//!
|
||||
//! **Elements.** Solids (tet4, tet10, wedge6, hex8, hex20 full and reduced, hybrid hex8, hex20
|
||||
//! and tet10) and 2D plane strain and axisymmetric elements, total Lagrangian
|
||||
//! ([`fieldcraft_fem::finite`]); without nonlinear geometry the elements are the linear ones
|
||||
//! (every material is elastic today, so such a step is the linear solution reached by Newton).
|
||||
//! ([`fieldcraft_fem::finite`]); without nonlinear geometry an element of a plastic material
|
||||
//! takes the same loops with small strain, and an elastic one its linear stiffness (K u).
|
||||
//! Springs act along their fixed directions. Trusses, shells and beams (large rotations), plane
|
||||
//! stress, incompatible modes, temperature and centrifugal loads are refused by name.
|
||||
//! stress, incompatible modes (with nonlinear geometry, or with an inelastic material) and
|
||||
//! centrifugal loads are refused by name.
|
||||
//!
|
||||
//! **Thermal strain.** The step's temperatures are its temperature loads, or, when it has
|
||||
//! temperature boundary conditions or heat loads, their steady conduction solved first (one-way
|
||||
//! coupling, as the linear coupled temperature–displacement step). They ramp with the load
|
||||
//! factor from each material's reference temperature, ΔT(λ) = λ (T − T_ref). With nonlinear
|
||||
//! geometry the deformation gradient splits multiplicatively, F = F_m F_θ with
|
||||
//! F_θ = diag(1 + αᵢ ΔT), the material sees F_m and the stress is J_θ τ_m per reference volume
|
||||
//! (fem's `Temperature`); with small strain the split is additive, ε_m = ε − α ΔT; elements on
|
||||
//! their linear stiffness take the equivalent nodal forces, a dead load. A Johnson–Cook material
|
||||
//! softens at the point's temperature.
|
||||
//!
|
||||
//! **Plasticity** (`Material::plastic`, J2 with the radial return explicit dynamics uses,
|
||||
//! [`fieldcraft_fem::elastoplastic`]): Simo's exponential return map at finite strain, the
|
||||
//! additive radial return at small strain, both with their consistent tangents, so Newton keeps
|
||||
//! its quadratic rate. The plastic history is committed when an increment converges and
|
||||
//! discarded when it is cut back. A static step has no time, so the Johnson–Cook strain-rate term
|
||||
//! is refused (C = 0 is the rate-independent law), and kinematic hardening at finite strain is
|
||||
//! refused (its back stress breaks the principal-frame return); damage, erosion and adiabatic
|
||||
//! heating act in explicit dynamics only, with a warning. Results add the equivalent plastic
|
||||
//! strain at the integration points.
|
||||
//! Couplings stay the linear (small-rotation) constraints of the linear solver, with a warning.
|
||||
//!
|
||||
//! Element forces and tangents are computed in parallel and added in label order, so results are
|
||||
@@ -62,9 +85,11 @@
|
||||
use fieldcraft_fem::constitutive::MaterialModel;
|
||||
use fieldcraft_fem::dense::Matrix;
|
||||
use fieldcraft_fem::element::{Extrapolation, extrapolation};
|
||||
use fieldcraft_fem::finite::{Kinematics, Temperature};
|
||||
use fieldcraft_fem::plane::Theory;
|
||||
use fieldcraft_fem::quadrature::Rule;
|
||||
use fieldcraft_fem::{DofKind, DofSet, ElementType, PointStress, face, von_mises};
|
||||
use fieldcraft_model::{Extent, Load, Model, NonlinearStatic, Procedure, Step};
|
||||
use fieldcraft_model::{BoundaryCondition, Extent, Hardening, Load, Model, NonlinearStatic, Plastic, Procedure, SectionKind, Step};
|
||||
use fieldcraft_parallel as parallel;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -75,7 +100,7 @@ use crate::analysis::{
|
||||
use crate::cholesky::FactorError;
|
||||
use crate::constraints::Reduction;
|
||||
use crate::sparse::{Csr, add_block};
|
||||
use crate::system::{Mesh, inelastic_warnings, loads, translations};
|
||||
use crate::system::{Mesh, add_element_thermal_loads, loads, temperature_loads, translations};
|
||||
use crate::{Error, Limits, Result};
|
||||
|
||||
/// One Newton iteration of an increment.
|
||||
@@ -179,6 +204,9 @@ struct Problem<'a> {
|
||||
element_kinds: Vec<DofSet>,
|
||||
/// Each element's material model.
|
||||
materials: Vec<MaterialModel>,
|
||||
/// The step's temperature field at λ = 1, per mesh node (`None` where the field leaves a
|
||||
/// node at its material's reference temperature); `None` without one.
|
||||
field: Option<Vec<Option<f64>>>,
|
||||
/// The tangent's pattern, zero.
|
||||
pattern: Csr,
|
||||
}
|
||||
@@ -236,21 +264,45 @@ impl Problem<'_> {
|
||||
self.element_dofs.get(e).map(|dofs| dofs.iter().map(|d| u.get(*d).copied().unwrap_or(0.0)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// How element `e` is evaluated: total Lagrangian with nonlinear geometry, with small
|
||||
/// strain when its material is inelastic, and by its linear stiffness otherwise (`None`).
|
||||
fn kinematics(&self, e: usize) -> Option<Kinematics> {
|
||||
if self.finite { Some(Kinematics::Finite) } else { self.materials.get(e).filter(|m| m.is_path_dependent()).map(|_| Kinematics::Small) }
|
||||
}
|
||||
|
||||
/// The temperatures of element `e` at load factor λ, which ramps them from the reference
|
||||
/// temperature to the field's (J2 Johnson–Cook softening and thermal strain): the
|
||||
/// material's expansion and reference temperature, or no expansion from the Johnson–Cook
|
||||
/// room temperature; `None` without a field or for a material neither affects.
|
||||
fn temperature(&self, e: usize, lambda: f64) -> Option<Temperature> {
|
||||
let field = self.field.as_ref()?;
|
||||
let el = self.mesh.elements.get(e)?;
|
||||
let softens = el.plastic.as_ref().is_some_and(|j2| j2.is_temperature_dependent());
|
||||
let (alpha, reference) = match el.expansion {
|
||||
Some(x) => x,
|
||||
None if softens => ([0.0; 3], el.plastic.as_ref().map_or(0.0, |j2| j2.initial_temperature())),
|
||||
None => return None,
|
||||
};
|
||||
let nodal: Vec<f64> =
|
||||
el.nodes.iter().map(|n| field.get(*n).copied().flatten().map_or(reference, |t| reference + lambda * (t - reference))).collect();
|
||||
(softens || nodal.iter().any(|t| *t != reference)).then_some(Temperature { alpha, reference, nodal })
|
||||
}
|
||||
|
||||
/// Internal forces and the tangent of element `e` at displacements `u` (all degrees of
|
||||
/// freedom).
|
||||
fn element(&self, e: usize, u: &[f64], tangent: bool, committed: &[f64]) -> Result<(Vec<f64>, Option<Matrix>, Vec<f64>)> {
|
||||
/// freedom) and load factor λ.
|
||||
fn element(&self, e: usize, u: &[f64], lambda: f64, tangent: bool, committed: &[f64]) -> Result<(Vec<f64>, Option<Matrix>, Vec<f64>)> {
|
||||
let el = self.mesh.elements.get(e).ok_or_else(|| Error::Internal("element index".into()))?;
|
||||
let at = |err| Error::element(el.label, err);
|
||||
let ue = self.element_vector(e, u);
|
||||
let kernel = el.kernel()?;
|
||||
if !self.finite {
|
||||
let Some(kin) = self.kinematics(e) else {
|
||||
let k = kernel.stiffness(&el.d).map_err(at)?;
|
||||
let f = k.mul_vec(&ue);
|
||||
return Ok((f, tangent.then_some(k), committed.to_vec()));
|
||||
}
|
||||
};
|
||||
let material = self.materials.get(e).ok_or_else(|| Error::Internal("element material".into()))?;
|
||||
let mut updated = committed.to_vec();
|
||||
let r = kernel.finite_strain(material, &ue, committed, &mut updated, tangent).map_err(at)?;
|
||||
let r = kernel.nonlinear(material, &ue, kin, self.temperature(e, lambda).as_ref(), committed, &mut updated, tangent).map_err(at)?;
|
||||
Ok((r.force, r.tangent, updated))
|
||||
}
|
||||
|
||||
@@ -284,7 +336,7 @@ impl Problem<'_> {
|
||||
for round in parallel::blocks(self.mesh.elements.len(), ROUND) {
|
||||
let start = round.start;
|
||||
let computed = parallel::map_grained(round.len(), GRAIN, |i| {
|
||||
self.element(start + i, u, tangent, committed.get(start + i).map(Vec::as_slice).unwrap_or_default())
|
||||
self.element(start + i, u, lambda, tangent, committed.get(start + i).map(Vec::as_slice).unwrap_or_default())
|
||||
});
|
||||
for (i, c) in computed.into_iter().enumerate() {
|
||||
let (f, ke, updated) = c?;
|
||||
@@ -381,6 +433,10 @@ struct Run<'a> {
|
||||
/// Every element's material history at the end of the last converged increment
|
||||
/// (integration points × the material's history length).
|
||||
committed: Vec<Vec<f64>>,
|
||||
/// Whether a material keeps history (the tangent predictor and its convergence rules).
|
||||
path_dependent: bool,
|
||||
/// Whether the undeformed model's tangent factors, once asked.
|
||||
undeformed_stable: Option<bool>,
|
||||
}
|
||||
|
||||
/// The outcome of a converged increment.
|
||||
@@ -404,12 +460,53 @@ impl Run<'_> {
|
||||
Ok(self.problem.evaluate(&self.problem.red.expand_scaled(q, lambda), lambda, tangent, &self.committed)?)
|
||||
}
|
||||
|
||||
/// Whether the tangent of the undeformed, unloaded model is positive definite: a first
|
||||
/// tangent that is not, at a state the first increment has already strained, is then the
|
||||
/// material or the geometry (a cutback), not a model that can move freely (an error).
|
||||
fn undeformed_is_stable(&mut self) -> std::result::Result<bool, Failure> {
|
||||
if let Some(stable) = self.undeformed_stable {
|
||||
return Ok(stable);
|
||||
}
|
||||
let mut e = self.problem.evaluate(&vec![0.0; self.problem.mesh.dofs()], 0.0, true, &self.committed)?;
|
||||
let k = e.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
|
||||
let [kff] = self.problem.red.reduce([&k])?;
|
||||
let stable = factor_raw(&kff, self.limits).is_ok();
|
||||
self.undeformed_stable = Some(stable);
|
||||
Ok(stable)
|
||||
}
|
||||
|
||||
/// The tangent predictor of an increment from the converged `q0` at load factor `from` to
|
||||
/// `to`, and its work δq · r (the increment's reference for the energy criterion, since the
|
||||
/// Newton iterations after it only correct it): one linear solve with the tangent of the converged state for the change of the
|
||||
/// loads and of the prescribed displacements, K δq = T_fᵀ (f_ext(to) − f_int(u₀) − K T_p Δλ u_p).
|
||||
/// It spreads a prescribed motion into the interior before the material sees it, which a
|
||||
/// path-dependent material needs: started from the boundary's jump alone, the elements next
|
||||
/// to it would flow plastically far beyond the solution. `None` when the tangent cannot be
|
||||
/// factored (the increment then starts from `q0`).
|
||||
fn tangent_predictor(&mut self, q0: &[f64], from: f64, to: f64) -> std::result::Result<Option<(Vec<f64>, f64)>, Failure> {
|
||||
let red = self.problem.red;
|
||||
let mut e = self.problem.evaluate(&red.expand_scaled(q0, from), to, true, &self.committed)?;
|
||||
let k = e.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
|
||||
let jump = red.expand_scaled(&vec![0.0; red.neq()], to - from);
|
||||
let mut kj = vec![0.0; jump.len()];
|
||||
k.mul(&jump, &mut kj);
|
||||
let rf: Vec<f64> = free_residual(red, &e).iter().zip(red.restrict(&kj)).map(|(a, b)| a - b).collect();
|
||||
let mut dq = rf.clone();
|
||||
let [kff] = red.reduce([&k])?;
|
||||
drop(k);
|
||||
let Ok(chol) = factor_raw(&kff, self.limits) else { return Ok(None) };
|
||||
chol.solve(&mut dq)?;
|
||||
let energy = dot(&dq, &rf).abs();
|
||||
Ok(dq.iter().all(|v| v.is_finite()).then(|| (q0.iter().zip(&dq).map(|(a, d)| a + d).collect(), energy)))
|
||||
}
|
||||
|
||||
/// Newton iterations from the converged `q0` (at the previous load factor) to load factor
|
||||
/// `lambda`.
|
||||
fn increment(
|
||||
&mut self,
|
||||
q0: &[f64],
|
||||
predictor: Option<&[f64]>,
|
||||
predicted_energy: Option<f64>,
|
||||
lambda: f64,
|
||||
size: f64,
|
||||
iterations: &mut Vec<Iteration>,
|
||||
@@ -424,8 +521,14 @@ impl Run<'_> {
|
||||
Some(Err(Failure::Cutback(_))) | None => (q0.to_vec(), self.state(q0, lambda, true)?),
|
||||
};
|
||||
let mut r = self.residual(&eval);
|
||||
// With path-dependent materials, an increment that starts within the force tolerance (a
|
||||
// predictor that already solved it) has only round-off left for the energy criterion to
|
||||
// measure against: it is waived.
|
||||
let started_converged = self.path_dependent && self.force_floor.is_some() && norm_inf(&r) <= tol.force * self.scale();
|
||||
let prescribed = size * red.largest_prescribed();
|
||||
let mut first_energy: Option<f64> = None;
|
||||
// The energy criterion's reference: the first correction's work, or the tangent
|
||||
// predictor's when the increment started from one that is still in use.
|
||||
let mut first_energy: Option<f64> = predicted_energy.filter(|_| q.as_slice() != q0);
|
||||
let mut history: Vec<f64> = Vec::new();
|
||||
let max_iterations = usize::try_from(self.settings.max_iterations).unwrap_or(usize::MAX);
|
||||
for _ in 0..max_iterations {
|
||||
@@ -452,7 +555,7 @@ impl Run<'_> {
|
||||
drop(k);
|
||||
let chol = match factor_raw(&kff, self.limits) {
|
||||
Ok(c) => c,
|
||||
Err(FactorError::NotPositiveDefinite { .. }) if !first => {
|
||||
Err(FactorError::NotPositiveDefinite { .. }) if !first || self.undeformed_is_stable()? => {
|
||||
return Err(Failure::Cutback(
|
||||
"the tangent stiffness is not positive definite (a limit point, a bifurcation or an unstable material)".into(),
|
||||
));
|
||||
@@ -487,7 +590,8 @@ impl Run<'_> {
|
||||
if !it.force.is_finite() {
|
||||
return Err(Failure::Cutback("the residual is not finite".into()));
|
||||
}
|
||||
if (it.force <= tol.force && it.displacement <= tol.displacement && it.energy <= tol.energy) || it.force <= ROUND_OFF * tol.force {
|
||||
let energy_met = it.energy <= tol.energy || started_converged;
|
||||
if (it.force <= tol.force && it.displacement <= tol.displacement && energy_met) || it.force <= ROUND_OFF * tol.force {
|
||||
return Ok(Converged { q, evaluation: eval });
|
||||
}
|
||||
history.push(it.force);
|
||||
@@ -574,13 +678,10 @@ fn check_supported(model: &Model, mesh: &Mesh, step: &Step, settings: &Nonlinear
|
||||
return Err(at(format!("a hyperelastic material without nonlinear geometry (element {})", e.label)));
|
||||
}
|
||||
}
|
||||
for name in &step.loads {
|
||||
match model.load(name) {
|
||||
Some(Load::Temperature { .. }) => return Err(at(format!("temperature loads (load `{name}`): thermal strain"))),
|
||||
Some(Load::Centrifugal { .. }) => return Err(at(format!("centrifugal loads (load `{name}`), which depend on the deformed position"))),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(name) = step.loads.iter().find(|n| matches!(model.load(n), Some(Load::Centrifugal { .. }))) {
|
||||
return Err(at(format!("centrifugal loads (load `{name}`), which depend on the deformed position")));
|
||||
}
|
||||
plasticity(model, step, settings, warnings)?;
|
||||
if settings.nonlinear_geometry
|
||||
&& let Some(name) = model.couplings().keys().next()
|
||||
{
|
||||
@@ -591,6 +692,62 @@ fn check_supported(model: &Model, mesh: &Mesh, step: &Step, settings: &Nonlinear
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Refuse the plasticity a static step cannot take, and warn about what it leaves out, for the
|
||||
/// materials of the solid and 2D sections.
|
||||
fn plasticity(model: &Model, step: &Step, settings: &NonlinearStatic, warnings: &mut Vec<String>) -> Result<()> {
|
||||
let at = |what: String| Error::NotSupported(format!("{what} in nonlinear steps (step `{}`)", step.name));
|
||||
let used: std::collections::BTreeSet<&str> = model
|
||||
.sections()
|
||||
.values()
|
||||
.filter(|s| matches!(s.kind, SectionKind::Solid {}) || Theory::from_section(&s.kind).is_some())
|
||||
.map(|s| s.material.as_str())
|
||||
.collect();
|
||||
for name in used {
|
||||
let Some(m) = model.material(name) else { continue };
|
||||
match &m.plastic {
|
||||
Some(Plastic::JohnsonCook { c, specific_heat, .. }) => {
|
||||
if *c != 0.0 {
|
||||
return Err(at(format!(
|
||||
"the Johnson–Cook strain-rate term of material `{name}` (C = {c}): a static step has no time, so no strain rate; set C to 0 or use explicit dynamics"
|
||||
)));
|
||||
}
|
||||
if specific_heat.is_some() {
|
||||
warnings.push(format!(
|
||||
"material `{name}`: a static step is isothermal, so plastic work does not heat it (adiabatic heating acts in explicit dynamics)"
|
||||
));
|
||||
}
|
||||
}
|
||||
Some(Plastic::Bilinear { hardening: Hardening::Kinematic, .. }) if settings.nonlinear_geometry => {
|
||||
return Err(at(format!(
|
||||
"the kinematic hardening of material `{name}` at finite strain (its back stress is not coaxial with the elastic stretch, which the exponential return map needs); use nonlinear_geometry false or isotropic hardening"
|
||||
)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if m.plastic.is_some() && (m.damage.is_some() || m.erosion.is_some()) {
|
||||
warnings.push(format!("material `{name}`: damage and erosion act in explicit dynamics; this static step leaves them out"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The step's temperature field per mesh node: the steady heat solution of its temperature
|
||||
/// boundary conditions and heat loads when it has some (one-way coupling, as the coupled
|
||||
/// temperature–displacement step of the linear solver), or its temperature loads.
|
||||
fn temperatures(model: &Model, resolved: &fieldcraft_model::Resolved, mesh: &Mesh, step: &Step, limits: &Limits) -> Result<Option<Vec<Option<f64>>>> {
|
||||
let conducts = step.boundary_conditions.iter().any(|b| model.boundary_conditions().get(b).is_some_and(BoundaryCondition::is_thermal))
|
||||
|| step.loads.iter().any(|l| model.load(l).is_some_and(Load::is_thermal));
|
||||
let loads = temperature_loads(model, resolved, mesh, step)?;
|
||||
match (conducts, loads) {
|
||||
(true, Some(_)) => Err(Error::Invalid(format!(
|
||||
"step `{}` gives temperatures both by temperature loads and by heat conditions (temperature boundary conditions, heat loads); use one",
|
||||
step.name
|
||||
))),
|
||||
(true, None) => Ok(Some(crate::heat::temperatures_at(model, resolved, step, limits, &mesh.labels)?)),
|
||||
(false, field) => Ok(field),
|
||||
}
|
||||
}
|
||||
|
||||
/// The follower pressures of a step (with nonlinear geometry), and the step without them for
|
||||
/// the dead loads.
|
||||
fn followers(model: &Model, resolved: &fieldcraft_model::Resolved, mesh: &Mesh, step: &Step) -> Result<(Vec<Follower>, Step)> {
|
||||
@@ -645,7 +802,6 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
settings.validate()?;
|
||||
let mesh = Mesh::new(model, &resolved, limits.max_dofs)?;
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).chain(mesh.warnings.iter().cloned()).collect();
|
||||
warnings.extend(inelastic_warnings(model));
|
||||
check_supported(model, &mesh, step, settings, &mut warnings)?;
|
||||
let (graph, order) = pattern(&mesh);
|
||||
ignored_dashpots(&mesh, &mut warnings);
|
||||
@@ -657,6 +813,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
let (follower_loads, dead_step) =
|
||||
if settings.nonlinear_geometry { followers(model, &resolved, &mesh, step)? } else { (Vec::new(), step.clone()) };
|
||||
let dead = loads(model, &resolved, &mesh, &dead_step, &red.active)?;
|
||||
let field = temperatures(model, &resolved, &mesh, step, limits)?;
|
||||
let springs = if mesh.discrete.springs.iter().any(|s| !s.dashpot) {
|
||||
let mut s = graph.zero_matrix(&mesh.layout);
|
||||
mesh.discrete.add_stiffness(&mut s)?;
|
||||
@@ -673,7 +830,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
mesh.layout.element_dofs(&e.nodes, *kinds).ok_or_else(|| Error::Internal(format!("element {} degrees of freedom", e.label)))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let problem = Problem {
|
||||
let mut problem = Problem {
|
||||
mesh: &mesh,
|
||||
red: &red,
|
||||
dead,
|
||||
@@ -682,9 +839,24 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
finite: settings.nonlinear_geometry,
|
||||
element_dofs,
|
||||
element_kinds,
|
||||
materials: mesh.elements.iter().map(|e| MaterialModel::from_elasticity(&e.d, e.hyperelastic.as_ref())).collect(),
|
||||
materials: mesh
|
||||
.elements
|
||||
.iter()
|
||||
.map(|e| match &e.plastic {
|
||||
Some(j2) => MaterialModel::Plastic(j2.clone()),
|
||||
None => MaterialModel::from_elasticity(&e.d, e.hyperelastic.as_ref()),
|
||||
})
|
||||
.collect(),
|
||||
field,
|
||||
pattern: graph.zero_matrix(&mesh.layout),
|
||||
};
|
||||
// Elements on their linear stiffness take the thermal strain as the equivalent nodal
|
||||
// forces, a dead load proportional to the load factor like the temperatures.
|
||||
if let Some(field) = problem.field.take() {
|
||||
let linear: Vec<bool> = (0..mesh.elements.len()).map(|i| problem.kinematics(i).is_none()).collect();
|
||||
add_element_thermal_loads(&mut problem.dead, &mesh, &field, |i| linear.get(i).copied().unwrap_or(false))?;
|
||||
problem.field = Some(field);
|
||||
}
|
||||
let (lo, hi) = mesh.xyz.iter().fold(([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]), |(lo, hi), p| {
|
||||
(std::array::from_fn(|k| lo[k].min(p[k])), std::array::from_fn(|k| hi[k].max(p[k])))
|
||||
});
|
||||
@@ -698,8 +870,20 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
// The undeformed state first: an element that is bad before anything moves is an error in
|
||||
// the model, not a reason to cut increments back.
|
||||
problem.evaluate(&vec![0.0; mesh.dofs()], 0.0, false, &committed)?;
|
||||
let mut run =
|
||||
Run { problem: &problem, settings, limits, force_scale: 0.0, force_floor: None, length, iterations: 0, factor_entries: 0, committed };
|
||||
let path_dependent = problem.materials.iter().any(MaterialModel::is_path_dependent);
|
||||
let mut run = Run {
|
||||
problem: &problem,
|
||||
settings,
|
||||
limits,
|
||||
force_scale: 0.0,
|
||||
force_floor: None,
|
||||
length,
|
||||
iterations: 0,
|
||||
factor_entries: 0,
|
||||
committed,
|
||||
path_dependent,
|
||||
undeformed_stable: None,
|
||||
};
|
||||
|
||||
let mut history = History::default();
|
||||
let (mut frame_values, mut frames_full) = (0usize, false);
|
||||
@@ -718,32 +902,50 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
};
|
||||
keep_frame(&mut history, 0.0, &vec![0.0; mesh.dofs()], &mut warnings);
|
||||
let mut q = vec![0.0; red.neq()];
|
||||
let mut lambda = 0.0f64;
|
||||
// The step fraction t and the load factor λ(t) of the step's load path (λ = t without one).
|
||||
let (mut t, mut lambda) = (0.0f64, 0.0f64);
|
||||
let mut size = settings.initial_increment;
|
||||
let mut fast = 0usize;
|
||||
let mut last: Option<Evaluation> = None;
|
||||
// The last converged increment's free motion and the change of the load factor over it.
|
||||
let mut previous: Option<(Vec<f64>, f64)> = None;
|
||||
let max_increments = usize::try_from(settings.max_increments).unwrap_or(usize::MAX);
|
||||
while lambda < 1.0 {
|
||||
while t < 1.0 {
|
||||
if history.increments.len() >= max_increments {
|
||||
return Err(Error::Limit(format!(
|
||||
"step `{}`: {} increments reached the step's cap at load factor {lambda:.6} (raise max_increments)",
|
||||
step.name, settings.max_increments
|
||||
)));
|
||||
}
|
||||
let remaining = 1.0 - lambda;
|
||||
let (target, this) = if size >= remaining * (1.0 - 1e-9) { (1.0, remaining) } else { (lambda + size, size) };
|
||||
// An increment ends at the next point of the load path at the latest.
|
||||
let turn = settings.next_turn(t);
|
||||
let remaining = turn - t;
|
||||
let (target, this) = if size >= remaining * (1.0 - 1e-9) { (turn, remaining) } else { (t + size, size) };
|
||||
let target_lambda = settings.load_factor(target);
|
||||
let change = if settings.load_path.is_some() { target_lambda - lambda } else { this };
|
||||
let mut iterations = Vec::new();
|
||||
let number = history.increments.len() + 1;
|
||||
// Linear extrapolation of the last converged increment's free motion.
|
||||
let predictor: Option<Vec<f64>> =
|
||||
previous.as_ref().map(|(dq, prev): &(Vec<f64>, f64)| q.iter().zip(dq).map(|(a, d)| a + this / prev * d).collect());
|
||||
match run.increment(&q, predictor.as_deref(), target, this, &mut iterations) {
|
||||
// Linear extrapolation of the last converged increment's free motion, in proportion to
|
||||
// the change of the load factor (so it turns with the load path).
|
||||
let mut predictor: Option<Vec<f64>> = previous
|
||||
.as_ref()
|
||||
.filter(|(_, prev)| *prev != 0.0)
|
||||
.map(|(dq, prev): &(Vec<f64>, f64)| q.iter().zip(dq).map(|(a, d)| a + change / prev * d).collect());
|
||||
// Path-dependent materials start without one from the tangent predictor.
|
||||
let mut predicted_energy = None;
|
||||
if predictor.is_none() && path_dependent {
|
||||
match run.tangent_predictor(&q, lambda, target_lambda) {
|
||||
Ok(Some((p, energy))) => (predictor, predicted_energy) = (Some(p), Some(energy)),
|
||||
Ok(None) | Err(Failure::Cutback(_)) => {}
|
||||
Err(Failure::Fatal(e)) => return Err(e),
|
||||
}
|
||||
}
|
||||
match run.increment(&q, predictor.as_deref(), predicted_energy, target_lambda, change.abs(), &mut iterations) {
|
||||
Ok(c) => {
|
||||
previous = Some((c.q.iter().zip(&q).map(|(a, b)| a - b).collect(), this));
|
||||
previous = Some((c.q.iter().zip(&q).map(|(a, b)| a - b).collect(), change));
|
||||
let count = iterations.len();
|
||||
history.increments.push(Increment { number, load_factor: target, size: this, iterations, converged: true, cutback: None });
|
||||
lambda = target;
|
||||
history.increments.push(Increment { number, load_factor: target_lambda, size: this, iterations, converged: true, cutback: None });
|
||||
(t, lambda) = (target, target_lambda);
|
||||
q = c.q;
|
||||
keep_frame(&mut history, lambda, &red.expand_scaled(&q, lambda), &mut warnings);
|
||||
run.committed.clone_from(&c.evaluation.history);
|
||||
@@ -760,7 +962,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
Err(Failure::Cutback(reason)) => {
|
||||
history.increments.push(Increment {
|
||||
number,
|
||||
load_factor: target,
|
||||
load_factor: target_lambda,
|
||||
size: this,
|
||||
iterations,
|
||||
converged: false,
|
||||
@@ -770,7 +972,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
size = this * CUTBACK;
|
||||
if size < settings.min_increment * (1.0 - 1e-9) {
|
||||
return Err(Error::NotConverged(format!(
|
||||
"step `{}` stopped at load factor {lambda:.6}: the increment to {target:.6} failed ({reason}) and a smaller one would be below the minimum increment {}",
|
||||
"step `{}` stopped at load factor {lambda:.6} (step fraction {t:.6}): the increment to {target_lambda:.6} failed ({reason}) and a smaller one would be below the minimum increment {}",
|
||||
step.name, settings.min_increment
|
||||
)));
|
||||
}
|
||||
@@ -790,7 +992,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
|
||||
"follower pressure: the skew part of its load stiffness ({a:.1e} of it, over the free degrees of freedom) is left out of the tangent; the solution is exact, Newton may converge more slowly"
|
||||
));
|
||||
}
|
||||
finish(&mesh, step, &red, &problem, u, evaluation, &run.committed, history, run.factor_entries, warnings)
|
||||
finish(&mesh, step, &red, &problem, u, lambda, evaluation, &run.committed, history, run.factor_entries, warnings)
|
||||
}
|
||||
|
||||
/// The final state as a static result: displacements, reactions, Cauchy stresses and
|
||||
@@ -802,6 +1004,7 @@ fn finish(
|
||||
red: &Reduction,
|
||||
problem: &Problem,
|
||||
u: Vec<f64>,
|
||||
lambda: f64,
|
||||
e: Evaluation,
|
||||
committed: &[Vec<f64>],
|
||||
history: History,
|
||||
@@ -826,27 +1029,36 @@ fn finish(
|
||||
.zip(point_motion.iter().zip(&point_reaction))
|
||||
.map(|(p, (d, r))| PointResult { name: p.name.clone(), label: p.label, displacement: *d, reaction: *r })
|
||||
.collect();
|
||||
let all_points = parallel::map_grained(mesh.elements.len(), GRAIN, |i| -> Result<(Vec<PointStress>, &'static ElementType, Rule)> {
|
||||
#[allow(clippy::type_complexity)]
|
||||
let all_points = parallel::map_grained(mesh.elements.len(), GRAIN, |i| -> Result<(Vec<PointStress>, Vec<f64>, &'static ElementType, Rule)> {
|
||||
let el = mesh.elements.get(i).ok_or_else(|| Error::Internal("element index".into()))?;
|
||||
let at = |err| Error::element(el.label, err);
|
||||
let ue = problem.element_vector(i, &u);
|
||||
let kernel = el.kernel()?;
|
||||
let points = if problem.finite {
|
||||
let material = problem.materials.get(i).ok_or_else(|| Error::Internal("element material".into()))?;
|
||||
kernel.finite_strain_stresses(material, &ue, committed.get(i).map(Vec::as_slice).unwrap_or_default())
|
||||
} else {
|
||||
let per_node: Vec<[f64; 3]> = el.nodes.iter().map(|n| displacement.get(*n).copied().unwrap_or_default()).collect();
|
||||
kernel.stresses_with(&el.d, &per_node, None)
|
||||
let history = committed.get(i).map(Vec::as_slice).unwrap_or_default();
|
||||
let material = problem.materials.get(i).ok_or_else(|| Error::Internal("element material".into()))?;
|
||||
let points = match problem.kinematics(i) {
|
||||
Some(kin) => kernel.nonlinear_stresses(material, &ue, kin, problem.temperature(i, lambda).as_ref(), history),
|
||||
None => {
|
||||
let per_node: Vec<[f64; 3]> = el.nodes.iter().map(|n| displacement.get(*n).copied().unwrap_or_default()).collect();
|
||||
let thermal = problem.field.as_deref().and_then(|f| el.thermal_strain(f)).map(|mut t| {
|
||||
t.delta.iter_mut().for_each(|d| *d *= lambda);
|
||||
t
|
||||
});
|
||||
kernel.stresses_with(&el.d, &per_node, thermal.as_ref())
|
||||
}
|
||||
}
|
||||
.map_err(at)?;
|
||||
Ok((points, kernel.ty(), kernel.rule()))
|
||||
let len = material.history_len();
|
||||
let plastic: Vec<f64> = if len > 0 { history.chunks(len).filter_map(|h| material.plastic_strain(h)).collect() } else { Vec::new() };
|
||||
Ok((points, plastic, kernel.ty(), kernel.rule()))
|
||||
});
|
||||
let mut extrapolations: Vec<(&'static ElementType, Rule, Extrapolation)> = Vec::new();
|
||||
let mut elements = Vec::with_capacity(mesh.elements.len());
|
||||
let mut sum = vec![[0.0; 6]; mesh.labels.len()];
|
||||
let mut count = vec![0u32; mesh.labels.len()];
|
||||
for (el, computed) in mesh.elements.iter().zip(all_points) {
|
||||
let (points, ty, rule) = computed?;
|
||||
let (points, plastic_strain, ty, rule) = computed?;
|
||||
let values: Vec<[f64; 6]> = points.iter().map(|p| p.stress).collect();
|
||||
let index = match extrapolations.iter().position(|(t, r, _)| std::ptr::eq(*t, ty) && *r == rule) {
|
||||
Some(i) => i,
|
||||
@@ -864,7 +1076,7 @@ fn finish(
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
elements.push(ElementStress { label: el.label, kind: el.kind, points, nodal });
|
||||
elements.push(ElementStress { label: el.label, kind: el.kind, points, nodal, plastic_strain });
|
||||
}
|
||||
let stress: Vec<[f64; 6]> = sum.iter().zip(&count).map(|(s, c)| s.map(|v| if *c > 0 { v / f64::from(*c) } else { 0.0 })).collect();
|
||||
let von_mises = stress.iter().map(von_mises).collect();
|
||||
|
||||
+97
-26
@@ -11,12 +11,15 @@ use fieldcraft_fem::beam::{Beam, BeamSection};
|
||||
use fieldcraft_fem::constitutive::MaterialModel;
|
||||
use fieldcraft_fem::dense::Matrix;
|
||||
use fieldcraft_fem::element::{Formulation, Solid, ThermalStrain};
|
||||
use fieldcraft_fem::finite::{Kinematics, Temperature};
|
||||
use fieldcraft_fem::plane::{Plane, Theory};
|
||||
use fieldcraft_fem::plasticity::J2;
|
||||
use fieldcraft_fem::shell::{Shell, ShellSection};
|
||||
use fieldcraft_fem::truss::{Truss, young_modulus};
|
||||
use fieldcraft_fem::{DofKind, DofLayout, DofSet, Elasticity, ElementDofs, PointStress, elasticity, face, plane};
|
||||
use fieldcraft_model::nodal::edge_pressure;
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fieldcraft_model::{Elastic, ElementKind, Hyperelastic, Integration, Load, Model, Resolved, SectionKind, Step, Target, centrifugal_acceleration};
|
||||
|
||||
@@ -41,6 +44,9 @@ pub struct Elem {
|
||||
pub expansion: Option<([f64; 3], f64)>,
|
||||
/// The large-strain option of the element's material (nonlinear steps).
|
||||
pub hyperelastic: Option<Hyperelastic>,
|
||||
/// The element's material's plasticity (nonlinear steps), shared by the elements of a
|
||||
/// section.
|
||||
pub plastic: Option<Arc<J2>>,
|
||||
}
|
||||
|
||||
/// An element's kernel: a solid or a 2D element.
|
||||
@@ -113,27 +119,39 @@ impl Kernel<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Finite-strain internal forces and tangent ([`fieldcraft_fem::finite`]) for the element's
|
||||
/// displacements `u` (its degrees of freedom in order: 3 per node, or 2 for 2D elements).
|
||||
pub fn finite_strain(
|
||||
/// Nonlinear internal forces and tangent ([`fieldcraft_fem::finite`]) for the element's
|
||||
/// displacements `u` (its degrees of freedom in order: 3 per node, or 2 for 2D elements),
|
||||
/// total Lagrangian or with small strain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn nonlinear(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[f64],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> fieldcraft_fem::Result<fieldcraft_fem::finite::Finite> {
|
||||
match self {
|
||||
Kernel::Solid(s) => s.finite_strain(m, u.as_chunks::<3>().0, committed, updated, tangent),
|
||||
Kernel::Plane(p) => p.finite_strain(m, u.as_chunks::<2>().0, committed, updated, tangent),
|
||||
Kernel::Solid(s) => s.nonlinear(m, u.as_chunks::<3>().0, kin, thermal, committed, updated, tangent),
|
||||
Kernel::Plane(p) => p.nonlinear(m, u.as_chunks::<2>().0, kin, thermal, committed, updated, tangent),
|
||||
}
|
||||
}
|
||||
|
||||
/// Cauchy stress and logarithmic strain at the integration points, as [`Self::finite_strain`].
|
||||
pub fn finite_strain_stresses(&self, m: &MaterialModel, u: &[f64], committed: &[f64]) -> fieldcraft_fem::Result<Vec<PointStress>> {
|
||||
/// Cauchy stress and strain (logarithmic at finite strain) at the integration points, as
|
||||
/// [`Self::nonlinear`].
|
||||
pub fn nonlinear_stresses(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
u: &[f64],
|
||||
kin: Kinematics,
|
||||
thermal: Option<&Temperature>,
|
||||
committed: &[f64],
|
||||
) -> fieldcraft_fem::Result<Vec<PointStress>> {
|
||||
match self {
|
||||
Kernel::Solid(s) => s.finite_strain_stresses(m, u.as_chunks::<3>().0, committed),
|
||||
Kernel::Plane(p) => p.finite_strain_stresses(m, u.as_chunks::<2>().0, committed),
|
||||
Kernel::Solid(s) => s.nonlinear_stresses(m, u.as_chunks::<3>().0, kin, thermal, committed),
|
||||
Kernel::Plane(p) => p.nonlinear_stresses(m, u.as_chunks::<2>().0, kin, thermal, committed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +222,21 @@ fn thermal_strain(expansion: Option<([f64; 3], f64)>, nodes: &[usize], field: &[
|
||||
delta.iter().any(|d| *d != 0.0).then_some(ThermalStrain { alpha, delta })
|
||||
}
|
||||
|
||||
/// A solid or 2D element of a section, by model labels, before the mesh numbers its nodes.
|
||||
struct SolidEntry {
|
||||
label: u32,
|
||||
kind: ElementKind,
|
||||
nodes: Vec<u32>,
|
||||
formulation: Formulation,
|
||||
d: Elasticity,
|
||||
density: Option<f64>,
|
||||
theory: Option<Theory>,
|
||||
dofs: ElementDofs,
|
||||
expansion: Option<([f64; 3], f64)>,
|
||||
hyperelastic: Option<Hyperelastic>,
|
||||
plastic: Option<Arc<J2>>,
|
||||
}
|
||||
|
||||
/// What a shell or beam element is made of.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Section {
|
||||
@@ -468,6 +501,13 @@ impl Mesh {
|
||||
continue;
|
||||
}
|
||||
let d = elasticity(elastic).map_err(|e| Error::Invalid(format!("material `{}`: {e}", section.material)))?;
|
||||
let plastic = match &material.plastic {
|
||||
Some(p) => Some(Arc::new(
|
||||
J2::from_model(elastic, Some(p), material.damage.as_ref())
|
||||
.map_err(|e| Error::Invalid(format!("material `{}`: {e}", section.material)))?,
|
||||
)),
|
||||
None => None,
|
||||
};
|
||||
for &label in &set.elements {
|
||||
let el = model.element(label).ok_or_else(|| Error::Invalid(format!("element {label} is missing")))?;
|
||||
let (formulation, dofs) = match theory {
|
||||
@@ -476,20 +516,32 @@ impl Mesh {
|
||||
}
|
||||
.map_err(|e| Error::element(label, e))?;
|
||||
let expansion = material.thermal.and_then(|t| t.expansion.map(|a| (a.axes(), t.reference_temperature)));
|
||||
elements.push((label, el.kind, el.nodes.clone(), formulation, d, material.density, theory, dofs, expansion, material.hyperelastic));
|
||||
elements.push(SolidEntry {
|
||||
label,
|
||||
kind: el.kind,
|
||||
nodes: el.nodes.clone(),
|
||||
formulation,
|
||||
d,
|
||||
density: material.density,
|
||||
theory,
|
||||
dofs,
|
||||
expansion,
|
||||
hyperelastic: material.hyperelastic,
|
||||
plastic: plastic.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if let (Some(_), Some(name)) = (planar, model.couplings().keys().next()) {
|
||||
return Err(Error::NotSupported(format!("couplings in 2D models (coupling `{name}`)")));
|
||||
}
|
||||
elements.sort_by_key(|e| e.0);
|
||||
elements.sort_by_key(|e| e.label);
|
||||
trusses.sort_by_key(|e| e.0);
|
||||
structural.sort_by_key(|e| e.0);
|
||||
let translations = translation_kinds(planar.is_some());
|
||||
// What discrete elements need: at mesh nodes (some only they use), and at reference
|
||||
// points, which then get their six degrees of freedom like coupled ones.
|
||||
let needs: Vec<(u32, DofSet)> = crate::discrete::needs(resolved, translations).collect();
|
||||
let mut labels: Vec<u32> = elements.iter().flat_map(|e| e.2.iter().copied()).collect();
|
||||
let mut labels: Vec<u32> = elements.iter().flat_map(|e| e.nodes.iter().copied()).collect();
|
||||
labels.extend(trusses.iter().flat_map(|e| e.2.iter().copied()));
|
||||
labels.extend(structural.iter().flat_map(|e| e.2.iter().copied()));
|
||||
labels.extend(needs.iter().map(|n| n.0).filter(|l| model.node(*l).is_some()));
|
||||
@@ -507,7 +559,7 @@ impl Mesh {
|
||||
.collect::<Result<_>>()?;
|
||||
let mut layout = DofLayout::builder();
|
||||
for e in &elements {
|
||||
layout.element(&e.2, e.7);
|
||||
layout.element(&e.nodes, e.dofs);
|
||||
}
|
||||
for e in &trusses {
|
||||
layout.element(&e.2, fieldcraft_fem::ElementDofs::new(translations));
|
||||
@@ -542,10 +594,22 @@ impl Mesh {
|
||||
warnings,
|
||||
discrete: crate::discrete::Discrete::default(),
|
||||
};
|
||||
for (label, kind, nodes, formulation, d, density, theory, _, expansion, hyperelastic) in elements {
|
||||
let idx: Vec<usize> = nodes.iter().filter_map(|n| mesh.node(*n)).collect();
|
||||
for e in elements {
|
||||
let idx: Vec<usize> = e.nodes.iter().filter_map(|n| mesh.node(*n)).collect();
|
||||
let exyz: Vec<[f64; 3]> = idx.iter().filter_map(|i| mesh.xyz.get(*i).copied()).collect();
|
||||
mesh.elements.push(Elem { label, kind, nodes: idx, xyz: exyz, formulation, d, density, theory, expansion, hyperelastic });
|
||||
mesh.elements.push(Elem {
|
||||
label: e.label,
|
||||
kind: e.kind,
|
||||
nodes: idx,
|
||||
xyz: exyz,
|
||||
formulation: e.formulation,
|
||||
d: e.d,
|
||||
density: e.density,
|
||||
theory: e.theory,
|
||||
expansion: e.expansion,
|
||||
hyperelastic: e.hyperelastic,
|
||||
plastic: e.plastic,
|
||||
});
|
||||
}
|
||||
for (label, kind, nodes, young, area, density, expansion) in trusses {
|
||||
let idx: Vec<usize> = nodes.iter().filter_map(|n| mesh.node(*n)).collect();
|
||||
@@ -881,13 +945,7 @@ pub(crate) fn hold(slot: Option<&mut Option<f64>>, value: f64, conflict: impl Fn
|
||||
|
||||
/// Add the equivalent nodal forces of the thermal strains of a temperature field.
|
||||
pub fn add_thermal_loads(f: &mut [f64], mesh: &Mesh, field: &[Option<f64>]) -> Result<()> {
|
||||
for e in &mesh.elements {
|
||||
let Some(th) = e.thermal_strain(field) else { continue };
|
||||
let forces = e.kernel()?.thermal_load(&e.d, &th).map_err(|err| Error::element(e.label, err))?;
|
||||
for (node, v) in e.nodes.iter().zip(forces) {
|
||||
add(f, &translations(mesh, *node)?, v);
|
||||
}
|
||||
}
|
||||
add_element_thermal_loads(f, mesh, field, |_| true)?;
|
||||
for t in &mesh.trusses {
|
||||
let Some(th) = t.thermal_strain(field) else { continue };
|
||||
let forces = t.truss()?.thermal_load(t.young, t.area, &th).map_err(|err| Error::element(t.label, err))?;
|
||||
@@ -898,6 +956,19 @@ pub fn add_thermal_loads(f: &mut [f64], mesh: &Mesh, field: &[Option<f64>]) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add the equivalent nodal forces of the thermal strains of the solid and 2D elements whose
|
||||
/// index `keep` accepts.
|
||||
pub fn add_element_thermal_loads(f: &mut [f64], mesh: &Mesh, field: &[Option<f64>], keep: impl Fn(usize) -> bool) -> Result<()> {
|
||||
for (_, e) in mesh.elements.iter().enumerate().filter(|(i, _)| keep(*i)) {
|
||||
let Some(th) = e.thermal_strain(field) else { continue };
|
||||
let forces = e.kernel()?.thermal_load(&e.d, &th).map_err(|err| Error::element(e.label, err))?;
|
||||
for (node, v) in e.nodes.iter().zip(forces) {
|
||||
add(f, &translations(mesh, *node)?, v);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The uniform force per volume a step's gravity and body forces put on each shell and beam
|
||||
/// element ([`Mesh::structural`] order): beam end forces are recovered with it.
|
||||
pub fn structural_body_forces(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step) -> Result<Vec<[f64; 3]>> {
|
||||
@@ -936,15 +1007,15 @@ fn body(f: &mut [f64], mesh: &Mesh, e: &Elem, b: [f64; 3]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The linear solver uses the elastic constants alone: a warning for each material in use whose
|
||||
/// plasticity, damage or erosion it leaves out.
|
||||
/// The linear solvers use the elastic constants alone: a warning for each material in use whose
|
||||
/// plasticity, damage or erosion they leave out.
|
||||
pub fn inelastic_warnings(model: &Model) -> Vec<String> {
|
||||
let used: BTreeSet<&str> = model.sections().values().map(|s| s.material.as_str()).collect();
|
||||
used.into_iter()
|
||||
.filter(|name| model.material(name).is_some_and(|m| m.plastic.is_some() || m.damage.is_some() || m.erosion.is_some()))
|
||||
.map(|name| {
|
||||
format!(
|
||||
"material `{name}`: the linear solver uses its elastic constants only (its plasticity, damage and erosion act in explicit dynamics)"
|
||||
"material `{name}`: the linear solver uses its elastic constants only (its plasticity acts in nonlinear static steps and explicit dynamics, its damage and erosion in explicit dynamics)"
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -130,7 +130,7 @@ fn nonlinear_patch_test() {
|
||||
};
|
||||
let (e, nu) = (100.0, 0.3);
|
||||
let neo = fieldcraft_fem::constitutive::MaterialModel::from_model(&iso(e, nu), Some(&Hyperelastic::NeoHookean {})).unwrap();
|
||||
let tau = neo.response(&f, &[], &mut []).unwrap().stress;
|
||||
let tau = neo.response(&f, None, &[], &mut []).unwrap().stress;
|
||||
let j = fieldcraft_fem::dense::det3(&f);
|
||||
let hybrid = Formulation { hybrid: true, ..Formulation::default() };
|
||||
let mut cases: Vec<(ElementKind, Formulation, &str)> = variants().into_iter().filter(|v| v.1 != incompatible()).collect();
|
||||
@@ -387,7 +387,7 @@ fn nonlinear_steps_fail_cleanly() {
|
||||
step.set_procedure(Procedure::NonlinearStatic(bad.clone()));
|
||||
assert!(capped.model.replace_step(step).is_err(), "{bad:?}");
|
||||
}
|
||||
// Refused by name: incompatible modes, a hybrid St Venant–Kirchhoff element, temperature.
|
||||
// Refused by name: incompatible modes, a hybrid St Venant–Kirchhoff element, centrifugal loads.
|
||||
let mut im = grid([2, 1, 1], ElementKind::Hex8, |q| q);
|
||||
im.define(iso(100.0, 0.3), None, incompatible(), nonlinear());
|
||||
im.fixed("root", |q| near(q[0], 0.0));
|
||||
@@ -402,10 +402,13 @@ fn nonlinear_steps_fail_cleanly() {
|
||||
hy.load("p", Load::Pressure { set: "end".into(), magnitude: 1.0 });
|
||||
let err = solve_nonlinear(&hy.model, None, &Limits::default()).unwrap_err().to_string();
|
||||
assert!(err.contains("hybrid elements of a St Venant–Kirchhoff material"), "{err}");
|
||||
hy.load("hot", Load::Temperature { target: Target::Set("root".into()), value: 50.0 });
|
||||
hy.load("spin", Load::Centrifugal { angular_velocity: 10.0, point: [0.0; 3], axis: [0.0, 0.0, 1.0], set: None });
|
||||
neo_hookean(&mut hy, 100.0, 0.3);
|
||||
let mut m = hy.model.material("Mat").unwrap().clone();
|
||||
m.density = Some(1.0);
|
||||
hy.model.replace_material("Mat", m).unwrap();
|
||||
let err = solve_nonlinear(&hy.model, None, &Limits::default()).unwrap_err().to_string();
|
||||
assert!(err.contains("temperature loads"), "{err}");
|
||||
assert!(err.contains("centrifugal loads"), "{err}");
|
||||
// A model that can move freely: the first tangent is singular, named like the linear solver.
|
||||
let mut free = grid([2, 1, 1], ElementKind::Hex8, |q| q);
|
||||
free.define(iso(100.0, 0.3), None, full(), nonlinear());
|
||||
@@ -433,3 +436,81 @@ fn nonlinear_results_are_deterministic() {
|
||||
assert_eq!(fieldcraft_parallel::with_threads(threads, || solve(&g)), a, "{threads} threads");
|
||||
}
|
||||
}
|
||||
|
||||
/// The principal Cauchy stresses of a decoupled polynomial energy at principal stretches `l`:
|
||||
/// σᵢ = (λᵢ/J) ∂W/∂λᵢ by central differences of W itself (independent of the solver's
|
||||
/// stress and tangent), W = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ + κ/2 (J − 1)².
|
||||
fn polynomial_stress(c: [f64; 4], kappa: f64, l: [f64; 3]) -> [f64; 3] {
|
||||
let w = |l: [f64; 3]| {
|
||||
let j = l[0] * l[1] * l[2];
|
||||
let b: Vec<f64> = l.iter().map(|x| x * x * j.powf(-2.0 / 3.0)).collect();
|
||||
let i1 = b[0] + b[1] + b[2];
|
||||
let i2 = b[0] * b[1] + b[1] * b[2] + b[2] * b[0];
|
||||
let x = i1 - 3.0;
|
||||
c[0] * x + c[1] * (i2 - 3.0) + c[2] * x * x + c[3] * x * x * x + 0.5 * kappa * (j - 1.0) * (j - 1.0)
|
||||
};
|
||||
let j = l[0] * l[1] * l[2];
|
||||
std::array::from_fn(|i| {
|
||||
let h = 1e-6 * l[i];
|
||||
let (mut p, mut m) = (l, l);
|
||||
p[i] += h;
|
||||
m[i] -= h;
|
||||
l[i] / j * (w(p) - w(m)) / (2.0 * h)
|
||||
})
|
||||
}
|
||||
|
||||
/// Mooney–Rivlin and Yeoh blocks in the homogeneous modes of rubber testing (L. R. G. Treloar,
|
||||
/// *The Physics of Rubber Elasticity*, 3rd ed., Oxford 1975, ch. 10): uniaxial and equibiaxial
|
||||
/// tension and pure shear, against the closed form of the compressible decoupled energy (the
|
||||
/// free stretches found by bisection so the free faces carry no stress).
|
||||
#[test]
|
||||
fn mooney_rivlin_and_yeoh_in_the_homogeneous_modes() {
|
||||
let (e, nu) = (3.0, 0.49);
|
||||
let (mu, kappa) = (e / (2.0 * (1.0 + nu)), e / (3.0 * (1.0 - 2.0 * nu)));
|
||||
let hybrid = Formulation { hybrid: true, ..Formulation::default() };
|
||||
let (c01, c20, c30) = (0.2, -0.02, 0.004);
|
||||
for option in [Hyperelastic::MooneyRivlin { c01 }, Hyperelastic::Yeoh { c20, c30 }] {
|
||||
let c = option.constants(mu);
|
||||
for (mode, lam) in [("uniaxial", 2.0), ("equibiaxial", 1.5), ("pure shear", 2.0)] {
|
||||
// The stretches with the free one(s) l from σ_free(l) = 0.
|
||||
let stretches = |l: f64| match mode {
|
||||
"uniaxial" => [lam, l, l],
|
||||
"equibiaxial" => [lam, lam, l],
|
||||
_ => [lam, 1.0, l],
|
||||
};
|
||||
let (mut lo, mut hi) = (0.05, 1.5);
|
||||
for _ in 0..200 {
|
||||
let mid = 0.5 * (lo + hi);
|
||||
if polynomial_stress(c, kappa, stretches(mid))[2] > 0.0 { hi = mid } else { lo = mid }
|
||||
}
|
||||
let want = polynomial_stress(c, kappa, stretches(0.5 * (lo + hi)));
|
||||
let mut g = grid([1, 1, 1], ElementKind::Hex20, |x| x);
|
||||
g.define(iso(e, nu), None, hybrid, nonlinear());
|
||||
g.model.replace_material("Mat", Material { elastic: Some(iso(e, nu)), hyperelastic: Some(option), ..Material::default() }).unwrap();
|
||||
g.symmetry("x0", Symmetry::X, |x| near(x[0], 0.0));
|
||||
g.symmetry("y0", Symmetry::Y, |x| near(x[1], 0.0));
|
||||
g.symmetry("z0", Symmetry::Z, |x| near(x[2], 0.0));
|
||||
g.node_set("x1", |x| near(x[0], 1.0));
|
||||
g.bc(
|
||||
"pull",
|
||||
BoundaryCondition::Displacement { target: Target::Set("x1".into()), values: [Some(lam - 1.0), None, None, None, None, None] },
|
||||
);
|
||||
g.node_set("y1", |x| near(x[1], 1.0));
|
||||
let y = match mode {
|
||||
"equibiaxial" => Some(lam - 1.0),
|
||||
"pure shear" => Some(0.0),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(v) = y {
|
||||
g.bc("y1", BoundaryCondition::Displacement { target: Target::Set("y1".into()), values: [None, Some(v), None, None, None, None] });
|
||||
}
|
||||
let r = solve(&g);
|
||||
for s in &r.stress {
|
||||
for k in 0..3 {
|
||||
assert!((s[k] - want[k]).abs() < 1e-7 * want[0], "{} {mode}: {s:?} vs {want:?}", option.name());
|
||||
}
|
||||
}
|
||||
eprintln!("{} {mode} λ = {lam}: σ = {:.6?} (closed form {want:.6?})", option.name(), &r.stress[0][..3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//! Thermal strain in nonlinear static steps against closed forms: free expansion and a fully
|
||||
//! constrained heated block at finite strain (the multiplicative split F = F_m F_θ), a heated
|
||||
//! bar held at its ends past yield at small strain (the additive split), the linear limit
|
||||
//! against the linear solver, and a step's own heat conduction feeding its temperatures.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use fieldcraft_model::*;
|
||||
use fieldcraft_solver::{Limits, StaticResult, solve_nonlinear, solve_static};
|
||||
|
||||
fn expanding(g: &mut Grid, e: f64, nu: f64, alpha: f64, hyperelastic: Option<Hyperelastic>, plastic: Option<Plastic>) {
|
||||
let thermal = Thermal {
|
||||
expansion: Some(Directional::Isotropic(alpha)),
|
||||
reference_temperature: 20.0,
|
||||
conductivity: Some(Directional::Isotropic(45.0)),
|
||||
..Thermal::default()
|
||||
};
|
||||
let m = Material { elastic: Some(iso(e, nu)), hyperelastic, plastic, thermal: Some(thermal), ..Material::default() };
|
||||
g.model.replace_material("Mat", m).unwrap();
|
||||
}
|
||||
|
||||
fn heat_all(g: &mut Grid, value: f64) {
|
||||
g.node_set("everything", |_| true);
|
||||
g.load("heat", Load::Temperature { target: Target::Set("everything".into()), value });
|
||||
}
|
||||
|
||||
fn solve(g: &Grid) -> StaticResult {
|
||||
solve_nonlinear(&g.model, Some("Step"), &Limits::default()).unwrap()
|
||||
}
|
||||
|
||||
fn symmetric(g: &mut Grid) {
|
||||
g.symmetry("x0", Symmetry::X, |x| near(x[0], 0.0));
|
||||
g.symmetry("y0", Symmetry::Y, |x| near(x[1], 0.0));
|
||||
g.symmetry("z0", Symmetry::Z, |x| near(x[2], 0.0));
|
||||
}
|
||||
|
||||
/// A free neo-Hookean cube heated by 200 with α = 10⁻³ grows by the thermal stretch 1.2 along
|
||||
/// every edge, stress-free; held on all six faces it takes the pressure of the mechanical
|
||||
/// volume ratio ϑ⁻³: σ = κ (ϑ⁻³ − 1), full and hybrid elements.
|
||||
#[test]
|
||||
fn finite_strain_free_and_constrained_expansion() {
|
||||
let (e, nu, alpha) = (10.0, 0.3, 1e-3);
|
||||
let kappa = e / (3.0 * (1.0 - 2.0 * nu));
|
||||
let theta: f64 = 1.0 + alpha * 200.0;
|
||||
for formulation in [Formulation::default(), Formulation { hybrid: true, ..Formulation::default() }] {
|
||||
let mut g = grid([2, 2, 2], ElementKind::Hex20, |x| x);
|
||||
g.define(iso(e, nu), None, formulation, Procedure::NonlinearStatic(NonlinearStatic::default()));
|
||||
expanding(&mut g, e, nu, alpha, Some(Hyperelastic::NeoHookean {}), None);
|
||||
symmetric(&mut g);
|
||||
heat_all(&mut g, 220.0);
|
||||
let r = solve(&g);
|
||||
for (label, u) in r.nodes.iter().zip(&r.displacement) {
|
||||
let x = g.model.node(*label).unwrap().xyz;
|
||||
assert!((0..3).all(|k| (u[k] - (theta - 1.0) * x[k]).abs() < 1e-9), "{formulation:?} node {label}: {u:?}");
|
||||
}
|
||||
assert!(r.stress.iter().flatten().all(|s| s.abs() < 1e-9 * kappa), "{formulation:?}: {:?}", r.stress[0]);
|
||||
assert!((r.elements[0].points[0].strain[0] - theta.ln()).abs() < 1e-9, "the total logarithmic strain");
|
||||
|
||||
g.symmetry("x1", Symmetry::X, |x| near(x[0], 1.0));
|
||||
g.symmetry("y1", Symmetry::Y, |x| near(x[1], 1.0));
|
||||
g.symmetry("z1", Symmetry::Z, |x| near(x[2], 1.0));
|
||||
let r = solve(&g);
|
||||
let want = kappa * (theta.powi(-3) - 1.0);
|
||||
for s in &r.stress {
|
||||
assert!((0..3).all(|k| (s[k] - want).abs() < 1e-9 * kappa) && s[3].abs() < 1e-9 * kappa, "{formulation:?}: {s:?} vs {want}");
|
||||
}
|
||||
eprintln!("{formulation:?}: constrained σ = {:.9} (closed form {want:.9})", r.stress[0][0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A steel bar held axially at both ends (lateral faces free) and heated at small strain: the
|
||||
/// mechanical strain is −αΔT, so the stress is −EαΔT until it yields, then
|
||||
/// −(σ_y + E_t (αΔT − σ_y/E)) with linear hardening.
|
||||
#[test]
|
||||
fn small_strain_heated_bar_held_at_its_ends_yields() {
|
||||
let (e, nu, alpha, y0, h) = (200_000.0, 0.3, 1.2e-5, 250.0, 2000.0);
|
||||
let small = NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() };
|
||||
let plastic = Plastic::Bilinear { yield_stress: y0, hardening_modulus: h, hardening: Hardening::Isotropic };
|
||||
let et = e * h / (e + h);
|
||||
for (dt, want) in [(80.0, -e * alpha * 80.0), (300.0, -(y0 + et * (alpha * 300.0 - y0 / e)))] {
|
||||
let mut g = grid([4, 1, 1], ElementKind::Hex8, |x| [4.0 * x[0], x[1], x[2]]);
|
||||
g.define(iso(e, nu), None, Formulation::default(), Procedure::NonlinearStatic(small.clone()));
|
||||
expanding(&mut g, e, nu, alpha, None, Some(plastic.clone()));
|
||||
symmetric(&mut g);
|
||||
g.symmetry("x1", Symmetry::X, |x| near(x[0], 1.0));
|
||||
heat_all(&mut g, 20.0 + dt);
|
||||
let r = solve(&g);
|
||||
for s in &r.stress {
|
||||
assert!((s[0] - want).abs() < 1e-8 * want.abs() && s[1].abs() < 1e-8 * want.abs(), "ΔT = {dt}: {s:?} vs {want}");
|
||||
}
|
||||
eprintln!("ΔT = {dt}: σ = {:.6} (closed form {want:.6})", r.stress[0][0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Without nonlinear geometry an elastic model is the linear solution, thermal strain
|
||||
/// included; a step's own temperature boundary conditions give its temperatures by conduction,
|
||||
/// as in the linear coupled step.
|
||||
#[test]
|
||||
fn thermal_linear_limit_and_conducted_temperatures() {
|
||||
let (e, nu, alpha) = (200_000.0, 0.3, 1.2e-5);
|
||||
let build = |procedure: Procedure, plastic: Option<Plastic>, conducted: bool| {
|
||||
let mut g = grid([6, 1, 2], ElementKind::Hex20, |q| [6.0 * q[0], q[1], q[2]]);
|
||||
g.define(iso(e, nu), None, Formulation::default(), procedure);
|
||||
expanding(&mut g, e, nu, alpha, None, plastic);
|
||||
g.fixed("root", |q| near(q[0], 0.0));
|
||||
if conducted {
|
||||
g.node_set("cold", |q| near(q[0], 0.0));
|
||||
g.bc("cold_t", BoundaryCondition::Temperature { target: Target::Set("cold".into()), value: 20.0 });
|
||||
g.node_set("hot", |q| near(q[0], 1.0));
|
||||
g.bc("hot_t", BoundaryCondition::Temperature { target: Target::Set("hot".into()), value: 320.0 });
|
||||
} else {
|
||||
// Hot on top, cold below: the bar bends.
|
||||
g.node_set("top", |q| near(q[2], 1.0));
|
||||
g.load("hot", Load::Temperature { target: Target::Set("top".into()), value: 120.0 });
|
||||
}
|
||||
g
|
||||
};
|
||||
let small = Procedure::NonlinearStatic(NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() });
|
||||
let linear = solve_static(&build(Procedure::Static {}, None, false).model, None, &Limits::default()).unwrap();
|
||||
let r = solve(&build(small.clone(), None, false));
|
||||
let scale = linear.displacement.iter().flatten().fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
for (a, b) in r.displacement.iter().zip(&linear.displacement) {
|
||||
assert!((0..3).all(|k| (a[k] - b[k]).abs() < 1e-9 * scale), "{a:?} vs {b:?}");
|
||||
}
|
||||
for (a, b) in r.stress.iter().zip(&linear.stress) {
|
||||
assert!((0..6).all(|k| (a[k] - b[k]).abs() < 1e-6 * e * alpha * 100.0), "{a:?} vs {b:?}");
|
||||
}
|
||||
// Conducted: T linear in x from 20 to 320 over a bar fixed at its cold end; the step's
|
||||
// temperature conditions are solved first, as the coupled linear step does, and a plastic
|
||||
// material that stays below yield gives that step's solution.
|
||||
let plastic = Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 0.0, hardening: Hardening::Isotropic };
|
||||
let r = solve(&build(small, Some(plastic), true));
|
||||
let coupled = solve_static(&build(Procedure::CoupledTemperatureDisplacement {}, None, true).model, None, &Limits::default()).unwrap();
|
||||
let largest = coupled.von_mises.iter().fold(0.0f64, |m, v| m.max(*v));
|
||||
assert!(largest < 250.0 && largest > 1.0, "{largest}");
|
||||
let scale = coupled.displacement.iter().flatten().fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
for (a, b) in r.displacement.iter().zip(&coupled.displacement) {
|
||||
assert!((0..3).all(|k| (a[k] - b[k]).abs() < 1e-8 * scale), "{a:?} vs {b:?}");
|
||||
}
|
||||
assert!(r.elements.iter().flat_map(|e| &e.plastic_strain).all(|p| *p == 0.0));
|
||||
// Both ways of giving temperatures at once is refused.
|
||||
let mut both = build(Procedure::NonlinearStatic(NonlinearStatic::default()), None, true);
|
||||
heat_all(&mut both, 50.0);
|
||||
let err = solve_nonlinear(&both.model, None, &Limits::default()).unwrap_err().to_string();
|
||||
assert!(err.contains("both by temperature loads and by heat conditions"), "{err}");
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
//! Implicit J2 plasticity in nonlinear static steps against closed forms: uniaxial tension at
|
||||
//! finite strain (Kirchhoff stress against the logarithmic strain) and at small strain in every
|
||||
//! element kind, the Bauschinger effect of linear kinematic hardening on a load path that comes
|
||||
//! back, Newton's quadratic rate with the consistent tangent, and what plastic steps refuse or
|
||||
//! stop at. Run with `--nocapture` to see the residuals.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use fieldcraft_model::*;
|
||||
use fieldcraft_solver::{Limits, StaticResult, solve_nonlinear};
|
||||
|
||||
const E: f64 = 200_000.0;
|
||||
const NU: f64 = 0.3;
|
||||
const Y0: f64 = 250.0;
|
||||
const H: f64 = 2000.0;
|
||||
|
||||
fn bilinear(hardening: Hardening) -> Plastic {
|
||||
Plastic::Bilinear { yield_stress: Y0, hardening_modulus: H, hardening }
|
||||
}
|
||||
|
||||
fn plastic(g: &mut Grid, p: Plastic) {
|
||||
let m = Material { elastic: Some(iso(E, NU)), plastic: Some(p), ..Material::default() };
|
||||
g.model.replace_material("Mat", m).unwrap();
|
||||
}
|
||||
|
||||
/// A unit cube on three symmetry planes, its x = 1 face pulled by `stretch` (uniaxial stress).
|
||||
fn cube(kind: ElementKind, formulation: Formulation, step: NonlinearStatic, p: Plastic, stretch: f64) -> Grid {
|
||||
let mut g = grid([2, 2, 2], kind, |x| x);
|
||||
g.define(iso(E, NU), None, formulation, Procedure::NonlinearStatic(step));
|
||||
plastic(&mut g, p);
|
||||
g.symmetry("x0", Symmetry::X, |x| near(x[0], 0.0));
|
||||
g.symmetry("y0", Symmetry::Y, |x| near(x[1], 0.0));
|
||||
g.symmetry("z0", Symmetry::Z, |x| near(x[2], 0.0));
|
||||
g.node_set("x1", |x| near(x[0], 1.0));
|
||||
g.bc("pull", BoundaryCondition::Displacement { target: Target::Set("x1".into()), values: [Some(stretch), None, None, None, None, None] });
|
||||
g
|
||||
}
|
||||
|
||||
fn solve(g: &Grid) -> StaticResult {
|
||||
solve_nonlinear(&g.model, Some("Step"), &Limits::default()).unwrap()
|
||||
}
|
||||
|
||||
/// The uniaxial stress of linear hardening at strain ε past yield: σ_y + E_t (ε − σ_y/E).
|
||||
fn uniaxial(strain: f64) -> f64 {
|
||||
let et = E * H / (E + H);
|
||||
Y0 + et * (strain - Y0 / E)
|
||||
}
|
||||
|
||||
fn hybrid() -> Formulation {
|
||||
Formulation { hybrid: true, ..Formulation::default() }
|
||||
}
|
||||
|
||||
/// Every element kind (and hybrid hex8, hex20, tet10) stretched to a logarithmic strain of 0.2:
|
||||
/// the Kirchhoff stress τ = J σ follows the small-strain curve in the logarithmic strain
|
||||
/// exactly (Simo's exponential map integrates a proportional path exactly), and the
|
||||
/// equivalent plastic strain is (τ − σ_y)/H.
|
||||
#[test]
|
||||
fn finite_strain_uniaxial_tension_follows_the_logarithmic_closed_form() {
|
||||
let strain: f64 = 0.2;
|
||||
let tau = uniaxial(strain);
|
||||
let mut cases: Vec<(ElementKind, Formulation, &str)> = variants().into_iter().filter(|v| v.1 != incompatible()).collect();
|
||||
cases.extend([
|
||||
(ElementKind::Hex8, hybrid(), "hex8 hybrid"),
|
||||
(ElementKind::Hex20, hybrid(), "hex20 hybrid"),
|
||||
(ElementKind::Tet10, hybrid(), "tet10 hybrid"),
|
||||
]);
|
||||
for (kind, formulation, name) in cases {
|
||||
let g = cube(kind, formulation, NonlinearStatic::default(), bilinear(Hardening::Isotropic), strain.exp() - 1.0);
|
||||
let r = solve(&g);
|
||||
for e in &r.elements {
|
||||
for (p, ep) in e.points.iter().zip(&e.plastic_strain) {
|
||||
// J from the logarithmic strains, σ the Cauchy stress.
|
||||
let j = (p.strain[0] + p.strain[1] + p.strain[2]).exp();
|
||||
assert!((p.stress[0] * j - tau).abs() < 1e-7 * tau, "{name}: τ = {} vs {tau}", p.stress[0] * j);
|
||||
assert!(p.stress[1].abs() < 1e-7 * tau && p.stress[3].abs() < 1e-7 * tau, "{name}: {:?}", p.stress);
|
||||
assert!((ep - (tau - Y0) / H).abs() < 1e-9, "{name}: ε_p = {ep}");
|
||||
assert!((p.strain[0] - strain).abs() < 1e-12, "{name}: {:?}", p.strain);
|
||||
}
|
||||
assert_eq!(e.plastic_strain.len(), e.points.len(), "{name}");
|
||||
}
|
||||
let h = r.nonlinear.as_ref().unwrap();
|
||||
eprintln!("{name}: {} increments, {} iterations", h.increments.len(), h.iterations);
|
||||
}
|
||||
}
|
||||
|
||||
/// Without nonlinear geometry: σ = σ_y + E_t (ε − σ_y/E) in the small strain, isotropic and
|
||||
/// kinematic hardening alike under monotonic loading, hybrid elements too.
|
||||
#[test]
|
||||
fn small_strain_uniaxial_tension_follows_the_closed_form() {
|
||||
let strain = 0.01;
|
||||
let sigma = uniaxial(strain);
|
||||
let small = NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() };
|
||||
for hardening in [Hardening::Isotropic, Hardening::Kinematic] {
|
||||
for (kind, formulation, name) in [
|
||||
(ElementKind::Hex8, full(), "hex8"),
|
||||
(ElementKind::Tet10, full(), "tet10"),
|
||||
(ElementKind::Hex20, reduced(), "hex20 reduced"),
|
||||
(ElementKind::Hex20, hybrid(), "hex20 hybrid"),
|
||||
] {
|
||||
let r = solve(&cube(kind, formulation, small.clone(), bilinear(hardening), strain));
|
||||
for e in &r.elements {
|
||||
for (p, ep) in e.points.iter().zip(&e.plastic_strain) {
|
||||
assert!((p.stress[0] - sigma).abs() < 1e-8 * sigma && p.stress[1].abs() < 1e-8 * sigma, "{name} {hardening:?}: {:?}", p.stress);
|
||||
assert!((ep - (sigma - Y0) / H).abs() < 1e-10, "{name}: {ep}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Bauschinger effect: pulled to ε_a and brought back to zero strain on a load path,
|
||||
/// linear kinematic hardening yields again in reverse at σ_a − 2σ_y and ends at the residual
|
||||
/// stress σ_a − 2σ_y − E_t (ε_a − 2σ_y/E); isotropic hardening yields again only at −σ_a and
|
||||
/// ends at −σ_a − E_t (ε_a − 2σ_a/E).
|
||||
#[test]
|
||||
fn kinematic_hardening_shows_the_bauschinger_effect_on_unloading() {
|
||||
let strain = 0.01;
|
||||
let sa = uniaxial(strain);
|
||||
let et = E * H / (E + H);
|
||||
let path = NonlinearStatic {
|
||||
nonlinear_geometry: false,
|
||||
initial_increment: 0.05,
|
||||
max_increment: 0.05,
|
||||
load_path: Some(vec![[0.0, 0.0], [0.5, 1.0], [1.0, 0.0]]),
|
||||
..NonlinearStatic::default()
|
||||
};
|
||||
for (hardening, want) in
|
||||
[(Hardening::Kinematic, sa - 2.0 * Y0 - et * (strain - 2.0 * Y0 / E)), (Hardening::Isotropic, -sa - et * (strain - 2.0 * sa / E))]
|
||||
{
|
||||
let r = solve(&cube(ElementKind::Hex8, full(), path.clone(), bilinear(hardening), strain));
|
||||
let s = r.elements[0].points[0].stress[0];
|
||||
assert!((s - want).abs() < 1e-8 * sa, "{hardening:?}: residual stress {s} vs {want}");
|
||||
assert!(r.displacement.iter().all(|u| u[0].abs() < 1e-12), "back at zero strain");
|
||||
let h = r.nonlinear.unwrap();
|
||||
assert!(h.increments.iter().any(|i| i.converged && i.load_factor == 1.0), "the path's turn is an increment's end");
|
||||
assert_eq!(h.frames.last().map(|f| f.load_factor), Some(0.0));
|
||||
eprintln!("{hardening:?}: residual stress {s:.6} (closed form {want:.6})");
|
||||
}
|
||||
}
|
||||
|
||||
/// Newton with the consistent tangent converges quadratically in a plastic structure: a notched
|
||||
/// bar pulled past general yield, and a cantilever bent into plastic flow at finite strain.
|
||||
#[test]
|
||||
fn plastic_newton_converges_quadratically() {
|
||||
for nonlinear_geometry in [false, true] {
|
||||
let mut step = NonlinearStatic { initial_increment: 0.5, nonlinear_geometry, ..NonlinearStatic::default() };
|
||||
step.tolerances.force = 1e-8;
|
||||
step.tolerances.displacement = 1e-8;
|
||||
step.tolerances.energy = 1e-16;
|
||||
let mut g = grid([8, 1, 2], ElementKind::Hex20, |q| [10.0 * q[0], q[1], q[2]]);
|
||||
g.define(iso(E, NU), None, full(), Procedure::NonlinearStatic(step));
|
||||
plastic(&mut g, bilinear(Hardening::Isotropic));
|
||||
g.fixed("root", |q| near(q[0], 0.0));
|
||||
g.node_set("tip", |q| near(q[0], 1.0));
|
||||
// A tip deflection of ten times the first yield's.
|
||||
let first_yield = 2.0 * Y0 * 100.0 / (3.0 * E * 2.0);
|
||||
g.bc(
|
||||
"push",
|
||||
BoundaryCondition::Displacement { target: Target::Set("tip".into()), values: [None, None, Some(-10.0 * first_yield), None, None, None] },
|
||||
);
|
||||
let r = solve(&g);
|
||||
assert!(r.elements.iter().flat_map(|e| &e.plastic_strain).any(|p| *p > 0.0), "plastic");
|
||||
let h = r.nonlinear.unwrap();
|
||||
let mut best = 0.0f64;
|
||||
for inc in h.increments.iter().filter(|i| i.converged) {
|
||||
let res: Vec<f64> = inc.iterations.iter().map(|i| i.force).collect();
|
||||
let orders: Vec<f64> = res.windows(3).filter(|w| w[0] < 1e-2 && w[2] > 1e-11).map(|w| (w[2] / w[1]).ln() / (w[1] / w[0]).ln()).collect();
|
||||
eprintln!("geometry {nonlinear_geometry}: increment {} (λ = {}): residuals {res:?}, orders {orders:?}", inc.number, inc.load_factor);
|
||||
best = orders.iter().fold(best, |m, o| m.max(*o));
|
||||
}
|
||||
assert!(best > 1.8, "nonlinear geometry {nonlinear_geometry}: no quadratic convergence (best order {best})");
|
||||
}
|
||||
}
|
||||
|
||||
/// What a static step cannot take is refused by name; a material that softens faster than it
|
||||
/// unloads makes the step cut back and stop cleanly; the results do not depend on the threads.
|
||||
#[test]
|
||||
fn plastic_steps_refuse_and_fail_cleanly() {
|
||||
let jc = |c: f64| Plastic::JohnsonCook {
|
||||
a: 350.0,
|
||||
b: 275.0,
|
||||
n: 0.36,
|
||||
c,
|
||||
m: 1.0,
|
||||
reference_strain_rate: 1.0,
|
||||
room_temperature: 293.0,
|
||||
melt_temperature: 1356.0,
|
||||
specific_heat: Some(4.5e8),
|
||||
taylor_quinney: 0.9,
|
||||
};
|
||||
let err = solve_nonlinear(&cube(ElementKind::Hex8, full(), NonlinearStatic::default(), jc(0.02), 0.01).model, None, &Limits::default())
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("strain-rate term") && err.contains("C = 0.02"), "{err}");
|
||||
// C = 0 is the rate-independent law: it solves, warning that a static step does not heat.
|
||||
let r = solve(&cube(ElementKind::Hex8, full(), NonlinearStatic::default(), jc(0.0), 0.01));
|
||||
assert!(r.warnings.iter().any(|w| w.contains("isothermal")), "{:?}", r.warnings);
|
||||
let err = solve_nonlinear(
|
||||
&cube(ElementKind::Hex8, full(), NonlinearStatic::default(), bilinear(Hardening::Kinematic), 0.01).model,
|
||||
None,
|
||||
&Limits::default(),
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("kinematic hardening") && err.contains("finite strain"), "{err}");
|
||||
let small = NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() };
|
||||
let err = solve_nonlinear(
|
||||
&cube(ElementKind::Hex8, incompatible(), small.clone(), bilinear(Hardening::Isotropic), 0.01).model,
|
||||
None,
|
||||
&Limits::default(),
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("incompatible modes with an inelastic material"), "{err}");
|
||||
// Softening steeper than −3G past a small plastic strain: the point localises, the step cuts
|
||||
// back to its minimum increment and stops, naming the element.
|
||||
let g_mod = E / (2.0 * (1.0 + NU));
|
||||
let softening =
|
||||
Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: Y0 }, HardeningPoint { strain: 1e-4, stress: Y0 - 4.0 * g_mod * 1e-4 }] };
|
||||
let soft = NonlinearStatic { min_increment: 1e-3, ..small.clone() };
|
||||
let err = solve_nonlinear(&cube(ElementKind::Hex8, full(), soft, softening, 0.01).model, None, &Limits::default()).unwrap_err().to_string();
|
||||
assert!(err.contains("stopped at load factor") && err.contains("element"), "{err}");
|
||||
// Damage is left out with a warning.
|
||||
let mut g = cube(ElementKind::Hex8, full(), small, bilinear(Hardening::Isotropic), 0.01);
|
||||
let mut m = g.model.material("Mat").unwrap().clone();
|
||||
m.damage = Some(Damage::JohnsonCook { d1: 0.5, d2: 0.0, d3: 0.0, d4: 0.0, d5: 0.0 });
|
||||
g.model.replace_material("Mat", m).unwrap();
|
||||
let r = solve(&g);
|
||||
assert!(r.warnings.iter().any(|w| w.contains("damage and erosion")), "{:?}", r.warnings);
|
||||
// Hostile plasticity never reaches the solver: non-finite or negative tables and yield.
|
||||
let mut g = cube(ElementKind::Hex8, full(), NonlinearStatic::default(), bilinear(Hardening::Isotropic), 0.01);
|
||||
for bad in [
|
||||
Plastic::Bilinear { yield_stress: -250.0, hardening_modulus: H, hardening: Hardening::Isotropic },
|
||||
Plastic::Bilinear { yield_stress: Y0, hardening_modulus: f64::NAN, hardening: Hardening::Isotropic },
|
||||
Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: f64::INFINITY }] },
|
||||
Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: Y0 }, HardeningPoint { strain: f64::NAN, stress: Y0 }] },
|
||||
Plastic::Table { points: vec![HardeningPoint { strain: 0.0, stress: Y0 }, HardeningPoint { strain: 0.1, stress: -1.0 }] },
|
||||
] {
|
||||
let m = Material { elastic: Some(iso(E, NU)), plastic: Some(bad.clone()), ..Material::default() };
|
||||
assert!(g.model.replace_material("Mat", m).is_err(), "{bad:?}");
|
||||
}
|
||||
// Bit for bit on any number of threads.
|
||||
let g = cube(ElementKind::Tet10, full(), NonlinearStatic::default(), bilinear(Hardening::Isotropic), 0.1);
|
||||
let a = fieldcraft_parallel::with_threads(1, || solve(&g));
|
||||
for threads in [2, 7] {
|
||||
assert_eq!(fieldcraft_parallel::with_threads(threads, || solve(&g)), a, "{threads} threads");
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
];
|
||||
|
||||
|
||||
@@ -549,6 +549,16 @@ fn fixture_text(f: &FixtureCheck) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The fixture column of docs/oracle.md. A value blessed in this run matches its new fixture by
|
||||
/// construction, so the page reads the same whether or not the run blessed anything; otherwise
|
||||
/// a page written by a blessing run would differ from the one CI regenerates.
|
||||
fn page_fixture_text(f: &FixtureCheck) -> &'static str {
|
||||
match f {
|
||||
FixtureCheck::Blessed => fixture_text(&FixtureCheck::Matches),
|
||||
other => fixture_text(other),
|
||||
}
|
||||
}
|
||||
|
||||
fn cell(s: &str) -> String {
|
||||
s.replace('|', "\\|").replace('\n', " ")
|
||||
}
|
||||
@@ -609,7 +619,7 @@ benchmark, {} experiment; {} pending cases wait for capabilities still being bui
|
||||
error_of(r),
|
||||
r.quantity.tolerance.describe(),
|
||||
cell(&result),
|
||||
fixture_text(&r.fixture),
|
||||
page_fixture_text(&r.fixture),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+2
-1
@@ -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) | |
|
||||
|
||||
+82
-10
@@ -171,6 +171,23 @@ converges, so a cut-back increment leaves no trace. The materials:
|
||||
|---|---|
|
||||
| Elastic (isotropic or orthotropic), no option | St Venant–Kirchhoff: S = D E, E = ½(FᵀF − I). Right for large rotations with small strains; it softens without bound in large compression. |
|
||||
| Isotropic elastic with `"hyperelastic": {"type": "neo_hookean"}` | Compressible neo-Hookean with the isochoric–volumetric split, W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)², μ and κ from E and ν (C₁₀ = μ/2, D₁ = 2/κ). Its small-strain limit is the linear material, which linear steps use. |
|
||||
| Isotropic elastic with `{"type": "mooney_rivlin", "c01": …}` or `{"type": "yeoh", "c20": …, "c30": …}` | Decoupled polynomials W̄ = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ with the same volume κ/2 (J − 1)²: Mooney–Rivlin with C₁₀ = μ/2 − C₀₁ (0 ≤ C₀₁ ≤ μ/2), Yeoh with C₁₀ = μ/2, so 2(C₁₀ + C₀₁) is the elastic μ and the small-strain material stays the elastic one. Stress and tangent from the fictitious τ̄ and c̄ (Holzapfel 2000, eqs. 6.168, 6.193). Abaqus `*HYPERELASTIC, MOONEY-RIVLIN` / `YEOH`; LS-DYNA refuses them by name. |
|
||||
|
||||
| Isotropic elastic with `"plastic"` (bilinear isotropic or kinematic, table, Johnson–Cook) | J2 plasticity (`fieldcraft_fem::elastoplastic`): at finite strain Simo's multiplicative model (CMAME 99, 1992) with a Hencky elastic energy, τ = K ln J I + 2G dev ½ ln bₑ, the history Cₚ⁻¹ and the equivalent plastic strain, integrated by the exponential return map on the principal logarithmic strains; at small strain the additive radial return with the back stress. Both call `J2::radial_return`, the update of explicit dynamics, and its consistent tangent `J2::tangent`. |
|
||||
|
||||
**Plasticity's tangent.** With Cₚ⁻¹ frozen over an iteration, L_v τ = 𝔻ᵉᵖ : ½ (∂ ln b/∂b) : (d b + b d) − (d τ + τ d), 𝔻ᵉᵖ the algorithmic tangent of the return
|
||||
(K I⊗I + 2Gθ ℙ − 2Gθ̄ n⊗n, Simo & Hughes 1998 Box 3.2) and the derivative of the logarithm the
|
||||
Daleckii–Krein divided differences θ_AB = artanh(r)/r, r = (λ²_A − λ²_B)/(λ²_A + λ²_B), in the
|
||||
principal frame of the trial bₑ: smooth through equal stretches, so uniaxial and undeformed
|
||||
states need no special case. The tangent is symmetric (isotropic hardening) and checked against
|
||||
central differences of the stress and of every element's forces from a plastic history. Hybrid
|
||||
elements take the deviatoric return and the volume K/2 (ln J)². Refused by name in nonlinear
|
||||
steps: kinematic hardening at finite strain (a back stress is not coaxial with bₑ, which the
|
||||
principal-frame return needs; it works with small strain), the Johnson–Cook strain-rate term (a
|
||||
static step has no time; C = 0 is the rate-independent law), and incompatible-mode elements of a
|
||||
plastic material. Damage, erosion and adiabatic heating act in explicit dynamics only (a warning
|
||||
says so). A hardening slope below −3G leaves the point without a tangent: the increment cuts
|
||||
back and the step stops at the minimum increment, naming the element.
|
||||
|
||||
"Isotropic elastic" keeps meaning the same thing everywhere: the neo-Hookean model is chosen
|
||||
only by the explicit option, and explicit dynamics refuses a hyperelastic material by name.
|
||||
@@ -190,9 +207,11 @@ differences of its forces.
|
||||
| plane stress | refused (σ_zz = 0 needs a local iteration for the thickness) |
|
||||
| trusses, shells, beams | refused (large rotations of structural elements come later in M15) |
|
||||
|
||||
Without nonlinear geometry (`"nonlinear_geometry": false`) the elements are the linear ones:
|
||||
every material is elastic today, so such a step is the linear solution reached by Newton (the
|
||||
path the next materials, plasticity first, take with small strains).
|
||||
Without nonlinear geometry (`"nonlinear_geometry": false`) an elastic element is the linear one
|
||||
(K u), and an element of a plastic material takes the same loops with small strain
|
||||
(`Kinematics::Small`: ε = sym ∇u, the reference gradients, f = ∫ Bᵀσ dV, K = ∫ Bᵀ𝔻B dV with the
|
||||
consistent tangent, no geometric stiffness; hybrid elements project 1 + tr ε and K tr ε), so an
|
||||
elastic model is still the linear solution reached by Newton.
|
||||
|
||||
**Follower pressure** (`fieldcraft_fem::face::follower_pressure`, `follower_edge_pressure`):
|
||||
fᵢ = −p ∫ Nᵢ (x,ξ × x,η) dξ dη on the current face, with its load stiffness
|
||||
@@ -207,6 +226,23 @@ the rate of convergence of open loaded surfaces; it is revisited with the symmet
|
||||
(LDLᵀ) factorization that arc-length and Taylor–Hood need. Without nonlinear geometry a pressure
|
||||
is a dead load.
|
||||
|
||||
**Load path.** A step's `load_path`, `[[fraction, factor], …]` from `[0, 0]` to fraction 1, makes
|
||||
the load factor piecewise linear in the step fraction (without one it equals the fraction), so
|
||||
loads and prescribed displacements can come down or reverse within a step: cyclic loading.
|
||||
Increments end at every point of the path, and the extrapolated predictor scales with the change
|
||||
of the load factor, so it turns with the path. LS-DYNA writes the path as the step's curve;
|
||||
Abaqus refuses it by name for now (it would be an amplitude on every load and boundary
|
||||
condition).
|
||||
|
||||
**Predictors.** An increment starts from the linear extrapolation of the last one; with
|
||||
path-dependent materials an increment without one (the first, after a turn of the path to zero
|
||||
change) starts from the tangent predictor: one solve with the converged state's tangent for the
|
||||
change of the loads and of the prescribed displacements, so a prescribed motion is spread into the
|
||||
interior before the material sees it (started from the boundary's jump alone, the elements next
|
||||
to it would flow plastically far beyond the solution). Its work is then the energy criterion's
|
||||
reference. A first tangent that is not positive definite is a cutback when the undeformed
|
||||
model's tangent is (the strain of the increment, not a model that can move freely).
|
||||
|
||||
**Newton–Raphson** with the consistent tangent refactored every iteration, an optional line
|
||||
search (off by default: on large elastic rotations the full step converges in fewer
|
||||
iterations), and convergence when the force residual (∞-norm over the largest force seen), the
|
||||
@@ -219,19 +255,35 @@ tangent is not positive definite, or the iterations run out; the step stops with
|
||||
and the reason at the minimum increment, the increment cap, or `Limits::max_newton_iterations`.
|
||||
|
||||
**Results**: the final displacements and reactions, the **Cauchy stress** and the
|
||||
**logarithmic strain** (ln V, engineering shears) at the integration points and averaged at the
|
||||
nodes, every increment's load factor, iterations and residuals, and a displacement frame per
|
||||
**logarithmic strain** (ln V, engineering shears; the small strain without nonlinear geometry) at
|
||||
the integration points and averaged at the nodes, the **equivalent plastic strain** at the
|
||||
integration points of plastic elements (`solve.probe` with `elements`, the largest in the step
|
||||
summary), every increment's load factor, iterations and residuals, and a displacement frame per
|
||||
converged increment (`results.frame`, the slider in the results panel).
|
||||
|
||||
**Couplings** stay the linear constraints of the linear solver (small rotations of the
|
||||
reference points), with a warning; springs act along their fixed directions. Temperature and
|
||||
centrifugal loads are refused by name in nonlinear steps.
|
||||
reference points), with a warning; springs act along their fixed directions. Centrifugal loads
|
||||
are refused by name in nonlinear steps.
|
||||
|
||||
**Thermal strain.** A nonlinear step's temperatures are its temperature loads, or the steady
|
||||
conduction of its temperature boundary conditions and heat loads solved first (one-way, as the
|
||||
linear coupled temperature–displacement step); they ramp with the load factor from each
|
||||
material's reference temperature. With nonlinear geometry F = F_m F_θ, F_θ = diag(1 + αᵢ ΔT) at
|
||||
each integration point (V. A. Lubarda, Appl. Mech. Rev. 57:95–108, 2004): the material sees F_m,
|
||||
the loops run on the expanded volume J_θ dV₀ (mixed elements project det F_m), and the reported
|
||||
strain is the total logarithmic one; the linear stretch 1 + α ΔT keeps α the engineering
|
||||
coefficient, so a free body grows by exactly α ΔT. Without nonlinear geometry the split is
|
||||
additive (ε_m = ε − α ΔT), and elastic elements on their linear stiffness take the equivalent
|
||||
nodal forces as a dead load. Johnson–Cook plasticity softens at the point's temperature. The
|
||||
expansion is constant in temperature (the model has no temperature-dependent coefficient yet).
|
||||
|
||||
**Decks**: Abaqus `*STEP, NLGEOM=YES|NO, INC=` with `*STATIC` (initial, period 1, minimum,
|
||||
maximum) and `*CONTROLS, PARAMETERS=LINE SEARCH`; the neo-Hookean option as `*HYPERELASTIC, NEO
|
||||
HOOKE`. LS-DYNA: the ramped implicit run with full Newton (`ilimit` 1, `maxref`) and
|
||||
HOOKE`; plasticity as for explicit dynamics (`*PLASTIC`); a load path, and a plastic deck of
|
||||
several general steps (Abaqus carries the plastic state from one to the next, every model step
|
||||
starts undeformed), are refused. LS-DYNA: the ramped implicit run with full Newton (`ilimit` 1, `maxref`) and
|
||||
`*CONTROL_IMPLICIT_AUTO`; hyperelastic materials in nonlinear steps and steps without nonlinear
|
||||
geometry are refused. Nastran refuses nonlinear steps (SOL 400) by name. Iteration caps and
|
||||
geometry are refused; a load path is the step's curve. Nastran refuses nonlinear steps (SOL 400) by name. Iteration caps and
|
||||
tolerances are left to each solver's own controls, whose norms differ from ours.
|
||||
|
||||
**Verification** (`crates/solver/tests/nonlinear.rs`, `crates/fem/src/{constitutive,finite,face}.rs`,
|
||||
@@ -242,4 +294,24 @@ test on a distorted mesh (all solids, hybrid ones included); a 90° rigid rotati
|
||||
the elastica (Bisshopp–Drucker) at P L²/(E I) = 1, 2, 5, 10 with hex20 (0.2–0.5 %); the inflation of a thick neo-Hookean tube by a follower pressure (Rivlin's closed form);
|
||||
Newton's quadratic convergence; the linear limit; a finite-strain Cook's membrane is pending
|
||||
its transcribed reference; and the failures (an element inverted by
|
||||
crushing stops at the minimum increment, naming it; the caps; what is refused).
|
||||
crushing stops at the minimum increment, naming it; the caps; what is refused). Plasticity
|
||||
(`crates/solver/tests/plasticity.rs`, `crates/fem/src/elastoplastic.rs`, `tests/oracle/`
|
||||
`nl-j2-uniaxial`, `j2-bauschinger`, `plastic-thick-cylinder`, `plastic-hinge-cantilever`): the
|
||||
consistent tangents against central differences from a plastic history (material point and
|
||||
every element, finite and small strain, full and hybrid); uniaxial tension to a logarithmic
|
||||
strain of 0.2 in every element kind, hybrid ones included (Kirchhoff stress exact to 10⁻⁷, and
|
||||
0.13 % from explicit dynamics' hypoelastic Cauchy stress, the elastic volume change between the
|
||||
two laws); the Bauschinger effect on a load path back to zero strain (exact); Hill's thick
|
||||
cylinder, the bore displacement at a plastic radius of 1.5a (0.009 %) and the collapse pressure
|
||||
(2·10⁻⁸); a cantilever's collapse 3.1 % above beam theory's hinge, converged under refinement;
|
||||
Newton's order with the consistent tangent 1.9 (small strain) and 2.3 (finite strain) in a
|
||||
plastic cantilever; and the refusals and the softening material that stops at the minimum
|
||||
increment. Mooney–Rivlin and Yeoh (`nl-mooney-rivlin-modes`, `nl-yeoh-uniaxial`,
|
||||
`nl-mooney-rivlin-tube`): tangents by central differences (material point and hybrid
|
||||
elements), Treloar's uniaxial, equibiaxial and pure-shear modes against the compressible closed
|
||||
form (exact), and a tube inflated by a follower pressure while stretched axially to λ_z = 1.5
|
||||
(Rivlin's incompressible solution, 0.6 % at ν = 0.4995). Thermal strain (`nl-thermal-expansion`,
|
||||
`j2-heated-bar`, `crates/solver/tests/nonlinear_thermal.rs`): free expansion and a fully held
|
||||
block at a thermal stretch of 1.2 (exact, full and hybrid), a bar held at its ends heated past
|
||||
yield (exact), the linear limit against the linear and coupled solvers, and tangents with a
|
||||
temperature field.
|
||||
+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).
|
||||
|
||||
+31
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the policy is [verification.md](verification.md). Every case is our own model, built with FieldCraft commands from the problem's published description, and compared with the reference value its `expected.json` cites, within the stated tolerance and for the stated reason. Accepted results are frozen in each case's `fixture.json`: a later change that moves one beyond its band (10⁻⁹ of its size) fails CI even while the reference still passes. No reference value was produced with a commercial product.
|
||||
|
||||
**142 of 148 quantities pass** (6 known failure(s)) in 40 active cases: 31 theory, 9 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built.
|
||||
**163 of 169 quantities pass** (6 known failure(s)) in 49 active cases: 40 theory, 9 published benchmark, 0 experiment; 9 pending cases wait for capabilities still being built.
|
||||
|
||||
## Theory: closed-form solutions, patch tests and mesh invariants
|
||||
|
||||
@@ -31,6 +31,10 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [free-beam-frequencies](../tests/oracle/free-beam-frequencies/) | sixth frequency (the last rigid-body mode) | 0 | ≈ 0 | ≈ 0 | ± 8.1264e-5 | pass | matches |
|
||||
| [free-beam-frequencies](../tests/oracle/free-beam-frequencies/) | first free–free bending frequency (mode 7) | 0.081264 | 0.080618 | -0.795% | 1.5% | pass | matches |
|
||||
| [free-beam-frequencies](../tests/oracle/free-beam-frequencies/) | total mass | 20 | 20 | < 1e-7% | 1e-12 rel. | pass | matches |
|
||||
| [j2-bauschinger](../tests/oracle/j2-bauschinger/) | axial stress back at zero strain after a pull to ε_a = 0.01 | -247.52 | -247.52 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [j2-bauschinger](../tests/oracle/j2-bauschinger/) | accumulated equivalent plastic strain | 0.016089 | 0.016089 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [j2-heated-bar](../tests/oracle/j2-heated-bar/) | axial stress, ΔT = 300 (α ΔT = 3.6·10⁻³, 2.9 times the yield strain) | -254.65 | -254.65 | < 1e-7% | 1e-8 rel. | pass | matches |
|
||||
| [j2-heated-bar](../tests/oracle/j2-heated-bar/) | equivalent plastic strain | 0.0023267 | 0.0023267 | < 1e-7% | 1e-7 rel. | pass | matches |
|
||||
| [j2-uniaxial-explicit](../tests/oracle/j2-uniaxial-explicit/) | axial Cauchy stress at a logarithmic strain of 0.2 | 643.56 | 643.57 | 0.000174% | 1% | pass | matches |
|
||||
| [j2-uniaxial-explicit](../tests/oracle/j2-uniaxial-explicit/) | equivalent plastic strain | 0.19678 | 0.19678 | -0.00000113% | 1% | pass | matches |
|
||||
| [kirsch-plate](../tests/oracle/kirsch-plate/) | σ_xx at the top of the hole (0, 1, 0) over the remote stress | 3 | 3.0121 | 0.402% | 2% | pass | matches |
|
||||
@@ -57,6 +61,15 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip shortening δ_h/L at P L²/(E I) = 5 | 0.38763 | 0.38955 | 0.495% | 2% | pass | matches |
|
||||
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip deflection δ_v/L at P L²/(E I) = 10 | 0.81061 | 0.81421 | 0.444% | 1% | pass | matches |
|
||||
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip shortening δ_h/L at P L²/(E I) = 10 | 0.555 | 0.558 | 0.541% | 2% | pass | matches |
|
||||
| [nl-j2-uniaxial](../tests/oracle/nl-j2-uniaxial/) | axial Cauchy stress at a logarithmic strain of 0.2 | 642.74 | 642.74 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-j2-uniaxial](../tests/oracle/nl-j2-uniaxial/) | equivalent plastic strain | 0.19678 | 0.19678 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | axial Cauchy stress, uniaxial stretch λ = 2 | 2.7366 | 2.7366 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | lateral displacement at (1, 1, 1), uniaxial: l − 1 | -0.28647 | -0.28647 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | in-plane Cauchy stress, equibiaxial stretch λ = 1.5 | 2.8427 | 2.8427 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | Cauchy stress along the stretch, pure shear λ = 2 | 3.5261 | 3.5261 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | Cauchy stress across, in the held direction, pure shear λ = 2 | 1.5067 | 1.5067 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-mooney-rivlin-tube](../tests/oracle/nl-mooney-rivlin-tube/) | radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15 at λ_z = 1.5 | 5 | 5.0311 | 0.622% | 1% | pass | matches |
|
||||
| [nl-mooney-rivlin-tube](../tests/oracle/nl-mooney-rivlin-tube/) | radial displacement of the outer wall (20, 0, 0): b − B, a small difference of radii | 0.61553 | 0.64339 | 2.8e-2 | ± 0.04 | pass | matches |
|
||||
| [nl-neo-hookean-uniaxial](../tests/oracle/nl-neo-hookean-uniaxial/) | axial Cauchy stress at the corner (1, 1, 1), stretch λ = 2 | 8.0601 | 8.0601 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-neo-hookean-uniaxial](../tests/oracle/nl-neo-hookean-uniaxial/) | lateral displacement u_y at the corner (1, 1, 1), l − 1 | -0.18686 | -0.18686 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | u_x at inner node 22 | 0.13583 | 0.13583 | < 1e-7% | 1e-8 rel. | pass | matches |
|
||||
@@ -72,6 +85,11 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [nl-rigid-rotation](../tests/oracle/nl-rigid-rotation/) | u_y at inner node 22: the rotation's | 0.078591 | 0.078591 | -5.6e-17 | ± 1.0000e-9 | pass | matches |
|
||||
| [nl-rigid-rotation](../tests/oracle/nl-rigid-rotation/) | u_z at inner node 22: the rotation's | -0.067441 | -0.067441 | -1.2e-16 | ± 1.0000e-9 | pass | matches |
|
||||
| [nl-rigid-rotation](../tests/oracle/nl-rigid-rotation/) | von Mises stress at inner node 22 | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-8 | pass | matches |
|
||||
| [nl-thermal-expansion](../tests/oracle/nl-thermal-expansion/) | displacement of the corner (1, 1, 1) along x, free, α ΔT = 0.2 | 0.2 | 0.2 | < 1e-7% | 1e-8 rel. | pass | matches |
|
||||
| [nl-thermal-expansion](../tests/oracle/nl-thermal-expansion/) | stress of the free body (σ_xx at the corner) | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-8 | pass | matches |
|
||||
| [nl-thermal-expansion](../tests/oracle/nl-thermal-expansion/) | stress of the held block, σ_xx (hydrostatic) | -3.5108 | -3.5108 | < 1e-7% | 1e-8 rel. | pass | matches |
|
||||
| [nl-yeoh-uniaxial](../tests/oracle/nl-yeoh-uniaxial/) | axial Cauchy stress, stretch λ = 2 | 3.1743 | 3.1743 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-yeoh-uniaxial](../tests/oracle/nl-yeoh-uniaxial/) | lateral displacement at (1, 1, 1): l − 1 | -0.28545 | -0.28545 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [patch-hex20](../tests/oracle/patch-hex20/) | u_x at the corner (1, 1, 1) | -2.5000e-4 | -2.5000e-4 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [patch-hex20](../tests/oracle/patch-hex20/) | u_y at the corner (1, 1, 1) | 0.001 | 0.001 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [patch-hex20](../tests/oracle/patch-hex20/) | u_z at the corner (1, 1, 1) | 0.00225 | 0.00225 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
@@ -121,6 +139,9 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [patch-wedge6](../tests/oracle/patch-wedge6/) | σ_yy at the inner node (0.46, 0.55, 0.53) | 2 | 2 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [patch-wedge6](../tests/oracle/patch-wedge6/) | σ_zz at the inner node (0.46, 0.55, 0.53) | 3 | 3 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [patch-wedge6](../tests/oracle/patch-wedge6/) | σ_xy at the inner node | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-9 | pass | matches |
|
||||
| [plastic-hinge-cantilever](../tests/oracle/plastic-hinge-cantilever/) | tip force at a tip deflection of 15 (4.5 times the first yield's) | -312.5 | -322.61 | -3.23% | 5% | pass | matches |
|
||||
| [plastic-thick-cylinder](../tests/oracle/plastic-thick-cylinder/) | bore displacement at the pressure that takes the plastic zone to c = 15 (a = 10, b = 20, σ_y = 250) | 0.024355 | 0.024358 | 0.0089% | 0.2% | pass | matches |
|
||||
| [plastic-thick-cylinder](../tests/oracle/plastic-thick-cylinder/) | radial force on the bore (per radian times 2π, height 1) at a bore displacement of 0.3, 24 times the first yield's: the collapse pressure 2k ln 2 = 200.094 times 2πa | 12572 | 12572 | 0.00000178% | 0.1% | pass | matches |
|
||||
| [pure-bending-coupling](../tests/oracle/pure-bending-coupling/) | bending stress σ_xx at mid-span, top fibre (10, 0.5, 1) | 15 | 15 | -0.0000103% | 0.5% | pass | matches |
|
||||
| [pure-bending-coupling](../tests/oracle/pure-bending-coupling/) | rotation of the reference point about y | 0.3 | 0.30355 | 1.18% | 1% | known failure: 1.2% high: the distributing coupling weights every node of the end face equally, corner and mid-side nodes of hex20 alike, which is not the consistent linear traction; the end face deforms locally and the fitted rotation includes it. Area-consistent weights would remove it | — |
|
||||
| [rotating-cylinder-axisymmetric](../tests/oracle/rotating-cylinder-axisymmetric/) | radial stress σ_r on the axis (r = 0) | 0.38571 | 0.38608 | 0.0958% | 1% | pass | matches |
|
||||
@@ -173,6 +194,8 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
- **[cdt-cylinder](../tests/oracle/cdt-cylinder/)** (theory): Constrained Delaunay mesh of a cylinder: volume within the chordal bound, valid elements. Source: mesh invariants: an inscribed polygon of sagitta s loses at most 2s/R of a circle's area (elementary geometry); constrained Delaunay tetrahedralization after Diazzi et al., ACM TOG 42(6) 2023. Tolerances — volume: the tessellation inscribes the cylinder within the chordal tolerance s = 0.05: the polygon section loses between 0 and 2s/R = 0.5% of the area; the reference is the middle of that band; invalid: every element of a valid mesh has positive volume
|
||||
- **[cdt-stepped-block](../tests/oracle/cdt-stepped-block/)** (theory): Constrained Delaunay mesh of a stepped block: exact volume, valid elements. Source: mesh invariants: a tetrahedralization of a polyhedron fills its volume exactly (Si, TetGen paper ACM TOMS 41(2) 2015 for the definition of a CDT; Diazzi et al., ACM TOG 42(6) 2023); the volume is 60·40·10 + 40·40·10 + 20·40·10 − 10·10·10. Tolerances — volume: a planar boundary is tessellated exactly and the constrained Delaunay mesh fills it exactly: only rounding; invalid: every element of a valid mesh has positive volume; undetermined: a linear tetrahedron's sign is exact
|
||||
- **[free-beam-frequencies](../tests/oracle/free-beam-frequencies/)** (theory): Natural frequencies of a free beam: rigid-body modes and the first bending mode. Source: Euler–Bernoulli free–free beam, β₁L = 4.7300; Timoshenko, Young and Weaver, Vibration Problems in Engineering, 4th ed., Wiley 1974, §5.3. Tolerances — rigid_6: a free body has six zero frequencies; numerical rigid-body frequencies below 10⁻³ of the first elastic one (eigenvalues below 10⁻⁶ of it) are rounding of the shifted solve; f7: Euler–Bernoulli neglects shear and rotary inertia, about 0.8% at L/h = 20 for the free–free mode; mass: the mass matrix integrates the density exactly
|
||||
- **[j2-bauschinger](../tests/oracle/j2-bauschinger/)** (theory): Bauschinger effect of linear kinematic hardening: tension, then back to zero strain. Source: J2 plasticity with Prager's linear kinematic hardening in uniaxial stress (W. Prager, Ing.-Arch. 1955; Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3): from σ_a = σ_y + E_t (ε_a − σ_y/E) the material unloads elastically by 2σ_y and then hardens with E_t, so at zero strain σ = σ_a − 2σ_y − E_t (ε_a − 2σ_y/E). Tolerances — residual_stress: a homogeneous small-strain state the element represents exactly, with increments that end at the load path's turn; isotropic hardening would give −281.835 (it yields again only at −σ_a); plastic_strain: (σ_a − σ_y)/H forward plus E/(E + H) (ε_a − 2σ_y/E) in reverse
|
||||
- **[j2-heated-bar](../tests/oracle/j2-heated-bar/)** (theory): A steel bar held at its ends and heated past yield (small strain). Source: Uniaxial J2 plasticity with linear hardening (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3) with the additive thermal strain: the bar's total axial strain is held at zero, so its mechanical strain is −α ΔT and σ = −(σ_y + E_t (α ΔT − σ_y/E)) once α ΔT exceeds σ_y/E. Tolerances — sigma: a homogeneous uniaxial state the elements represent exactly; plastic_strain: (\|σ\| − σ_y)/H, as for the stress
|
||||
- **[j2-uniaxial-explicit](../tests/oracle/j2-uniaxial-explicit/)** (theory): Uniaxial tension of a J2 bilinear-hardening cube to large strain (explicit). Source: von Mises plasticity with linear isotropic hardening under uniaxial stress (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3); logarithmic strain for a hypoelastic–plastic law under coaxial stretching. Tolerances — sigma: uniaxial J2 with linear hardening: σ = σ_y + E_t (ε − σ_y/E) in logarithmic strain; the stretch takes 10⁴ wave transits, so inertia and the ringing of the start (ρcv ≈ 0.1% of σ) stay well under 1%; plastic_strain: ε_p = ε − σ/E, as for the stress
|
||||
- **[kirsch-plate](../tests/oracle/kirsch-plate/)** (theory): Plate with a circular hole in tension (Kirsch). Source: Kirsch, Zeitschrift des VDI 42 (1898) 797–807; Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §35. Tolerances — kt: the reference is the infinite plate; the finite plate (W = 60a, Heywood's width correction about +0.1%, and the loaded ends 30a away) raises the peak by a few tenths of a percent, and the stress extrapolated to the hole surface adds up to about 1%; side: infinite-plate value −S; finite width and extrapolation as for Kt, on a smaller value
|
||||
- **[kirsch-plate-plane-stress](../tests/oracle/kirsch-plate-plane-stress/)** (theory): Plate with a circular hole in tension, 2D plane stress (Kirsch). Source: Kirsch, Zeitschrift des VDI 42 (1898) 797–807; Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §35. Tolerances — kt: the reference is the infinite plate; the finite plate (W = 60a, loaded ends 30a away) raises the peak by a few tenths of a percent, and the stress extrapolated to the hole surface adds up to about 1%; side: infinite-plate value −S; finite width and extrapolation as for Kt, on a smaller value
|
||||
@@ -189,9 +212,14 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
- **[nafems-le10](../tests/oracle/nafems-le10/)** (benchmark): NAFEMS LE10 thick plate under pressure (hex20). Source: NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE10 (target σ_yy = −5.38 MPa at D); as reproduced in the Abaqus 2017 Benchmarks Guide, 'LE10: Thick plate under pressure'. Tolerances — syy_D: the target is published to three figures; at D the stress is extrapolated to a corner where the hole edge meets the loaded face, and published converged solutions scatter by 1–2% (Abaqus Benchmarks Guide fine meshes: −5.51 to −5.89)
|
||||
- **[nl-cylinder-inflation](../tests/oracle/nl-cylinder-inflation/)** (theory): Inflation of a thick neo-Hookean cylinder by a follower pressure (plane strain). Source: Finite inflation of an incompressible neo-Hookean thick-walled tube in plane strain: r² = R² + a² − A², σ_θ − σ_r = μ (λ² − λ⁻²) with λ = r/R, integrated through the wall: p = (μ/2)[(ln λ_a² − λ_a⁻²) − (ln λ_b² − λ_b⁻²)] (R. S. Rivlin, Large elastic deformations of isotropic materials VI, Phil. Trans. R. Soc. A 242:173–195, 1949; R. W. Ogden, Non-Linear Elastic Deformations, Dover 1997, §5.3.2). Tolerances — ua: the closed form is incompressible and our material nearly so (κ/μ = 1000, a volume change of order p/κ ≈ 4·10⁻⁴); hybrid hex20, 6 × 16, follow the wall's strain gradient well inside; ub: as at the bore
|
||||
- **[nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/)** (theory): Large-deflection cantilever under a dead tip load against the elastica (solids). Source: The elastica of a cantilever under a vertical end load: K. E. Bisshopp, D. C. Drucker, Large deflection of cantilever beams, Quarterly of Applied Mathematics 3(3):272–275, 1945 (elliptic-integral solution); values computed by integrating θ″ = −(P/EI) cos θ by shooting (our own RK4, 20000 steps), agreeing with the table of K. Mattiasson, Numerical results from large deflection beam and frame problems analysed by means of elliptic integrals, IJNME 17(1):145–153, 1981, to all six digits. Tolerances — v1: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u1: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's; v2: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u2: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's; v5: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u5: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's; v10: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u10: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's
|
||||
- **[nl-j2-uniaxial](../tests/oracle/nl-j2-uniaxial/)** (theory): Uniaxial tension of a J2 bilinear-hardening cube to large strain (implicit, finite strain). Source: J2 plasticity with linear isotropic hardening in uniaxial stress (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3) in the logarithmic strain, which Simo's exponential return map (CMAME 99:61–112, 1992) integrates exactly on a proportional path: Kirchhoff stress τ = σ_y + E_t (ε − σ_y/E), Cauchy stress σ = τ/J with ln J = τ/(3K) (Hencky's volume law). Tolerances — sigma: a homogeneous deformation the element represents exactly, reached on a proportional path: the tolerance only admits the Newton tolerance and rounding; plastic_strain: ε_p = (τ − σ_y)/H, as for the stress
|
||||
- **[nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/)** (theory): Mooney–Rivlin block in uniaxial, equibiaxial and pure-shear tension. Source: Treloar's homogeneous modes of rubber testing (L. R. G. Treloar, The Physics of Rubber Elasticity, 3rd ed., Oxford 1975, ch. 10) for the decoupled compressible energy W = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + κ/2 (J − 1)²: principal Cauchy stresses σᵢ = (λᵢ/J) ∂W/∂λᵢ with the free stretch found so the free faces carry no stress (bisection; E = 3, ν = 0.49, C₀₁ = 0.2, C₁₀ = μ/2 − C₀₁). Tolerances — uniaxial: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; uniaxial_lateral: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; equibiaxial: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; shear_xx: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; shear_yy: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding
|
||||
- **[nl-mooney-rivlin-tube](../tests/oracle/nl-mooney-rivlin-tube/)** (theory): Inflation of a stretched Mooney–Rivlin tube (follower pressure). Source: Finite inflation and extension of an incompressible Mooney–Rivlin tube (R. S. Rivlin, Large elastic deformations of isotropic materials VI, Phil. Trans. R. Soc. A 242:173–195, 1949; R. W. Ogden, Non-Linear Elastic Deformations, Dover 1997, §5.3): at axial stretch λ_z, r² = a² + (R² − A²)/λ_z, σ_θ − σ_r = 2 (λ_θ² − λ_r²)(C₁₀ + C₀₁ λ_z²) with λ_θ = r/R, λ_r = 1/(λ_θ λ_z), integrated through the wall: p = ∫ (σ_θ − σ_r) dr/r (Simpson, 2·10⁵ intervals). Tolerances — ua: 16 × 6 hybrid hex20 around the quarter; ν = 0.4995 stands in for incompressibility (a volume change of order p/κ ≈ 10⁻³ shifts the radii by about 0.1 %), and the tube is stretched axially by its ends, which a nearly incompressible solid takes with larger pressures than the plane-strain neo-Hookean tube; ub: 0.2 % of the outer radius b = 20.616: the same volume change of order 10⁻³ moves b by about 0.03, which is 4.5 % of b − B; the incompressible kinematics r² = a² + (R² − A²)/λ_z amplify it there
|
||||
- **[nl-neo-hookean-uniaxial](../tests/oracle/nl-neo-hookean-uniaxial/)** (theory): Uniaxial stretch of a neo-Hookean block to twice its length. Source: Closed form of the compressible neo-Hookean model W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)² in uniaxial stress (G. A. Holzapfel, Nonlinear Solid Mechanics, Wiley 2000, §6.5, eq. 6.148 with the volumetric term of §6.4): σ_yy = μ J^(−5/3)(l² − Ī/3) + κ(J − 1) = 0 fixes the lateral stretch l, then σ_xx = μ J^(−5/3)(λ² − l²). Tolerances — sigma: a homogeneous deformation every element represents exactly: the tolerance only admits rounding and the Newton tolerance; lateral: a homogeneous deformation every element represents exactly: the tolerance only admits rounding and the Newton tolerance
|
||||
- **[nl-patch-hex8](../tests/oracle/nl-patch-hex8/)** (theory): Nonlinear patch test, hex8: a homogeneous finite deformation of a distorted mesh. Source: The patch test (B. M. Irons, A. Razzaque, in A. K. Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, 557–587) at finite strain: a boundary moved by x = F X makes F the solution everywhere (J. Bonet, R. D. Wood, Nonlinear Continuum Mechanics for Finite Element Analysis, 2nd ed., Cambridge 2008, §8); the neo-Hookean Cauchy stress σ = μ J^(−5/3) dev b + κ (J − 1) I of that F. Tolerances — ux: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; uy: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; uz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; sxx: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; syy: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; szz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; sxy: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; sxz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; syz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance
|
||||
- **[nl-rigid-rotation](../tests/oracle/nl-rigid-rotation/)** (theory): A quarter turn strains and stresses nothing (objectivity). Source: Objectivity of the total Lagrangian formulation and of the hyperelastic stress: a rigid motion x = R X + c gives C = I and σ = 0 whatever the rotation (Bonet & Wood 2008, §3.4 and §5.2; T. Belytschko, W. K. Liu, B. Moran, Nonlinear Finite Elements for Continua and Structures, 2nd ed., Wiley 2014, §5.5). Tolerances — ux: the inner nodes follow the rigid rotation exactly; uy: the inner nodes follow the rigid rotation exactly; uz: the inner nodes follow the rigid rotation exactly; von_mises: zero up to rounding of stresses of order E = 1000: a 90° turn leaves no strain
|
||||
- **[nl-thermal-expansion](../tests/oracle/nl-thermal-expansion/)** (theory): Free and fully constrained thermal expansion at finite strain. Source: The multiplicative split F = F_m F_θ with F_θ = (1 + α ΔT) I (V. A. Lubarda, Appl. Mech. Rev. 57(2):95–108, 2004): a free body follows F = F_θ stress-free; held on every face, F = I and F_m = ϑ⁻¹ I, so the compressible neo-Hookean material carries the pressure of its volume ratio, σ = κ (ϑ⁻³ − 1). Tolerances — free_u: a homogeneous stretch every element represents exactly; free_stress: rounding only; held_stress: a homogeneous state; E = 10, ν = 0.3 (κ = 8.3333), ϑ = 1.2
|
||||
- **[nl-yeoh-uniaxial](../tests/oracle/nl-yeoh-uniaxial/)** (theory): Yeoh block in uniaxial tension. Source: Uniaxial tension of the decoupled compressible Yeoh energy W = C₁₀ (Ī₁ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ + κ/2 (J − 1)² (O. H. Yeoh, Rubber Chem. Technol. 66(5):754–771, 1993): σ = (λ/J) ∂W/∂λ with the lateral stretch found so σ_yy = 0 (bisection; E = 3, ν = 0.49, C₁₀ = μ/2, C₂₀ = −0.02, C₃₀ = 0.004). Tolerances — sigma: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; lateral: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding
|
||||
- **[patch-hex20](../tests/oracle/patch-hex20/)** (theory): Constant-stress patch test, hex20. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
- **[patch-hex20-reduced](../tests/oracle/patch-hex20-reduced/)** (theory): Constant-stress patch test, hex20-reduced. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
- **[patch-hex8](../tests/oracle/patch-hex8/)** (theory): Constant-stress patch test, hex8. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
@@ -199,6 +227,8 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
- **[patch-tet10](../tests/oracle/patch-tet10/)** (theory): Constant-stress patch test, tet10. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
- **[patch-tet4](../tests/oracle/patch-tet4/)** (theory): Constant-stress patch test, tet4. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
- **[patch-wedge6](../tests/oracle/patch-wedge6/)** (theory): Constant-stress patch test, wedge6. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)
|
||||
- **[plastic-hinge-cantilever](../tests/oracle/plastic-hinge-cantilever/)** (theory): Plastic collapse of a cantilever: the fully plastic moment at the root. Source: Limit analysis (B. G. Neal, The Plastic Methods of Structural Analysis, 3rd ed., Chapman & Hall 1977, ch. 1–2): beam theory's plastic hinge at the root, the fully plastic moment M_p = σ_y b h²/4 of a rectangular section, collapses a cantilever of length L at P_c = M_p/L = 312.5 (b = h = 10, L = 200, σ_y = 250); a bound for the solid, whose collapse lies above it (W. Prager, P. G. Hodge, Theory of Perfectly Plastic Solids, Wiley 1951: a continuum mechanism allows no normal velocity jump, which the beam's hinge rotation is). Tolerances — collapse_force: the solid's collapse lies above beam theory's hinge: refined through the depth (4 to 8 elements) and along the length (40 to 160) the force settles 3.1 % above M_p/L (322.3), so the band is that bound's gap; the root is a symmetry plane (no rotation, free lateral contraction) held on average through a coupling, the tip pushed through another; reduced hex20, 20 × 2 × 4
|
||||
- **[plastic-thick-cylinder](../tests/oracle/plastic-thick-cylinder/)** (theory): Elastic–perfectly plastic thick cylinder: plastic zone and collapse pressure. Source: Hill, The Mathematical Theory of Plasticity, Oxford 1950, ch. V §2: a von Mises cylinder in plane strain, elastically incompressible, with the plastic zone a ≤ r ≤ c under the pressure p = k (2 ln(c/a) + 1 − c²/b²), k = σ_y/√3, and the bore displacement u_a = k c²/(2 G a); collapse when c = b at p = 2k ln(b/a). Tolerances — u_bore: 20 reduced quad8 elements through the wall resolve the logarithmic plastic field and ν = 0.4999 stands in for Hill's incompressible elasticity (0.009 % apart); u_a ∝ c², so 0.2 % on u_a is 0.1 % on the plastic radius; collapse_force: once the wall is plastic through, perfect plasticity holds the force at the collapse value (σ_θ − σ_r = 2k everywhere, σ_z the mean of the others as plastic flow dominates); the band admits the quadrature of the logarithmic field
|
||||
- **[pure-bending-coupling](../tests/oracle/pure-bending-coupling/)** (theory): Pure bending through a distributing coupling. Source: Euler–Bernoulli pure bending, σ = M_y z/I, θ = ML/(EI) (exact in elasticity for ν = 0); Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §102. Tolerances — sigma_top: with ν = 0 pure bending is a quadratic displacement field that hex20 represents exactly; the coupling's nodal force pattern differs from the linear traction only within a depth or two of the end (Saint-Venant), five depths from the probe; rotation: the reference point follows the best-fit rigid motion of the end section, which the end zone's local deformation disturbs by well under 1%
|
||||
- **[rotating-cylinder-axisymmetric](../tests/oracle/rotating-cylinder-axisymmetric/)** (theory): Long rotating solid cylinder, axisymmetric. Source: Timoshenko and Goodier, Theory of Elasticity, 3rd ed., McGraw-Hill 1970, §32 (long rotating cylinder, ε_z = 0). Tolerances — radial_axis: stress extrapolated to the axis, where σ_r = σ_θ: under 1% with sixteen quadratic elements across; hoop_rim: stress extrapolated to the surface: under 1%
|
||||
- **[rotating-disc-plane-stress](../tests/oracle/rotating-disc-plane-stress/)** (theory): Rotating disc with a central hole, plane stress. Source: Timoshenko and Goodier, Theory of Elasticity, 3rd ed., McGraw-Hill 1970, §32 (rotating disc of uniform thickness with a hole). Tolerances — hoop_bore: stress extrapolated to the bore surface, where the a²b²/r² term is steepest: 16 radial quadratic elements graded towards the bore leave about 0.2% (8 uniform: −2.4%; 32 graded: −0.05%); radial_max: the nearest node to √(ab) is within half an element of it, where σ_r is stationary (an error of order (h/r)², under 0.5%), and nodal averaging adds a few tenths of a percent; hoop_rim: as at the bore
|
||||
@@ -218,6 +248,5 @@ Reference values already cited; each case gets its recipe when the capability it
|
||||
| [nl-cook-membrane](../tests/oracle/nl-cook-membrane/) | benchmark | Cook's membrane, nearly incompressible neo-Hookean, large load (plane strain) | the published finite-strain reference value, transcribed from the paper with its load and material constants | vertical displacement of the top corner C (48, 60) = to be transcribed | R. D. Cook, Improved two-dimensional finite element, J. Struct. Div. ASCE 100(ST9):1851–1863, 1974 (the tapered panel (0, 0), (48, 44), (48, 60), (0, 44), clamped on x = 0, shear on x = 48); finite-strain versions with a neo-Hookean material are published by J. C. Simo, F. Armero, IJNME 33:1413–1449, 1992; the values to compare with are to be transcribed from the paper |
|
||||
| [pinched-cylinder](../tests/oracle/pinched-cylinder/) | benchmark | Pinched cylinder with rigid diaphragms | shell elements (M8) | radial displacement under the load = -1.8248e-5 | Belytschko, Stolarski, Liu, Carpenter and Ong, 'Stress projection for membrane and shear locking in shell finite elements', Computer Methods in Applied Mechanics and Engineering 51 (1985) 221–258 (the shell obstacle course); reference 1.8248 × 10⁻⁵ under the load |
|
||||
| [pinched-hemisphere](../tests/oracle/pinched-hemisphere/) | benchmark | Hemispherical shell with an 18° hole under alternating point loads | shell elements (M8), or mapped blocks that follow doubly curved faces | displacement under the load along the load = 0.094 | MacNeal and Harder, 'A proposed standard set of problems to test finite element accuracy', Finite Elements in Analysis and Design 1 (1985) 3–20, Fig. 9 (p. 9), Table 5(b) (p. 11): 0.0940 (lower bound 0.0924 from their [7]) |
|
||||
| [plastic-thick-cylinder](../tests/oracle/plastic-thick-cylinder/) | theory | Elastic–perfectly plastic thick cylinder: collapse pressure | implicit (static) J2 plasticity: explicit J2 is in, a collapse load needs the static solver | collapse pressure for a = 10, b = 20, σ_y = 250 = 200.09 | Hill, The Mathematical Theory of Plasticity, Oxford 1950, ch. V §2: plane strain von Mises collapse pressure p = (2/√3)σ_y ln(b/a) |
|
||||
| [scordelis-lo-roof-shell](../tests/oracle/scordelis-lo-roof-shell/) | benchmark | Scordelis–Lo roof with shell elements | shell elements (MITC4, M8) | vertical displacement at the midpoint of the free edge = -0.3024 | MacNeal and Harder, 'A proposed standard set of problems to test finite element accuracy', Finite Elements in Analysis and Design 1 (1985) 3–20, Fig. 8 (p. 8), Table 5(a) (p. 11); Belytschko, Stolarski, Liu, Carpenter and Ong, 'Stress projection for membrane and shear locking in shell finite elements', Computer Methods in Applied Mechanics and Engineering 51 (1985) 221–258 (the shell obstacle course) |
|
||||
| [taylor-bar](../tests/oracle/taylor-bar/) | experiment | Taylor bar impact (copper) | the measured final shapes, to be transcribed from the paper (explicit Johnson–Cook plasticity is in) | final over initial length = to be transcribed | Wilkins and Guinan, 'Impact of cylinders on a rigid boundary', Journal of Applied Physics 44 (1973) 1200–1206 (final length and mushroom diameter vs impact speed) |
|
||||
+3
-3
@@ -44,7 +44,7 @@ products are run, no documentation text is copied). Status: **done**, **started*
|
||||
| Mesh | Mesh editing (move, merge, split, collapse, remesh region), imported orphan meshes | M6 | yes | started (node/element add/remove) |
|
||||
| Model | Sets and surfaces bound to geometry names | M3 | yes | done |
|
||||
| Model | Materials: elastic (isotropic, orthotropic), density, unit-system check | M3 | | done |
|
||||
| Model | Materials: plasticity, hyperelasticity, damage, thermal | M8 | | started (von Mises plasticity: bilinear, table, Johnson–Cook; Johnson–Cook damage, erosion; written to Abaqus `*PLASTIC`, Nastran `MATS1`, LS-DYNA plastic materials where expressible; compressible neo-Hookean for nonlinear steps, Abaqus `*HYPERELASTIC, NEO HOOKE`) |
|
||||
| Model | Materials: plasticity, hyperelasticity, damage, thermal | M8 | | started (von Mises plasticity: bilinear, table, Johnson–Cook; Johnson–Cook damage, erosion; plasticity in explicit and nonlinear static steps; written to Abaqus `*PLASTIC`, Nastran `MATS1`, LS-DYNA plastic materials where expressible; compressible neo-Hookean, Mooney–Rivlin and Yeoh for nonlinear steps, Abaqus `*HYPERELASTIC, NEO HOOKE`, `MOONEY-RIVLIN`, `YEOH`; thermal strain in nonlinear steps) |
|
||||
| Model | Sections: solid, shell, beam profiles; assignment by set | M3 | | done |
|
||||
| Model | 2D sections: plane stress, plane strain, axisymmetric (XY plane, Y the axis); element edges in sets; centrifugal load | M4 | | done |
|
||||
| Model | Composite layups, material orientations, continuum shells | M8 | yes | planned |
|
||||
@@ -53,7 +53,7 @@ products are run, no documentation text is copied). Status: **done**, **started*
|
||||
| Model | Loads: force and moment, pressure, gravity, body force | M3 | | done |
|
||||
| Model | Reference points, kinematic and distributing coupling | M3 | | done |
|
||||
| Model | Steps: linear static, frequency; output presets | M3 | | done |
|
||||
| Model | Steps: nonlinear static, explicit dynamics, heat transfer; amplitudes | M7–M8, M15 | | started (nonlinear static with nonlinear geometry, increments and tolerances, written to Abaqus `*STEP, NLGEOM` and LS-DYNA implicit, refused by Nastran; explicit dynamics; heat transfer; amplitudes planned) |
|
||||
| Model | Steps: nonlinear static, explicit dynamics, heat transfer; amplitudes | M7–M8, M15 | | started (nonlinear static with or without nonlinear geometry, increments, tolerances and a load path (cyclic loading), written to Abaqus `*STEP, NLGEOM` and LS-DYNA implicit (the load path as LS-DYNA's curve; refused by Abaqus for now), refused by Nastran; explicit dynamics; heat transfer; amplitudes per load planned) |
|
||||
| Interaction | Tie between non-matching meshes | M7 | | planned |
|
||||
| Interaction | Contact (general and pairs), friction, automatic pair detection | M7 | | planned |
|
||||
| Interaction | Springs and dashpots (ground or two-point; DOF, vector or line), point masses with rotary inertia | M4 | | done (implicit; explicit translational; all three decks with what each cannot take reported) |
|
||||
@@ -63,7 +63,7 @@ products are run, no documentation text is copied). Status: **done**, **started*
|
||||
| Decks | LS-DYNA keyword writer (implicit; explicit in M8) | M3 | | done (implicit static and eigenvalue; beam sections planned) |
|
||||
| Decks | Readers keeping unknown keywords | M9 | | planned |
|
||||
| Solve | Own linear static and modal solver, verified against analytic solutions | M4 | yes | started (solid elements: tet4, tet10, wedge6, hex8, hex20; 2D plane stress, plane strain and axisymmetric tri3, tri6, quad4 (full, incompatible modes), quad8 (full, reduced); quad4/tri3 MITC shells and Timoshenko beams with profiles, mixed with solids; reference points, kinematic and distributing couplings, moments; centrifugal loads) |
|
||||
| Solve | Own nonlinear implicit solver: Newton–Raphson with line search and automatic increments, total Lagrangian solids and 2D elements, mixed u–p at finite strain, follower pressure | M15 | yes | started (solids, plane strain, axisymmetric; St Venant–Kirchhoff and neo-Hookean; verified by the elastica, tube inflation, patch and objectivity tests; plasticity, shells, contact, arc-length next) |
|
||||
| Solve | Own nonlinear implicit solver: Newton–Raphson with line search and automatic increments, total Lagrangian solids and 2D elements, mixed u–p at finite strain, follower pressure | M15 | yes | started (solids, plane strain, axisymmetric; St Venant–Kirchhoff, neo-Hookean and J2 plasticity (Simo's exponential return map at finite strain, radial return at small strain, consistent tangents, the return shared with explicit dynamics); verified by the elastica, tube inflation, patch and objectivity tests, uniaxial tension to large strain, Hill's thick cylinder, a cantilever's collapse and the Bauschinger effect; Mooney–Rivlin and Yeoh; thermal strain at finite and small strain; shells, contact, arc-length next) |
|
||||
| Solve | Own explicit dynamics solver (central differences, one-point hex8 with hourglass control, nodal-pressure tet4, bulk viscosity, energy balance), verified against analytic wave and impact solutions | M10 | yes | started (X1 core and X2: large deformation, objective rates, J2 and Johnson–Cook plasticity, damage, erosion; contact planned) |
|
||||
| Results | Deformed shape, contours, mode shapes, probes, reaction sums | M4 | | started (results in the session, `solve.probe`, reaction sums; display planned) |
|
||||
| Results | XY and path plots, cuts, display groups, free-body forces, animation | M9 | | planned |
|
||||
|
||||
@@ -1186,3 +1186,45 @@ update are written from (cited again in the module docs):
|
||||
transcribed finite-strain values), R. D. Cook, J. Struct. Div. ASCE 100(ST9):1851–1863, 1974,
|
||||
and J. C. Simo, F. Armero, IJNME 33:1413–1449, 1992; tube inflation, R. S. Rivlin, Phil. Trans. R. Soc. A
|
||||
242:173–195, 1949, and R. W. Ogden, *Non-Linear Elastic Deformations*, Dover 1997.
|
||||
|
||||
## Part N: Implicit plasticity (finite and small strain), as implemented
|
||||
|
||||
- Multiplicative J2 plasticity with a Hencky elastic energy and the exponential return map in
|
||||
the principal logarithmic strains, which keeps the classical radial return of the
|
||||
infinitesimal theory: J. C. Simo, *Algorithms for static and dynamic multiplicative plasticity
|
||||
that preserve the classical return mapping schemes of the infinitesimal theory*, CMAME
|
||||
99(1):61–112, 1992, DOI [10.1016/0045-7825(92)90123-2](https://doi.org/10.1016/0045-7825(92)90123-2);
|
||||
J. C. Simo, T. J. R. Hughes, *Computational Inelasticity*, Springer 1998, ch. 9.
|
||||
- The radial return (shared with explicit dynamics) and its consistent tangent
|
||||
K I⊗I + 2Gθ ℙ − 2Gθ̄ n⊗n: Simo & Hughes 1998, §3.3, Box 3.2 (A4.1); J. C. Simo, R. L. Taylor,
|
||||
*Consistent tangent operators for rate-independent elastoplasticity*, CMAME 48(1):101–118,
|
||||
1985, DOI [10.1016/0045-7825(85)90070-2](https://doi.org/10.1016/0045-7825(85)90070-2).
|
||||
- The derivative of the matrix logarithm by divided differences (Daleckii–Krein), smooth through
|
||||
equal stretches: R. Bhatia, *Matrix Analysis*, Springer 1997, §V.3.
|
||||
- Kinematic hardening at finite strain (refused for now; the route when it comes): additive
|
||||
plasticity in the Lagrangian logarithmic strain space, C. Miehe, N. Apel, M. Lambrecht, CMAME
|
||||
191(47–48):5383–5425, 2002, DOI [10.1016/S0045-7825(02)00438-3](https://doi.org/10.1016/S0045-7825(02)00438-3).
|
||||
- Prager's linear kinematic hardening: W. Prager, *The theory of plasticity: a survey of recent
|
||||
achievements*, Proc. Inst. Mech. Eng. 169:41–57, 1955.
|
||||
- Verification references: the thick cylinder's plastic zone and collapse pressure, R. Hill,
|
||||
*The Mathematical Theory of Plasticity*, Oxford 1950, ch. V; the cantilever's plastic hinge,
|
||||
B. G. Neal, *The Plastic Methods of Structural Analysis*, 3rd ed., Chapman & Hall 1977, and the
|
||||
continuum's velocity fields, W. Prager, P. G. Hodge, *Theory of Perfectly Plastic Solids*,
|
||||
Wiley 1951.
|
||||
|
||||
## Part O: Polynomial hyperelasticity and thermal strain in nonlinear steps, as implemented
|
||||
|
||||
- Decoupled invariant-based hyperelasticity, the fictitious stress and elasticity and their
|
||||
projection: G. A. Holzapfel, *Nonlinear Solid Mechanics*, Wiley 2000, §6.4–6.6 (eqs. 6.168,
|
||||
6.193).
|
||||
- Mooney–Rivlin: M. Mooney, *A theory of large elastic deformation*, J. Appl. Phys.
|
||||
11(9):582–592, 1940; R. S. Rivlin, *Large elastic deformations of isotropic materials IV*,
|
||||
Phil. Trans. R. Soc. A 241:379–397, 1948. Yeoh: O. H. Yeoh, *Some forms of the strain energy
|
||||
function for rubber*, Rubber Chem. Technol. 66(5):754–771, 1993.
|
||||
- The homogeneous modes of rubber testing: L. R. G. Treloar, *The Physics of Rubber
|
||||
Elasticity*, 3rd ed., Oxford 1975, ch. 10; the inflation and extension of a tube: R. S. Rivlin,
|
||||
Phil. Trans. R. Soc. A 242:173–195, 1949, and R. W. Ogden, *Non-Linear Elastic Deformations*,
|
||||
Dover 1997, §5.3.
|
||||
- Thermal strain by the multiplicative split F = F_m F_θ: V. A. Lubarda, *Constitutive
|
||||
theories based on the multiplicative decomposition of deformation gradient: thermoelasticity,
|
||||
elastoplasticity, and biomechanics*, Appl. Mech. Rev. 57(2):95–108, 2004.
|
||||
@@ -0,0 +1 @@
|
||||
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,12 @@
|
||||
# Bauschinger effect of linear kinematic hardening
|
||||
|
||||
One hex8 cube (E = 200000, ν = 0.3, σ_y = 250, H = 2000, Prager's linear kinematic hardening)
|
||||
held by symmetry on three faces, its fourth face pulled to a strain of 0.01 and brought back to
|
||||
zero within one nonlinear static step without nonlinear geometry, by the load path
|
||||
[[0, 0], [0.5, 1], [1, 0]] in increments of 0.05.
|
||||
|
||||
At ε_a the stress is σ_a = σ_y + E_t (ε_a − σ_y/E) = 267.327. The yield surface keeps its size
|
||||
and moves with the back stress, so unloading is elastic only until σ_a − 2σ_y and then hardens
|
||||
with E_t: back at zero strain σ = σ_a − 2σ_y − E_t (ε_a − 2σ_y/E) = −247.525. Isotropic hardening
|
||||
would yield again only at −σ_a and end at −281.835. The equivalent plastic strain accumulates
|
||||
(σ_a − σ_y)/H forward and E/(E + H)(ε_a − 2σ_y/E) in reverse, 0.0160891.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"title": "Bauschinger effect of linear kinematic hardening: tension, then back to zero strain",
|
||||
"kind": "theory",
|
||||
"source": "J2 plasticity with Prager's linear kinematic hardening in uniaxial stress (W. Prager, Ing.-Arch. 1955; Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3): from σ_a = σ_y + E_t (ε_a − σ_y/E) the material unloads elastically by 2σ_y and then hardens with E_t, so at zero strain σ = σ_a − 2σ_y − E_t (ε_a − 2σ_y/E)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "residual_stress",
|
||||
"quantity": "axial stress back at zero strain after a pull to ε_a = 0.01",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0"
|
||||
},
|
||||
"value": -247.52475247524754,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous small-strain state the element represents exactly, with increments that end at the load path's turn; isotropic hardening would give −281.835 (it yields again only at −σ_a)"
|
||||
},
|
||||
{
|
||||
"id": "plastic_strain",
|
||||
"quantity": "accumulated equivalent plastic strain",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/plastic_strain/0"
|
||||
},
|
||||
"value": 0.016089108910891083,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "(σ_a − σ_y)/H forward plus E/(E + H) (ε_a − 2σ_y/E) in reverse"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"plastic_strain": {
|
||||
"band": 1.6089108910891082e-11,
|
||||
"value": 0.01608910891089074
|
||||
},
|
||||
"residual_stress": {
|
||||
"band": 2.4752475247524754e-7,
|
||||
"value": -247.52475247484173
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0", "4": "x1"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.3}, "plastic": {"type": "bilinear", "yield_stress": 250.0, "hardening_modulus": 2000.0, "hardening": "kinematic"}}},
|
||||
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Steel", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "pull", "type": "displacement", "target": {"set": "x1"}, "values": [0.01, null, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Pull", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "initial_increment": 0.05, "max_increment": 0.05, "load_path": [[0, 0], [0.5, 1], [1, 0]]}, "boundary_conditions": ["sx", "sy", "sz", "pull"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Pull"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
# A heated bar held at its ends past yield
|
||||
|
||||
A steel bar 4 × 1 × 1 of 4 hex8 elements (E = 200000, ν = 0.3, σ_y = 250, H = 2000 isotropic,
|
||||
α = 1.2·10⁻⁵ from 20 degrees) on symmetry planes at x = 0, y = 0, z = 0 and held at x = 4 (its
|
||||
axial strain is zero; the lateral faces are free), heated uniformly to 320 by a temperature load
|
||||
in a nonlinear static step without nonlinear geometry. The mechanical strain is −α ΔT = −0.0036,
|
||||
2.9 times the yield strain, so the bar yields in compression: σ = −(σ_y + E_t (α ΔT − σ_y/E)) =
|
||||
−254.653 with E_t = E H/(E + H), and ε_p = (|σ| − σ_y)/H.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"title": "A steel bar held at its ends and heated past yield (small strain)",
|
||||
"kind": "theory",
|
||||
"source": "Uniaxial J2 plasticity with linear hardening (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3) with the additive thermal strain: the bar's total axial strain is held at zero, so its mechanical strain is −α ΔT and σ = −(σ_y + E_t (α ΔT − σ_y/E)) once α ΔT exceeds σ_y/E",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "sigma",
|
||||
"quantity": "axial stress, ΔT = 300 (α ΔT = 3.6·10⁻³, 2.9 times the yield strain)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Heat",
|
||||
"elements": [
|
||||
2
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0"
|
||||
},
|
||||
"value": -254.65346534653466,
|
||||
"tolerance": {
|
||||
"relative": 1e-08
|
||||
},
|
||||
"reason": "a homogeneous uniaxial state the elements represent exactly"
|
||||
},
|
||||
{
|
||||
"id": "plastic_strain",
|
||||
"quantity": "equivalent plastic strain",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Heat",
|
||||
"elements": [
|
||||
2
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/plastic_strain/0"
|
||||
},
|
||||
"value": 0.0023267326732673297,
|
||||
"tolerance": {
|
||||
"relative": 1e-07
|
||||
},
|
||||
"reason": "(|σ| − σ_y)/H, as for the stress"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"plastic_strain": {
|
||||
"band": 2.3267326732675587e-12,
|
||||
"value": 0.0023267326732675587
|
||||
},
|
||||
"sigma": {
|
||||
"band": 2.5465346534653467e-7,
|
||||
"value": -254.6534653464418
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Bar", "kind": "hex8", "corners": [[0, 0, 0], [4, 0, 0], [4, 1, 0], [0, 1, 0], [0, 0, 1], [4, 0, 1], [4, 1, 1], [0, 1, 1]], "divisions": [4, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0", "4": "x1"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Bar"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Bar", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Bar", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Bar", "face": "z0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Bar", "face": "x1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.3}, "plastic": {"type": "bilinear", "yield_stress": 250.0, "hardening_modulus": 2000.0}, "thermal": {"expansion": 1.2e-05, "reference_temperature": 20.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "bar", "set": "all", "material": "Steel", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx0", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy0", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz0", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx1", "type": "symmetry", "target": {"set": "x1"}, "plane": "x"}},
|
||||
{"command": "model.load.create", "params": {"name": "heat", "type": "temperature", "target": {"set": "all"}, "value": 320.0}},
|
||||
{"command": "model.step.create", "params": {"name": "Heat", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false}, "boundary_conditions": ["sx0", "sy0", "sz0", "sx1"], "loads": ["heat"]}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Heat"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Uniaxial tension of a J2 bilinear-hardening cube to large strain (implicit)
|
||||
|
||||
The cube of `j2-uniaxial-explicit` solved by the implicit nonlinear solver: one fully integrated
|
||||
hex8, E = 200000, ν = 0.3, σ_y = 250, H = 2000 (isotropic), held by symmetry on three faces and
|
||||
stretched on the fourth to λ = e^0.2 in a nonlinear static step, lateral faces free.
|
||||
|
||||
Simo's exponential return map integrates this proportional path exactly in the logarithmic
|
||||
strain, so the Kirchhoff stress is the small-strain curve τ = σ_y + E_t (ε − σ_y/E) = 643.564 at
|
||||
ε = 0.2 and the equivalent plastic strain (τ − σ_y)/H = 0.196782. The Cauchy stress is
|
||||
σ = τ/J with Hencky's volume law ln J = τ/(3K): 642.737.
|
||||
|
||||
Cross-check with the explicit solver: `j2-uniaxial-explicit` reaches 643.57 for the Cauchy stress
|
||||
with the same plastic strain. Its hypoelastic update takes the logarithmic strain rate against
|
||||
the Cauchy stress, while the implicit model is hyperelastic in the Kirchhoff stress: the two
|
||||
laws differ by the factor J = 1.0013 (the elastic volume change), which is the 0.13 % between
|
||||
them; both meet the closed form of their own law.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"title": "Uniaxial tension of a J2 bilinear-hardening cube to large strain (implicit, finite strain)",
|
||||
"kind": "theory",
|
||||
"source": "J2 plasticity with linear isotropic hardening in uniaxial stress (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3) in the logarithmic strain, which Simo's exponential return map (CMAME 99:61–112, 1992) integrates exactly on a proportional path: Kirchhoff stress τ = σ_y + E_t (ε − σ_y/E), Cauchy stress σ = τ/J with ln J = τ/(3K) (Hencky's volume law)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "sigma",
|
||||
"quantity": "axial Cauchy stress at a logarithmic strain of 0.2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0"
|
||||
},
|
||||
"value": 642.7365391418857,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the element represents exactly, reached on a proportional path: the tolerance only admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "plastic_strain",
|
||||
"quantity": "equivalent plastic strain",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/plastic_strain/0"
|
||||
},
|
||||
"value": 0.19678217821782182,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "ε_p = (τ − σ_y)/H, as for the stress"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"plastic_strain": {
|
||||
"band": 1.9678217821795454e-10,
|
||||
"value": 0.19678217821795452
|
||||
},
|
||||
"sigma": {
|
||||
"band": 6.427365391418857e-7,
|
||||
"value": 642.7365390754569
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0", "4": "x1"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.3}, "plastic": {"type": "bilinear", "yield_stress": 250.0, "hardening_modulus": 2000.0, "hardening": "isotropic"}}},
|
||||
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Steel", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "pull", "type": "displacement", "target": {"set": "x1"}, "values": [0.22140275816016985, null, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Pull", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Pull"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
# Mooney–Rivlin block in Treloar's three modes
|
||||
|
||||
One hybrid hex20 unit cube on three symmetry planes, E = 3, ν = 0.49 (μ = 1.006711,
|
||||
κ = 50), Mooney–Rivlin with C₀₁ = 0.2 (C₁₀ = μ/2 − C₀₁ = 0.303356), in three nonlinear
|
||||
static steps:
|
||||
|
||||
- **Uniaxial**: the face x = 1 pulled to λ = 2, the others free.
|
||||
- **Equibiaxial**: x = 1 and y = 1 pulled to λ = 1.5, z free.
|
||||
- **Pure shear**: x = 1 pulled to λ = 2, y = 1 held (λ_y = 1), z free.
|
||||
|
||||
The references are the compressible decoupled energy's principal Cauchy stresses
|
||||
σᵢ = (λᵢ/J) ∂W/∂λᵢ, by differences of W alone, with the free stretch found by bisection so the
|
||||
free faces carry no stress. The deformation is homogeneous, so the element represents it
|
||||
exactly.
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"title": "Mooney–Rivlin block in uniaxial, equibiaxial and pure-shear tension",
|
||||
"kind": "theory",
|
||||
"source": "Treloar's homogeneous modes of rubber testing (L. R. G. Treloar, The Physics of Rubber Elasticity, 3rd ed., Oxford 1975, ch. 10) for the decoupled compressible energy W = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + κ/2 (J − 1)²: principal Cauchy stresses σᵢ = (λᵢ/J) ∂W/∂λᵢ with the free stretch found so the free faces carry no stress (bisection; E = 3, ν = 0.49, C₀₁ = 0.2, C₁₀ = μ/2 − C₀₁)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "uniaxial",
|
||||
"quantity": "axial Cauchy stress, uniaxial stretch λ = 2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Uniaxial",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": 2.736633402084229,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "uniaxial_lateral",
|
||||
"quantity": "lateral displacement at (1, 1, 1), uniaxial: l − 1",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Uniaxial",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/1"
|
||||
},
|
||||
"value": -0.28647206688129256,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "equibiaxial",
|
||||
"quantity": "in-plane Cauchy stress, equibiaxial stretch λ = 1.5",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Equibiaxial",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": 2.842734818108988,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "shear_xx",
|
||||
"quantity": "Cauchy stress along the stretch, pure shear λ = 2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Shear",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": 3.526124960232999,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "shear_yy",
|
||||
"quantity": "Cauchy stress across, in the held direction, pure shear λ = 2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Shear",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/1"
|
||||
},
|
||||
"value": 1.506681177907714,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"equibiaxial": {
|
||||
"band": 2.8427348183579755e-9,
|
||||
"value": 2.8427348183579753
|
||||
},
|
||||
"shear_xx": {
|
||||
"band": 3.5261249603786212e-9,
|
||||
"value": 3.526124960378621
|
||||
},
|
||||
"shear_yy": {
|
||||
"band": 1.5066811779077142e-9,
|
||||
"value": 1.5066811778673217
|
||||
},
|
||||
"uniaxial": {
|
||||
"band": 2.736633402109491e-9,
|
||||
"value": 2.736633402109491
|
||||
},
|
||||
"uniaxial_lateral": {
|
||||
"band": 2.8647206688145424e-10,
|
||||
"value": -0.2864720668814542
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0", "4": "x1", "5": "y1"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y1", "geometry": [{"body": "Cube", "face": "y1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 3.0, "poisson": 0.49}, "hyperelastic": {"type": "mooney_rivlin", "c01": 0.2}}},
|
||||
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Rubber", "kind": {"type": "solid"}, "formulation": {"hybrid": true}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "pull_u", "type": "displacement", "target": {"set": "x1"}, "values": [1.0, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "pull_e", "type": "displacement", "target": {"set": "x1"}, "values": [0.5, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "pull_ey", "type": "displacement", "target": {"set": "y1"}, "values": [null, 0.5, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "pull_s", "type": "displacement", "target": {"set": "x1"}, "values": [1.0, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "hold_y", "type": "displacement", "target": {"set": "y1"}, "values": [null, 0.0, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Uniaxial", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull_u"], "loads": []}},
|
||||
{"command": "model.step.create", "params": {"name": "Equibiaxial", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull_e", "pull_ey"], "loads": []}},
|
||||
{"command": "model.step.create", "params": {"name": "Shear", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull_s", "hold_y"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Uniaxial"}, "id": "uniaxial"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Equibiaxial"}, "id": "equibiaxial"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Shear"}, "id": "shear"}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
# Inflation of a stretched Mooney–Rivlin tube
|
||||
|
||||
The quarter tube of `nl-cylinder-inflation` (A = 10, B = 20, one unit long, 6 × 16 hybrid
|
||||
hex20), now Mooney–Rivlin (E = 3, ν = 0.4995, so μ = 1.000333; C₀₁ = 0.2, C₁₀ = μ/2 − C₀₁) and
|
||||
stretched axially by its end face (u_z = 0.5 at z = 1: λ_z = 1.5) while a follower pressure
|
||||
p = 0.540850 inflates the bore. In plane strain (λ_z = 1) Mooney–Rivlin is neo-Hookean with
|
||||
μ = 2(C₁₀ + C₀₁), since Ī₁ = Ī₂ there; the axial stretch makes the second invariant's share
|
||||
count: σ_θ − σ_r = 2(λ_θ² − λ_r²)(C₁₀ + C₀₁ λ_z²). Integrated through the incompressible wall,
|
||||
r² = a² + (R² − A²)/λ_z, that pressure puts the bore at a = 15 and the outer wall at
|
||||
b = 20.615528.
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"title": "Inflation of a stretched Mooney–Rivlin tube (follower pressure)",
|
||||
"kind": "theory",
|
||||
"source": "Finite inflation and extension of an incompressible Mooney–Rivlin tube (R. S. Rivlin, Large elastic deformations of isotropic materials VI, Phil. Trans. R. Soc. A 242:173–195, 1949; R. W. Ogden, Non-Linear Elastic Deformations, Dover 1997, §5.3): at axial stretch λ_z, r² = a² + (R² − A²)/λ_z, σ_θ − σ_r = 2 (λ_θ² − λ_r²)(C₁₀ + C₀₁ λ_z²) with λ_θ = r/R, λ_r = 1/(λ_θ λ_z), integrated through the wall: p = ∫ (σ_θ − σ_r) dr/r (Simpson, 2·10⁵ intervals)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "ua",
|
||||
"quantity": "radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15 at λ_z = 1.5",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Inflate",
|
||||
"at": [
|
||||
[
|
||||
10.0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/0"
|
||||
},
|
||||
"value": 5.0,
|
||||
"tolerance": {
|
||||
"relative": 0.01
|
||||
},
|
||||
"reason": "16 × 6 hybrid hex20 around the quarter; ν = 0.4995 stands in for incompressibility (a volume change of order p/κ ≈ 10⁻³ shifts the radii by about 0.1 %), and the tube is stretched axially by its ends, which a nearly incompressible solid takes with larger pressures than the plane-strain neo-Hookean tube"
|
||||
},
|
||||
{
|
||||
"id": "ub",
|
||||
"quantity": "radial displacement of the outer wall (20, 0, 0): b − B, a small difference of radii",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Inflate",
|
||||
"at": [
|
||||
[
|
||||
20.0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/0"
|
||||
},
|
||||
"value": 0.6155281280883038,
|
||||
"tolerance": {
|
||||
"absolute": 0.04
|
||||
},
|
||||
"reason": "0.2 % of the outer radius b = 20.616: the same volume change of order 10⁻³ moves b by about 0.03, which is 4.5 % of b − B; the incompressible kinematics r² = a² + (R² − A²)/λ_z amplify it there"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"ua": {
|
||||
"band": 5.031118164303424e-9,
|
||||
"value": 5.0311181643034235
|
||||
},
|
||||
"ub": {
|
||||
"band": 0.00004000064339064634,
|
||||
"value": 0.64339064633861
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Pipe", "kind": "hex20", "corners": [[10.0, 0, 0], [20.0, 0, 0], [0, 20.0, 0], [0, 10.0, 0], [10.0, 0, 1], [20.0, 0, 1], [0, 20.0, 1], [0, 10.0, 1]], "divisions": [6, 16, 1], "edges": [{"corners": [2, 3], "arc": {"center": [0, 0, 0]}}, {"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 1]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 1]}}], "faces": {"6": "bore", "3": "y0", "5": "x0", "1": "z0", "2": "z1"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Pipe"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "bore", "geometry": [{"body": "Pipe", "face": "bore"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Pipe", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Pipe", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Pipe", "face": "z0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Pipe", "face": "z1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 3.0, "poisson": 0.4995}, "hyperelastic": {"type": "mooney_rivlin", "c01": 0.2}}},
|
||||
{"command": "model.section.create", "params": {"name": "pipe", "set": "all", "material": "Rubber", "kind": {"type": "solid"}, "formulation": {"hybrid": true}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz0", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "stretch", "type": "displacement", "target": {"set": "z1"}, "values": [null, null, 0.5, null, null, null]}},
|
||||
{"command": "model.load.create", "params": {"name": "p", "type": "pressure", "set": "bore", "magnitude": 0.5408503415684837}},
|
||||
{"command": "model.step.create", "params": {"name": "Inflate", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz0", "stretch"], "loads": ["p"]}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Inflate"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
# Free and constrained thermal expansion at finite strain
|
||||
|
||||
A unit cube of 2 × 2 × 2 hex20, compressible neo-Hookean (E = 10, ν = 0.3) with an expansion
|
||||
coefficient α = 10⁻³ from 20 degrees, heated to 220 by a temperature load in two nonlinear static
|
||||
steps: **Free** (three symmetry planes) and **Held** (symmetry planes on all six faces). The
|
||||
thermal stretch is ϑ = 1 + α ΔT = 1.2. Free, the cube grows to 1.2 along every edge without
|
||||
stress; held, F = I, the material's mechanical part is F_m = ϑ⁻¹ I and its stress the neo-Hookean
|
||||
pressure κ (J_m − 1) with J_m = ϑ⁻³: −3.510802.
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"title": "Free and fully constrained thermal expansion at finite strain",
|
||||
"kind": "theory",
|
||||
"source": "The multiplicative split F = F_m F_θ with F_θ = (1 + α ΔT) I (V. A. Lubarda, Appl. Mech. Rev. 57(2):95–108, 2004): a free body follows F = F_θ stress-free; held on every face, F = I and F_m = ϑ⁻¹ I, so the compressible neo-Hookean material carries the pressure of its volume ratio, σ = κ (ϑ⁻³ − 1)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "free_u",
|
||||
"quantity": "displacement of the corner (1, 1, 1) along x, free, α ΔT = 0.2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Free",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/0"
|
||||
},
|
||||
"value": 0.2,
|
||||
"tolerance": {
|
||||
"relative": 1e-08
|
||||
},
|
||||
"reason": "a homogeneous stretch every element represents exactly"
|
||||
},
|
||||
{
|
||||
"id": "free_stress",
|
||||
"quantity": "stress of the free body (σ_xx at the corner)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Free",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 1e-08
|
||||
},
|
||||
"reason": "rounding only"
|
||||
},
|
||||
{
|
||||
"id": "held_stress",
|
||||
"quantity": "stress of the held block, σ_xx (hydrostatic)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Held",
|
||||
"at": [
|
||||
[
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": -3.5108024691358017,
|
||||
"tolerance": {
|
||||
"relative": 1e-08
|
||||
},
|
||||
"reason": "a homogeneous state; E = 10, ν = 0.3 (κ = 8.3333), ϑ = 1.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"free_stress": {
|
||||
"band": 1e-8,
|
||||
"value": -1.0882345665298319e-14
|
||||
},
|
||||
"free_u": {
|
||||
"band": 2.0000000000000037e-10,
|
||||
"value": 0.20000000000000034
|
||||
},
|
||||
"held_stress": {
|
||||
"band": 3.510802469135803e-9,
|
||||
"value": -3.5108024691358026
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], "divisions": [2, 2, 2], "faces": {"6": "x0", "3": "y0", "1": "z0", "4": "x1", "5": "y1", "2": "z1"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y1", "geometry": [{"body": "Cube", "face": "y1"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Cube", "face": "z1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 10.0, "poisson": 0.3}, "hyperelastic": {"type": "neo_hookean"}, "thermal": {"expansion": 0.001, "reference_temperature": 20.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Rubber", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx0", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy0", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz0", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx1", "type": "symmetry", "target": {"set": "x1"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy1", "type": "symmetry", "target": {"set": "y1"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz1", "type": "symmetry", "target": {"set": "z1"}, "plane": "z"}},
|
||||
{"command": "model.load.create", "params": {"name": "heat", "type": "temperature", "target": {"set": "all"}, "value": 220.0}},
|
||||
{"command": "model.step.create", "params": {"name": "Free", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx0", "sy0", "sz0"], "loads": ["heat"]}},
|
||||
{"command": "model.step.create", "params": {"name": "Held", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx0", "sy0", "sz0", "sx1", "sy1", "sz1"], "loads": ["heat"]}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Free"}, "id": "free"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Held"}, "id": "held"}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
# Yeoh block in uniaxial tension
|
||||
|
||||
One hybrid hex20 unit cube on three symmetry planes, E = 3, ν = 0.49, Yeoh with C₁₀ = μ/2,
|
||||
C₂₀ = −0.02 and C₃₀ = 0.004, pulled to λ = 2 in a nonlinear static step. The reference is the
|
||||
compressible decoupled energy's axial Cauchy stress σ = (λ/J) ∂W/∂λ with the lateral stretch
|
||||
found by bisection so σ_yy = 0.
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"title": "Yeoh block in uniaxial tension",
|
||||
"kind": "theory",
|
||||
"source": "Uniaxial tension of the decoupled compressible Yeoh energy W = C₁₀ (Ī₁ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ + κ/2 (J − 1)² (O. H. Yeoh, Rubber Chem. Technol. 66(5):754–771, 1993): σ = (λ/J) ∂W/∂λ with the lateral stretch found so σ_yy = 0 (bisection; E = 3, ν = 0.49, C₁₀ = μ/2, C₂₀ = −0.02, C₃₀ = 0.004)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "sigma",
|
||||
"quantity": "axial Cauchy stress, stretch λ = 2",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Uniaxial",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/0"
|
||||
},
|
||||
"value": 3.174263919393508,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
},
|
||||
{
|
||||
"id": "lateral",
|
||||
"quantity": "lateral displacement at (1, 1, 1): l − 1",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Uniaxial",
|
||||
"at": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/1"
|
||||
},
|
||||
"value": -0.2854505757255106,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"lateral": {
|
||||
"band": 2.854505757255106e-10,
|
||||
"value": -0.285450575725269
|
||||
},
|
||||
"sigma": {
|
||||
"band": 3.174263919404631e-9,
|
||||
"value": 3.1742639194046305
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0", "4": "x1", "5": "y1"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 3.0, "poisson": 0.49}, "hyperelastic": {"type": "yeoh", "c20": -0.02, "c30": 0.004}}},
|
||||
{"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Rubber", "kind": {"type": "solid"}, "formulation": {"hybrid": true}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sx", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sy", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sz", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "pull", "type": "displacement", "target": {"set": "x1"}, "values": [1.0, null, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Uniaxial", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["sx", "sy", "sz", "pull"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Uniaxial"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Plastic collapse of a cantilever
|
||||
|
||||
A steel bar 200 long with a 10 × 10 section (E = 200000, ν = 0.3, elastic–perfectly plastic at
|
||||
σ_y = 250), reduced hex20 20 × 2 × 4, in a nonlinear static step without nonlinear geometry. The
|
||||
root x = 0 is a plane of symmetry (no rotation, the section free to contract laterally), held in
|
||||
Y, Z and against twist on average through a distributing coupling; a coupling at the tip pushes
|
||||
the end down by 15, 4.5 times the deflection at first yield.
|
||||
|
||||
Beam theory's collapse is a plastic hinge at the root: M_p = σ_y b h²/4 = 62500 and
|
||||
P_c = M_p/L = 312.5. The solid's collapse lies above it, since a continuum cannot rotate about a
|
||||
hinge (that needs a jump of the normal velocity across the section): refined to 8 elements
|
||||
through the depth or 160 along the length, the force settles at 322.3, 3.1 % above. The case
|
||||
checks that the implicit plasticity reaches this plateau and holds it.
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "Plastic collapse of a cantilever: the fully plastic moment at the root",
|
||||
"kind": "theory",
|
||||
"source": "Limit analysis (B. G. Neal, The Plastic Methods of Structural Analysis, 3rd ed., Chapman & Hall 1977, ch. 1–2): beam theory's plastic hinge at the root, the fully plastic moment M_p = σ_y b h²/4 of a rectangular section, collapses a cantilever of length L at P_c = M_p/L = 312.5 (b = h = 10, L = 200, σ_y = 250); a bound for the solid, whose collapse lies above it (W. Prager, P. G. Hodge, Theory of Perfectly Plastic Solids, Wiley 1951: a continuum mechanism allows no normal velocity jump, which the beam's hinge rotation is)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "collapse_force",
|
||||
"quantity": "tip force at a tip deflection of 15 (4.5 times the first yield's)",
|
||||
"measure": {
|
||||
"from": "solve",
|
||||
"pointer": "/reference_points/1/reaction/2"
|
||||
},
|
||||
"value": -312.5,
|
||||
"tolerance": {
|
||||
"relative": 0.05
|
||||
},
|
||||
"reason": "the solid's collapse lies above beam theory's hinge: refined through the depth (4 to 8 elements) and along the length (40 to 160) the force settles 3.1 % above M_p/L (322.3), so the band is that bound's gap; the root is a symmetry plane (no rotation, free lateral contraction) held on average through a coupling, the tip pushed through another; reduced hex20, 20 × 2 × 4"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"collapse_force": {
|
||||
"band": 3.2260685847425373e-7,
|
||||
"value": -322.6068584742537
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Beam", "kind": "hex20", "corners": [[0, 0, -5.0], [200.0, 0, -5.0], [200.0, 10.0, -5.0], [0, 10.0, -5.0], [0, 0, 5.0], [200.0, 0, 5.0], [200.0, 10.0, 5.0], [0, 10.0, 5.0]], "divisions": [20, 2, 4], "faces": {"6": "root", "4": "tip"}}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Beam"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Beam", "face": "root"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "tip", "geometry": [{"body": "Beam", "face": "tip"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.3}, "plastic": {"type": "bilinear", "yield_stress": 250.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Steel", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "plane", "type": "symmetry", "target": {"set": "root"}, "plane": "x"}},
|
||||
{"command": "model.reference_point.create", "params": {"name": "Root", "xyz": [0, 5.0, 0]}},
|
||||
{"command": "model.coupling.create", "params": {"name": "support", "reference_point": "Root", "set": "root", "kind": "distributing"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "hold", "type": "displacement", "target": {"reference_point": "Root"}, "values": [null, 0, 0, 0, null, null]}},
|
||||
{"command": "model.reference_point.create", "params": {"name": "Tip", "xyz": [200.0, 5.0, 0]}},
|
||||
{"command": "model.coupling.create", "params": {"name": "end", "reference_point": "Tip", "set": "tip", "kind": "distributing"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "push", "type": "displacement", "target": {"reference_point": "Tip"}, "values": [null, null, -15.0, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Collapse", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false}, "boundary_conditions": ["plane", "hold", "push"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Collapse"}, "id": "solve"}
|
||||
]
|
||||
@@ -1,3 +1,14 @@
|
||||
# Elastic–perfectly plastic thick cylinder: collapse pressure
|
||||
# Elastic–perfectly plastic thick cylinder: plastic zone and collapse pressure
|
||||
|
||||
The Lamé cylinder of `lame-cylinder`, elastic–perfectly plastic, loaded to collapse.
|
||||
The Lamé cylinder of `lame-cylinder-axisymmetric` (a = 10, b = 20, an axisymmetric slice held
|
||||
axially on both faces, so plane strain), now elastic–perfectly plastic: E = 200000,
|
||||
ν = 0.4999 (Hill's solution takes the elasticity incompressible), σ_y = 250, one row of 20
|
||||
reduced quad8 elements through the wall, in two nonlinear static steps without nonlinear
|
||||
geometry:
|
||||
|
||||
- **Pressure**: the bore pressure p = k (2 ln(c/a) + 1 − c²/b²) = 180.195 (k = σ_y/√3) that
|
||||
takes the plastic zone to c = 15. The bore then moves u_a = k c²/(2 G a) = 0.024355: the plastic
|
||||
zone moves incompressibly (u r constant) and the elastic ring outside yields at its inner edge.
|
||||
- **Collapse**: the bore pushed out by 0.3 (24 times its first-yield displacement), past the
|
||||
pressure at which the wall is plastic through; the radial force on the bore stays at the
|
||||
collapse pressure 2k ln(b/a) = 200.094 times 2πa per unit height.
|
||||
@@ -1,14 +1,43 @@
|
||||
{
|
||||
"title": "Elastic–perfectly plastic thick cylinder: collapse pressure",
|
||||
"title": "Elastic–perfectly plastic thick cylinder: plastic zone and collapse pressure",
|
||||
"kind": "theory",
|
||||
"source": "Hill, The Mathematical Theory of Plasticity, Oxford 1950, ch. V §2: plane strain von Mises collapse pressure p = (2/√3)σ_y ln(b/a)",
|
||||
"status": "pending",
|
||||
"requires": "implicit (static) J2 plasticity: explicit J2 is in, a collapse load needs the static solver",
|
||||
"source": "Hill, The Mathematical Theory of Plasticity, Oxford 1950, ch. V §2: a von Mises cylinder in plane strain, elastically incompressible, with the plastic zone a ≤ r ≤ c under the pressure p = k (2 ln(c/a) + 1 − c²/b²), k = σ_y/√3, and the bore displacement u_a = k c²/(2 G a); collapse when c = b at p = 2k ln(b/a)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "p_collapse",
|
||||
"quantity": "collapse pressure for a = 10, b = 20, σ_y = 250",
|
||||
"value": 200.0944
|
||||
"id": "u_bore",
|
||||
"quantity": "bore displacement at the pressure that takes the plastic zone to c = 15 (a = 10, b = 20, σ_y = 250)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pressure",
|
||||
"at": [
|
||||
[
|
||||
10.0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/0"
|
||||
},
|
||||
"value": 0.02435534068380524,
|
||||
"tolerance": {
|
||||
"relative": 0.002
|
||||
},
|
||||
"reason": "20 reduced quad8 elements through the wall resolve the logarithmic plastic field and ν = 0.4999 stands in for Hill's incompressible elasticity (0.009 % apart); u_a ∝ c², so 0.2 % on u_a is 0.1 % on the plastic radius"
|
||||
},
|
||||
{
|
||||
"id": "collapse_force",
|
||||
"quantity": "radial force on the bore (per radian times 2π, height 1) at a bore displacement of 0.3, 24 times the first yield's: the collapse pressure 2k ln 2 = 200.094 times 2πa",
|
||||
"measure": {
|
||||
"from": "collapse",
|
||||
"pointer": "/reaction_total/0"
|
||||
},
|
||||
"value": 12572.299154203694,
|
||||
"tolerance": {
|
||||
"relative": 0.001
|
||||
},
|
||||
"reason": "once the wall is plastic through, perfect plasticity holds the force at the collapse value (σ_θ − σ_r = 2k everywhere, σ_z the mean of the others as plastic flow dominates); the band admits the quadrature of the logarithmic field"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"collapse_force": {
|
||||
"band": 0.000012572299377396248,
|
||||
"value": 12572.299377396248
|
||||
},
|
||||
"u_bore": {
|
||||
"band": 2.4357507851220506e-11,
|
||||
"value": 0.024357507851220504
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Pipe", "kind": "quad8", "corners": [[10.0, 0, 0], [20.0, 0, 0], [20.0, 1, 0], [10.0, 1, 0]], "divisions": [20, 1], "bias": [1, 1], "edges": [{"corners": [1, 4], "name": "bore"}, {"corners": [1, 2], "name": "bottom"}, {"corners": [3, 4], "name": "top"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Pipe", "face": "area"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "bore", "geometry": [{"body": "Pipe", "edge": "bore"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "ends", "geometry": [{"body": "Pipe", "edge": "bottom"}, {"body": "Pipe", "edge": "top"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.4999}, "plastic": {"type": "bilinear", "yield_stress": 250.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "pipe", "set": "all", "material": "Steel", "kind": {"type": "axisymmetric"}, "formulation": {"integration": "reduced"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "ends", "type": "symmetry", "target": {"set": "ends"}, "plane": "y"}},
|
||||
{"command": "model.load.create", "params": {"name": "p", "type": "pressure", "set": "bore", "magnitude": 180.19538034924005}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "expand", "type": "displacement", "target": {"set": "bore"}, "values": [0.3, null, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Pressure", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false}, "boundary_conditions": ["ends"], "loads": ["p"]}},
|
||||
{"command": "model.step.create", "params": {"name": "Collapse", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false}, "boundary_conditions": ["ends", "expand"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Pressure"}, "id": "pressure"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Collapse"}, "id": "collapse"}
|
||||
]
|
||||
Reference in new issue
Block a user