forked from 1b-agent/fieldcraft-upstream
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98eaede2dd | ||
|
|
227c4544ed | ||
|
|
6baa189231 | ||
|
|
2ca5f84113 | ||
|
|
6da3055a1c | ||
|
|
6bd52c0b29 | ||
|
|
98d263ae54 | ||
|
|
0b7f3021d9 | ||
|
|
f5a0a1289a | ||
|
|
ecdff11e72 | ||
|
|
cf8ed71e4f | ||
|
|
581646bdac | ||
|
|
c9a765aa02 | ||
|
|
0220e2d8bb | ||
|
|
85f0905db1 | ||
|
|
2b1c52d021 | ||
|
|
794aa9f158 | ||
|
|
7e9366f726 | ||
|
|
f9981d305e | ||
|
|
8d2fa980a5 | ||
|
|
a91ac5baf9 | ||
|
|
32c5c1b307 | ||
|
|
6ca3233aaa | ||
|
|
945a2076c3 | ||
|
|
5770288f47 | ||
|
|
8871f6721d | ||
|
|
37f338c9e8 |
No files matched your search
@@ -18,6 +18,11 @@ is committed and listed here with its exports.
|
||||
| docs/media/ibeam-analysis.mp4, docs/media/ibeam-analysis.gif | FieldCraft contributors | Original: the FieldCraft window captured during an MCP session, generated by `tools/demo-video/make.py` | MIT OR Apache-2.0 |
|
||||
| docs/media/og.svg, docs/media/og.png | FieldCraft contributors | Original social-preview image (1280 × 640): the application icon's cube layer (assets/app-icon/FieldCraft.icon/Assets/02-part.svg) beside the name and tagline set in Inter (OFL, crates/ui-egui/fonts) | MIT OR Apache-2.0 |
|
||||
| docs/media/bracket-partition.png, docs/media/bracket-hex.png, docs/media/bracket-stress.png, docs/media/setup-symbols.png | FieldCraft contributors | Original: FieldCraft renders (headless MCP screenshots and a window capture) of corpus parts and the cantilever sample | MIT OR Apache-2.0 |
|
||||
| docs/media/examples/drop-test.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved drop-test sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||
| docs/media/examples/rubber-seal.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved rubber-seal sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||
| docs/media/examples/heated-bracket.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved heated-bracket sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||
| docs/media/examples/bracket-hex.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved bracket-hex sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||
| docs/media/examples/crank-arm.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved crank-arm sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||
| crates/decks/tests/golden/nastran/*.bdf | FieldCraft contributors | Written by our Nastran writer from our own test models (crates/decks/tests/nastran.rs) | MIT OR Apache-2.0 |
|
||||
| assets/icons/assembly.instance.svg, assets/icons/assembly.instance.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/assembly.instance.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
| assets/icons/deck.read.svg, assets/icons/deck.read.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||
|
||||
@@ -92,6 +92,19 @@ The same part also meshes all-hex automatically with the octree mesher (`mesh.he
|
||||
|
||||

|
||||
|
||||
## Examples
|
||||
|
||||
Ready-to-solve samples: open one from **File ▸ Samples** (or the start screen), or start the app
|
||||
with `fieldcraft --sample NAME`, and press **Solve**. Each opens and solves within half a minute and is checked
|
||||
against a closed form or a reference solution ([docs/examples.md](docs/examples.md)).
|
||||
|
||||
| | | |
|
||||
|---|---|---|
|
||||
|  |  |  |
|
||||
| **Drop test** (`drop-test`): an aluminium bracket dropped 2 m onto a tilted floor; explicit dynamics, the strike's stress waves | **Rubber seal** (`rubber-seal`): a Mooney–Rivlin gasket ring squeezed 25 %; nonlinear static, large strain | **Heated bracket** (`heated-bracket`): a clamped steel bracket heated at one end; steady heat and thermal stress |
|
||||
|  | *No picture yet: the viewport does not draw flow results (M12).* |  |
|
||||
| **Hex-meshed bracket** (`bracket-hex`): the bracket split into 30 cells, meshed all-hex (hex20), von Mises | **Flow past a bracket** (`flow-past`): air past an L-bracket on the floor; lattice Boltzmann flow, the drag and the force on each face | **Bicycle crank arm** (`crank-arm`): an aluminium crank swept all-hex (hex20), a rider's weight on the pedal; root bending within 0.3 % of M/Z |
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
@@ -106,6 +119,7 @@ Rust 1.95 or later, with the `wasm32-unknown-unknown` target for `cargo xtask ci
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Examples](docs/examples.md): the ready-to-solve samples and their checks
|
||||
- [Architecture](docs/architecture.md): crates, layers and the meshing pipeline
|
||||
- [Element library](docs/elements.md) and [verification and validation](docs/verification.md)
|
||||
- [Capability checklist](docs/parity.md) and [roadmap](ROADMAP.md)
|
||||
|
||||
+3
-3
@@ -33,7 +33,7 @@ pressure) is a degree of freedom of the existing DOF framework, not a separate s
|
||||
| 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 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 all-hex for any part (first version: done — `fieldcraft-mesh::octree`, `mesh.hex.octree`: 3-refinement grid with conforming templates refined by curvature, thickness and narrow faces, a core cleared to a manifold boundary, feature map onto it, padding and a pillow per patch capturing every sharp curve, foldover-free fitting, certified validity, B-rep face tags; the corpus hex column and the filleted-bracket oracle case; next: transitions for concave refined regions so thin and curved parts stop over-refining); hex-dominant by tet recombination; polycube for blocky parts; layers through thin walls; mesh editing and orphan-mesh import | started |
|
||||
| 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 (the model's contact definitions, `interaction.contact.*` with `find`, the deck mapping, `fieldcraft_fem::contact`'s closest-point projection and bucket grid, and the explicit solver's segmentation and friction return map are in place to reuse; the implicit solver refuses contact by name until then); 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 |
|
||||
| M7 Assemblies, contact and friction | Needs M15. Part instances from SolidCraft occurrences; tie between non-matching meshes (mortar: done — `interaction.tie`, dual mortar constraints in the implicit analyses, Abaqus `*TIE` and LS-DYNA tied contact, the tie patch test); implicit contact: surface-to-surface mortar and node-to-surface, penalty and augmented Lagrange, self-contact, automatic pair detection (mortar contact pairs with Coulomb friction in nonlinear static steps: done — segment-to-segment mortar with the consistent tangent by automatic differentiation of the segmentation, augmented Lagrangian with Uzawa updates to a gap tolerance, semismooth active set with a chattering cutback, small sliding without nonlinear geometry and finite sliding with it, overclosure adjusted or refused, Coulomb friction (stick/slip return map, static and kinetic coefficients, symmetrised tangent), pressure, gap, status, shear and slip output; verified by the contact patch test, Hertz cylinder and sphere, Newton's quadratic rate and sliding blocks; the linear analyses refuse contact by name); 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 | started |
|
||||
| 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 |
|
||||
| M9 Post-processing and deck readers | Results readers for public formats (Nastran OP2, LS-DYNA d3plot, Abaqus `.fil`); XY and path plots, cuts, display groups, free-body forces; deck readers that keep unknown keywords; mesh convergence studies | planned |
|
||||
| M10 Explicit dynamics | Shared element and material kernels with the implicit solver (`fem`); central-difference solver with lumped mass, stable time step, one-point hex8 with hourglass control and nodal-pressure tet4, bulk viscosity, energy accounting (X1 core: done — `fieldcraft-explicit`, `explicit_dynamic` steps with initial velocities, prescribed motion and planar rigid walls, `solve.explicit`/`solve.history`, small-strain linear elastic; bar wave, rigid-wall impact, energy balance and cantilever-period tests); large-deformation materials (X2: done — updated Lagrangian elements, Jaumann/Hughes–Winget and Green–Naghdi/Flanagan–Taylor rates, J2 bilinear/table plasticity, Johnson–Cook strength with heating and damage, erosion; materials gain `plastic`, `damage`, `erosion` with `model.material.plasticity`/`damage`; Dienes shear, rigid rotation, J2 uniaxial and Taylor-bar tests); contact (X3: done — `interaction.contact.pair`/`general`/`find`, segment-to-segment penalty contact both ways with polygon clipping (passes the contact patch test on non-matching meshes), pairs, general (all exterior faces) and self contact, bucket-grid search, deterministic on any thread count, a penalty from the masses and the stable increment that never shortens it, Coulomb friction with exponential decay by a return map, friction on rigid walls, contact energy and friction dissipation in the balance, initial penetrations adjusted or refused; Abaqus `*CONTACT PAIR`/`*CONTACT`/`*FRICTION` and LS-DYNA automatic contacts in the decks; verified by colliding bars, sliding blocks, the patch test and Hertz's sphere impact); hyperelastic materials (neo-Hookean, Mooney–Rivlin, Yeoh) on the shared material point with average nodal pressure tets, and opt-in mass scaling with the added mass reported: done; runs on hex and tet meshes | started |
|
||||
@@ -52,7 +52,7 @@ pressure) is a degree of freedom of the existing DOF framework, not a separate s
|
||||
| M23 Wave propagation and ultrasound | Elastic, acoustic and coupled waves from the explicit solver's 1D bar today to guided and ultrasonic waves, by two methods. Finite differences first, for fast 2D (then 3D) ultrasound on regular grids: staggered-grid velocity–stress FDTD (Virieux) and the elastodynamic finite integration technique for solids, the rotated staggered grid for anisotropic media, a k-space pseudospectral scheme for weakly heterogeneous tissue and water, all on grids voxelised from the face-tagged geometry by the LBM's exact voxeliser, with heterogeneous, layered and fluid–solid media, CPML boundaries, threads and the GPU backend; staircased curved boundaries scatter spuriously, so the FD grid is checked against the finite elements on curved parts. Finite elements for curved geometry and coupling with structures: spectral elements (high-order Gauss–Lobatto–Legendre hexes with diagonal mass) on the hex meshes, and a mesh check in elements per wavelength with the dispersion error it implies; absorbing boundaries (convolutional PML, absorbing layers with increasing damping) and infinite media; tone-burst sources (Hann-windowed cycles), point, line and surface excitation, and piezoelectric transducers (M19); anisotropic media (composites, crystals) and viscoelastic, power-law-in-frequency attenuation; fluid–solid interfaces and fluid-loaded plates; ultrasound in water and tissue, nonlinear acoustics (Westervelt, KZK) for focused ultrasound. Guided waves: dispersion curves by semi-analytical finite elements (SAFE) for plates, pipes, rails and layered and anisotropic laminates — Lamb, Rayleigh, Love and SH modes — with phase and group velocities and mode shapes; mode-selective excitation. Output: A-scans and B-scans at sensors, wavefield snapshots, 2D FFT (frequency–wavenumber) for mode identification, and time–frequency diagrams of any signal — short-time Fourier spectrograms, continuous wavelet transforms (Morlet), reassigned spectrograms and smoothed pseudo Wigner–Ville — with the SAFE or Rayleigh–Lamb group-velocity curves overlaid as arrival times, and group velocities extracted from the ridges. GPU paths (M12's backend) for long runs. Verified by Lamb's problem (point load on a half-space), Rayleigh-wave speed, Rayleigh–Lamb dispersion, Love waves in a layer over a half-space, reflection and transmission at interfaces, and published guided-wave measurements | planned |
|
||||
| Continuous: verification and validation | Every capability checked against theory (closed forms), experiments (published measurements) and published benchmarks (NAFEMS, vendor benchmark and verification manuals such as Abaqus's), each case our own model in `tests/oracle/` with its cited reference and tolerance; `cargo xtask oracle` runs them and writes docs/oracle.md; once a case matches, our result is frozen as a fixture so fidelity cannot regress (docs/verification.md). Runner, fixtures and CI job: done; 26 theory cases (patch tests of every solid element, cantilevers, Lamé in 3D, plane strain and axisymmetric, Kirsch in 3D and plane stress, rotating disc and cylinder, beam frequencies, couplings, explicit waves, impact and J2 plasticity, CDT invariants), 9 published benchmarks (MacNeal–Harder solid tests, Scordelis–Lo with solids, NAFEMS LE1 in 3D and 2D, LE10, FV32) and 9 pending cases with citations (shells, thermal, static plasticity, Taylor bar, CFD); `mesh.block` mapped blocks and 2D patches build their meshes | started |
|
||||
| Continuous: performance | CPU first, deterministic: results never depend on the thread count (fixed partitions, sums in a fixed order; checked by hashing results on 1, 2 and 7 threads). Supernodal left-looking Cholesky (11–15× faster than the column solver on one thread), parallel element matrices, stresses and Lanczos products, explicit element updates and LBM cell blocks on rayon threads (done — `fieldcraft-parallel`, `threads` on the solve commands, `FIELDCRAFT_THREADS`); parallel forward/back substitution and explicit tet4 assembly; GPU paths later, within a stated tolerance of the CPU | started |
|
||||
| Continuous: desktop and agents | Desktop app (viewport, model tree, commands, control channel, the open design drawn, `ui.capture` of the whole window offscreen: done; toolbar of every capability from the catalogue, ready or coming soon with its milestone, icons drawn with VectorCraft, docs/features.md: done; the Craft-family shell — title bar, workspaces and ribbon drop-downs, browser, context panel, command search, journal timeline, light and dark themes — native file pickers with background opening, drag and drop and recent files, samples and one-click Solve: done; MCP server over stdio, headless or bridged to the app, with read and write roots, docs/mcp.md: done; caps labels in Inter SemiBold; the README's analysis video recorded through MCP by `tools/demo-video`: done; support, load, reference-point and coupling symbols in the viewport and headless renders, with their browser toggles: done; selection by picking in the viewport — geometry faces, edges, vertices and bodies, mesh nodes, element faces and elements, box selection, hover, a Target row in command forms, `ui.select`/`ui.selection` and MCP `select`: done; a camera parameter for screenshots, a ribbon that follows the command being run: in progress; the browser build — the same app in a web page with browser file pickers, downloads, drops and autosave to browser storage, deployed to GitHub Pages from each release, docs/web.md: done), command schema as the public API | started |
|
||||
| Continuous: desktop and agents | Desktop app (viewport, model tree, commands, control channel, the open design drawn, `ui.capture` of the whole window offscreen: done; toolbar of every capability from the catalogue, ready or coming soon with its milestone, icons drawn with VectorCraft, docs/features.md: done; the Craft-family shell — title bar, workspaces and ribbon drop-downs, browser, context panel, command search, journal timeline, light and dark themes — native file pickers with background opening, drag and drop and recent files, samples and one-click Solve, and an examples gallery of ready-to-solve samples, each checked by a test (docs/examples.md): done; MCP server over stdio, headless or bridged to the app, with read and write roots, docs/mcp.md: done; caps labels in Inter SemiBold; the README's analysis video recorded through MCP by `tools/demo-video`: done; support, load, reference-point and coupling symbols in the viewport and headless renders, with their browser toggles: done; selection by picking in the viewport — geometry faces, edges, vertices and bodies, mesh nodes, element faces and elements, box selection, hover, a Target row in command forms, `ui.select`/`ui.selection` and MCP `select`: done; a camera parameter for screenshots, a ribbon that follows the command being run: in progress; the browser build — the same app in a web page with browser file pickers, downloads, drops and autosave to browser storage, deployed to GitHub Pages from each release, docs/web.md: done), command schema as the public API | started |
|
||||
|
||||
## Next
|
||||
|
||||
@@ -77,7 +77,7 @@ 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 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 deformable contact (pairs, general, self; segment to segment, penalty) with Coulomb friction and exponential decay; rigid walls with friction; couplings, springs, dashpots | Implicit contact, tie, anisotropic and pressure- or temperature-dependent friction, thermal contact, wear, eroding contact (M7, M10); cohesive adhesion and debonding (M20) |
|
||||
| Contact, friction, adhesion | Explicit deformable contact (pairs, general, self; segment to segment, penalty) with Coulomb friction and exponential decay; rigid walls with friction; implicit mortar contact pairs (augmented Lagrangian, Coulomb friction) in nonlinear static steps; ties between non-matching meshes (dual mortar); couplings, springs, dashpots | Implicit self contact, anisotropic and pressure- or temperature-dependent friction, thermal contact, wear, eroding contact (M7, M10); 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, 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) |
|
||||
|
||||
@@ -17,7 +17,8 @@ use fieldcraft_engine::Session;
|
||||
use fieldcraft_engine::autosave::{self, Autosaver};
|
||||
use fieldcraft_ui_egui::{Filter, ModelApp, Services};
|
||||
|
||||
const USAGE: &str = "usage: fieldcraft [--sample [parts|cantilever|impact]] [--control PORT] [FILE…] (FILE: .fieldcraft, .solidcraft or .3mf)";
|
||||
const USAGE: &str =
|
||||
"usage: fieldcraft [--sample [NAME]] [--control PORT] [FILE…] (NAME: a sample, docs/examples.md; FILE: .fieldcraft, .solidcraft or .3mf)";
|
||||
const DEPTH_BITS: u8 = 24;
|
||||
/// The window draws its own caption buttons where the OS ones would sit apart from the app bar.
|
||||
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
//! initial overclosure have no Abaqus counterpart and are noted in comments (Abaqus chooses its
|
||||
//! own penalty and resolves overclosure in the first increment).
|
||||
//!
|
||||
//! # Ties
|
||||
//!
|
||||
//! Each tie is `*TIE, NAME=…, ADJUST=NO` with the secondary surface first and its position
|
||||
//! tolerance when it has one: FieldCraft ties the nodes where they are (no adjustment).
|
||||
//!
|
||||
//! # Steps and the activity table
|
||||
//!
|
||||
//! The model lists, for every step, exactly the boundary conditions and loads active in it; Abaqus
|
||||
@@ -187,6 +192,7 @@ pub fn write_resolved(model: &Model, r: &Resolved) -> Result<String> {
|
||||
d.discrete_properties(&discrete)?;
|
||||
d.couplings(model, r);
|
||||
d.contacts(model, r)?;
|
||||
d.ties(model)?;
|
||||
d.steps(model, r)?;
|
||||
Ok(d.out)
|
||||
}
|
||||
@@ -1042,6 +1048,20 @@ impl Deck {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ties: `*TIE` per tie, secondary surface first.
|
||||
fn ties(&mut self, model: &Model) -> Result<()> {
|
||||
if model.ties().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
self.comment("Ties: each secondary surface bonded to its main surface");
|
||||
for (name, t) in model.ties() {
|
||||
let tolerance = t.position_tolerance.map_or(String::new(), |v| format!(", POSITION TOLERANCE={v:?}"));
|
||||
self.line(&format!("*TIE, NAME={name}, ADJUST=NO{tolerance}"));
|
||||
self.line(&format!("{}, {}", t.secondary, t.main));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn boundary_lines<'n>(&mut self, model: &Model, r: &Resolved, names: impl IntoIterator<Item = &'n String>) -> Result<()> {
|
||||
for name in names {
|
||||
let Some(bc) = model.boundary_condition(name) else { continue };
|
||||
@@ -1220,7 +1240,16 @@ impl Deck {
|
||||
self.out.push('\n');
|
||||
}
|
||||
} else {
|
||||
self.line("*STATIC");
|
||||
match n.stabilization {
|
||||
// Abaqus's automatic stabilization takes the dissipated energy
|
||||
// fraction, which FieldCraft's damping fraction is.
|
||||
Some(a) => {
|
||||
self.out.push_str("*STATIC, STABILIZE=");
|
||||
push_number(&mut self.out, a)?;
|
||||
self.out.push('\n');
|
||||
}
|
||||
None => self.line("*STATIC"),
|
||||
}
|
||||
self.numbers_line("", &[n.initial_increment, 1.0, n.min_increment, n.max_increment])?;
|
||||
}
|
||||
if n.line_search {
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
//! node set), distributing as `*CONSTRAINED_INTERPOLATION` (the reference point dependent on
|
||||
//! the set's translations, equal weights);
|
||||
//! - contact: a pair as `*CONTACT_AUTOMATIC_SURFACE_TO_SURFACE` between segment sets (the
|
||||
//! secondary surface first), general contact as `*CONTACT_AUTOMATIC_SINGLE_SURFACE` on a segment
|
||||
//! secondary surface first; a tie as `*CONTACT_TIED_SURFACE_TO_SURFACE` likewise), general
|
||||
//! contact as `*CONTACT_AUTOMATIC_SINGLE_SURFACE` on a segment
|
||||
//! set or on every part (`surfatyp` 5); the static and kinetic coefficients and the decay
|
||||
//! coefficient (`fs`, `fd`, `dc`: the same exponential decay law) and the penalty scale on both
|
||||
//! sides (`sfsa`, `sfsb`, a factor on LS-DYNA's own default stiffness). LS-DYNA moves nodes
|
||||
@@ -280,12 +281,18 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
|
||||
for c in model.contacts().values() {
|
||||
segment_sets.extend(c.surfaces.sets());
|
||||
}
|
||||
for t in model.ties().values() {
|
||||
segment_sets.extend([t.main.as_str(), t.secondary.as_str()]);
|
||||
}
|
||||
let mut gravity_sets: BTreeSet<Option<&str>> = BTreeSet::new();
|
||||
for step in model.steps() {
|
||||
if matches!(step.procedure, Procedure::ExplicitDynamic(_)) {
|
||||
refused.insert(format!("step `{}`: explicit dynamic steps (this writer writes implicit analyses so far)", step.name));
|
||||
}
|
||||
if let Procedure::NonlinearStatic(n) = &step.procedure {
|
||||
if n.stabilization.is_some() {
|
||||
refused.insert(format!("step `{}`: stabilization (artificial damping of a static step; not written yet)", step.name));
|
||||
}
|
||||
if !n.nonlinear_geometry {
|
||||
refused.insert(format!(
|
||||
"step `{}`: a nonlinear step without nonlinear geometry (LS-DYNA's nonlinear implicit analysis always follows the deformed shape)",
|
||||
@@ -733,6 +740,21 @@ fn contacts(d: &mut Deck, model: &Model, plan: &Plan) -> Result<()> {
|
||||
d.header(&["sfsa", "sfsb"]);
|
||||
d.card([Field::from(c.penalty_scale), Field::from(c.penalty_scale)]);
|
||||
}
|
||||
// Ties: a tied surface-to-surface contact between the segment sets, secondary first.
|
||||
for (name, t) in model.ties() {
|
||||
let sid = |set: &str| plan.sets.get(set).copied().ok_or_else(|| broken(format!("tie `{name}`: set `{set}`")));
|
||||
d.comment(&format!("tie `{name}`"));
|
||||
if t.position_tolerance.is_some() {
|
||||
d.comment("FieldCraft's position tolerance: LS-DYNA ties the nodes within its own search distance");
|
||||
}
|
||||
d.line("*CONTACT_TIED_SURFACE_TO_SURFACE");
|
||||
d.header(&["surfa", "surfb", "surfatyp", "surfbtyp"]);
|
||||
d.card([Field::from(sid(&t.secondary)?), Field::from(sid(&t.main)?), Field::from(0), Field::from(0)]);
|
||||
d.header(&["fs", "fd", "dc"]);
|
||||
d.card([Field::from(0.0), Field::from(0.0), Field::from(0.0)]);
|
||||
d.header(&["sfsa", "sfsb"]);
|
||||
d.card([Field::from(1.0), Field::from(1.0)]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -337,6 +337,9 @@ impl<'m> Deck<'m> {
|
||||
for name in self.model.contacts().keys() {
|
||||
self.problems.insert(format!("contact `{name}` (Nastran's linear solutions SOL 101 and 103 have no contact)"));
|
||||
}
|
||||
for name in self.model.ties().keys() {
|
||||
self.problems.insert(format!("tie `{name}` (this writer does not write glued contact yet)"));
|
||||
}
|
||||
for sub in &mut self.subcases {
|
||||
if matches!(sub.step.procedure, Procedure::Frequency { .. }) {
|
||||
sub.method = Some(next_id(&mut self.next_sid)?);
|
||||
|
||||
@@ -776,6 +776,12 @@ fn nonlinear_steps_and_hyperelastic_materials_are_written() {
|
||||
assert_eq!(bend.get(at + 1..at + 4), Some(&["0.05, 1.0, 0.0001, 0.5", "*CONTROLS, PARAMETERS=LINE SEARCH", "5,"][..]), "{bend:?}");
|
||||
assert!(deck.contains("*ELASTIC, TYPE=ISOTROPIC\n210000.0, 0.3\n"), "St Venant–Kirchhoff is the elastic material");
|
||||
|
||||
// Stabilization: Abaqus's dissipated energy fraction.
|
||||
let stabilized = NonlinearStatic { stabilization: Some(2e-4), ..controls.clone() };
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(stabilized), &["clamp"], &["end"])).unwrap();
|
||||
let deck = abaqus::write(&m).unwrap();
|
||||
assert!(block(&deck, "Bend").contains(&"*STATIC, STABILIZE=0.0002"), "{deck}");
|
||||
|
||||
let small = NonlinearStatic { nonlinear_geometry: false, line_search: false, ..controls };
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(small), &["clamp"], &["end"])).unwrap();
|
||||
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::NeoHookean {}), ..steel() }).unwrap();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use fieldcraft_decks::{Error, abaqus, lsdyna, nastran};
|
||||
use fieldcraft_model::{
|
||||
BoundaryCondition, Contact, ContactSurfaces, Elastic, ElementFace, ElementKind, Friction, InitialPenetration, Material, Model, Procedure,
|
||||
Section, SectionKind, Set, Step, Target, UnitSystem,
|
||||
Section, SectionKind, Set, Step, Target, Tie, UnitSystem,
|
||||
};
|
||||
|
||||
/// Two unit hex8 cubes, the second on top of the first (separate nodes), with face sets for the
|
||||
@@ -42,6 +42,7 @@ fn model() -> Model {
|
||||
m.add_contact("stack", pair).unwrap();
|
||||
m.add_contact("everything", Contact { friction: Friction::coulomb(0.1), ..Contact::new(ContactSurfaces::General { surface: None }) }).unwrap();
|
||||
m.add_contact("fold", Contact { penalty_scale: 0.5, ..Contact::new(ContactSurfaces::General { surface: Some("upper".into()) }) }).unwrap();
|
||||
m.add_tie("glue", Tie { position_tolerance: Some(0.05), ..Tie::new("lower", "upper") }).unwrap();
|
||||
m
|
||||
}
|
||||
|
||||
@@ -56,6 +57,7 @@ fn abaqus_writes_interactions_pairs_and_general_contact() {
|
||||
"*SURFACE INTERACTION, NAME=fold-interaction\n*FRICTION\n0.0\n",
|
||||
"*CONTACT\n*CONTACT INCLUSIONS, ALL EXTERIOR\n*CONTACT INCLUSIONS\nupper,\n*CONTACT PROPERTY ASSIGNMENT\n, , everything-interaction\nupper, upper, fold-interaction\n",
|
||||
"** contact `fold`: penalty scale 0.5",
|
||||
"*TIE, NAME=glue, ADJUST=NO, POSITION TOLERANCE=0.05\nupper, lower\n",
|
||||
] {
|
||||
assert!(deck.contains(expected), "{expected}\n{deck}");
|
||||
}
|
||||
@@ -83,6 +85,7 @@ fn lsdyna_writes_automatic_contacts_on_segment_sets() {
|
||||
for set in ["lower", "upper"] {
|
||||
assert!(deck.contains(&format!("*SET_SEGMENT_TITLE\n{set}\n")), "{set}\n{deck}");
|
||||
}
|
||||
assert_eq!(after("*CONTACT_TIED_SURFACE_TO_SURFACE"), ["4 3 0 0", "0.0 0.0 0.0", "1.0 1.0"], "{deck}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -92,6 +95,7 @@ fn nastran_refuses_contact_by_name() {
|
||||
for name in ["everything", "fold", "stack"] {
|
||||
assert!(what.contains(&format!("contact `{name}`")), "{what}");
|
||||
}
|
||||
assert!(what.contains("tie `glue`"), "{what}");
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
|
||||
@@ -774,6 +774,9 @@ fn nonlinear_static_steps() {
|
||||
.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 { stabilization: Some(2e-4), ..controls.clone() }), &["clamp"], &["tip_load"]))
|
||||
.unwrap();
|
||||
unsupported(&m, "stabilization");
|
||||
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic { nonlinear_geometry: false, ..controls }), &["clamp"], &["tip_load"]))
|
||||
.unwrap();
|
||||
unsupported(&m, "a nonlinear step without nonlinear geometry");
|
||||
|
||||
@@ -10,7 +10,8 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
fieldcraft-model = { workspace = true }
|
||||
fieldcraft-geometry = { workspace = true }
|
||||
# Samples build their geometry from SolidCraft scripts (sample.rs).
|
||||
fieldcraft-geometry = { workspace = true, features = ["scripts"] }
|
||||
fieldcraft-mesh = { workspace = true }
|
||||
fieldcraft-io = { workspace = true }
|
||||
fieldcraft-decks = { workspace = true }
|
||||
@@ -27,5 +28,3 @@ log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
zip = { workspace = true }
|
||||
# Multi-body designs from SolidCraft scripts in tests.
|
||||
fieldcraft-geometry = { workspace = true, features = ["scripts"] }
|
||||
@@ -0,0 +1,27 @@
|
||||
{"commands": [
|
||||
{"command": "parameters.change", "params": {"name": "arm_thickness", "expression": "16 mm"}},
|
||||
{"command": "sketch.create", "params": {"plane": "XY", "name": "ArmProfile"}},
|
||||
{"command": "sketch.line", "params": {"points": [[15.471735240951828, -18.362928548901984], [160.67227699330076, -12.384082712040556]], "ids": ["lo"]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [160.91912663845798, -18.3790026658586], "start": [161.58293237729853, -12.415835559271503], "end": [160.67227699330076, -12.384082712040556]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [170.0, 0], "start": [161.58293237729853, -12.415835559271503], "end": [161.58293237729853, 12.415835559271503]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [160.91912663845798, 18.3790026658586], "start": [160.67227699330076, 12.384082712040556], "end": [161.58293237729853, 12.415835559271503]}},
|
||||
{"command": "sketch.line", "params": {"points": [[160.67227699330076, 12.384082712040556], [15.471735240951828, 18.362928548901984]], "ids": ["hi"]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [15.883151316213851, 28.354461805265395], "start": [13.455063072993756, 18.65372021077189], "end": [15.471735240951828, 18.362928548901984]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [0, 0], "start": [13.455063072993756, 18.65372021077189], "end": [13.455063072993756, -18.65372021077189]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [15.883151316213851, -28.354461805265395], "start": [15.471735240951828, -18.362928548901984], "end": [13.455063072993756, -18.65372021077189]}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"distance": "arm_thickness", "body_name": "CrankArm", "name": "Arm"}},
|
||||
{"command": "solid.cylinder", "params": {"diameter": 46.0, "height": 22, "base": [0, 0, 0], "operation": "join", "targets": ["CrankArm"], "name": "SpindleBoss"}},
|
||||
{"command": "solid.cylinder", "params": {"diameter": 30.0, "height": 20, "base": [170.0, 0, 0], "operation": "join", "targets": ["CrankArm"], "name": "PedalBoss"}},
|
||||
{"command": "sketch.create", "params": {"plane": "XY", "name": "BackPocket"}},
|
||||
{"command": "sketch.rectangle.two_point", "params": {"p0": [48, -7], "p1": [130, 7]}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"distance": 8, "operation": "cut", "targets": ["CrankArm"], "name": "Pocket"}},
|
||||
{"command": "sketch.create", "params": {"plane": "XY", "name": "SpindleSquare"}},
|
||||
{"command": "sketch.rectangle.center", "params": {"center": [0, 0], "corner": [6.35, 6.35]}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"through_all": true, "operation": "cut", "targets": ["CrankArm"], "name": "SpindleTaper"}},
|
||||
{"command": "solid.fillet", "params": {"edges": [[6.35, 6.35, 10], [-6.35, 6.35, 10], [-6.35, -6.35, 10], [6.35, -6.35, 10]], "radius": 1.5, "name": "SquareCornerFillet"}},
|
||||
{"command": "solid.hole", "params": {"position": [170.0, 0, 20], "diameter": 14, "name": "PedalHole"}},
|
||||
{"command": "solid.fillet", "params": {"edges": [[48, -7, 4], [48, 7, 4], [130, -7, 4], [130, 7, 4]], "radius": 5, "name": "PocketCornerFillet"}}
|
||||
]}
|
||||
@@ -414,13 +414,20 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
"Kinematic or distributing coupling of a set to a reference point",
|
||||
&["model.coupling.edit", "model.coupling.delete"],
|
||||
),
|
||||
soon("interaction.tie", "Tie", Interactions, "Constraints", "M7", "Tie non-matching meshes together"),
|
||||
ready_more(
|
||||
"interaction.tie",
|
||||
"Tie",
|
||||
Interactions,
|
||||
"Constraints",
|
||||
"Tie non-matching meshes together (dual mortar constraints)",
|
||||
&["interaction.tie.edit", "interaction.tie.delete"],
|
||||
),
|
||||
ready_more(
|
||||
"interaction.contact.general",
|
||||
"General Contact",
|
||||
Interactions,
|
||||
"Contact",
|
||||
"Contact between everything that may touch, or self contact of a surface, with friction (explicit dynamics)",
|
||||
"Contact between everything that may touch, or self contact of a surface, with friction (explicit dynamics and nonlinear static steps)",
|
||||
&["interaction.contact.edit", "interaction.contact.delete"],
|
||||
),
|
||||
ready_more(
|
||||
@@ -428,7 +435,7 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
"Contact Pair",
|
||||
Interactions,
|
||||
"Contact",
|
||||
"Contact between two surfaces, with friction (explicit dynamics)",
|
||||
"Contact between two surfaces: with friction in explicit dynamics, mortar contact with friction in nonlinear static steps",
|
||||
&["interaction.contact.edit", "interaction.contact.delete"],
|
||||
),
|
||||
ready(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! `interaction.contact.*`: contact definitions (pairs of surfaces, general contact) and the
|
||||
//! search for surfaces that touch.
|
||||
//! search for surfaces that touch; `interaction.tie*`: ties between non-matching meshes.
|
||||
//!
|
||||
//! A contact's surfaces are sets of element faces ([`fieldcraft_model::contact`]). The commands
|
||||
//! A contact's surfaces are sets of element faces, or of element edges in a 2D model
|
||||
//! ([`fieldcraft_model::contact`]). The commands
|
||||
//! take the friction, penalty scale and initial-penetration choice of the model's form; `edit`
|
||||
//! replaces a definition with the model's JSON form, `delete` removes it.
|
||||
//!
|
||||
@@ -14,7 +15,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use fieldcraft_fem::contact::{Grid, bounds, closest_point};
|
||||
use fieldcraft_model::{Contact, ContactSurfaces, ElementFace, Friction, InitialPenetration, Model, Set};
|
||||
use fieldcraft_model::{Contact, ContactSurfaces, ElementFace, Friction, InitialPenetration, Model, Set, Tie};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{name, opt};
|
||||
@@ -40,8 +41,8 @@ pub const PAIR: CommandSpec = CommandSpec {
|
||||
label: "Contact Pair",
|
||||
params: concat!(
|
||||
r#"{"name": "impact", "main": "plate" (a set of element faces), "secondary": "ball" (another), "#,
|
||||
r#""friction": {"static": 0.3, "kinetic": 0.2, "decay": 10} (Coulomb, μ = μk + (μs − μk) e^(−decay × slip rate); default frictionless), "penalty_scale": 1 (0 < scale ≤ 1: a factor on the stiffest penalty that leaves the time increment alone), "initial_penetration": "adjust" (move nodes that start inside onto the surface, strain-free, and report them) | "refuse" (refuse the analysis, naming the first)"#,
|
||||
r#"} — contact between two surfaces, each one's nodes kept out of the other's faces (explicit dynamics; fails if the name is taken)"#
|
||||
r#""friction": {"static": 0.3, "kinetic": 0.2, "decay": 10} (Coulomb, μ = μk + (μs − μk) e^(−decay × slip rate); default frictionless), "penalty_scale": 1 (0 < scale ≤ 1: a factor on the penalty: explicit, the stiffest that leaves the time increment alone; nonlinear static, the augmented Lagrangian's), "initial_penetration": "adjust" (nodes that start inside: explicit moves them onto the surface, strain-free; nonlinear static takes the overlap out of their gap; both report them) | "refuse" (refuse the analysis, naming the first)"#,
|
||||
r#"} — contact between two surfaces: in explicit dynamics each one's nodes are kept out of the other's faces; in nonlinear static steps `secondary` is the mortar (non-mortar) side whose pressure field is enforced against `main` by an augmented Lagrangian, with Coulomb friction by a return map (static coefficient while a node sticks, kinetic once it slips) (fails if the name is taken)"#
|
||||
),
|
||||
form: Some(super::forms::CONTACT_PAIR),
|
||||
kind: CommandKind::Model,
|
||||
@@ -58,8 +59,8 @@ pub const GENERAL: CommandSpec = CommandSpec {
|
||||
label: "General Contact",
|
||||
params: concat!(
|
||||
r#"{"name": "all", "surface": "skin" (optional: a set of element faces, each in contact with every other: self contact; default every exterior face of the solids), "#,
|
||||
r#""friction": {"static": 0.3, "kinetic": 0.2, "decay": 10} (Coulomb, μ = μk + (μs − μk) e^(−decay × slip rate); default frictionless), "penalty_scale": 1 (0 < scale ≤ 1: a factor on the stiffest penalty that leaves the time increment alone), "initial_penetration": "adjust" (move nodes that start inside onto the surface, strain-free, and report them) | "refuse" (refuse the analysis, naming the first)"#,
|
||||
r#"} — single-surface contact of everything that may touch (explicit dynamics; fails if the name is taken)"#
|
||||
r#""friction": {"static": 0.3, "kinetic": 0.2, "decay": 10} (Coulomb, μ = μk + (μs − μk) e^(−decay × slip rate); default frictionless), "penalty_scale": 1 (0 < scale ≤ 1: a factor on the penalty: explicit, the stiffest that leaves the time increment alone; nonlinear static, the augmented Lagrangian's), "initial_penetration": "adjust" (nodes that start inside: explicit moves them onto the surface, strain-free; nonlinear static takes the overlap out of their gap; both report them) | "refuse" (refuse the analysis, naming the first)"#,
|
||||
r#"} — single-surface contact of everything that may touch (explicit dynamics, and mortar self-contact in nonlinear static steps; fails if the name is taken)"#
|
||||
),
|
||||
form: Some(super::forms::CONTACT_GENERAL),
|
||||
kind: CommandKind::Model,
|
||||
@@ -102,6 +103,53 @@ pub const DELETE: CommandSpec = CommandSpec {
|
||||
},
|
||||
};
|
||||
|
||||
/// A tie from the parameters.
|
||||
fn tie(p: &Value) -> Result<Tie> {
|
||||
Ok(Tie {
|
||||
main: text(p, "main")?.to_string(),
|
||||
secondary: text(p, "secondary")?.to_string(),
|
||||
position_tolerance: opt::<f64>(p, "position_tolerance")?,
|
||||
})
|
||||
}
|
||||
|
||||
pub const TIE: CommandSpec = CommandSpec {
|
||||
id: "interaction.tie",
|
||||
label: "Tie",
|
||||
params: r#"{"name": "glue", "main": "plate" (a set of element faces), "secondary": "rib" (another), "position_tolerance": 0.1 (optional: how far a secondary node may lie from the main surface and still be tied; default half the main element's thickness)} — bonds the secondary surface to the main one across non-matching meshes: each secondary node follows the main surface under it (dual mortar constraints in the implicit analyses; a constant stress crosses the tie exactly); fails if the name is taken"#,
|
||||
form: Some(super::forms::TIE),
|
||||
kind: CommandKind::Model,
|
||||
run: |s: &mut Session, p: &Value| {
|
||||
let n = name(p)?;
|
||||
s.model.add_tie(n, tie(p)?)?;
|
||||
Ok(json!({"name": n}))
|
||||
},
|
||||
};
|
||||
|
||||
pub const TIE_EDIT: CommandSpec = CommandSpec {
|
||||
id: "interaction.tie.edit",
|
||||
label: "Edit Tie",
|
||||
params: r#"{"name": "glue", "main": "plate", "secondary": "rib", "position_tolerance": 0.1} — replaces the tie of that name (see interaction.tie)"#,
|
||||
form: Some(super::forms::TIE_EDIT),
|
||||
kind: CommandKind::Model,
|
||||
run: |s: &mut Session, p: &Value| {
|
||||
let n = name(p)?;
|
||||
s.model.replace_tie(n, tie(p)?)?;
|
||||
Ok(json!({"name": n}))
|
||||
},
|
||||
};
|
||||
|
||||
pub const TIE_DELETE: CommandSpec = CommandSpec {
|
||||
id: "interaction.tie.delete",
|
||||
label: "Delete Tie",
|
||||
params: r#"{"name": "glue"}"#,
|
||||
form: Some(super::forms::TIE_DELETE),
|
||||
kind: CommandKind::Model,
|
||||
run: |s: &mut Session, p: &Value| {
|
||||
s.model.remove_tie(name(p)?)?;
|
||||
Ok(Value::Null)
|
||||
},
|
||||
};
|
||||
|
||||
pub const FIND: CommandSpec = CommandSpec {
|
||||
id: "interaction.contact.find",
|
||||
label: "Find Contacts",
|
||||
@@ -334,6 +382,32 @@ mod tests {
|
||||
assert!(s.model.contacts().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ties_are_created_edited_and_deleted() {
|
||||
let mut s = Session::default();
|
||||
stacked(&mut s);
|
||||
s.execute("model.set.create", &json!({"name": "a", "faces": [{"element": 5, "face": 2}]})).unwrap();
|
||||
s.execute("model.set.create", &json!({"name": "b", "faces": [{"element": 9, "face": 1}]})).unwrap();
|
||||
s.execute("interaction.tie", &json!({"name": "glue", "main": "a", "secondary": "b"})).unwrap();
|
||||
assert_eq!(s.model.tie("glue"), Some(&Tie::new("a", "b")));
|
||||
for bad in [
|
||||
json!({"name": "glue", "main": "a", "secondary": "b"}),
|
||||
json!({"name": "x", "main": "a", "secondary": "a"}),
|
||||
json!({"name": "x", "main": "a", "secondary": "nope"}),
|
||||
json!({"name": "x", "main": "a", "secondary": "b", "position_tolerance": -1}),
|
||||
json!({"name": "x", "main": "a", "secondary": "b", "position_tolerance": "far"}),
|
||||
json!({"name": "x", "main": 3, "secondary": "b"}),
|
||||
] {
|
||||
assert!(s.execute("interaction.tie", &bad).is_err(), "{bad}");
|
||||
}
|
||||
s.execute("interaction.tie.edit", &json!({"name": "glue", "main": "b", "secondary": "a", "position_tolerance": 0.01})).unwrap();
|
||||
assert_eq!(s.model.tie("glue").and_then(|t| t.position_tolerance), Some(0.01));
|
||||
assert!(s.execute("interaction.tie.edit", &json!({"name": "nope", "main": "a", "secondary": "b"})).is_err());
|
||||
assert!(s.execute("model.set.delete", &json!({"name": "a"})).unwrap_err().to_string().contains("tie `glue`"));
|
||||
s.execute("interaction.tie.delete", &json!({"name": "glue"})).unwrap();
|
||||
assert!(s.model.ties().is_empty() && s.execute("interaction.tie.delete", &json!({"name": "glue"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_reports_and_creates_the_touching_parts() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -292,7 +292,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, "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", "friction": {"static": 0.3, "kinetic": 0.2, "decay": 10} (Coulomb; default frictionless)}], "hourglass": {"type": "viscous|stiffness", "coefficient": 0.1}, "bulk_viscosity": {"linear": 0.06, "quadratic": 1.5}, "mass_scaling": {"target_increment": 1e-6, "set": "…" (optional, default every element)} (opt-in: elements below the target increment get the density that reaches it; the added mass is reported)}, "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), "stabilization": 2e-4 (optional: viscous damping of local instabilities, folding and snap-through, as the fraction of the step's work it may dissipate; calibrated on the first increment)} (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", "friction": {"static": 0.3, "kinetic": 0.2, "decay": 10} (Coulomb; default frictionless)}], "hourglass": {"type": "viscous|stiffness", "coefficient": 0.1}, "bulk_viscosity": {"linear": 0.06, "quadratic": 1.5}, "mass_scaling": {"target_increment": 1e-6, "set": "…" (optional, default every element)} (opt-in: elements below the target increment get the density that reaches it; the added mass is reported)}, "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)}"#,
|
||||
form: Some(super::forms::STEP_CREATE),
|
||||
kind: CommandKind::Model,
|
||||
run: step_create,
|
||||
|
||||
@@ -76,7 +76,7 @@ pub const RECOVERY_DISCARD: CommandSpec = CommandSpec {
|
||||
pub const SAMPLE: CommandSpec = CommandSpec {
|
||||
id: "file.sample",
|
||||
label: "Open Sample",
|
||||
params: r#"{"name": "parts|cantilever|impact"} — opens a sample model (fieldcraft_engine::sample) in place of the current one, unsaved changes discarded; `cantilever` and `impact` are ready to solve (solve.run)"#,
|
||||
params: r#"{"name": "cantilever"} — opens a sample model (fieldcraft_engine::sample; the list is in docs/examples.md) in place of the current one, unsaved changes discarded; every sample but `parts` is ready to solve (solve.run)"#,
|
||||
form: Some(super::forms::SAMPLE),
|
||||
kind: CommandKind::Session,
|
||||
run: open_sample,
|
||||
@@ -87,7 +87,12 @@ fn open_sample(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let names = || crate::sample::SAMPLES.iter().map(|x| x.name).collect::<Vec<_>>().join(", ");
|
||||
let sample = crate::sample::find(name).ok_or_else(|| EngineError::Params(format!("no sample `{name}` (samples: {})", names())))?;
|
||||
let mut opened = Session::default();
|
||||
opened.run_script(&(sample.script)())?;
|
||||
if let Some(recipe) = sample.design {
|
||||
opened.design = Some(Arc::new(open_design(&fieldcraft_geometry::script_design(recipe)?)?));
|
||||
}
|
||||
let bodies = opened.design.as_ref().map_or(&[][..], |d| d.bodies.as_slice());
|
||||
let script = (sample.script)(bodies);
|
||||
opened.run_script(&script)?;
|
||||
// A sample just opened has nothing to save: it opens again the same way.
|
||||
opened.saved_revision = Some(opened.revision);
|
||||
s.replace_with(opened);
|
||||
|
||||
@@ -31,6 +31,7 @@ pub const BOUNDARY_CONDITION_DELETE: &[Field] = existing!("Boundary condition",
|
||||
pub const LOAD_DELETE: &[Field] = existing!("Load", C::Load);
|
||||
pub const FLUID_DOMAIN_DELETE: &[Field] = existing!("Fluid domain", C::FluidDomain);
|
||||
pub const CONTACT_DELETE: &[Field] = existing!("Contact", C::Contact);
|
||||
pub const TIE_DELETE: &[Field] = existing!("Tie", C::Tie);
|
||||
pub const STEP_DELETE: &[Field] = existing!("Step", C::Step);
|
||||
pub const SET_RESOLVE: &[Field] = existing!("Set", C::Set);
|
||||
|
||||
@@ -439,6 +440,7 @@ const PROCEDURE: Field = variants(
|
||||
),
|
||||
flag("line_search", "Line search").def("false"),
|
||||
json("load_path", "Load path").hint("[[fraction, factor], …]; a ramp when empty"),
|
||||
positive("stabilization", "Stabilization", D::None).hint("damping fraction (e.g. 2e-4) for folding and snap-through; none when empty"),
|
||||
group(
|
||||
"imperfection",
|
||||
"Imperfection",
|
||||
@@ -614,6 +616,18 @@ pub const CONTACT_EDIT: &[Field] = &[
|
||||
PENALTY,
|
||||
PENETRATION,
|
||||
];
|
||||
pub const TIE: &[Field] = &[
|
||||
NAME,
|
||||
reference("main", "Main surface", C::Set).req(),
|
||||
reference("secondary", "Secondary surface", C::Set).req(),
|
||||
positive("position_tolerance", "Position tolerance", D::Length).hint("half the main element's thickness"),
|
||||
];
|
||||
pub const TIE_EDIT: &[Field] = &[
|
||||
reference("name", "Tie", C::Tie).req(),
|
||||
reference("main", "Main surface", C::Set).req(),
|
||||
reference("secondary", "Secondary surface", C::Set).req(),
|
||||
positive("position_tolerance", "Position tolerance", D::Length).hint("half the main element's thickness"),
|
||||
];
|
||||
pub const CONTACT_FIND: &[Field] = &[
|
||||
positive("tolerance", "Tolerance", D::Length).hint("a tenth of the mean face size"),
|
||||
flag("create", "Create pairs").def("false"),
|
||||
@@ -742,6 +756,12 @@ pub const PLANAR: &[Field] = &[
|
||||
int("max_elements", "Max elements", 1, 4_000_000),
|
||||
];
|
||||
|
||||
pub const REVOLVE: &[Field] = &[
|
||||
num("angle", "Angle (°)", D::None).def("360"),
|
||||
int("sectors", "Sectors", 1, 3600).def("36"),
|
||||
num("start", "Start (°)", D::None).def("0"),
|
||||
];
|
||||
|
||||
pub const QUALITY: &[Field] = &[json("elements", "Elements").hint("[labels]; all when empty"), int("bins", "Histogram bins", 1, 1000).def("10")];
|
||||
|
||||
pub const SURFACE_CHECK: &[Field] =
|
||||
@@ -795,8 +815,23 @@ pub const FILE_SAVE_AS: &[Field] = &[path("path", "File", true).req()];
|
||||
pub const RECOVERY_LIST: &[Field] = &[path("dir", "Folder", false).hint("the recovery folder")];
|
||||
pub const RECOVER: &[Field] = &[text("id", "Autosave").req(), path("dir", "Folder", false).hint("the recovery folder")];
|
||||
pub const RECOVERY_DISCARD: &[Field] = &[text("id", "Autosave"), flag("all", "All"), path("dir", "Folder", false).hint("the recovery folder")];
|
||||
pub const SAMPLE: &[Field] =
|
||||
&[choice("name", "Sample", &[("cantilever", "Cantilever"), ("impact", "Impact"), ("parts", "Parts")]).req().def("\"cantilever\"")];
|
||||
pub const SAMPLE: &[Field] = &[choice(
|
||||
"name",
|
||||
"Sample",
|
||||
&[
|
||||
("cantilever", "Cantilever"),
|
||||
("impact", "Impact"),
|
||||
("parts", "Parts"),
|
||||
("drop-test", "Drop test"),
|
||||
("rubber-seal", "Rubber seal"),
|
||||
("flow-past", "Flow past a bracket"),
|
||||
("heated-bracket", "Heated bracket"),
|
||||
("bracket-hex", "Hex-meshed bracket"),
|
||||
("crank-arm", "Bicycle crank arm"),
|
||||
],
|
||||
)
|
||||
.req()
|
||||
.def("\"cantilever\"")];
|
||||
pub const COUNT: &[Field] = &[int("count", "Count", 1, 1_000_000).def("1")];
|
||||
|
||||
pub const DECK: &[Field] = &[
|
||||
@@ -880,6 +915,11 @@ pub const CONTOUR: &[Field] = &[
|
||||
("stress_xy", "Stress XY"),
|
||||
("stress_xz", "Stress XZ"),
|
||||
("stress_yz", "Stress YZ"),
|
||||
("contact_pressure", "Contact pressure"),
|
||||
("contact_gap", "Contact gap"),
|
||||
("contact_status", "Contact status"),
|
||||
("contact_shear", "Contact shear"),
|
||||
("contact_slip", "Contact slip"),
|
||||
("none", "None"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ pub use mesh::analytic_surfaces;
|
||||
mod model;
|
||||
pub(crate) mod octree;
|
||||
mod results;
|
||||
mod revolve;
|
||||
pub(crate) mod solve;
|
||||
mod tags;
|
||||
|
||||
@@ -40,6 +41,7 @@ pub static ALL: &[CommandSpec] = &[
|
||||
mesh::DELAUNAY_POINTS,
|
||||
mesh::SURFACE_CHECK,
|
||||
mesh::PLANAR,
|
||||
revolve::REVOLVE,
|
||||
mesh::TET_GENERATE,
|
||||
octree::HEX_OCTREE,
|
||||
block::BLOCK,
|
||||
@@ -74,6 +76,9 @@ pub static ALL: &[CommandSpec] = &[
|
||||
contact::EDIT,
|
||||
contact::DELETE,
|
||||
contact::FIND,
|
||||
contact::TIE,
|
||||
contact::TIE_EDIT,
|
||||
contact::TIE_DELETE,
|
||||
define::MASS_CREATE,
|
||||
define::MASS_EDIT,
|
||||
define::MASS_DELETE,
|
||||
|
||||
@@ -20,7 +20,7 @@ pub const DEFORMED: CommandSpec = CommandSpec {
|
||||
pub const CONTOUR: CommandSpec = CommandSpec {
|
||||
id: "results.contour",
|
||||
label: "Contours",
|
||||
params: r#"{"step": "Load", "field": "von_mises", "bands": 12} — contours of a nodal field: displacement, displacement_x/y/z, von_mises, max/mid/min_principal, stress_xx/yy/zz/xy/xz/yz (stresses for static steps); "none" turns them off; 2 to 24 bands"#,
|
||||
params: r#"{"step": "Load", "field": "von_mises", "bands": 12} — contours of a nodal field: displacement, displacement_x/y/z, von_mises, max/mid/min_principal, stress_xx/yy/zz/xy/xz/yz (stresses for static steps), contact_pressure, contact_gap, contact_status (0 open, 1 closed or stick, 2 slip), contact_shear, contact_slip (friction; nonlinear static steps with contact, at the contacts' secondary surfaces); "none" turns them off; 2 to 24 bands"#,
|
||||
form: Some(super::forms::CONTOUR),
|
||||
kind: CommandKind::Session,
|
||||
run: contour,
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
//! `mesh.revolve`: a 2D mesh of a profile in the XY plane (x the radius, Y the axis, as
|
||||
//! axisymmetric sections take it) swept about the axis into a solid mesh.
|
||||
//!
|
||||
//! The profile's point (r, y) at angle θ becomes (r cos θ, r sin θ, y): the axis becomes Z (up)
|
||||
//! and the profile's half-plane at θ = `start` the first ring of nodes. Each quad4 becomes a hex8
|
||||
//! per sector and each tri3 a wedge6, the profile's node order reversed so the swept elements are
|
||||
//! positively oriented. The 2D elements and their nodes are replaced (new labels after the
|
||||
//! largest ones). Geometry tags carry over: an element filling face F of body B fills body B, an
|
||||
//! element edge on edge E of body B gives the swept faces F named E of body B, and when the
|
||||
//! sweep is not a whole turn its first and last sides are faces `start` and `end` of the body,
|
||||
//! so sets named by geometry resolve on the solid. A whole turn joins its last ring to the first.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use fieldcraft_model::{ElementKind, Entity, GeometryRef};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::mesh::optional_count;
|
||||
use super::optional_number;
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
|
||||
pub const REVOLVE: CommandSpec = CommandSpec {
|
||||
id: "mesh.revolve",
|
||||
label: "Revolve 2D Mesh",
|
||||
params: r#"{"angle": 360 (degrees, up to a whole turn), "sectors": 36 (elements around, 1..=3600), "start": 0 (degrees: where the profile's half-plane lies, from +X towards +Y)} — sweeps the 2D mesh (quad4 and tri3 in the XY plane, x ≥ 0 the radius, Y the axis) about the axis into hex8 and wedge6 with the axis along Z: (r, y) at angle θ becomes (r cos θ, r sin θ, y). Replaces the 2D elements and their nodes; geometry tags carry over (a face's elements fill its body, an edge's element edges sweep to faces of that name, and a partial turn's first and last sides are faces `start` and `end`), so sets named by geometry resolve on the solid"#,
|
||||
form: Some(super::forms::REVOLVE),
|
||||
kind: CommandKind::Model,
|
||||
run: revolve,
|
||||
};
|
||||
|
||||
/// Most sectors of a sweep.
|
||||
const MAX_SECTORS: u64 = 3600;
|
||||
|
||||
/// The hex8 face (1-based) swept by each quad4 edge, and the wedge6 face swept by each tri3
|
||||
/// edge, with the profile's nodes reversed (see [`swept`]); faces 1 and 2 are the first and
|
||||
/// last sides of a sector.
|
||||
const QUAD_EDGE_FACE: [u8; 4] = [6, 5, 4, 3];
|
||||
const TRI_EDGE_FACE: [u8; 3] = [5, 4, 3];
|
||||
|
||||
/// A swept element's nodes: the profile's nodes reversed on ring `k`, then on ring `k + 1`.
|
||||
fn swept(profile: &[u32], ring: impl Fn(u32, usize) -> u32, k: usize) -> Vec<u32> {
|
||||
let reversed: Vec<u32> = profile.first().copied().into_iter().chain(profile.iter().skip(1).rev().copied()).collect();
|
||||
reversed.iter().map(|n| ring(*n, k)).chain(reversed.iter().map(|n| ring(*n, k + 1))).collect()
|
||||
}
|
||||
|
||||
fn revolve(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let angle = optional_number(p, "angle", 360.0)?;
|
||||
if !(angle > 0.0 && angle <= 360.0) {
|
||||
return Err(EngineError::Params(format!("`angle` must be in (0, 360] degrees, got {angle}")));
|
||||
}
|
||||
let start = optional_number(p, "start", 0.0)?;
|
||||
if !start.is_finite() {
|
||||
return Err(EngineError::Params("`start` must be a number of degrees".into()));
|
||||
}
|
||||
let sectors = optional_count(p, "sectors", 36, 1..=MAX_SECTORS)?;
|
||||
let whole = angle >= 360.0;
|
||||
if whole && sectors < 3 {
|
||||
return Err(EngineError::Params("a whole turn takes at least 3 sectors".into()));
|
||||
}
|
||||
// The profile: every element a linear 2D element in the XY plane, off the axis.
|
||||
let profile: Vec<(u32, ElementKind, Vec<u32>)> = s.model.elements().iter().map(|(l, e)| (*l, e.kind, e.nodes.clone())).collect();
|
||||
if profile.is_empty() {
|
||||
return Err(EngineError::Params("there is no 2D mesh to revolve".into()));
|
||||
}
|
||||
if let Some((l, k, _)) = profile.iter().find(|(_, k, _)| !matches!(k, ElementKind::Quad4 | ElementKind::Tri3)) {
|
||||
return Err(EngineError::Params(format!(
|
||||
"not supported yet: revolving {} elements (element {l}): mesh.revolve takes a mesh of quad4 and tri3 elements",
|
||||
k.name()
|
||||
)));
|
||||
}
|
||||
let used: Vec<u32> = {
|
||||
let mut v: Vec<u32> = profile.iter().flat_map(|(_, _, n)| n.iter().copied()).collect();
|
||||
v.sort_unstable();
|
||||
v.dedup();
|
||||
v
|
||||
};
|
||||
let mut points = Vec::with_capacity(used.len());
|
||||
for n in &used {
|
||||
let xyz = s.model.node(*n).map(|n| n.xyz).ok_or_else(|| EngineError::Params(format!("node {n} is missing")))?;
|
||||
if xyz[2] != 0.0 {
|
||||
return Err(EngineError::Params(format!("node {n} is off the XY plane (z = {})", xyz[2])));
|
||||
}
|
||||
if !(xyz[0] > 0.0) {
|
||||
return Err(EngineError::Params(format!(
|
||||
"not supported yet: revolving a profile that reaches the axis (node {n} at x = {}): its elements would collapse; keep the profile at x > 0",
|
||||
xyz[0]
|
||||
)));
|
||||
}
|
||||
points.push(xyz);
|
||||
}
|
||||
let rings = if whole { sectors } else { sectors + 1 };
|
||||
let count = |a: usize, b: usize| a.checked_mul(b).ok_or_else(|| EngineError::Params("the revolved mesh is too large".into()));
|
||||
let (node_count, element_count) = (count(used.len(), rings)?, count(profile.len(), sectors)?);
|
||||
if element_count > fieldcraft_model::MAX_ELEMENTS {
|
||||
return Err(EngineError::Params(format!("the revolved mesh would have {element_count} elements, more than {}", fieldcraft_model::MAX_ELEMENTS)));
|
||||
}
|
||||
// The tags to carry over, read before the profile is removed.
|
||||
let tags = s.model.classification().clone();
|
||||
let entity = |tag: u32| tags.entity(tag).cloned();
|
||||
let (first_node, first_element) = super::fresh_labels(&s.model, node_count, element_count)?;
|
||||
let index: BTreeMap<u32, usize> = used.iter().enumerate().map(|(i, n)| (*n, i)).collect();
|
||||
let ring = |n: u32, k: usize| -> u32 {
|
||||
let i = index.get(&n).copied().unwrap_or(0);
|
||||
first_node + ((k % rings) * used.len() + i) as u32
|
||||
};
|
||||
let (start_rad, step) = (start.to_radians(), angle.to_radians() / sectors as f64);
|
||||
for k in 0..rings {
|
||||
let (sin, cos) = (start_rad + step * k as f64).sin_cos();
|
||||
for (i, x) in points.iter().enumerate() {
|
||||
s.model.set_node(first_node + (k * used.len() + i) as u32, [x[0] * cos, x[0] * sin, x[1]])?;
|
||||
}
|
||||
}
|
||||
let mut label = first_element;
|
||||
let mut new_tags: Vec<(u32, u8, GeometryRef)> = Vec::new();
|
||||
let mut regions: Vec<(u32, GeometryRef)> = Vec::new();
|
||||
for k in 0..sectors {
|
||||
for (old, kind, nodes) in &profile {
|
||||
let (solid, edge_face) = match kind {
|
||||
ElementKind::Quad4 => (ElementKind::Hex8, &QUAD_EDGE_FACE[..]),
|
||||
_ => (ElementKind::Wedge6, &TRI_EDGE_FACE[..]),
|
||||
};
|
||||
s.model.set_element(label, solid, swept(nodes, ring, k))?;
|
||||
if let Some(t) = tags.elements().get(old) {
|
||||
if let Some(region) = t.region.and_then(entity) {
|
||||
let body = GeometryRef::body(region.body.clone())?;
|
||||
regions.push((label, body.clone()));
|
||||
if !whole && k == 0 {
|
||||
new_tags.push((label, 1, GeometryRef::face(body.body.clone(), "start".to_string())?));
|
||||
}
|
||||
if !whole && k + 1 == sectors {
|
||||
new_tags.push((label, 2, GeometryRef::face(body.body.clone(), "end".to_string())?));
|
||||
}
|
||||
}
|
||||
for (edge, tag) in &t.edges {
|
||||
let (Some(e), Some(face)) = (entity(*tag), edge_face.get(usize::from(*edge).saturating_sub(1))) else { continue };
|
||||
if let Entity::Edge(name) = &e.entity {
|
||||
new_tags.push((label, *face, GeometryRef::face(e.body.clone(), name.clone())?));
|
||||
}
|
||||
}
|
||||
}
|
||||
label += 1;
|
||||
}
|
||||
}
|
||||
// The profile goes; its tags with it.
|
||||
for (old, _, _) in &profile {
|
||||
s.model.remove_element(*old)?;
|
||||
}
|
||||
for n in &used {
|
||||
s.model.remove_node(*n)?;
|
||||
}
|
||||
for (element, body) in regions {
|
||||
let tag = s.model.geometry_tag(&body)?;
|
||||
s.model.tag_element(element, tag)?;
|
||||
}
|
||||
for (element, face, reference) in new_tags {
|
||||
let tag = s.model.geometry_tag(&reference)?;
|
||||
s.model.tag_element_face(element, face, tag)?;
|
||||
}
|
||||
Ok(json!({
|
||||
"nodes": node_count,
|
||||
"elements": element_count,
|
||||
"first_node": first_node,
|
||||
"first_element": first_element,
|
||||
"sectors": sectors,
|
||||
"angle": angle,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A ring of rectangular section (1 ≤ r ≤ 2, 0 ≤ y ≤ 1) as two quads and two triangles,
|
||||
/// revolved: its volume is π (2² − 1²) · 1 = 3π, every element valid, the tags carried.
|
||||
#[test]
|
||||
fn a_revolved_profile_is_a_valid_solid_of_the_right_volume() {
|
||||
for (angle, sectors) in [(360.0f64, 48usize), (90.0, 12)] {
|
||||
let mut s = Session::default();
|
||||
s.execute("mesh.planar", &json!({"size": 0.5, "polygon": [[1, 0], [2, 0], [2, 1], [1, 1]], "name": "Ring"})).unwrap();
|
||||
let profile = s.model.elements().len();
|
||||
let r = s.execute("mesh.revolve", &json!({"angle": angle, "sectors": sectors})).unwrap();
|
||||
assert_eq!(r["elements"].as_u64(), Some((profile * sectors) as u64));
|
||||
let q = s.execute("mesh.quality", &json!({})).unwrap();
|
||||
assert_eq!(q["invalid"]["count"].as_u64(), Some(0), "{q}");
|
||||
let volume: f64 = q["volume"].as_f64().unwrap();
|
||||
// Chords: a polygon of `sectors` sides over the arc.
|
||||
let chord = (angle.to_radians() / sectors as f64).sin() * sectors as f64 / angle.to_radians();
|
||||
let exact = 3.0 * std::f64::consts::PI * angle / 360.0;
|
||||
assert!((volume - exact * chord).abs() < 1e-9 * exact, "{volume} {}", exact * chord);
|
||||
// The bottom edge (e1, y = 0) sweeps to face e1: a set on it holds the z = 0 faces.
|
||||
s.execute("model.set.create", &json!({"name": "bottom", "geometry": [{"body": "Ring", "face": "e1"}]})).unwrap();
|
||||
let set = s.execute("model.set.resolve", &json!({"name": "bottom"})).unwrap();
|
||||
assert_eq!(set["faces"]["count"].as_u64(), Some(2 * sectors as u64), "two element edges along e1: {set}");
|
||||
if angle < 360.0 {
|
||||
s.execute("model.set.create", &json!({"name": "cut", "geometry": [{"body": "Ring", "face": "start"}]})).unwrap();
|
||||
let set = s.execute("model.set.resolve", &json!({"name": "cut"})).unwrap();
|
||||
assert_eq!(set["faces"]["count"].as_u64(), Some(profile as u64), "{set}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_mesh_revolve_refuses() {
|
||||
let mut s = Session::default();
|
||||
assert!(s.execute("mesh.revolve", &json!({})).is_err(), "no mesh");
|
||||
s.execute("mesh.planar", &json!({"size": 0.5, "polygon": [[0, 0], [2, 0], [2, 1], [0, 1]]})).unwrap();
|
||||
let e = s.execute("mesh.revolve", &json!({})).unwrap_err().to_string();
|
||||
assert!(e.contains("axis"), "{e}");
|
||||
let mut s = Session::default();
|
||||
s.execute("mesh.planar", &json!({"size": 0.5, "kind": "quad8", "polygon": [[1, 0], [2, 0], [2, 1], [1, 1]]})).unwrap();
|
||||
assert!(s.execute("mesh.revolve", &json!({})).unwrap_err().to_string().contains("quad8"));
|
||||
for bad in [json!({"angle": 0}), json!({"angle": 400}), json!({"sectors": 0}), json!({"angle": 360, "sectors": 2}), json!({"start": "x"})] {
|
||||
assert!(s.execute("mesh.revolve", &bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
-11
@@ -36,7 +36,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 (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"#,
|
||||
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, and with contact pairs (interaction.contact.pair: mortar contact by augmented Lagrangian, Coulomb friction by a return map) each contact's force, normal force, largest pressure, closed and slipping nodes, deepest penetration and augmentations (solve.probe gives each secondary node's pressure, gap and status, and with friction its shear traction and slip); the caps may only be lowered (`max_newton_iterations` over the whole step); `threads` as for solve.static"#,
|
||||
form: Some(super::forms::SOLVE_NONLINEAR),
|
||||
kind: CommandKind::Model,
|
||||
run: run_nonlinear,
|
||||
@@ -239,6 +239,16 @@ fn run_nonlinear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"frames": history.map(|h| h.load_factors()),
|
||||
"load_factor": history.and_then(|h| h.frames.last()).map(|f| f.load_factor),
|
||||
"critical_points": history.map(|h| &h.critical_points),
|
||||
"contacts": r.contacts.iter().map(|c| json!({
|
||||
"name": c.name,
|
||||
"normal_force": c.normal_force,
|
||||
"force": c.force,
|
||||
"max_pressure": largest(c.nodes.iter().copied().zip(c.pressure.iter().copied())),
|
||||
"closed_nodes": c.closed(),
|
||||
"slipping_nodes": c.status.iter().filter(|s| **s == fieldcraft_solver::contact::Status::Slip).count(),
|
||||
"penetration": c.penetration,
|
||||
"augmentations": c.augmentations,
|
||||
})).collect::<Vec<_>>(),
|
||||
"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)))),
|
||||
@@ -609,16 +619,36 @@ fn probe_implicit(result: &ImplicitResult, step: &str, nodes: &[u32], p: &Value)
|
||||
let p = r.reference_points.get(k);
|
||||
json!({"node": l, "reference_point": p.map(|p| &p.name), "displacement": p.map(|p| p.displacement), "reaction": p.map(|p| p.reaction)})
|
||||
}
|
||||
Location::Node(i) => json!({
|
||||
"node": l,
|
||||
"displacement": r.displacement.get(i),
|
||||
"rotation": r.rotation.get(i),
|
||||
"reaction": r.reaction.get(i),
|
||||
"reaction_moment": r.reaction_moment.get(i),
|
||||
"stress": r.stress.get(i),
|
||||
"von_mises": r.von_mises.get(i),
|
||||
"temperature": r.temperature.as_ref().and_then(|t| t.get(i)),
|
||||
}),
|
||||
Location::Node(i) => {
|
||||
let mut v = json!({
|
||||
"node": l,
|
||||
"displacement": r.displacement.get(i),
|
||||
"rotation": r.rotation.get(i),
|
||||
"reaction": r.reaction.get(i),
|
||||
"reaction_moment": r.reaction_moment.get(i),
|
||||
"stress": r.stress.get(i),
|
||||
"von_mises": r.von_mises.get(i),
|
||||
"temperature": r.temperature.as_ref().and_then(|t| t.get(i)),
|
||||
});
|
||||
// The contacts the node is a secondary node of: pressure, gap, status.
|
||||
let contact: Vec<Value> = r
|
||||
.contacts
|
||||
.iter()
|
||||
.filter_map(|c| {
|
||||
let k = c.nodes.binary_search(&l).ok()?;
|
||||
let mut entry = json!({"contact": c.name, "pressure": c.pressure.get(k), "gap": c.gap.get(k), "status": c.status.get(k)});
|
||||
if let (Some(shear), Some(slip)) = (c.shear.get(k), c.slip.get(k)) {
|
||||
entry["shear"] = json!(shear);
|
||||
entry["slip"] = json!(slip);
|
||||
}
|
||||
Some(entry)
|
||||
})
|
||||
.collect();
|
||||
if !contact.is_empty() {
|
||||
v["contact"] = Value::Array(contact);
|
||||
}
|
||||
v
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
@@ -1509,4 +1539,86 @@ pub(crate) mod tests {
|
||||
let all = s.execute("solve.run", &json!({})).unwrap();
|
||||
assert_eq!(all["steps"].as_array().map(Vec::len), Some(3), "{all}");
|
||||
}
|
||||
|
||||
/// Two blocks pressed together across a contact pair, as commands.
|
||||
fn pressed_blocks(s: &mut Session) {
|
||||
let script = json!([
|
||||
{"command": "mesh.block", "params": {"body": "Lower", "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": [2, 2, 1], "faces": {"1": "bottom", "2": "top"}}},
|
||||
{"command": "mesh.block", "params": {"body": "Upper", "kind": "hex8", "corners": [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1], [0, 0, 2], [1, 0, 2], [1, 1, 2], [0, 1, 2]], "divisions": [3, 3, 1], "faces": {"1": "bottom", "2": "top"}, "merge": 0}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Lower"}, {"body": "Upper"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "lower_top", "geometry": [{"body": "Lower", "face": "top"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "upper_bottom", "geometry": [{"body": "Upper", "face": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Lower", "face": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Upper", "face": "top"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "all", "set": "all", "material": "Mat", "kind": {"type": "solid"}}},
|
||||
{"command": "interaction.contact.pair", "params": {"name": "interface", "main": "lower_top", "secondary": "upper_bottom"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "base", "type": "fixed", "target": {"set": "base"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "push", "type": "displacement", "target": {"set": "top"}, "values": [0, 0, -0.02, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Squeeze", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false}, "boundary_conditions": ["base", "push"], "loads": []}}
|
||||
]);
|
||||
s.run_script(&script).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solve_and_probe_a_contact() {
|
||||
let mut s = Session::default();
|
||||
pressed_blocks(&mut s);
|
||||
let r = s.execute("solve.nonlinear", &json!({"step": "Squeeze"})).unwrap();
|
||||
let c = &r["contacts"][0];
|
||||
assert_eq!(c["name"], "interface");
|
||||
// ν = 0 and held ends: uniaxial strain 1% of E = 1000, over the unit area.
|
||||
let force = c["normal_force"].as_f64().unwrap();
|
||||
assert!((force - 10.0).abs() < 2e-3, "{r}");
|
||||
assert_eq!(c["closed_nodes"], 16, "{r}");
|
||||
// A secondary node gives its contact pressure, gap and status; a main one gives none.
|
||||
let third = 1.0 / 3.0;
|
||||
let probe = s.execute("solve.probe", &json!({"step": "Squeeze", "at": [[third, third, 1.0], [0.5, 0.5, 1.0]]})).unwrap();
|
||||
let contact = &probe["nodes"][0]["contact"][0];
|
||||
assert_eq!(contact["status"], "closed", "{probe}");
|
||||
assert!((contact["pressure"].as_f64().unwrap() - 10.0).abs() < 2e-3, "{probe}");
|
||||
assert!(contact["gap"].as_f64().unwrap() < 0.0, "{probe}");
|
||||
assert!(probe["nodes"][1].get("contact").is_none(), "{probe}");
|
||||
// The contact fields on the results display.
|
||||
let v = s.execute("results.contour", &json!({"step": "Squeeze", "field": "contact_pressure"})).unwrap();
|
||||
let range = &v["contour"]["range"];
|
||||
assert!((range["max"]["value"].as_f64().unwrap() - 10.0).abs() < 2e-3 && range["min"]["value"] == 0.0, "{v}");
|
||||
let status = s.execute("results.contour", &json!({"step": "Squeeze", "field": "contact_status"})).unwrap();
|
||||
assert_eq!(status["contour"]["range"]["max"]["value"], 1.0, "{status}");
|
||||
// The linear analyses refuse the contact by name.
|
||||
s.execute(
|
||||
"model.step.create",
|
||||
&json!({"name": "Linear", "procedure": {"type": "static"}, "boundary_conditions": ["base", "push"], "loads": []}),
|
||||
)
|
||||
.unwrap();
|
||||
let e = s.execute("solve.static", &json!({"step": "Linear"})).unwrap_err().to_string();
|
||||
assert!(e.contains("interface"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solve_and_probe_a_frictional_contact() {
|
||||
let mut s = Session::default();
|
||||
pressed_blocks(&mut s);
|
||||
s.execute("interaction.contact.edit", &json!({"name": "interface", "surfaces": {"type": "pair", "main": "lower_top", "secondary": "upper_bottom"}, "friction": {"static": 0.2}})).unwrap();
|
||||
s.execute(
|
||||
"model.boundary_condition.edit",
|
||||
&json!({"name": "push", "type": "displacement", "target": {"set": "top"}, "values": [0.05, 0, -0.02, null, null, null]}),
|
||||
)
|
||||
.unwrap();
|
||||
let r = s.execute("solve.nonlinear", &json!({"step": "Squeeze"})).unwrap();
|
||||
let c = &r["contacts"][0];
|
||||
assert_eq!(c["slipping_nodes"], 16, "{r}");
|
||||
// Slipping, the interface carries μ N sideways.
|
||||
let (n, t) = (c["force"][2].as_f64().unwrap(), c["force"][0].as_f64().unwrap());
|
||||
assert!((t + 0.2 * n).abs() < 1e-3 * n, "{r}");
|
||||
let third = 1.0 / 3.0;
|
||||
let probe = s.execute("solve.probe", &json!({"step": "Squeeze", "at": [[third, third, 1.0]]})).unwrap();
|
||||
let contact = &probe["nodes"][0]["contact"][0];
|
||||
assert_eq!(contact["status"], "slip", "{probe}");
|
||||
assert!(contact["shear"][0].as_f64().unwrap() < 0.0 && contact["slip"][0].as_f64().unwrap() > 0.0, "{probe}");
|
||||
let v = s.execute("results.contour", &json!({"step": "Squeeze", "field": "contact_status"})).unwrap();
|
||||
assert_eq!(v["contour"]["range"]["max"]["value"], 2.0, "{v}");
|
||||
let v = s.execute("results.contour", &json!({"step": "Squeeze", "field": "contact_shear"})).unwrap();
|
||||
assert!(v["contour"]["range"]["max"]["value"].as_f64().unwrap() > 1.0, "{v}");
|
||||
}
|
||||
}
|
||||
@@ -87,10 +87,22 @@ pub enum Field {
|
||||
Temperature,
|
||||
/// Heat flux magnitude (heat transfer steps, at the end of the step).
|
||||
HeatFlux,
|
||||
/// Contact pressure (nonlinear static steps with contact: 0 off the contacts' secondary
|
||||
/// surfaces).
|
||||
ContactPressure,
|
||||
/// The normalised contact gap of the secondary surfaces' nodes (negative: penetration).
|
||||
ContactGap,
|
||||
/// Contact status of the secondary surfaces' nodes: 0 open, 1 closed (frictionless) or
|
||||
/// sticking, 2 slipping.
|
||||
ContactStatus,
|
||||
/// The magnitude of the friction's shear traction (0 off the secondary surfaces).
|
||||
ContactShear,
|
||||
/// The magnitude of the frictional slip over the last increment (secondary surfaces' nodes).
|
||||
ContactSlip,
|
||||
}
|
||||
|
||||
impl Field {
|
||||
pub const ALL: [Field; 17] = [
|
||||
pub const ALL: [Field; 22] = [
|
||||
Field::Displacement,
|
||||
Field::DisplacementX,
|
||||
Field::DisplacementY,
|
||||
@@ -108,6 +120,11 @@ impl Field {
|
||||
Field::StressYz,
|
||||
Field::Temperature,
|
||||
Field::HeatFlux,
|
||||
Field::ContactPressure,
|
||||
Field::ContactGap,
|
||||
Field::ContactStatus,
|
||||
Field::ContactShear,
|
||||
Field::ContactSlip,
|
||||
];
|
||||
|
||||
/// The id commands take (`von_mises`…).
|
||||
@@ -130,6 +147,11 @@ impl Field {
|
||||
Field::StressYz => "stress_yz",
|
||||
Field::Temperature => "temperature",
|
||||
Field::HeatFlux => "heat_flux",
|
||||
Field::ContactPressure => "contact_pressure",
|
||||
Field::ContactGap => "contact_gap",
|
||||
Field::ContactStatus => "contact_status",
|
||||
Field::ContactShear => "contact_shear",
|
||||
Field::ContactSlip => "contact_slip",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +174,11 @@ impl Field {
|
||||
Field::StressYz => "Stress YZ",
|
||||
Field::Temperature => "Temperature",
|
||||
Field::HeatFlux => "Heat flux magnitude",
|
||||
Field::ContactPressure => "Contact pressure",
|
||||
Field::ContactGap => "Contact gap",
|
||||
Field::ContactStatus => "Contact status (0 open, 1 closed or stick, 2 slip)",
|
||||
Field::ContactShear => "Contact shear traction",
|
||||
Field::ContactSlip => "Contact slip",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +197,12 @@ impl Field {
|
||||
| Field::Velocity
|
||||
| Field::Temperature
|
||||
| Field::HeatFlux
|
||||
)
|
||||
) && !self.is_contact()
|
||||
}
|
||||
|
||||
/// Whether it is a contact quantity.
|
||||
pub fn is_contact(self) -> bool {
|
||||
matches!(self, Field::ContactPressure | Field::ContactGap | Field::ContactStatus | Field::ContactShear | Field::ContactSlip)
|
||||
}
|
||||
|
||||
/// Whether it is a heat transfer quantity.
|
||||
@@ -181,6 +213,7 @@ impl Field {
|
||||
/// The fields a result has: displacements everywhere, stresses for static steps and explicit
|
||||
/// frames that kept them (mode shapes have no stress scale), velocity for explicit frames.
|
||||
pub fn available(r: &StepResult) -> impl Iterator<Item = Field> + '_ {
|
||||
let contact = matches!(r.kind(), Kind::Static(s) if !s.contacts.is_empty());
|
||||
let (stress, velocity, temperature, flux) = match r.kind() {
|
||||
Kind::Static(s) => (true, false, s.temperature.is_some(), false),
|
||||
Kind::Frequency(_) => (false, false, false, false),
|
||||
@@ -197,6 +230,7 @@ impl Field {
|
||||
Field::Velocity => velocity,
|
||||
Field::Temperature => temperature,
|
||||
Field::HeatFlux => flux,
|
||||
f if f.is_contact() => contact,
|
||||
f => stress || !f.is_stress(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -333,6 +367,10 @@ pub fn field_values(model: &Model, r: &StepResult, field: Field, pick: Pick) ->
|
||||
Kind::Heat(h) => Ok(h.flux.iter().map(|v| magnitude(*v)).collect()),
|
||||
_ => Err(unavailable()),
|
||||
},
|
||||
Field::ContactPressure | Field::ContactGap | Field::ContactStatus | Field::ContactShear | Field::ContactSlip => match r.kind() {
|
||||
Kind::Static(s) if !s.contacts.is_empty() => Ok(contact_values(s, field)),
|
||||
_ => Err(unavailable()),
|
||||
},
|
||||
Field::VonMises => each(&fieldcraft_fem::von_mises),
|
||||
Field::MaxPrincipal => principal(0),
|
||||
Field::MidPrincipal => principal(1),
|
||||
@@ -346,6 +384,45 @@ pub fn field_values(model: &Model, r: &StepResult, field: Field, pick: Pick) ->
|
||||
}
|
||||
}
|
||||
|
||||
/// A contact field at every node of a static result: the largest pressure, shear, slip or status
|
||||
/// code, or the smallest gap, over the contacts a node is a secondary node of; off them 0
|
||||
/// pressure and shear, and no gap, status or slip (NaN).
|
||||
fn contact_values(s: &StaticResult, field: Field) -> Vec<f64> {
|
||||
use fieldcraft_solver::contact::Status;
|
||||
let zero = matches!(field, Field::ContactPressure | Field::ContactShear);
|
||||
let mut out = vec![if zero { 0.0 } else { f64::NAN }; s.nodes.len()];
|
||||
let larger = |v: &mut f64, x: f64| *v = if v.is_nan() { x } else { v.max(x) };
|
||||
for c in &s.contacts {
|
||||
for (k, label) in c.nodes.iter().enumerate() {
|
||||
let Some(v) = s.nodes.binary_search(label).ok().and_then(|i| out.get_mut(i)) else { continue };
|
||||
match field {
|
||||
Field::ContactPressure => larger(v, c.pressure.get(k).copied().unwrap_or(0.0)),
|
||||
Field::ContactShear => larger(v, c.shear.get(k).map_or(0.0, |t| magnitude(*t))),
|
||||
Field::ContactSlip => {
|
||||
if let Some(g) = c.slip.get(k) {
|
||||
larger(v, magnitude(*g));
|
||||
}
|
||||
}
|
||||
Field::ContactGap => {
|
||||
if let Some(Some(g)) = c.gap.get(k) {
|
||||
*v = if v.is_nan() { *g } else { v.min(*g) };
|
||||
}
|
||||
}
|
||||
Field::ContactStatus => {
|
||||
let code = match c.status.get(k) {
|
||||
Some(Status::Closed | Status::Stick) => 1.0,
|
||||
Some(Status::Slip) => 2.0,
|
||||
Some(Status::Open) | None => 0.0,
|
||||
};
|
||||
larger(v, code);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Element values (in `elements` order) averaged at the nodes (in `nodes` order) of the
|
||||
/// elements around them; a node no element reaches gets NaN.
|
||||
fn nodal_average(model: &Model, nodes: &[u32], elements: &[u32], values: &[[f64; 6]]) -> Vec<[f64; 6]> {
|
||||
@@ -472,7 +549,7 @@ mod tests {
|
||||
}
|
||||
let (modes, _) = s.latest_result("Modes").unwrap();
|
||||
assert_eq!(Field::available(modes).count(), 4);
|
||||
assert_eq!(Field::available(bend).count(), Field::ALL.len() - 3, "all but velocity and the thermal fields");
|
||||
assert_eq!(Field::available(bend).count(), Field::ALL.len() - 8, "all but velocity, the thermal and the contact fields");
|
||||
assert!(field_values(&s.model, modes, Field::VonMises, Pick { mode: 1, frame: None }).is_err());
|
||||
assert!(field_values(&s.model, modes, Field::Displacement, Pick { mode: 9, frame: None }).is_err());
|
||||
assert_eq!(field_values(&s.model, modes, Field::Displacement, Pick { mode: 2, frame: None }).unwrap().len(), modes.nodes().len());
|
||||
|
||||
+573
-4
@@ -9,7 +9,15 @@
|
||||
//! [`cantilever`], a model ready to solve: a steel beam clamped at one end with a load on the
|
||||
//! other, a static and a frequency step. [`impact`]: the same beam flying onto a rigid wall, an
|
||||
//! explicit dynamic step.
|
||||
//!
|
||||
//! The examples gallery (docs/examples.md), each ready to solve with `solve.run`:
|
||||
//! [`drop_test`], [`rubber_seal`], [`flow_past`], [`heated_bracket`], [`bracket_hex`] and [`crank_arm`]. Their
|
||||
//! geometry is a SolidCraft script run when the sample opens ([`Sample::design`]): a corpus
|
||||
//! recipe (`tests/corpus`) or [`UPRIGHT_BRACKET`].
|
||||
|
||||
pub mod boot;
|
||||
|
||||
use fieldcraft_geometry::Shape;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// A sample the app offers (Help › Samples, the empty viewport, `file.sample`, `--sample NAME`).
|
||||
@@ -18,23 +26,77 @@ pub struct Sample {
|
||||
pub label: &'static str,
|
||||
/// One line: what it shows.
|
||||
pub about: &'static str,
|
||||
pub script: fn() -> Value,
|
||||
/// The SolidCraft script (`{"commands": [...]}`, the corpus recipe form) that builds the
|
||||
/// sample's geometry, opened as its design before [`Sample::script`] runs.
|
||||
pub design: Option<&'static str>,
|
||||
/// The model's commands, given the design's bodies (none without a design).
|
||||
pub script: fn(&[Shape]) -> Value,
|
||||
}
|
||||
|
||||
/// Every sample, the default first.
|
||||
pub const SAMPLES: &[Sample] = &[
|
||||
Sample { name: "parts", label: "Hex and Tet Parts", about: "A hex8 block beside a tet4 cube: meshes to look at, nothing to solve", script },
|
||||
Sample {
|
||||
name: "parts",
|
||||
label: "Hex and Tet Parts",
|
||||
about: "A hex8 block beside a tet4 cube: meshes to look at, nothing to solve",
|
||||
design: None,
|
||||
script: |_| script(),
|
||||
},
|
||||
Sample {
|
||||
name: "cantilever",
|
||||
label: "Cantilever",
|
||||
about: "A clamped steel beam with an end load: a static and a frequency step, ready to solve",
|
||||
script: cantilever,
|
||||
design: None,
|
||||
script: |_| cantilever(),
|
||||
},
|
||||
Sample {
|
||||
name: "impact",
|
||||
label: "Bar Impact",
|
||||
about: "The steel beam flying onto a rigid wall: an explicit dynamic step, ready to solve",
|
||||
script: impact,
|
||||
design: None,
|
||||
script: |_| impact(),
|
||||
},
|
||||
Sample {
|
||||
name: "drop-test",
|
||||
label: "Drop Test",
|
||||
about: "An aluminium L-bracket dropped 2 m onto a tilted rigid floor: explicit dynamics, the strike and its stress waves",
|
||||
design: Some(UPRIGHT_BRACKET),
|
||||
script: |_| drop_test(),
|
||||
},
|
||||
Sample {
|
||||
name: "rubber-seal",
|
||||
label: "Rubber Seal",
|
||||
about: "A rubber gasket ring squeezed 25 % between bonded plates: nonlinear static, Mooney–Rivlin, large strain",
|
||||
design: None,
|
||||
script: |_| rubber_seal(),
|
||||
},
|
||||
Sample {
|
||||
name: "flow-past",
|
||||
label: "Flow Past a Bracket",
|
||||
about: "Air flowing past an L-bracket on the floor: lattice Boltzmann flow and the force on each face",
|
||||
design: Some(UPRIGHT_BRACKET),
|
||||
script: |_| flow_past(),
|
||||
},
|
||||
Sample {
|
||||
name: "heated-bracket",
|
||||
label: "Heated Bracket",
|
||||
about: "A steel bracket clamped at its holes and heated at one end: steady heat conduction and the thermal stress",
|
||||
design: Some(FILLETED_BRACKET),
|
||||
script: |_| heated_bracket(),
|
||||
},
|
||||
Sample {
|
||||
name: "bracket-hex",
|
||||
label: "Hex-Meshed Bracket",
|
||||
about: "The filleted bracket split into 30 cells and meshed all-hex (hex20): clamped, loaded, von Mises",
|
||||
design: Some(PARTITIONED_BRACKET),
|
||||
script: bracket_hex,
|
||||
},
|
||||
Sample {
|
||||
name: "crank-arm",
|
||||
label: "Bicycle Crank Arm",
|
||||
about: "An aluminium bicycle crank swept all-hex (hex20): square bore fixed, a rider's weight on the pedal, von Mises",
|
||||
design: Some(CRANK_ARM),
|
||||
script: crank_arm,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -210,6 +272,267 @@ pub fn impact() -> Value {
|
||||
Value::Array(steps)
|
||||
}
|
||||
|
||||
/// An L-bracket standing on its foot, Z up, as a SolidCraft script: a 60 × 40 mm foot and a 50 mm
|
||||
/// upright, both 8 mm thick, with an R6 fillet inside the bend (the corpus `filleted-bracket`'s
|
||||
/// profile without its holes, sketched on XZ so it sweeps along y). Body `Bracket`; its faces
|
||||
/// `F2:side:l1` (the foot's underside, z = 0), `l2` (the foot's end, x = 60), `l3` (the foot's top),
|
||||
/// `l4` (the upright's inside, x = 8), `l5` (the upright's top, z = 50) and `l6` (its back, x = 0).
|
||||
pub const UPRIGHT_BRACKET: &str = r#"{"commands": [
|
||||
{"command": "sketch.create", "params": {"plane": "XZ", "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, 20, 8]], "radius": 6}}
|
||||
]}"#;
|
||||
|
||||
/// Aluminium (E = 70 GPa, ν = 0.33, ρ = 2.7 t/m³, yield 240 MPa with 700 MPa linear hardening),
|
||||
/// in mm, t, s, N, MPa.
|
||||
fn aluminium(name: &str) -> Value {
|
||||
json!({"command": "model.material.create", "params": {"name": name, "elastic": {"type": "isotropic", "young": 70000, "poisson": 0.33}, "density": 2.7e-9,
|
||||
"plastic": {"type": "bilinear", "yield_stress": 240, "hardening_modulus": 700, "hardening": "isotropic"}}})
|
||||
}
|
||||
|
||||
/// Standard gravity, mm/s².
|
||||
pub const GRAVITY: f64 = 9810.0;
|
||||
/// The drop test's height of fall, mm: the bracket meets the floor at √(2 g h) = 6.26 m/s.
|
||||
pub const DROP_HEIGHT: f64 = 2000.0;
|
||||
/// The drop test's floor tilt about y, degrees: the foot's end strikes first.
|
||||
const DROP_TILT: f64 = 15.0;
|
||||
|
||||
/// [`UPRIGHT_BRACKET`] in aluminium, hex8 elements swept 3 mm (one-point, stiffness hourglass
|
||||
/// control), falling at √(2 g h) onto a rigid floor tilted 15° so the foot's end lands first,
|
||||
/// 0.1 mm below that corner; gravity on. Step `Drop` (explicit dynamics, 1 ms: the strike, the
|
||||
/// stress waves through the bend and the bracket pivoting onto its foot; 20 field frames, the
|
||||
/// upright's top (set `top`) in the histories).
|
||||
pub fn drop_test() -> Value {
|
||||
let speed = (2.0 * GRAVITY * DROP_HEIGHT).sqrt();
|
||||
let tilt = DROP_TILT.to_radians();
|
||||
let step = json!({
|
||||
"type": "explicit_dynamic", "end_time": 1e-3, "output_interval": 5e-5, "history_interval": 5e-6, "history_nodes": "top",
|
||||
"initial_velocities": [{"set": "bracket", "velocity": [0, 0, -speed]}],
|
||||
"rigid_walls": [{"set": "bracket", "point": [60, 0, -0.1], "normal": [-tilt.sin(), 0, tilt.cos()]}],
|
||||
"hourglass": {"type": "stiffness", "coefficient": 0.05}
|
||||
});
|
||||
json!([
|
||||
{"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}},
|
||||
{"command": "mesh.hex.generate", "params": {"body": "Bracket", "size": 3, "method": "extrude"}},
|
||||
{"command": "model.set.create", "params": {"name": "bracket", "geometry": [{"body": "Bracket"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Bracket", "face": "F2:side:l5"}]}},
|
||||
aluminium("Aluminium"),
|
||||
{"command": "model.section.create", "params": {"name": "Bracket", "set": "bracket", "material": "Aluminium", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
|
||||
{"command": "model.load.create", "params": {"name": "Gravity", "type": "gravity", "acceleration": [0, 0, -GRAVITY]}},
|
||||
{"command": "model.step.create", "params": {"name": "Drop", "procedure": step, "loads": ["Gravity"]}},
|
||||
])
|
||||
}
|
||||
|
||||
/// The rubber seal: inner and outer radius and height of the ring (mm), and how far it is squeezed.
|
||||
pub const SEAL: [f64; 3] = [20.0, 30.0, 10.0];
|
||||
pub const SEAL_SQUEEZE: f64 = 2.5;
|
||||
|
||||
/// A rubber gasket ring ([`SEAL`]: 20 mm inside radius, 30 mm outside, 10 mm high) of hybrid hex8
|
||||
/// elements, four mapped quarter blocks with arc edges (body `Seal`, block faces `bottom` and
|
||||
/// `top`), squeezed by [`SEAL_SQUEEZE`] (25 %) between plates bonded to it: `bottom` fixed, `top`
|
||||
/// moved down with its in-plane motion held. Generic rubber: E = 6 MPa, ν = 0.49, Mooney–Rivlin
|
||||
/// with C01 = 0.2 MPa (C10 = μ/2 − C01 = 0.81 MPa). Step `Squeeze` (nonlinear static).
|
||||
pub fn rubber_seal() -> Value {
|
||||
let [inner, outer, height] = SEAL;
|
||||
let mut steps = vec![json!({"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}})];
|
||||
for quarter in 0..4u8 {
|
||||
let (a0, a1) = (f64::from(quarter) * std::f64::consts::FRAC_PI_2, f64::from(quarter + 1) * std::f64::consts::FRAC_PI_2);
|
||||
let at = |r: f64, a: f64, z: f64| [r * a.cos(), r * a.sin(), z];
|
||||
let corners = [
|
||||
at(inner, a0, 0.0),
|
||||
at(outer, a0, 0.0),
|
||||
at(outer, a1, 0.0),
|
||||
at(inner, a1, 0.0),
|
||||
at(inner, a0, height),
|
||||
at(outer, a0, height),
|
||||
at(outer, a1, height),
|
||||
at(inner, a1, height),
|
||||
];
|
||||
let arc = |a: u8, b: u8, z: f64| json!({"corners": [a, b], "arc": {"center": [0, 0, z]}});
|
||||
steps.push(json!({"command": "mesh.block", "params": {
|
||||
"body": "Seal", "kind": "hex8", "corners": corners, "divisions": [5, 10, 5],
|
||||
"edges": [arc(1, 4, 0.0), arc(2, 3, 0.0), arc(5, 8, height), arc(6, 7, height)],
|
||||
"faces": {"1": "bottom", "2": "top"}
|
||||
}}));
|
||||
}
|
||||
steps.extend([
|
||||
json!({"command": "model.set.create", "params": {"name": "seal", "geometry": [{"body": "Seal"}]}}),
|
||||
json!({"command": "model.set.create", "params": {"name": "bottom", "geometry": [{"body": "Seal", "face": "bottom"}]}}),
|
||||
json!({"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Seal", "face": "top"}]}}),
|
||||
json!({"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 6, "poisson": 0.49}, "density": 1.1e-9,
|
||||
"hyperelastic": {"type": "mooney_rivlin", "c01": 0.2}}}),
|
||||
json!({"command": "model.section.create", "params": {"name": "Seal", "set": "seal", "material": "Rubber", "kind": {"type": "solid"}, "formulation": {"hybrid": true}}}),
|
||||
json!({"command": "model.boundary_condition.create", "params": {"name": "Base", "type": "fixed", "target": {"set": "bottom"}}}),
|
||||
json!({"command": "model.boundary_condition.create", "params": {"name": "Press", "type": "displacement", "target": {"set": "top"}, "values": [0, 0, -SEAL_SQUEEZE, null, null, null]}}),
|
||||
json!({"command": "model.step.create", "params": {"name": "Squeeze", "procedure": {"type": "nonlinear_static", "initial_increment": 0.1}, "boundary_conditions": ["Base", "Press"]}}),
|
||||
]);
|
||||
Value::Array(steps)
|
||||
}
|
||||
|
||||
/// The flow sample's free-stream speed, mm/s.
|
||||
pub const FLOW_SPEED: f64 = 25.0;
|
||||
/// Air at 20 °C: density (t/mm³) and dynamic viscosity (MPa s).
|
||||
const AIR: [f64; 2] = [1.2e-12, 1.8e-11];
|
||||
|
||||
/// Air at [`FLOW_SPEED`] past [`UPRIGHT_BRACKET`] standing on the floor, the upright's back face
|
||||
/// to the wind: a 240 × 120 × 100 mm box of 2.5 mm cells (184 320), a uniform inlet at x_min, an
|
||||
/// outlet at zero pressure at x_max, a no-slip floor and slip walls at the sides and top. Reynolds
|
||||
/// number ρ U h / μ = 83 on the upright's 50 mm height: laminar, a steady wake. Step `Flow` (16 s,
|
||||
/// about 6.7 flow-throughs; ten frames) reports the force on each named face of the bracket.
|
||||
pub fn flow_past() -> Value {
|
||||
let [density, viscosity] = AIR;
|
||||
json!([
|
||||
{"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}},
|
||||
{"command": "model.material.create", "params": {"name": "Air", "density": density, "fluid": {"type": "newtonian", "viscosity": viscosity}}},
|
||||
{"command": "model.fluid_domain.create", "params": {
|
||||
"name": "Wind", "material": "Air", "min": [-60, -40, 0], "max": [180, 80, 100], "spacing": 2.5,
|
||||
"faces": {
|
||||
"x_min": {"type": "velocity", "velocity": [FLOW_SPEED, 0, 0]}, "x_max": {"type": "pressure", "pressure": 0},
|
||||
"y_min": {"type": "slip"}, "y_max": {"type": "slip"}, "z_min": {"type": "wall"}, "z_max": {"type": "slip"}
|
||||
},
|
||||
"obstacles": ["Bracket"], "initial_velocity": [FLOW_SPEED, 0, 0]
|
||||
}},
|
||||
{"command": "model.step.create", "params": {"name": "Flow", "procedure": {"type": "fluid", "domain": "Wind", "end_time": 16, "frames": 10}}},
|
||||
])
|
||||
}
|
||||
|
||||
/// The corpus filleted bracket (`tests/corpus/filleted-bracket`): an 8 mm L-bracket, foot along
|
||||
/// x (60 mm) and upright along y (50 mm), extruded 40 mm along z, an R6 fillet, an 8 mm hole
|
||||
/// through each leg (`F4` through the foot at x = 40, `F5` through the upright at y = 32).
|
||||
pub const FILLETED_BRACKET: &str = include_str!("../../../tests/corpus/filleted-bracket/recipe.json");
|
||||
|
||||
/// The heated bracket: the heater's and the surrounding air's temperatures (°C, also the stress
|
||||
/// free temperature), and steel's expansion coefficient (1/K).
|
||||
pub const HEATER: f64 = 120.0;
|
||||
pub const AMBIENT: f64 = 20.0;
|
||||
pub const STEEL_EXPANSION: f64 = 1.2e-5;
|
||||
|
||||
/// [`FILLETED_BRACKET`] in steel (k = 50 W/(m K), α = 12 × 10⁻⁶/K, stress free at 20 °C), tet10
|
||||
/// elements of 4 mm: its upright's end (`F2:side:l5`, set `hot`) held at [`HEATER`], the rest of
|
||||
/// its outside (set `skin`) cooled by still air at [`AMBIENT`] (h = 10 W/(m² K)), both hole bores
|
||||
/// clamped (set `holes`). Step `Heat` (coupled temperature–displacement, steady): the
|
||||
/// temperature field and the stress of the expansion the clamps hold back. In mm, t, s, N, MPa:
|
||||
/// k in mW/(mm K), h in mW/(mm² K).
|
||||
pub fn heated_bracket() -> Value {
|
||||
let face = |f: &str| json!({"body": "Bracket", "face": f});
|
||||
let skin: Vec<Value> =
|
||||
["F2:start", "F2:top", "F2:side:l1", "F2:side:l2", "F2:side:l3", "F2:side:l4", "F2:side:l6", "F3:blend:F2:side:l3|F2:side:l4"]
|
||||
.into_iter()
|
||||
.map(face)
|
||||
.collect();
|
||||
let holes: Vec<Value> = ["F4:bore:side:0", "F4:bore:side:1", "F5:bore:side:0", "F5:bore:side:1"].into_iter().map(face).collect();
|
||||
json!([
|
||||
{"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}},
|
||||
{"command": "mesh.tet.generate", "params": {"body": "Bracket", "size": 4, "order": "quadratic", "tolerance": 0.05}},
|
||||
{"command": "model.set.create", "params": {"name": "bracket", "geometry": [{"body": "Bracket"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "holes", "geometry": holes}},
|
||||
{"command": "model.set.create", "params": {"name": "hot", "geometry": [face("F2:side:l5")]}},
|
||||
{"command": "model.set.create", "params": {"name": "skin", "geometry": skin}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3}, "density": 7.85e-9,
|
||||
"thermal": {"conductivity": 50, "specific_heat": 4.6e8, "expansion": STEEL_EXPANSION, "reference_temperature": AMBIENT}}},
|
||||
{"command": "model.section.create", "params": {"name": "Bracket", "set": "bracket", "material": "Steel", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "Clamp", "type": "fixed", "target": {"set": "holes"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "Heater", "type": "temperature", "target": {"set": "hot"}, "value": HEATER}},
|
||||
{"command": "model.load.create", "params": {"name": "Air", "type": "convection", "set": "skin", "film": 0.01, "sink": AMBIENT}},
|
||||
{"command": "model.step.create", "params": {"name": "Heat", "procedure": {"type": "coupled_temperature_displacement"}, "boundary_conditions": ["Clamp", "Heater"], "loads": ["Air"]}},
|
||||
])
|
||||
}
|
||||
|
||||
/// The corpus filleted bracket split into 30 cells (`tests/corpus/filleted-bracket-blocks`),
|
||||
/// each a mapped block: the part `mesh.hex.blocks` meshes all-hex.
|
||||
pub const PARTITIONED_BRACKET: &str = include_str!("../../../tests/corpus/filleted-bracket-blocks/recipe.json");
|
||||
|
||||
/// [`PARTITIONED_BRACKET`] meshed all-hex as one conforming mesh of hex20 elements of 3 mm, in
|
||||
/// steel: the upright's end (`F2:side:l5` in every cell that has it, set `top`) clamped, 1 kN down
|
||||
/// (−y) on the foot's end (`F2:side:l2`, set `tip`). Step `Load` (linear static). `bodies` are the
|
||||
/// design's cells.
|
||||
pub fn bracket_hex(bodies: &[Shape]) -> Value {
|
||||
let faces = |name: &str| -> Vec<Value> {
|
||||
bodies.iter().filter(|b| b.faces().iter().any(|f| f.name == name)).map(|b| json!({"body": b.name, "face": name})).collect()
|
||||
};
|
||||
let cells: Vec<Value> = bodies.iter().map(|b| json!({"body": b.name})).collect();
|
||||
json!([
|
||||
{"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}},
|
||||
{"command": "mesh.hex.blocks", "params": {"size": 3, "order": 2}},
|
||||
{"command": "model.set.create", "params": {"name": "bracket", "geometry": cells}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": faces("F2:side:l5")}},
|
||||
{"command": "model.set.create", "params": {"name": "tip", "geometry": faces("F2:side:l2")}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3}, "density": 7.85e-9}},
|
||||
{"command": "model.section.create", "params": {"name": "Bracket", "set": "bracket", "material": "Steel", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "Clamp", "type": "fixed", "target": {"set": "top"}}},
|
||||
{"command": "model.load.create", "params": {"name": "Push", "type": "force", "target": {"set": "tip"}, "vector": [0, -1000, 0]}},
|
||||
{"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["Clamp"], "loads": ["Push"]}},
|
||||
])
|
||||
}
|
||||
|
||||
/// The bicycle crank arm (`crates/engine/samples/crank-arm.json`): a 170 mm aluminium arm, 16 mm
|
||||
/// thick and tapering from 38 to 24 mm, joined by tangent root arcs to a Ø46 × 22 spindle boss
|
||||
/// with a 12.7 mm square bore (R1.5 corners) and a Ø30 × 20 pedal boss with a Ø14 hole, and an
|
||||
/// 82 × 14 × 8 pocket in its back (R5 corners). Made to be swept all-hex by `mesh.hex.generate`
|
||||
/// as a stack of extrusions along z: no pocket floor fillet or boss chamfers, and the root arcs
|
||||
/// meet the bosses about 20° off tangent (tangent to a circle 0.5 mm inside each boss), since a
|
||||
/// tangent meeting leaves a zero-angle cusp on the arm's top face.
|
||||
pub const CRANK_ARM: &str = include_str!("../samples/crank-arm.json");
|
||||
|
||||
/// The crank arm's pedal load (N): a standing rider's weight on one pedal.
|
||||
pub const PEDAL_LOAD: f64 = 1500.0;
|
||||
|
||||
/// [`CRANK_ARM`] swept all-hex (hex20 of 3 mm), in 6061-T6 aluminium: the square bore's faces
|
||||
/// and corner fillets (set `spindle`) fixed, [`PEDAL_LOAD`] on the pedal hole (set `pedal`), in
|
||||
/// the crank's plane and across the arm (−y). Step `Pedal` (linear static). The faces are picked
|
||||
/// by their surfaces, not their names: the square bore's are the vertical planes 6.35 mm from the
|
||||
/// spindle axis and the R1.5 cylinders beside it, the pedal hole is the Ø14 cylinder on the
|
||||
/// pedal axis.
|
||||
pub fn crank_arm(bodies: &[Shape]) -> Value {
|
||||
let [spindle, pedal] = crank_faces(bodies);
|
||||
json!([
|
||||
{"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}},
|
||||
{"command": "mesh.hex.generate", "params": {"size": 3, "order": 2, "method": "extrude"}},
|
||||
{"command": "model.set.create", "params": {"name": "crank", "geometry": bodies.iter().map(|b| json!({"body": b.name})).collect::<Vec<_>>()}},
|
||||
{"command": "model.set.create", "params": {"name": "spindle", "geometry": spindle}},
|
||||
{"command": "model.set.create", "params": {"name": "pedal", "geometry": pedal}},
|
||||
{"command": "model.material.create", "params": {"name": "Al6061-T6", "elastic": {"type": "isotropic", "young": 68900, "poisson": 0.33}, "density": 2.7e-9}},
|
||||
{"command": "model.section.create", "params": {"name": "Crank", "set": "crank", "material": "Al6061-T6", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "Spindle", "type": "fixed", "target": {"set": "spindle"}}},
|
||||
{"command": "model.load.create", "params": {"name": "Rider", "type": "force", "target": {"set": "pedal"}, "vector": [0, -PEDAL_LOAD, 0]}},
|
||||
{"command": "model.step.create", "params": {"name": "Pedal", "procedure": {"type": "static"}, "boundary_conditions": ["Spindle"], "loads": ["Rider"]}},
|
||||
])
|
||||
}
|
||||
|
||||
/// The crank's square-bore faces and pedal-hole faces as set members, found by their surfaces.
|
||||
fn crank_faces(bodies: &[Shape]) -> [Vec<Value>; 2] {
|
||||
use fieldcraft_geometry::FaceSurface;
|
||||
let vertical = |axis: [f64; 3]| axis[2].abs() > 1.0 - 1e-6;
|
||||
let (mut spindle, mut pedal) = (Vec::new(), Vec::new());
|
||||
for body in bodies {
|
||||
let surfaces = body.face_surfaces().unwrap_or_default();
|
||||
for (face, surface) in body.faces().iter().zip(surfaces) {
|
||||
let member = json!({"body": body.name, "face": face.name});
|
||||
match surface {
|
||||
Some(FaceSurface::Plane { normal, point })
|
||||
if normal[2].abs() < 1e-6 && ((normal[0] * point[0] + normal[1] * point[1]).abs() - 6.35).abs() < 1e-6 =>
|
||||
{
|
||||
spindle.push(member)
|
||||
}
|
||||
Some(FaceSurface::Cylinder { origin, axis, radius })
|
||||
if vertical(axis) && (radius - 1.5).abs() < 1e-6 && origin[0].hypot(origin[1]) < 8.0 =>
|
||||
{
|
||||
spindle.push(member)
|
||||
}
|
||||
Some(FaceSurface::Cylinder { origin, axis, radius })
|
||||
if vertical(axis) && (radius - 7.0).abs() < 1e-6 && (origin[0] - 170.0).hypot(origin[1]) < 1e-6 =>
|
||||
{
|
||||
pedal.push(member)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
[spindle, pedal]
|
||||
}
|
||||
|
||||
/// Six times the signed volume of the tetrahedron `p[0..4]` (positive when p3 lies on the side
|
||||
/// p0 → p1 → p2 turns counter-clockwise around).
|
||||
fn signed_volume(p: &[[f64; 3]]) -> f64 {
|
||||
@@ -225,6 +548,17 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::Session;
|
||||
|
||||
/// The `file.sample` form offers every sample, by name.
|
||||
#[test]
|
||||
fn the_form_lists_every_sample() {
|
||||
let Some(crate::schema::Kind::Choice(options)) = crate::cmd::forms::SAMPLE.first().map(|f| f.kind) else { panic!("not a choice") };
|
||||
let mut offered: Vec<&str> = options.iter().map(|(n, _)| *n).collect();
|
||||
let mut names: Vec<&str> = SAMPLES.iter().map(|s| s.name).collect();
|
||||
offered.sort_unstable();
|
||||
names.sort_unstable();
|
||||
assert_eq!(offered, names);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_sample_builds_positive_elements_that_fill_both_parts() {
|
||||
let mut s = Session::default();
|
||||
@@ -266,4 +600,239 @@ mod tests {
|
||||
assert_eq!((r["completed"].as_bool(), r["frames"].as_u64()), (Some(true), Some(11)), "{r}");
|
||||
assert!(r["energy_balance"].as_f64().unwrap() < 0.05, "{r}");
|
||||
}
|
||||
|
||||
/// `file.sample NAME`, checked clean: ready, no errors or warnings.
|
||||
fn open(name: &str) -> Session {
|
||||
let mut s = Session::default();
|
||||
s.execute("file.sample", &json!({"name": name})).unwrap();
|
||||
let check = s.execute("model.check", &json!({})).unwrap();
|
||||
assert_eq!((check["ready"].as_bool(), check["errors"].as_u64(), check["warnings"].as_u64()), (Some(true), Some(0), Some(0)), "{check}");
|
||||
s
|
||||
}
|
||||
|
||||
/// The squeeze force of the ring, against the closed form for an incompressible Mooney–Rivlin
|
||||
/// solid in uniaxial compression, P = 2 (λ − λ⁻²)(C10 + C01/λ) per unit of original area: the
|
||||
/// same ring between frictionless plates (each face held only along z, the rigid motions
|
||||
/// held at three nodes on the symmetry planes) compresses evenly and matches it within 3 %
|
||||
/// (ν = 0.49 is nearly incompressible); bonded, the bulge needs more force (shape factor
|
||||
/// ¼ (b − a)/h = 0.5 by the loaded over the free area), within 2.5 times.
|
||||
#[test]
|
||||
fn the_rubber_seal_squeezes_as_mooney_rivlin_rubber() {
|
||||
let [inner, outer, height] = SEAL;
|
||||
let area = std::f64::consts::PI * (outer * outer - inner * inner);
|
||||
let lambda: f64 = 1.0 - SEAL_SQUEEZE / height;
|
||||
let (c01, c10) = (0.2, 6.0 / (2.0 * 1.49) / 2.0 - 0.2);
|
||||
let closed = 2.0 * (lambda - lambda.powi(-2)) * (c10 + c01 / lambda) * area;
|
||||
let squeeze = |s: &mut Session| -> f64 {
|
||||
s.execute("solve.nonlinear", &json!({})).unwrap();
|
||||
let r = s.execute("results.reactions", &json!({"step": "Squeeze", "sets": ["top"]})).unwrap();
|
||||
r["sets"][0]["force"][2].as_f64().unwrap()
|
||||
};
|
||||
let mut s = open("rubber-seal");
|
||||
let bonded = squeeze(&mut s);
|
||||
let node = |s: &Session, xyz: [f64; 3]| -> u32 {
|
||||
*s.model.nodes().iter().find(|(_, n)| (0..3).all(|k| (n.xyz[k] - xyz[k]).abs() < 1e-9)).unwrap().0
|
||||
};
|
||||
let held = [
|
||||
("hold_y", [inner, 0.0, 0.0], [None, Some(0.0), None]),
|
||||
("hold_x", [0.0, inner, 0.0], [Some(0.0), None, None]),
|
||||
("hold_y2", [-inner, 0.0, 0.0], [None, Some(0.0), None]),
|
||||
];
|
||||
for (name, at, [ux, uy, _]) in held {
|
||||
let n = node(&s, at);
|
||||
s.execute("model.set.create", &json!({"name": name, "nodes": [n]})).unwrap();
|
||||
s.execute(
|
||||
"model.boundary_condition.create",
|
||||
&json!({"name": name, "type": "displacement", "target": {"set": name}, "values": [ux, uy, null, null, null, null]}),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
s.execute(
|
||||
"model.boundary_condition.edit",
|
||||
&json!({"name": "Base", "type": "displacement", "target": {"set": "bottom"}, "values": [null, null, 0, null, null, null]}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute(
|
||||
"model.boundary_condition.edit",
|
||||
&json!({"name": "Press", "type": "displacement", "target": {"set": "top"}, "values": [null, null, -SEAL_SQUEEZE, null, null, null]}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute("model.step.edit", &json!({"name": "Squeeze", "boundary_conditions": ["Base", "Press", "hold_y", "hold_x", "hold_y2"]})).unwrap();
|
||||
let free = squeeze(&mut s);
|
||||
assert!((free - closed).abs() < 0.03 * closed.abs(), "frictionless {free} N against {closed} N");
|
||||
let stiffer = bonded / free;
|
||||
assert!(stiffer > 1.05 && stiffer < 2.5, "bonded {bonded} N, frictionless {free} N");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_flow_sample_opens_ready_to_solve() {
|
||||
let s = open("flow-past");
|
||||
assert_eq!(s.model.fluid_domains().len(), 1);
|
||||
}
|
||||
|
||||
/// The drag on the bracket, as a coefficient on the upright's frontal area (40 × 50 mm), is
|
||||
/// that of a bluff plate standing on a wall at a Reynolds number of 83, raised by the 17 %
|
||||
/// blockage of the box: between 1 and 3.5 (a flat plate broadside to the flow is about 1.2 at
|
||||
/// high Reynolds number and rises as it falls). The lattice conserves mass.
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn the_flow_past_the_bracket_has_a_bluff_body_drag() {
|
||||
let mut s = open("flow-past");
|
||||
let r = s.execute("solve.run", &json!({})).unwrap();
|
||||
let r = &r["steps"][0]["summary"];
|
||||
let drag: f64 = r["forces"].as_array().unwrap().iter().filter(|f| f["body"] == "Bracket").map(|f| f["mean"][0].as_f64().unwrap()).sum();
|
||||
let [density, _] = AIR;
|
||||
let cd = drag / (0.5 * density * FLOW_SPEED * FLOW_SPEED * 40.0 * 50.0);
|
||||
assert!(cd > 1.0 && cd < 3.5, "Cd {cd}: {r}");
|
||||
assert!(r["mass"]["relative_change"].as_f64().unwrap().abs() < 0.01, "{r}");
|
||||
}
|
||||
|
||||
/// The heated bracket's temperatures lie between the air's and the heater's, falling along
|
||||
/// the bracket away from the heater. Its thermal expansion: with the air at the heater's
|
||||
/// temperature the bracket warms evenly by ΔT, and held at the foot's hole alone (at x = 40)
|
||||
/// it grows freely, so a point moves α ΔT times its distance from the hole's axis along x:
|
||||
/// the upright's top by −0.0432 mm (within 2 %), the foot's end by +0.024 mm (within 6 %:
|
||||
/// the clamped bore cannot grow).
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn the_heated_bracket_warms_and_expands_as_steel() {
|
||||
let mut s = open("heated-bracket");
|
||||
let r = s.execute("solve.run", &json!({})).unwrap();
|
||||
let r = &r["steps"][0]["summary"];
|
||||
assert!((r["max_temperature"]["value"].as_f64().unwrap() - HEATER).abs() < 1e-9, "{r}");
|
||||
let probe = |s: &mut Session| s.execute("solve.probe", &json!({"step": "Heat", "at": [[4.0, 50.0, 20.0], [60.0, 4.0, 20.0]]})).unwrap();
|
||||
let p = probe(&mut s);
|
||||
let (upright, foot) = (p["nodes"][0]["temperature"].as_f64().unwrap(), p["nodes"][1]["temperature"].as_f64().unwrap());
|
||||
assert!(upright > foot && foot > AMBIENT && upright <= HEATER, "{upright} {foot}");
|
||||
let foot_hole = json!([{"body": "Bracket", "face": "F4:bore:side:0"}, {"body": "Bracket", "face": "F4:bore:side:1"}]);
|
||||
s.execute("model.set.create", &json!({"name": "foot_hole", "geometry": foot_hole})).unwrap();
|
||||
s.execute("model.boundary_condition.edit", &json!({"name": "Clamp", "type": "fixed", "target": {"set": "foot_hole"}})).unwrap();
|
||||
s.execute("model.load.edit", &json!({"name": "Air", "type": "convection", "set": "skin", "film": 0.01, "sink": HEATER})).unwrap();
|
||||
s.execute("solve.static", &json!({})).unwrap();
|
||||
let p = probe(&mut s);
|
||||
let grow = STEEL_EXPANSION * (HEATER - AMBIENT);
|
||||
// The bore itself cannot grow, which holds back the foot's end, 16 mm from its edge, a
|
||||
// little more than the upright's top, 32 mm away.
|
||||
for (i, tolerance) in [(0, 0.02), (1, 0.06)] {
|
||||
let node = &p["nodes"][i];
|
||||
let (ux, expected) = (node["displacement"][0].as_f64().unwrap(), grow * (node["xyz"][0].as_f64().unwrap() - 40.0));
|
||||
assert!((ux - expected).abs() < tolerance * expected.abs(), "{node}: {ux} against {expected}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The hex20 bracket's tip deflection agrees with a fine tet10 solution of the unpartitioned
|
||||
/// part (0.5093 mm, crates/testkit/tests/bracket_blocks.rs) within 1 %, its mesh conforms
|
||||
/// across all 30 cells, and the load reaches the clamp.
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn the_hex_bracket_bends_like_the_fine_tet_solution() {
|
||||
let mut s = open("bracket-hex");
|
||||
let elements = s.model.elements().values().filter(|e| e.kind == fieldcraft_model::ElementKind::Hex20).count();
|
||||
assert_eq!(elements, s.model.elements().len());
|
||||
let r = s.execute("solve.run", &json!({})).unwrap();
|
||||
let r = &r["steps"][0]["summary"];
|
||||
let probe = s.execute("solve.probe", &json!({"step": "Load", "at": [[60.0, 4.0, 20.0]]})).unwrap();
|
||||
let tip = probe["nodes"][0]["displacement"][1].as_f64().unwrap();
|
||||
assert!((tip + 0.5093).abs() < 0.01 * 0.5093, "{tip}");
|
||||
let reaction = s.execute("results.reactions", &json!({"step": "Load", "sets": ["top"]})).unwrap();
|
||||
assert!((reaction["sets"][0]["force"][1].as_f64().unwrap() - 1000.0).abs() < 1e-6, "{reaction}");
|
||||
assert!(r["max_von_mises"]["value"].as_f64().unwrap() > 200.0, "{r}");
|
||||
}
|
||||
|
||||
/// The crank arm opens swept all-hex20 with every bore face found, and its root bends as a
|
||||
/// beam: σxx on the arm's sides at x = 29 mm (35.6 mm wide, 16 thick, 141 mm from the pedal)
|
||||
/// within 3 % of M/Z = 1500 · 141 / (16 · 35.6² / 6) = 62.6 MPa, the spindle carrying the
|
||||
/// whole load.
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn the_crank_arm_root_bends_as_the_beam_formula_says() {
|
||||
let mut s = open("crank-arm");
|
||||
let elements = s.model.elements().values().filter(|e| e.kind == fieldcraft_model::ElementKind::Hex20).count();
|
||||
assert_eq!(elements, s.model.elements().len());
|
||||
let faces = |set: &str| s.model.sets().get(set).map_or(0, |x| x.geometry.len());
|
||||
assert_eq!((faces("spindle"), faces("pedal")), (8, 2));
|
||||
s.execute("solve.run", &json!({})).unwrap();
|
||||
let half_width: f64 = 19.0 - 7.0 * 29.0 / 170.0;
|
||||
let probe = s.execute("solve.probe", &json!({"step": "Pedal", "at": [[29.0, -half_width, 8.0], [29.0, half_width, 8.0]]})).unwrap();
|
||||
let sxx = |i: usize| probe["nodes"][i]["stress"][0].as_f64().unwrap();
|
||||
let hand = PEDAL_LOAD * (170.0 - 29.0) / (16.0 * (2.0 * half_width).powi(2) / 6.0);
|
||||
for (i, sign) in [(0, -1.0), (1, 1.0)] {
|
||||
assert!((sign * sxx(i) - hand).abs() < 0.03 * hand, "{} against {hand} MPa: {probe}", sxx(i));
|
||||
}
|
||||
let reaction = s.execute("results.reactions", &json!({"step": "Pedal", "sets": ["spindle"]})).unwrap();
|
||||
assert!((reaction["sets"][0]["force"][1].as_f64().unwrap() - PEDAL_LOAD).abs() < 1e-6, "{reaction}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_drop_test_opens_ready_to_solve() {
|
||||
let s = open("drop-test");
|
||||
assert!(s.model.elements().len() > 1000);
|
||||
}
|
||||
|
||||
/// The bracket's mass is ρ V (V: foot, upright and the fillet's corner, 36 − 9π mm² by 40 mm),
|
||||
/// it meets the floor at √(2 g h) with ½ m v² of kinetic energy and conserves energy; by the
|
||||
/// end the floor has taken half its downward momentum and turned kinetic energy into
|
||||
/// vibration.
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn the_drop_test_lands_at_the_free_fall_speed_and_bounces() {
|
||||
let mut s = open("drop-test");
|
||||
let r = s.execute("solve.run", &json!({})).unwrap();
|
||||
let r = &r["steps"][0]["summary"];
|
||||
let volume = 60.0 * 8.0 * 40.0 + 8.0 * 42.0 * 40.0 + (36.0 - 9.0 * std::f64::consts::PI) * 40.0;
|
||||
let mass = 2.7e-9 * volume;
|
||||
let close = |a: f64, b: f64, rel: f64| (a - b).abs() <= rel * b.abs();
|
||||
assert!(close(r["total_mass"].as_f64().unwrap(), mass, 0.005), "{r}");
|
||||
assert!(r["completed"].as_bool() == Some(true) && r["energy_balance"].as_f64().unwrap() < 0.01, "{r}");
|
||||
let h = s.execute("solve.history", &json!({"step": "Drop"})).unwrap();
|
||||
let speed = (2.0 * GRAVITY * DROP_HEIGHT).sqrt();
|
||||
let pz = |i: usize| h["momentum"][i][2].as_f64().unwrap();
|
||||
let kinetic = |i: usize| h["energies"][i]["kinetic"].as_f64().unwrap();
|
||||
assert!(close(pz(0), -mass * speed, 0.005), "{}", pz(0));
|
||||
assert!(close(kinetic(0), 0.5 * mass * speed * speed, 0.01), "{}", kinetic(0));
|
||||
let last = h["time"].as_array().unwrap().len() - 1;
|
||||
let kept = pz(last) / pz(0);
|
||||
assert!(kept > 0.4 && kept < 0.6 && kinetic(last) < kinetic(0), "{} {}", pz(last), kinetic(last));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "scratch"]
|
||||
fn boot_axisymmetric_scratch() {
|
||||
let travel: f64 = std::env::var("TRAVEL").ok().and_then(|v| v.parse().ok()).unwrap_or(30.0);
|
||||
let mut s = Session::default();
|
||||
let script = match std::env::var("SECTORS").ok().and_then(|v| v.parse::<usize>().ok()) {
|
||||
Some(n) => super::boot::solid(travel, n, std::env::var("FULL").is_err()),
|
||||
None => super::boot::axisymmetric(travel),
|
||||
};
|
||||
let t0 = std::time::Instant::now();
|
||||
for c in script.as_array().unwrap() {
|
||||
s.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
|
||||
}
|
||||
eprintln!("built {} elements in {:?}, height {}", s.model.elements().len(), t0.elapsed(), super::boot::height());
|
||||
if let Ok(path) = std::env::var("DUMP_MODEL") {
|
||||
std::fs::write(path, serde_json::to_string(&s.model).unwrap()).unwrap();
|
||||
}
|
||||
if std::env::var("NO_CONTACT").is_ok() {
|
||||
s.execute("interaction.contact.delete", &json!({"name": "Folds"})).unwrap();
|
||||
}
|
||||
let check = s.execute("model.check", &json!({})).unwrap();
|
||||
eprintln!("{check}");
|
||||
let q = s.execute("mesh.quality", &json!({})).unwrap();
|
||||
eprintln!("quality {}", q.to_string().chars().take(300).collect::<String>());
|
||||
let t0 = std::time::Instant::now();
|
||||
let r = s.execute("solve.nonlinear", &json!({}));
|
||||
eprintln!("solve {:?}: {}", t0.elapsed(), match &r { Ok(v) => v.to_string().chars().take(3000).collect::<String>(), Err(e) => e.to_string() });
|
||||
if r.is_ok() {
|
||||
let labels: Vec<u32> = s.model.nodes().keys().copied().collect();
|
||||
let mut out = Vec::new();
|
||||
for chunk in labels.chunks(5000) {
|
||||
let p = s.execute("solve.probe", &json!({"step": "Squeeze", "nodes": chunk})).unwrap();
|
||||
for (l, n) in chunk.iter().zip(p["nodes"].as_array().unwrap()) {
|
||||
out.push(json!({"label": l, "xyz": s.model.nodes()[l].xyz, "u": n["displacement"], "p": n["contact"][0]["pressure"]}));
|
||||
}
|
||||
}
|
||||
let elements: Vec<Value> = s.model.elements().iter().map(|(l, e)| json!({"label": l, "nodes": e.nodes})).collect();
|
||||
std::fs::write("/private/tmp/claude-501/fc/boot_state.json", json!({"nodes": out, "elements": elements}).to_string()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
//! The boot seal: a conical rubber bellows of our own design, its six rounded convolutions
|
||||
//! squeezed axially by a rigid collar until they fold onto each other.
|
||||
//!
|
||||
//! **Profile.** The wall's mid-surface in the (r, y) half-plane, y the axis: a base cylinder
|
||||
//! (r = [`BASE_RADIUS`]), then six V-shaped convolutions zig-zagging out to a crest and back in to
|
||||
//! a root, the crests and roots tapering linearly towards the top, and the top collar, a cylinder
|
||||
//! at the last root's radius. Every corner is rounded with radius [`BEND`]. The wall is the
|
||||
//! mid-surface offset by ± [`WALL`]/2 along its normal: a structured quad mesh, [`LAYERS`]
|
||||
//! elements through the wall and about [`STEP`] mm along it.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// Mid-surface radius of the base cylinder (mm).
|
||||
pub const BASE_RADIUS: f64 = 27.0;
|
||||
/// Wall thickness (mm).
|
||||
pub const WALL: f64 = 1.8;
|
||||
/// Radius of the rounded corners of the mid-surface (mm).
|
||||
pub const BEND: f64 = 1.6;
|
||||
/// Convolutions, and their pitch along the axis (mm).
|
||||
pub const FOLDS: usize = 6;
|
||||
const PITCH: f64 = 9.5;
|
||||
/// Height of the straight base, and the boot's height (mm).
|
||||
const BASE_LENGTH: f64 = 4.0;
|
||||
pub const HEIGHT: f64 = 70.0;
|
||||
/// Crest and root radii of the first and last convolutions (mm).
|
||||
const CREST: [f64; 2] = [38.0, 26.0];
|
||||
const ROOT: [f64; 2] = [28.0, 17.0];
|
||||
/// Elements through the wall and the target element length along it (mm).
|
||||
pub const LAYERS: usize = 3;
|
||||
pub const STEP: f64 = 0.6;
|
||||
|
||||
/// A piece of the mid-surface: a straight line, or an arc about `center` of `radius` from angle
|
||||
/// `start` through `sweep` (radians, counter-clockwise positive).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Piece {
|
||||
Line { from: [f64; 2], to: [f64; 2] },
|
||||
Arc { center: [f64; 2], radius: f64, start: f64, sweep: f64 },
|
||||
}
|
||||
|
||||
impl Piece {
|
||||
fn length(&self) -> f64 {
|
||||
match *self {
|
||||
Piece::Line { from, to } => (to[0] - from[0]).hypot(to[1] - from[1]),
|
||||
Piece::Arc { radius, sweep, .. } => radius * sweep.abs(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The point and unit tangent at fraction `s` of the piece.
|
||||
fn at(&self, s: f64) -> ([f64; 2], [f64; 2]) {
|
||||
match *self {
|
||||
Piece::Line { from, to } => {
|
||||
let l = self.length();
|
||||
([from[0] + s * (to[0] - from[0]), from[1] + s * (to[1] - from[1])], [(to[0] - from[0]) / l, (to[1] - from[1]) / l])
|
||||
}
|
||||
Piece::Arc { center, radius, start, sweep } => {
|
||||
let a = start + s * sweep;
|
||||
let (sin, cos) = a.sin_cos();
|
||||
let turn = sweep.signum();
|
||||
([center[0] + radius * cos, center[1] + radius * sin], [-turn * sin, turn * cos])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// The crest or root radius of convolution `k` (linear from the first to the last).
|
||||
fn taper(ends: [f64; 2], k: usize) -> f64 {
|
||||
let f = k as f64 / (FOLDS - 1) as f64;
|
||||
ends[0] + f * (ends[1] - ends[0])
|
||||
}
|
||||
|
||||
/// The corners of the mid-surface's polyline, base to collar top.
|
||||
fn corners() -> Vec<[f64; 2]> {
|
||||
let mut out = vec![[BASE_RADIUS, 0.0], [BASE_RADIUS, BASE_LENGTH]];
|
||||
for k in 0..FOLDS {
|
||||
let y = BASE_LENGTH + k as f64 * PITCH;
|
||||
out.push([taper(CREST, k), y + 0.5 * PITCH]);
|
||||
out.push([taper(ROOT, k), y + PITCH]);
|
||||
}
|
||||
let collar = out.last().map_or(0.0, |c| c[0]);
|
||||
out.push([collar, HEIGHT]);
|
||||
out
|
||||
}
|
||||
|
||||
/// The mid-surface, base to collar: the polyline of [`corners`] with each inner corner rounded
|
||||
/// by an arc of radius [`BEND`] tangent to both sides (`None` when a side is too short for its
|
||||
/// arcs).
|
||||
fn mid_surface() -> Option<Vec<Piece>> {
|
||||
let c = corners();
|
||||
let unit = |a: [f64; 2], b: [f64; 2]| {
|
||||
let l = (b[0] - a[0]).hypot(b[1] - a[1]);
|
||||
[(b[0] - a[0]) / l, (b[1] - a[1]) / l]
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
let mut from = *c.first()?;
|
||||
for w in c.windows(3) {
|
||||
let (a, v, b) = (w[0], w[1], w[2]);
|
||||
let (d1, d2) = (unit(a, v), unit(v, b));
|
||||
let cross = d1[0] * d2[1] - d1[1] * d2[0];
|
||||
let turn = cross.atan2(d1[0] * d2[0] + d1[1] * d2[1]);
|
||||
let tangent = BEND * (0.5 * turn.abs()).tan();
|
||||
let enter = [v[0] - tangent * d1[0], v[1] - tangent * d1[1]];
|
||||
// The side must hold both of its arcs' tangent lengths.
|
||||
if tangent > (v[0] - from[0]).hypot(v[1] - from[1]) {
|
||||
return None;
|
||||
}
|
||||
out.push(Piece::Line { from, to: enter });
|
||||
// The centre on the inside of the turn: left of d1 for a left (counter-clockwise) turn.
|
||||
let side = turn.signum();
|
||||
let center = [enter[0] - side * BEND * d1[1], enter[1] + side * BEND * d1[0]];
|
||||
let start = (enter[1] - center[1]).atan2(enter[0] - center[0]);
|
||||
out.push(Piece::Arc { center, radius: BEND, start, sweep: turn });
|
||||
from = [v[0] + tangent * d2[0], v[1] + tangent * d2[1]];
|
||||
}
|
||||
out.push(Piece::Line { from, to: *c.last()? });
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// The mesh of the wall in the (r, y) half-plane: nodes row by row along the mid-surface
|
||||
/// (`LAYERS + 1` per row, from the outer face in), and the number of rows.
|
||||
pub fn wall_nodes() -> (Vec<[f64; 2]>, usize) {
|
||||
let mut rows: Vec<([f64; 2], [f64; 2])> = Vec::new();
|
||||
for piece in mid_surface().unwrap_or_default() {
|
||||
let n = (piece.length() / STEP).round().max(1.0) as usize;
|
||||
let first = if rows.is_empty() { 0 } else { 1 };
|
||||
for i in first..=n {
|
||||
rows.push(piece.at(i as f64 / n as f64));
|
||||
}
|
||||
}
|
||||
let mut nodes = Vec::with_capacity(rows.len() * (LAYERS + 1));
|
||||
for (p, t) in &rows {
|
||||
// The left normal of the tangent: through the wall from the outer face in, so the quads
|
||||
// run counter-clockwise.
|
||||
let n = [-t[1], t[0]];
|
||||
for j in 0..=LAYERS {
|
||||
let d = WALL * (j as f64 / LAYERS as f64 - 0.5);
|
||||
nodes.push([p[0] + d * n[0], p[1] + d * n[1]]);
|
||||
}
|
||||
}
|
||||
(nodes, rows.len())
|
||||
}
|
||||
|
||||
/// The top of the collar (mm): the boot's height.
|
||||
pub fn height() -> f64 {
|
||||
HEIGHT
|
||||
}
|
||||
|
||||
/// The wall's quad4 mesh in the (r, y) = (x, y) plane, tagged as face `wall` of body `Boot`
|
||||
/// with its edges `base`, `collar` (the ends), `outer` and `inner` (the faces of the wall).
|
||||
fn profile(steps: &mut Vec<Value>) {
|
||||
let (nodes, rows) = wall_nodes();
|
||||
let per = LAYERS + 1;
|
||||
let label = |i: usize, j: usize| (i * per + j + 1) as u64;
|
||||
for (k, p) in nodes.iter().enumerate() {
|
||||
steps.push(json!({"command": "mesh.node.add", "params": {"label": k + 1, "xyz": [p[0], p[1], 0.0]}}));
|
||||
}
|
||||
let mut element = 0u64;
|
||||
let mut edges: [Vec<Value>; 4] = Default::default();
|
||||
for i in 0..rows.saturating_sub(1) {
|
||||
for j in 0..LAYERS {
|
||||
element += 1;
|
||||
let quad = [label(i, j), label(i + 1, j), label(i + 1, j + 1), label(i, j + 1)];
|
||||
steps.push(json!({"command": "mesh.element.add", "params": {"label": element, "kind": "quad4", "nodes": quad}}));
|
||||
// Edge 1 runs along the outer face (j = 0), 3 along the inner one, 4 and 2 across
|
||||
// the wall at the base and the collar.
|
||||
let mut on = |k: usize, edge: u8| edges[k].push(json!({"element": element, "edge": edge}));
|
||||
if i == 0 {
|
||||
on(0, 4);
|
||||
}
|
||||
if i + 2 == rows {
|
||||
on(1, 2);
|
||||
}
|
||||
if j == 0 {
|
||||
on(2, 1);
|
||||
}
|
||||
if j + 1 == LAYERS {
|
||||
on(3, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
steps.push(json!({"command": "mesh.tag", "params": {"geometry": {"body": "Boot", "face": "wall"}, "elements": (1..=element).collect::<Vec<_>>()}}));
|
||||
for (name, list) in ["base", "collar", "outer", "inner"].iter().zip(edges) {
|
||||
steps.push(json!({"command": "mesh.tag", "params": {"geometry": {"body": "Boot", "edge": name}, "edges": list}}));
|
||||
}
|
||||
}
|
||||
|
||||
/// The rubber, its section, general contact on every exterior face or edge, the clamped base
|
||||
/// and the collar pushed down by `travel` along the axis (`axis`: 1 for Y in 2D, 2 for Z in 3D)
|
||||
/// and held across it (a rigid collar).
|
||||
fn analysis(steps: &mut Vec<Value>, section: Value, entity: &str, axis: usize, travel: f64, extra: &[&str]) {
|
||||
// The elements: the profile's face, or the solid body.
|
||||
let region = if axis == 1 { json!({"body": "Boot", "face": "wall"}) } else { json!({"body": "Boot"}) };
|
||||
let mut push = [json!(0), json!(0), json!(0), Value::Null, Value::Null, Value::Null];
|
||||
if axis == 1 {
|
||||
push[2] = Value::Null;
|
||||
}
|
||||
push[axis] = json!(-travel);
|
||||
let mut held = vec!["Base", "Push"];
|
||||
held.extend_from_slice(extra);
|
||||
steps.extend([
|
||||
json!({"command": "model.set.create", "params": {"name": "boot", "geometry": [region]}}),
|
||||
json!({"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Boot", entity: "base"}]}}),
|
||||
json!({"command": "model.set.create", "params": {"name": "collar", "geometry": [{"body": "Boot", entity: "collar"}]}}),
|
||||
json!({"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": RUBBER[0], "poisson": RUBBER[1]}, "density": 1.1e-9,
|
||||
"hyperelastic": {"type": "mooney_rivlin", "c01": RUBBER[2]}}}),
|
||||
json!({"command": "model.section.create", "params": {"name": "Boot", "set": "boot", "material": "Rubber", "kind": section, "formulation": {"hybrid": true}}}),
|
||||
json!({"command": "interaction.contact.general", "params": {"name": "Folds"}}),
|
||||
json!({"command": "model.boundary_condition.create", "params": {"name": "Base", "type": "fixed", "target": {"set": "base"}}}),
|
||||
json!({"command": "model.boundary_condition.create", "params": {"name": "Push", "type": "displacement", "target": {"set": "collar"}, "values": push}}),
|
||||
json!({"command": "model.step.create", "params": {"name": "Squeeze", "procedure": {"type": "nonlinear_static", "initial_increment": 0.02, "max_increment": 0.05, "max_increments": 1000, "line_search": true, "stabilization": STABILIZATION}, "boundary_conditions": held}}),
|
||||
]);
|
||||
}
|
||||
|
||||
/// The rubber: Young's modulus (MPa), Poisson's ratio and Mooney–Rivlin C01 (MPa), so
|
||||
/// μ = E / (2 (1 + ν)) = 1.0 MPa, C10 = μ/2 − C01 = 0.4 MPa: a soft generic rubber, nearly
|
||||
/// incompressible (κ/μ ≈ 830).
|
||||
pub const RUBBER: [f64; 3] = [3.0, 0.499, 0.1];
|
||||
|
||||
/// The squeeze's stabilization (the fraction of its work the damping may dissipate).
|
||||
pub const STABILIZATION: f64 = 2e-3;
|
||||
|
||||
/// The axisymmetric boot (hybrid quad4) squeezed by `travel` mm.
|
||||
pub fn axisymmetric(travel: f64) -> Value {
|
||||
let mut steps = vec![json!({"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}})];
|
||||
profile(&mut steps);
|
||||
analysis(&mut steps, json!({"type": "axisymmetric"}), "edge", 1, travel, &[]);
|
||||
Value::Array(steps)
|
||||
}
|
||||
|
||||
/// The boot in 3D: the axisymmetric wall mesh revolved about the axis (`mesh.revolve`, Z up)
|
||||
/// into hybrid hex8, `sectors` around the circumference, or around the half x ≥ 0 (from −90° to
|
||||
/// 90°) with the cut held on the symmetry plane x = 0.
|
||||
pub fn solid(travel: f64, sectors: usize, half: bool) -> Value {
|
||||
let mut steps = vec![json!({"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}})];
|
||||
profile(&mut steps);
|
||||
let (angle, start) = if half { (180, -90) } else { (360, 0) };
|
||||
steps.push(json!({"command": "mesh.revolve", "params": {"angle": angle, "sectors": sectors, "start": start}}));
|
||||
let mut extra = Vec::new();
|
||||
if half {
|
||||
steps.extend([
|
||||
json!({"command": "model.set.create", "params": {"name": "cut", "geometry": [{"body": "Boot", "face": "start"}, {"body": "Boot", "face": "end"}]}}),
|
||||
json!({"command": "model.boundary_condition.create", "params": {"name": "Symmetry", "type": "symmetry", "target": {"set": "cut"}, "plane": "x"}}),
|
||||
]);
|
||||
extra.push("Symmetry");
|
||||
}
|
||||
analysis(&mut steps, json!({"type": "solid"}), "face", 2, travel, &extra);
|
||||
Value::Array(steps)
|
||||
}
|
||||
@@ -107,6 +107,7 @@ pub enum Collection {
|
||||
ReferencePoint,
|
||||
Coupling,
|
||||
Contact,
|
||||
Tie,
|
||||
Mass,
|
||||
Spring,
|
||||
BoundaryCondition,
|
||||
@@ -128,6 +129,7 @@ impl Collection {
|
||||
Collection::ReferencePoint => "reference point",
|
||||
Collection::Coupling => "coupling",
|
||||
Collection::Contact => "contact",
|
||||
Collection::Tie => "tie",
|
||||
Collection::Mass => "point mass",
|
||||
Collection::Spring => "spring",
|
||||
Collection::BoundaryCondition => "boundary condition",
|
||||
@@ -151,6 +153,7 @@ impl Session {
|
||||
Collection::ReferencePoint => Box::new(m.reference_points().keys().map(String::as_str)),
|
||||
Collection::Coupling => Box::new(m.couplings().keys().map(String::as_str)),
|
||||
Collection::Contact => Box::new(m.contacts().keys().map(String::as_str)),
|
||||
Collection::Tie => Box::new(m.ties().keys().map(String::as_str)),
|
||||
Collection::Mass => Box::new(m.masses().keys().map(String::as_str)),
|
||||
Collection::Spring => Box::new(m.springs().keys().map(String::as_str)),
|
||||
Collection::BoundaryCondition => Box::new(m.boundary_conditions().keys().map(String::as_str)),
|
||||
|
||||
@@ -215,6 +215,9 @@ impl System {
|
||||
if let Some(name) = model.couplings().keys().next() {
|
||||
return Err(Error::NotSupported(format!("couplings (coupling `{name}`) in explicit dynamics")));
|
||||
}
|
||||
if let Some(name) = model.ties().keys().next() {
|
||||
return Err(Error::NotSupported(format!("ties (tie `{name}`) in explicit dynamics")));
|
||||
}
|
||||
|
||||
// Elements by label, with their section's material.
|
||||
let mut materials: Vec<Material> = Vec::new();
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
//! Forward-mode automatic differentiation in [`VARS`] variables, for kernels whose exact first
|
||||
//! and second derivatives are too long to write by hand (the mortar contact integrals of
|
||||
//! [`crate::mortar`], whose segmentation moves with the nodes).
|
||||
//!
|
||||
//! A kernel is written once over a [`Real`] scalar and run on `f64` for its value, on [`Dual`]
|
||||
//! for its gradient, or on [`Hyper`] for its gradient and Hessian. Arithmetic propagates the
|
||||
//! derivatives by the chain rule (truncated Taylor arithmetic: A. Griewank, A. Walther,
|
||||
//! *Evaluating Derivatives*, 2nd ed., SIAM 2008, ch. 3 and 13), so the derivatives are those of
|
||||
//! the code as it runs, to round-off: the branches a kernel takes (which polygon edges clip,
|
||||
//! which root of a quadratic) are decided on the values and differentiated where they are
|
||||
//! taken, which is the derivative of the piecewise smooth function wherever it is smooth. The
|
||||
//! automated linearisation of contact kernels this way is that of J. Lengiewicz, J. Korelc, S.
|
||||
//! Stupkiewicz, *Automation of finite element formulations for large deformation contact
|
||||
//! problems*, IJNME 85:1252–1279, 2011.
|
||||
//!
|
||||
//! [`jet::Jet`](crate::jet::Jet) is the three-variable sibling the shape-function library uses.
|
||||
|
||||
use std::ops::{Add, Div, Mul, Neg, Sub};
|
||||
|
||||
/// Independent variables of [`Dual`] and [`Hyper`]: the corners of two faces of up to four
|
||||
/// nodes, three coordinates each.
|
||||
pub const VARS: usize = 24;
|
||||
|
||||
/// Entries of a packed symmetric [`VARS`] × [`VARS`] Hessian (its upper triangle by rows).
|
||||
pub const PACKED: usize = VARS * (VARS + 1) / 2;
|
||||
|
||||
/// The position of (i, j) in a packed upper triangle (either order).
|
||||
pub fn packed(i: usize, j: usize) -> usize {
|
||||
let (a, b) = if i <= j { (i, j) } else { (j, i) };
|
||||
// Row a starts after rows 0..a, of VARS, VARS − 1, … entries.
|
||||
a * VARS - a * a.saturating_sub(1) / 2 + (b - a)
|
||||
}
|
||||
|
||||
/// A scalar a kernel can be written over: `f64`, [`Dual`] or [`Hyper`].
|
||||
pub trait Real:
|
||||
Copy
|
||||
+ Add<Output = Self>
|
||||
+ Sub<Output = Self>
|
||||
+ Mul<Output = Self>
|
||||
+ Div<Output = Self>
|
||||
+ Neg<Output = Self>
|
||||
+ Add<f64, Output = Self>
|
||||
+ Sub<f64, Output = Self>
|
||||
+ Mul<f64, Output = Self>
|
||||
{
|
||||
/// A constant.
|
||||
fn cst(v: f64) -> Self;
|
||||
/// The value.
|
||||
fn val(&self) -> f64;
|
||||
fn sqrt(self) -> Self;
|
||||
}
|
||||
|
||||
impl Real for f64 {
|
||||
fn cst(v: f64) -> f64 {
|
||||
v
|
||||
}
|
||||
fn val(&self) -> f64 {
|
||||
*self
|
||||
}
|
||||
fn sqrt(self) -> f64 {
|
||||
f64::sqrt(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A value and its gradient.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Dual {
|
||||
pub v: f64,
|
||||
pub g: [f64; VARS],
|
||||
}
|
||||
|
||||
impl Dual {
|
||||
/// Variable `k` at value `v` (a constant when `k` is out of range).
|
||||
pub fn var(v: f64, k: usize) -> Dual {
|
||||
let mut d = Dual::cst(v);
|
||||
if let Some(g) = d.g.get_mut(k) {
|
||||
*g = 1.0;
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
fn chain(self, v: f64, d: f64) -> Dual {
|
||||
Dual { v, g: self.g.map(|x| d * x) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Real for Dual {
|
||||
fn cst(v: f64) -> Dual {
|
||||
Dual { v, g: [0.0; VARS] }
|
||||
}
|
||||
fn val(&self) -> f64 {
|
||||
self.v
|
||||
}
|
||||
fn sqrt(self) -> Dual {
|
||||
let s = self.v.sqrt();
|
||||
self.chain(s, 0.5 / s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Dual {
|
||||
type Output = Dual;
|
||||
fn add(mut self, o: Dual) -> Dual {
|
||||
self.v += o.v;
|
||||
self.g.iter_mut().zip(o.g).for_each(|(a, b)| *a += b);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Dual {
|
||||
type Output = Dual;
|
||||
fn sub(mut self, o: Dual) -> Dual {
|
||||
self.v -= o.v;
|
||||
self.g.iter_mut().zip(o.g).for_each(|(a, b)| *a -= b);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul for Dual {
|
||||
type Output = Dual;
|
||||
fn mul(self, o: Dual) -> Dual {
|
||||
let mut g = [0.0; VARS];
|
||||
for k in 0..VARS {
|
||||
g[k] = self.v * o.g[k] + o.v * self.g[k];
|
||||
}
|
||||
Dual { v: self.v * o.v, g }
|
||||
}
|
||||
}
|
||||
|
||||
impl Div for Dual {
|
||||
type Output = Dual;
|
||||
fn div(self, o: Dual) -> Dual {
|
||||
let r = 1.0 / o.v;
|
||||
let v = self.v * r;
|
||||
let mut g = [0.0; VARS];
|
||||
for k in 0..VARS {
|
||||
g[k] = (self.g[k] - v * o.g[k]) * r;
|
||||
}
|
||||
Dual { v, g }
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Dual {
|
||||
type Output = Dual;
|
||||
fn neg(self) -> Dual {
|
||||
Dual { v: -self.v, g: self.g.map(|x| -x) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<f64> for Dual {
|
||||
type Output = Dual;
|
||||
fn add(mut self, o: f64) -> Dual {
|
||||
self.v += o;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<f64> for Dual {
|
||||
type Output = Dual;
|
||||
fn sub(mut self, o: f64) -> Dual {
|
||||
self.v -= o;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<f64> for Dual {
|
||||
type Output = Dual;
|
||||
fn mul(self, o: f64) -> Dual {
|
||||
Dual { v: self.v * o, g: self.g.map(|x| x * o) }
|
||||
}
|
||||
}
|
||||
|
||||
/// A value, its gradient and its Hessian (packed upper triangle, [`packed`]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Hyper {
|
||||
pub v: f64,
|
||||
pub g: [f64; VARS],
|
||||
pub h: [f64; PACKED],
|
||||
}
|
||||
|
||||
impl Hyper {
|
||||
/// Variable `k` at value `v` (a constant when `k` is out of range).
|
||||
pub fn var(v: f64, k: usize) -> Hyper {
|
||||
let mut d = Hyper::cst(v);
|
||||
if let Some(g) = d.g.get_mut(k) {
|
||||
*g = 1.0;
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// ∂²/∂x_i∂x_j.
|
||||
pub fn hessian(&self, i: usize, j: usize) -> f64 {
|
||||
self.h.get(packed(i, j)).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// f(self) from f's value `f0`, first derivative `f1` and second derivative `f2` at the
|
||||
/// value: g = f1 g, H = f1 H + f2 g gᵀ.
|
||||
fn chain(&self, f0: f64, f1: f64, f2: f64) -> Hyper {
|
||||
let mut out = Hyper { v: f0, g: self.g.map(|x| f1 * x), h: [0.0; PACKED] };
|
||||
let mut at = 0;
|
||||
for i in 0..VARS {
|
||||
let gi = f2 * self.g[i];
|
||||
for j in i..VARS {
|
||||
out.h[at] = f1 * self.h[at] + gi * self.g[j];
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl Real for Hyper {
|
||||
fn cst(v: f64) -> Hyper {
|
||||
Hyper { v, g: [0.0; VARS], h: [0.0; PACKED] }
|
||||
}
|
||||
fn val(&self) -> f64 {
|
||||
self.v
|
||||
}
|
||||
fn sqrt(self) -> Hyper {
|
||||
let s = self.v.sqrt();
|
||||
self.chain(s, 0.5 / s, -0.25 / (s * s * s))
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Hyper {
|
||||
type Output = Hyper;
|
||||
fn add(mut self, o: Hyper) -> Hyper {
|
||||
self.v += o.v;
|
||||
self.g.iter_mut().zip(&o.g).for_each(|(a, b)| *a += b);
|
||||
self.h.iter_mut().zip(&o.h).for_each(|(a, b)| *a += b);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Hyper {
|
||||
type Output = Hyper;
|
||||
fn sub(mut self, o: Hyper) -> Hyper {
|
||||
self.v -= o.v;
|
||||
self.g.iter_mut().zip(&o.g).for_each(|(a, b)| *a -= b);
|
||||
self.h.iter_mut().zip(&o.h).for_each(|(a, b)| *a -= b);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul for Hyper {
|
||||
type Output = Hyper;
|
||||
fn mul(self, o: Hyper) -> Hyper {
|
||||
let mut out = Hyper { v: self.v * o.v, g: [0.0; VARS], h: [0.0; PACKED] };
|
||||
for k in 0..VARS {
|
||||
out.g[k] = self.v * o.g[k] + o.v * self.g[k];
|
||||
}
|
||||
let mut at = 0;
|
||||
for i in 0..VARS {
|
||||
let (a, b) = (self.g[i], o.g[i]);
|
||||
for j in i..VARS {
|
||||
out.h[at] = self.v * o.h[at] + o.v * self.h[at] + a * o.g[j] + b * self.g[j];
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl Div for Hyper {
|
||||
type Output = Hyper;
|
||||
fn div(self, o: Hyper) -> Hyper {
|
||||
let r = 1.0 / o.v;
|
||||
self * o.chain(r, -r * r, 2.0 * r * r * r)
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Hyper {
|
||||
type Output = Hyper;
|
||||
fn neg(self) -> Hyper {
|
||||
Hyper { v: -self.v, g: self.g.map(|x| -x), h: self.h.map(|x| -x) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<f64> for Hyper {
|
||||
type Output = Hyper;
|
||||
fn add(mut self, o: f64) -> Hyper {
|
||||
self.v += o;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<f64> for Hyper {
|
||||
type Output = Hyper;
|
||||
fn sub(mut self, o: f64) -> Hyper {
|
||||
self.v -= o;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<f64> for Hyper {
|
||||
type Output = Hyper;
|
||||
fn mul(self, o: f64) -> Hyper {
|
||||
Hyper { v: self.v * o, g: self.g.map(|x| x * o), h: self.h.map(|x| x * o) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn f<T: Real>(x: T, y: T) -> T {
|
||||
(x * x * y + T::cst(1.0)).sqrt() / (y - x * 0.5) + x * 3.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packed_indices_cover_the_upper_triangle_once() {
|
||||
let mut seen = vec![false; PACKED];
|
||||
let mut at = 0;
|
||||
for i in 0..VARS {
|
||||
for j in i..VARS {
|
||||
assert_eq!(packed(i, j), at);
|
||||
assert_eq!(packed(j, i), at);
|
||||
seen[at] = true;
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
assert!(seen.iter().all(|s| *s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derivatives_match_finite_differences() {
|
||||
let (x, y) = (0.7, 2.3);
|
||||
let h = 1e-5;
|
||||
let fx = |x: f64, y: f64| f(x, y);
|
||||
let gx = (fx(x + h, y) - fx(x - h, y)) / (2.0 * h);
|
||||
let gy = (fx(x, y + h) - fx(x, y - h)) / (2.0 * h);
|
||||
let hxy = (fx(x + h, y + h) - fx(x + h, y - h) - fx(x - h, y + h) + fx(x - h, y - h)) / (4.0 * h * h);
|
||||
let hxx = (fx(x + h, y) - 2.0 * fx(x, y) + fx(x - h, y)) / (h * h);
|
||||
let d = f(Dual::var(x, 3), Dual::var(y, 17));
|
||||
assert!((d.v - fx(x, y)).abs() < 1e-14 && (d.g[3] - gx).abs() < 1e-8 && (d.g[17] - gy).abs() < 1e-8);
|
||||
let e = f(Hyper::var(x, 3), Hyper::var(y, 17));
|
||||
assert!((e.g[3] - gx).abs() < 1e-8 && (e.g[17] - gy).abs() < 1e-8);
|
||||
assert!((e.hessian(3, 17) - hxy).abs() < 1e-4 && (e.hessian(17, 3) - hxy).abs() < 1e-4, "{} {hxy}", e.hessian(3, 17));
|
||||
assert!((e.hessian(3, 3) - hxx).abs() < 1e-3);
|
||||
assert_eq!(e.hessian(0, 5), 0.0);
|
||||
}
|
||||
}
|
||||
+155
-12
@@ -515,6 +515,21 @@ impl Plane<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The kinematics of one point of a 2D element in a nonlinear step: F, det F_m, the volume share, the strain columns (current at finite strain), the small strain,
|
||||
/// the temperature and the reference position.
|
||||
struct PlaneKinematics {
|
||||
f: Mat3,
|
||||
/// det F_m (small strain: 1), and the dilatation the mixed element averages (small strain:
|
||||
/// 1 + tr ε).
|
||||
j: f64,
|
||||
dilatation: f64,
|
||||
dv: f64,
|
||||
current: Vec<[f64; 3]>,
|
||||
strain: [f64; 6],
|
||||
heat: Heat,
|
||||
x: [f64; 3],
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -525,8 +540,14 @@ struct PlanePoint {
|
||||
}
|
||||
|
||||
impl Plane<'_> {
|
||||
fn check_nonlinear(&self, u: &[[f64; 2]], kin: Kinematics) -> Result<()> {
|
||||
fn check_nonlinear(&self, m: &MaterialModel, u: &[[f64; 2]], kin: Kinematics) -> Result<()> {
|
||||
check_displacements(u, self.ty.nodes)?;
|
||||
if matches!(self.formulation, Formulation::Hybrid { .. }) && !m.is_decoupled() {
|
||||
return 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()
|
||||
)));
|
||||
}
|
||||
if let Theory::PlaneStress { .. } = self.theory {
|
||||
let why = match kin {
|
||||
Kinematics::Finite => "the thickness change needs a local iteration",
|
||||
@@ -558,7 +579,7 @@ impl Plane<'_> {
|
||||
updated: &mut [f64],
|
||||
tangent: bool,
|
||||
) -> Result<(Finite, Vec<PlanePoint>)> {
|
||||
self.check_nonlinear(u, kin)?;
|
||||
self.check_nonlinear(m, u, kin)?;
|
||||
let n = self.ty.nodes;
|
||||
if let Some(t) = thermal {
|
||||
t.check(n)?;
|
||||
@@ -570,8 +591,9 @@ impl Plane<'_> {
|
||||
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() {
|
||||
// The kinematics of every point first: the mixed element's pressure needs them all.
|
||||
let mut kinematics = Vec::with_capacity(rule.len());
|
||||
for p in &rule {
|
||||
let at = self.at(p)?;
|
||||
let cols = at.g.get(..n).unwrap_or_default();
|
||||
let mut f = IDENTITY;
|
||||
@@ -585,12 +607,11 @@ impl Plane<'_> {
|
||||
f[2][2] += ua[0] * g[2];
|
||||
}
|
||||
}
|
||||
let updated_point = updated.get_mut(i * len..(i + 1) * len).unwrap_or_default();
|
||||
let heat = Heat::at(thermal, at.shape.values())?;
|
||||
let (j, dv, current, r) = if small {
|
||||
let point = 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)
|
||||
let volume = 1.0 + strain[0] + strain[1] + strain[2];
|
||||
PlaneKinematics { f, j: 1.0, dilatation: volume, dv: at.dv, current: cols.to_vec(), strain, heat, x: at.x }
|
||||
} else {
|
||||
let det2 = f[0][0] * f[1][1] - f[0][1] * f[1][0];
|
||||
let j = det2 * f[2][2];
|
||||
@@ -600,9 +621,29 @@ impl Plane<'_> {
|
||||
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 j = j / heat.volume();
|
||||
PlaneKinematics { f, j, dilatation: j, dv: at.dv * heat.volume(), current, strain: [0.0; 6], heat, x: at.x }
|
||||
};
|
||||
kinematics.push(point);
|
||||
}
|
||||
let responses: Vec<Response> = if let Formulation::Hybrid { .. } = self.formulation {
|
||||
self.mean_dilatation(m, &kinematics, small, committed, updated, k.as_mut())?
|
||||
} else {
|
||||
let mut out = Vec::with_capacity(kinematics.len());
|
||||
for (i, p) in kinematics.iter().enumerate() {
|
||||
let updated_point = updated.get_mut(i * len..(i + 1) * len).unwrap_or_default();
|
||||
let history = point_history(committed, len, i);
|
||||
out.push(if small {
|
||||
m.small_strain(&p.strain, p.heat.temperature, history, updated_point, true)?
|
||||
} else {
|
||||
m.response(&p.heat.mechanical(&p.f), p.heat.temperature, history, updated_point)?
|
||||
});
|
||||
}
|
||||
out
|
||||
};
|
||||
let mut out = Vec::with_capacity(rule.len());
|
||||
for (p, r) in kinematics.into_iter().zip(responses) {
|
||||
let (dv, current) = (p.dv, &p.current);
|
||||
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) {
|
||||
@@ -614,7 +655,7 @@ impl Plane<'_> {
|
||||
}
|
||||
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, dv);
|
||||
crate::plane::accumulate(&mut k.data, current, &c4, dv);
|
||||
if !small {
|
||||
for (a, ga) in current.iter().enumerate() {
|
||||
for (b, gb) in current.iter().enumerate() {
|
||||
@@ -625,11 +666,56 @@ impl Plane<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(PlanePoint { f, j, x: at.x, response: r });
|
||||
out.push(PlanePoint { f: p.f, j: p.j, x: p.x, response: r });
|
||||
}
|
||||
Ok((Finite { force, tangent: k }, out))
|
||||
}
|
||||
|
||||
/// The mean dilatation responses of a hybrid 2D element (one constant pressure): θ = ∫ J dV₀
|
||||
/// / V₀ (hoop stretch included), p = U′(θ), τ = τ_iso + J p I (small strain: s + p I), and
|
||||
/// the dilatation stiffness U″(θ) g gᵀ / V₀ added to `k`, g = ∫ J ∇ₓ·N dV₀ (small strain
|
||||
/// ∫ ∇₀·N dV₀), the divergence taking the hoop column N/r too (the constant-pressure case of
|
||||
/// the solids' three-field form, Simo, Taylor & Pister 1985).
|
||||
fn mean_dilatation(
|
||||
&self,
|
||||
m: &MaterialModel,
|
||||
points: &[PlaneKinematics],
|
||||
small: bool,
|
||||
committed: &[f64],
|
||||
updated: &mut [f64],
|
||||
k: Option<&mut Matrix>,
|
||||
) -> Result<Vec<Response>> {
|
||||
let len = m.history_len();
|
||||
let volume: f64 = points.iter().map(|p| p.dv).sum();
|
||||
if !(volume > 0.0 && volume.is_finite()) {
|
||||
return Err(inverted(&format!("2D {}", self.ty.kind.name()), volume));
|
||||
}
|
||||
let theta = points.iter().map(|p| p.dv * p.dilatation).sum::<f64>() / volume;
|
||||
if !(theta > 0.0 && theta.is_finite()) {
|
||||
return Err(inverted(&format!("2D {}", self.ty.kind.name()), theta));
|
||||
}
|
||||
let (pressure, stiffness) = m.volumetric(theta, small).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
|
||||
let mut out = Vec::with_capacity(points.len());
|
||||
for (i, p) in points.iter().enumerate() {
|
||||
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.heat.temperature, c, u, false)?;
|
||||
r.stress.iter_mut().take(3).for_each(|s| *s += pressure);
|
||||
r
|
||||
} else {
|
||||
add_pressure(&m.isochoric(&p.heat.mechanical(&p.f), p.heat.temperature, c, u)?, p.j, pressure, 0.0)
|
||||
});
|
||||
}
|
||||
if let Some(k) = k {
|
||||
let mut g = vec![0.0; k.n];
|
||||
for p in points {
|
||||
crate::plane::divergence(&mut g, &p.current, p.dv * p.j);
|
||||
}
|
||||
crate::plane::add_outer(k, &g, stiffness / volume);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Internal forces (u, v per node) and (with `tangent`) the tangent stiffness at nodal
|
||||
/// displacements `u`, as for [`Solid::finite_strain`].
|
||||
pub fn finite_strain(&self, m: &MaterialModel, u: &[[f64; 2]], committed: &[f64], updated: &mut [f64], tangent: bool) -> Result<Finite> {
|
||||
@@ -1016,6 +1102,63 @@ mod tests {
|
||||
assert!(matches!(stress.finite_strain(&svk, &[[0.0; 2]; 4], &[], &mut [], true), Err(Error::NotSupported(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hybrid_2d_tangents_are_the_derivative_of_the_forces() {
|
||||
let quad8 = [
|
||||
[1.0, 0.0, 0.0],
|
||||
[2.0, 0.1, 0.0],
|
||||
[2.2, 1.0, 0.0],
|
||||
[0.9, 1.1, 0.0],
|
||||
[1.5, 0.0, 0.0],
|
||||
[2.1, 0.55, 0.0],
|
||||
[1.5, 1.1, 0.0],
|
||||
[0.95, 0.5, 0.0],
|
||||
];
|
||||
let (shear, bulk) = (300.0, 30000.0);
|
||||
let neo = MaterialModel::NeoHookean { shear, bulk };
|
||||
let mr = MaterialModel::Polynomial { name: "Mooney–Rivlin", c: [100.0, 50.0, 0.0, 0.0], bulk };
|
||||
let hybrid = Formulation::Hybrid { pressures: 1 };
|
||||
for theory in [Theory::PlaneStrain { thickness: 2.0 }, Theory::Axisymmetric] {
|
||||
for (kind, x) in [(ElementKind::Quad8, &quad8[..]), (ElementKind::Quad4, &quad8[..4])] {
|
||||
let plane = Plane::new(kind, x, hybrid, theory).unwrap();
|
||||
for m in [&neo, &mr] {
|
||||
let u: Vec<[f64; 2]> = displacement(x.len(), 0.06).iter().map(|v| [v[0], v[1]]).collect();
|
||||
let k = plane.finite_strain(m, &u, &[], &mut [], true).unwrap().tangent.unwrap();
|
||||
let scale = k.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let h = 1e-6;
|
||||
for j in 0..2 * x.len() {
|
||||
let shifted = |s: f64| {
|
||||
let mut v = u.clone();
|
||||
v[j / 2][j % 2] += s;
|
||||
plane.finite_strain(m, &v, &[], &mut [], false).unwrap().force
|
||||
};
|
||||
let (p, q) = (shifted(h), shifted(-h));
|
||||
for i in 0..2 * x.len() {
|
||||
let numeric = (p[i] - q[i]) / (2.0 * h);
|
||||
assert!((numeric - k.get(i, j)).abs() < 1e-6 * scale, "{theory:?} {kind:?} {i} {j}: {numeric} vs {}", k.get(i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
// At rest the tangent is the linear hybrid stiffness of the same moduli.
|
||||
let young = 9.0 * bulk * shear / (3.0 * bulk + shear);
|
||||
let poisson = (3.0 * bulk - 2.0 * shear) / (2.0 * (3.0 * bulk + shear));
|
||||
let d = isotropic(young, poisson).unwrap();
|
||||
let k = plane.finite_strain(&neo, &vec![[0.0; 2]; x.len()], &[], &mut [], true).unwrap().tangent.unwrap();
|
||||
let linear = plane.stiffness(&d).unwrap();
|
||||
let scale = linear.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
for (a, b) in k.data.iter().zip(&linear.data) {
|
||||
assert!((a - b).abs() < 1e-9 * scale, "{a} {b}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// The linear triangle and plane stress have none; St Venant–Kirchhoff has no split.
|
||||
assert!(Plane::new(ElementKind::Tri3, &quad8[..3], hybrid, Theory::PlaneStrain { thickness: 1.0 }).is_err());
|
||||
assert!(Plane::new(ElementKind::Quad4, &quad8[..4], hybrid, Theory::PlaneStress { thickness: 1.0 }).is_err());
|
||||
let svk = MaterialModel::StVenantKirchhoff(Box::new(isotropic(1000.0, 0.3).unwrap()));
|
||||
let plane = Plane::new(ElementKind::Quad4, &quad8[..4], hybrid, Theory::Axisymmetric).unwrap();
|
||||
assert!(matches!(plane.finite_strain(&svk, &[[0.0; 2]; 4], &[], &mut [], true), Err(Error::NotSupported(_))));
|
||||
}
|
||||
|
||||
/// uᵀ K_σ u = ∫ σ_xx (∂u_y/∂x)² dV = σ_xx V for u_y = x under a uniform σ_xx, in a distorted
|
||||
/// hex20 and a plane strain quad8, and K_σ annihilates rigid translations.
|
||||
#[test]
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
//! - [`face`]: consistent nodal forces of pressure on element faces;
|
||||
//! - [`contact`]: contact geometry: the closest point of an element face to a point and the
|
||||
//! bucket grid of the global search;
|
||||
//! - [`mortar`]: mortar segmentation of a secondary face against a main face and the integration
|
||||
//! points on the overlap, over [`dual`]'s automatic differentiation for the exact tangent;
|
||||
//! - [`heat`]: heat conduction (conductivity, capacity, sources, surface flux and convection,
|
||||
//! heat flux); thermal strain loads and stresses are in [`element`];
|
||||
//! - [`uniform`]: uniform-strain (one-point) hexahedron and tetrahedron kinematics, hourglass
|
||||
@@ -59,6 +61,7 @@ pub mod constitutive;
|
||||
pub mod contact;
|
||||
pub mod dense;
|
||||
pub mod dof;
|
||||
pub mod dual;
|
||||
pub mod elastoplastic;
|
||||
pub mod element;
|
||||
pub mod face;
|
||||
@@ -68,6 +71,7 @@ pub mod heat;
|
||||
pub mod hybrid;
|
||||
pub mod jet;
|
||||
pub mod material;
|
||||
pub mod mortar;
|
||||
pub mod plane;
|
||||
pub mod plasticity;
|
||||
pub mod quadrature;
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
//! Mortar segmentation and integration: the overlap of a secondary (non-mortar) face with a main
|
||||
//! (mortar) face, and the integration points on it where both faces' shape functions and
|
||||
//! positions are known.
|
||||
//!
|
||||
//! **Segmentation** (M. A. Puso, T. A. Laursen, *A mortar segment-to-segment contact method for
|
||||
//! large deformation solid mechanics*, CMAME 193:601–629, 2004, §3; A. Popp, M. W. Gee, W. A.
|
||||
//! Wall, *A finite deformation mortar contact formulation using a primal–dual active set
|
||||
//! strategy*, IJNME 79:1354–1391, 2009, §3.3). Both faces are projected along the secondary
|
||||
//! face's unit normal n onto its plane (through its centroid), and the main polygon is clipped by
|
||||
//! the secondary one (Sutherland–Hodgman; a quadrilateral whose projection is not convex is
|
||||
//! split into its two triangles first). Each overlap polygon is split into a fan of triangles
|
||||
//! integrated with the seven-point degree-5 rule (J. Radon 1948; A. H. Stroud, *Approximate
|
||||
//! Calculation of Multiple Integrals*, Prentice-Hall 1971, T2:5-1), which integrates the products
|
||||
//! of two shape functions that the tie and the patch test need exactly on triangles and
|
||||
//! parallelograms (on a general quadrilateral the inverse of its map is not a polynomial, and the
|
||||
//! rule is accurate to its degree).
|
||||
//!
|
||||
//! **Points.** At an integration point y of the plane, each face's coordinates are found by
|
||||
//! inverting its projected map: the linear one of a triangle, or the bilinear one of a
|
||||
//! quadrilateral, x(u, v) = a + b u + c v + d u v, whose inverse is the root in [0, 1] of the
|
||||
//! quadratic (b × d) u² + (b × c − h × d) u − h × c = 0 (h = y − a, × the scalar cross product of
|
||||
//! the plane). The projection of a bilinear patch onto a plane is the bilinear patch of its
|
||||
//! projected corners, so the main face's point found this way is the point of the main surface
|
||||
//! along n through y, warped or not. The [`Sample`] then holds both faces' shape functions, both
|
||||
//! points in space, the normal and the weight (the plane's area element times the rule's weight).
|
||||
//!
|
||||
//! The kernel is written over [`Real`], so the same code gives the values (`f64`), their
|
||||
//! gradient ([`crate::dual::Dual`]) or their gradient and Hessian ([`crate::dual::Hyper`]) with
|
||||
//! respect to the corner coordinates, variables `3 i + k` for secondary corner i and
|
||||
//! `12 + 3 j + k` for main corner j: the exact linearisation of the segmentation, the normal and
|
||||
//! the projection, which the consistent tangent of mortar contact needs (Popp et al. 2009, §4).
|
||||
//!
|
||||
//! **Edges of 2D models** ([`integrate_edges`]): plane strain, plane stress and axisymmetric
|
||||
//! models touch along element edges, two-node segments in the XY plane. The main edge is
|
||||
//! projected along the secondary edge's unit normal onto its line, which maps each main point to
|
||||
//! one parameter of the secondary edge linearly, so the overlap is an interval of that parameter
|
||||
//! (B. Yang, T. A. Laursen, X. Meng, *Two dimensional mortar contact methods for large
|
||||
//! deformation frictional sliding*, IJNME 62:1183–1225, 2005, §3). It is integrated with the
|
||||
//! three-point Gauss–Legendre rule, exact for the degree-3 products Φ_a N_b r that the
|
||||
//! axisymmetric patch test needs (the edge's out-of-plane measure, the thickness or 2π r at the
|
||||
//! secondary point, is part of the weight). Points stay three-component with z = 0, so the solver
|
||||
//! takes edges through the same variables as faces.
|
||||
|
||||
use crate::dual::Real;
|
||||
use fieldcraft_model::Extent;
|
||||
|
||||
/// A point in space over a scalar.
|
||||
pub type Point<T> = [T; 3];
|
||||
|
||||
/// A point of the secondary face's plane.
|
||||
type Flat<T> = [T; 2];
|
||||
|
||||
/// Radon's seven-point rule on a triangle, exact for degree 5: barycentric coordinates of the
|
||||
/// second and third corners and weights summing to one.
|
||||
fn rule() -> [([f64; 2], f64); 7] {
|
||||
let s = 15f64.sqrt();
|
||||
let (a, b) = ((6.0 - s) / 21.0, (9.0 + 2.0 * s) / 21.0);
|
||||
let (c, d) = ((6.0 + s) / 21.0, (9.0 - 2.0 * s) / 21.0);
|
||||
let (wa, wc) = ((155.0 - s) / 1200.0, (155.0 + s) / 1200.0);
|
||||
[([1.0 / 3.0, 1.0 / 3.0], 0.225), ([a, a], wa), ([b, a], wa), ([a, b], wa), ([c, c], wc), ([d, c], wc), ([c, d], wc)]
|
||||
}
|
||||
|
||||
/// One integration point of a segment.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Sample<T> {
|
||||
/// The rule's weight times the plane's area element (the secondary face's projected area).
|
||||
pub weight: T,
|
||||
/// The secondary face's shape functions at the point (0 past its corners).
|
||||
pub secondary: [T; 4],
|
||||
/// The main face's shape functions at the point.
|
||||
pub main: [T; 4],
|
||||
/// The point on the secondary face, and on the main face along the normal.
|
||||
pub xs: Point<T>,
|
||||
pub xm: Point<T>,
|
||||
/// The secondary face's unit normal (outward from its element).
|
||||
pub normal: Point<T>,
|
||||
}
|
||||
|
||||
impl<T: Real> Sample<T> {
|
||||
/// The gap along the secondary normal, (x_m − x_s) · n: positive where the faces are apart.
|
||||
pub fn gap(&self) -> T {
|
||||
dot(sub(self.xm, self.xs), self.normal)
|
||||
}
|
||||
}
|
||||
|
||||
fn sub<T: Real>(a: Point<T>, b: Point<T>) -> Point<T> {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
|
||||
fn dot<T: Real>(a: Point<T>, b: Point<T>) -> T {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
fn cross<T: Real>(a: Point<T>, b: Point<T>) -> Point<T> {
|
||||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||||
}
|
||||
|
||||
fn scale<T: Real>(a: Point<T>, s: T) -> Point<T> {
|
||||
[a[0] * s, a[1] * s, a[2] * s]
|
||||
}
|
||||
|
||||
fn cross2<T: Real>(a: Flat<T>, b: Flat<T>) -> T {
|
||||
a[0] * b[1] - a[1] * b[0]
|
||||
}
|
||||
|
||||
fn sub2<T: Real>(a: Flat<T>, b: Flat<T>) -> Flat<T> {
|
||||
[a[0] - b[0], a[1] - b[1]]
|
||||
}
|
||||
|
||||
/// A face's area vector: its outward normal times its area (flat triangle; for a bilinear
|
||||
/// quadrilateral half the cross product of its diagonals, the area of its projection on the
|
||||
/// plane normal to it).
|
||||
pub fn area_vector<T: Real>(x: &[Point<T>]) -> Option<Point<T>> {
|
||||
let half = |v: Point<T>| scale(v, T::cst(0.5));
|
||||
match x {
|
||||
[a, b, c] => Some(half(cross(sub(*b, *a), sub(*c, *a)))),
|
||||
[a, b, c, d] => Some(half(cross(sub(*c, *a), sub(*d, *b)))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A projected face as convex counter-clockwise pieces for the clipping: itself when it is
|
||||
/// convex (no artificial diagonal, whose crossings with the other face's edges would change the
|
||||
/// overlap's polygons, and the integration's round-off, as the faces move), else its two
|
||||
/// triangles.
|
||||
fn pieces<T: Real>(points: &[Flat<T>]) -> Vec<Vec<Flat<T>>> {
|
||||
let mut whole = points.to_vec();
|
||||
if signed_area(&whole).val() < 0.0 {
|
||||
whole.reverse();
|
||||
}
|
||||
let n = whole.len();
|
||||
let convex = (0..n).all(|i| {
|
||||
let (a, b, c) = (whole[i], whole[(i + 1) % n], whole[(i + 2) % n]);
|
||||
cross2(sub2(b, a), sub2(c, b)).val() > 0.0
|
||||
});
|
||||
if convex || n == 3 {
|
||||
return vec![whole];
|
||||
}
|
||||
[[0, 1, 2], [0, 2, 3]].iter().map(|t| counter_clockwise(t, points)).collect()
|
||||
}
|
||||
|
||||
fn signed_area<T: Real>(p: &[Flat<T>]) -> T {
|
||||
let mut s = T::cst(0.0);
|
||||
for (i, a) in p.iter().enumerate() {
|
||||
let b = p.get((i + 1) % p.len()).copied().unwrap_or(*a);
|
||||
s = s + cross2(*a, b);
|
||||
}
|
||||
s * 0.5
|
||||
}
|
||||
|
||||
/// The corners `t` of `points`, counter-clockwise.
|
||||
fn counter_clockwise<T: Real>(t: &[usize; 3], points: &[Flat<T>]) -> Vec<Flat<T>> {
|
||||
let mut out: Vec<Flat<T>> = t.iter().filter_map(|i| points.get(*i).copied()).collect();
|
||||
if signed_area(&out).val() < 0.0 {
|
||||
out.reverse();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A vertex this close to a clip edge, relative to the edge's length, is on it.
|
||||
const ON_EDGE: f64 = 1e-8;
|
||||
|
||||
/// `subject` clipped by the convex counter-clockwise polygon `clip` (Sutherland–Hodgman), the
|
||||
/// sides decided on the values. A vertex within [`ON_EDGE`] of a clip edge's line counts as on
|
||||
/// it (kept, and never cut): the intersection of two nearly coincident edges is ill-conditioned,
|
||||
/// its position and above all its derivatives swamped by round-off, while the vertex itself is
|
||||
/// as good a corner of the overlap to that precision. Edges are cut only between a vertex
|
||||
/// clearly inside and one clearly outside, where the intersection is well conditioned.
|
||||
fn clip<T: Real>(subject: &[Flat<T>], clip: &[Flat<T>]) -> Vec<Flat<T>> {
|
||||
let mut out: Vec<Flat<T>> = subject.to_vec();
|
||||
for (i, a) in clip.iter().enumerate() {
|
||||
let Some(b) = clip.get((i + 1) % clip.len()).copied() else { break };
|
||||
let input = std::mem::take(&mut out);
|
||||
let edge = sub2(b, *a);
|
||||
let eps = ON_EDGE * (edge[0].val() * edge[0].val() + edge[1].val() * edge[1].val());
|
||||
let side = |p: Flat<T>| cross2(edge, sub2(p, *a));
|
||||
for (j, p) in input.iter().enumerate() {
|
||||
let Some(q) = input.get((j + 1) % input.len()).copied() else { break };
|
||||
let (sp, sq) = (side(*p), side(q));
|
||||
if sp.val() >= -eps {
|
||||
out.push(*p);
|
||||
}
|
||||
if (sp.val() > eps && sq.val() < -eps) || (sp.val() < -eps && sq.val() > eps) {
|
||||
let t = sp / (sp - sq);
|
||||
out.push([p[0] + t * (q[0] - p[0]), p[1] + t * (q[1] - p[1])]);
|
||||
}
|
||||
}
|
||||
if out.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The shape functions of a face (of 3 or 4 projected corners `p`) at plane point `y`: the
|
||||
/// inverse of its linear or bilinear map. `None` when it has no inverse there.
|
||||
fn shape_at<T: Real>(p: &[Flat<T>], y: Flat<T>) -> Option<[T; 4]> {
|
||||
let zero = T::cst(0.0);
|
||||
match p {
|
||||
[p0, p1, p2] => {
|
||||
let (e1, e2, h) = (sub2(*p1, *p0), sub2(*p2, *p0), sub2(y, *p0));
|
||||
let det = cross2(e1, e2);
|
||||
if !(det.val().abs() > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let v = cross2(h, e2) / det;
|
||||
let w = cross2(e1, h) / det;
|
||||
Some([T::cst(1.0) - v - w, v, w, zero])
|
||||
}
|
||||
[p0, p1, p2, p3] => {
|
||||
let b = sub2(*p1, *p0);
|
||||
let c = sub2(*p3, *p0);
|
||||
let d = [p0[0] - p1[0] + p2[0] - p3[0], p0[1] - p1[1] + p2[1] - p3[1]];
|
||||
let h = sub2(y, *p0);
|
||||
let k2 = cross2(b, d);
|
||||
let k1 = cross2(b, c) - cross2(h, d);
|
||||
let k0 = -cross2(h, c);
|
||||
let u = if k2.val().abs() <= 1e-12 * k1.val().abs() {
|
||||
if !(k1.val().abs() > 0.0) {
|
||||
return None;
|
||||
}
|
||||
-k0 / k1
|
||||
} else {
|
||||
// The roots of k2 u² + k1 u + k0, computed without cancellation; the one in [0, 1]
|
||||
// (the nearer when round-off puts both outside).
|
||||
let disc = k1 * k1 - k2 * k0 * 4.0;
|
||||
let root = if disc.val() > 0.0 { disc.sqrt() } else { T::cst(0.0) };
|
||||
let q = if k1.val() >= 0.0 { -(k1 + root) * 0.5 } else { (root - k1) * 0.5 };
|
||||
let mut candidates = [None, None];
|
||||
if q.val().abs() > 0.0 {
|
||||
candidates[0] = Some(q / k2);
|
||||
candidates[1] = Some(k0 / q);
|
||||
} else {
|
||||
candidates[0] = Some(-k1 / (k2 * 2.0));
|
||||
}
|
||||
let distance = |u: f64| {
|
||||
if u < 0.0 {
|
||||
-u
|
||||
} else if u > 1.0 {
|
||||
u - 1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
candidates.into_iter().flatten().filter(|u| u.val().is_finite()).min_by(|a, b| distance(a.val()).total_cmp(&distance(b.val())))?
|
||||
};
|
||||
// v from the better conditioned component of h − u b = v (c + d u).
|
||||
let m = [c[0] + d[0] * u, c[1] + d[1] * u];
|
||||
let k = if m[0].val().abs() >= m[1].val().abs() { 0 } else { 1 };
|
||||
if !(m[k].val().abs() > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let v = (h[k] - b[k] * u) / m[k];
|
||||
let (s, t) = (u * 2.0 - 1.0, v * 2.0 - 1.0);
|
||||
let one = T::cst(1.0);
|
||||
Some([(one - s) * (one - t) * 0.25, (one + s) * (one - t) * 0.25, (one + s) * (one + t) * 0.25, (one - s) * (one + t) * 0.25])
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn interpolate<T: Real>(x: &[Point<T>], n: &[T; 4]) -> Point<T> {
|
||||
let mut out = [T::cst(0.0); 3];
|
||||
for (xi, ni) in x.iter().zip(n) {
|
||||
for k in 0..3 {
|
||||
out[k] = out[k] + xi[k] * *ni;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Integrate over the overlap of secondary face `secondary` with main face `main` (3 or 4
|
||||
/// corners each, in their faces' node order): `f` is called at every integration point. `None`
|
||||
/// when a face has another number of corners or the secondary face has no normal (collapsed);
|
||||
/// faces that do not overlap give no point.
|
||||
pub fn integrate<T: Real>(secondary: &[Point<T>], main: &[Point<T>], mut f: impl FnMut(&Sample<T>)) -> Option<()> {
|
||||
if !matches!(main.len(), 3 | 4) {
|
||||
return None;
|
||||
}
|
||||
let area = area_vector(secondary)?;
|
||||
let length = dot(area, area).sqrt();
|
||||
if !(length.val() > 0.0 && length.val().is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let normal = scale(area, T::cst(1.0) / length);
|
||||
let count = T::cst(secondary.len() as f64);
|
||||
let mut center = [T::cst(0.0); 3];
|
||||
for x in secondary {
|
||||
for k in 0..3 {
|
||||
center[k] = center[k] + x[k] / count;
|
||||
}
|
||||
}
|
||||
// A basis of the plane, (e1, e2, n) right-handed, e1 towards the first corner.
|
||||
let r = sub(*secondary.first()?, center);
|
||||
let r = sub(r, scale(normal, dot(r, normal)));
|
||||
let rl = dot(r, r).sqrt();
|
||||
if !(rl.val() > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let e1 = scale(r, T::cst(1.0) / rl);
|
||||
let e2 = cross(normal, e1);
|
||||
let flat = |x: &Point<T>| -> Flat<T> {
|
||||
let d = sub(*x, center);
|
||||
[dot(d, e1), dot(d, e2)]
|
||||
};
|
||||
let own: Vec<Flat<T>> = secondary.iter().map(flat).collect();
|
||||
let theirs: Vec<Flat<T>> = main.iter().map(flat).collect();
|
||||
let rule = rule();
|
||||
let (own_pieces, their_pieces) = (pieces(&own), pieces(&theirs));
|
||||
for s in &own_pieces {
|
||||
for m in &their_pieces {
|
||||
let overlap = clip(m, s);
|
||||
let Some(o) = overlap.first().copied() else { continue };
|
||||
for w in overlap.windows(2).skip(1) {
|
||||
let (p, q) = (w[0], w[1]);
|
||||
let doubled = cross2(sub2(p, o), sub2(q, o));
|
||||
if !(doubled.val() > 0.0) {
|
||||
continue;
|
||||
}
|
||||
for (bary, weight) in rule {
|
||||
let y = [0, 1].map(|k| o[k] + (p[k] - o[k]) * bary[0] + (q[k] - o[k]) * bary[1]);
|
||||
let (Some(ns), Some(nm)) = (shape_at(&own, y), shape_at(&theirs, y)) else { continue };
|
||||
f(&Sample {
|
||||
weight: doubled * (0.5 * weight),
|
||||
secondary: ns,
|
||||
main: nm,
|
||||
xs: interpolate(secondary, &ns),
|
||||
xm: interpolate(main, &nm),
|
||||
normal,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// The out-of-plane measure of a 2D model at a point: the thickness, or 2π x.
|
||||
fn measure<T: Real>(extent: Extent, x: T) -> T {
|
||||
match extent {
|
||||
Extent::Thickness(t) => T::cst(t),
|
||||
Extent::Revolved => x * (2.0 * std::f64::consts::PI),
|
||||
}
|
||||
}
|
||||
|
||||
/// The unit tangent of a two-point edge in the XY plane and its length.
|
||||
fn edge_frame<T: Real>(x: &[Point<T>]) -> Option<(Flat<T>, T)> {
|
||||
let [a, b] = x else { return None };
|
||||
let t = [b[0] - a[0], b[1] - a[1]];
|
||||
let length = (t[0] * t[0] + t[1] * t[1]).sqrt();
|
||||
if !(length.val() > 0.0 && length.val().is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let inv = T::cst(1.0) / length;
|
||||
Some(([t[0] * inv, t[1] * inv], length))
|
||||
}
|
||||
|
||||
/// An edge's area vector in a 2D model (two corners in the XY plane): its unit normal
|
||||
/// (t_y, −t_x), outward when the edge runs counter-clockwise round its element, times its
|
||||
/// length and its out-of-plane measure at its midpoint (∫ 2π r ds exactly for a straight edge).
|
||||
pub fn edge_area_vector<T: Real>(x: &[Point<T>], extent: Extent) -> Option<Point<T>> {
|
||||
let (t, length) = edge_frame(x)?;
|
||||
let [a, b] = x else { return None };
|
||||
let size = length * measure(extent, (a[0] + b[0]) * 0.5);
|
||||
Some([t[1] * size, -(t[0] * size), T::cst(0.0)])
|
||||
}
|
||||
|
||||
/// Integrate over the overlap of secondary edge `secondary` with main edge `main` of a 2D model
|
||||
/// (two corners each, z ignored), as [`integrate`] does for faces: the samples' shape functions
|
||||
/// are those of the edges' two corners (the last two are zero), the normal is the secondary
|
||||
/// edge's (t_y, −t_x, 0) and the weight carries the out-of-plane measure `extent` at the
|
||||
/// secondary point. `None` when an edge does not have two corners or the secondary one has no
|
||||
/// length; edges that do not overlap, or a main edge along the normal, give no point.
|
||||
pub fn integrate_edges<T: Real>(secondary: &[Point<T>], main: &[Point<T>], extent: Extent, mut f: impl FnMut(&Sample<T>)) -> Option<()> {
|
||||
let (t, length) = edge_frame(secondary)?;
|
||||
let ([s0, s1], [m0, m1]) = (secondary, main) else { return None };
|
||||
let inv = T::cst(1.0) / length;
|
||||
let along = |p: &Point<T>| ((p[0] - s0[0]) * t[0] + (p[1] - s0[1]) * t[1]) * inv;
|
||||
let (a, b) = (along(m0), along(m1));
|
||||
let span = b - a;
|
||||
if !(span.val().abs() > 1e-12) || !span.val().is_finite() {
|
||||
return Some(());
|
||||
}
|
||||
let zero = T::cst(0.0);
|
||||
let one = T::cst(1.0);
|
||||
let (low, high) = if a.val() <= b.val() { (a, b) } else { (b, a) };
|
||||
let lo = if low.val() > 0.0 { low } else { zero };
|
||||
let hi = if high.val() < 1.0 { high } else { one };
|
||||
let width = hi - lo;
|
||||
if !(width.val() > 0.0) {
|
||||
return Some(());
|
||||
}
|
||||
let normal = [t[1], -t[0], zero];
|
||||
let size = width * length;
|
||||
// Three-point Gauss–Legendre mapped from [−1, 1] onto the overlap.
|
||||
for &(g, w) in crate::quadrature::gauss_legendre(3) {
|
||||
let xi = lo + width * (0.5 * (1.0 + g));
|
||||
let eta = (xi - a) / span;
|
||||
let xs = [s0[0] + (s1[0] - s0[0]) * xi, s0[1] + (s1[1] - s0[1]) * xi, zero];
|
||||
let xm = [m0[0] + (m1[0] - m0[0]) * eta, m0[1] + (m1[1] - m0[1]) * eta, zero];
|
||||
f(&Sample {
|
||||
weight: size * measure(extent, xs[0]) * (0.5 * w),
|
||||
secondary: [one - xi, xi, zero, zero],
|
||||
main: [one - eta, eta, zero, zero],
|
||||
xs,
|
||||
xm,
|
||||
normal,
|
||||
});
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dual::{Dual, Hyper, VARS};
|
||||
|
||||
fn square(x0: f64, y0: f64, side: f64, z: f64, up: bool) -> Vec<[f64; 3]> {
|
||||
let c = [[x0, y0, z], [x0 + side, y0, z], [x0 + side, y0 + side, z], [x0, y0 + side, z]];
|
||||
if up { c.to_vec() } else { vec![c[0], c[3], c[2], c[1]] }
|
||||
}
|
||||
|
||||
/// ∫ N_a over the overlap and ∫ N_a g, per secondary corner.
|
||||
fn integrals(s: &[[f64; 3]], m: &[[f64; 3]]) -> ([f64; 4], [f64; 4], f64) {
|
||||
let (mut d, mut g, mut area) = ([0.0; 4], [0.0; 4], 0.0);
|
||||
integrate(s, m, |p| {
|
||||
area += p.weight;
|
||||
for a in 0..4 {
|
||||
d[a] += p.weight * p.secondary[a];
|
||||
g[a] += p.weight * p.secondary[a] * p.gap();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
(d, g, area)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_covered_face_integrates_its_shape_functions_and_gap() {
|
||||
// Secondary face up (normal +z), main face facing it 0.1 above.
|
||||
let s = square(0.0, 0.0, 1.0, 0.0, true);
|
||||
let m = square(-0.5, -0.5, 2.0, 0.1, false);
|
||||
let (d, g, area) = integrals(&s, &m);
|
||||
assert!((area - 1.0).abs() < 1e-14);
|
||||
for a in 0..4 {
|
||||
assert!((d[a] - 0.25).abs() < 1e-14 && (g[a] - 0.025).abs() < 1e-14, "{d:?} {g:?}");
|
||||
}
|
||||
// Half covered: the overlap is the strip x > 0.5.
|
||||
let (d, _, area) = integrals(&s, &square(0.5, -0.5, 2.0, 0.1, false));
|
||||
assert!((area - 0.5).abs() < 1e-14);
|
||||
assert!((d[0] - 0.0625).abs() < 1e-14 && (d[1] - 0.1875).abs() < 1e-14, "{d:?}");
|
||||
// Apart: nothing.
|
||||
let (_, _, area) = integrals(&s, &square(3.0, 0.0, 1.0, 0.1, false));
|
||||
assert_eq!(area, 0.0);
|
||||
}
|
||||
|
||||
/// The sums over a 3 × 3 grid of main faces (triangles in one corner) under secondary face
|
||||
/// `s`, against a tensor-product Gauss rule on the face itself.
|
||||
fn covered(s: &[[f64; 3]; 4]) -> ([f64; 4], [f64; 4], f64, f64) {
|
||||
let mut d = [0.0; 4];
|
||||
let mut area = 0.0;
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
let quad = square(-0.3 + 0.6 * i as f64, -0.3 + 0.6 * j as f64, 0.6, 0.2, false);
|
||||
let faces: Vec<Vec<[f64; 3]>> =
|
||||
if i == 0 && j == 0 { vec![vec![quad[0], quad[1], quad[2]], vec![quad[0], quad[2], quad[3]]] } else { vec![quad] };
|
||||
for f in faces {
|
||||
let (da, _, a) = integrals(s, &f);
|
||||
area += a;
|
||||
for k in 0..4 {
|
||||
d[k] += da[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut reference = [0.0; 4];
|
||||
let mut exact = 0.0;
|
||||
let g = [-(0.6f64).sqrt(), 0.0, 0.6f64.sqrt()];
|
||||
let w = [5.0 / 9.0, 8.0 / 9.0, 5.0 / 9.0];
|
||||
for (a, wa) in g.iter().zip(w) {
|
||||
for (b, wb) in g.iter().zip(w) {
|
||||
let n = [(1.0 - a) * (1.0 - b), (1.0 + a) * (1.0 - b), (1.0 + a) * (1.0 + b), (1.0 - a) * (1.0 + b)].map(|v| v / 4.0);
|
||||
let dxa: [f64; 3] =
|
||||
std::array::from_fn(|k| 0.25 * (-(1.0 - b) * s[0][k] + (1.0 - b) * s[1][k] + (1.0 + b) * s[2][k] - (1.0 + b) * s[3][k]));
|
||||
let dxb: [f64; 3] =
|
||||
std::array::from_fn(|k| 0.25 * (-(1.0 - a) * s[0][k] - (1.0 + a) * s[1][k] + (1.0 + a) * s[2][k] + (1.0 - a) * s[3][k]));
|
||||
let j = crate::frame::norm(crate::frame::cross(dxa, dxb));
|
||||
exact += wa * wb * j;
|
||||
for k in 0..4 {
|
||||
reference[k] += wa * wb * j * n[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
(d, reference, area, exact)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_matching_main_faces_add_up_to_the_covered_integrals() {
|
||||
// A parallelogram: its shape functions are polynomials of the plane, integrated exactly.
|
||||
let (d, reference, area, exact) = covered(&[[0.0, 0.0, 0.0], [1.0, 0.2, 0.0], [1.1, 1.1, 0.0], [0.1, 0.9, 0.0]]);
|
||||
assert!((area - exact).abs() < 1e-13, "{area} {exact}");
|
||||
for k in 0..4 {
|
||||
assert!((d[k] - reference[k]).abs() < 1e-13, "{d:?} {reference:?}");
|
||||
}
|
||||
// A general quadrilateral: its inverse map is not polynomial; the rule is accurate to
|
||||
// its degree.
|
||||
let (d, reference, area, exact) = covered(&[[0.0, 0.0, 0.0], [1.1, 0.1, 0.0], [1.0, 0.9, 0.0], [0.05, 1.2, 0.0]]);
|
||||
assert!((area - exact).abs() < 1e-13, "{area} {exact}");
|
||||
for k in 0..4 {
|
||||
assert!((d[k] - reference[k]).abs() < 1e-5 * reference[k], "{d:?} {reference:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// ∫ N_a g over a warped, tilted pair, as a function of all 24 coordinates.
|
||||
fn weighted_gap<T: Real>(x: &[T; VARS], a: usize) -> T {
|
||||
let s: Vec<Point<T>> = (0..4).map(|i| [x[3 * i], x[3 * i + 1], x[3 * i + 2]]).collect();
|
||||
let m: Vec<Point<T>> = (0..4).map(|j| [x[12 + 3 * j], x[13 + 3 * j], x[14 + 3 * j]]).collect();
|
||||
let mut g = T::cst(0.0);
|
||||
integrate(&s, &m, |p| g = g + p.weight * p.secondary[a] * p.gap()).unwrap();
|
||||
g
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_automatic_derivatives_are_those_of_the_integral() {
|
||||
let base: [f64; VARS] = [
|
||||
0.0, 0.0, 0.0, 1.0, 0.05, 0.02, 1.1, 0.9, -0.03, -0.05, 1.0, 0.01, // secondary
|
||||
-0.2, -0.3, 0.12, -0.25, 1.15, 0.08, 1.2, 1.1, 0.1, 0.9, -0.2, 0.15, // main, facing down
|
||||
];
|
||||
let a = 2;
|
||||
let duals: [Dual; VARS] = std::array::from_fn(|k| Dual::var(base[k], k));
|
||||
let hypers: [Hyper; VARS] = std::array::from_fn(|k| Hyper::var(base[k], k));
|
||||
let d = weighted_gap(&duals, a);
|
||||
let e = weighted_gap(&hypers, a);
|
||||
let v = weighted_gap(&base, a);
|
||||
assert!((d.v - v).abs() < 1e-14 && (e.v - v).abs() < 1e-14);
|
||||
let h = 1e-6;
|
||||
let shifted = |k: usize, by: f64| {
|
||||
let mut x = base;
|
||||
x[k] += by;
|
||||
x
|
||||
};
|
||||
for k in 0..VARS {
|
||||
let fd = (weighted_gap(&shifted(k, h), a) - weighted_gap(&shifted(k, -h), a)) / (2.0 * h);
|
||||
assert!((d.g[k] - fd).abs() < 1e-7 && (e.g[k] - d.g[k]).abs() < 1e-13, "gradient {k}: {} {fd}", d.g[k]);
|
||||
// The Hessian's row k against differences of the gradient.
|
||||
let (plus, minus) = (shifted(k, h), shifted(k, -h));
|
||||
let gp = weighted_gap(&std::array::from_fn::<Dual, VARS, _>(|i| Dual::var(plus[i], i)), a);
|
||||
let gm = weighted_gap(&std::array::from_fn::<Dual, VARS, _>(|i| Dual::var(minus[i], i)), a);
|
||||
for j in 0..VARS {
|
||||
let fd = (gp.g[j] - gm.g[j]) / (2.0 * h);
|
||||
assert!((e.hessian(k, j) - fd).abs() < 1e-6, "hessian ({k}, {j}): {} {fd}", e.hessian(k, j));
|
||||
}
|
||||
}
|
||||
// A rigid translation changes nothing: the gradient sums to zero per direction.
|
||||
for c in 0..3 {
|
||||
let sum: f64 = (0..8).map(|n| d.g[3 * n + c]).sum();
|
||||
assert!(sum.abs() < 1e-13, "{sum}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edges_integrate_their_overlap_and_measure() {
|
||||
// Secondary edge along +x at y = 0 (normal −y), main edge 0.1 below, half overlapping.
|
||||
let s = [[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
|
||||
let m = [[2.5, -0.1, 0.0], [1.5, -0.1, 0.0]];
|
||||
for (extent, at) in [(Extent::Thickness(3.0), (3.0, 0.0)), (Extent::Revolved, (0.0, 2.0 * std::f64::consts::PI))] {
|
||||
let at = |x: f64| at.0 + at.1 * x;
|
||||
let (mut d, mut g, mut area) = ([0.0; 2], 0.0, 0.0);
|
||||
integrate_edges(&s, &m, extent, |p| {
|
||||
area += p.weight;
|
||||
g += p.weight * p.gap();
|
||||
for a in 0..2 {
|
||||
d[a] += p.weight * p.secondary[a];
|
||||
}
|
||||
assert!((p.xm[1] + 0.1).abs() < 1e-15 && (p.xm[0] - p.xs[0]).abs() < 1e-15);
|
||||
})
|
||||
.unwrap();
|
||||
// ∫_{1.5}^{2} m(x) dx and ∫ (x − 1) m(x) dx (the second corner's share).
|
||||
let simpson = |f: &dyn Fn(f64) -> f64| (f(1.5) + 4.0 * f(1.75) + f(2.0)) * 0.5 / 6.0;
|
||||
assert!((area - simpson(&|x| at(x))).abs() < 1e-12, "{area}");
|
||||
assert!((d[1] - simpson(&|x| (x - 1.0) * at(x))).abs() < 1e-12);
|
||||
assert!((g - 0.1 * area).abs() < 1e-12);
|
||||
}
|
||||
// The area vector points along −y with the edge's measure.
|
||||
let v = edge_area_vector(&s, Extent::Revolved).unwrap();
|
||||
assert!(v[0].abs() < 1e-15 && (v[1] + 2.0 * std::f64::consts::PI * 1.5).abs() < 1e-12);
|
||||
// Apart, perpendicular or degenerate: nothing.
|
||||
let mut count = 0;
|
||||
integrate_edges(&s, &[[3.0, -0.1, 0.0], [4.0, -0.1, 0.0]], Extent::Revolved, |_| count += 1).unwrap();
|
||||
integrate_edges(&s, &[[1.5, -0.1, 0.0], [1.5, -1.0, 0.0]], Extent::Revolved, |_| count += 1).unwrap();
|
||||
assert_eq!(count, 0);
|
||||
assert!(integrate_edges(&[[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], &m, Extent::Revolved, |_| {}).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_derivatives_are_those_of_the_integral() {
|
||||
let base: [f64; VARS] = std::array::from_fn(|k| match k {
|
||||
0 => 1.0,
|
||||
1 => 0.02,
|
||||
3 => 2.1,
|
||||
4 => -0.05,
|
||||
12 => 2.4,
|
||||
13 => -0.12,
|
||||
15 => 1.3,
|
||||
16 => -0.08,
|
||||
_ => 0.0,
|
||||
});
|
||||
let gap = |x: &[Hyper; VARS]| {
|
||||
let s = [[x[0], x[1], x[2]], [x[3], x[4], x[5]]];
|
||||
let m = [[x[12], x[13], x[14]], [x[15], x[16], x[17]]];
|
||||
let mut g = Hyper::cst(0.0);
|
||||
integrate_edges(&s, &m, Extent::Revolved, |p| g = g + p.weight * p.secondary[1] * p.gap()).unwrap();
|
||||
g
|
||||
};
|
||||
let value = |x: [f64; VARS]| gap(&std::array::from_fn(|k| Hyper::cst(x[k]))).v;
|
||||
let e = gap(&std::array::from_fn(|k| Hyper::var(base[k], k)));
|
||||
let h = 1e-6;
|
||||
for k in [0, 1, 3, 4, 12, 13, 15, 16] {
|
||||
let (mut p, mut m) = (base, base);
|
||||
p[k] += h;
|
||||
m[k] -= h;
|
||||
let fd = (value(p) - value(m)) / (2.0 * h);
|
||||
assert!((e.g[k] - fd).abs() < 1e-7, "gradient {k}: {} {fd}", e.g[k]);
|
||||
for j in [0, 1, 3, 4, 12, 13, 15, 16] {
|
||||
let (mut pp, mut pm, mut mp, mut mm) = (p, p, m, m);
|
||||
pp[j] += h;
|
||||
pm[j] -= h;
|
||||
mp[j] += h;
|
||||
mm[j] -= h;
|
||||
let fd = (value(pp) - value(pm) - value(mp) + value(mm)) / (4.0 * h * h);
|
||||
assert!((e.hessian(k, j) - fd).abs() < 2e-3, "hessian ({k}, {j}): {} {fd}", e.hessian(k, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_faces_give_nothing() {
|
||||
let s = square(0.0, 0.0, 1.0, 0.0, true);
|
||||
let collapsed = [[0.0; 3], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
|
||||
assert!(integrate(&collapsed, &s, |_| {}).is_none());
|
||||
assert!(integrate(&s, &s[..2], |_| {}).is_none());
|
||||
let mut count = 0;
|
||||
integrate(&s, &[[f64::NAN, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], |_| count += 1);
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
}
|
||||
+167
-7
@@ -35,6 +35,15 @@
|
||||
//! leaves the plane case unchanged (its mean is zero) and makes the axisymmetric element pass the
|
||||
//! patch test.
|
||||
//!
|
||||
//! **Hybrid** (mixed displacement–pressure, plane strain and axisymmetric quad4, quad8 and tri6):
|
||||
//! one constant pressure per element, the mean dilatation method (J. C. Nagtegaal, D. M. Parks,
|
||||
//! J. R. Rice, *On numerically accurate finite element solutions in the fully plastic range*,
|
||||
//! CMAME 4(2):153–177, 1974; T. J. R. Hughes, *The Finite Element Method*, Dover 2000, §4.5 for
|
||||
//! the axisymmetric B-bar): the volumetric strain, hoop part included, is replaced by its volume
|
||||
//! mean, so K = ∫ Bᵀ D_dev B dV + κ g gᵀ / V with g = ∫ Bᵀ m dV, m = (1, 1, 1, 0). The linear
|
||||
//! triangle is refused (one pressure per element still locks it), plane stress too (σ_zz = 0
|
||||
//! leaves no volume constraint to relax). The finite-strain form is [`crate::finite`]'s.
|
||||
//!
|
||||
//! On the axis (r = 0) the hoop strain u/r is evaluated only at integration points, which lie
|
||||
//! inside the element (r > 0) for every element with some node off the axis; the solver holds
|
||||
//! the radial displacement of nodes on the axis at zero, as symmetry requires.
|
||||
@@ -165,11 +174,24 @@ pub fn plane_type(kind: ElementKind) -> Result<&'static ElementType> {
|
||||
.ok_or_else(|| Error::NotSupported(format!("{} elements in a 2D section (2D sections take tri3, tri6, quad4 and quad8)", kind.name())))
|
||||
}
|
||||
|
||||
/// The 2D element kinds with a hybrid (constant pressure) formulation.
|
||||
const HYBRID_PRESSURES: [ElementKind; 3] = [ElementKind::Quad4, ElementKind::Quad8, ElementKind::Tri6];
|
||||
|
||||
/// The formulation a 2D section asks for, if this element type has it.
|
||||
pub fn formulation(kind: ElementKind, f: &fieldcraft_model::Formulation) -> Result<Formulation> {
|
||||
let ty = plane_type(kind)?;
|
||||
if f.hybrid {
|
||||
return Err(Error::NotSupported("hybrid (mixed displacement–pressure) 2D elements".into()));
|
||||
if f.incompatible_modes || f.integration == fieldcraft_model::Integration::Reduced {
|
||||
return Err(Error::NotSupported("hybrid 2D elements with incompatible modes or reduced integration".into()));
|
||||
}
|
||||
return if HYBRID_PRESSURES.contains(&kind) {
|
||||
Ok(Formulation::Hybrid { pressures: 1 })
|
||||
} else {
|
||||
Err(Error::NotSupported(format!(
|
||||
"hybrid 2D {} elements (one pressure per element still locks the linear triangle; use quad4, quad8 or tri6)",
|
||||
kind.name()
|
||||
)))
|
||||
};
|
||||
}
|
||||
if f.incompatible_modes {
|
||||
return if ty.incompatible_modes {
|
||||
@@ -270,11 +292,14 @@ impl<'a> Plane<'a> {
|
||||
Formulation::Full => true,
|
||||
Formulation::Reduced => ty.reduced.is_some(),
|
||||
Formulation::IncompatibleModes => ty.incompatible_modes,
|
||||
Formulation::Hybrid { .. } => false,
|
||||
Formulation::Hybrid { pressures } => pressures == 1 && HYBRID_PRESSURES.contains(&kind),
|
||||
};
|
||||
if !ok {
|
||||
return Err(Error::NotSupported(format!("{formulation:?} formulation of 2D {} elements", kind.name())));
|
||||
}
|
||||
if matches!(formulation, Formulation::Hybrid { .. }) && matches!(theory, Theory::PlaneStress { .. }) {
|
||||
return Err(Error::NotSupported("hybrid plane stress elements (σ_zz = 0 leaves no volume constraint; use full integration)".into()));
|
||||
}
|
||||
Ok(Plane { ty, xyz, formulation, theory })
|
||||
}
|
||||
|
||||
@@ -282,7 +307,8 @@ impl<'a> Plane<'a> {
|
||||
/// modes it condenses out.
|
||||
pub fn dofs(&self) -> ElementDofs {
|
||||
let incompatible_modes = if self.formulation == Formulation::IncompatibleModes { INCOMPATIBLE_DOFS } else { 0 };
|
||||
ElementDofs { node: DofSet::PLANE, internal: Internal { incompatible_modes, pressure: 0 } }
|
||||
let pressure = if let Formulation::Hybrid { pressures } = self.formulation { pressures } else { 0 };
|
||||
ElementDofs { node: DofSet::PLANE, internal: Internal { incompatible_modes, pressure } }
|
||||
}
|
||||
|
||||
/// The stiffness integration rule.
|
||||
@@ -411,6 +437,13 @@ impl<'a> Plane<'a> {
|
||||
|
||||
/// The stiffness matrix, 2n × 2n, degrees of freedom ordered node by node (u, v).
|
||||
pub fn stiffness(&self, d: &Elasticity) -> Result<Matrix> {
|
||||
if let Some(dev) = self.deviatoric(d)? {
|
||||
// K_dev + κ g gᵀ / V.
|
||||
let (mut k, points, _) = self.full_stiffness(&dev)?;
|
||||
let mixed = self.mixed(d, &points, None)?;
|
||||
add_outer(&mut k, &mixed.g, mixed.bulk / mixed.volume);
|
||||
return Ok(k);
|
||||
}
|
||||
let d2 = self.theory.elasticity(d)?;
|
||||
let (full, _, modes) = self.full_stiffness(&d2)?;
|
||||
if modes.is_none() {
|
||||
@@ -433,6 +466,34 @@ impl<'a> Plane<'a> {
|
||||
Ok(k)
|
||||
}
|
||||
|
||||
/// A hybrid element's deviatoric elasticity in the theory's form; `None` for the other
|
||||
/// formulations.
|
||||
fn deviatoric(&self, d: &Elasticity) -> Result<Option<Elasticity2>> {
|
||||
if !matches!(self.formulation, Formulation::Hybrid { .. }) {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(self.theory.elasticity(&crate::hybrid::split(d)?.0)?))
|
||||
}
|
||||
|
||||
/// A hybrid element's parts over its stiffness `points`: the bulk modulus, g = ∫ Bᵀ m dV
|
||||
/// (2n), the volume and, with a thermal strain, ∫ tr ε_th dV.
|
||||
pub(crate) fn mixed(&self, d: &Elasticity, points: &[(Point, At)], thermal: Option<&ThermalStrain>) -> Result<Mixed2> {
|
||||
let bulk = crate::hybrid::split(d)?.1;
|
||||
let (mut g, mut volume, mut thermal_volume) = (vec![0.0; 2 * self.ty.nodes], 0.0, 0.0);
|
||||
for (_, a) in points {
|
||||
volume += a.dv;
|
||||
divergence(&mut g, a.g.get(..self.ty.nodes).unwrap_or_default(), a.dv);
|
||||
if let Some(t) = thermal {
|
||||
let e = Self::thermal_strain(t, a.shape.values());
|
||||
thermal_volume += a.dv * (e[0] + e[1] + e[2]);
|
||||
}
|
||||
}
|
||||
if !(volume > 0.0) {
|
||||
return Err(self.degenerate());
|
||||
}
|
||||
Ok(Mixed2 { bulk, g, volume, thermal_volume })
|
||||
}
|
||||
|
||||
/// The consistent mass matrix per displacement component, n × n: m_ij = ∫ ρ Nᵢ Nⱼ dV.
|
||||
pub fn mass(&self, density: f64) -> Result<Matrix> {
|
||||
let n = self.ty.nodes;
|
||||
@@ -518,8 +579,18 @@ impl<'a> Plane<'a> {
|
||||
/// strains the hoop direction too); incompatible modes are condensed as in the stiffness.
|
||||
pub fn thermal_load(&self, d: &Elasticity, thermal: &ThermalStrain) -> Result<Vec<[f64; 2]>> {
|
||||
thermal.check(self.ty.nodes)?;
|
||||
let d2 = self.theory.elasticity(d)?;
|
||||
let n = 2 * self.ty.nodes;
|
||||
if let Some(dev) = self.deviatoric(d)? {
|
||||
// ∫ Bᵀ D_dev ε_th dV + κ g (∫ tr ε_th dV) / V.
|
||||
let points = self.points()?;
|
||||
let mixed = self.mixed(d, &points, Some(thermal))?;
|
||||
let (mut fu, _) = Self::thermal_forces(&dev, thermal, &points, None, self.ty.nodes);
|
||||
for (f, g) in fu.iter_mut().zip(&mixed.g) {
|
||||
*f += mixed.bulk * g * mixed.thermal_volume / mixed.volume;
|
||||
}
|
||||
return Ok(fu.as_chunks::<2>().0.to_vec());
|
||||
}
|
||||
let d2 = self.theory.elasticity(d)?;
|
||||
let mut fu = if self.formulation == Formulation::IncompatibleModes {
|
||||
let (full, points, modes) = self.full_stiffness(&d2)?;
|
||||
let (mut fu, mut fa) = Self::thermal_forces(&d2, thermal, &points, modes.as_ref(), self.ty.nodes);
|
||||
@@ -549,9 +620,21 @@ impl<'a> Plane<'a> {
|
||||
if let Some(t) = thermal {
|
||||
t.check(self.ty.nodes)?;
|
||||
}
|
||||
let d2 = self.theory.elasticity(d)?;
|
||||
let deviatoric = self.deviatoric(d)?;
|
||||
let d2 = match deviatoric {
|
||||
Some(dev) => dev,
|
||||
None => self.theory.elasticity(d)?,
|
||||
};
|
||||
let (full, points, modes) =
|
||||
if self.formulation == Formulation::IncompatibleModes { self.full_stiffness(&d2)? } else { (Matrix::zeros(0), self.points()?, None) };
|
||||
// Hybrid: the element's pressure κ (gᵀ u − ∫ tr ε_th dV) / V.
|
||||
let pressure = if deviatoric.is_some() {
|
||||
let m = self.mixed(d, &points, thermal)?;
|
||||
let gu: f64 = m.g.iter().zip(u.iter().flatten()).map(|(g, x)| g * x).sum();
|
||||
m.bulk * (gu - m.thermal_volume) / m.volume
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// Incompatible modes: K_aa α = f_a − K_au u (f_a the thermal load on the modes).
|
||||
let alpha = if modes.is_some() {
|
||||
let n = 2 * self.ty.nodes;
|
||||
@@ -595,10 +678,11 @@ impl<'a> Plane<'a> {
|
||||
};
|
||||
}
|
||||
// Plane stress: D̃ has no zz row or column, so the εzz just recovered adds nothing.
|
||||
let s: [f64; 4] = match eth {
|
||||
let mut s: [f64; 4] = match eth {
|
||||
Some(t) => std::array::from_fn(|r| (0..4).map(|c| d2[r][c] * (e[c] - t[c])).sum()),
|
||||
None => std::array::from_fn(|r| (0..4).map(|c| d2[r][c] * e[c]).sum()),
|
||||
};
|
||||
s.iter_mut().take(3).for_each(|v| *v += pressure);
|
||||
out.push(PointStress { xyz: a.x, strain: [e[0], e[1], e[2], e[3], 0.0, 0.0], stress: [s[0], s[1], s[2], s[3], 0.0, 0.0] });
|
||||
}
|
||||
Ok(out)
|
||||
@@ -614,6 +698,32 @@ impl<'a> Plane<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The parts of a hybrid 2D element ([`Plane::mixed`]).
|
||||
pub(crate) struct Mixed2 {
|
||||
bulk: f64,
|
||||
pub(crate) g: Vec<f64>,
|
||||
volume: f64,
|
||||
thermal_volume: f64,
|
||||
}
|
||||
|
||||
/// g += w Bᵀ m over strain columns `cols` (u takes ∂N/∂x and the hoop N/r, v takes ∂N/∂y): the
|
||||
/// divergence of each nodal motion, which the hybrid elements' volume constraint acts on.
|
||||
pub(crate) fn divergence(g: &mut [f64], cols: &[Column], w: f64) {
|
||||
for (pair, col) in g.as_chunks_mut::<2>().0.iter_mut().zip(cols) {
|
||||
pair[0] += w * (col[0] + col[2]);
|
||||
pair[1] += w * col[1];
|
||||
}
|
||||
}
|
||||
|
||||
/// k += c g gᵀ.
|
||||
pub(crate) fn add_outer(k: &mut Matrix, g: &[f64], c: f64) {
|
||||
for (i, gi) in g.iter().enumerate() {
|
||||
for (j, gj) in g.iter().enumerate() {
|
||||
k.add(i, j, c * gi * gj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `size` × `size` block of `m` starting at (`row`, `col`).
|
||||
fn sub_block(m: &Matrix, row: usize, col: usize, size: usize) -> Matrix {
|
||||
let mut out = Matrix::zeros(size);
|
||||
@@ -661,6 +771,53 @@ mod tests {
|
||||
|
||||
const THEORIES: [Theory; 3] = [Theory::PlaneStress { thickness: 0.7 }, Theory::PlaneStrain { thickness: 1.0 }, Theory::Axisymmetric];
|
||||
|
||||
#[test]
|
||||
fn hybrid_elements_reproduce_a_uniform_strain_and_do_not_lock() {
|
||||
// u = a x, v = b y: a uniform strain (axisymmetric: the hoop strain a too), whose
|
||||
// stress the mean dilatation reproduces exactly at every point.
|
||||
let d = isotropic(1000.0, 0.4999).unwrap();
|
||||
let (a, b) = (0.002, -0.003);
|
||||
for theory in [Theory::PlaneStrain { thickness: 1.5 }, Theory::Axisymmetric] {
|
||||
for kind in [ElementKind::Quad4, ElementKind::Quad8, ElementKind::Tri6] {
|
||||
let x = element(kind);
|
||||
let plane = Plane::new(kind, &x, Formulation::Hybrid { pressures: 1 }, theory).unwrap();
|
||||
let u: Vec<[f64; 2]> = x.iter().map(|p| [a * p[0], b * p[1]]).collect();
|
||||
let hoop = if theory == Theory::Axisymmetric { a } else { 0.0 };
|
||||
let e = [a, b, hoop, 0.0, 0.0, 0.0];
|
||||
let exact = crate::material::stress(&d, &e);
|
||||
for p in plane.stresses(&d, &u).unwrap() {
|
||||
for c in 0..4 {
|
||||
assert!((p.stress[c] - exact[c]).abs() < 1e-9 * exact[0].abs().max(1.0), "{theory:?} {kind:?} {c}: {:?} {exact:?}", p.stress);
|
||||
}
|
||||
}
|
||||
// K u is the forces of that stress: the stiffness agrees with the stresses.
|
||||
let k = plane.stiffness(&d).unwrap();
|
||||
let flat: Vec<f64> = u.iter().flatten().copied().collect();
|
||||
let full = Plane::new(kind, &x, Formulation::Full, theory).unwrap().stiffness(&d).unwrap();
|
||||
let (ku, kf): (Vec<f64>, Vec<f64>) = (0..flat.len())
|
||||
.map(|i| (0..flat.len()).fold((0.0, 0.0), |(s, t), j| (s + k.get(i, j) * flat[j], t + full.get(i, j) * flat[j])))
|
||||
.unzip();
|
||||
for (p, q) in ku.iter().zip(&kf) {
|
||||
assert!((p - q).abs() < 1e-8 * kf.iter().fold(0.0f64, |m, v| m.max(v.abs())), "{p} {q}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Nearly incompressible: the hybrid element's volume is one constraint, so a motion that
|
||||
// keeps the element's volume (orthogonal to g) costs only shear, where the full element's
|
||||
// four point constraints lock it.
|
||||
let x = element(ElementKind::Quad4);
|
||||
let hybrid = Plane::new(ElementKind::Quad4, &x, Formulation::Hybrid { pressures: 1 }, Theory::PlaneStrain { thickness: 1.0 }).unwrap();
|
||||
let g = hybrid.mixed(&d, &hybrid.points().unwrap(), None).unwrap().g;
|
||||
let raw: Vec<f64> = (0..8).map(|i| ((i * 7 % 5) as f64 - 2.0) * 0.01).collect();
|
||||
let along = raw.iter().zip(&g).map(|(a, b)| a * b).sum::<f64>() / g.iter().map(|v| v * v).sum::<f64>();
|
||||
let shear: Vec<f64> = raw.iter().zip(&g).map(|(a, b)| a - along * b).collect();
|
||||
let energy = |f: Formulation| {
|
||||
let k = Plane::new(ElementKind::Quad4, &x, f, Theory::PlaneStrain { thickness: 1.0 }).unwrap().stiffness(&d).unwrap();
|
||||
(0..8).map(|i| (0..8).map(|j| shear[i] * k.get(i, j) * shear[j]).sum::<f64>()).sum::<f64>()
|
||||
};
|
||||
assert!(energy(Formulation::Hybrid { pressures: 1 }) < 0.01 * energy(Formulation::Full));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rigid_motion_strains_nothing_and_matrices_are_symmetric() {
|
||||
let d = isotropic(1000.0, 0.3).unwrap();
|
||||
@@ -783,6 +940,9 @@ mod tests {
|
||||
let on_axis = sq.map(|p| [p[0] - 1.0, p[1], 0.0]);
|
||||
let e = Plane::new(ElementKind::Quad4, &on_axis, Formulation::IncompatibleModes, Theory::Axisymmetric).unwrap();
|
||||
assert!(e.stiffness(&d).is_ok());
|
||||
assert!(formulation(ElementKind::Quad4, &fieldcraft_model::Formulation { hybrid: true, ..Default::default() }).is_err());
|
||||
let hybrid = fieldcraft_model::Formulation { hybrid: true, ..Default::default() };
|
||||
assert_eq!(formulation(ElementKind::Quad4, &hybrid).unwrap(), Formulation::Hybrid { pressures: 1 });
|
||||
assert!(formulation(ElementKind::Tri3, &hybrid).is_err());
|
||||
assert!(formulation(ElementKind::Quad4, &fieldcraft_model::Formulation { incompatible_modes: true, ..hybrid }).is_err());
|
||||
}
|
||||
}
|
||||
@@ -388,13 +388,16 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
// Contacts: their surfaces are element faces of solids.
|
||||
// Contacts: their surfaces are element faces of solids, or edges of 2D elements.
|
||||
for (name, c) in &self.contacts {
|
||||
for set in c.surfaces.sets() {
|
||||
let faces = &set_of(set).faces;
|
||||
let solid = |f: &crate::ElementFace| self.elements.get(&f.element).is_some_and(|e| e.kind.dimension() == 3);
|
||||
if faces.is_empty() {
|
||||
d.error(format!("contact `{name}`"), format!("set `{set}` has no element faces (a contact surface is a set of faces)"));
|
||||
if faces.is_empty() && resolved.planar_edges(set).is_empty() {
|
||||
d.error(
|
||||
format!("contact `{name}`"),
|
||||
format!("set `{set}` has no element faces (a contact surface is a set of faces, or of 2D element edges)"),
|
||||
);
|
||||
} else if !faces.iter().all(solid) {
|
||||
d.error(
|
||||
format!("contact `{name}`"),
|
||||
@@ -404,6 +407,22 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
// Ties: their surfaces are element faces of solids too.
|
||||
for (name, t) in &self.ties {
|
||||
for set in [&t.main, &t.secondary] {
|
||||
let faces = &set_of(set).faces;
|
||||
let solid = |f: &crate::ElementFace| self.elements.get(&f.element).is_some_and(|e| e.kind.dimension() == 3);
|
||||
if faces.is_empty() {
|
||||
d.error(format!("tie `{name}`"), format!("set `{set}` has no element faces (a tie surface is a set of faces)"));
|
||||
} else if !faces.iter().all(solid) {
|
||||
d.error(
|
||||
format!("tie `{name}`"),
|
||||
format!("set `{set}` has faces of elements that are not solids (ties bond the faces of solid elements so far)"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nodes with rotational degrees of freedom: those of shells and beams, and reference points
|
||||
// (worked out only when a boundary condition or load needs it).
|
||||
let rotational = std::cell::OnceCell::new();
|
||||
|
||||
+59
-16
@@ -1,26 +1,32 @@
|
||||
//! Contact: which surfaces may touch, and the friction between them.
|
||||
//!
|
||||
//! A contact is a model definition, active in every step that can take it (explicit dynamics;
|
||||
//! the implicit solver refuses contact by name for now). Its surfaces are sets with element
|
||||
//! faces, so they survive remeshing like a pressure load does:
|
||||
//! A contact is a model definition, active in every step that can take it (explicit dynamics,
|
||||
//! and nonlinear static steps; the linear analyses refuse contact by name). Its
|
||||
//! surfaces are sets with element
|
||||
//! faces (in a 2D model, element edges), so they survive remeshing like a pressure load does:
|
||||
//! - a **pair**: the faces of `main` against those of `secondary`, both ways (each surface's
|
||||
//! nodes are kept out of the other's faces, so neither surface is privileged);
|
||||
//! - **general** contact: every face against every other face of one surface (single-surface or
|
||||
//! self contact, for folding and buckling), or of every exterior face of the model's solid
|
||||
//! elements when no surface is named.
|
||||
//! elements (in a 2D model every exterior edge of its 2D elements) when no surface is named.
|
||||
//!
|
||||
//! **Friction** is Coulomb's law with a coefficient that falls from its static to its kinetic
|
||||
//! value with the slip rate γ̇: μ = μ_k + (μ_s − μ_k) e^{−d γ̇} (the exponential decay of
|
||||
//! E. Rabinowicz's observations, as LS-DYNA's and Abaqus's exponential decay friction write it).
|
||||
//! A static step has no slip rate: its nodes take the static coefficient while they stick and the
|
||||
//! kinetic one once they slip.
|
||||
//! The same friction applies to the planar rigid walls of an explicit step
|
||||
//! ([`crate::RigidWall`]).
|
||||
//!
|
||||
//! **Penalty.** Explicit contact pushes penetrating nodes back with a penalty spring whose
|
||||
//! stiffness the solver chooses from the masses and the stable time increment, the stiffest
|
||||
//! that leaves the increment alone; `penalty_scale` (at most 1) softens it.
|
||||
//! that leaves the increment alone; `penalty_scale` (at most 1) softens it. Nonlinear static
|
||||
//! steps enforce a pair's mortar pressure by an augmented Lagrangian whose penalty the solver
|
||||
//! chooses from the elements under the secondary surface; `penalty_scale` scales it too.
|
||||
//!
|
||||
//! **Initial penetration.** Nodes of a surface that start inside the other surface are either
|
||||
//! moved onto it before the analysis, strain-free (`adjust`, the default, each one reported), or
|
||||
//! moved onto it before the analysis, strain-free (`adjust`, the default, each one reported;
|
||||
//! nonlinear static steps take the overlap out of their gap instead), or
|
||||
//! the analysis is refused naming the first node, its depth and the face it is behind
|
||||
//! (`refuse`).
|
||||
|
||||
@@ -213,27 +219,47 @@ impl Model {
|
||||
remove_existing(&mut self.contacts, "contact", name).map(|_| ())
|
||||
}
|
||||
|
||||
/// The exterior edges of the 2D elements (the edges no other 2D element shares), in
|
||||
/// element order: the boundary of a 2D model, which general contact takes by default.
|
||||
pub fn exterior_edges(&self) -> Vec<crate::ElementEdge> {
|
||||
self.unshared(2, |kind| kind.edges(), 2, |element, i| crate::ElementEdge { element, edge: i })
|
||||
}
|
||||
|
||||
/// The exterior faces of the solid elements (the faces no other solid element shares), in
|
||||
/// element and face order: the surface of general contact without a named set.
|
||||
/// element order.
|
||||
pub fn exterior_faces(&self) -> Vec<crate::ElementFace> {
|
||||
let mut keyed: Vec<([u32; 4], crate::ElementFace)> = Vec::new();
|
||||
for (label, e) in self.elements.iter().filter(|(_, e)| e.kind.dimension() == 3) {
|
||||
for (i, local) in e.kind.faces().iter().enumerate() {
|
||||
// A face is known by its sorted corner nodes (three or four).
|
||||
let corners = if local.len() == 6 || local.len() == 3 { 3 } else { 4 };
|
||||
// A face is known by its sorted corner nodes (three or four).
|
||||
self.unshared(3, |kind| kind.faces(), 4, |element, face| crate::ElementFace { element, face })
|
||||
}
|
||||
|
||||
/// The sides (`sides` of each element kind) of the elements of `dimension` that no other
|
||||
/// such element shares, each known by its sorted corners (at most `corners`; triangles of
|
||||
/// quadratic kinds by their first three), made by `make` from the element and the 1-based
|
||||
/// side number, sorted.
|
||||
fn unshared<T: Ord>(
|
||||
&self,
|
||||
dimension: u8,
|
||||
sides: impl Fn(crate::ElementKind) -> &'static [&'static [usize]],
|
||||
corners: usize,
|
||||
make: impl Fn(u32, u8) -> T,
|
||||
) -> Vec<T> {
|
||||
let mut keyed: Vec<([u32; 4], u32, u8)> = Vec::new();
|
||||
for (label, e) in self.elements.iter().filter(|(_, e)| e.kind.dimension() == dimension) {
|
||||
for (i, local) in sides(e.kind).iter().enumerate() {
|
||||
let count = if local.len() == 6 || local.len() == 3 { 3 } else { 4 }.min(corners);
|
||||
let mut key = [u32::MAX; 4];
|
||||
for (k, n) in key.iter_mut().zip(local.iter().take(corners)) {
|
||||
for (k, n) in key.iter_mut().zip(local.iter().take(count)) {
|
||||
*k = e.nodes.get(*n).copied().unwrap_or(u32::MAX);
|
||||
}
|
||||
key.sort_unstable();
|
||||
keyed.push((key, crate::ElementFace { element: *label, face: i as u8 + 1 }));
|
||||
keyed.push((key, *label, i as u8 + 1));
|
||||
}
|
||||
}
|
||||
keyed.sort_unstable();
|
||||
let mut out: Vec<crate::ElementFace> = keyed
|
||||
let mut out: Vec<T> = keyed
|
||||
.chunk_by(|a, b| a.0 == b.0)
|
||||
.filter_map(|run| match run {
|
||||
[(_, face)] => Some(*face),
|
||||
[(_, element, side)] => Some(make(*element, *side)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
@@ -350,6 +376,23 @@ mod tests {
|
||||
assert!(Model::default().exterior_faces().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exterior_edges_are_those_of_one_2d_element() {
|
||||
// Two quads sharing an edge and a triangle on the second's top edge.
|
||||
let mut m = Model::default();
|
||||
for (i, xy) in [[0., 0.], [1., 0.], [2., 0.], [0., 1.], [1., 1.], [2., 1.], [1.5, 2.]].iter().enumerate() {
|
||||
m.set_node(i as u32 + 1, [xy[0], xy[1], 0.0]).unwrap();
|
||||
}
|
||||
m.set_element(1, crate::ElementKind::Quad4, vec![1, 2, 5, 4]).unwrap();
|
||||
m.set_element(2, crate::ElementKind::Quad4, vec![2, 3, 6, 5]).unwrap();
|
||||
m.set_element(3, crate::ElementKind::Tri3, vec![5, 6, 7]).unwrap();
|
||||
let edges = m.exterior_edges();
|
||||
let of = |e: u32| edges.iter().filter(|f| f.element == e).map(|f| f.edge).collect::<Vec<_>>();
|
||||
assert_eq!(of(1), [1, 3, 4]);
|
||||
assert_eq!(of(2), [1, 2]);
|
||||
assert_eq!(of(3), [2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_model_holds_a_bounded_number_of_contacts() {
|
||||
let mut m = model();
|
||||
|
||||
+12
-1
@@ -45,6 +45,7 @@ pub mod nodal;
|
||||
pub mod sections;
|
||||
pub mod sets;
|
||||
pub mod steps;
|
||||
pub mod tie;
|
||||
pub mod units;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
@@ -71,6 +72,7 @@ pub use steps::{
|
||||
MAX_LOAD_PATH_POINTS, MAX_NONLINEAR_INCREMENTS, MAX_NONLINEAR_ITERATIONS, MassScaling, NonlinearStatic, OutputVariable, Procedure, RigidWall,
|
||||
Step, StressRate, Tolerances, Transient,
|
||||
};
|
||||
pub use tie::Tie;
|
||||
pub use units::UnitSystem;
|
||||
|
||||
/// The largest node or element label. Nastran's GRID and element ids stop at 99 999 999 (eight
|
||||
@@ -128,6 +130,8 @@ pub struct Model {
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
contacts: BTreeMap<String, Contact>,
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
ties: BTreeMap<String, Tie>,
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
masses: BTreeMap<String, PointMass>,
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
springs: BTreeMap<String, Spring>,
|
||||
@@ -164,6 +168,8 @@ struct ModelData {
|
||||
#[serde(default)]
|
||||
contacts: BTreeMap<String, Contact>,
|
||||
#[serde(default)]
|
||||
ties: BTreeMap<String, Tie>,
|
||||
#[serde(default)]
|
||||
masses: BTreeMap<String, PointMass>,
|
||||
#[serde(default)]
|
||||
springs: BTreeMap<String, Spring>,
|
||||
@@ -225,6 +231,9 @@ impl TryFrom<ModelData> for Model {
|
||||
for (n, v) in d.contacts {
|
||||
m.add_contact(&n, v)?;
|
||||
}
|
||||
for (n, v) in d.ties {
|
||||
m.add_tie(&n, v)?;
|
||||
}
|
||||
for (n, v) in d.masses {
|
||||
m.add_mass(&n, v)?;
|
||||
}
|
||||
@@ -341,6 +350,7 @@ impl Model {
|
||||
reference_points: self.reference_points.clone(),
|
||||
couplings: self.couplings.clone(),
|
||||
contacts: self.contacts.clone(),
|
||||
ties: self.ties.clone(),
|
||||
masses: self.masses.clone(),
|
||||
springs: self.springs.clone(),
|
||||
boundary_conditions: self.boundary_conditions.clone(),
|
||||
@@ -351,7 +361,7 @@ impl Model {
|
||||
}
|
||||
|
||||
/// Counts of each kind of definition, for summaries.
|
||||
pub fn definition_counts(&self) -> [(&'static str, usize); 12] {
|
||||
pub fn definition_counts(&self) -> [(&'static str, usize); 13] {
|
||||
[
|
||||
("sets", self.sets.len()),
|
||||
("materials", self.materials.len()),
|
||||
@@ -359,6 +369,7 @@ impl Model {
|
||||
("reference_points", self.reference_points.len()),
|
||||
("couplings", self.couplings.len()),
|
||||
("contacts", self.contacts.len()),
|
||||
("ties", self.ties.len()),
|
||||
("masses", self.masses.len()),
|
||||
("springs", self.springs.len()),
|
||||
("boundary_conditions", self.boundary_conditions.len()),
|
||||
|
||||
@@ -278,8 +278,9 @@ impl Formulation {
|
||||
if !kind.is_continuum() && self.incompatible_modes {
|
||||
return Err(Error::Invalid(format!("incompatible modes are for solid and 2D sections, not {}", kind.name())));
|
||||
}
|
||||
if kind.dimension() != 3 && self.hybrid {
|
||||
return Err(Error::Invalid(format!("hybrid formulations are for solid sections, not {}", kind.name())));
|
||||
let volume_constrained = kind.dimension() == 3 || matches!(kind, SectionKind::PlaneStrain { .. } | SectionKind::Axisymmetric {});
|
||||
if self.hybrid && !volume_constrained {
|
||||
return Err(Error::Invalid(format!("hybrid formulations are for solid, plane strain and axisymmetric sections, not {}", kind.name())));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -391,7 +392,10 @@ mod tests {
|
||||
let im = Formulation { incompatible_modes: true, ..Formulation::default() };
|
||||
im.validate_for(&stress).unwrap();
|
||||
im.validate_for(&axi).unwrap();
|
||||
assert!(Formulation { hybrid: true, ..Formulation::default() }.validate_for(&strain).is_err());
|
||||
let hybrid = Formulation { hybrid: true, ..Formulation::default() };
|
||||
hybrid.validate_for(&strain).unwrap();
|
||||
hybrid.validate_for(&axi).unwrap();
|
||||
assert!(hybrid.validate_for(&stress).is_err());
|
||||
assert!(im.validate_for(&SectionKind::Shell { thickness: 1.0, offset: 0.0, points: 5 }).is_err());
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,7 @@ impl Model {
|
||||
users.extend(self.sections.iter().filter(|(_, s)| s.set == set).map(|(n, _)| format!("section `{n}`")));
|
||||
users.extend(self.couplings.iter().filter(|(_, c)| c.set == set).map(|(n, _)| format!("coupling `{n}`")));
|
||||
users.extend(self.contact_users(set));
|
||||
users.extend(self.tie_users(set));
|
||||
users.extend(self.discrete_users(|t| t.set() == Some(set)));
|
||||
users
|
||||
.extend(self.boundary_conditions.iter().filter(|(_, b)| b.target().set() == Some(set)).map(|(n, _)| format!("boundary condition `{n}`")));
|
||||
|
||||
@@ -155,6 +155,13 @@ pub struct NonlinearStatic {
|
||||
/// lengths in proportion. `None`: load control.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub arc_length: Option<ArcLength>,
|
||||
/// Viscous stabilization of local instabilities (wrinkling, folding, snap-through of a
|
||||
/// part under displacement control): damping forces α D (u − uₙ)/Δt against each
|
||||
/// increment's motion, with D the diagonal of the step's first tangent and Δt the increment
|
||||
/// as a fraction of the step, so α is about the fraction of the forces the damping takes
|
||||
/// where the motion is smooth. `None`: none.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stabilization: Option<f64>,
|
||||
}
|
||||
|
||||
/// The constraint of an arc-length step.
|
||||
@@ -242,6 +249,7 @@ impl Default for NonlinearStatic {
|
||||
load_path: None,
|
||||
imperfection: None,
|
||||
arc_length: None,
|
||||
stabilization: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,6 +281,14 @@ impl NonlinearStatic {
|
||||
}
|
||||
finite("imperfection scale", imp.scale)?;
|
||||
}
|
||||
if let Some(a) = self.stabilization {
|
||||
if !(a > 0.0 && a <= MAX_STABILIZATION) {
|
||||
return Err(Error::Invalid(format!("the stabilization is a damping fraction in (0, {MAX_STABILIZATION}], got {a}")));
|
||||
}
|
||||
if self.arc_length.is_some() {
|
||||
return Err(Error::Invalid("an arc-length step follows instabilities along its path; it takes no stabilization".into()));
|
||||
}
|
||||
}
|
||||
let initial = fraction("initial increment", self.initial_increment)?;
|
||||
let min = fraction("minimum increment", self.min_increment)?;
|
||||
let max = fraction("maximum increment", self.max_increment)?;
|
||||
@@ -331,6 +347,9 @@ impl NonlinearStatic {
|
||||
}
|
||||
}
|
||||
|
||||
/// The largest damping fraction of a nonlinear step's stabilization.
|
||||
pub const MAX_STABILIZATION: f64 = 1.0;
|
||||
|
||||
/// Most time increments of a transient heat transfer step.
|
||||
pub const MAX_HEAT_INCREMENTS: f64 = 100_000.0;
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
//! Tie: two surfaces of non-matching meshes bonded together.
|
||||
//!
|
||||
//! A tie names a `main` and a `secondary` surface, sets of element faces. The solvers make every
|
||||
//! node of the secondary surface follow the main surface under it (the implicit solver by mortar
|
||||
//! constraints with dual shape functions, so a constant stress crosses the tie exactly). Nodes of
|
||||
//! the secondary surface farther than the `position_tolerance` from the main surface stay free
|
||||
//! (by default half the thickness of the main element under them).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Error, Model, Result, context, insert_new, remove_existing, replace_existing, require};
|
||||
|
||||
/// Most ties in a model.
|
||||
pub const MAX_TIES: usize = 1000;
|
||||
|
||||
/// A tie definition.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Tie {
|
||||
pub main: String,
|
||||
pub secondary: String,
|
||||
/// How far a secondary node may lie from the main surface and still be tied (`None`: half
|
||||
/// the thickness of the main element under it).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub position_tolerance: Option<f64>,
|
||||
}
|
||||
|
||||
impl Tie {
|
||||
pub fn new(main: &str, secondary: &str) -> Tie {
|
||||
Tie { main: main.to_string(), secondary: secondary.to_string(), position_tolerance: None }
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.main == self.secondary {
|
||||
return Err(Error::Invalid(format!("a tie needs two different surfaces, got `{}` twice", self.main)));
|
||||
}
|
||||
if let Some(t) = self.position_tolerance
|
||||
&& !(t > 0.0 && t.is_finite())
|
||||
{
|
||||
return Err(Error::Invalid(format!("a tie's position tolerance is a positive length, got {t}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn ties(&self) -> &std::collections::BTreeMap<String, Tie> {
|
||||
&self.ties
|
||||
}
|
||||
|
||||
pub fn tie(&self, name: &str) -> Option<&Tie> {
|
||||
self.ties.get(name)
|
||||
}
|
||||
|
||||
fn check_tie(&self, name: &str, t: &Tie) -> Result<()> {
|
||||
t.validate().map_err(|e| context("tie", name, e))?;
|
||||
for set in [&t.main, &t.secondary] {
|
||||
require(&self.sets, "set", set).map_err(|e| context("tie", name, e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_tie(&mut self, name: &str, tie: Tie) -> Result<()> {
|
||||
self.check_tie(name, &tie)?;
|
||||
if self.ties.len() >= MAX_TIES {
|
||||
return Err(Error::Limit(format!("a model holds at most {MAX_TIES} ties")));
|
||||
}
|
||||
insert_new(&mut self.ties, "tie", name, tie)
|
||||
}
|
||||
|
||||
pub fn replace_tie(&mut self, name: &str, tie: Tie) -> Result<()> {
|
||||
self.check_tie(name, &tie)?;
|
||||
replace_existing(&mut self.ties, "tie", name, tie)
|
||||
}
|
||||
|
||||
pub fn remove_tie(&mut self, name: &str) -> Result<()> {
|
||||
remove_existing(&mut self.ties, "tie", name).map(|_| ())
|
||||
}
|
||||
|
||||
/// The ties that name a set, as ``tie `name` `` strings.
|
||||
pub(crate) fn tie_users<'a>(&'a self, set: &'a str) -> impl Iterator<Item = String> + 'a {
|
||||
self.ties.iter().filter(move |(_, t)| t.main == set || t.secondary == set).map(|(n, _)| format!("tie `{n}`"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Set;
|
||||
|
||||
#[test]
|
||||
fn ties_validate_refer_to_sets_and_round_trip() {
|
||||
let mut m = Model::default();
|
||||
for s in ["a", "b"] {
|
||||
m.add_set(s, Set { nodes: vec![1], ..Set::default() }).unwrap();
|
||||
}
|
||||
m.add_tie("glue", Tie::new("a", "b")).unwrap();
|
||||
assert!(matches!(m.remove_set("a"), Err(Error::InUse(_))), "the tie refers to the set");
|
||||
assert!(m.add_tie("glue", Tie::new("a", "b")).is_err(), "names stay unique");
|
||||
assert!(m.add_tie("self", Tie::new("a", "a")).is_err());
|
||||
assert!(m.add_tie("far", Tie { position_tolerance: Some(-1.0), ..Tie::new("a", "b") }).is_err());
|
||||
assert!(matches!(m.add_tie("missing", Tie::new("a", "c")), Err(Error::NotFound(_))));
|
||||
let text = serde_json::to_string(&m).unwrap();
|
||||
let back: Model = serde_json::from_str(&text).unwrap();
|
||||
assert_eq!(back.ties(), m.ties());
|
||||
m.replace_tie("glue", Tie { position_tolerance: Some(0.1), ..Tie::new("b", "a") }).unwrap();
|
||||
m.remove_tie("glue").unwrap();
|
||||
assert!(m.remove_tie("glue").is_err());
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ use crate::cholesky::{Factor, FactorError, UpperColumns};
|
||||
use crate::constraints::{Budget, Reduction, coupling_rows};
|
||||
use crate::eigen::{Operator, Wanted, lanczos};
|
||||
use crate::ldlt::{Kind, Symmetric};
|
||||
use crate::ordering::nested_dissection;
|
||||
use crate::ordering::{fill, graph_dissection, nested_dissection};
|
||||
use crate::sparse::{Csr, NodeGraph, add_block, add_diagonal, add_scalar};
|
||||
use crate::system::{
|
||||
Mesh, Structural, StructuralKernel, add_thermal_loads, inelastic_warnings, loads, prescribed, structural_body_forces, temperature_loads,
|
||||
@@ -113,6 +113,8 @@ pub struct StaticResult {
|
||||
/// The increments of a nonlinear static step (its stresses are Cauchy stresses and its
|
||||
/// strains logarithmic strains).
|
||||
pub nonlinear: Option<crate::nonlinear::History>,
|
||||
/// Each contact's state at the end of a nonlinear static step, by name.
|
||||
pub contacts: Vec<crate::contact::ContactResult>,
|
||||
}
|
||||
|
||||
/// One natural mode.
|
||||
@@ -309,12 +311,12 @@ pub(crate) fn pattern(mesh: &Mesh) -> (NodeGraph, Vec<usize>) {
|
||||
let graph = NodeGraph::new(mesh.block_count(), mesh.blocks());
|
||||
// The order covers the mesh nodes; reference point equations follow them.
|
||||
let n = mesh.labels.len();
|
||||
let order = if graph.adjacency.len() == n {
|
||||
nested_dissection(&mesh.xyz, &graph.adjacency)
|
||||
} else {
|
||||
let mesh_part: Vec<Vec<usize>> = graph.adjacency.iter().take(n).map(|row| row.iter().copied().filter(|b| *b < n).collect()).collect();
|
||||
nested_dissection(&mesh.xyz, &mesh_part)
|
||||
};
|
||||
let mesh_part: Vec<Vec<usize>> = graph.adjacency.iter().take(n).map(|row| row.iter().copied().filter(|b| *b < n).collect()).collect();
|
||||
// The dissection that fills the factor less: by coordinate planes, or by the graph's own
|
||||
// level structures (better on thin curved walls).
|
||||
let by_planes = nested_dissection(&mesh.xyz, &mesh_part);
|
||||
let by_graph = graph_dissection(&mesh_part);
|
||||
let order = if fill(&mesh_part, &by_graph) < fill(&mesh_part, &by_planes) { by_graph } else { by_planes };
|
||||
(graph, order)
|
||||
}
|
||||
|
||||
@@ -339,7 +341,8 @@ pub(crate) struct Setup<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn reduction(s: &Setup, values: bool, order: &[usize], budget: Budget, warnings: &mut Vec<String>) -> Result<Reduction> {
|
||||
let rows = coupling_rows(s.model, s.resolved, s.mesh)?;
|
||||
let mut rows = coupling_rows(s.model, s.resolved, s.mesh)?;
|
||||
rows.extend(crate::tie::rows(s.model, s.resolved, s.mesh, warnings)?);
|
||||
Reduction::new(s.mesh, order, &rows, |active| prescribed(s.model, s.resolved, s.mesh, s.step, values, active, warnings), budget)
|
||||
}
|
||||
|
||||
@@ -362,6 +365,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
|p| matches!(p, Procedure::Static {} | Procedure::CoupledTemperatureDisplacement {}),
|
||||
"static or coupled temperature–displacement",
|
||||
)?;
|
||||
crate::system::refuse_contact(model, "linear static")?;
|
||||
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));
|
||||
@@ -497,7 +501,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
}
|
||||
let stress: Vec<[f64; 6]> = sum.iter().zip(&count).map(|(s, c)| s.map(|v| if *c > 0 { v / f64::from(*c) } else { 0.0 })).collect();
|
||||
let von_mises = stress.iter().map(von_mises).collect();
|
||||
let scale = applied_total.iter().chain(&reaction_total).fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
let scale = crate::system::balance_scale(&applied_total, &reaction_total, &reaction);
|
||||
let ground = mesh.discrete.ground_force(&u, &mesh);
|
||||
let imbalance = (0..3).map(|c| (applied_total[c] + reaction_total[c] + ground[c]).abs()).fold(0.0, f64::max);
|
||||
if scale > 0.0 && imbalance > 1e-6 * scale {
|
||||
@@ -522,6 +526,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
warnings,
|
||||
temperature,
|
||||
nonlinear: None,
|
||||
contacts: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -568,6 +573,7 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
{
|
||||
return Err(Error::Invalid(format!("the shift must be a finite number, got {s}")));
|
||||
}
|
||||
crate::system::refuse_contact(model, "frequency")?;
|
||||
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));
|
||||
|
||||
@@ -113,6 +113,7 @@ pub fn solve_buckle(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
};
|
||||
step.procedure.validate()?;
|
||||
let resolved = model.resolve()?;
|
||||
crate::system::refuse_contact(model, "buckling")?;
|
||||
let mesh = Mesh::new(model, &resolved, limits.max_dofs)?;
|
||||
if let Some(t) = mesh.trusses.first() {
|
||||
return Err(Error::NotSupported(format!(
|
||||
|
||||
@@ -152,6 +152,11 @@ fn ereach(a: &UpperColumns, k: usize, parent: &[usize], stack: &mut [usize], mar
|
||||
}
|
||||
|
||||
/// The elimination tree and the entry count of each column of L, without computing values.
|
||||
/// The entries of the Cholesky factor of `a`'s pattern (in its own order).
|
||||
pub(crate) fn factor_entries(a: &UpperColumns) -> usize {
|
||||
symbolic(a).1.iter().sum()
|
||||
}
|
||||
|
||||
fn symbolic(a: &UpperColumns) -> (Vec<usize>, Vec<usize>) {
|
||||
let parent = etree(a);
|
||||
let mut counts = vec![1usize; a.n];
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -414,15 +414,25 @@ pub(crate) fn temperatures(model: &Model, resolved: &Resolved, step: &Step, limi
|
||||
Ok((s.mesh.labels, t))
|
||||
}
|
||||
|
||||
/// Heat transfer does not tie temperatures across a tie yet.
|
||||
fn refuse_ties(model: &Model) -> Result<()> {
|
||||
match model.ties().keys().next() {
|
||||
Some(name) => Err(crate::Error::NotSupported(format!("ties (tie `{name}`) in heat transfer"))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>>> {
|
||||
refuse_ties(model)?;
|
||||
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> {
|
||||
refuse_ties(model)?;
|
||||
let resolved = model.resolve()?;
|
||||
let step = model.select_step(step, |p| matches!(p, Procedure::HeatTransfer { .. }), "heat transfer")?;
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).collect();
|
||||
|
||||
@@ -527,7 +527,9 @@ impl Symmetric {
|
||||
Kind::Definite => Factor::new(a, max_entries).map(Symmetric::Cholesky),
|
||||
Kind::Indefinite => LdltFactor::new(a, max_entries).map(Symmetric::Indefinite),
|
||||
Kind::Auto => {
|
||||
let t0 = std::time::Instant::now();
|
||||
let symbolic = Symbolic::new(a, max_entries)?;
|
||||
if std::env::var("FC_TRACE").is_ok() { eprintln!("TRACE symbolic {:?}", t0.elapsed()); }
|
||||
match Factor::from_symbolic(&symbolic) {
|
||||
Ok(f) => Ok(Symmetric::Cholesky(f)),
|
||||
Err(FactorError::NotPositiveDefinite { .. }) => LdltFactor::from_symbolic(&symbolic).map(Symmetric::Indefinite),
|
||||
|
||||
@@ -31,6 +31,7 @@ pub mod analysis;
|
||||
pub mod buckle;
|
||||
pub mod cholesky;
|
||||
pub mod constraints;
|
||||
pub mod contact;
|
||||
pub mod discrete;
|
||||
pub mod eigen;
|
||||
pub mod heat;
|
||||
@@ -39,6 +40,7 @@ pub mod nonlinear;
|
||||
pub mod ordering;
|
||||
pub mod sparse;
|
||||
pub mod system;
|
||||
pub mod tie;
|
||||
|
||||
pub use analysis::{
|
||||
ElementStress, FrequencyOptions, FrequencyResult, Location, MassKind, Mode, PointResult, StaticResult, Stats, StepResult, solve_frequency,
|
||||
|
||||
+415
-29
@@ -88,6 +88,15 @@
|
||||
//! strain at the integration points.
|
||||
//! Couplings stay the linear (small-rotation) constraints of the linear solver, with a warning.
|
||||
//!
|
||||
//! **Contact** ([`crate::contact`]): the model's contact pairs add their mortar forces to the
|
||||
//! internal forces and their consistent tangent to the stiffness, whose pattern takes the blocks
|
||||
//! of the closed nodes' face pairs at every evaluation. Each converged increment is solved again
|
||||
//! with updated multipliers until the gaps are tight (the augmented Lagrangian's Uzawa loop, the
|
||||
//! increment's `augmentations`, each iteration tagged with its `augmentation`); an increment whose
|
||||
//! active set chatters is cut back. With contact the force scale of the convergence test is that
|
||||
//! of the converged states, the loads and the current state, not the largest force of every
|
||||
//! trial: a trial state can press a body deep into another before the active set settles.
|
||||
//!
|
||||
//! Element forces and tangents are computed in parallel and added in label order, so results are
|
||||
//! the same bit for bit on any number of threads.
|
||||
|
||||
@@ -111,9 +120,10 @@ use crate::analysis::{
|
||||
};
|
||||
use crate::cholesky::FactorError;
|
||||
use crate::constraints::Reduction;
|
||||
use crate::contact::{self, ContactResult, Interface};
|
||||
use crate::ldlt::{Kind, Symmetric};
|
||||
use crate::sparse::{Csr, add_block};
|
||||
use crate::system::{Mesh, add_element_thermal_loads, loads, temperature_loads, translations};
|
||||
use crate::sparse::{Csr, NodeGraph, add_block, add_entry};
|
||||
use crate::system::{Mesh, add_element_thermal_loads, loads, temperature_loads, translation_block, translations};
|
||||
use crate::{Error, Limits, Result};
|
||||
|
||||
/// One Newton iteration of an increment.
|
||||
@@ -131,6 +141,9 @@ pub struct Iteration {
|
||||
/// tangent is factored by LDLᵀ, [`crate::ldlt`]).
|
||||
#[serde(skip_serializing_if = "is_zero")]
|
||||
pub negative_pivots: usize,
|
||||
/// Contact: the augmentation it belongs to (0: the increment's first solution).
|
||||
#[serde(skip_serializing_if = "is_zero")]
|
||||
pub augmentation: usize,
|
||||
}
|
||||
|
||||
fn is_zero(n: &usize) -> bool {
|
||||
@@ -155,6 +168,9 @@ pub struct Increment {
|
||||
/// factor it for the next predictor).
|
||||
#[serde(skip_serializing_if = "is_zero")]
|
||||
pub negative_pivots: usize,
|
||||
/// Contact: the times the increment was solved again with updated multipliers.
|
||||
#[serde(skip_serializing_if = "is_zero")]
|
||||
pub augmentations: usize,
|
||||
}
|
||||
|
||||
/// Where the tangent's inertia changes along an arc-length path.
|
||||
@@ -201,6 +217,16 @@ pub struct History {
|
||||
pub iterations: usize,
|
||||
/// Arc-length steps: where the tangent's inertia changes along the path.
|
||||
pub critical_points: Vec<CriticalPoint>,
|
||||
/// Steps with stabilization: the energy it dissipated.
|
||||
pub stabilization: Option<Stabilization>,
|
||||
}
|
||||
|
||||
/// What a step's viscous stabilization did: the energy its damping dissipated, and the work of
|
||||
/// the internal forces over the step (the energy the model stores, contact's included).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
|
||||
pub struct Stabilization {
|
||||
pub dissipated: f64,
|
||||
pub stored: f64,
|
||||
}
|
||||
|
||||
impl History {
|
||||
@@ -213,6 +239,11 @@ impl History {
|
||||
/// Most displacement values the frames of one step keep (3 per node per frame).
|
||||
pub const MAX_FRAME_VALUES: usize = 30_000_000;
|
||||
|
||||
|
||||
/// A stabilization that dissipates more than this fraction of the work of the internal forces is
|
||||
/// reported.
|
||||
const STABILIZATION_WARNING: f64 = 0.05;
|
||||
|
||||
/// Line search: accept a step whose residual work is at most this fraction of the first.
|
||||
const LINE_SEARCH_ACCEPT: f64 = 0.8;
|
||||
/// Line search: most trial steps, and the range of the step.
|
||||
@@ -259,8 +290,12 @@ struct Problem<'a> {
|
||||
/// 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.
|
||||
/// The tangent's pattern, zero, and the mesh's node graph it comes from (contact adds the
|
||||
/// blocks of its face pairs to it).
|
||||
pattern: Csr,
|
||||
graph: NodeGraph,
|
||||
/// The contact pairs ([`crate::contact`]).
|
||||
contacts: Vec<Interface>,
|
||||
}
|
||||
|
||||
/// The internal and external forces at a state, and the tangent when asked for.
|
||||
@@ -271,6 +306,8 @@ struct Evaluation {
|
||||
/// Every element's material history at this state (committed when the increment
|
||||
/// converges).
|
||||
history: Vec<Vec<f64>>,
|
||||
/// Each contact's state.
|
||||
contact: Vec<contact::State>,
|
||||
}
|
||||
|
||||
/// Why an attempt at an increment failed, when that calls for a cutback rather than an error.
|
||||
@@ -291,6 +328,11 @@ impl From<Error> for Failure {
|
||||
}
|
||||
}
|
||||
|
||||
/// One hash of the contacts' active sets.
|
||||
fn active_sets(e: &Evaluation) -> u64 {
|
||||
e.contact.iter().fold(0u64, |h, s| h.rotate_left(17) ^ s.active_set())
|
||||
}
|
||||
|
||||
fn norm_inf(v: &[f64]) -> f64 {
|
||||
v.iter().fold(0.0, |m, x| m.max(x.abs()))
|
||||
}
|
||||
@@ -387,11 +429,47 @@ impl Problem<'_> {
|
||||
|
||||
/// The forces at displacements `u` and load factor λ, and the tangent (symmetric part of the
|
||||
/// load stiffness included) when `tangent`.
|
||||
fn evaluate(&self, u: &[f64], lambda: f64, tangent: bool, committed: &[Vec<f64>]) -> Result<Evaluation> {
|
||||
/// `contact` is each contact's state at the last converged increment (augmented Lagrange
|
||||
/// multipliers, friction tractions; none: zero) and `reference` the displacements there (the
|
||||
/// start of the friction's slip increment; none: zero).
|
||||
fn evaluate(
|
||||
&self,
|
||||
u: &[f64],
|
||||
lambda: f64,
|
||||
tangent: bool,
|
||||
committed: &[Vec<f64>],
|
||||
contact: &[contact::Committed],
|
||||
reference: &[f64],
|
||||
) -> Result<Evaluation> {
|
||||
let layout = &self.mesh.layout;
|
||||
let n = u.len();
|
||||
let mut internal = vec![0.0; n];
|
||||
let mut k = tangent.then(|| self.pattern.clone());
|
||||
// Contact first: its face pairs extend the tangent's pattern.
|
||||
let mut blocks = contact::Blocks::new();
|
||||
let mut states = Vec::with_capacity(self.contacts.len());
|
||||
let t0 = std::time::Instant::now();
|
||||
for (i, c) in self.contacts.iter().enumerate() {
|
||||
let empty = contact::Committed::default();
|
||||
states.push(c.evaluate(self.mesh, u, reference, contact.get(i).unwrap_or(&empty), tangent, &mut internal, &mut blocks)?);
|
||||
}
|
||||
TIMES[0].fetch_add(t0.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
|
||||
let t0 = std::time::Instant::now();
|
||||
let mut k = tangent.then(|| {
|
||||
if blocks.is_empty() {
|
||||
self.pattern.clone()
|
||||
} else {
|
||||
self.graph.with_blocks(blocks.iter().map(|(nodes, _)| nodes.as_slice())).zero_matrix(layout)
|
||||
}
|
||||
});
|
||||
if let Some(k) = k.as_mut() {
|
||||
// Contact blocks are over X, Y and Z per node; a 2D model keeps the in-plane rows.
|
||||
let kinds = self.mesh.translation_kinds();
|
||||
for (nodes, data) in &blocks {
|
||||
add_block(k, layout, nodes, kinds, &translation_block(data, nodes.len(), kinds))?;
|
||||
}
|
||||
}
|
||||
TIMES[1].fetch_add(t0.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
|
||||
let t0 = std::time::Instant::now();
|
||||
let mut history = Vec::with_capacity(self.mesh.elements.len());
|
||||
for round in parallel::blocks(self.mesh.elements.len(), ROUND) {
|
||||
let start = round.start;
|
||||
@@ -414,15 +492,25 @@ impl Problem<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
TIMES[2].fetch_add(t0.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(s) = &self.springs {
|
||||
let mut ku = vec![0.0; s.n];
|
||||
s.mul(u.get(..s.n).unwrap_or_default(), &mut ku);
|
||||
for (x, v) in internal.iter_mut().zip(&ku) {
|
||||
*x += v;
|
||||
}
|
||||
// Both are on the mesh's pattern.
|
||||
// On the mesh's pattern, which contact may have extended.
|
||||
if let Some(k) = k.as_mut() {
|
||||
k.val.iter_mut().zip(&s.val).for_each(|(x, v)| *x += v);
|
||||
if k.ptr == s.ptr && k.idx == s.idx {
|
||||
k.val.iter_mut().zip(&s.val).for_each(|(x, v)| *x += v);
|
||||
} else {
|
||||
for r in 0..s.n {
|
||||
let (cols, vals) = s.row(r);
|
||||
for (c, v) in cols.iter().zip(vals).filter(|(_, v)| **v != 0.0) {
|
||||
add_entry(k, r, *c, *v)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut external: Vec<f64> = self.dead.iter().map(|f| lambda * f).collect();
|
||||
@@ -440,7 +528,7 @@ impl Problem<'_> {
|
||||
if internal.iter().chain(&external).any(|v| !v.is_finite()) {
|
||||
return Err(Error::NotConverged("the forces are not finite".into()));
|
||||
}
|
||||
Ok(Evaluation { internal, external, tangent: k, history })
|
||||
Ok(Evaluation { internal, external, tangent: k, history, contact: states })
|
||||
}
|
||||
|
||||
/// The loads at load factor 1 at displacements `u` (dead loads and follower pressures on
|
||||
@@ -512,18 +600,43 @@ struct Run<'a> {
|
||||
path_dependent: bool,
|
||||
/// How the undeformed model's tangent factors (Cholesky), once asked.
|
||||
undeformed: Option<std::result::Result<(), FactorError>>,
|
||||
/// The contacts' multipliers per secondary node (the last converged increment's, or the
|
||||
/// current augmentation's), and the augmentations over the step.
|
||||
contact: Vec<contact::Committed>,
|
||||
/// The displacements of the last converged increment (all degrees of freedom).
|
||||
reference: Vec<f64>,
|
||||
augmentations: usize,
|
||||
/// The augmentation being solved in the current increment.
|
||||
augmentation: usize,
|
||||
/// The largest force of the converged states (contact's force scale, [`Run::residual`]).
|
||||
converged_scale: f64,
|
||||
/// Viscous stabilization: the volume each free equation's node carries, and the damping
|
||||
/// c V per free equation once the first increment has calibrated c; the current
|
||||
/// increment's size as a fraction of the step (the pseudo-time step).
|
||||
volumes: Option<Vec<f64>>,
|
||||
damping: Option<Vec<f64>>,
|
||||
pseudo_time: f64,
|
||||
/// The energy the stabilization has dissipated over the converged increments.
|
||||
dissipated: f64,
|
||||
}
|
||||
|
||||
/// The outcome of a converged increment.
|
||||
struct Converged {
|
||||
q: Vec<f64>,
|
||||
evaluation: Evaluation,
|
||||
/// The work of the increment's first correction (the energy criterion's reference).
|
||||
energy: Option<f64>,
|
||||
}
|
||||
|
||||
impl Run<'_> {
|
||||
/// The residual over the free equations, T_fᵀ (f_ext − f_int), and the force scale.
|
||||
///
|
||||
/// With contact a trial state can be far from equilibrium (a body pressed deep into another
|
||||
/// before the active set settles, its contact forces huge), so the scale is not the largest
|
||||
/// force of every trial but that of the converged states, the loads and the current state.
|
||||
fn residual(&mut self, e: &Evaluation) -> Vec<f64> {
|
||||
self.force_scale = self.force_scale.max(norm_inf(&e.external)).max(norm_inf(&e.internal));
|
||||
let current = norm_inf(&e.external).max(norm_inf(&e.internal));
|
||||
self.force_scale = if self.problem.contacts.is_empty() { self.force_scale.max(current) } else { self.converged_scale.max(current) };
|
||||
free_residual(self.problem.red, e)
|
||||
}
|
||||
|
||||
@@ -536,12 +649,41 @@ impl Run<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the stabilization's damping forces (D/Δt)(q − q₀) out of the free residual `r` at
|
||||
/// `q`, the increment starting from `q0`.
|
||||
fn damp(&self, r: &mut [f64], q: &[f64], q0: &[f64]) {
|
||||
if let Some(d) = &self.damping {
|
||||
let c = 1.0 / self.pseudo_time;
|
||||
for (((ri, di), a), b) in r.iter_mut().zip(d).zip(q).zip(q0) {
|
||||
*ri -= c * di * (a - b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the stabilization's D/Δt to the free tangent.
|
||||
fn damp_tangent(&self, kff: &mut Csr) -> std::result::Result<(), Failure> {
|
||||
if let Some(d) = &self.damping {
|
||||
let c = 1.0 / self.pseudo_time;
|
||||
for (i, di) in d.iter().enumerate() {
|
||||
add_entry(kff, i, i, c * di).map_err(Failure::Fatal)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scale(&self) -> f64 {
|
||||
self.force_scale.max(self.force_floor.unwrap_or(0.0)).max(f64::MIN_POSITIVE)
|
||||
}
|
||||
|
||||
fn state(&self, q: &[f64], lambda: f64, tangent: bool) -> std::result::Result<Evaluation, Failure> {
|
||||
Ok(self.problem.evaluate(&self.problem.red.expand_scaled(q, lambda), lambda, tangent, &self.committed)?)
|
||||
Ok(self.problem.evaluate(
|
||||
&self.problem.red.expand_scaled(q, lambda),
|
||||
lambda,
|
||||
tangent,
|
||||
&self.committed,
|
||||
&self.contact,
|
||||
&self.reference,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// How the tangent of the undeformed, unloaded model factors (Cholesky): a first tangent
|
||||
@@ -551,7 +693,7 @@ impl Run<'_> {
|
||||
if let Some(u) = &self.undeformed {
|
||||
return Ok(u.clone());
|
||||
}
|
||||
let mut e = self.problem.evaluate(&vec![0.0; self.problem.mesh.dofs()], 0.0, true, &self.committed)?;
|
||||
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 u = factor_raw(&kff, self.limits).map(|_| ());
|
||||
@@ -600,7 +742,7 @@ impl Run<'_> {
|
||||
/// 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 mut e = self.problem.evaluate(&red.expand_scaled(q0, from), to, true, &self.committed, &self.contact, &self.reference)?;
|
||||
let k = e.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
|
||||
let rf: Vec<f64> = free_residual(red, &e).iter().zip(prescribed_force(red, &k, to - from)).map(|(a, b)| a - b).collect();
|
||||
let mut dq = rf.clone();
|
||||
@@ -625,6 +767,12 @@ impl Run<'_> {
|
||||
) -> std::result::Result<Converged, Failure> {
|
||||
let red = self.problem.red;
|
||||
let tol = self.settings.tolerances;
|
||||
// A held active set is the increment's own (`Committed::active`).
|
||||
for c in &mut self.contact {
|
||||
c.active = None;
|
||||
}
|
||||
let mut held = false;
|
||||
let mut updates = 0usize;
|
||||
// Start from the extrapolated predictor when it is a valid state, else from q0.
|
||||
let start = predictor.map(|p| self.state(p, lambda, true).map(|e| (p.to_vec(), e)));
|
||||
let (mut q, mut eval) = match start {
|
||||
@@ -633,6 +781,7 @@ impl Run<'_> {
|
||||
Some(Err(Failure::Cutback(_))) | None => (q0.to_vec(), self.state(q0, lambda, true)?),
|
||||
};
|
||||
let mut r = self.residual(&eval);
|
||||
self.damp(&mut r, &q, q0);
|
||||
// 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.
|
||||
@@ -642,11 +791,24 @@ impl Run<'_> {
|
||||
// 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();
|
||||
// Contact: the active sets of the iterations, and how often they went back to the one
|
||||
// before the last (chattering).
|
||||
let mut sets = (None, active_sets(&eval));
|
||||
let mut flips = 0usize;
|
||||
let max_iterations = usize::try_from(self.settings.max_iterations).unwrap_or(usize::MAX);
|
||||
for _ in 0..max_iterations {
|
||||
// Holding an active set starts the increment's iterations again (once).
|
||||
let mut remaining = max_iterations;
|
||||
while remaining > 0 {
|
||||
remaining -= 1;
|
||||
// The round-off rule: nothing left to correct.
|
||||
if norm_inf(&r) <= ROUND_OFF * tol.force * self.scale() && self.force_floor.is_some() {
|
||||
return Ok(Converged { q, evaluation: eval });
|
||||
if self.release(&mut eval, &q, lambda, &mut updates)? {
|
||||
r = self.residual(&eval);
|
||||
self.damp(&mut r, &q, q0);
|
||||
remaining = max_iterations;
|
||||
continue;
|
||||
}
|
||||
return Ok(Converged { q, evaluation: eval, energy: first_energy });
|
||||
}
|
||||
if self.iterations >= self.limits.max_newton_iterations {
|
||||
return Err(Failure::Fatal(Error::Limit(format!(
|
||||
@@ -656,14 +818,17 @@ impl Run<'_> {
|
||||
}
|
||||
self.iterations += 1;
|
||||
let k = eval.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
|
||||
let [kff] = red.reduce([&k])?;
|
||||
let [mut kff] = red.reduce([&k])?;
|
||||
// The first tangent of the step: a singular one is a modelling error, not a cutback.
|
||||
let first = self.force_floor.is_none();
|
||||
if first {
|
||||
self.seed_force_floor(&kff);
|
||||
}
|
||||
self.damp_tangent(&mut kff)?;
|
||||
drop(k);
|
||||
let t0 = std::time::Instant::now();
|
||||
let factor = self.factor(&kff, first)?;
|
||||
TIMES[3].fetch_add(t0.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
|
||||
self.factor_entries = factor.entries();
|
||||
drop(kff);
|
||||
let mut dq = r.clone();
|
||||
@@ -672,12 +837,38 @@ impl Run<'_> {
|
||||
return Err(Failure::Cutback("the Newton correction is not finite".into()));
|
||||
}
|
||||
let g0 = dot(&dq, &r);
|
||||
let (s, next) = self.line_search(&q, &dq, lambda, g0)?;
|
||||
let (s, next) = self.line_search(&q, q0, &dq, lambda, g0)?;
|
||||
let negative = factor.inertia().negative;
|
||||
for (x, d) in q.iter_mut().zip(&dq) {
|
||||
*x += s * d;
|
||||
}
|
||||
eval = next;
|
||||
let r_old = std::mem::replace(&mut r, self.residual(&eval));
|
||||
let holding = self.contact.iter().any(|c| c.active.is_some());
|
||||
if !self.problem.contacts.is_empty() && !holding {
|
||||
let set = active_sets(&eval);
|
||||
if sets.0 == Some(set) && set != sets.1 {
|
||||
flips += 1;
|
||||
if flips >= contact::CHATTER_LIMIT {
|
||||
if held {
|
||||
return Err(Failure::Cutback("the contact status alternates between two states (chattering)".into()));
|
||||
}
|
||||
// Hold the current active set until the increment converges (then
|
||||
// check it, `release`): the nodes touching with no pressure stop
|
||||
// flipping, and the tangent stays the derivative of the forces.
|
||||
held = true;
|
||||
for (c, st) in self.contact.iter_mut().zip(&eval.contact) {
|
||||
c.active = Some(st.pressure.iter().map(|p| *p > 0.0).collect());
|
||||
}
|
||||
eval = self.state(&q, lambda, true)?;
|
||||
flips = 0;
|
||||
remaining = max_iterations;
|
||||
}
|
||||
}
|
||||
sets = (Some(sets.1), active_sets(&eval));
|
||||
}
|
||||
let mut r_new = self.residual(&eval);
|
||||
self.damp(&mut r_new, &q, q0);
|
||||
let r_old = std::mem::replace(&mut r, r_new);
|
||||
let energy = (s * dot(&dq, &r_old)).abs();
|
||||
let first = *first_energy.get_or_insert(energy);
|
||||
let moved = q.iter().zip(q0).fold(0.0f64, |m, (a, b)| m.max((a - b).abs()));
|
||||
@@ -687,7 +878,8 @@ impl Run<'_> {
|
||||
displacement: s * norm_inf(&dq) / reference,
|
||||
energy: if first > 0.0 { energy / first } else { 0.0 },
|
||||
line_search: s,
|
||||
negative_pivots: factor.inertia().negative,
|
||||
negative_pivots: negative,
|
||||
augmentation: self.augmentation,
|
||||
};
|
||||
iterations.push(it);
|
||||
if !it.force.is_finite() {
|
||||
@@ -695,7 +887,14 @@ impl Run<'_> {
|
||||
}
|
||||
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 });
|
||||
if self.release(&mut eval, &q, lambda, &mut updates)? {
|
||||
r = self.residual(&eval);
|
||||
self.damp(&mut r, &q, q0);
|
||||
history.clear();
|
||||
remaining = max_iterations;
|
||||
continue;
|
||||
}
|
||||
return Ok(Converged { q, evaluation: eval, energy: first_energy });
|
||||
}
|
||||
history.push(it.force);
|
||||
if let [.., a, b, c] = history[..]
|
||||
@@ -708,9 +907,89 @@ impl Run<'_> {
|
||||
Err(Failure::Cutback(format!("no convergence in {} iterations", self.settings.max_iterations)))
|
||||
}
|
||||
|
||||
/// The augmented Lagrangian loop of a converged increment (Uzawa: the contacts' multipliers
|
||||
/// take the converged pressures, and the increment is solved again from its solution) until
|
||||
/// the gaps are within [`contact::GAP_TOLERANCE`] ([`Interface::augmentation_error`]), at
|
||||
/// most [`contact::MAX_AUGMENTATIONS`] times (then with a warning). Without contact it
|
||||
/// returns the increment as it is.
|
||||
fn augment(
|
||||
&mut self,
|
||||
mut c: Converged,
|
||||
q0: &[f64],
|
||||
lambda: f64,
|
||||
size: f64,
|
||||
iterations: &mut Vec<Iteration>,
|
||||
count: &mut usize,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> std::result::Result<Converged, Failure> {
|
||||
if self.problem.contacts.is_empty() {
|
||||
return Ok(c);
|
||||
}
|
||||
loop {
|
||||
let error = self
|
||||
.problem
|
||||
.contacts
|
||||
.iter()
|
||||
.zip(&self.contact)
|
||||
.zip(&c.evaluation.contact)
|
||||
.fold(0.0f64, |m, ((i, l), s)| m.max(i.augmentation_error(&l.multipliers, s)));
|
||||
if error <= contact::GAP_TOLERANCE {
|
||||
return Ok(c);
|
||||
}
|
||||
if *count >= contact::MAX_AUGMENTATIONS {
|
||||
warnings.push(format!(
|
||||
"contact: at load factor {lambda:.6} a gap is still {error:.1e} of its element's thickness after {} augmentations (the penalty's regularisation)",
|
||||
contact::MAX_AUGMENTATIONS
|
||||
));
|
||||
return Ok(c);
|
||||
}
|
||||
*count += 1;
|
||||
self.augmentations += 1;
|
||||
self.augmentation = *count;
|
||||
for (l, s) in self.contact.iter_mut().zip(&c.evaluation.contact) {
|
||||
// A held-closed node may have pulled (within the tolerance); multipliers press.
|
||||
l.multipliers = s.pressure.iter().map(|p| p.max(0.0)).collect();
|
||||
}
|
||||
// From the converged state, measured against the increment's start and its first
|
||||
// correction like the increment itself.
|
||||
let start = std::mem::take(&mut c.q);
|
||||
c = self.increment(q0, Some(&start), c.energy, lambda, size, iterations)?;
|
||||
}
|
||||
}
|
||||
|
||||
/// The line search along `dq` from `q`: the accepted step and the state there (with the
|
||||
/// tangent).
|
||||
fn line_search(&mut self, q: &[f64], dq: &[f64], lambda: f64, g0: f64) -> std::result::Result<(f64, Evaluation), Failure> {
|
||||
/// At a converged state `eval` (at `q`, load factor `lambda`) with active sets held: when
|
||||
/// every one is consistent there ([`Interface::update_held`]) they are released and the
|
||||
/// state is the solution (false); otherwise the offending nodes switch, at most
|
||||
/// [`contact::MAX_ACTIVE_SET_UPDATES`] times, `eval` is evaluated again and the iterations
|
||||
/// go on (true).
|
||||
fn release(&mut self, eval: &mut Evaluation, q: &[f64], lambda: f64, updates: &mut usize) -> std::result::Result<bool, Failure> {
|
||||
if self.contact.iter().all(|c| c.active.is_none()) {
|
||||
return Ok(false);
|
||||
}
|
||||
let next: Vec<Option<Vec<bool>>> =
|
||||
self.problem.contacts.iter().zip(&self.contact).zip(&eval.contact).map(|((i, c), s)| i.update_held(c, s)).collect();
|
||||
if next.iter().all(Option::is_none) {
|
||||
for c in &mut self.contact {
|
||||
c.active = None;
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
*updates += 1;
|
||||
if *updates > contact::MAX_ACTIVE_SET_UPDATES {
|
||||
return Err(Failure::Cutback("the contact's active set does not settle".into()));
|
||||
}
|
||||
for (c, n) in self.contact.iter_mut().zip(next) {
|
||||
if n.is_some() {
|
||||
c.active = n;
|
||||
}
|
||||
}
|
||||
*eval = self.state(q, lambda, true)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn line_search(&mut self, q: &[f64], q0: &[f64], dq: &[f64], lambda: f64, g0: f64) -> std::result::Result<(f64, Evaluation), Failure> {
|
||||
let at = |s: f64| -> Vec<f64> { q.iter().zip(dq).map(|(a, b)| a + s * b).collect() };
|
||||
// The full step, with its tangent (taken in most iterations).
|
||||
let full = self.state(&at(1.0), lambda, true);
|
||||
@@ -718,11 +997,15 @@ impl Run<'_> {
|
||||
return full.map(|e| (1.0, e));
|
||||
}
|
||||
let red = self.problem.red;
|
||||
let work = |e: &Evaluation| dot(dq, &free_residual(red, e));
|
||||
let work = |e: &Evaluation, s: f64| {
|
||||
let mut r = free_residual(red, e);
|
||||
self.damp(&mut r, &at(s), q0);
|
||||
dot(dq, &r)
|
||||
};
|
||||
let mut s = 1.0;
|
||||
let mut g = match &full {
|
||||
Ok(e) => {
|
||||
let g1 = work(e);
|
||||
let g1 = work(e, 1.0);
|
||||
if g1.abs() <= LINE_SEARCH_ACCEPT * g0.abs() {
|
||||
return full.map(|e| (1.0, e));
|
||||
}
|
||||
@@ -744,7 +1027,7 @@ impl Run<'_> {
|
||||
}
|
||||
s = next;
|
||||
g = match self.state(&at(s), lambda, false) {
|
||||
Ok(e) => Some(work(&e)),
|
||||
Ok(e) => Some(work(&e, s)),
|
||||
Err(Failure::Cutback(_)) => None,
|
||||
Err(f) => return Err(f),
|
||||
};
|
||||
@@ -767,6 +1050,26 @@ impl Run<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per free equation, the volume its node carries (the lumped mass of a unit density): the
|
||||
/// stabilization's mass matrix.
|
||||
fn nodal_volumes(problem: &Problem) -> Result<Vec<f64>> {
|
||||
let mut v = vec![0.0; problem.mesh.dofs()];
|
||||
for (e, dofs) in problem.mesh.elements.iter().zip(&problem.element_dofs) {
|
||||
let lumped = e.kernel()?.lumped_mass(1.0).map_err(|err| Error::element(e.label, err))?;
|
||||
let per = dofs.len() / lumped.len().max(1);
|
||||
for (n, m) in lumped.iter().enumerate() {
|
||||
for d in dofs.iter().skip(n * per).take(per) {
|
||||
if let Some(x) = v.get_mut(*d) {
|
||||
*x += m;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(problem.red.restrict(&v))
|
||||
}
|
||||
|
||||
pub static TIMES: [std::sync::atomic::AtomicU64; 4] = [const { std::sync::atomic::AtomicU64::new(0) }; 4];
|
||||
|
||||
/// Refuse what nonlinear steps cannot take yet, and warn about what they simplify.
|
||||
fn check_supported(model: &Model, mesh: &Mesh, step: &Step, settings: &NonlinearStatic, warnings: &mut Vec<String>) -> Result<()> {
|
||||
let at = |what: String| Error::NotSupported(format!("{what} in nonlinear steps (step `{}`)", step.name));
|
||||
@@ -785,6 +1088,11 @@ fn check_supported(model: &Model, mesh: &Mesh, step: &Step, settings: &Nonlinear
|
||||
return Err(at(format!("centrifugal loads (load `{name}`), which depend on the deformed position")));
|
||||
}
|
||||
plasticity(model, step, settings, warnings)?;
|
||||
if let Some(name) = model.contacts().keys().next()
|
||||
&& settings.arc_length.is_some()
|
||||
{
|
||||
return Err(at(format!("contact (`{name}`) with arc-length control")));
|
||||
}
|
||||
if settings.nonlinear_geometry
|
||||
&& let Some(name) = model.couplings().keys().next()
|
||||
{
|
||||
@@ -960,6 +1268,7 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let contacts = Interface::all(model, &resolved, &mesh, settings.nonlinear_geometry, &mut warnings)?;
|
||||
let element_kinds = mesh.elements.iter().map(|e| Ok(e.kernel()?.dofs().node)).collect::<Result<Vec<_>>>()?;
|
||||
let element_dofs = mesh
|
||||
.elements
|
||||
@@ -988,6 +1297,8 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
.collect(),
|
||||
field,
|
||||
pattern: graph.zero_matrix(&mesh.layout),
|
||||
graph,
|
||||
contacts,
|
||||
};
|
||||
// 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.
|
||||
@@ -1008,7 +1319,7 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
// The undeformed state first: an element that is bad before anything moves is an error in
|
||||
// the model, not a reason to cut increments back.
|
||||
problem.evaluate(&vec![0.0; mesh.dofs()], 0.0, false, &committed)?;
|
||||
problem.evaluate(&vec![0.0; mesh.dofs()], 0.0, false, &committed, &[], &[])?;
|
||||
let path_dependent = problem.materials.iter().any(MaterialModel::is_path_dependent);
|
||||
let mut run = Run {
|
||||
problem: &problem,
|
||||
@@ -1022,6 +1333,15 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
committed,
|
||||
path_dependent,
|
||||
undeformed: None,
|
||||
contact: problem.contacts.iter().map(Interface::committed).collect(),
|
||||
reference: vec![0.0; mesh.dofs()],
|
||||
augmentations: 0,
|
||||
augmentation: 0,
|
||||
converged_scale: 0.0,
|
||||
volumes: settings.stabilization.map(|_| nodal_volumes(&problem)).transpose()?,
|
||||
damping: None,
|
||||
pseudo_time: 1.0,
|
||||
dissipated: 0.0,
|
||||
};
|
||||
|
||||
let mut history = History::default();
|
||||
@@ -1052,6 +1372,9 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
let mut size = settings.initial_increment;
|
||||
let mut fast = 0usize;
|
||||
let mut last: Option<Evaluation> = None;
|
||||
// The work of the internal forces over the step (the energy the model stores, with
|
||||
// contact's), the stabilization's measure.
|
||||
let mut stored = 0.0f64;
|
||||
// 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);
|
||||
@@ -1085,10 +1408,50 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
Err(Failure::Fatal(e)) => return Err(e),
|
||||
}
|
||||
}
|
||||
match run.increment(&q, predictor.as_deref(), predicted_energy, target_lambda, change.abs(), &mut iterations) {
|
||||
let saved = run.contact.clone();
|
||||
run.pseudo_time = this;
|
||||
let mut augmentations = 0;
|
||||
run.augmentation = 0;
|
||||
let solved = run
|
||||
.increment(&q, predictor.as_deref(), predicted_energy, target_lambda, change.abs(), &mut iterations)
|
||||
.and_then(|c| run.augment(c, &q, target_lambda, change.abs(), &mut iterations, &mut augmentations, &mut warnings));
|
||||
if solved.is_err() {
|
||||
run.contact = saved;
|
||||
}
|
||||
if std::env::var("FC_TRACE").is_ok() {
|
||||
let f: Vec<String> = iterations.iter().map(|i| format!("{:.0e}{}", i.force, if i.line_search < 1.0 { format!("/s{:.2}", i.line_search) } else { String::new() })).collect();
|
||||
let closed: usize = match &solved { Ok(c) => c.evaluation.contact.iter().map(|s| s.pressure.iter().filter(|p| **p > 0.0).count()).sum(), Err(_) => 0 };
|
||||
eprintln!("TRACE times contact {} pattern {} elements {} factor {} ms", TIMES[0].load(std::sync::atomic::Ordering::Relaxed) / 1000, TIMES[1].load(std::sync::atomic::Ordering::Relaxed) / 1000, TIMES[2].load(std::sync::atomic::Ordering::Relaxed) / 1000, TIMES[3].load(std::sync::atomic::Ordering::Relaxed) / 1000);
|
||||
eprintln!("TRACE damp {:?} dissipated {:.3e} stored {stored:.3e}", run.damping.as_ref().map(|d| d.iter().fold(0.0f64, |m, v| m.max(*v))), run.dissipated);
|
||||
eprintln!("TRACE inc {number} lam {target_lambda:.5} size {:.2e} closed {closed} neg {:?} {} [{}] {}", change.abs(), iterations.last().map(|i| i.negative_pivots), if solved.is_ok() { "ok" } else { "CUT" }, f.join(" "), match &solved { Err(Failure::Cutback(r)) => r.clone(), _ => String::new() });
|
||||
}
|
||||
match solved {
|
||||
Ok(c) => {
|
||||
if let Some(d) = &run.damping {
|
||||
run.dissipated += c.q.iter().zip(&q).zip(d).map(|((a, b), di)| di * (a - b) * (a - b)).sum::<f64>() / this;
|
||||
}
|
||||
// The work of the internal forces over the increment (trapezoidal; zero forces
|
||||
// at the unloaded start).
|
||||
let (u0, u1) = (red.expand_scaled(&q, lambda), red.expand_scaled(&c.q, target_lambda));
|
||||
let zero = vec![0.0; u0.len()];
|
||||
let before = last.as_ref().map_or(zero.as_slice(), |e| e.internal.as_slice());
|
||||
let work = 0.5 * before.iter().zip(&c.evaluation.internal).zip(u0.iter().zip(&u1)).map(|((f0, f1), (a, b))| (f0 + f1) * (b - a)).sum::<f64>();
|
||||
stored += work;
|
||||
// The stabilization's damping, calibrated on the first increment (solved
|
||||
// without it): moving on at that increment's rate Δq/Δt, c V would dissipate
|
||||
// the fraction α of the work the step does at that rate, W₁/Δt² (the work
|
||||
// grows with the square of the motion), so c = α W₁ / (Δqᵀ V Δq).
|
||||
if let (Some(alpha), Some(v), None) = (settings.stabilization, &run.volumes, &run.damping) {
|
||||
let motion: f64 = c.q.iter().zip(&q).zip(v).map(|((a, b), vi)| vi * (a - b) * (a - b)).sum();
|
||||
if motion > 0.0 && work.abs() > 0.0 {
|
||||
let factor = alpha * work.abs() / motion;
|
||||
run.damping = Some(v.iter().map(|vi| factor * vi).collect());
|
||||
}
|
||||
}
|
||||
previous = Some((c.q.iter().zip(&q).map(|(a, b)| a - b).collect(), change));
|
||||
let count = iterations.len();
|
||||
// The increment's own Newton iterations size the next one (augmentations
|
||||
// solve the same increment again).
|
||||
let count = iterations.iter().filter(|i| i.augmentation == 0).count();
|
||||
history.increments.push(Increment {
|
||||
number,
|
||||
load_factor: target_lambda,
|
||||
@@ -1097,11 +1460,17 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
converged: true,
|
||||
cutback: None,
|
||||
negative_pivots: 0,
|
||||
augmentations,
|
||||
});
|
||||
(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);
|
||||
for (l, s) in run.contact.iter_mut().zip(&c.evaluation.contact) {
|
||||
l.commit(s);
|
||||
}
|
||||
run.reference = red.expand_scaled(&q, lambda);
|
||||
run.converged_scale = run.converged_scale.max(norm_inf(&c.evaluation.external)).max(norm_inf(&c.evaluation.internal));
|
||||
last = Some(c.evaluation);
|
||||
fast = if count <= FAST_ITERATIONS { fast + 1 } else { 0 };
|
||||
if fast >= 2 {
|
||||
@@ -1121,6 +1490,7 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
converged: false,
|
||||
cutback: Some(reason.clone()),
|
||||
negative_pivots: 0,
|
||||
augmentations: 0,
|
||||
});
|
||||
fast = 0;
|
||||
size = this * CUTBACK;
|
||||
@@ -1133,13 +1503,23 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
}
|
||||
}
|
||||
}
|
||||
if run.damping.is_some() {
|
||||
history.stabilization = Some(Stabilization { dissipated: run.dissipated, stored });
|
||||
if run.dissipated > STABILIZATION_WARNING * stored.abs() {
|
||||
warnings.push(format!(
|
||||
"the stabilization dissipated {:.3e}, {:.1} % of the work of the internal forces ({stored:.3e}): lower it, or the result is not quasi-static",
|
||||
run.dissipated,
|
||||
100.0 * run.dissipated / stored.abs().max(f64::MIN_POSITIVE)
|
||||
));
|
||||
}
|
||||
}
|
||||
(q, lambda, last)
|
||||
};
|
||||
history.iterations = run.iterations;
|
||||
let u = red.expand_scaled(&q, lambda);
|
||||
let evaluation = match last {
|
||||
Some(e) => e,
|
||||
None => problem.evaluate(&u, lambda, false, &run.committed)?,
|
||||
None => problem.evaluate(&u, lambda, false, &run.committed, &run.contact, &run.reference)?,
|
||||
};
|
||||
if let Some((at, negative)) = history
|
||||
.increments
|
||||
@@ -1160,7 +1540,8 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
"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, lambda, evaluation, &run.committed, history, run.factor_entries, warnings)
|
||||
let contacts = problem.contacts.iter().zip(&evaluation.contact).map(|(c, s)| c.result(&mesh, s, run.augmentations)).collect();
|
||||
finish(&mesh, step, &red, &problem, u, lambda, evaluation, &run.committed, history, run.factor_entries, contacts, warnings)
|
||||
}
|
||||
|
||||
/// The final state as a static result: displacements, reactions, Cauchy stresses and
|
||||
@@ -1177,6 +1558,7 @@ fn finish(
|
||||
committed: &[Vec<f64>],
|
||||
history: History,
|
||||
factor_entries: usize,
|
||||
contacts: Vec<ContactResult>,
|
||||
mut warnings: Vec<String>,
|
||||
) -> Result<StaticResult> {
|
||||
let (displacement, rotation, point_motion) = split(&u, mesh);
|
||||
@@ -1248,7 +1630,7 @@ fn finish(
|
||||
}
|
||||
let stress: Vec<[f64; 6]> = sum.iter().zip(&count).map(|(s, c)| s.map(|v| if *c > 0 { v / f64::from(*c) } else { 0.0 })).collect();
|
||||
let von_mises = stress.iter().map(von_mises).collect();
|
||||
let scale = applied_total.iter().chain(&reaction_total).fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
let scale = crate::system::balance_scale(&applied_total, &reaction_total, &reaction);
|
||||
let ground = mesh.discrete.ground_force(&u, mesh);
|
||||
let imbalance = (0..3).map(|c| (applied_total[c] + reaction_total[c] + ground[c]).abs()).fold(0.0, f64::max);
|
||||
if scale > 0.0 && imbalance > 1e-6 * scale {
|
||||
@@ -1273,6 +1655,7 @@ fn finish(
|
||||
warnings,
|
||||
temperature: None,
|
||||
nonlinear: Some(history),
|
||||
contacts,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1403,6 +1786,7 @@ impl Run<'_> {
|
||||
energy: if first > 0.0 { energy / first } else { 0.0 },
|
||||
line_search: 1.0,
|
||||
negative_pivots: factor.inertia().negative,
|
||||
augmentation: 0,
|
||||
});
|
||||
}
|
||||
Err(Failure::Cutback(format!("no convergence in {} iterations", self.settings.max_iterations)))
|
||||
@@ -1491,6 +1875,7 @@ fn arc_length(
|
||||
converged: true,
|
||||
cutback: None,
|
||||
negative_pivots: negative,
|
||||
augmentations: 0,
|
||||
});
|
||||
states.push((number, lambda, negative));
|
||||
last = Some(evaluation);
|
||||
@@ -1516,6 +1901,7 @@ fn arc_length(
|
||||
converged: false,
|
||||
cutback: Some(reason.clone()),
|
||||
negative_pivots: 0,
|
||||
augmentations: 0,
|
||||
});
|
||||
fast = 0;
|
||||
size *= CUTBACK;
|
||||
|
||||
@@ -86,6 +86,182 @@ pub fn nested_dissection(xyz: &[[f64; 3]], adjacency: &[Vec<usize>]) -> Vec<usiz
|
||||
order
|
||||
}
|
||||
|
||||
/// A permutation of the nodes by nested dissection on the graph alone: each part's separator is
|
||||
/// a middle level of the breadth-first level structure rooted at a pseudo-peripheral node, so
|
||||
/// separators follow the mesh rather than a coordinate plane (A. George, J. W. H. Liu, *An
|
||||
/// automatic nested dissection algorithm for irregular finite element problems*, SIAM J. Numer.
|
||||
/// Anal. 15(5):1053–1069, 1978; the pseudo-peripheral node of N. E. Gibbs, W. G. Poole,
|
||||
/// P. K. Stockmeyer, SIAM J. Numer. Anal. 13(2):236–250, 1976). A thin curved wall (a shell, a
|
||||
/// bellows) is cut across its surface instead of many times by a plane. Disconnected parts are
|
||||
/// ordered one after the other; ties go by node index, so the ordering is deterministic.
|
||||
pub fn graph_dissection(adjacency: &[Vec<usize>]) -> Vec<usize> {
|
||||
let n = adjacency.len();
|
||||
let mut order = Vec::with_capacity(n);
|
||||
// The part each node belongs to (a token per split), and its level in the current search.
|
||||
let mut owner = vec![0usize; n];
|
||||
let mut level = vec![usize::MAX; n];
|
||||
let mut token = 1usize;
|
||||
enum Task {
|
||||
Split(Vec<usize>),
|
||||
Emit(Vec<usize>),
|
||||
}
|
||||
let mut stack = vec![Task::Split((0..n).collect())];
|
||||
// The level structure of the nodes of part `token` reached from `root`.
|
||||
let levels = |root: usize, token: usize, owner: &[usize], level: &mut Vec<usize>| -> Vec<Vec<usize>> {
|
||||
let mut out: Vec<Vec<usize>> = vec![vec![root]];
|
||||
if let Some(l) = level.get_mut(root) {
|
||||
*l = 0;
|
||||
}
|
||||
loop {
|
||||
let depth = out.len();
|
||||
let mut next = Vec::new();
|
||||
for &v in out.last().into_iter().flatten() {
|
||||
for &w in adjacency.get(v).into_iter().flatten() {
|
||||
if owner.get(w) == Some(&token) && level.get(w) == Some(&usize::MAX) {
|
||||
if let Some(l) = level.get_mut(w) {
|
||||
*l = depth;
|
||||
}
|
||||
next.push(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
if next.is_empty() {
|
||||
break;
|
||||
}
|
||||
out.push(next);
|
||||
}
|
||||
out
|
||||
};
|
||||
let degree = |v: usize, token: usize, owner: &[usize]| adjacency.get(v).map_or(0, |row| row.iter().filter(|w| owner.get(**w) == Some(&token)).count());
|
||||
while let Some(task) = stack.pop() {
|
||||
let part = match task {
|
||||
Task::Emit(sep) => {
|
||||
order.extend(sep);
|
||||
continue;
|
||||
}
|
||||
Task::Split(p) => p,
|
||||
};
|
||||
if part.len() <= LEAF {
|
||||
order.extend(part);
|
||||
continue;
|
||||
}
|
||||
token += 1;
|
||||
for &v in &part {
|
||||
if let Some(o) = owner.get_mut(v) {
|
||||
*o = token;
|
||||
}
|
||||
}
|
||||
let reset = |level: &mut Vec<usize>, nodes: &[usize]| {
|
||||
for &v in nodes {
|
||||
if let Some(l) = level.get_mut(v) {
|
||||
*l = usize::MAX;
|
||||
}
|
||||
}
|
||||
};
|
||||
// A pseudo-peripheral root: from the first node, the end of the deepest level structure
|
||||
// with the fewest neighbours, while the depth grows.
|
||||
let mut root = part.first().copied().unwrap_or(0);
|
||||
let mut structure = levels(root, token, &owner, &mut level);
|
||||
for _ in 0..PERIPHERAL_SEARCHES {
|
||||
let reached: Vec<usize> = structure.iter().flatten().copied().collect();
|
||||
reset(&mut level, &reached);
|
||||
let Some(candidate) = structure.last().and_then(|last| last.iter().copied().min_by_key(|v| (degree(*v, token, &owner), *v))) else { break };
|
||||
let deeper = levels(candidate, token, &owner, &mut level);
|
||||
if deeper.len() <= structure.len() {
|
||||
let reached: Vec<usize> = deeper.iter().flatten().copied().collect();
|
||||
reset(&mut level, &reached);
|
||||
structure = levels(root, token, &owner, &mut level);
|
||||
break;
|
||||
}
|
||||
(root, structure) = (candidate, deeper);
|
||||
}
|
||||
let reached: Vec<usize> = structure.iter().flatten().copied().collect();
|
||||
reset(&mut level, &reached);
|
||||
if reached.len() < part.len() {
|
||||
// Disconnected: this component, then the rest.
|
||||
let mut inside = vec![false; 0];
|
||||
inside.resize(n, false);
|
||||
for &v in &reached {
|
||||
if let Some(x) = inside.get_mut(v) {
|
||||
*x = true;
|
||||
}
|
||||
}
|
||||
let rest: Vec<usize> = part.iter().copied().filter(|v| !inside.get(*v).copied().unwrap_or(false)).collect();
|
||||
let mut component = reached;
|
||||
component.sort_unstable();
|
||||
stack.push(Task::Split(rest));
|
||||
stack.push(Task::Split(component));
|
||||
continue;
|
||||
}
|
||||
if structure.len() < 3 {
|
||||
order.extend(part);
|
||||
continue;
|
||||
}
|
||||
// The middle level: the first whose levels up to it hold half the part.
|
||||
let mut count = 0;
|
||||
let mut middle = 1;
|
||||
for (i, l) in structure.iter().enumerate().skip(1) {
|
||||
count += l.len();
|
||||
if 2 * count >= part.len() {
|
||||
middle = i.min(structure.len() - 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Its nodes that touch the next level separate the levels before it from those after.
|
||||
for (i, l) in structure.iter().enumerate() {
|
||||
for &v in l {
|
||||
if let Some(x) = level.get_mut(v) {
|
||||
*x = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
let touches_next = |v: usize, level: &[usize]| adjacency.get(v).is_some_and(|row| row.iter().any(|w| owner.get(*w) == Some(&token) && level.get(*w) == Some(&(middle + 1))));
|
||||
let mut separator = Vec::new();
|
||||
let mut before: Vec<usize> = structure.iter().take(middle).flatten().copied().collect();
|
||||
for &v in structure.get(middle).into_iter().flatten() {
|
||||
if touches_next(v, &level) { separator.push(v) } else { before.push(v) }
|
||||
}
|
||||
let after: Vec<usize> = structure.iter().skip(middle + 1).flatten().copied().collect();
|
||||
reset(&mut level, &part);
|
||||
before.sort_unstable();
|
||||
separator.sort_unstable();
|
||||
let mut after = after;
|
||||
after.sort_unstable();
|
||||
stack.push(Task::Emit(separator));
|
||||
stack.push(Task::Split(after));
|
||||
stack.push(Task::Split(before));
|
||||
}
|
||||
order
|
||||
}
|
||||
|
||||
/// Searches for a pseudo-peripheral node.
|
||||
const PERIPHERAL_SEARCHES: usize = 8;
|
||||
|
||||
/// The entries of the Cholesky factor of the node graph `adjacency` eliminated in `order` (one
|
||||
/// per node pair): the fill an ordering gives.
|
||||
pub fn fill(adjacency: &[Vec<usize>], order: &[usize]) -> usize {
|
||||
let n = adjacency.len();
|
||||
let mut position = vec![0usize; n];
|
||||
for (k, &v) in order.iter().enumerate() {
|
||||
if let Some(p) = position.get_mut(v) {
|
||||
*p = k;
|
||||
}
|
||||
}
|
||||
let mut ptr = vec![0usize];
|
||||
let mut idx = Vec::new();
|
||||
for &v in order {
|
||||
let k = position.get(v).copied().unwrap_or(0);
|
||||
let mut rows: Vec<usize> = adjacency.get(v).into_iter().flatten().filter_map(|w| position.get(*w).copied()).filter(|r| *r <= k).collect();
|
||||
rows.push(k);
|
||||
rows.sort_unstable();
|
||||
rows.dedup();
|
||||
idx.extend(rows);
|
||||
ptr.push(idx.len());
|
||||
}
|
||||
let val = vec![1.0; idx.len()];
|
||||
crate::cholesky::factor_entries(&crate::cholesky::UpperColumns { n, ptr, idx, val })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -105,6 +105,30 @@ impl NodeGraph {
|
||||
NodeGraph { adjacency }
|
||||
}
|
||||
|
||||
/// The graph with more blocks, each coupling all its slots (contact: the slots a contact
|
||||
/// element couples change with the deformation).
|
||||
pub fn with_blocks<'a>(&self, blocks: impl Iterator<Item = &'a [usize]>) -> NodeGraph {
|
||||
let mut adjacency = self.adjacency.clone();
|
||||
let mut touched = Vec::new();
|
||||
for b in blocks {
|
||||
for &a in b {
|
||||
if let Some(row) = adjacency.get_mut(a) {
|
||||
row.extend_from_slice(b);
|
||||
touched.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
touched.sort_unstable();
|
||||
touched.dedup();
|
||||
for a in touched {
|
||||
if let Some(row) = adjacency.get_mut(a) {
|
||||
row.sort_unstable();
|
||||
row.dedup();
|
||||
}
|
||||
}
|
||||
NodeGraph { adjacency }
|
||||
}
|
||||
|
||||
/// Nonzeros of the dof-level matrix: a dense block per node pair.
|
||||
pub fn entries(&self, layout: &DofLayout) -> usize {
|
||||
self.adjacency.iter().enumerate().map(|(a, row)| layout.range(a).len() * row.iter().map(|b| layout.range(*b).len()).sum::<usize>()).sum()
|
||||
|
||||
@@ -426,11 +426,6 @@ impl Mesh {
|
||||
|
||||
/// Build it from the model and its resolved view, refusing what the solver cannot represent.
|
||||
pub fn new(model: &Model, resolved: &Resolved, max_dofs: usize) -> Result<Mesh> {
|
||||
if let Some(name) = model.contacts().keys().next() {
|
||||
return Err(Error::NotSupported(format!(
|
||||
"contact (`{name}`) in implicit analyses (it comes with M7): a model's contacts apply to all its steps, so solve its explicit dynamic steps, or remove the contacts to solve the implicit ones"
|
||||
)));
|
||||
}
|
||||
let mut elements = Vec::new();
|
||||
let mut trusses = Vec::new();
|
||||
let mut structural = Vec::new();
|
||||
@@ -661,6 +656,24 @@ impl Mesh {
|
||||
}
|
||||
}
|
||||
|
||||
/// The scale of the loads-and-reactions balance check: the largest load or reaction, nodal ones
|
||||
/// too, since reactions that cancel out (a part squeezed between held faces) still set it.
|
||||
pub(crate) fn balance_scale(applied: &[f64; 3], total: &[f64; 3], reaction: &[[f64; 3]]) -> f64 {
|
||||
let largest = reaction.iter().flatten().fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
applied.iter().chain(total).fold(largest, |m, v| m.max(v.abs()))
|
||||
}
|
||||
|
||||
/// Refuse contact in an analysis that cannot take it (linear static, frequency, buckling): a
|
||||
/// model's contacts apply to all its steps.
|
||||
pub fn refuse_contact(model: &Model, analysis: &str) -> Result<()> {
|
||||
match model.contacts().keys().next() {
|
||||
Some(name) => Err(Error::NotSupported(format!(
|
||||
"contact (`{name}`) in {analysis} steps: a model's contacts apply to all its steps, so solve its nonlinear static or explicit dynamic steps, or remove the contacts to solve this one"
|
||||
))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The prescribed value of every degree of freedom (`None` = free) under a step's boundary
|
||||
/// conditions; `values: false` holds them at zero (frequency steps). Rotations of solid nodes do
|
||||
/// not exist and are ignored, like the directions of a reference point no coupling ties
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
//! Ties: non-matching meshes bonded by mortar constraints with dual shape functions.
|
||||
//!
|
||||
//! Each node A of a tie's secondary surface follows the main surface under it through the
|
||||
//! mortar condition ∫ ψ_A (u_s − u_m) dA = 0 per direction, integrated over the overlaps of the
|
||||
//! secondary faces with the main faces in the reference configuration ([`fieldcraft_fem::mortar`]).
|
||||
//! The multiplier space uses the dual shape functions of each secondary face, ψ_a = Σ_b A_ab N_b
|
||||
//! with A = D M⁻¹ (M_ab = ∫ N_a N_b over the face, D = diag ∫ N_a), biorthogonal to the face's
|
||||
//! own shape functions, ∫ ψ_a N_b = δ_ab ∫ N_b (B. I. Wohlmuth, *A mortar finite element method
|
||||
//! using dual spaces for the Lagrange multiplier*, SIAM J. Numer. Anal. 38(3):989–1012, 2000;
|
||||
//! A. Popp, B. I. Wohlmuth, M. W. Gee, W. A. Wall, *Dual quadratic mortar finite element methods
|
||||
//! for 3D finite deformation contact*, SIAM J. Sci. Comput. 34(4):B421–B446, 2012). The mortar
|
||||
//! matrix of the secondary side is then diagonal, and the condition makes each secondary node a
|
||||
//! weighted average of the main nodes under it,
|
||||
//!
|
||||
//! u_A = Σ_J (M_AJ / Σ_K M_AK) u_J, M_AJ = Σ_overlaps ∫ ψ_A N_J dA,
|
||||
//!
|
||||
//! a linear constraint row per direction that the constraint elimination takes like a coupling's
|
||||
//! ([`crate::constraints`]): the secondary node is the intended dependent. Where a secondary
|
||||
//! face is wholly covered, Σ_K M_AK = ∫ N_A, the plain mortar condition; dividing by the sum keeps
|
||||
//! rigid motions exact at the surface's edges, where it is not. A uniform stress crosses a tie on
|
||||
//! flat faces exactly (the tie patch test), because the dual mortar conditions are satisfied by
|
||||
//! the linear field on both sides.
|
||||
//!
|
||||
//! **Pairing.** Each secondary face takes the main faces whose boxes, grown by the position
|
||||
//! tolerance, overlap its own, and that it overlaps along its normal with a mean gap within the
|
||||
//! tolerance (by default half the thickness of the main element). A secondary node with no main
|
||||
//! face under it stays free, with a warning. The constraints are small-displacement (linear)
|
||||
//! constraints in nonlinear steps too.
|
||||
//!
|
||||
//! **Scope.** Linear solid faces (tri3, quad4) in 3D models; heat transfer and explicit dynamics
|
||||
//! refuse ties by name.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use fieldcraft_fem::DofKind;
|
||||
use fieldcraft_fem::contact::{Aabb, Grid, bounds};
|
||||
use fieldcraft_fem::mortar::integrate;
|
||||
use fieldcraft_model::{Model, Resolved};
|
||||
|
||||
use crate::constraints::Row;
|
||||
use crate::contact::{Face, faces};
|
||||
use crate::system::Mesh;
|
||||
use crate::{Error, Result};
|
||||
|
||||
type Vec3 = fieldcraft_fem::frame::Vec3;
|
||||
|
||||
/// Faces per main grid cell budget (the grid's cap is this times the faces).
|
||||
const GRID_CELLS_PER_FACE: usize = 4;
|
||||
|
||||
/// A secondary coefficient below this fraction of its node's total is round-off.
|
||||
const NEGLIGIBLE: f64 = 1e-12;
|
||||
|
||||
fn corners(mesh: &Mesh, f: &Face) -> Vec<Vec3> {
|
||||
f.nodes.iter().map(|n| mesh.xyz.get(*n).copied().unwrap_or_default()).collect()
|
||||
}
|
||||
|
||||
/// x = M⁻¹ b for a small symmetric positive definite M (Gauss–Jordan with partial pivoting);
|
||||
/// `None` when it is singular.
|
||||
fn solve(m: &[[f64; 4]; 4], n: usize, b: [f64; 4]) -> Option<[f64; 4]> {
|
||||
let mut a = *m;
|
||||
let mut x = b;
|
||||
for c in 0..n {
|
||||
let p = (c..n).max_by(|i, j| a[*i][c].abs().total_cmp(&a[*j][c].abs()))?;
|
||||
if !(a[p][c].abs() > 0.0) {
|
||||
return None;
|
||||
}
|
||||
a.swap(c, p);
|
||||
x.swap(c, p);
|
||||
for r in 0..n {
|
||||
if r != c {
|
||||
let f = a[r][c] / a[c][c];
|
||||
for k in c..n {
|
||||
a[r][k] -= f * a[c][k];
|
||||
}
|
||||
x[r] -= f * x[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in 0..n {
|
||||
x[c] /= a[c][c];
|
||||
}
|
||||
x.iter().all(|v| v.is_finite()).then_some(x)
|
||||
}
|
||||
|
||||
/// The dual coefficients A = D M⁻¹ of a face (rows a: ψ_a = Σ_b A_ab N_b).
|
||||
fn dual(x: &[Vec3]) -> Option<[[f64; 4]; 4]> {
|
||||
let n = x.len();
|
||||
let mirror: Vec<Vec3> = x.iter().rev().copied().collect();
|
||||
let mut mass = [[0.0; 4]; 4];
|
||||
integrate(x, &mirror, |p| {
|
||||
for a in 0..4 {
|
||||
for b in 0..4 {
|
||||
mass[a][b] += p.weight * p.secondary[a] * p.secondary[b];
|
||||
}
|
||||
}
|
||||
})?;
|
||||
// A M = D, M symmetric: row a of A solves M (row a)ᵀ = D_aa e_a.
|
||||
let mut out = [[0.0; 4]; 4];
|
||||
for a in 0..n {
|
||||
let d: f64 = mass[a].iter().take(n).sum();
|
||||
let mut e = [0.0; 4];
|
||||
e[a] = d;
|
||||
out[a] = solve(&mass, n, e)?;
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// The constraint rows of every tie (ties by name, secondary nodes by label, directions in
|
||||
/// order), and warnings for the secondary nodes left free.
|
||||
pub fn rows<'m>(model: &'m Model, resolved: &Resolved, mesh: &Mesh, warnings: &mut Vec<String>) -> Result<Vec<Row<'m>>> {
|
||||
let mut out = Vec::new();
|
||||
if model.ties().is_empty() {
|
||||
return Ok(out);
|
||||
}
|
||||
if mesh.theory.is_some() {
|
||||
let name = model.ties().keys().next().map_or("", String::as_str);
|
||||
return Err(Error::NotSupported(format!("ties (tie `{name}`) in 2D models")));
|
||||
}
|
||||
for (name, tie) in model.ties() {
|
||||
let owner = format!("tie `{name}`");
|
||||
let secondary = faces(model, resolved, mesh, &owner, &tie.secondary)?;
|
||||
let main = faces(model, resolved, mesh, &owner, &tie.main)?;
|
||||
let main_x: Vec<Vec<Vec3>> = main.iter().map(|f| corners(mesh, f)).collect();
|
||||
let reach = |f: &Face| tie.position_tolerance.unwrap_or(0.5 * f.thickness);
|
||||
let boxes: Vec<Aabb> = main.iter().zip(&main_x).map(|(f, x)| bounds(x, reach(f))).collect();
|
||||
let grid = Grid::new(&boxes, GRID_CELLS_PER_FACE * boxes.len() + 64);
|
||||
// Per secondary node (mesh slot), its main nodes' weights M_AJ, in face order.
|
||||
let mut weights: BTreeMap<usize, BTreeMap<usize, f64>> = BTreeMap::new();
|
||||
let mut candidates = Vec::new();
|
||||
for f in &secondary {
|
||||
let xs = corners(mesh, f);
|
||||
let coefficients =
|
||||
dual(&xs).ok_or_else(|| Error::Invalid(format!("{owner}: a secondary face at node {} has no area", label(mesh, f))))?;
|
||||
for n in &f.nodes {
|
||||
weights.entry(*n).or_default();
|
||||
}
|
||||
grid.candidates_in(&bounds(&xs, 0.0), &mut candidates);
|
||||
for &c in &candidates {
|
||||
let (Some(m), Some(xm)) = (main.get(c as usize), main_x.get(c as usize)) else { continue };
|
||||
if m.nodes.iter().any(|n| f.nodes.contains(n)) {
|
||||
continue;
|
||||
}
|
||||
let mut local = [[0.0; 4]; 4];
|
||||
let (mut area, mut gap) = (0.0, 0.0);
|
||||
integrate(&xs, xm, |p| {
|
||||
area += p.weight;
|
||||
gap += p.weight * p.gap().abs();
|
||||
for a in 0..4 {
|
||||
let psi: f64 = (0..4).map(|b| coefficients[a][b] * p.secondary[b]).sum();
|
||||
for j in 0..4 {
|
||||
local[a][j] += p.weight * psi * p.main[j];
|
||||
}
|
||||
}
|
||||
});
|
||||
if !(area > 0.0) || gap / area > reach(m) {
|
||||
continue;
|
||||
}
|
||||
for (a, s) in f.nodes.iter().enumerate() {
|
||||
let row = weights.entry(*s).or_default();
|
||||
for (j, mn) in m.nodes.iter().enumerate() {
|
||||
*row.entry(*mn).or_insert(0.0) += local[a][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut free = Vec::new();
|
||||
for (node, row) in &weights {
|
||||
let total: f64 = row.values().sum();
|
||||
if !(total.abs() > 0.0 && total.is_finite()) {
|
||||
free.push(mesh.labels.get(*node).copied().unwrap_or(0));
|
||||
continue;
|
||||
}
|
||||
let terms: Vec<(usize, f64)> = row.iter().map(|(j, w)| (*j, w / total)).filter(|(_, w)| w.abs() > NEGLIGIBLE).collect();
|
||||
for k in 0..3 {
|
||||
let kind = DofKind::translation(k).ok_or_else(|| Error::Internal("axis".into()))?;
|
||||
let pivot = mesh.dof(*node, kind)?;
|
||||
let mut row = vec![(pivot, 1.0)];
|
||||
for (j, w) in &terms {
|
||||
row.push((mesh.dof(*j, kind)?, -w));
|
||||
}
|
||||
out.push(Row { terms: row, pivot, coupling: name.as_str() });
|
||||
}
|
||||
}
|
||||
if let Some(first) = free.first() {
|
||||
warnings.push(format!(
|
||||
"{owner}: {} secondary node(s) have no main face within the position tolerance and stay free (node {first} first)",
|
||||
free.len()
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn label(mesh: &Mesh, f: &Face) -> u32 {
|
||||
f.nodes.first().and_then(|n| mesh.labels.get(*n)).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dual_functions_are_biorthogonal_to_the_face_s_own() {
|
||||
let quad = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [2.2, 1.0, 0.0], [0.1, 1.3, 0.0]];
|
||||
for face in [&quad[..], &quad[..3]] {
|
||||
let a = dual(face).unwrap();
|
||||
let mirror: Vec<Vec3> = face.iter().rev().copied().collect();
|
||||
let mut check = [[0.0; 4]; 4];
|
||||
let mut own = [0.0; 4];
|
||||
integrate(face, &mirror, |p| {
|
||||
for i in 0..4 {
|
||||
own[i] += p.weight * p.secondary[i];
|
||||
let psi: f64 = (0..4).map(|b| a[i][b] * p.secondary[b]).sum();
|
||||
for j in 0..4 {
|
||||
check[i][j] += p.weight * psi * p.secondary[j];
|
||||
}
|
||||
}
|
||||
});
|
||||
for i in 0..face.len() {
|
||||
for j in 0..face.len() {
|
||||
let want = if i == j { own[i] } else { 0.0 };
|
||||
assert!((check[i][j] - want).abs() < 1e-12, "{i} {j}: {} vs {want}", check[i][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(dual(&[[0.0; 3], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,821 @@
|
||||
//! Contact in nonlinear static steps against closed forms: the contact patch test on
|
||||
//! non-matching meshes, Newton's convergence with contact, and what implicit contact refuses.
|
||||
//! Run with `--nocapture` to see the iterations.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use fieldcraft_model::*;
|
||||
use fieldcraft_solver::contact::Status;
|
||||
use fieldcraft_solver::{Limits, StaticResult, solve_nonlinear};
|
||||
|
||||
/// A model built block by block.
|
||||
#[derive(Default)]
|
||||
struct Blocks {
|
||||
model: Model,
|
||||
xyz: BTreeMap<u32, [f64; 3]>,
|
||||
next_node: u32,
|
||||
next_element: u32,
|
||||
}
|
||||
|
||||
impl Blocks {
|
||||
/// A structured block of `n` cells over the unit cube mapped by `map`, hex8 or tet4 (six
|
||||
/// Kuhn tetrahedra per cell); its elements' labels.
|
||||
fn block(&mut self, kind: ElementKind, n: [usize; 3], map: impl Fn([f64; 3]) -> [f64; 3]) -> Vec<u32> {
|
||||
let first = self.next_node + 1;
|
||||
let label = |i: [usize; 3]| first + (i[0] + (n[0] + 1) * (i[1] + (n[1] + 1) * i[2])) as u32;
|
||||
for k in 0..=n[2] {
|
||||
for j in 0..=n[1] {
|
||||
for i in 0..=n[0] {
|
||||
let x = map([i as f64 / n[0] as f64, j as f64 / n[1] as f64, k as f64 / n[2] as f64]);
|
||||
let l = label([i, j, k]);
|
||||
self.model.set_node(l, x).unwrap();
|
||||
self.xyz.insert(l, x);
|
||||
self.next_node = self.next_node.max(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for k in 0..n[2] {
|
||||
for j in 0..n[1] {
|
||||
for i in 0..n[0] {
|
||||
let at = |d: [usize; 3]| label([i + d[0], j + d[1], k + d[2]]);
|
||||
let cells: Vec<Vec<u32>> = match kind {
|
||||
ElementKind::Hex8 => vec![
|
||||
[[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]].iter().map(|d| at(*d)).collect(),
|
||||
],
|
||||
_ => [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]
|
||||
.iter()
|
||||
.map(|perm| {
|
||||
let mut v = [[0usize; 3]; 4];
|
||||
for s in 0..3 {
|
||||
v[s + 1] = v[s];
|
||||
v[s + 1][perm[s]] = 1;
|
||||
}
|
||||
let p = v.map(|c| c.map(|x| x as f64));
|
||||
let e = |i: usize| [0, 1, 2].map(|k| p[i][k] - p[0][k]);
|
||||
let (a, b, c) = (e(1), e(2), e(3));
|
||||
let vol =
|
||||
a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0]) + a[2] * (b[0] * c[1] - b[1] * c[0]);
|
||||
if vol < 0.0 {
|
||||
v.swap(1, 2);
|
||||
}
|
||||
v.iter().map(|d| at(*d)).collect()
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
for nodes in cells {
|
||||
self.next_element += 1;
|
||||
self.model.set_element(self.next_element, kind, nodes).unwrap();
|
||||
out.push(self.next_element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The faces of `elements` whose nodes all satisfy `f`.
|
||||
fn faces(&self, elements: &[u32], f: impl Fn([f64; 3]) -> bool) -> Vec<ElementFace> {
|
||||
let mut out = Vec::new();
|
||||
for l in elements {
|
||||
let e = self.model.element(*l).unwrap();
|
||||
for (i, face) in e.kind.faces().iter().enumerate() {
|
||||
if face.iter().all(|k| f(self.xyz[&e.nodes[*k]])) {
|
||||
out.push(ElementFace { element: *l, face: i as u8 + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn nodes(&self, f: impl Fn([f64; 3]) -> bool) -> Vec<u32> {
|
||||
self.xyz.iter().filter(|(_, x)| f(**x)).map(|(l, _)| *l).collect()
|
||||
}
|
||||
|
||||
fn node_set(&mut self, name: &str, f: impl Fn([f64; 3]) -> bool) {
|
||||
let nodes = self.nodes(f);
|
||||
self.model.add_set(name, Set { nodes, ..Set::default() }).unwrap();
|
||||
}
|
||||
|
||||
fn face_set(&mut self, name: &str, elements: &[u32], f: impl Fn([f64; 3]) -> bool) {
|
||||
let faces = self.faces(elements, f);
|
||||
assert!(!faces.is_empty(), "no faces for `{name}`");
|
||||
self.model.add_set(name, Set { faces, ..Set::default() }).unwrap();
|
||||
}
|
||||
|
||||
fn bc(&mut self, name: &str, values: [Option<f64>; 3], f: impl Fn([f64; 3]) -> bool) {
|
||||
self.node_set(name, f);
|
||||
let [a, b, c] = values;
|
||||
self.model
|
||||
.add_boundary_condition(name, BoundaryCondition::Displacement { target: Target::Set(name.into()), values: [a, b, c, None, None, None] })
|
||||
.unwrap();
|
||||
self.model.set_boundary_condition_active("Step", name, true).unwrap();
|
||||
}
|
||||
|
||||
/// One elastic material and section over every element, and the nonlinear step.
|
||||
fn define(&mut self, young: f64, poisson: f64, settings: NonlinearStatic) {
|
||||
let all: Vec<u32> = self.model.elements().keys().copied().collect();
|
||||
self.model.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
|
||||
self.model.add_material("Mat", Material { elastic: Some(Elastic::Isotropic { young, poisson }), ..Material::default() }).unwrap();
|
||||
self.model
|
||||
.add_section(
|
||||
"solid",
|
||||
Section { set: "all".into(), material: "Mat".into(), kind: SectionKind::Solid {}, formulation: Formulation::default() },
|
||||
)
|
||||
.unwrap();
|
||||
self.model.add_step(Step::new("Step", Procedure::NonlinearStatic(settings)), None).unwrap();
|
||||
}
|
||||
|
||||
fn pair(&mut self, name: &str, main: &str, secondary: &str) {
|
||||
let c = Contact::new(ContactSurfaces::Pair { main: main.into(), secondary: secondary.into() });
|
||||
self.model.add_contact(name, c).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn near(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < 1e-9
|
||||
}
|
||||
|
||||
fn report(r: &StaticResult) {
|
||||
let h = r.nonlinear.as_ref().unwrap();
|
||||
for inc in &h.increments {
|
||||
let forces: Vec<String> = inc.iterations.iter().map(|i| format!("{:.1e}", i.force)).collect();
|
||||
eprintln!("increment {} λ {:.3} aug {} [{}]", inc.number, inc.load_factor, inc.augmentations, forces.join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
/// Two blocks pressed together across non-matching meshes (`lower` × `upper` cells, each of
|
||||
/// `lower_kind` / `upper_kind`), between frictionless symmetry planes: a uniaxial stress.
|
||||
fn patch(lower_kind: ElementKind, upper_kind: ElementKind, geometry: bool, distorted: bool) -> (Blocks, StaticResult) {
|
||||
let mut b = Blocks::default();
|
||||
// A graded lower block, its cells boxes of different sizes, or distorted (its faces stay the
|
||||
// planes of the cube either way).
|
||||
let low = if distorted {
|
||||
b.block(lower_kind, [3, 3, 2], |p| [p[0] + 0.3 * p[0] * (1.0 - p[0]) * (p[1] - 0.5), p[1] + 0.3 * p[1] * (1.0 - p[1]) * (p[0] - 0.4), p[2]])
|
||||
} else {
|
||||
b.block(lower_kind, [3, 3, 2], |p| [p[0].powf(1.4), p[1].powf(0.8), p[2]])
|
||||
};
|
||||
let upper = [2, 2, 2];
|
||||
let up = b.block(upper_kind, upper, |p| [p[0], p[1], 1.0 + p[2]]);
|
||||
b.define(
|
||||
1000.0,
|
||||
0.3,
|
||||
NonlinearStatic {
|
||||
nonlinear_geometry: geometry,
|
||||
initial_increment: 0.5,
|
||||
// Tight for small sliding (linear: exact); finite sliding converges to the default ones.
|
||||
tolerances: if geometry { Tolerances::default() } else { Tolerances { force: 1e-10, displacement: 1e-8, energy: 1e-16 } },
|
||||
..NonlinearStatic::default()
|
||||
},
|
||||
);
|
||||
b.face_set("lower_top", &low, |x| near(x[2], 1.0));
|
||||
b.face_set("upper_bottom", &up, |x| near(x[2], 1.0));
|
||||
b.pair("interface", "lower_top", "upper_bottom");
|
||||
b.bc("x0", [Some(0.0), None, None], |x| near(x[0], 0.0));
|
||||
b.bc("y0", [None, Some(0.0), None], |x| near(x[1], 0.0));
|
||||
b.bc("z0", [None, None, Some(0.0)], |x| near(x[2], 0.0));
|
||||
b.bc("push", [None, None, Some(-0.02)], |x| near(x[2], 2.0));
|
||||
let r = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap();
|
||||
report(&r);
|
||||
(b, r)
|
||||
}
|
||||
|
||||
/// The contact patch test (Taylor & Papadopoulos 1991; Crisfield 2000): a uniform pressure
|
||||
/// crosses a non-matching interface exactly, so every element of both blocks has the same
|
||||
/// uniaxial stress, with nothing across.
|
||||
#[test]
|
||||
fn the_mortar_interface_passes_the_contact_patch_test() {
|
||||
for (lower, upper, name) in [
|
||||
(ElementKind::Hex8, ElementKind::Hex8, "hex8 on hex8"),
|
||||
(ElementKind::Hex8, ElementKind::Tet4, "tet4 on hex8"),
|
||||
(ElementKind::Tet4, ElementKind::Hex8, "hex8 on tet4"),
|
||||
] {
|
||||
for geometry in [false, true] {
|
||||
// Small sliding is linear and exact; finite sliding is exact up to the gaps the
|
||||
// augmentation leaves (their geometric terms, GAP_TOLERANCE) and the convergence.
|
||||
let tol = if geometry { 1e-4 } else { 1e-9 };
|
||||
let (b, r) = patch(lower, upper, geometry, false);
|
||||
let szz: Vec<f64> = r.elements.iter().flat_map(|e| e.nodal.iter().map(|s| s[2])).collect();
|
||||
let mean = szz.iter().sum::<f64>() / szz.len() as f64;
|
||||
assert!(mean < 0.0, "{name}: {mean}");
|
||||
for (e, s) in r.elements.iter().flat_map(|e| e.nodal.iter().map(move |s| (e.label, s))) {
|
||||
assert!((s[2] - mean).abs() < tol * mean.abs(), "{name} ({geometry}) element {e}: σzz {} vs {mean}", s[2]);
|
||||
assert!(s[0].abs() < tol * mean.abs() && s[1].abs() < tol * mean.abs(), "{name} ({geometry}) element {e}: {s:?}");
|
||||
}
|
||||
let c = &r.contacts[0];
|
||||
// The pressure is uniform, equal to the stress, at every secondary node.
|
||||
assert!(c.pressure.iter().all(|p| (p + mean).abs() < tol * mean.abs()), "{name}: {:?} vs {mean}", c.pressure);
|
||||
// The contact force on the upper block balances the reaction at its top.
|
||||
let top: f64 = b.nodes(|x| near(x[2], 2.0)).iter().map(|l| r.reaction[r.nodes.binary_search(l).unwrap()][2]).sum();
|
||||
assert!((c.force[2] + top).abs() < tol * top.abs(), "{name}: {:?} vs {top}", c.force);
|
||||
assert!(c.force[0].abs() < tol * top.abs() && c.force[1].abs() < tol * top.abs(), "{name}: {:?}", c.force);
|
||||
assert!((c.normal_force + top).abs() < tol * top.abs(), "{name}: {} vs {top}", c.normal_force);
|
||||
// Augmented to a gap tolerance far below the penetration a penalty alone leaves.
|
||||
assert!(c.penetration < 1e-5, "{name}: {}", c.penetration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hertz contact of a cylinder on a flat (plane strain): a quarter model of an indenter whose
|
||||
/// bottom is a circle of radius `r` on a block, symmetric about x = 0, one element thick in z
|
||||
/// with z held (plane strain). Returns the model and the result.
|
||||
fn hertz_cylinder(finite: bool, push: f64) -> (Blocks, StaticResult) {
|
||||
let r = 10.0;
|
||||
let mut b = Blocks::default();
|
||||
let grade = |s: f64| s.powf(1.6);
|
||||
let foundation = b.block(ElementKind::Hex8, [24, 14, 1], |p| [5.0 * grade(p[0]), -5.0 * grade(1.0 - p[1]), 0.2 * p[2]]);
|
||||
let indenter = b.block(ElementKind::Hex8, [20, 10, 1], |p| {
|
||||
let x = 2.5 * grade(p[0]);
|
||||
[x, r - (r * r - x * x).sqrt() + 2.5 * grade(p[1]), 0.2 * p[2]]
|
||||
});
|
||||
b.define(1000.0, 0.3, NonlinearStatic { nonlinear_geometry: finite, initial_increment: 0.25, ..NonlinearStatic::default() });
|
||||
b.face_set("flat", &foundation, |x| near(x[1], 0.0));
|
||||
let bottom = |x: [f64; 3]| (x[1] - (r - (r * r - x[0] * x[0]).sqrt())).abs() < 1e-9;
|
||||
b.face_set("round", &indenter, bottom);
|
||||
b.pair("hertz", "flat", "round");
|
||||
b.bc("symmetry", [Some(0.0), None, None], |x| near(x[0], 0.0));
|
||||
b.bc("plane", [None, None, Some(0.0)], |_| true);
|
||||
b.bc("base", [None, Some(0.0), None], |x| near(x[1], -5.0));
|
||||
let top = 2.5 + r - (r * r - 2.5 * 2.5f64).sqrt();
|
||||
b.bc("push", [None, Some(-push), None], move |x| {
|
||||
x[1] > 2.5 && (x[1] - (r - (r * r - x[0] * x[0]).sqrt()) - 2.5).abs() < 1e-9 && x[1] <= top + 1e-9
|
||||
});
|
||||
let r = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap();
|
||||
report(&r);
|
||||
(b, r)
|
||||
}
|
||||
|
||||
/// Hertz's line contact (H. Hertz, *Über die Berührung fester elastischer Körper*, J. reine
|
||||
/// angew. Math. 92:156–171, 1882; K. L. Johnson, *Contact Mechanics*, Cambridge 1985, §4.3):
|
||||
/// for a load P per length, b = √(4 P R / (π E*)) and p₀ = 2 P / (π b), E* = E / (2 (1 − ν²))
|
||||
/// for two bodies of one material.
|
||||
#[test]
|
||||
fn hertz_cylinder_on_a_flat() {
|
||||
for finite in [false, true] {
|
||||
let (b, r) = hertz_cylinder(finite, 0.03);
|
||||
let c = &r.contacts[0];
|
||||
let thickness = 0.2;
|
||||
// The load per length: the reaction at the base over the slab's thickness, both halves.
|
||||
let base: f64 = b.nodes(|x| near(x[1], -5.0)).iter().map(|l| r.reaction[r.nodes.binary_search(l).unwrap()][1]).sum();
|
||||
let p = 2.0 * base / thickness;
|
||||
let e_star = 1000.0 / (2.0 * (1.0 - 0.09));
|
||||
let half_width = (4.0 * p * 10.0 / (std::f64::consts::PI * e_star)).sqrt();
|
||||
let peak = 2.0 * p / (std::f64::consts::PI * half_width);
|
||||
let found = c.pressure.iter().fold(0.0f64, |m, v| m.max(*v));
|
||||
let (before, next) = edge(&b, c, 2);
|
||||
eprintln!("finite {finite}: P {p:.4} b {half_width:.4} ({before:.4}..{next:.4}) p0 {peak:.4} found {found:.4}");
|
||||
assert!((found - peak).abs() < 0.03 * peak, "p0 {found} vs {peak}");
|
||||
assert!(before <= half_width && half_width <= next, "b {half_width} not in ({before}, {next})");
|
||||
if finite {
|
||||
quadratic(&r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The contact's edge along the x axis (the nodes on the plane where coordinate `across` is 0):
|
||||
/// the node before the last closed one and the first open one after it, so that the edge lies
|
||||
/// within an element of the last closed node (whose status the augmentation's tolerance decides).
|
||||
fn edge(b: &Blocks, c: &fieldcraft_solver::contact::ContactResult, across: usize) -> (f64, f64) {
|
||||
let mut along: Vec<(f64, Status)> =
|
||||
c.nodes.iter().zip(&c.status).filter(|(l, _)| near(b.xyz[l][across], 0.0)).map(|(l, s)| (b.xyz[l][0], *s)).collect();
|
||||
along.sort_by(|x, y| x.0.total_cmp(&y.0));
|
||||
let last = along.iter().rposition(|(_, s)| *s == Status::Closed).unwrap_or(0);
|
||||
let before = last.checked_sub(1).and_then(|i| along.get(i)).map_or(0.0, |p| p.0);
|
||||
let next = along.get(last + 1).map_or(f64::INFINITY, |p| p.0);
|
||||
(before, next)
|
||||
}
|
||||
|
||||
/// Newton's rate with contact: within each solution of an increment (its first, and each
|
||||
/// augmentation's), once the residual is small every correction squares it, up to a constant
|
||||
/// (the consistent tangent of the mortar integrals, segmentation and normal included).
|
||||
fn quadratic(r: &StaticResult) {
|
||||
let mut checked = 0;
|
||||
for inc in &r.nonlinear.as_ref().unwrap().increments {
|
||||
for w in inc.iterations.windows(2) {
|
||||
let (a, b) = (w[0], w[1]);
|
||||
// Below 1e-10 of the force scale the sums' round-off takes over.
|
||||
if a.augmentation != b.augmentation || a.force > 1e-3 || b.force < 1e-10 {
|
||||
continue;
|
||||
}
|
||||
assert!(b.force <= 100.0 * a.force * a.force, "increment {}: {:e} after {:e}", inc.number, b.force, a.force);
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
assert!(checked >= 3, "too few iterations near the solution to judge ({checked})");
|
||||
}
|
||||
|
||||
/// Two unit blocks, the upper one starting `overlap` into the lower one, pressed together.
|
||||
fn stacked(overlap: f64, penetration: InitialPenetration) -> Blocks {
|
||||
let mut b = Blocks::default();
|
||||
let low = b.block(ElementKind::Hex8, [2, 2, 1], |p| p);
|
||||
let up = b.block(ElementKind::Hex8, [3, 3, 1], |p| [p[0], p[1], 1.0 - overlap + p[2]]);
|
||||
b.define(1000.0, 0.3, NonlinearStatic { nonlinear_geometry: true, initial_increment: 1.0, ..NonlinearStatic::default() });
|
||||
b.face_set("lower_top", &low, |x| near(x[2], 1.0));
|
||||
b.face_set("upper_bottom", &up, |x| near(x[2], 1.0 - overlap));
|
||||
let mut c = Contact::new(ContactSurfaces::Pair { main: "lower_top".into(), secondary: "upper_bottom".into() });
|
||||
c.initial_penetration = penetration;
|
||||
b.model.add_contact("interface", c).unwrap();
|
||||
b.bc("x0", [Some(0.0), None, None], |x| near(x[0], 0.0));
|
||||
b.bc("y0", [None, Some(0.0), None], |x| near(x[1], 0.0));
|
||||
b.bc("z0", [None, None, Some(0.0)], |x| near(x[2], 0.0));
|
||||
b.bc("push", [None, None, Some(-0.01)], move |x| near(x[2], 2.0 - overlap));
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_overclosure_is_adjusted_or_refused() {
|
||||
let b = stacked(1e-3, InitialPenetration::Adjust);
|
||||
let r = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap();
|
||||
assert!(r.warnings.iter().any(|w| w.contains("start inside the main surface")), "{:?}", r.warnings);
|
||||
// Adjusted, the blocks behave as if they started touching: the same stress as without the
|
||||
// overlap.
|
||||
let touching = solve_nonlinear(&stacked(0.0, InitialPenetration::Adjust).model, Some("Step"), &Limits::default()).unwrap();
|
||||
let (a, t) = (r.elements[0].nodal[0][2], touching.elements[0].nodal[0][2]);
|
||||
assert!((a - t).abs() < 1e-4 * t.abs(), "{a} vs {t}");
|
||||
let e = solve_nonlinear(&stacked(1e-3, InitialPenetration::Refuse).model, Some("Step"), &Limits::default()).unwrap_err().to_string();
|
||||
assert!(e.contains("starts") && e.contains("inside"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contact_results_do_not_depend_on_the_thread_count() {
|
||||
let b = stacked(0.0, InitialPenetration::Adjust);
|
||||
let run = |threads| fieldcraft_parallel::with_threads(threads, || solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap());
|
||||
let (one, many) = (run(1), run(4));
|
||||
assert_eq!(one.displacement, many.displacement);
|
||||
assert_eq!(one.contacts, many.contacts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_implicit_contact_refuses() {
|
||||
let solve = |m: &Model| solve_nonlinear(m, Some("Step"), &Limits::default()).unwrap_err().to_string();
|
||||
// Linear analyses refuse every contact by name.
|
||||
let mut b = stacked(0.0, InitialPenetration::Adjust);
|
||||
b.model.add_step(Step::new("Linear", Procedure::Static {}), None).unwrap();
|
||||
let e = fieldcraft_solver::solve_static(&b.model, Some("Linear"), &Limits::default()).unwrap_err().to_string();
|
||||
assert!(e.contains("interface") && e.contains("linear static"), "{e}");
|
||||
// Arc-length control.
|
||||
let mut b = stacked(0.0, InitialPenetration::Adjust);
|
||||
let arc = NonlinearStatic { arc_length: Some(ArcLength::default()), ..NonlinearStatic::default() };
|
||||
b.model.replace_step(Step { procedure: Procedure::NonlinearStatic(arc), ..b.model.step("Step").unwrap().clone() }).unwrap();
|
||||
assert!(solve(&b.model).contains("arc-length"));
|
||||
}
|
||||
|
||||
/// Hertz's point contact (Johnson 1985, §4.2): a sphere of radius R on a flat of the same
|
||||
/// material under a load P has the contact radius a = (3 P R / (4 E*))^(1/3) and the peak
|
||||
/// pressure p₀ = 3 P / (2 π a²), E* = E / (2 (1 − ν²)). A quarter model, displacement controlled;
|
||||
/// the load is the reaction at the base.
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn hertz_sphere_on_a_flat() {
|
||||
let (r, e, nu) = (10.0, 1000.0, 0.3);
|
||||
let mut b = Blocks::default();
|
||||
let grade = |s: f64| s.powf(1.5);
|
||||
let foundation = b.block(ElementKind::Hex8, [16, 16, 10], |p| [5.0 * grade(p[0]), 5.0 * grade(p[1]), -5.0 * grade(1.0 - p[2])]);
|
||||
let cap = move |x: f64, y: f64| r - (r * r - x * x - y * y).sqrt();
|
||||
let indenter = b.block(ElementKind::Hex8, [14, 14, 8], |p| {
|
||||
let (x, y) = (2.5 * grade(p[0]), 2.5 * grade(p[1]));
|
||||
let bottom = cap(x, y);
|
||||
[x, y, bottom + (3.0 - bottom) * grade(p[2])]
|
||||
});
|
||||
b.define(e, nu, NonlinearStatic { nonlinear_geometry: false, initial_increment: 0.25, ..NonlinearStatic::default() });
|
||||
b.face_set("flat", &foundation, |x| near(x[2], 0.0));
|
||||
b.face_set("round", &indenter, move |x| (x[2] - cap(x[0], x[1])).abs() < 1e-9 && x[2] < 1.0);
|
||||
b.pair("hertz", "flat", "round");
|
||||
b.bc("x0", [Some(0.0), None, None], |x| near(x[0], 0.0));
|
||||
b.bc("y0", [None, Some(0.0), None], |x| near(x[1], 0.0));
|
||||
b.bc("base", [None, None, Some(0.0)], |x| near(x[2], -5.0));
|
||||
b.bc("push", [None, None, Some(-0.04)], |x| near(x[2], 3.0));
|
||||
let res = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap();
|
||||
report(&res);
|
||||
let c = &res.contacts[0];
|
||||
let base: f64 = b.nodes(|x| near(x[2], -5.0)).iter().map(|l| res.reaction[res.nodes.binary_search(l).unwrap()][2]).sum();
|
||||
let p = 4.0 * base;
|
||||
let e_star = e / (2.0 * (1.0 - nu * nu));
|
||||
let a = (3.0 * p * r / (4.0 * e_star)).cbrt();
|
||||
let peak = 3.0 * p / (2.0 * std::f64::consts::PI * a * a);
|
||||
let found = c.pressure.iter().fold(0.0f64, |m, v| m.max(*v));
|
||||
let (before, next) = edge(&b, c, 1);
|
||||
eprintln!("P {p:.4} a {a:.4} ({before:.4}..{next:.4}) p0 {peak:.4} found {found:.4}");
|
||||
assert!((found - peak).abs() < 0.05 * peak, "p0 {found} vs {peak}");
|
||||
assert!(before <= a && a <= next, "a {a} not in ({before}, {next})");
|
||||
}
|
||||
|
||||
/// Two blocks, the upper one pressed down by `down` and moved sideways by `side` at its top
|
||||
/// (both in proportion over the step), with Coulomb friction `friction`; the reactions at the
|
||||
/// top (normal, tangential) and the result.
|
||||
fn sheared(down: f64, side: f64, friction: Friction, finite: bool) -> (f64, f64, StaticResult) {
|
||||
let mut b = Blocks::default();
|
||||
let low = b.block(ElementKind::Hex8, [3, 3, 1], |p| p);
|
||||
let up = b.block(ElementKind::Hex8, [2, 2, 1], |p| [p[0], p[1], 1.0 + p[2]]);
|
||||
b.define(1000.0, 0.3, NonlinearStatic { nonlinear_geometry: finite, initial_increment: 0.25, ..NonlinearStatic::default() });
|
||||
b.face_set("lower_top", &low, |x| near(x[2], 1.0));
|
||||
b.face_set("upper_bottom", &up, |x| near(x[2], 1.0));
|
||||
let mut c = Contact::new(ContactSurfaces::Pair { main: "lower_top".into(), secondary: "upper_bottom".into() });
|
||||
c.friction = friction;
|
||||
b.model.add_contact("interface", c).unwrap();
|
||||
b.bc("base", [Some(0.0), Some(0.0), Some(0.0)], |x| near(x[2], 0.0));
|
||||
b.bc("top", [Some(side), Some(0.0), Some(-down)], |x| near(x[2], 2.0));
|
||||
let r = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap();
|
||||
report(&r);
|
||||
let sum = |k: usize| -> f64 { b.nodes(|x| near(x[2], 2.0)).iter().map(|l| r.reaction[r.nodes.binary_search(l).unwrap()][k]).sum() };
|
||||
(-sum(2), sum(0), r)
|
||||
}
|
||||
|
||||
/// Coulomb's law across a frictional interface: a tangential demand below μ N sticks (the blocks
|
||||
/// shear as one, nothing slips), above it the interface slips and carries exactly μ N; with a
|
||||
/// kinetic coefficient the slipping interface carries μ_k N.
|
||||
#[test]
|
||||
fn coulomb_friction_sticks_below_the_cone_and_slips_on_it() {
|
||||
for finite in [false, true] {
|
||||
let mu = 0.3;
|
||||
// Stuck as one body, the shear the top displacement asks for: a frictionless trial gives
|
||||
// the normal force, a stuck one with a very large μ the tangential force.
|
||||
let (n, t, _) = sheared(0.01, 0.001, Friction::coulomb(5.0), finite);
|
||||
let ratio = t / n;
|
||||
eprintln!("finite {finite}: stuck N {n:.4} T {t:.4} ratio {ratio:.4}");
|
||||
// Well below the cone (the shear concentrates at the edges of the interface, which slip
|
||||
// first): sticks, the same forces as glued.
|
||||
let below = 0.4 * mu / ratio;
|
||||
let (n1, t1, r1) = sheared(0.01, 0.001 * below, Friction::coulomb(mu), finite);
|
||||
assert!((t1 / n1 - 0.4 * mu).abs() < 1e-3 * mu, "finite {finite}: {} vs {}", t1 / n1, 0.4 * mu);
|
||||
// The middle of the interface sticks (its corners, where the shear concentrates, may slip).
|
||||
// (The upper block's 3 × 3 bottom nodes come first, in x then y order: the fifth.)
|
||||
assert_eq!(r1.contacts[0].status[4], Status::Stick, "finite {finite}: {:?}", r1.contacts[0].status);
|
||||
// Above it: slips, carrying μ N.
|
||||
let (n2, t2, r2) = sheared(0.01, 0.001 * 2.0 * mu / ratio, Friction::coulomb(mu), finite);
|
||||
// With finite sliding the upper block turns a little as it slides, and the contact acts
|
||||
// along its face's normal, a few thousandths of a radian from the vertical.
|
||||
let tol = if finite { 0.03 } else { 1e-3 };
|
||||
assert!((t2 - mu * n2).abs() < tol * mu * n2, "finite {finite}: T {t2} vs μN {}", mu * n2);
|
||||
assert!(r2.contacts[0].status.iter().all(|s| *s == Status::Slip), "finite {finite}: {:?}", r2.contacts[0].status);
|
||||
let shear: f64 = r2.contacts[0].shear.iter().map(|s| s[0]).zip(&r2.contacts[0].pressure).map(|(s, p)| s / p).sum::<f64>()
|
||||
/ r2.contacts[0].nodes.len() as f64;
|
||||
// Node by node the nodal forces' ratio scatters a little about μ (the smoothing of the
|
||||
// nodal pressure and shear differs at the interface's edges); the mean is μ.
|
||||
assert!((shear + mu).abs() < 0.05 * mu, "finite {finite}: shear over pressure {shear}");
|
||||
// Kinetic friction once slipping.
|
||||
let decaying = Friction { static_coefficient: mu, kinetic: Some(0.2), decay: 1.0 };
|
||||
let (n3, t3, _) = sheared(0.01, 0.001 * 2.0 * mu / ratio, decaying, finite);
|
||||
assert!((t3 - 0.2 * n3).abs() < tol * 0.2 * n3, "finite {finite}: T {t3} vs μk N {}", 0.2 * n3);
|
||||
}
|
||||
}
|
||||
|
||||
/// Two blocks of non-matching meshes tied at z = 1 (`lower_kind` 3 × 3 × 2, graded or distorted,
|
||||
/// below; `upper_kind` 2 × 2 × 2 above), every outer boundary node moved by the linear field
|
||||
/// u = G x: the tie patch test.
|
||||
fn tied(lower_kind: ElementKind, upper_kind: ElementKind, distorted: bool, procedure: Procedure) -> (Blocks, [[f64; 3]; 3]) {
|
||||
let mut b = Blocks::default();
|
||||
let low = if distorted {
|
||||
b.block(lower_kind, [3, 3, 2], |p| [p[0] + 0.3 * p[0] * (1.0 - p[0]) * (p[1] - 0.5), p[1] + 0.3 * p[1] * (1.0 - p[1]) * (p[0] - 0.4), p[2]])
|
||||
} else {
|
||||
b.block(lower_kind, [3, 3, 2], |p| [p[0].powf(1.4), p[1].powf(0.8), p[2]])
|
||||
};
|
||||
let up = b.block(upper_kind, [2, 2, 2], |p| [p[0], p[1], 1.0 + p[2]]);
|
||||
let all: Vec<u32> = b.model.elements().keys().copied().collect();
|
||||
b.model.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
|
||||
b.model.add_material("Mat", Material { elastic: Some(Elastic::Isotropic { young: 1000.0, poisson: 0.3 }), ..Material::default() }).unwrap();
|
||||
b.model
|
||||
.add_section("solid", Section { set: "all".into(), material: "Mat".into(), kind: SectionKind::Solid {}, formulation: Formulation::default() })
|
||||
.unwrap();
|
||||
b.model.add_step(Step::new("Step", procedure), None).unwrap();
|
||||
b.face_set("lower_top", &low, |x| near(x[2], 1.0));
|
||||
b.face_set("upper_bottom", &up, |x| near(x[2], 1.0));
|
||||
b.model.add_tie("glue", Tie::new("lower_top", "upper_bottom")).unwrap();
|
||||
let g = [[1e-3, 2e-4, -3e-4], [4e-4, -5e-4, 1e-4], [-2e-4, 3e-4, 2e-3]];
|
||||
let outer = |x: [f64; 3]| near(x[0], 0.0) || near(x[0], 1.0) || near(x[1], 0.0) || near(x[1], 1.0) || near(x[2], 0.0) || near(x[2], 2.0);
|
||||
for l in b.nodes(outer) {
|
||||
let x = b.xyz[&l];
|
||||
let u: [f64; 3] = std::array::from_fn(|i| (0..3).map(|j| g[i][j] * x[j]).sum());
|
||||
let name = format!("n{l}");
|
||||
b.model.add_set(&name, Set { nodes: vec![l], ..Set::default() }).unwrap();
|
||||
b.model
|
||||
.add_boundary_condition(
|
||||
&name,
|
||||
BoundaryCondition::Displacement { target: Target::Set(name.clone()), values: [Some(u[0]), Some(u[1]), Some(u[2]), None, None, None] },
|
||||
)
|
||||
.unwrap();
|
||||
b.model.set_boundary_condition_active("Step", &name, true).unwrap();
|
||||
}
|
||||
(b, g)
|
||||
}
|
||||
|
||||
/// The tie patch test (dual mortar: Wohlmuth 2000): a linear displacement field across a tie
|
||||
/// of non-matching meshes is reproduced exactly, so every element has the same stress, in linear
|
||||
/// static, nonlinear static and through the interface nodes the tie moves.
|
||||
#[test]
|
||||
fn a_tie_passes_the_patch_test() {
|
||||
for (lower, upper, distorted, name) in [
|
||||
(ElementKind::Hex8, ElementKind::Hex8, false, "hex8 on graded hex8"),
|
||||
(ElementKind::Hex8, ElementKind::Tet4, false, "tet4 on hex8"),
|
||||
(ElementKind::Tet4, ElementKind::Hex8, false, "hex8 on tet4"),
|
||||
(ElementKind::Hex8, ElementKind::Hex8, true, "hex8 on distorted hex8"),
|
||||
] {
|
||||
let (b, g) = tied(lower, upper, distorted, Procedure::Static {});
|
||||
let r = fieldcraft_solver::solve_static(&b.model, Some("Step"), &Limits::default()).unwrap();
|
||||
// The interface nodes follow the field too.
|
||||
let tol = if distorted { 1e-6 } else { 1e-11 };
|
||||
for l in b.nodes(|x| near(x[2], 1.0)) {
|
||||
let x = b.xyz[&l];
|
||||
let u = r.displacement[r.nodes.binary_search(&l).unwrap()];
|
||||
for i in 0..3 {
|
||||
let want: f64 = (0..3).map(|j| g[i][j] * x[j]).sum();
|
||||
assert!((u[i] - want).abs() < tol * 1e-2, "{name}: node {l} u {u:?}");
|
||||
}
|
||||
}
|
||||
let first = r.elements[0].nodal[0];
|
||||
for e in &r.elements {
|
||||
for s in &e.nodal {
|
||||
for k in 0..6 {
|
||||
assert!((s[k] - first[k]).abs() < tol * 10.0, "{name}: element {} {s:?} vs {first:?}", e.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(r.warnings.iter().all(|w| !w.contains("stay free")), "{name}: {:?}", r.warnings);
|
||||
}
|
||||
// The same in a nonlinear step with small strain.
|
||||
let (b, _) = tied(
|
||||
ElementKind::Hex8,
|
||||
ElementKind::Hex8,
|
||||
false,
|
||||
Procedure::NonlinearStatic(NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() }),
|
||||
);
|
||||
let r = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap();
|
||||
let first = r.elements[0].nodal[0];
|
||||
assert!(r.elements.iter().flat_map(|e| &e.nodal).all(|s| (0..6).all(|k| (s[k] - first[k]).abs() < 1e-8)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_ties_refuse() {
|
||||
let (mut b, _) = tied(ElementKind::Hex8, ElementKind::Hex8, false, Procedure::Static {});
|
||||
b.model.add_step(Step::new("Heat", Procedure::HeatTransfer { transient: None }), None).unwrap();
|
||||
let e = fieldcraft_solver::solve_heat(&b.model, Some("Heat"), &Limits::default()).unwrap_err().to_string();
|
||||
assert!(e.contains("tie `glue`") && e.contains("heat transfer"), "{e}");
|
||||
let (mut b, _) = tied(ElementKind::Hex8, ElementKind::Hex8, false, Procedure::Static {});
|
||||
// (Touching surfaces are tied within any tolerance.)
|
||||
b.model.replace_tie("glue", Tie { position_tolerance: Some(1e-9), ..Tie::new("lower_top", "upper_bottom") }).unwrap();
|
||||
let r = fieldcraft_solver::solve_static(&b.model, Some("Step"), &Limits::default()).unwrap();
|
||||
assert!(r.warnings.iter().all(|w| !w.contains("stay free")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A C-shaped ring of rubber (an annulus of radii 1 and 1.5 with a 10° opening at +x, one hex8
|
||||
/// layer thick in z with z held: plane strain) whose two ends are pressed together through the
|
||||
/// outer nodes two elements from each end, against general (single-surface) contact on every
|
||||
/// exterior face of the body: Mooney–Rivlin, nearly incompressible, hybrid elements.
|
||||
fn c_ring(finite: bool, closing: f64) -> (Blocks, StaticResult) {
|
||||
let mut b = Blocks::default();
|
||||
let (n, open) = (36usize, 5f64.to_radians());
|
||||
let angle = move |i: f64| open + i * (2.0 * std::f64::consts::PI - 2.0 * open);
|
||||
b.block(ElementKind::Hex8, [n, 2, 1], |p| {
|
||||
let (t, r) = (angle(p[0]), 1.5 - 0.5 * p[1]);
|
||||
[r * t.cos(), r * t.sin(), 0.2 * p[2]]
|
||||
});
|
||||
let all: Vec<u32> = b.model.elements().keys().copied().collect();
|
||||
b.model.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
|
||||
let rubber = Material {
|
||||
elastic: Some(Elastic::Isotropic { young: 3.0, poisson: 0.499 }),
|
||||
hyperelastic: Some(Hyperelastic::MooneyRivlin { c01: 0.1 }),
|
||||
..Material::default()
|
||||
};
|
||||
b.model.add_material("Rubber", rubber).unwrap();
|
||||
b.model
|
||||
.add_section(
|
||||
"solid",
|
||||
Section {
|
||||
set: "all".into(),
|
||||
material: "Rubber".into(),
|
||||
kind: SectionKind::Solid {},
|
||||
formulation: Formulation { hybrid: true, ..Formulation::default() },
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let settings = NonlinearStatic { nonlinear_geometry: finite, initial_increment: 0.1, max_increments: 200, ..NonlinearStatic::default() };
|
||||
b.model.add_step(Step::new("Step", Procedure::NonlinearStatic(settings)), None).unwrap();
|
||||
b.model.add_contact("self", Contact::new(ContactSurfaces::General { surface: None })).unwrap();
|
||||
// The outer nodes two elements from each end close towards the axis of symmetry y = 0; the
|
||||
// back of the ring is held in its plane; every node in z.
|
||||
let step = 2.0 * std::f64::consts::PI - 2.0 * open;
|
||||
let at = |x: [f64; 3], i: f64| {
|
||||
((x[0] * x[0] + x[1] * x[1]).sqrt() - 1.5).abs() < 1e-9
|
||||
&& (x[1].atan2(x[0]).rem_euclid(2.0 * std::f64::consts::PI) - angle(i)).abs() < 1e-6 * step
|
||||
};
|
||||
let near_end = 2.0 / n as f64;
|
||||
b.bc("upper", [None, Some(-closing), None], move |x| at(x, near_end));
|
||||
b.bc("lower", [None, Some(closing), None], move |x| at(x, 1.0 - near_end));
|
||||
b.bc("back", [Some(0.0), Some(0.0), None], move |x| at(x, 0.5));
|
||||
b.bc("plane", [None, None, Some(0.0)], |_| true);
|
||||
let r = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap();
|
||||
report(&r);
|
||||
(b, r)
|
||||
}
|
||||
|
||||
/// Self-contact: the ring's two ends, faces of one body and one general contact, meet and press
|
||||
/// on each other without passing through, and the result keeps the model's mirror symmetry
|
||||
/// about y = 0.
|
||||
#[test]
|
||||
fn a_rubber_ring_closes_on_itself_without_penetration() {
|
||||
for finite in [true] {
|
||||
let (b, r) = c_ring(finite, 0.15);
|
||||
let c = &r.contacts[0];
|
||||
assert!(c.closed() > 0, "finite {finite}: the ends touch");
|
||||
// No penetration beyond the augmented Lagrangian's tolerance.
|
||||
assert!(c.penetration < 1e-4, "finite {finite}: {}", c.penetration);
|
||||
let deformed = |l: u32| -> [f64; 3] {
|
||||
let u = r.displacement[r.nodes.binary_search(&l).unwrap()];
|
||||
let x = b.xyz[&l];
|
||||
[x[0] + u[0], x[1] + u[1], x[2] + u[2]]
|
||||
};
|
||||
// The ends' nodes stay on their own side of the plane of symmetry, up to the overlap the
|
||||
// mortar's weak (weighted) enforcement allows at the edge of a contact zone: a hundredth
|
||||
// of the elements' size (0.25).
|
||||
for (l, x) in &b.xyz {
|
||||
let y = x[1];
|
||||
if x[0] > 0.9 && y.abs() < 0.2 {
|
||||
let d = deformed(*l);
|
||||
assert!(d[1] * y.signum() >= -2.5e-3, "finite {finite}: node {l} crossed: {y} → {}", d[1]);
|
||||
}
|
||||
}
|
||||
// Mirror symmetry: the node at (x, −y) moves as the mirror of the node at (x, y).
|
||||
let mut checked = 0;
|
||||
for (l, x) in &b.xyz {
|
||||
let Some((m, _)) = b.xyz.iter().find(|(_, y)| (y[0] - x[0]).abs() < 1e-9 && (y[1] + x[1]).abs() < 1e-9 && (y[2] - x[2]).abs() < 1e-9)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let (u, v) = (r.displacement[r.nodes.binary_search(l).unwrap()], r.displacement[r.nodes.binary_search(m).unwrap()]);
|
||||
assert!((u[0] - v[0]).abs() < 1e-6 && (u[1] + v[1]).abs() < 1e-6, "finite {finite}: nodes {l}, {m}: {u:?} {v:?}");
|
||||
checked += 1;
|
||||
}
|
||||
assert!(checked > 50, "{checked}");
|
||||
eprintln!("finite {finite}: closed {} penetration {:.2e} force {:?}", c.closed(), c.penetration, c.force);
|
||||
}
|
||||
}
|
||||
|
||||
/// A rubber bellows (a quarter of a corrugated tube: three convolutions, mean radius 2, fold
|
||||
/// depth 0.5, wall 0.15, height 3) squeezed axially by 0.8 with general contact on all its faces:
|
||||
/// its folds close and press on one another, as in a boot seal. (Squeezed to half its height
|
||||
/// the step stalls at 57%: deeply folded rubber needs better increment control; see M7.)
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn a_rubber_bellows_folds_onto_itself() {
|
||||
let mut b = Blocks::default();
|
||||
let (folds, height, depth, wall) = (3.0, 3.0, 0.5, 0.15);
|
||||
let quarter = std::f64::consts::FRAC_PI_2;
|
||||
b.block(ElementKind::Hex8, [4, 48, 1], move |p| {
|
||||
let theta = quarter * p[0];
|
||||
let s = p[1] * folds;
|
||||
let tri = 1.0 - 2.0 * (s - s.floor() - 0.5).abs(); // 0 → 1 → 0 per convolution
|
||||
let r = 2.0 + depth * tri + wall * p[2];
|
||||
[r * theta.cos(), r * theta.sin(), height * p[1]]
|
||||
});
|
||||
let all: Vec<u32> = b.model.elements().keys().copied().collect();
|
||||
b.model.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
|
||||
let rubber = Material {
|
||||
elastic: Some(Elastic::Isotropic { young: 3.0, poisson: 0.499 }),
|
||||
hyperelastic: Some(Hyperelastic::MooneyRivlin { c01: 0.1 }),
|
||||
..Material::default()
|
||||
};
|
||||
b.model.add_material("Rubber", rubber).unwrap();
|
||||
b.model
|
||||
.add_section(
|
||||
"solid",
|
||||
Section {
|
||||
set: "all".into(),
|
||||
material: "Rubber".into(),
|
||||
kind: SectionKind::Solid {},
|
||||
formulation: Formulation { hybrid: true, ..Formulation::default() },
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let stabilization = std::env::var("STAB").ok().and_then(|v| v.parse().ok());
|
||||
let settings = NonlinearStatic { initial_increment: 0.05, max_increment: 0.1, max_increments: 1000, line_search: stabilization.is_some(), stabilization, ..NonlinearStatic::default() };
|
||||
b.model.add_step(Step::new("Step", Procedure::NonlinearStatic(settings)), None).unwrap();
|
||||
b.model.add_contact("folds", Contact::new(ContactSurfaces::General { surface: None })).unwrap();
|
||||
b.bc("x0", [Some(0.0), None, None], |x| near(x[0], 0.0));
|
||||
b.bc("y0", [None, Some(0.0), None], |x| near(x[1], 0.0));
|
||||
b.bc("base", [None, None, Some(0.0)], |x| near(x[2], 0.0));
|
||||
let squeeze: f64 = std::env::var("SQUEEZE").ok().and_then(|v| v.parse().ok()).unwrap_or(0.8);
|
||||
b.bc("squeeze", [None, None, Some(-squeeze)], move |x| near(x[2], height));
|
||||
let t0 = std::time::Instant::now();
|
||||
let r = solve_nonlinear(&b.model, Some("Step"), &Limits::default());
|
||||
eprintln!("bellows squeeze {squeeze}: {:?} {}", t0.elapsed(), r.as_ref().err().map(ToString::to_string).unwrap_or_default());
|
||||
let r = r.unwrap();
|
||||
report(&r);
|
||||
let c = &r.contacts[0];
|
||||
eprintln!("closed {} penetration {:.2e} augmentations {}", c.closed(), c.penetration, c.augmentations);
|
||||
assert!(c.closed() > 0, "the folds touch");
|
||||
assert!(c.penetration < 1e-3 * wall, "{}", c.penetration);
|
||||
}
|
||||
|
||||
/// The C-ring of [`c_ring`] as a plane strain model (quad4, hybrid, 0.2 thick: the hex8 layer's
|
||||
/// thickness) with general contact on every exterior edge.
|
||||
fn planar_c_ring(closing: f64, threads: usize) -> (Blocks, StaticResult) {
|
||||
let mut b = Blocks::default();
|
||||
let (n, open) = (36usize, 5f64.to_radians());
|
||||
let angle = move |i: f64| open + i * (2.0 * std::f64::consts::PI - 2.0 * open);
|
||||
let label = |i: usize, j: usize| 1 + (i + (n + 1) * j) as u32;
|
||||
for j in 0..=2 {
|
||||
for i in 0..=n {
|
||||
let (t, r) = (angle(i as f64 / n as f64), 1.5 - 0.25 * j as f64);
|
||||
let x = [r * t.cos(), r * t.sin(), 0.0];
|
||||
b.model.set_node(label(i, j), x).unwrap();
|
||||
b.xyz.insert(label(i, j), x);
|
||||
}
|
||||
}
|
||||
let mut next = 0;
|
||||
for j in 0..2 {
|
||||
for i in 0..n {
|
||||
next += 1;
|
||||
// Counter-clockwise in the plane: along the outer arc, then inwards and back.
|
||||
b.model.set_element(next, ElementKind::Quad4, vec![label(i, j), label(i + 1, j), label(i + 1, j + 1), label(i, j + 1)]).unwrap();
|
||||
}
|
||||
}
|
||||
let all: Vec<u32> = b.model.elements().keys().copied().collect();
|
||||
b.model.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
|
||||
let rubber = Material {
|
||||
elastic: Some(Elastic::Isotropic { young: 3.0, poisson: 0.499 }),
|
||||
hyperelastic: Some(Hyperelastic::MooneyRivlin { c01: 0.1 }),
|
||||
..Material::default()
|
||||
};
|
||||
b.model.add_material("Rubber", rubber).unwrap();
|
||||
let section = Section {
|
||||
set: "all".into(),
|
||||
material: "Rubber".into(),
|
||||
kind: SectionKind::PlaneStrain { thickness: 0.2 },
|
||||
formulation: Formulation { hybrid: true, ..Formulation::default() },
|
||||
};
|
||||
b.model.add_section("ring", section).unwrap();
|
||||
let settings = NonlinearStatic { initial_increment: 0.1, max_increments: 200, ..NonlinearStatic::default() };
|
||||
b.model.add_step(Step::new("Step", Procedure::NonlinearStatic(settings)), None).unwrap();
|
||||
b.model.add_contact("self", Contact::new(ContactSurfaces::General { surface: None })).unwrap();
|
||||
let step = 2.0 * std::f64::consts::PI - 2.0 * open;
|
||||
let at = |x: [f64; 3], i: f64| {
|
||||
((x[0] * x[0] + x[1] * x[1]).sqrt() - 1.5).abs() < 1e-9
|
||||
&& (x[1].atan2(x[0]).rem_euclid(2.0 * std::f64::consts::PI) - angle(i)).abs() < 1e-6 * step
|
||||
};
|
||||
let near_end = 2.0 / n as f64;
|
||||
b.bc("upper", [None, Some(-closing), None], move |x| at(x, near_end));
|
||||
b.bc("lower", [None, Some(closing), None], move |x| at(x, 1.0 - near_end));
|
||||
b.bc("back", [Some(0.0), Some(0.0), None], move |x| at(x, 0.5));
|
||||
let r = fieldcraft_parallel::with_threads(threads, || solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap());
|
||||
(b, r)
|
||||
}
|
||||
|
||||
/// 2D self-contact: the plane strain ring closes on itself like the one-layer solid ring with
|
||||
/// its z held (the same problem), the edges' mortar pressing as the faces' does; bit for bit on
|
||||
/// any thread count.
|
||||
#[test]
|
||||
fn a_plane_strain_ring_closes_on_itself_as_the_solid_ring_does() {
|
||||
let (b2, r2) = planar_c_ring(0.15, 1);
|
||||
let (b3, r3) = c_ring(true, 0.15);
|
||||
let (c2, c3) = (&r2.contacts[0], &r3.contacts[0]);
|
||||
assert!(c2.closed() > 0 && c2.penetration < 1e-4, "{} {}", c2.closed(), c2.penetration);
|
||||
// Each edge node closes where its face nodes do (two per edge node, at z = 0 and 0.2).
|
||||
assert_eq!(2 * c2.closed(), c3.closed());
|
||||
// The same motion: every node of the plane against the solid's node at z = 0.
|
||||
let mut checked = 0;
|
||||
for (l, x) in &b2.xyz {
|
||||
let Some((m, _)) = b3.xyz.iter().find(|(_, y)| (y[0] - x[0]).abs() < 1e-9 && (y[1] - x[1]).abs() < 1e-9 && y[2] == 0.0) else { continue };
|
||||
let (u, v) = (r2.displacement[r2.nodes.binary_search(l).unwrap()], r3.displacement[r3.nodes.binary_search(m).unwrap()]);
|
||||
assert!((u[0] - v[0]).abs() < 1e-5 && (u[1] - v[1]).abs() < 1e-5, "node {l}: {u:?} {v:?}");
|
||||
checked += 1;
|
||||
}
|
||||
assert_eq!(checked, b2.xyz.len());
|
||||
let (_, many) = planar_c_ring(0.15, 4);
|
||||
assert_eq!(r2.displacement, many.displacement);
|
||||
assert_eq!(r2.contacts, many.contacts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_2d_contact_refuses() {
|
||||
// Quadratic edges.
|
||||
let (mut b, _) = planar_c_ring(0.0, 1);
|
||||
let mut m = Model::default();
|
||||
for (i, x) in [[0., 0.], [1., 0.], [1., 1.], [0., 1.], [0.5, 0.], [1., 0.5], [0.5, 1.], [0., 0.5]].iter().enumerate() {
|
||||
m.set_node(i as u32 + 1, [x[0], x[1], 0.0]).unwrap();
|
||||
}
|
||||
m.set_element(1, ElementKind::Quad8, (1..=8).collect()).unwrap();
|
||||
m.add_set("all", Set { elements: vec![1], ..Set::default() }).unwrap();
|
||||
m.add_material("Mat", Material { elastic: Some(Elastic::Isotropic { young: 1.0, poisson: 0.3 }), ..Material::default() }).unwrap();
|
||||
m.add_section(
|
||||
"s",
|
||||
Section { set: "all".into(), material: "Mat".into(), kind: SectionKind::PlaneStrain { thickness: 1.0 }, formulation: Formulation::default() },
|
||||
)
|
||||
.unwrap();
|
||||
m.add_step(Step::new("Step", Procedure::NonlinearStatic(NonlinearStatic::default())), None).unwrap();
|
||||
m.add_contact("self", Contact::new(ContactSurfaces::General { surface: None })).unwrap();
|
||||
let e = solve_nonlinear(&m, Some("Step"), &Limits::default()).unwrap_err().to_string();
|
||||
assert!(e.contains("edges of quad8") && e.contains("tri3 and quad4"), "{e}");
|
||||
// A plane strain ring with arc-length control.
|
||||
let arc = NonlinearStatic { arc_length: Some(ArcLength::default()), ..NonlinearStatic::default() };
|
||||
b.model.replace_step(Step { procedure: Procedure::NonlinearStatic(arc), ..b.model.step("Step").unwrap().clone() }).unwrap();
|
||||
let e = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap_err().to_string();
|
||||
assert!(e.contains("arc-length"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "scratch"]
|
||||
fn scratch_quadratic_2d() {
|
||||
let (_, r2) = planar_c_ring(0.15, 1);
|
||||
report(&r2);
|
||||
quadratic(&r2);
|
||||
}
|
||||
@@ -740,6 +740,10 @@ pub fn show(app: &mut ModelApp, ctx: &egui::Context) {
|
||||
/// One row per sample: its name, what it shows and an Open button; `open` gets the one clicked.
|
||||
pub fn sample_rows(ui: &mut egui::Ui, t: &crate::theme::Tokens, open: &mut Option<&'static str>) {
|
||||
let width = ui.available_width().clamp(280.0, 420.0);
|
||||
egui::ScrollArea::vertical().auto_shrink([false, true]).show(ui, |ui| sample_list(ui, t, open, width));
|
||||
}
|
||||
|
||||
fn sample_list(ui: &mut egui::Ui, t: &crate::theme::Tokens, open: &mut Option<&'static str>, width: f32) {
|
||||
for sample in fieldcraft_engine::sample::SAMPLES {
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# Examples
|
||||
|
||||
Every example is a built-in sample: open it from **File ▸ Samples** (or the start screen), with
|
||||
`fieldcraft --sample NAME`, or with the `file.sample` command (`{"name": "drop-test"}`), then
|
||||
press **Solve** (`solve.run`). Each opens fully defined — geometry, mesh, materials, boundary
|
||||
conditions, loads and steps — in mm, t, s, N and MPa, and opens and solves within half a minute. The geometry is our
|
||||
own: SolidCraft scripts run when the sample opens (a recipe from [`tests/corpus`](../tests/corpus)
|
||||
or one written in `crates/engine/src/sample.rs`). Each sample has a test in
|
||||
`crates/engine/src/sample.rs` that opens it, checks it clean, solves it and holds its result to
|
||||
the check given below.
|
||||
|
||||
## Drop test (`drop-test`)
|
||||
|
||||

|
||||
|
||||
An aluminium L-bracket (60 × 40 mm foot, 50 mm upright, 8 mm thick, R6 fillet) falls 2 m onto a
|
||||
rigid floor tilted 15°, so the end of its foot strikes first. Explicit dynamics for 1 ms with
|
||||
gravity: 1 911 swept hex8 elements (one-point, stiffness hourglass control), bilinear plasticity
|
||||
(yield 240 MPa), the floor a rigid wall. About 8 400 increments, a few seconds.
|
||||
|
||||
**Look at:** the frames from the strike (Results ▸ Frame): the compression wave leaves the
|
||||
struck corner and the upright's inertia bends the fillet, where von Mises peaks near 120 MPa —
|
||||
below yield, so the drop leaves no permanent set. The energies plot shows the kinetic energy
|
||||
turning into vibration with a balance under 0.01 %.
|
||||
|
||||
**Check:** the mass equals ρ V of the bracket (0.5 %); it meets the floor at √(2 g h) =
|
||||
6.26 m/s, with ½ m v² = 1 748 mJ of kinetic energy (1 %); energy is conserved (balance < 1 %);
|
||||
by 1 ms the floor has taken about half its downward momentum.
|
||||
|
||||
## Rubber seal (`rubber-seal`)
|
||||
|
||||

|
||||
|
||||
A rubber gasket ring (20 mm inside radius, 30 mm outside, 10 mm high) squeezed 2.5 mm (25 %)
|
||||
between two plates bonded to it. Generic rubber, E = 6 MPa, ν = 0.49, Mooney–Rivlin with
|
||||
C01 = 0.2 MPa; 1 000 hybrid (u–p) hex8 elements in four mapped quarter blocks. Nonlinear static
|
||||
with large deformation: Newton–Raphson in nine increments, under a second.
|
||||
|
||||
**Look at:** the deformed shape at scale 1 — the free inner and outer walls bulge out, and
|
||||
von Mises peaks (about 7 MPa) where the bulge meets the bonded plates. Results ▸ Increment steps
|
||||
through the converged increments.
|
||||
|
||||
**Check:** the same ring between frictionless plates compresses evenly, and its force matches
|
||||
the closed form for an incompressible Mooney–Rivlin solid in uniaxial compression,
|
||||
P = 2 (λ − λ⁻²)(C10 + C01/λ) per original area: 3 430 N against 3 466 N (within 3 %). Bonded,
|
||||
the bulge makes it stiffer (shape factor 0.5): 6 518 N, held between 1.05 and 2.5 times the
|
||||
frictionless force. The groove and the plates are prescribed displacements today; with implicit
|
||||
contact (#161) the ring will be pressed by contact against a groove.
|
||||
|
||||
## Flow past a bracket (`flow-past`)
|
||||
|
||||
Air (ρ = 1.2 kg/m³, μ = 1.8 × 10⁻⁵ Pa s) at 25 mm/s past the same L-bracket standing on the
|
||||
floor, the upright's back to the wind. A 240 × 120 × 100 mm box of 2.5 mm cells (184 320),
|
||||
uniform inlet, outlet at zero pressure, a no-slip floor and slip walls at the sides and top;
|
||||
D3Q19 lattice Boltzmann (TRT) for 16 s, about 6.7 flow-throughs, ten frames. Reynolds number
|
||||
83 on the upright's 50 mm height: laminar, a steady wake. About 10 s.
|
||||
|
||||
**Look at:** the step's report (`solve.fluid`, or Results after Solve): the force on every
|
||||
named face of the bracket and on each side of the box, final and time-averaged, and the mass
|
||||
history; `solve.probe` with `points` gives the velocity and pressure in any cell, and with
|
||||
`forces` each face's force history.
|
||||
|
||||
**Check:** the drag on the bracket, as a coefficient on the upright's frontal area
|
||||
(40 × 50 mm), is 2.3 — a bluff plate standing on a wall at low Reynolds number in a box it
|
||||
blocks by 17 %, held between 1 and 3.5 (a flat plate broadside to the flow is about 1.2 at high
|
||||
Reynolds number and rises as it falls); the lattice's mass changes by less than 1 %.
|
||||
|
||||
There is no picture yet: the viewport does not draw flow results (velocity slices and
|
||||
streamlines are planned in M12), so this example is numbers only for now.
|
||||
|
||||
## Heated bracket (`heated-bracket`)
|
||||
|
||||

|
||||
|
||||
The corpus filleted bracket (`tests/corpus/filleted-bracket`: an 8 mm L-bracket, R6 fillet, a
|
||||
hole through each leg) in steel, both hole bores clamped. The end of the upright is held at
|
||||
120 °C by a heater; the rest of its outside loses heat to still air at 20 °C
|
||||
(h = 10 W/(m² K)); steel conducts with k = 50 W/(m K), expands with α = 12 × 10⁻⁶/K and is
|
||||
stress free at 20 °C. One coupled temperature–displacement step (steady) on 23 641 tet10
|
||||
elements: about 8 s to open (meshing) and 13 s to solve.
|
||||
|
||||
**Look at:** Contours ▸ Temperature: the upright stays within a few degrees of the heater and
|
||||
the foot falls to 95 °C, a steady balance between conduction and the air. Contours ▸ Von Mises
|
||||
shows the stress of the growth the clamps hold back: tens of MPa through the legs, peaking at the
|
||||
edges of the clamped bores, where a rigid clamp is a stress singularity (about 1 GPa there, a
|
||||
figure that grows as the mesh is refined: read the stress a little away from the bore).
|
||||
|
||||
**Check:** the temperatures lie between the air's and the heater's and fall away from the heater.
|
||||
For the expansion, the test puts the air at the heater's temperature, so the bracket warms
|
||||
evenly by ΔT = 100 K, and clamps the foot's hole alone: the bracket then grows freely about it,
|
||||
and a point moves α ΔT times its distance from the hole's axis. The upright's top moves −0.0432
|
||||
mm along x (within 2 %); the foot's end, 16 mm from the clamped bore, which cannot grow, moves
|
||||
0.0228 mm against 0.024 mm (within 6 %).
|
||||
|
||||
## Hex-meshed bracket (`bracket-hex`)
|
||||
|
||||

|
||||
|
||||
The same filleted bracket split into 30 cells in SolidCraft
|
||||
(`tests/corpus/filleted-bracket-blocks`: planar cuts through the legs and diagonal cuts round
|
||||
the holes and the fillet, each cell a six-sided block), meshed by `mesh.hex.blocks` as one
|
||||
conforming all-hex mesh: 5 778 hex20 elements of 3 mm, O-grids round the holes, graded through
|
||||
the fillet, every shared face meshed once. Steel; the upright's end clamped, 1 kN down on the
|
||||
foot's end. Linear static: about 2 s to open (the cells are rebuilt from the script) and 3 s to
|
||||
solve.
|
||||
|
||||
**Look at:** the mesh (Prepare ▸ Mesh) — mapped blocks meeting face to face with no pyramids or
|
||||
tets — and the von Mises contours on the deformed shape: the stress runs round the fillet and
|
||||
peaks at 295 MPa at the upright's hole.
|
||||
|
||||
**Check:** the tip of the foot moves 0.509 mm down, within 1 % of a 48 016-element tet10
|
||||
solution of the unpartitioned part (0.5093 mm, `crates/testkit/tests/bracket_blocks.rs`), and the
|
||||
clamp carries the whole 1 kN.
|
||||
|
||||
## Bicycle crank arm (`crank-arm`)
|
||||
|
||||

|
||||
|
||||
A 170 mm aluminium crank built in SolidCraft (`crates/engine/samples/crank-arm.json`): a 16 mm
|
||||
arm tapering from 38 to 24 mm, root arcs into a Ø46 × 22 spindle boss with a 12.7 mm square bore
|
||||
(R1.5 corners) and a Ø30 × 20 pedal boss with a Ø14 hole, and an 82 × 14 × 8 pocket in its back
|
||||
(R5 corners). It is a stack of extrusions along z, so `mesh.hex.generate` sweeps it all-hex:
|
||||
7 274 hex20 elements of 3 mm, all valid, scaled Jacobian min 0.17, mean 0.83. To keep it
|
||||
sweepable the hex variant leaves out the pocket's floor fillet and the bosses' 1.5 mm chamfers,
|
||||
and its root arcs meet the bosses about 20° off tangent (a tangent meeting leaves a zero-angle
|
||||
cusp on the arm's top face, where any hex is degenerate). 6061-T6 (E = 68 900 MPa, ν = 0.33); the
|
||||
square bore's faces fixed, 1500 N on the pedal hole in the crank's plane, across the arm (−y).
|
||||
Linear static: about 4 s to open and 7 s to solve.
|
||||
|
||||
**Look at:** the swept mesh (Prepare ▸ Mesh), eight layers through the arm, and the von Mises
|
||||
contours: the bending stress grows along the arm toward the root; the peak, 129 MPa, sits at a
|
||||
square-bore corner where the rigid clamp meets the free face, a support singularity.
|
||||
|
||||
**Check:** σxx on the arm's sides at x = 29 mm is ±62.4 MPa against the beam formula
|
||||
M/Z = 1500 · 141 / (16 · 35.6² / 6) = 62.6 MPa (0.3 %); a 41 815-element tet10 solution of the
|
||||
full part gives 62.3 MPa. The pedal bore moves 0.75 mm down (tet10: 0.70–0.75 mm; Euler–Bernoulli
|
||||
with a rigid clamp: 0.58 mm, without shear or the boss's compliance), and the spindle carries the
|
||||
whole 1500 N and 255 N·m.
|
||||
+4
-4
@@ -6,7 +6,7 @@ Every entry is a toolbar button. **Ready** entries run their command; **coming s
|
||||
shown greyed with the roadmap milestone that brings them (ROADMAP.md). Scripts, the command line,
|
||||
the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
||||
|
||||
59 ready, 45 coming soon.
|
||||
60 ready, 44 coming soon.
|
||||
|
||||
## Geometry
|
||||
|
||||
@@ -102,9 +102,9 @@ the control channel and AI agents over MCP (docs/mcp.md) run the same commands.
|
||||
|---|---|---|---|
|
||||
| Constraints | Reference Point — A reference point for couplings, loads and boundary conditions | ready | `model.reference_point.create`, `model.reference_point.edit`, `model.reference_point.delete` |
|
||||
| Constraints | Coupling — Kinematic or distributing coupling of a set to a reference point | ready | `model.coupling.create`, `model.coupling.edit`, `model.coupling.delete` |
|
||||
| Constraints | Tie — Tie non-matching meshes together | coming soon (M7) | |
|
||||
| Contact | General Contact — Contact between everything that may touch, or self contact of a surface, with friction (explicit dynamics) | ready | `interaction.contact.general`, `interaction.contact.edit`, `interaction.contact.delete` |
|
||||
| Contact | Contact Pair — Contact between two surfaces, with friction (explicit dynamics) | ready | `interaction.contact.pair`, `interaction.contact.edit`, `interaction.contact.delete` |
|
||||
| Constraints | Tie — Tie non-matching meshes together (dual mortar constraints) | ready | `interaction.tie`, `interaction.tie.edit`, `interaction.tie.delete` |
|
||||
| Contact | General Contact — Contact between everything that may touch, or self contact of a surface, with friction (explicit dynamics and nonlinear static steps) | ready | `interaction.contact.general`, `interaction.contact.edit`, `interaction.contact.delete` |
|
||||
| Contact | Contact Pair — Contact between two surfaces: with friction in explicit dynamics, mortar contact with friction in nonlinear static steps | ready | `interaction.contact.pair`, `interaction.contact.edit`, `interaction.contact.delete` |
|
||||
| Contact | Find Contacts — Find the parts of the mesh that touch, and make contact pairs of them | ready | `interaction.contact.find` |
|
||||
| Contact | Rigid Wall — Rigid walls for explicit dynamics | coming soon (M10) | |
|
||||
| Connectors | Bolt Load — Bolt pretension | coming soon (M7) | |
|
||||
|
||||
+144
-2
@@ -146,7 +146,10 @@ pressure field. The choice of pressure space follows the inf-sup analysis (docs
|
||||
checkerboard pressures; linear tetrahedra and wedges lock even so and are refused. A thermal
|
||||
strain splits the same way: ∫ Bᵀ D_dev ε_th on the nodes and, through the pressure,
|
||||
K G M⁻¹ ∫ N_p tr ε_th, so a heated nearly incompressible solid takes its bulk stress −3KαΔT
|
||||
when confined. A continuous
|
||||
when confined. Plane strain and axisymmetric quad4, quad8 and tri6 sections take one constant
|
||||
pressure (the mean dilatation, the hoop strain in the divergence): K_dev + κ g gᵀ / V with
|
||||
g = ∫ Bᵀ m dV (`crates/fem/src/plane.rs`); the linear triangle locks and plane stress has no
|
||||
volume constraint, so both are refused. A continuous
|
||||
nodal pressure (`DofKind::P`, Taylor–Hood) needs a symmetric indefinite factorization and is
|
||||
planned.
|
||||
|
||||
@@ -251,6 +254,7 @@ differences of its forces.
|
||||
| Element | Nonlinear steps |
|
||||
|---|---|
|
||||
| tet4, tet10, wedge6, hex8, hex20 (full), hex20 (reduced) | total Lagrangian |
|
||||
| hybrid quad4, quad8, tri6 (plane strain, axisymmetric) | mean dilatation: θ = ∫ J dV₀ / V₀ with the hoop stretch, p = U′(θ), stiffness U″ g gᵀ / V₀ |
|
||||
| hybrid hex8, hex20, tet10 | Simo–Taylor–Pister three-field form on the linear hybrid elements' pressure fields (θ the projection of J, p the projection of U′(θ)); needs the neo-Hookean option (St Venant–Kirchhoff has no volumetric split) |
|
||||
| hex8 and quad4 incompatible modes | refused: their finite-strain form (enhanced assumed strain, Simo–Armero 1992) needs the modes as unknowns carried between iterations and hourglasses in large compression; use full integration, hybrid or quadratic elements |
|
||||
| plane strain, axisymmetric (tri3, tri6, quad4, quad8) | total Lagrangian with F₃₃ = 1 or r/R (hoop column N/r, geometric term τ_θθ N_a N_b/r²) |
|
||||
@@ -408,7 +412,8 @@ analysis stops naming the node, the face and the depth). Planar rigid walls take
|
||||
Commands `interaction.contact.pair|general|edit|delete|find`; Abaqus writes `*SURFACE
|
||||
INTERACTION` with `*FRICTION` (exponential decay), `*CONTACT PAIR` and general `*CONTACT`, LS-DYNA
|
||||
`*CONTACT_AUTOMATIC_SURFACE_TO_SURFACE` and `_SINGLE_SURFACE` (`fs`, `fd`, `dc`, `sfsa`,
|
||||
`sfsb`); Nastran and the implicit solver refuse contact by name (implicit contact is M7).
|
||||
`sfsb`); Nastran and the linear implicit analyses refuse contact by name; nonlinear static steps
|
||||
take contact pairs (next section).
|
||||
|
||||
**Geometry** (`fieldcraft_fem::contact`, shared with the implicit solver to come): the closest
|
||||
point of a triangle (exact region tests) or of a bilinear quadrilateral (projected Newton with the
|
||||
@@ -451,6 +456,143 @@ Hertz (peak force +2.3%, duration −0.4% on a graded quarter-sphere mesh), a fr
|
||||
by general contact on all its faces, initial penetrations adjusted or refused, the face cap, and
|
||||
bit-for-bit results on one and four threads.
|
||||
|
||||
## Contact in nonlinear static steps (mortar, augmented Lagrangian)
|
||||
|
||||
**Discretisation** (`fieldcraft_solver::contact`): mortar contact, one pass, the pair's
|
||||
`secondary` surface the non-mortar side. Each secondary node A carries a pressure p_A and the
|
||||
weighted gap g̃_A = Σ ∫ Φ_A (x_m − x_s)·n dA over the overlaps of its faces with the main faces
|
||||
facing them, Φ_A the secondary face's own shape functions; ĝ_A = g̃_A / A_A with A_A = ∫ Φ_A over
|
||||
the node's faces. The pressure field is interpolated like the displacements, so a uniform
|
||||
pressure crosses a non-matching interface as the consistent nodal forces of both meshes: the
|
||||
contact patch test. General (single-surface) contact uses one face list, a set's or every
|
||||
exterior face of the solids, as both sides: each face against every other one facing it that
|
||||
shares no node with its element (its neighbours) and that it does not reach through more than
|
||||
half an element (the far side of a thin wall), paired both ways, so the folds of one body press
|
||||
on each other symmetrically. Linear faces (tri3, quad4: tet4, wedge6, hex8), and in 2D models
|
||||
(plane strain, plane stress, axisymmetric) the linear edges of tri3 and quad4 elements (sets'
|
||||
edges; general contact defaults to every exterior edge); quadratic faces and edges and
|
||||
arc-length steps are refused by name.
|
||||
|
||||
**2D models**: each edge is oriented with its normal (t_y, −t_x) out of its element; the main
|
||||
edge is projected along the secondary edge's normal onto its line, which maps main points to the
|
||||
secondary parameter linearly, so the overlap is an interval integrated with three-point
|
||||
Gauss–Legendre (exact for the degree-3 Φ_a N_b r of the axisymmetric patch test). The
|
||||
out-of-plane measure (the thickness, or 2πr at the secondary point) is in the weights, so A_A,
|
||||
g̃_A and the forces are totals over the slab or the ring, as the 2D elements' are (B. Yang,
|
||||
T. A. Laursen, X. Meng, IJNME 62:1183–1225, 2005). Points keep z = 0 and the same 24 variables
|
||||
as faces, and the tangent blocks keep their in-plane rows. The element's thickness under an edge
|
||||
is its area over the edge's length.
|
||||
|
||||
**Segmentation** (`fieldcraft_fem::mortar`): both faces are projected along the secondary
|
||||
face's unit normal onto its plane, the main polygon is clipped by the secondary one
|
||||
(Sutherland–Hodgman; a projected quadrilateral that is not convex goes as its two triangles), and
|
||||
the overlap's fan of triangles is integrated with Radon's seven-point degree-5 rule. At each point
|
||||
both faces' coordinates come from inverting their projected maps (linear, or bilinear by the root
|
||||
of its quadratic), so the main point is on the main surface along the normal, warped or not. A
|
||||
vertex within 10⁻⁸ of a clip edge's length of its line is on it and never cut: the intersection
|
||||
of two nearly coincident edges is ill-conditioned and its derivative swamped by round-off. The
|
||||
kernel is written over a generic scalar: `f64` for values, forward-mode `Dual` for gradients and
|
||||
`Hyper` (gradient and packed Hessian in the 24 corner coordinates) for the consistent tangent,
|
||||
the exact linearisation of the segmentation, normal and projection (`fieldcraft_fem::dual`).
|
||||
|
||||
**Enforcement**: augmented Lagrangian. With the multipliers λ_A fixed, p_A = ⟨λ_A − ε_A ĝ_A⟩,
|
||||
the derivative of Π = Σ A_A/(2ε_A) ⟨λ_A − ε_A ĝ_A⟩²: internal forces −p_A ∂g̃_A/∂x and the tangent
|
||||
Σ_closed (ε_A/A_A) ∂g̃_A ⊗ ∂g̃_A − p_A ∂²g̃_A (the second term the contact's geometric stiffness).
|
||||
Once Newton converges an increment, λ_A ← p_A (Uzawa) and the increment is solved again from its
|
||||
solution, until every closed node's gap is within 10⁻⁵ of the thickness of the element under it
|
||||
and no open node keeps a multiplier (at most 25 augmentations, then a warning). The penalty per
|
||||
length is ε_A = s · 10 · 3Ḡ/h (Ḡ the mean shear modulus, h = V/A of the element under the face,
|
||||
s the contact's `penalty_scale`): about ten times the local element stiffness, and 3Ḡ rather than
|
||||
the constrained modulus keeps it so for nearly incompressible rubber.
|
||||
|
||||
**Active set and Newton**: the max() decides at every evaluation which nodes are closed
|
||||
(semismooth Newton); an increment whose active set alternates between two states three times is
|
||||
cut back (chattering). The tangent's pattern takes the blocks of the closed nodes' face pairs on
|
||||
top of the mesh's. With contact, the force scale of the convergence test is that of the
|
||||
converged states, the loads and the current state rather than the largest force of every trial
|
||||
(a trial can press a body deep into another before the active set settles). The augmentations'
|
||||
re-solutions are measured against the increment's start and its first correction; the next
|
||||
increment's size follows the increment's own iterations.
|
||||
|
||||
**Small and finite sliding**: without nonlinear geometry the face pairs (those within a main
|
||||
element's thickness) and their mortar integrals D_ai = ∫ Φ_a N_i, M_aj = ∫ Φ_a N_j are fixed in the
|
||||
reference configuration, so g̃_a = n · (Σ M_aj x_j − Σ D_ai x_i) is linear: exact and quadratic-free.
|
||||
With nonlinear geometry the pairs are searched (bucket grid of the main faces' boxes grown by
|
||||
their depth limit) and integrated on the current configuration at every evaluation; a pair deeper
|
||||
than half the main element's thickness has passed through and is left out.
|
||||
|
||||
**Friction**: Coulomb's law at the closed secondary nodes by a return map (Wriggers 2006, §5.2;
|
||||
Simo & Laursen 1992). The node's weighted tangential slip since the last converged increment is
|
||||
w_A = B_A Δx with B_A = [∫ Φ_A N_i P, −∫ Φ_A N_j P] (P = I − n n; the rate form of the mortar slip,
|
||||
McDevitt & Laursen 2000), ĝ_T = w_A / A_A; the trial traction t* = P t_n − ε_A ĝ_T sticks when
|
||||
|t*| ≤ μ p_A and otherwise returns to the cone, t = μ p_A t*/|t*|, with the static coefficient for
|
||||
a node that stuck in the last increment and the kinetic one for one that slipped (a static step
|
||||
has no slip rate for the exponential decay). The traction does the virtual work t · B_A δx, so a
|
||||
slipping interface carries exactly μ times its normal force. The tangent is symmetrised: it
|
||||
keeps (ε_A/A_A) Bᵀ B in stick and (ε_A μ p_A/(A_A |t*|)) Bᵀ (I − t̂ t̂) B in slip and leaves out
|
||||
the coupling to the pressure (μ t̂ ⊗ ∂p_A/∂x) and the change of B with the geometry, so the
|
||||
factorization stays the symmetric one, like the follower pressure's; the residual is exact, so
|
||||
only the rate of convergence suffers while nodes slip. Tractions and slip states are committed
|
||||
when an increment converges. If an increment's active set chatters (a node touching with no
|
||||
pressure, at the edge of a contact that lifts under friction), the increment is cut back and the
|
||||
nodes open by less than 10⁻³ of their element's thickness keep their penalty in the tangent for
|
||||
the rest of the step.
|
||||
|
||||
**Initial overclosure**: nodes whose gap starts negative are reported; `adjust` takes the overlap
|
||||
out of their weighted gap (an offset: strain-free, nodes are not moved), `refuse` stops naming
|
||||
the node and the depth.
|
||||
|
||||
**Output**: per secondary node the pressure (its normal contact force over the area it carries,
|
||||
∫ Φ_A over the overlaps, which is smoother than the multipliers at the contact's edge), the
|
||||
multiplier, the gap ĝ, the status (open, closed; with friction stick or slip), and with friction
|
||||
the shear traction and the slip over the last increment; per contact the force on the secondary
|
||||
surface, the normal force Σ p_A ∫Φ_A, the deepest penetration and the augmentations.
|
||||
`solve.nonlinear` summarises each contact, `solve.probe` gives a secondary node's `contact`
|
||||
entries, and the results display draws `contact_pressure`, `contact_gap`, `contact_status`,
|
||||
`contact_shear` and `contact_slip`.
|
||||
|
||||
**Verification** (`crates/solver/tests/contact.rs`, `tests/oracle/nl-contact-patch`,
|
||||
`tests/oracle/nl-hertz-cylinder`, and their 2D forms `nl-contact-patch-plane-strain`,
|
||||
`nl-contact-patch-axisymmetric`, `nl-hertz-cylinder-plane-strain`): the contact patch test on
|
||||
non-matching hex8, tet4 and (plane strain and axisymmetric) quad4 meshes
|
||||
(uniform σ_zz, pressure equal to it, no lateral stress: to 10⁻⁹ with small sliding, 10⁻⁴ with
|
||||
finite sliding, where the gaps the augmentation leaves enter through their geometric terms);
|
||||
Hertz's line contact, plane strain (peak pressure within 0.3%, the pressure at two nodes within
|
||||
3% and 6%, the vertical stress under the contact within 2%, the half-width bracketed); Hertz's
|
||||
sphere on a flat, a quarter model (peak pressure within 4%, contact radius bracketed; heavy);
|
||||
Newton's quadratic rate near the solution with finite sliding; Coulomb friction between pressed
|
||||
blocks (stick well below the cone with the forces of glued blocks, slip on it carrying μ N to 0.1%
|
||||
with small sliding and 3% with finite sliding, where the block turns a little, and μ_k N once
|
||||
slipping; `tests/oracle/nl-friction-sliding-blocks`); initial overclosure adjusted and
|
||||
refused; bit-for-bit results on one and four threads; the refusals. In 2D: the patch tests pass
|
||||
to 10⁻⁹ (small sliding) and 10⁻⁴ (finite), Hertz's line contact on quad4 edges matches the
|
||||
hex8 model to 10⁻⁵, and a hybrid Mooney–Rivlin C-ring closing on itself in plane strain moves as
|
||||
the one-layer solid ring with z held, node for node to 10⁻⁵.
|
||||
|
||||
## Ties (non-matching meshes)
|
||||
|
||||
**Model** (`fieldcraft_model::tie`): a tie names a `main` and a `secondary` surface (sets of
|
||||
element faces) and an optional position tolerance; commands `interaction.tie|tie.edit|tie.delete`.
|
||||
Abaqus writes `*TIE, ADJUST=NO` (secondary surface first, `POSITION TOLERANCE` when given),
|
||||
LS-DYNA `*CONTACT_TIED_SURFACE_TO_SURFACE` between segment sets; Nastran refuses ties by name.
|
||||
|
||||
**Constraints** (`fieldcraft_solver::tie`): dual mortar. Each secondary face's dual shape
|
||||
functions ψ_a = Σ_b A_ab N_b, A = D M⁻¹ (M_ab = ∫ N_a N_b, D = diag ∫ N_a), are biorthogonal to its
|
||||
own, so the secondary mortar matrix is diagonal and each secondary node becomes the weighted
|
||||
average of the main nodes under it, u_A = Σ_J (M_AJ/Σ_K M_AK) u_J with M_AJ = ∫ ψ_A N_J over the
|
||||
overlaps of the reference configuration (`fieldcraft_fem::mortar`). The rows go to the constraint
|
||||
elimination like a coupling's, the secondary node the intended dependent, in linear static,
|
||||
frequency, buckling and nonlinear steps (small-displacement constraints). Dividing by the sum
|
||||
keeps rigid motions exact at a surface's edge where a face is only partly covered. Main faces
|
||||
count when the secondary face overlaps them along its normal with a mean gap within the position
|
||||
tolerance (default half the main element's thickness); uncovered secondary nodes stay free with a
|
||||
warning. Heat transfer and explicit dynamics refuse ties by name.
|
||||
|
||||
**Verification** (`crates/solver/tests/contact.rs`, `tests/oracle/tie-patch`): a linear field
|
||||
across non-matching hex8/tet4 meshes reproduced to round-off (to 10⁻⁶ on distorted quadrilateral
|
||||
faces, where the overlap integration is accurate to its degree) in linear and nonlinear static
|
||||
steps; uniaxial tension across a tie, σ_zz = Eε in every element to 10⁻⁹.
|
||||
|
||||
## Hyperelastic materials in explicit dynamics
|
||||
|
||||
Hyperelastic materials (neo-Hookean, Mooney–Rivlin, Yeoh) use the shared finite-strain material
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 278 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 572 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 350 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 247 KiB |
+78
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the policy is [verification.md](verification.md). Every case is our own model, built with FieldCraft commands from the problem's published description, and compared with the reference value its `expected.json` cites, within the stated tolerance and for the stated reason. Accepted results are frozen in each case's `fixture.json`: a later change that moves one beyond its band (10⁻⁹ of its size) fails CI even while the reference still passes. No reference value was produced with a commercial product.
|
||||
|
||||
**201 of 207 quantities pass** (6 known failure(s)) in 62 active cases: 53 theory, 9 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built.
|
||||
**270 of 276 quantities pass** (6 known failure(s)) in 70 active cases: 61 theory, 9 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built.
|
||||
|
||||
## Theory: closed-form solutions, patch tests and mesh invariants
|
||||
|
||||
@@ -80,6 +80,51 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [lame-cylinder-plane-strain](../tests/oracle/lame-cylinder-plane-strain/) | radial displacement at the bore (10, 0) | 0.0090794 | 0.0090793 | -0.000267% | 0.1% | pass | matches |
|
||||
| [lame-cylinder-plane-strain](../tests/oracle/lame-cylinder-plane-strain/) | hoop stress σ_θ = σ_yy at the bore (10, 0) | 166.67 | 166.36 | -0.183% | 1% | pass | matches |
|
||||
| [lame-cylinder-plane-strain](../tests/oracle/lame-cylinder-plane-strain/) | axial stress σ_zz = 2νA at the bore (plane strain) | 20 | 20.083 | 0.415% | 1% | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Small: σ_zz of the lower block's corner element under the interface (element 10) over that of the upper block's corner element over it (element 19) | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Small: σ_zz of the lower block's centre element under the interface (element 14) over that of element 19 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Small: σ_zz of the lower block's bottom corner element (element 1) over that of element 19 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Small: σ_zz of the upper block's top far corner element (element 26) over that of element 19 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Small: σ_xx of element 14 over its σ_zz (uniaxial stress: 0) | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-6 | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Small: the contact pressure at the secondary node (0.5, 0.5, 1) over −σ_zz of element 19 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Small: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Finite: σ_zz of the lower block's corner element under the interface (element 10) over that of the upper block's corner element over it (element 19) | 1 | 1 | 0.000151% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Finite: σ_zz of the lower block's centre element under the interface (element 14) over that of element 19 | 1 | 0.99999 | -0.00129% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Finite: σ_zz of the lower block's bottom corner element (element 1) over that of element 19 | 1 | 0.99999 | -0.000942% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Finite: σ_zz of the upper block's top far corner element (element 26) over that of element 19 | 1 | 0.99999 | -0.000872% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Finite: σ_xx of element 14 over its σ_zz (uniaxial stress: 0) | 0 | ≈ 0 | ≈ 0 | ± 2.0000e-4 | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Finite: the contact pressure at the secondary node (0.5, 0.5, 1) over −σ_zz of element 19 | 1 | 0.99998 | -0.00172% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Finite: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Small: σ_zz of element 19 (uniaxial stress E ε = −10 for the 1% squeeze) | -10 | -9.9996 | 0.00426% | 0.01% | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_yy of element 4 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_yy of element 6 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_yy of element 1 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_yy of element 10 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_xx of element 6 over its σ_yy (uniaxial stress: 0) | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-6 | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: the contact pressure at the secondary node (1.5, 1) over −σ_yy of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: σ_yy of element 4 over that of element 7 | 1 | 1 | 0.0000126% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: σ_yy of element 6 over that of element 7 | 1 | 0.99997 | -0.00325% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: σ_yy of element 1 over that of element 7 | 1 | 0.99999 | -0.00149% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: σ_yy of element 10 over that of element 7 | 1 | 0.99998 | -0.00185% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: σ_xx of element 6 over its σ_yy (uniaxial stress: 0) | 0 | -6.2023e-6 | -6.2e-6 | ± 2.0000e-4 | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: the contact pressure at the secondary node (1.5, 1) over −σ_yy of element 7 | 1 | 0.99997 | -0.00285% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_yy of element 7 (uniaxial stress E ε = −10 for the 1% squeeze) | -10 | -9.9996 | 0.00426% | 0.01% | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_yy of element 4 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_yy of element 6 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_yy of element 1 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_yy of element 10 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_xx of element 6 over its σ_yy (uniaxial stress: 0) | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-6 | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: the contact pressure at the secondary node (0.5, 1) over −σ_yy of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: σ_yy of element 4 over that of element 7 | 1 | 1 | 0.0000708% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: σ_yy of element 6 over that of element 7 | 1 | 0.99999 | -0.00087% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: σ_yy of element 1 over that of element 7 | 1 | 1 | -0.000312% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: σ_yy of element 10 over that of element 7 | 1 | 1 | -0.000428% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: σ_xx of element 6 over its σ_yy (uniaxial stress: 0) | 0 | ≈ 0 | ≈ 0 | ± 2.0000e-4 | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: the contact pressure at the secondary node (0.5, 1) over −σ_yy of element 7 | 1 | 0.99999 | -0.00075% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 0.02% | pass | matches |
|
||||
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_yy of element 7 (plane strain uniaxial stress E ε/(1 − ν²) = −10.989 for the 1% squeeze) | -10.989 | -10.988 | 0.00516% | 0.01% | pass | matches |
|
||||
| [nl-cylinder-inflation](../tests/oracle/nl-cylinder-inflation/) | radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15 | 5 | 5.004 | 0.0793% | 0.5% | pass | matches |
|
||||
| [nl-cylinder-inflation](../tests/oracle/nl-cylinder-inflation/) | radial displacement of the outer wall (20, 0, 0) | 2.9129 | 2.9173 | 0.152% | 0.5% | pass | matches |
|
||||
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip deflection δ_v/L at P L²/(E I) = 1 | 0.30172 | 0.30226 | 0.179% | 1% | pass | matches |
|
||||
@@ -90,6 +135,21 @@ 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-friction-sliding-blocks](../tests/oracle/nl-friction-sliding-blocks/) | the tangential over the normal force across the sliding interface | 0.2 | 0.2 | -0.000103% | 0.1% | pass | matches |
|
||||
| [nl-friction-sliding-blocks](../tests/oracle/nl-friction-sliding-blocks/) | the tangential contact force on the upper block over the sideways reaction at its top (they balance) | -1 | -1 | 0.00000224% | 1e-6 rel. | pass | matches |
|
||||
| [nl-friction-sliding-blocks](../tests/oracle/nl-friction-sliding-blocks/) | the secondary nodes that slip (all 9) | 9 | 9 | ≈ 0 | exact | pass | matches |
|
||||
| [nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/) | the largest contact pressure p₀ (at x = 0) | 2884.6 | 2892 | 0.254% | 2% | pass | matches |
|
||||
| [nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/) | the force across the contact (half model, 1 mm thick) | 1132.8 | 1133 | 0.0221% | 0.2% | pass | matches |
|
||||
| [nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/) | the contact pressure at the secondary node at x = 0.29594 (node 7) | 2325.1 | 2257.8 | -2.89% | 5% | pass | matches |
|
||||
| [nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/) | the contact pressure at the secondary node at x = 0.36701 (node 8) | 1959 | 2078.4 | 6.09% | 10% | pass | matches |
|
||||
| [nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/) | the contact pressure at the secondary node at x = 0.63325 (node 11, beyond the half-width b = 0.5) | 0 | ≈ 0 | ≈ 0 | ± 1 | pass | matches |
|
||||
| [nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/) | the vertical stress σ_yy under the centre of contact at depth 0.47127 (node 713) | -2099.2 | -2059.8 | 1.87% | 4% | pass | matches |
|
||||
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the largest contact pressure p₀ (at x = 0) | 2884.6 | 2892 | 0.255% | 2% | pass | matches |
|
||||
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the force across the contact (half model, 1 mm thick) | 1132.8 | 1133 | 0.0228% | 0.2% | pass | matches |
|
||||
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the contact pressure at the secondary node at x = 0.29594 | 2325.1 | 2257.9 | -2.89% | 5% | pass | matches |
|
||||
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the contact pressure at the secondary node at x = 0.36701 | 1959 | 2078.4 | 6.09% | 10% | pass | matches |
|
||||
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the contact pressure at the secondary node at x = 0.63325 (beyond the half-width b = 0.5) | 0 | ≈ 0 | ≈ 0 | ± 1 | pass | matches |
|
||||
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the vertical stress σ_yy under the centre of contact at depth 0.47127 | -2099.2 | -2059.8 | 1.87% | 4% | pass | matches |
|
||||
| [nl-hybrid-incompressible](../tests/oracle/nl-hybrid-incompressible/) | axial Cauchy stress, stretch λ = 2, ν = 0.4999 | 3.4988 | 3.4988 | 0.000000135% | 1e-6 rel. | pass | matches |
|
||||
| [nl-hybrid-incompressible](../tests/oracle/nl-hybrid-incompressible/) | lateral displacement at (1, 1, 1): l − 1 | -0.29281 | -0.29281 | < 1e-7% | 1e-6 rel. | 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 |
|
||||
@@ -101,6 +161,8 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [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-mooney-rivlin-tube-axisymmetric](../tests/oracle/nl-mooney-rivlin-tube-axisymmetric/) | radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15 at λ_z = 1.5 | 5 | 5.0329 | 0.658% | 1% | pass | matches |
|
||||
| [nl-mooney-rivlin-tube-axisymmetric](../tests/oracle/nl-mooney-rivlin-tube-axisymmetric/) | radial displacement of the outer wall (20, 0, 0): b − B, a small difference of radii | 0.61553 | 0.64473 | 2.9e-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 |
|
||||
@@ -187,6 +249,13 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
| [rotating-disc-plane-stress](../tests/oracle/rotating-disc-plane-stress/) | hoop stress σ_θ = σ_yy at the bore (20, 0) | 65.312 | 65.188 | -0.19% | 1% | pass | matches |
|
||||
| [rotating-disc-plane-stress](../tests/oracle/rotating-disc-plane-stress/) | radial stress σ_r = σ_xx at r = √(ab), its maximum | 20.724 | 20.683 | -0.2% | 1% | pass | matches |
|
||||
| [rotating-disc-plane-stress](../tests/oracle/rotating-disc-plane-stress/) | hoop stress σ_θ = σ_yy at the rim (100, 0) | 16.328 | 16.352 | 0.149% | 1% | pass | matches |
|
||||
| [tie-patch](../tests/oracle/tie-patch/) | σ_zz of the lower block's corner element under the tie (element 10) | 10 | 10 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [tie-patch](../tests/oracle/tie-patch/) | σ_zz of the lower block's centre element under the tie (element 14) | 10 | 10 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [tie-patch](../tests/oracle/tie-patch/) | σ_zz of the lower block's bottom corner element (element 1) | 10 | 10 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [tie-patch](../tests/oracle/tie-patch/) | σ_zz of the upper block's corner element over the tie (element 19) | 10 | 10 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [tie-patch](../tests/oracle/tie-patch/) | σ_zz of the upper block's top far corner element (element 26) | 10 | 10 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [tie-patch](../tests/oracle/tie-patch/) | σ_xx of element 14 (uniaxial stress: 0) | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-9 | pass | matches |
|
||||
| [tie-patch](../tests/oracle/tie-patch/) | u_z at the tied secondary node (0.5, 0.5, 1) | 0.01 | 0.01 | < 1e-7% | 1e-9 rel. | pass | matches |
|
||||
| [tube-torsion-coupling](../tests/oracle/tube-torsion-coupling/) | rotation of the reference point about z | 0.056071 | 0.056071 | 0.000619% | 0.5% | pass | matches |
|
||||
| [tube-torsion-coupling](../tests/oracle/tube-torsion-coupling/) | surface shear stress τ_θz = σ_yz at (10, 0, 20) | 1078.3 | 1081.4 | 0.288% | 1% | pass | matches |
|
||||
|
||||
@@ -259,12 +328,19 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
- **[nafems-le1](../tests/oracle/nafems-le1/)** (benchmark): NAFEMS LE1 elliptic membrane (hex20 plane stress layer). Source: NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE1 (target σ_yy = 92.7 MPa at D); as reproduced in the Abaqus 2017 Benchmarks Guide, 'LE1: Plane stress elements—elliptic membrane'. Tolerances — syy_D: the target is published to three figures; plane stress modelled by a thin solid layer and the stress extrapolated to the edge add a few tenths of a percent
|
||||
- **[nafems-le1-plane-stress](../tests/oracle/nafems-le1-plane-stress/)** (benchmark): NAFEMS LE1 elliptic membrane with 2D plane stress elements (quad8). Source: NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE1 (target σ_yy = 92.7 MPa at D); as reproduced in the Abaqus 2017 Benchmarks Guide, 'LE1: Plane stress elements—elliptic membrane'. Tolerances — syy_D: the target is published to three figures; the stress extrapolated to the edge on a converged mesh adds a few tenths of a percent
|
||||
- **[nafems-le10](../tests/oracle/nafems-le10/)** (benchmark): NAFEMS LE10 thick plate under pressure (hex20). Source: NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE10 (target σ_yy = −5.38 MPa at D); as reproduced in the Abaqus 2017 Benchmarks Guide, 'LE10: Thick plate under pressure'. Tolerances — syy_D: the target is published to three figures; at D the stress is extrapolated to a corner where the hole edge meets the loaded face, and published converged solutions scatter by 1–2% (Abaqus Benchmarks Guide fine meshes: −5.51 to −5.89)
|
||||
- **[nl-contact-patch](../tests/oracle/nl-contact-patch/)** (theory): Contact patch test: two blocks with non-matching meshes in uniaxial compression (implicit mortar contact). Source: The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; Crisfield, Re-visiting the contact patch test, IJNME 48:435–449, 2000; passed by mortar methods: Puso & Laursen, CMAME 193:601–629, 2004. Uniaxial stress between frictionless symmetry planes is uniform in both blocks. Tolerances — small_lower_corner: small sliding is linear and its mortar integrals exact on these rectangular faces: a uniform stress is the discrete solution up to the convergence tolerance (10⁻¹⁰); small_lower_centre: as for the corner element; small_lower_base: as for the corner element; small_upper_top: as for the corner element; small_lateral: frictionless symmetry planes and a frictionless interface leave the blocks free to expand sideways; the non-matching interface must not hold them; small_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; small_force: the pressure carries the whole load across the interface; finite_lower_corner: finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances; finite_lower_centre: as for the corner element; finite_lower_base: as for the corner element; finite_upper_top: as for the corner element; finite_lateral: frictionless symmetry planes and a frictionless interface leave the blocks free to expand sideways; the non-matching interface must not hold them; finite_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; finite_force: the pressure carries the whole load across the interface; small_stress: the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness, a few 10⁻⁵ of the squeeze)
|
||||
- **[nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/)** (theory): Contact patch test, axisymmetric: two rings with non-matching edges in axial compression (implicit mortar contact). Source: The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; passed by 2D mortar methods: Yang, Laursen & Meng, IJNME 62:1183–1225, 2005. Uniaxial stress between frictionless supports is uniform in both blocks. Tolerances — small_lower_corner: small sliding is linear and its edge mortar integrals exact (degree-3 Gauss on the overlaps): the uniform stress is the discrete solution up to the convergence tolerance; small_lower_far: as for the corner element; small_lower_base: as for the corner element; small_upper_top: as for the corner element; small_lateral: a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them; small_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; small_force: the pressure carries the whole load across the interface (both are totals over the circumference); finite_lower_corner: finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances; finite_lower_far: as for the corner element; finite_lower_base: as for the corner element; finite_upper_top: as for the corner element; finite_lateral: a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them; finite_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; finite_force: the pressure carries the whole load across the interface (both are totals over the circumference); small_stress: the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness)
|
||||
- **[nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/)** (theory): Contact patch test in plane strain: two blocks with non-matching edges in uniaxial compression (implicit mortar contact). Source: The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; passed by 2D mortar methods: Yang, Laursen & Meng, IJNME 62:1183–1225, 2005. Uniaxial stress between frictionless supports is uniform in both blocks. Tolerances — small_lower_corner: small sliding is linear and its edge mortar integrals exact (degree-3 Gauss on the overlaps): the uniform stress is the discrete solution up to the convergence tolerance; small_lower_far: as for the corner element; small_lower_base: as for the corner element; small_upper_top: as for the corner element; small_lateral: a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them; small_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; small_force: the pressure carries the whole load across the interface (both are totals over the thickness); finite_lower_corner: finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances; finite_lower_far: as for the corner element; finite_lower_base: as for the corner element; finite_upper_top: as for the corner element; finite_lateral: a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them; finite_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; finite_force: the pressure carries the whole load across the interface (both are totals over the thickness); small_stress: the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness)
|
||||
- **[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-friction-sliding-blocks](../tests/oracle/nl-friction-sliding-blocks/)** (theory): Coulomb friction: a block pressed on another and slid (implicit mortar contact). Source: Coulomb's law of dry friction: a sliding interface carries μ_k times the normal force against the slip (C. A. Coulomb, Théorie des machines simples, Mém. Math. Phys. Acad. Sci. 10:161–332, 1785; P. Wriggers, Computational Contact Mechanics, 2nd ed., Springer 2006, §5.2). Tolerances — kinetic: once the interface slips its friction is μ_k N exactly (the return map puts every slipping node's traction on the cone); the force opposes the upper block's motion; balance: the friction carries the whole sideways load; slipping: a sideways demand far beyond μ N slips the whole interface
|
||||
- **[nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/)** (theory): Hertz line contact: a cylinder pressed on a flat (implicit mortar contact, plane strain). Source: H. Hertz, Über die Berührung fester elastischer Körper, J. reine angew. Math. 92:156–171, 1882; K. L. Johnson, Contact Mechanics, Cambridge University Press 1985, §4.3 (two-dimensional contact of cylindrical bodies, eqs. 4.45–4.49): b = √(4PR/(πE*)), p₀ = 2P/(πb), with 1/E* = 2(1 − ν²)/E for two bodies of one material and R the cylinder's radius on a flat. Tolerances — peak_pressure: p₀ = 2P/(πb) for the load per length P = 2265.57 N/mm, b = 0.5 mm; the contact width spans seven elements of the graded mesh; normal_force: the pressure carries the whole load; the soft springs that hold the indenter before contact take a millionth; pressure_x0296: p₀ √(1 − x²/b²) at the node; the nodal pressure (the node's normal force over the area it carries) averages the distribution over the node's elements; pressure_x0367: as at node 7, nearer the edge, where the elements round off the square-root fall of the pressure to zero at b; pressure_x0633: outside the contact the surfaces are apart: no pressure (an element beyond b, since the edge element carries part of the last closed node's pressure); syy_depth: σ_z = −p₀ / √(1 + z²/b²) on the axis of a half-plane (Johnson 1985, eq. 4.49); the foundation is ten half-widths wide and deep, not a half-plane, and the stress is the nodal average of the elements around the node
|
||||
- **[nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/)** (theory): Hertz line contact: a cylinder pressed on a flat (implicit mortar contact of 2D edges, plane strain). Source: H. Hertz, Über die Berührung fester elastischer Körper, J. reine angew. Math. 92:156–171, 1882; K. L. Johnson, Contact Mechanics, Cambridge University Press 1985, §4.3 (two-dimensional contact of cylindrical bodies, eqs. 4.45–4.49): b = √(4PR/(πE*)), p₀ = 2P/(πb), with 1/E* = 2(1 − ν²)/E for two bodies of one material and R the cylinder's radius on a flat. Tolerances — peak_pressure: p₀ = 2P/(πb) for the load per length P = 2265.57 N/mm, b = 0.5 mm; the contact width spans seven elements of the graded mesh; normal_force: the pressure carries the whole load; the soft springs that hold the indenter before contact take a millionth; pressure_x0296: p₀ √(1 − x²/b²) at the node; the nodal pressure (the node's normal force over the area it carries) averages the distribution over the node's elements; pressure_x0367: as at node 7, nearer the edge, where the elements round off the square-root fall of the pressure to zero at b; pressure_x0633: outside the contact the surfaces are apart: no pressure (an element beyond b, since the edge element carries part of the last closed node's pressure); syy_depth: σ_z = −p₀ / √(1 + z²/b²) on the axis of a half-plane (Johnson 1985, eq. 4.49); the foundation is ten half-widths wide and deep, not a half-plane, and the stress is the nodal average of the elements around the node
|
||||
- **[nl-hybrid-incompressible](../tests/oracle/nl-hybrid-incompressible/)** (theory): Nearly incompressible hybrid block under prescribed stretch. Source: Uniaxial stress of the decoupled compressible neo-Hookean energy W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)² (P. J. Flory, Trans. Faraday Soc. 57:829–838, 1961): σ = μ J^(−5/3) (λ² − I₁/3) + κ (J − 1) with the lateral stretch found so σ_yy = 0 (bisection; E = 3, ν = 0.4999: μ = 1.0000667, κ = 5000). Tolerances — sigma: a homogeneous deformation every hybrid hex8 represents exactly; the 27-element mesh makes the global mixed tangent indefinite in the first iterations (LDLᵀ): the tolerance admits the Newton tolerance and rounding; lateral: a homogeneous deformation every hybrid hex8 represents exactly; the 27-element mesh makes the global mixed tangent indefinite in the first iterations (LDLᵀ): the tolerance admits the Newton tolerance and rounding
|
||||
- **[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-mooney-rivlin-tube-axisymmetric](../tests/oracle/nl-mooney-rivlin-tube-axisymmetric/)** (theory): Inflation of a stretched Mooney–Rivlin tube (axisymmetric hybrid quad4). 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
|
||||
@@ -284,6 +360,7 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
|
||||
- **[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
|
||||
- **[scordelis-lo-roof-solid](../tests/oracle/scordelis-lo-roof-solid/)** (benchmark): Scordelis–Lo roof with one layer of hex20 reduced-integration solids. Source: 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) geometry, material and load; Table 5(a) (p. 11) reference displacement 0.3024; Belytschko et al., Computer Methods in Applied Mechanics and Engineering 51 (1985) 221–258 (shell obstacle course). Tolerances — uz: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14); MacNeal and Harder normalise by 0.3024 (the deep-shell value quoted in their [5] is 0.3086), and a solid model of R/t = 100 differs from shell theory by O(t/R)
|
||||
- **[tie-patch](../tests/oracle/tie-patch/)** (theory): Tie patch test: two blocks with non-matching meshes tied together in uniaxial tension. Source: The patch test for mesh tying: a tie across non-matching meshes must transmit a constant stress exactly; B. I. Wohlmuth, A mortar finite element method using dual spaces for the Lagrange multiplier, SIAM J. Numer. Anal. 38(3):989–1012, 2000; M. A. Puso, A 3D mortar method for solid mechanics, IJNME 59:315–336, 2004. Uniaxial stress between frictionless symmetry planes is uniform in both blocks. Tolerances — szz_10: uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes; szz_14: uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes; szz_1: uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes; szz_19: uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes; szz_26: uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes; sxx_14: the tie must not hold the lateral contraction of either mesh; uz_interface: the tied node follows the uniform strain field
|
||||
- **[tube-torsion-coupling](../tests/oracle/tube-torsion-coupling/)** (theory): Torsion of a tube through a kinematically coupled reference point. Source: Saint-Venant torsion of a circular tube, φ = TL/(GJ), τ = Tr/J with J = π(b⁴ − a⁴)/2; Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §104. Tolerances — twist: Saint-Venant torsion of a circular tube is exact in elasticity; the rigid end coupling only stiffens a short zone near the tip; quadratic elements with 16 divisions around are within 0.1%; shear: stress extrapolated to the outer surface and averaged where two blocks meet: about 0.3% with four radial quadratic elements (two gave 1.1%)
|
||||
|
||||
## Pending cases
|
||||
|
||||
+3
-3
@@ -54,8 +54,8 @@ products are run, no documentation text is copied). Status: **done**, **started*
|
||||
| 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 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, M10 | | started (explicit: pairs, general and self contact, segment to segment with a penalty that keeps the time increment, Coulomb friction with exponential decay, friction on rigid walls, `interaction.contact.find`, Abaqus and LS-DYNA contact keywords; implicit contact in M7) |
|
||||
| Interaction | Tie between non-matching meshes | M7 | | done (`interaction.tie`: dual mortar constraints in the implicit analyses, the tie patch test; Abaqus `*TIE`, LS-DYNA tied contact) |
|
||||
| Interaction | Contact (general and pairs), friction, automatic pair detection | M7, M10 | | started (explicit: pairs, general and self contact, segment to segment with a penalty that keeps the time increment, Coulomb friction with exponential decay, friction on rigid walls, `interaction.contact.find`, Abaqus and LS-DYNA contact keywords; implicit: mortar contact pairs in nonlinear static steps by an augmented Lagrangian with Coulomb friction, self contact next) |
|
||||
| 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) |
|
||||
| Interaction | Bolt pretension, connectors | M7 | | planned |
|
||||
| Decks | Abaqus input writer (`deck.write`, flat deck; reports what Abaqus cannot express) | M3 | | done |
|
||||
@@ -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, 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; a symmetric indefinite LDLᵀ factorization with inertia, Newton through indefinite tangents (hybrid elements at ν = 0.4999 under prescribed displacement); linear buckling of solids (Euler columns, Bryan's plate) and imperfections from its modes; arc-length with critical points from the inertia (shallow-arch snap-through); shells, implicit contact 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; mortar contact with friction (patch test, Hertz, sliding blocks); Mooney–Rivlin and Yeoh; thermal strain at finite and small strain; a symmetric indefinite LDLᵀ factorization with inertia, Newton through indefinite tangents (hybrid elements at ν = 0.4999 under prescribed displacement); linear buckling of solids (Euler columns, Bryan's plate) and imperfections from its modes; arc-length with critical points from the inertia (shallow-arch snap-through); shells 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; X3: deformable contact and friction, verified by bar collision, sliding blocks, the contact patch test and Hertz; hyperelastic materials on the shared material point; opt-in mass scaling) |
|
||||
| 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 |
|
||||
|
||||
@@ -1299,3 +1299,46 @@ from (cited again in the module docs); docs/fem.md describes the formulation.
|
||||
impact of spheres, H. Hertz, J. reine angew. Math. 92:156–171, 1882, and K. L. Johnson,
|
||||
*Contact Mechanics*, Cambridge 1985, §11.4; the sliding block, any dynamics text (J. L. Meriam,
|
||||
L. G. Kraige, *Engineering Mechanics: Dynamics*, §3/5).
|
||||
|
||||
## Part R: Contact in nonlinear static steps (M7), as implemented
|
||||
|
||||
The papers `fieldcraft_fem::mortar`, `fieldcraft_fem::dual` and `fieldcraft_solver::contact` are
|
||||
written from (cited again in the module docs); docs/fem.md describes the formulation.
|
||||
|
||||
- Mortar contact, segment to segment, with nodal weighted gaps and the pressure interpolated by
|
||||
the non-mortar side's shape functions: M. A. Puso, T. A. Laursen, *A mortar segment-to-segment
|
||||
contact method for large deformation solid mechanics*, CMAME 193:601–629, 2004; T. W. McDevitt,
|
||||
T. A. Laursen, *A mortar-finite element formulation for frictional contact problems*, IJNME
|
||||
48:1525–1547, 2000; the segmentation (projection on the non-mortar plane, clipping, integration
|
||||
cells) and the linearisation it needs, A. Popp, M. W. Gee, W. A. Wall, *A finite deformation
|
||||
mortar contact formulation using a primal–dual active set strategy*, IJNME 79:1354–1391, 2009.
|
||||
Chosen over two-pass node-to-surface because mortar passes the contact patch test on
|
||||
non-matching meshes (R. L. Taylor, P. Papadopoulos 1991; M. A. Crisfield 2000, as in Part Q).
|
||||
- The consistent tangent by automatic differentiation of the mortar integrals rather than by
|
||||
hand: J. Lengiewicz, J. Korelc, S. Stupkiewicz, *Automation of finite element formulations for
|
||||
large deformation contact problems*, IJNME 85:1252–1279, 2011; forward-mode (truncated Taylor)
|
||||
arithmetic, A. Griewank, A. Walther, *Evaluating Derivatives*, 2nd ed., SIAM 2008.
|
||||
- Augmented Lagrangian with Uzawa updates after each converged increment: J. C. Simo, T. A.
|
||||
Laursen, *An augmented Lagrangian treatment of contact problems involving friction*, Comput.
|
||||
Struct. 42(1):97–116, 1992; P. Wriggers, *Computational Contact Mechanics*, 2nd ed., Springer
|
||||
2006, ch. 6 and 10; T. A. Laursen, *Computational Contact and Impact Mechanics*, Springer 2003.
|
||||
- Friction: the regularised Coulomb return map at the nodes, J. C. Simo, T. A. Laursen 1992 and
|
||||
P. Wriggers 2006, §5.2 (as in Part Q), on the mortar slip in its rate form (the weighted
|
||||
relative motion projected on the tangent plane), T. W. McDevitt, T. A. Laursen 2000 and M. A.
|
||||
Puso, T. A. Laursen, *A mortar segment-to-segment frictional contact method for large
|
||||
deformations*, CMAME 193:4891–4913, 2004; C. A. Coulomb's law, *Théorie des machines simples*,
|
||||
1785. The tangent is symmetrised like the follower pressure's (the non-symmetric coupling to
|
||||
the pressure is left out; the residual is exact).
|
||||
- Ties by dual mortar constraints: B. I. Wohlmuth, *A mortar finite element method using dual
|
||||
spaces for the Lagrange multiplier*, SIAM J. Numer. Anal. 38(3):989–1012, 2000; A. Popp, B. I.
|
||||
Wohlmuth, M. W. Gee, W. A. Wall, *Dual quadratic mortar finite element methods for 3D finite
|
||||
deformation contact*, SIAM J. Sci. Comput. 34(4):B421–B446, 2012; mesh tying in solid
|
||||
mechanics, M. A. Puso, *A 3D mortar method for solid mechanics*, IJNME 59:315–336, 2004.
|
||||
- The active set inside Newton (the max() of the regularised pressure, a semismooth Newton
|
||||
method): P. Alart, A. Curnier, *A mixed formulation for frictional contact problems prone to
|
||||
Newton like solution methods*, CMAME 92:353–375, 1991; Popp et al. 2009.
|
||||
- Integration of the overlap cells: J. Radon's seven-point degree-5 rule on triangles (A. H.
|
||||
Stroud, *Approximate Calculation of Multiple Integrals*, Prentice-Hall 1971, T2:5-1).
|
||||
- Verification: Hertz's contact of cylinders and spheres, H. Hertz, J. reine angew. Math.
|
||||
92:156–171, 1882; K. L. Johnson, *Contact Mechanics*, Cambridge 1985, §4.2–4.3 (eqs. 4.45–4.49
|
||||
for the line contact's pressure and subsurface stresses).
|
||||
@@ -0,0 +1,11 @@
|
||||
# Contact patch test, axisymmetric (implicit mortar contact, non-matching edges)
|
||||
|
||||
The axisymmetric form of `nl-contact-patch`: two rings of unit square section, 1 ≤ r ≤ 2, stacked
|
||||
along the axis y and meshed independently (3 × 2 quad4 below, graded in r; 2 × 2 above), so the
|
||||
nodes of the interface at y = 1 do not match. A frictionless contact pair joins the lower ring's
|
||||
top edge (main) and the upper ring's bottom edge (secondary). The base y = 0 is held in y and
|
||||
the top y = 2 pushed down by 0.02 (E = 1000, ν = 0.3), in a `Small` (small sliding) and a `Finite`
|
||||
(finite sliding) nonlinear static step. Axial compression with free radial expansion is a
|
||||
uniform uniaxial stress σ_yy in both rings (u_r = −ν ε r, so the hoop strain equals the radial
|
||||
one), carried by a uniform contact pressure; the edge mortar integrals weight by 2πr, so the
|
||||
patch test checks that the radius enters the gaps and the forces consistently.
|
||||
@@ -0,0 +1,432 @@
|
||||
{
|
||||
"title": "Contact patch test, axisymmetric: two rings with non-matching edges in axial compression (implicit mortar contact)",
|
||||
"kind": "theory",
|
||||
"source": "The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; passed by 2D mortar methods: Yang, Laursen & Meng, IJNME 62:1183–1225, 2005. Uniaxial stress between frictionless supports is uniform in both blocks",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "small_lower_corner",
|
||||
"quantity": "Small: σ_yy of element 4 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
4
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "small sliding is linear and its edge mortar integrals exact (degree-3 Gauss on the overlaps): the uniform stress is the discrete solution up to the convergence tolerance"
|
||||
},
|
||||
{
|
||||
"id": "small_lower_far",
|
||||
"quantity": "Small: σ_yy of element 6 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "small_lower_base",
|
||||
"quantity": "Small: σ_yy of element 1 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "small_upper_top",
|
||||
"quantity": "Small: σ_yy of element 10 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
10
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "small_lateral",
|
||||
"quantity": "Small: σ_xx of element 6 over its σ_yy (uniaxial stress: 0)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 1e-06
|
||||
},
|
||||
"reason": "a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them"
|
||||
},
|
||||
{
|
||||
"id": "small_pressure",
|
||||
"quantity": "Small: the contact pressure at the secondary node (1.5, 1) over −σ_yy of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"at": [
|
||||
[
|
||||
1.5,
|
||||
1.0,
|
||||
0.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface"
|
||||
},
|
||||
{
|
||||
"id": "small_force",
|
||||
"quantity": "Small: the integrated contact pressure over the reaction at the pushed top",
|
||||
"measure": {
|
||||
"from": "small",
|
||||
"pointer": "/contacts/0/normal_force",
|
||||
"over": {
|
||||
"command": "results.reactions",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"sets": [
|
||||
"top"
|
||||
]
|
||||
},
|
||||
"pointer": "/sets/0/force/1",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "the pressure carries the whole load across the interface (both are totals over the circumference)"
|
||||
},
|
||||
{
|
||||
"id": "finite_lower_corner",
|
||||
"quantity": "Finite: σ_yy of element 4 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
4
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances"
|
||||
},
|
||||
{
|
||||
"id": "finite_lower_far",
|
||||
"quantity": "Finite: σ_yy of element 6 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "finite_lower_base",
|
||||
"quantity": "Finite: σ_yy of element 1 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "finite_upper_top",
|
||||
"quantity": "Finite: σ_yy of element 10 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
10
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "finite_lateral",
|
||||
"quantity": "Finite: σ_xx of element 6 over its σ_yy (uniaxial stress: 0)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 0.0002
|
||||
},
|
||||
"reason": "a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them"
|
||||
},
|
||||
{
|
||||
"id": "finite_pressure",
|
||||
"quantity": "Finite: the contact pressure at the secondary node (1.5, 1) over −σ_yy of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"at": [
|
||||
[
|
||||
1.5,
|
||||
1.0,
|
||||
0.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface"
|
||||
},
|
||||
{
|
||||
"id": "finite_force",
|
||||
"quantity": "Finite: the integrated contact pressure over the reaction at the pushed top",
|
||||
"measure": {
|
||||
"from": "finite",
|
||||
"pointer": "/contacts/0/normal_force",
|
||||
"over": {
|
||||
"command": "results.reactions",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"sets": [
|
||||
"top"
|
||||
]
|
||||
},
|
||||
"pointer": "/sets/0/force/1",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "the pressure carries the whole load across the interface (both are totals over the circumference)"
|
||||
},
|
||||
{
|
||||
"id": "small_stress",
|
||||
"quantity": "Small: σ_yy of element 7 (uniaxial stress E ε = −10 for the 1% squeeze)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
},
|
||||
"value": -10.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0001
|
||||
},
|
||||
"reason": "the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"finite_force": {
|
||||
"band": 1.000000000000021e-9,
|
||||
"value": 1.0000000000000209
|
||||
},
|
||||
"finite_lateral": {
|
||||
"band": 0.0002,
|
||||
"value": -6.202292989414768e-6
|
||||
},
|
||||
"finite_lower_base": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999851288470675
|
||||
},
|
||||
"finite_lower_corner": {
|
||||
"band": 1.0000001256632718e-9,
|
||||
"value": 1.0000001256632718
|
||||
},
|
||||
"finite_lower_far": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999675186278648
|
||||
},
|
||||
"finite_pressure": {
|
||||
"band": 1e-9,
|
||||
"value": 0.999971468750515
|
||||
},
|
||||
"finite_upper_top": {
|
||||
"band": 1e-9,
|
||||
"value": 0.999981532712154
|
||||
},
|
||||
"small_force": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999044
|
||||
},
|
||||
"small_lateral": {
|
||||
"band": 1e-6,
|
||||
"value": 3.4213832901728455e-15
|
||||
},
|
||||
"small_lower_base": {
|
||||
"band": 1.0000000000000207e-9,
|
||||
"value": 1.0000000000000207
|
||||
},
|
||||
"small_lower_corner": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999943
|
||||
},
|
||||
"small_lower_far": {
|
||||
"band": 1.0000000000000418e-9,
|
||||
"value": 1.0000000000000417
|
||||
},
|
||||
"small_pressure": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999998981
|
||||
},
|
||||
"small_stress": {
|
||||
"band": 1e-8,
|
||||
"value": -9.999573821916737
|
||||
},
|
||||
"small_upper_top": {
|
||||
"band": 1.0000000000000296e-9,
|
||||
"value": 1.0000000000000295
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Lower", "kind": "quad4", "corners": [[1.0, 0, 0], [2.0, 0, 0], [2.0, 1, 0], [1.0, 1, 0]], "divisions": [3, 2], "bias": [2.0, 1.0], "edges": [{"corners": [1, 2], "name": "bottom"}, {"corners": [3, 4], "name": "top"}, {"corners": [4, 1], "name": "x0"}]}},
|
||||
{"command": "mesh.block", "params": {"body": "Upper", "kind": "quad4", "corners": [[1.0, 1, 0], [2.0, 1, 0], [2.0, 2, 0], [1.0, 2, 0]], "divisions": [2, 2], "edges": [{"corners": [1, 2], "name": "bottom"}, {"corners": [3, 4], "name": "top"}, {"corners": [4, 1], "name": "x0"}], "merge": 0}},
|
||||
{"command": "model.set.create", "params": {"name": "lower", "geometry": [{"body": "Lower", "face": "area"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "upper", "geometry": [{"body": "Upper", "face": "area"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "lower_top", "geometry": [{"body": "Lower", "edge": "top"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "upper_bottom", "geometry": [{"body": "Upper", "edge": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Lower", "edge": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Upper", "edge": "top"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}}},
|
||||
{"command": "model.section.create", "params": {"name": "lower", "set": "lower", "material": "Mat", "kind": {"type": "axisymmetric"}}},
|
||||
{"command": "model.section.create", "params": {"name": "upper", "set": "upper", "material": "Mat", "kind": {"type": "axisymmetric"}}},
|
||||
{"command": "interaction.contact.pair", "params": {"name": "interface", "main": "lower_top", "secondary": "upper_bottom"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "base", "type": "displacement", "target": {"set": "base"}, "values": [null, 0, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "push", "type": "displacement", "target": {"set": "top"}, "values": [null, -0.02, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Small", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "tolerances": {"force": 1e-10, "displacement": 1e-08, "energy": 1e-16}}, "boundary_conditions": ["base", "push"], "loads": []}},
|
||||
{"command": "model.step.create", "params": {"name": "Finite", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["base", "push"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Small"}, "id": "small"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Finite"}, "id": "finite"}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
# Contact patch test in plane strain (implicit mortar contact, non-matching edges)
|
||||
|
||||
The 2D form of `nl-contact-patch`: two unit squares stacked along y in plane strain (2 mm thick),
|
||||
meshed independently: 3 × 2 quad4 below (graded in x), 2 × 2 above, so the nodes of the interface
|
||||
at y = 1 do not match. A frictionless contact pair joins the lower block's top edge (main) and
|
||||
the upper block's bottom edge (secondary). The edge x = 0 is a symmetry line, the base y = 0 is
|
||||
held in y, and the top y = 2 is pushed down by 0.02 (E = 1000, ν = 0.3) in two nonlinear static
|
||||
steps: `Small` without nonlinear geometry (small sliding) and `Finite` with it (finite sliding).
|
||||
The exact state is uniaxial stress σ_yy (σ_zz = ν σ_yy out of plane), uniform in both blocks,
|
||||
with a uniform contact pressure equal to it.
|
||||
@@ -0,0 +1,432 @@
|
||||
{
|
||||
"title": "Contact patch test in plane strain: two blocks with non-matching edges in uniaxial compression (implicit mortar contact)",
|
||||
"kind": "theory",
|
||||
"source": "The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; passed by 2D mortar methods: Yang, Laursen & Meng, IJNME 62:1183–1225, 2005. Uniaxial stress between frictionless supports is uniform in both blocks",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "small_lower_corner",
|
||||
"quantity": "Small: σ_yy of element 4 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
4
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "small sliding is linear and its edge mortar integrals exact (degree-3 Gauss on the overlaps): the uniform stress is the discrete solution up to the convergence tolerance"
|
||||
},
|
||||
{
|
||||
"id": "small_lower_far",
|
||||
"quantity": "Small: σ_yy of element 6 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "small_lower_base",
|
||||
"quantity": "Small: σ_yy of element 1 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "small_upper_top",
|
||||
"quantity": "Small: σ_yy of element 10 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
10
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "small_lateral",
|
||||
"quantity": "Small: σ_xx of element 6 over its σ_yy (uniaxial stress: 0)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 1e-06
|
||||
},
|
||||
"reason": "a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them"
|
||||
},
|
||||
{
|
||||
"id": "small_pressure",
|
||||
"quantity": "Small: the contact pressure at the secondary node (0.5, 1) over −σ_yy of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"at": [
|
||||
[
|
||||
0.5,
|
||||
1.0,
|
||||
0.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface"
|
||||
},
|
||||
{
|
||||
"id": "small_force",
|
||||
"quantity": "Small: the integrated contact pressure over the reaction at the pushed top",
|
||||
"measure": {
|
||||
"from": "small",
|
||||
"pointer": "/contacts/0/normal_force",
|
||||
"over": {
|
||||
"command": "results.reactions",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"sets": [
|
||||
"top"
|
||||
]
|
||||
},
|
||||
"pointer": "/sets/0/force/1",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "the pressure carries the whole load across the interface (both are totals over the thickness)"
|
||||
},
|
||||
{
|
||||
"id": "finite_lower_corner",
|
||||
"quantity": "Finite: σ_yy of element 4 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
4
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances"
|
||||
},
|
||||
{
|
||||
"id": "finite_lower_far",
|
||||
"quantity": "Finite: σ_yy of element 6 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "finite_lower_base",
|
||||
"quantity": "Finite: σ_yy of element 1 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "finite_upper_top",
|
||||
"quantity": "Finite: σ_yy of element 10 over that of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
10
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "finite_lateral",
|
||||
"quantity": "Finite: σ_xx of element 6 over its σ_yy (uniaxial stress: 0)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
6
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
}
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 0.0002
|
||||
},
|
||||
"reason": "a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them"
|
||||
},
|
||||
{
|
||||
"id": "finite_pressure",
|
||||
"quantity": "Finite: the contact pressure at the secondary node (0.5, 1) over −σ_yy of element 7",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"at": [
|
||||
[
|
||||
0.5,
|
||||
1.0,
|
||||
0.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface"
|
||||
},
|
||||
{
|
||||
"id": "finite_force",
|
||||
"quantity": "Finite: the integrated contact pressure over the reaction at the pushed top",
|
||||
"measure": {
|
||||
"from": "finite",
|
||||
"pointer": "/contacts/0/normal_force",
|
||||
"over": {
|
||||
"command": "results.reactions",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"sets": [
|
||||
"top"
|
||||
]
|
||||
},
|
||||
"pointer": "/sets/0/force/1",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "the pressure carries the whole load across the interface (both are totals over the thickness)"
|
||||
},
|
||||
{
|
||||
"id": "small_stress",
|
||||
"quantity": "Small: σ_yy of element 7 (plane strain uniaxial stress E ε/(1 − ν²) = −10.989 for the 1% squeeze)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/1"
|
||||
},
|
||||
"value": -10.989010989010989,
|
||||
"tolerance": {
|
||||
"relative": 0.0001
|
||||
},
|
||||
"reason": "the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"finite_force": {
|
||||
"band": 1.0000000000022103e-9,
|
||||
"value": 1.0000000000022102
|
||||
},
|
||||
"finite_lateral": {
|
||||
"band": 0.0002,
|
||||
"value": -1.8586502985840345e-6
|
||||
},
|
||||
"finite_lower_base": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999968799408236
|
||||
},
|
||||
"finite_lower_corner": {
|
||||
"band": 1.0000007080194146e-9,
|
||||
"value": 1.0000007080194144
|
||||
},
|
||||
"finite_lower_far": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999913032274111
|
||||
},
|
||||
"finite_pressure": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999924972272739
|
||||
},
|
||||
"finite_upper_top": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999957165083484
|
||||
},
|
||||
"small_force": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999859
|
||||
},
|
||||
"small_lateral": {
|
||||
"band": 1e-6,
|
||||
"value": -8.925527731170228e-15
|
||||
},
|
||||
"small_lower_base": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999801
|
||||
},
|
||||
"small_lower_corner": {
|
||||
"band": 1.0000000000000059e-9,
|
||||
"value": 1.0000000000000058
|
||||
},
|
||||
"small_lower_far": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999546
|
||||
},
|
||||
"small_pressure": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999142
|
||||
},
|
||||
"small_stress": {
|
||||
"band": 1.098901098901099e-8,
|
||||
"value": -10.988443575433445
|
||||
},
|
||||
"small_upper_top": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999762
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Lower", "kind": "quad4", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], "divisions": [3, 2], "bias": [2.0, 1.0], "edges": [{"corners": [1, 2], "name": "bottom"}, {"corners": [3, 4], "name": "top"}, {"corners": [4, 1], "name": "x0"}]}},
|
||||
{"command": "mesh.block", "params": {"body": "Upper", "kind": "quad4", "corners": [[0, 1, 0], [1, 1, 0], [1, 2, 0], [0, 2, 0]], "divisions": [2, 2], "edges": [{"corners": [1, 2], "name": "bottom"}, {"corners": [3, 4], "name": "top"}, {"corners": [4, 1], "name": "x0"}], "merge": 0}},
|
||||
{"command": "model.set.create", "params": {"name": "lower", "geometry": [{"body": "Lower", "face": "area"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "upper", "geometry": [{"body": "Upper", "face": "area"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "lower_top", "geometry": [{"body": "Lower", "edge": "top"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "upper_bottom", "geometry": [{"body": "Upper", "edge": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Lower", "edge": "x0"}, {"body": "Upper", "edge": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Lower", "edge": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Upper", "edge": "top"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}}},
|
||||
{"command": "model.section.create", "params": {"name": "lower", "set": "lower", "material": "Mat", "kind": {"type": "plane_strain", "thickness": 2.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "upper", "set": "upper", "material": "Mat", "kind": {"type": "plane_strain", "thickness": 2.0}}},
|
||||
{"command": "interaction.contact.pair", "params": {"name": "interface", "main": "lower_top", "secondary": "upper_bottom"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "x0", "type": "displacement", "target": {"set": "x0"}, "values": [0, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "base", "type": "displacement", "target": {"set": "base"}, "values": [null, 0, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "push", "type": "displacement", "target": {"set": "top"}, "values": [null, -0.02, null, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Small", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "tolerances": {"force": 1e-10, "displacement": 1e-08, "energy": 1e-16}}, "boundary_conditions": ["x0", "base", "push"], "loads": []}},
|
||||
{"command": "model.step.create", "params": {"name": "Finite", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["x0", "base", "push"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Small"}, "id": "small"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Finite"}, "id": "finite"}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
# Contact patch test (implicit mortar contact, non-matching meshes)
|
||||
|
||||
Two unit cubes stacked along z, meshed independently: 3 × 3 × 2 hex8 below (graded in x and y),
|
||||
2 × 2 × 2 above, so the nodes of the interface at z = 1 do not match. A frictionless contact pair
|
||||
joins the lower block's top (main) and the upper block's bottom (secondary). The faces x = 0 and
|
||||
y = 0 of both blocks are symmetry planes, the base z = 0 is held in z, and the top z = 2 is pushed
|
||||
down by 0.02 (E = 1000, ν = 0.3) in two nonlinear static steps: `Small` without nonlinear
|
||||
geometry (small sliding, mortar integrals in the reference configuration) and `Finite` with it
|
||||
(finite sliding, searched and integrated on the deformed faces). The exact state is uniaxial
|
||||
stress, uniform in both blocks, with a uniform contact pressure equal to it.
|
||||
@@ -0,0 +1,432 @@
|
||||
{
|
||||
"title": "Contact patch test: two blocks with non-matching meshes in uniaxial compression (implicit mortar contact)",
|
||||
"kind": "theory",
|
||||
"source": "The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; Crisfield, Re-visiting the contact patch test, IJNME 48:435–449, 2000; passed by mortar methods: Puso & Laursen, CMAME 193:601–629, 2004. Uniaxial stress between frictionless symmetry planes is uniform in both blocks",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "small_lower_corner",
|
||||
"quantity": "Small: σ_zz of the lower block's corner element under the interface (element 10) over that of the upper block's corner element over it (element 19)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
10
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "small sliding is linear and its mortar integrals exact on these rectangular faces: a uniform stress is the discrete solution up to the convergence tolerance (10⁻¹⁰)"
|
||||
},
|
||||
{
|
||||
"id": "small_lower_centre",
|
||||
"quantity": "Small: σ_zz of the lower block's centre element under the interface (element 14) over that of element 19",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
14
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "small_lower_base",
|
||||
"quantity": "Small: σ_zz of the lower block's bottom corner element (element 1) over that of element 19",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "small_upper_top",
|
||||
"quantity": "Small: σ_zz of the upper block's top far corner element (element 26) over that of element 19",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
26
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "small_lateral",
|
||||
"quantity": "Small: σ_xx of element 14 over its σ_zz (uniaxial stress: 0)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
14
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
14
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
}
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 1e-06
|
||||
},
|
||||
"reason": "frictionless symmetry planes and a frictionless interface leave the blocks free to expand sideways; the non-matching interface must not hold them"
|
||||
},
|
||||
{
|
||||
"id": "small_pressure",
|
||||
"quantity": "Small: the contact pressure at the secondary node (0.5, 0.5, 1) over −σ_zz of element 19",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"at": [
|
||||
[
|
||||
0.5,
|
||||
0.5,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface"
|
||||
},
|
||||
{
|
||||
"id": "small_force",
|
||||
"quantity": "Small: the integrated contact pressure over the reaction at the pushed top",
|
||||
"measure": {
|
||||
"from": "small",
|
||||
"pointer": "/contacts/0/normal_force",
|
||||
"over": {
|
||||
"command": "results.reactions",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"sets": [
|
||||
"top"
|
||||
]
|
||||
},
|
||||
"pointer": "/sets/0/force/2",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "the pressure carries the whole load across the interface"
|
||||
},
|
||||
{
|
||||
"id": "finite_lower_corner",
|
||||
"quantity": "Finite: σ_zz of the lower block's corner element under the interface (element 10) over that of the upper block's corner element over it (element 19)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
10
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances"
|
||||
},
|
||||
{
|
||||
"id": "finite_lower_centre",
|
||||
"quantity": "Finite: σ_zz of the lower block's centre element under the interface (element 14) over that of element 19",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
14
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "finite_lower_base",
|
||||
"quantity": "Finite: σ_zz of the lower block's bottom corner element (element 1) over that of element 19",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "finite_upper_top",
|
||||
"quantity": "Finite: σ_zz of the upper block's top far corner element (element 26) over that of element 19",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
26
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "as for the corner element"
|
||||
},
|
||||
{
|
||||
"id": "finite_lateral",
|
||||
"quantity": "Finite: σ_xx of element 14 over its σ_zz (uniaxial stress: 0)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
14
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
14
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
}
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 0.0002
|
||||
},
|
||||
"reason": "frictionless symmetry planes and a frictionless interface leave the blocks free to expand sideways; the non-matching interface must not hold them"
|
||||
},
|
||||
{
|
||||
"id": "finite_pressure",
|
||||
"quantity": "Finite: the contact pressure at the secondary node (0.5, 0.5, 1) over −σ_zz of element 19",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"at": [
|
||||
[
|
||||
0.5,
|
||||
0.5,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure",
|
||||
"over": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface"
|
||||
},
|
||||
{
|
||||
"id": "finite_force",
|
||||
"quantity": "Finite: the integrated contact pressure over the reaction at the pushed top",
|
||||
"measure": {
|
||||
"from": "finite",
|
||||
"pointer": "/contacts/0/normal_force",
|
||||
"over": {
|
||||
"command": "results.reactions",
|
||||
"params": {
|
||||
"step": "Finite",
|
||||
"sets": [
|
||||
"top"
|
||||
]
|
||||
},
|
||||
"pointer": "/sets/0/force/2",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 1.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0002
|
||||
},
|
||||
"reason": "the pressure carries the whole load across the interface"
|
||||
},
|
||||
{
|
||||
"id": "small_stress",
|
||||
"quantity": "Small: σ_zz of element 19 (uniaxial stress E ε = −10 for the 1% squeeze)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Small",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
},
|
||||
"value": -10.0,
|
||||
"tolerance": {
|
||||
"relative": 0.0001
|
||||
},
|
||||
"reason": "the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness, a few 10⁻⁵ of the squeeze)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"finite_force": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999974551
|
||||
},
|
||||
"finite_lateral": {
|
||||
"band": 0.0002,
|
||||
"value": 1.223600676667172e-6
|
||||
},
|
||||
"finite_lower_base": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999905821256437
|
||||
},
|
||||
"finite_lower_centre": {
|
||||
"band": 1e-9,
|
||||
"value": 0.999987114528879
|
||||
},
|
||||
"finite_lower_corner": {
|
||||
"band": 1.000001514518151e-9,
|
||||
"value": 1.000001514518151
|
||||
},
|
||||
"finite_pressure": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999828003418422
|
||||
},
|
||||
"finite_upper_top": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999912761681571
|
||||
},
|
||||
"small_force": {
|
||||
"band": 1.0000000000000607e-9,
|
||||
"value": 1.0000000000000606
|
||||
},
|
||||
"small_lateral": {
|
||||
"band": 1e-6,
|
||||
"value": 7.880554574306202e-15
|
||||
},
|
||||
"small_lower_base": {
|
||||
"band": 1e-9,
|
||||
"value": 0.999999999999944
|
||||
},
|
||||
"small_lower_centre": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999398
|
||||
},
|
||||
"small_lower_corner": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999855
|
||||
},
|
||||
"small_pressure": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999953
|
||||
},
|
||||
"small_stress": {
|
||||
"band": 1e-8,
|
||||
"value": -9.99957382191768
|
||||
},
|
||||
"small_upper_top": {
|
||||
"band": 1e-9,
|
||||
"value": 0.9999999999999263
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Lower", "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": [3, 3, 2], "faces": {"1": "bottom", "2": "top", "3": "y0", "6": "x0"}, "bias": [2.0, 0.6, 1.0]}},
|
||||
{"command": "mesh.block", "params": {"body": "Upper", "kind": "hex8", "corners": [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1], [0, 0, 2], [1, 0, 2], [1, 1, 2], [0, 1, 2]], "divisions": [2, 2, 2], "faces": {"1": "bottom", "2": "top", "3": "y0", "6": "x0"}, "merge": 0}},
|
||||
{"command": "model.set.create", "params": {"name": "lower", "geometry": [{"body": "Lower"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "upper", "geometry": [{"body": "Upper"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "lower_top", "geometry": [{"body": "Lower", "face": "top"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "upper_bottom", "geometry": [{"body": "Upper", "face": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Lower", "face": "x0"}, {"body": "Upper", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Lower", "face": "y0"}, {"body": "Upper", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Lower", "face": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Upper", "face": "top"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}}},
|
||||
{"command": "model.section.create", "params": {"name": "lower", "set": "lower", "material": "Mat", "kind": {"type": "solid"}}},
|
||||
{"command": "model.section.create", "params": {"name": "upper", "set": "upper", "material": "Mat", "kind": {"type": "solid"}}},
|
||||
{"command": "interaction.contact.pair", "params": {"name": "interface", "main": "lower_top", "secondary": "upper_bottom"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "x0", "type": "displacement", "target": {"set": "x0"}, "values": [0, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "y0", "type": "displacement", "target": {"set": "y0"}, "values": [null, 0, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "z0", "type": "displacement", "target": {"set": "z0"}, "values": [null, null, 0, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "push", "type": "displacement", "target": {"set": "top"}, "values": [null, null, -0.02, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Small", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "tolerances": {"force": 1e-10, "displacement": 1e-08, "energy": 1e-16}}, "boundary_conditions": ["x0", "y0", "z0", "push"], "loads": []}},
|
||||
{"command": "model.step.create", "params": {"name": "Finite", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": true}, "boundary_conditions": ["x0", "y0", "z0", "push"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Small"}, "id": "small"},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Finite"}, "id": "finite"}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
# Coulomb friction: a block slid on another (implicit mortar contact)
|
||||
|
||||
A unit cube (2 × 2 × 2 hex8) on a 2 × 1 × 1 block (6 × 3 × 2 hex8), meshes not matching, E = 1000,
|
||||
ν = 0.3. The base of the lower block is held; the top of the upper block is pressed down by 0.02
|
||||
and moved 0.1 sideways in proportion over one nonlinear static step without nonlinear geometry.
|
||||
The contact pair has μ_s = 0.3 and μ_k = 0.2: the sideways demand far exceeds the friction, so
|
||||
the interface slips, with the kinetic coefficient once the nodes slip, and carries μ_k N.
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"title": "Coulomb friction: a block pressed on another and slid (implicit mortar contact)",
|
||||
"kind": "theory",
|
||||
"source": "Coulomb's law of dry friction: a sliding interface carries μ_k times the normal force against the slip (C. A. Coulomb, Théorie des machines simples, Mém. Math. Phys. Acad. Sci. 10:161–332, 1785; P. Wriggers, Computational Contact Mechanics, 2nd ed., Springer 2006, §5.2)",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "kinetic",
|
||||
"quantity": "the tangential over the normal force across the sliding interface",
|
||||
"measure": {
|
||||
"from": "slide",
|
||||
"pointer": "/contacts/0/force/0",
|
||||
"over": {
|
||||
"from": "slide",
|
||||
"pointer": "/contacts/0/force/2",
|
||||
"scale": -1.0
|
||||
}
|
||||
},
|
||||
"value": 0.2,
|
||||
"tolerance": {
|
||||
"relative": 0.001
|
||||
},
|
||||
"reason": "once the interface slips its friction is μ_k N exactly (the return map puts every slipping node's traction on the cone); the force opposes the upper block's motion"
|
||||
},
|
||||
{
|
||||
"id": "balance",
|
||||
"quantity": "the tangential contact force on the upper block over the sideways reaction at its top (they balance)",
|
||||
"measure": {
|
||||
"from": "slide",
|
||||
"pointer": "/contacts/0/force/0",
|
||||
"over": {
|
||||
"command": "results.reactions",
|
||||
"params": {
|
||||
"step": "Slide",
|
||||
"sets": [
|
||||
"top"
|
||||
]
|
||||
},
|
||||
"pointer": "/sets/0/force/0"
|
||||
}
|
||||
},
|
||||
"value": -1.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-06
|
||||
},
|
||||
"reason": "the friction carries the whole sideways load"
|
||||
},
|
||||
{
|
||||
"id": "slipping",
|
||||
"quantity": "the secondary nodes that slip (all 9)",
|
||||
"measure": {
|
||||
"from": "slide",
|
||||
"pointer": "/contacts/0/slipping_nodes"
|
||||
},
|
||||
"value": 9.0,
|
||||
"tolerance": {
|
||||
"exact": true
|
||||
},
|
||||
"reason": "a sideways demand far beyond μ N slips the whole interface"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": {
|
||||
"balance": {
|
||||
"band": 1e-9,
|
||||
"value": -0.999999977621409
|
||||
},
|
||||
"kinetic": {
|
||||
"band": 2.0000000000000003e-10,
|
||||
"value": 0.199999793528717
|
||||
},
|
||||
"slipping": {
|
||||
"band": 9.000000000000001e-9,
|
||||
"value": 9.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Lower", "kind": "hex8", "corners": [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0], [0, 0, 1], [2, 0, 1], [2, 1, 1], [0, 1, 1]], "divisions": [6, 3, 2], "faces": {"1": "bottom", "2": "top"}}},
|
||||
{"command": "mesh.block", "params": {"body": "Upper", "kind": "hex8", "corners": [[0.5, 0, 1], [1.5, 0, 1], [1.5, 1, 1], [0.5, 1, 1], [0.5, 0, 2], [1.5, 0, 2], [1.5, 1, 2], [0.5, 1, 2]], "divisions": [2, 2, 2], "faces": {"1": "bottom", "2": "top"}, "merge": 0}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Lower"}, {"body": "Upper"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "lower_top", "geometry": [{"body": "Lower", "face": "top"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "upper_bottom", "geometry": [{"body": "Upper", "face": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Lower", "face": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Upper", "face": "top"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}}},
|
||||
{"command": "model.section.create", "params": {"name": "all", "set": "all", "material": "Mat", "kind": {"type": "solid"}}},
|
||||
{"command": "interaction.contact.pair", "params": {"name": "interface", "main": "lower_top", "secondary": "upper_bottom", "friction": {"static": 0.3, "kinetic": 0.2, "decay": 1.0}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "base", "type": "fixed", "target": {"set": "base"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "slide", "type": "displacement", "target": {"set": "top"}, "values": [0.1, 0, -0.02, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Slide", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "initial_increment": 0.25}, "boundary_conditions": ["base", "slide"], "loads": []}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Slide"}, "id": "slide"}
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
# Hertz line contact in plane strain (implicit mortar contact of 2D edges)
|
||||
|
||||
The 2D form of `nl-hertz-cylinder`: the same steel cylinder of radius R = 10 mm on a steel block
|
||||
(E = 210 GPa, ν = 0.3; mm, N, MPa), now a plane strain model of unit thickness meshed with quad4:
|
||||
the cylinder's lower part a 2.5 mm wide block whose bottom edge is the circular arc (20 × 10,
|
||||
graded towards the contact), on a 5 × 5 mm foundation (24 × 14), symmetric about x = 0. A
|
||||
pressure on the indenter's top edge carries the load P = 2265.57 N/mm (per length of the whole
|
||||
cylinder) for Hertz's half-width b = 0.5 mm and peak pressure p₀ = 2884.6 MPa. Soft springs hold
|
||||
the indenter's top until the contact closes. One nonlinear static step without nonlinear
|
||||
geometry: small sliding, edge-to-edge mortar contact of the round edge (secondary) on the flat
|
||||
(main) by an augmented Lagrangian. The references and tolerances are the 3D case's, so the two
|
||||
discretisations of one problem are checked against one closed form.
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"title": "Hertz line contact: a cylinder pressed on a flat (implicit mortar contact of 2D edges, plane strain)",
|
||||
"kind": "theory",
|
||||
"source": "H. Hertz, Über die Berührung fester elastischer Körper, J. reine angew. Math. 92:156–171, 1882; K. L. Johnson, Contact Mechanics, Cambridge University Press 1985, §4.3 (two-dimensional contact of cylindrical bodies, eqs. 4.45–4.49): b = √(4PR/(πE*)), p₀ = 2P/(πb), with 1/E* = 2(1 − ν²)/E for two bodies of one material and R the cylinder's radius on a flat",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "peak_pressure",
|
||||
"quantity": "the largest contact pressure p₀ (at x = 0)",
|
||||
"measure": {
|
||||
"from": "solve",
|
||||
"pointer": "/contacts/0/max_pressure/value"
|
||||
},
|
||||
"value": 2884.6153846153843,
|
||||
"tolerance": {
|
||||
"relative": 0.02
|
||||
},
|
||||
"reason": "p₀ = 2P/(πb) for the load per length P = 2265.57 N/mm, b = 0.5 mm; the contact width spans seven elements of the graded mesh"
|
||||
},
|
||||
{
|
||||
"id": "normal_force",
|
||||
"quantity": "the force across the contact (half model, 1 mm thick)",
|
||||
"measure": {
|
||||
"from": "solve",
|
||||
"pointer": "/contacts/0/normal_force"
|
||||
},
|
||||
"value": 1132.7858125924733,
|
||||
"tolerance": {
|
||||
"relative": 0.002
|
||||
},
|
||||
"reason": "the pressure carries the whole load; the soft springs that hold the indenter before contact take a millionth"
|
||||
},
|
||||
{
|
||||
"id": "pressure_x0296",
|
||||
"quantity": "the contact pressure at the secondary node at x = 0.29594",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Press",
|
||||
"at": [
|
||||
[
|
||||
0.29594,
|
||||
0.00438,
|
||||
0.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure"
|
||||
},
|
||||
"value": 2325.066800691437,
|
||||
"tolerance": {
|
||||
"relative": 0.05
|
||||
},
|
||||
"reason": "p₀ √(1 − x²/b²) at the node; the nodal pressure (the node's normal force over the area it carries) averages the distribution over the node's elements"
|
||||
},
|
||||
{
|
||||
"id": "pressure_x0367",
|
||||
"quantity": "the contact pressure at the secondary node at x = 0.36701",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Press",
|
||||
"at": [
|
||||
[
|
||||
0.36701,
|
||||
0.006737,
|
||||
0.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure"
|
||||
},
|
||||
"value": 1959.0164149439815,
|
||||
"tolerance": {
|
||||
"relative": 0.1
|
||||
},
|
||||
"reason": "as at node 7, nearer the edge, where the elements round off the square-root fall of the pressure to zero at b"
|
||||
},
|
||||
{
|
||||
"id": "pressure_x0633",
|
||||
"quantity": "the contact pressure at the secondary node at x = 0.63325 (beyond the half-width b = 0.5)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Press",
|
||||
"at": [
|
||||
[
|
||||
0.63325,
|
||||
0.02007,
|
||||
0.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure"
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 1.0
|
||||
},
|
||||
"reason": "outside the contact the surfaces are apart: no pressure (an element beyond b, since the edge element carries part of the last closed node's pressure)"
|
||||
},
|
||||
{
|
||||
"id": "syy_depth",
|
||||
"quantity": "the vertical stress σ_yy under the centre of contact at depth 0.47127",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Press",
|
||||
"at": [
|
||||
[
|
||||
0.0,
|
||||
-0.47127,
|
||||
0.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/1"
|
||||
},
|
||||
"value": -2099.1580228637245,
|
||||
"tolerance": {
|
||||
"relative": 0.04
|
||||
},
|
||||
"reason": "σ_z = −p₀ / √(1 + z²/b²) on the axis of a half-plane (Johnson 1985, eq. 4.49); the foundation is ten half-widths wide and deep, not a half-plane, and the stress is the nodal average of the elements around the node"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"normal_force": {
|
||||
"band": 1.1330442926242355e-6,
|
||||
"value": 1133.0442926242354
|
||||
},
|
||||
"peak_pressure": {
|
||||
"band": 2.8919633998947767e-6,
|
||||
"value": 2891.9633998947766
|
||||
},
|
||||
"pressure_x0296": {
|
||||
"band": 2.325066800691437e-6,
|
||||
"value": 2257.8514097926386
|
||||
},
|
||||
"pressure_x0367": {
|
||||
"band": 2.078406343070884e-6,
|
||||
"value": 2078.406343070884
|
||||
},
|
||||
"pressure_x0633": {
|
||||
"band": 1.0,
|
||||
"value": -0.0
|
||||
},
|
||||
"syy_depth": {
|
||||
"band": 2.0991580228637247e-6,
|
||||
"value": -2059.829517894975
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Indenter", "kind": "quad4", "corners": [[0, 0, 0], [2.5, 0.3175416344814579, 0], [2.5, 3.0, 0], [0, 3.0, 0]], "divisions": [20, 10], "bias": [8, 8], "edges": [{"corners": [1, 2], "arc": {"center": [0, 10.0, 0]}, "name": "round"}, {"corners": [3, 4], "name": "top"}, {"corners": [4, 1], "name": "sym"}]}},
|
||||
{"command": "mesh.block", "params": {"body": "Foundation", "kind": "quad4", "corners": [[0, -5, 0], [5, -5, 0], [5, 0, 0], [0, 0, 0]], "divisions": [24, 14], "bias": [10, 0.1], "edges": [{"corners": [1, 2], "name": "base"}, {"corners": [3, 4], "name": "flat"}, {"corners": [4, 1], "name": "sym"}], "merge": 0}},
|
||||
{"command": "model.set.create", "params": {"name": "indenter", "geometry": [{"body": "Indenter", "face": "area"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "foundation", "geometry": [{"body": "Foundation", "face": "area"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "round", "geometry": [{"body": "Indenter", "edge": "round"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "flat", "geometry": [{"body": "Foundation", "edge": "flat"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Indenter", "edge": "top"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Foundation", "edge": "base"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "sym", "geometry": [{"body": "Indenter", "edge": "sym"}, {"body": "Foundation", "edge": "sym"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000.0, "poisson": 0.3}}},
|
||||
{"command": "model.section.create", "params": {"name": "indenter", "set": "indenter", "material": "Steel", "kind": {"type": "plane_strain", "thickness": 1.0}}},
|
||||
{"command": "model.section.create", "params": {"name": "foundation", "set": "foundation", "material": "Steel", "kind": {"type": "plane_strain", "thickness": 1.0}}},
|
||||
{"command": "interaction.contact.pair", "params": {"name": "hertz", "main": "flat", "secondary": "round"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sym", "type": "displacement", "target": {"set": "sym"}, "values": [0, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "base", "type": "displacement", "target": {"set": "base"}, "values": [null, 0, null, null, null, null]}},
|
||||
{"command": "model.spring.create", "params": {"name": "hold", "type": "spring", "first": {"set": "top"}, "direction": {"dof": 2}, "stiffness": 0.01}},
|
||||
{"command": "model.load.create", "params": {"name": "press", "type": "pressure", "set": "top", "magnitude": 453.1143250369893}},
|
||||
{"command": "model.step.create", "params": {"name": "Press", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "max_iterations": 60}, "boundary_conditions": ["sym", "base"], "loads": ["press"]}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Press"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Hertz line contact: cylinder on a flat (implicit mortar contact)
|
||||
|
||||
A steel cylinder of radius R = 10 mm pressed on a steel block (E = 210 GPa, ν = 0.3; mm, N, MPa)
|
||||
in plane strain: a quarter model of the cylinder's lower part (a 2.5 mm wide block whose bottom
|
||||
is the circular arc, graded towards the contact) on a 5 × 5 mm foundation, symmetric about
|
||||
x = 0, one element thick with every node held in z. A pressure on the indenter's top carries
|
||||
the load P = 2265.57 N/mm (per length of the whole cylinder) chosen so that Hertz's half-width is
|
||||
b = 0.5 mm and the peak pressure p₀ = 2884.6 MPa. Soft springs (0.01 N/mm per node) hold the
|
||||
indenter's top vertically until the contact closes. One nonlinear static step without nonlinear
|
||||
geometry: small sliding, mortar contact of the round face (secondary) on the flat (main) by an
|
||||
augmented Lagrangian.
|
||||
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"title": "Hertz line contact: a cylinder pressed on a flat (implicit mortar contact, plane strain)",
|
||||
"kind": "theory",
|
||||
"source": "H. Hertz, Über die Berührung fester elastischer Körper, J. reine angew. Math. 92:156–171, 1882; K. L. Johnson, Contact Mechanics, Cambridge University Press 1985, §4.3 (two-dimensional contact of cylindrical bodies, eqs. 4.45–4.49): b = √(4PR/(πE*)), p₀ = 2P/(πb), with 1/E* = 2(1 − ν²)/E for two bodies of one material and R the cylinder's radius on a flat",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "peak_pressure",
|
||||
"quantity": "the largest contact pressure p₀ (at x = 0)",
|
||||
"measure": {
|
||||
"from": "solve",
|
||||
"pointer": "/contacts/0/max_pressure/value"
|
||||
},
|
||||
"value": 2884.6153846153843,
|
||||
"tolerance": {
|
||||
"relative": 0.02
|
||||
},
|
||||
"reason": "p₀ = 2P/(πb) for the load per length P = 2265.57 N/mm, b = 0.5 mm; the contact width spans seven elements of the graded mesh"
|
||||
},
|
||||
{
|
||||
"id": "normal_force",
|
||||
"quantity": "the force across the contact (half model, 1 mm thick)",
|
||||
"measure": {
|
||||
"from": "solve",
|
||||
"pointer": "/contacts/0/normal_force"
|
||||
},
|
||||
"value": 1132.7858125924733,
|
||||
"tolerance": {
|
||||
"relative": 0.002
|
||||
},
|
||||
"reason": "the pressure carries the whole load; the soft springs that hold the indenter before contact take a millionth"
|
||||
},
|
||||
{
|
||||
"id": "pressure_x0296",
|
||||
"quantity": "the contact pressure at the secondary node at x = 0.29594 (node 7)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Press",
|
||||
"nodes": [
|
||||
7
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure"
|
||||
},
|
||||
"value": 2325.066800691437,
|
||||
"tolerance": {
|
||||
"relative": 0.05
|
||||
},
|
||||
"reason": "p₀ √(1 − x²/b²) at the node; the nodal pressure (the node's normal force over the area it carries) averages the distribution over the node's elements"
|
||||
},
|
||||
{
|
||||
"id": "pressure_x0367",
|
||||
"quantity": "the contact pressure at the secondary node at x = 0.36701 (node 8)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Press",
|
||||
"nodes": [
|
||||
8
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure"
|
||||
},
|
||||
"value": 1959.0164149439815,
|
||||
"tolerance": {
|
||||
"relative": 0.1
|
||||
},
|
||||
"reason": "as at node 7, nearer the edge, where the elements round off the square-root fall of the pressure to zero at b"
|
||||
},
|
||||
{
|
||||
"id": "pressure_x0633",
|
||||
"quantity": "the contact pressure at the secondary node at x = 0.63325 (node 11, beyond the half-width b = 0.5)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Press",
|
||||
"nodes": [
|
||||
11
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/contact/0/pressure"
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 1.0
|
||||
},
|
||||
"reason": "outside the contact the surfaces are apart: no pressure (an element beyond b, since the edge element carries part of the last closed node's pressure)"
|
||||
},
|
||||
{
|
||||
"id": "syy_depth",
|
||||
"quantity": "the vertical stress σ_yy under the centre of contact at depth 0.47127 (node 713)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Press",
|
||||
"nodes": [
|
||||
713
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/stress/1"
|
||||
},
|
||||
"value": -2099.1580228637245,
|
||||
"tolerance": {
|
||||
"relative": 0.04
|
||||
},
|
||||
"reason": "σ_z = −p₀ / √(1 + z²/b²) on the axis of a half-plane (Johnson 1985, eq. 4.49); the foundation is ten half-widths wide and deep, not a half-plane, and the stress is the nodal average of the elements around the node"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"normal_force": {
|
||||
"band": 1.133036636011328e-6,
|
||||
"value": 1133.0366360113278
|
||||
},
|
||||
"peak_pressure": {
|
||||
"band": 2.891953889449524e-6,
|
||||
"value": 2891.953889449524
|
||||
},
|
||||
"pressure_x0296": {
|
||||
"band": 2.325066800691437e-6,
|
||||
"value": 2257.8357909118563
|
||||
},
|
||||
"pressure_x0367": {
|
||||
"band": 2.0784005834495077e-6,
|
||||
"value": 2078.4005834495074
|
||||
},
|
||||
"pressure_x0633": {
|
||||
"band": 1.0,
|
||||
"value": -0.0
|
||||
},
|
||||
"syy_depth": {
|
||||
"band": 2.0991580228637247e-6,
|
||||
"value": -2059.819639241533
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Indenter", "kind": "hex8", "corners": [[0, 0, 0], [2.5, 0.3175416344814579, 0], [2.5, 3.0, 0], [0, 3.0, 0], [0, 0, 1.0], [2.5, 0.3175416344814579, 1.0], [2.5, 3.0, 1.0], [0, 3.0, 1.0]], "divisions": [20, 10, 1], "bias": [8, 8, 1], "edges": [{"corners": [1, 2], "arc": {"center": [0, 10.0, 0]}}, {"corners": [5, 6], "arc": {"center": [0, 10.0, 1.0]}}], "faces": {"3": "round", "5": "top", "6": "sym"}}},
|
||||
{"command": "mesh.block", "params": {"body": "Foundation", "kind": "hex8", "corners": [[0, -5, 0], [5, -5, 0], [5, 0, 0], [0, 0, 0], [0, -5, 1.0], [5, -5, 1.0], [5, 0, 1.0], [0, 0, 1.0]], "divisions": [24, 14, 1], "bias": [10, 0.1, 1], "faces": {"3": "base", "5": "flat", "6": "sym"}, "merge": 0}},
|
||||
{"command": "model.set.create", "params": {"name": "indenter", "geometry": [{"body": "Indenter"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "foundation", "geometry": [{"body": "Foundation"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Indenter"}, {"body": "Foundation"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "round", "geometry": [{"body": "Indenter", "face": "round"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "flat", "geometry": [{"body": "Foundation", "face": "flat"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Indenter", "face": "top"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Foundation", "face": "base"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "sym", "geometry": [{"body": "Indenter", "face": "sym"}, {"body": "Foundation", "face": "sym"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000.0, "poisson": 0.3}}},
|
||||
{"command": "model.section.create", "params": {"name": "indenter", "set": "indenter", "material": "Steel", "kind": {"type": "solid"}}},
|
||||
{"command": "model.section.create", "params": {"name": "foundation", "set": "foundation", "material": "Steel", "kind": {"type": "solid"}}},
|
||||
{"command": "interaction.contact.pair", "params": {"name": "hertz", "main": "flat", "secondary": "round"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sym", "type": "displacement", "target": {"set": "sym"}, "values": [0, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "plane", "type": "displacement", "target": {"set": "all"}, "values": [null, null, 0, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "base", "type": "displacement", "target": {"set": "base"}, "values": [null, 0, null, null, null, null]}},
|
||||
{"command": "model.spring.create", "params": {"name": "hold", "type": "spring", "first": {"set": "top"}, "direction": {"dof": 2}, "stiffness": 0.01}},
|
||||
{"command": "model.load.create", "params": {"name": "press", "type": "pressure", "set": "top", "magnitude": 453.1143250369893}},
|
||||
{"command": "model.step.create", "params": {"name": "Press", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "max_iterations": 60}, "boundary_conditions": ["sym", "plane", "base"], "loads": ["press"]}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Press"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
# Inflation of a stretched Mooney–Rivlin tube, axisymmetric
|
||||
|
||||
The tube of `nl-mooney-rivlin-tube` (A = 10, B = 20, one unit long; E = 3, ν = 0.4995,
|
||||
C₀₁ = 0.2) as an axisymmetric model of 12 × 2 hybrid quad4 (one constant pressure each, the mean
|
||||
dilatation with the hoop strain): the end y = 0 on a symmetry line, the end y = 1 stretched to
|
||||
λ_z = 1.5 while the follower pressure p = 0.540850 inflates the bore. The closed form puts the
|
||||
bore at a = 15 and the outer wall at b = 20.615528 (see `nl-mooney-rivlin-tube`); this case checks
|
||||
the nearly incompressible 2D hybrid elements at large strain against it.
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"title": "Inflation of a stretched Mooney–Rivlin tube (axisymmetric hybrid quad4)",
|
||||
"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.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.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.03287906011305e-9,
|
||||
"value": 5.03287906011305
|
||||
},
|
||||
"ub": {
|
||||
"band": 0.000040000644728979273,
|
||||
"value": 0.6447289792693816
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Pipe", "kind": "quad4", "corners": [[10.0, 0, 0], [20.0, 0, 0], [20.0, 1, 0], [10.0, 1, 0]], "divisions": [12, 2], "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": "bottom", "geometry": [{"body": "Pipe", "edge": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Pipe", "edge": "top"}]}},
|
||||
{"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": "axisymmetric"}, "formulation": {"hybrid": true}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "sb", "type": "symmetry", "target": {"set": "bottom"}, "plane": "y"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "stretch", "type": "displacement", "target": {"set": "top"}, "values": [null, 0.5, null, 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": ["sb", "stretch"], "loads": ["p"]}},
|
||||
{"command": "solve.nonlinear", "params": {"step": "Inflate"}, "id": "solve"}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
# Tie patch test (non-matching meshes)
|
||||
|
||||
Two unit cubes stacked along z, meshed independently: 3 × 3 × 2 hex8 below (graded in x and y),
|
||||
2 × 2 × 2 above, so the nodes at z = 1 do not match. A tie bonds the upper block's bottom
|
||||
(secondary) to the lower block's top (main). The faces x = 0 and y = 0 are symmetry planes, the
|
||||
base z = 0 is held in z, and the top z = 2 is pulled up by 0.02 (E = 1000, ν = 0.3) in a linear
|
||||
static step. The exact state is the uniaxial stress σ_zz = 10, uniform in both blocks.
|
||||
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"title": "Tie patch test: two blocks with non-matching meshes tied together in uniaxial tension",
|
||||
"kind": "theory",
|
||||
"source": "The patch test for mesh tying: a tie across non-matching meshes must transmit a constant stress exactly; B. I. Wohlmuth, A mortar finite element method using dual spaces for the Lagrange multiplier, SIAM J. Numer. Anal. 38(3):989–1012, 2000; M. A. Puso, A 3D mortar method for solid mechanics, IJNME 59:315–336, 2004. Uniaxial stress between frictionless symmetry planes is uniform in both blocks",
|
||||
"quantities": [
|
||||
{
|
||||
"id": "szz_10",
|
||||
"quantity": "σ_zz of the lower block's corner element under the tie (element 10)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
10
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
},
|
||||
"value": 10.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-09
|
||||
},
|
||||
"reason": "uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes"
|
||||
},
|
||||
{
|
||||
"id": "szz_14",
|
||||
"quantity": "σ_zz of the lower block's centre element under the tie (element 14)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
14
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
},
|
||||
"value": 10.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-09
|
||||
},
|
||||
"reason": "uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes"
|
||||
},
|
||||
{
|
||||
"id": "szz_1",
|
||||
"quantity": "σ_zz of the lower block's bottom corner element (element 1)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
},
|
||||
"value": 10.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-09
|
||||
},
|
||||
"reason": "uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes"
|
||||
},
|
||||
{
|
||||
"id": "szz_19",
|
||||
"quantity": "σ_zz of the upper block's corner element over the tie (element 19)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
19
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
},
|
||||
"value": 10.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-09
|
||||
},
|
||||
"reason": "uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes"
|
||||
},
|
||||
{
|
||||
"id": "szz_26",
|
||||
"quantity": "σ_zz of the upper block's top far corner element (element 26)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
26
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/2"
|
||||
},
|
||||
"value": 10.0,
|
||||
"tolerance": {
|
||||
"relative": 1e-09
|
||||
},
|
||||
"reason": "uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes"
|
||||
},
|
||||
{
|
||||
"id": "sxx_14",
|
||||
"quantity": "σ_xx of element 14 (uniaxial stress: 0)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"elements": [
|
||||
14
|
||||
]
|
||||
},
|
||||
"pointer": "/elements/0/stress/0/0"
|
||||
},
|
||||
"value": 0.0,
|
||||
"tolerance": {
|
||||
"absolute": 1e-09
|
||||
},
|
||||
"reason": "the tie must not hold the lateral contraction of either mesh"
|
||||
},
|
||||
{
|
||||
"id": "uz_interface",
|
||||
"quantity": "u_z at the tied secondary node (0.5, 0.5, 1)",
|
||||
"measure": {
|
||||
"command": "solve.probe",
|
||||
"params": {
|
||||
"step": "Pull",
|
||||
"at": [
|
||||
[
|
||||
0.5,
|
||||
0.5,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"pointer": "/nodes/0/displacement/2"
|
||||
},
|
||||
"value": 0.01,
|
||||
"tolerance": {
|
||||
"relative": 1e-09
|
||||
},
|
||||
"reason": "the tied node follows the uniform strain field"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
|
||||
"values": {
|
||||
"sxx_14": {
|
||||
"band": 1e-9,
|
||||
"value": -5.317400101732486e-15
|
||||
},
|
||||
"szz_1": {
|
||||
"band": 1.0000000000000018e-8,
|
||||
"value": 10.000000000000018
|
||||
},
|
||||
"szz_10": {
|
||||
"band": 1e-8,
|
||||
"value": 9.999999999999993
|
||||
},
|
||||
"szz_14": {
|
||||
"band": 1.0000000000000002e-8,
|
||||
"value": 10.000000000000002
|
||||
},
|
||||
"szz_19": {
|
||||
"band": 1.0000000000000017e-8,
|
||||
"value": 10.000000000000016
|
||||
},
|
||||
"szz_26": {
|
||||
"band": 1.000000000000001e-8,
|
||||
"value": 10.000000000000009
|
||||
},
|
||||
"uz_interface": {
|
||||
"band": 1.0000000000000001e-11,
|
||||
"value": 0.009999999999999997
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{"command": "mesh.block", "params": {"body": "Lower", "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": [3, 3, 2], "faces": {"1": "bottom", "2": "top", "3": "y0", "6": "x0"}, "bias": [2.0, 0.6, 1.0]}},
|
||||
{"command": "mesh.block", "params": {"body": "Upper", "kind": "hex8", "corners": [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1], [0, 0, 2], [1, 0, 2], [1, 1, 2], [0, 1, 2]], "divisions": [2, 2, 2], "faces": {"1": "bottom", "2": "top", "3": "y0", "6": "x0"}, "merge": 0}},
|
||||
{"command": "model.set.create", "params": {"name": "lower", "geometry": [{"body": "Lower"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "upper", "geometry": [{"body": "Upper"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "lower_top", "geometry": [{"body": "Lower", "face": "top"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "upper_bottom", "geometry": [{"body": "Upper", "face": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Lower", "face": "x0"}, {"body": "Upper", "face": "x0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Lower", "face": "y0"}, {"body": "Upper", "face": "y0"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Lower", "face": "bottom"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Upper", "face": "top"}]}},
|
||||
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}}},
|
||||
{"command": "model.section.create", "params": {"name": "lower", "set": "lower", "material": "Mat", "kind": {"type": "solid"}}},
|
||||
{"command": "model.section.create", "params": {"name": "upper", "set": "upper", "material": "Mat", "kind": {"type": "solid"}}},
|
||||
{"command": "interaction.tie", "params": {"name": "glue", "main": "lower_top", "secondary": "upper_bottom"}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "x0", "type": "displacement", "target": {"set": "x0"}, "values": [0, null, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "y0", "type": "displacement", "target": {"set": "y0"}, "values": [null, 0, null, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "z0", "type": "displacement", "target": {"set": "z0"}, "values": [null, null, 0, null, null, null]}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "push", "type": "displacement", "target": {"set": "top"}, "values": [null, null, 0.02, null, null, null]}},
|
||||
{"command": "model.step.create", "params": {"name": "Pull", "procedure": {"type": "static"}, "boundary_conditions": ["x0", "y0", "z0", "push"], "loads": []}},
|
||||
{"command": "solve.static", "params": {"step": "Pull"}, "id": "pull"}
|
||||
]
|
||||
Reference in new issue
Block a user