From 6d9452ad840047182ea93c735287ae335b6fd76b Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:44:34 -0400 Subject: [PATCH] Verification oracle: tests/oracle cases, cargo xtask oracle, fixtures, docs/oracle.md, CI job The oracle binary (modelcraft-testkit) runs each case's recipe through the command engine, measures its quantities by JSON pointers into step outputs or commands, compares them with the cited references and the frozen fixtures, and writes docs/oracle.md. 20 theory cases (patch tests of every solid element, Timoshenko cantilevers, Lame, Kirsch, beam frequencies, torsion and bending through couplings, explicit waves and impact, CDT invariants), 8 published benchmarks (MacNeal-Harder patch, straight and curved cantilevers, thick cylinder, Scordelis-Lo with solids, NAFEMS LE1, LE10, FV32) and 10 pending cases with citations. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --- .github/workflows/ci.yml | 18 + Cargo.lock | 1 + crates/engine/src/cmd/block.rs | 173 +++-- crates/testkit/Cargo.toml | 3 +- crates/testkit/src/bin/oracle.rs | 43 ++ crates/testkit/src/lib.rs | 4 + crates/testkit/src/oracle.rs | 688 ++++++++++++++++++ docs/oracle.md | 169 +++++ docs/verification.md | 58 ++ tests/oracle/bar-pressure-wave/README.md | 4 + tests/oracle/bar-pressure-wave/expected.json | 66 ++ tests/oracle/bar-pressure-wave/fixture.json | 17 + tests/oracle/bar-pressure-wave/recipe.json | 10 + tests/oracle/bar-rigid-wall-impact/README.md | 5 + .../bar-rigid-wall-impact/expected.json | 67 ++ .../oracle/bar-rigid-wall-impact/fixture.json | 17 + .../oracle/bar-rigid-wall-impact/recipe.json | 9 + tests/oracle/cantilever-frequencies/README.md | 4 + .../cantilever-frequencies/expected.json | 46 ++ .../cantilever-frequencies/fixture.json | 17 + .../oracle/cantilever-frequencies/recipe.json | 10 + tests/oracle/cantilever-hex20/README.md | 5 + tests/oracle/cantilever-hex20/expected.json | 43 ++ tests/oracle/cantilever-hex20/fixture.json | 13 + tests/oracle/cantilever-hex20/recipe.json | 12 + .../cantilever-hex8-incompatible/README.md | 5 + .../expected.json | 43 ++ .../cantilever-hex8-incompatible/fixture.json | 13 + .../cantilever-hex8-incompatible/recipe.json | 12 + tests/oracle/cantilever-tet10/README.md | 5 + tests/oracle/cantilever-tet10/expected.json | 43 ++ tests/oracle/cantilever-tet10/fixture.json | 13 + tests/oracle/cantilever-tet10/recipe.json | 12 + tests/oracle/cdt-cylinder/README.md | 5 + tests/oracle/cdt-cylinder/design.json | 12 + tests/oracle/cdt-cylinder/expected.json | 33 + tests/oracle/cdt-cylinder/fixture.json | 13 + tests/oracle/cdt-cylinder/recipe.json | 3 + tests/oracle/cdt-stepped-block/README.md | 5 + tests/oracle/cdt-stepped-block/design.json | 69 ++ tests/oracle/cdt-stepped-block/expected.json | 46 ++ tests/oracle/cdt-stepped-block/fixture.json | 17 + tests/oracle/cdt-stepped-block/recipe.json | 3 + tests/oracle/cylinder-channel-2d1/README.md | 3 + .../oracle/cylinder-channel-2d1/expected.json | 19 + tests/oracle/free-beam-frequencies/README.md | 4 + .../free-beam-frequencies/expected.json | 46 ++ .../oracle/free-beam-frequencies/fixture.json | 17 + .../oracle/free-beam-frequencies/recipe.json | 8 + tests/oracle/kirsch-plate/README.md | 7 + tests/oracle/kirsch-plate/expected.json | 53 ++ tests/oracle/kirsch-plate/fixture.json | 13 + tests/oracle/kirsch-plate/recipe.json | 17 + tests/oracle/lame-cylinder/README.md | 6 + tests/oracle/lame-cylinder/expected.json | 76 ++ tests/oracle/lame-cylinder/fixture.json | 17 + tests/oracle/lame-cylinder/recipe.json | 18 + .../oracle/lid-driven-cavity-re100/README.md | 3 + .../lid-driven-cavity-re100/expected.json | 14 + tests/oracle/mh-curved-cantilever/README.md | 6 + .../oracle/mh-curved-cantilever/expected.json | 55 ++ .../oracle/mh-curved-cantilever/fixture.json | 4 + tests/oracle/mh-curved-cantilever/recipe.json | 15 + tests/oracle/mh-solid-patch/README.md | 6 + tests/oracle/mh-solid-patch/expected.json | 168 +++++ tests/oracle/mh-solid-patch/fixture.json | 33 + tests/oracle/mh-solid-patch/recipe.json | 46 ++ tests/oracle/mh-straight-cantilever/README.md | 7 + .../mh-straight-cantilever/expected.json | 92 +++ .../mh-straight-cantilever/fixture.json | 9 + .../oracle/mh-straight-cantilever/recipe.json | 23 + tests/oracle/mh-thick-cylinder/README.md | 7 + tests/oracle/mh-thick-cylinder/expected.json | 76 ++ tests/oracle/mh-thick-cylinder/fixture.json | 17 + tests/oracle/mh-thick-cylinder/recipe.json | 58 ++ tests/oracle/mh-twisted-beam/README.md | 3 + tests/oracle/mh-twisted-beam/expected.json | 19 + tests/oracle/nafems-fv32/README.md | 6 + tests/oracle/nafems-fv32/expected.json | 85 +++ tests/oracle/nafems-fv32/fixture.json | 29 + tests/oracle/nafems-fv32/recipe.json | 12 + .../oracle/nafems-le1-plane-stress/README.md | 3 + .../nafems-le1-plane-stress/expected.json | 14 + tests/oracle/nafems-le1/README.md | 8 + tests/oracle/nafems-le1/expected.json | 30 + tests/oracle/nafems-le1/fixture.json | 9 + tests/oracle/nafems-le1/recipe.json | 16 + tests/oracle/nafems-le10/README.md | 6 + tests/oracle/nafems-le10/expected.json | 30 + tests/oracle/nafems-le10/fixture.json | 9 + tests/oracle/nafems-le10/recipe.json | 19 + tests/oracle/nafems-le11/README.md | 3 + tests/oracle/nafems-le11/expected.json | 14 + tests/oracle/patch-hex20-reduced/README.md | 8 + .../oracle/patch-hex20-reduced/expected.json | 168 +++++ tests/oracle/patch-hex20-reduced/fixture.json | 33 + tests/oracle/patch-hex20-reduced/recipe.json | 27 + tests/oracle/patch-hex20/README.md | 8 + tests/oracle/patch-hex20/expected.json | 168 +++++ tests/oracle/patch-hex20/fixture.json | 33 + tests/oracle/patch-hex20/recipe.json | 27 + .../oracle/patch-hex8-incompatible/README.md | 8 + .../patch-hex8-incompatible/expected.json | 168 +++++ .../patch-hex8-incompatible/fixture.json | 33 + .../patch-hex8-incompatible/recipe.json | 27 + tests/oracle/patch-hex8/README.md | 8 + tests/oracle/patch-hex8/expected.json | 168 +++++ tests/oracle/patch-hex8/fixture.json | 33 + tests/oracle/patch-hex8/recipe.json | 27 + tests/oracle/patch-tet10/README.md | 8 + tests/oracle/patch-tet10/expected.json | 168 +++++ tests/oracle/patch-tet10/fixture.json | 33 + tests/oracle/patch-tet10/recipe.json | 192 +++++ tests/oracle/patch-tet4/README.md | 8 + tests/oracle/patch-tet4/expected.json | 168 +++++ tests/oracle/patch-tet4/fixture.json | 33 + tests/oracle/patch-tet4/recipe.json | 27 + tests/oracle/patch-wedge6/README.md | 8 + tests/oracle/patch-wedge6/expected.json | 168 +++++ tests/oracle/patch-wedge6/fixture.json | 33 + tests/oracle/patch-wedge6/recipe.json | 27 + tests/oracle/pinched-cylinder/README.md | 3 + tests/oracle/pinched-cylinder/expected.json | 14 + tests/oracle/pinched-hemisphere/README.md | 3 + tests/oracle/pinched-hemisphere/expected.json | 14 + tests/oracle/plastic-thick-cylinder/README.md | 3 + .../plastic-thick-cylinder/expected.json | 14 + tests/oracle/pure-bending-coupling/README.md | 5 + .../pure-bending-coupling/expected.json | 44 ++ .../oracle/pure-bending-coupling/fixture.json | 9 + .../oracle/pure-bending-coupling/recipe.json | 14 + .../oracle/scordelis-lo-roof-shell/README.md | 3 + .../scordelis-lo-roof-shell/expected.json | 14 + .../oracle/scordelis-lo-roof-solid/README.md | 7 + .../scordelis-lo-roof-solid/expected.json | 30 + .../scordelis-lo-roof-solid/fixture.json | 9 + .../scordelis-lo-roof-solid/recipe.json | 15 + tests/oracle/taylor-bar/README.md | 3 + tests/oracle/taylor-bar/expected.json | 14 + tests/oracle/tube-torsion-coupling/README.md | 5 + .../tube-torsion-coupling/expected.json | 43 ++ .../oracle/tube-torsion-coupling/fixture.json | 13 + .../oracle/tube-torsion-coupling/recipe.json | 17 + xtask/src/main.rs | 11 + 144 files changed, 5162 insertions(+), 74 deletions(-) create mode 100644 crates/testkit/src/bin/oracle.rs create mode 100644 crates/testkit/src/oracle.rs create mode 100644 docs/oracle.md create mode 100644 tests/oracle/bar-pressure-wave/README.md create mode 100644 tests/oracle/bar-pressure-wave/expected.json create mode 100644 tests/oracle/bar-pressure-wave/fixture.json create mode 100644 tests/oracle/bar-pressure-wave/recipe.json create mode 100644 tests/oracle/bar-rigid-wall-impact/README.md create mode 100644 tests/oracle/bar-rigid-wall-impact/expected.json create mode 100644 tests/oracle/bar-rigid-wall-impact/fixture.json create mode 100644 tests/oracle/bar-rigid-wall-impact/recipe.json create mode 100644 tests/oracle/cantilever-frequencies/README.md create mode 100644 tests/oracle/cantilever-frequencies/expected.json create mode 100644 tests/oracle/cantilever-frequencies/fixture.json create mode 100644 tests/oracle/cantilever-frequencies/recipe.json create mode 100644 tests/oracle/cantilever-hex20/README.md create mode 100644 tests/oracle/cantilever-hex20/expected.json create mode 100644 tests/oracle/cantilever-hex20/fixture.json create mode 100644 tests/oracle/cantilever-hex20/recipe.json create mode 100644 tests/oracle/cantilever-hex8-incompatible/README.md create mode 100644 tests/oracle/cantilever-hex8-incompatible/expected.json create mode 100644 tests/oracle/cantilever-hex8-incompatible/fixture.json create mode 100644 tests/oracle/cantilever-hex8-incompatible/recipe.json create mode 100644 tests/oracle/cantilever-tet10/README.md create mode 100644 tests/oracle/cantilever-tet10/expected.json create mode 100644 tests/oracle/cantilever-tet10/fixture.json create mode 100644 tests/oracle/cantilever-tet10/recipe.json create mode 100644 tests/oracle/cdt-cylinder/README.md create mode 100644 tests/oracle/cdt-cylinder/design.json create mode 100644 tests/oracle/cdt-cylinder/expected.json create mode 100644 tests/oracle/cdt-cylinder/fixture.json create mode 100644 tests/oracle/cdt-cylinder/recipe.json create mode 100644 tests/oracle/cdt-stepped-block/README.md create mode 100644 tests/oracle/cdt-stepped-block/design.json create mode 100644 tests/oracle/cdt-stepped-block/expected.json create mode 100644 tests/oracle/cdt-stepped-block/fixture.json create mode 100644 tests/oracle/cdt-stepped-block/recipe.json create mode 100644 tests/oracle/cylinder-channel-2d1/README.md create mode 100644 tests/oracle/cylinder-channel-2d1/expected.json create mode 100644 tests/oracle/free-beam-frequencies/README.md create mode 100644 tests/oracle/free-beam-frequencies/expected.json create mode 100644 tests/oracle/free-beam-frequencies/fixture.json create mode 100644 tests/oracle/free-beam-frequencies/recipe.json create mode 100644 tests/oracle/kirsch-plate/README.md create mode 100644 tests/oracle/kirsch-plate/expected.json create mode 100644 tests/oracle/kirsch-plate/fixture.json create mode 100644 tests/oracle/kirsch-plate/recipe.json create mode 100644 tests/oracle/lame-cylinder/README.md create mode 100644 tests/oracle/lame-cylinder/expected.json create mode 100644 tests/oracle/lame-cylinder/fixture.json create mode 100644 tests/oracle/lame-cylinder/recipe.json create mode 100644 tests/oracle/lid-driven-cavity-re100/README.md create mode 100644 tests/oracle/lid-driven-cavity-re100/expected.json create mode 100644 tests/oracle/mh-curved-cantilever/README.md create mode 100644 tests/oracle/mh-curved-cantilever/expected.json create mode 100644 tests/oracle/mh-curved-cantilever/fixture.json create mode 100644 tests/oracle/mh-curved-cantilever/recipe.json create mode 100644 tests/oracle/mh-solid-patch/README.md create mode 100644 tests/oracle/mh-solid-patch/expected.json create mode 100644 tests/oracle/mh-solid-patch/fixture.json create mode 100644 tests/oracle/mh-solid-patch/recipe.json create mode 100644 tests/oracle/mh-straight-cantilever/README.md create mode 100644 tests/oracle/mh-straight-cantilever/expected.json create mode 100644 tests/oracle/mh-straight-cantilever/fixture.json create mode 100644 tests/oracle/mh-straight-cantilever/recipe.json create mode 100644 tests/oracle/mh-thick-cylinder/README.md create mode 100644 tests/oracle/mh-thick-cylinder/expected.json create mode 100644 tests/oracle/mh-thick-cylinder/fixture.json create mode 100644 tests/oracle/mh-thick-cylinder/recipe.json create mode 100644 tests/oracle/mh-twisted-beam/README.md create mode 100644 tests/oracle/mh-twisted-beam/expected.json create mode 100644 tests/oracle/nafems-fv32/README.md create mode 100644 tests/oracle/nafems-fv32/expected.json create mode 100644 tests/oracle/nafems-fv32/fixture.json create mode 100644 tests/oracle/nafems-fv32/recipe.json create mode 100644 tests/oracle/nafems-le1-plane-stress/README.md create mode 100644 tests/oracle/nafems-le1-plane-stress/expected.json create mode 100644 tests/oracle/nafems-le1/README.md create mode 100644 tests/oracle/nafems-le1/expected.json create mode 100644 tests/oracle/nafems-le1/fixture.json create mode 100644 tests/oracle/nafems-le1/recipe.json create mode 100644 tests/oracle/nafems-le10/README.md create mode 100644 tests/oracle/nafems-le10/expected.json create mode 100644 tests/oracle/nafems-le10/fixture.json create mode 100644 tests/oracle/nafems-le10/recipe.json create mode 100644 tests/oracle/nafems-le11/README.md create mode 100644 tests/oracle/nafems-le11/expected.json create mode 100644 tests/oracle/patch-hex20-reduced/README.md create mode 100644 tests/oracle/patch-hex20-reduced/expected.json create mode 100644 tests/oracle/patch-hex20-reduced/fixture.json create mode 100644 tests/oracle/patch-hex20-reduced/recipe.json create mode 100644 tests/oracle/patch-hex20/README.md create mode 100644 tests/oracle/patch-hex20/expected.json create mode 100644 tests/oracle/patch-hex20/fixture.json create mode 100644 tests/oracle/patch-hex20/recipe.json create mode 100644 tests/oracle/patch-hex8-incompatible/README.md create mode 100644 tests/oracle/patch-hex8-incompatible/expected.json create mode 100644 tests/oracle/patch-hex8-incompatible/fixture.json create mode 100644 tests/oracle/patch-hex8-incompatible/recipe.json create mode 100644 tests/oracle/patch-hex8/README.md create mode 100644 tests/oracle/patch-hex8/expected.json create mode 100644 tests/oracle/patch-hex8/fixture.json create mode 100644 tests/oracle/patch-hex8/recipe.json create mode 100644 tests/oracle/patch-tet10/README.md create mode 100644 tests/oracle/patch-tet10/expected.json create mode 100644 tests/oracle/patch-tet10/fixture.json create mode 100644 tests/oracle/patch-tet10/recipe.json create mode 100644 tests/oracle/patch-tet4/README.md create mode 100644 tests/oracle/patch-tet4/expected.json create mode 100644 tests/oracle/patch-tet4/fixture.json create mode 100644 tests/oracle/patch-tet4/recipe.json create mode 100644 tests/oracle/patch-wedge6/README.md create mode 100644 tests/oracle/patch-wedge6/expected.json create mode 100644 tests/oracle/patch-wedge6/fixture.json create mode 100644 tests/oracle/patch-wedge6/recipe.json create mode 100644 tests/oracle/pinched-cylinder/README.md create mode 100644 tests/oracle/pinched-cylinder/expected.json create mode 100644 tests/oracle/pinched-hemisphere/README.md create mode 100644 tests/oracle/pinched-hemisphere/expected.json create mode 100644 tests/oracle/plastic-thick-cylinder/README.md create mode 100644 tests/oracle/plastic-thick-cylinder/expected.json create mode 100644 tests/oracle/pure-bending-coupling/README.md create mode 100644 tests/oracle/pure-bending-coupling/expected.json create mode 100644 tests/oracle/pure-bending-coupling/fixture.json create mode 100644 tests/oracle/pure-bending-coupling/recipe.json create mode 100644 tests/oracle/scordelis-lo-roof-shell/README.md create mode 100644 tests/oracle/scordelis-lo-roof-shell/expected.json create mode 100644 tests/oracle/scordelis-lo-roof-solid/README.md create mode 100644 tests/oracle/scordelis-lo-roof-solid/expected.json create mode 100644 tests/oracle/scordelis-lo-roof-solid/fixture.json create mode 100644 tests/oracle/scordelis-lo-roof-solid/recipe.json create mode 100644 tests/oracle/taylor-bar/README.md create mode 100644 tests/oracle/taylor-bar/expected.json create mode 100644 tests/oracle/tube-torsion-coupling/README.md create mode 100644 tests/oracle/tube-torsion-coupling/expected.json create mode 100644 tests/oracle/tube-torsion-coupling/fixture.json create mode 100644 tests/oracle/tube-torsion-coupling/recipe.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b640ca..d98ecbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,24 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - run: cargo xtask test --heavy + oracle: + # Verification and validation (docs/verification.md): every case against its reference and + # its fixture, and docs/oracle.md must be the table the cases produce. Cases marked `slow` + # run here too while the whole set stays within a few minutes; `--quick` skips them. + name: cargo xtask oracle + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + key: oracle + save-if: ${{ github.ref == 'refs/heads/main' }} + - run: cargo xtask oracle + - name: docs/oracle.md is current + run: git diff --exit-code docs/oracle.md + advisories: name: Dependency advisories and sources needs: changes diff --git a/Cargo.lock b/Cargo.lock index 9ebcd3e..04acef1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1995,6 +1995,7 @@ dependencies = [ name = "modelcraft-testkit" version = "0.1.0" dependencies = [ + "modelcraft-engine", "modelcraft-geometry", "modelcraft-mesh", "serde_json", diff --git a/crates/engine/src/cmd/block.rs b/crates/engine/src/cmd/block.rs index ebd8a48..33a865a 100644 --- a/crates/engine/src/cmd/block.rs +++ b/crates/engine/src/cmd/block.rs @@ -145,7 +145,7 @@ fn block_mesh(s: &mut Session, p: &Value) -> Result { let merged: BTreeSet = existing.iter().flatten().copied().collect(); let elements: Vec> = mesh.elements.iter().map(|e| e.iter().map(|&i| labels.get(i as usize).copied().unwrap_or(0)).collect()).collect(); if !merged.is_empty() { - check_conforming(&s.model, kind, &mesh, &elements, &merged)?; + check_conforming(&s.model, kind, &mesh, &elements, &merged, merge)?; } let body_tag = s.model.geometry_tag(&body)?; @@ -233,84 +233,95 @@ fn key(nodes: impl Iterator) -> Vec { k } -/// Where the block joins existing nodes, its boundary faces and the existing element faces on -/// those nodes must be the same faces: no hanging nodes, no mixed linear and quadratic sides. -/// A block joins along whole block faces and edges: when every corner of a block face (both -/// ends of a block edge) lands on an existing node, every node on it must. -fn check_conforming(model: &Model, kind: ElementKind, mesh: &BlockMesh, elements: &[Vec], merged: &BTreeSet) -> Result<()> { - // Labels of the block's corners: the corner point's label in any element that uses it. - let mut point_label: BTreeMap = BTreeMap::new(); - for (e, points) in mesh.elements.iter().enumerate() { - for (k, &pt) in points.iter().enumerate() { - if let Some(&l) = elements.get(e).and_then(|n| n.get(k)) { - point_label.insert(pt, l); - } - } - } - let joined = |pt: u32| point_label.get(&pt).is_some_and(|l| merged.contains(l)); - let hex_faces = ElementKind::Hex8.faces(); - let pieces = |list: &[(u32, u8, u8)], which: usize, local: fn(ElementKind, usize) -> Option<&'static [usize]>| -> Vec { - list.iter() - .filter(|t| usize::from(t.2) == which) - .flat_map(|&(e, k, _)| { - let nodes = elements.get(e as usize); - local(kind, usize::from(k)).unwrap_or(&[]).iter().filter_map(move |&i| nodes.and_then(|n| n.get(i)).copied()) - }) - .collect() +/// Where the block joins existing nodes it must meet the mesh face to face: an element face +/// (of the block, or of the mesh) whose corners all landed on shared nodes must be a face of +/// the other side too, with the same nodes (no mixed linear and quadratic sides); and no node on +/// the block's boundary may lie on an edge of the mesh between two of its nodes, nor the other +/// way round (no hanging nodes where the divisions differ). Edges are tested as straight +/// segments, so a mismatch along a curved shared edge is caught only through its faces. +fn check_conforming(model: &Model, kind: ElementKind, mesh: &BlockMesh, elements: &[Vec], merged: &BTreeSet, merge: f64) -> Result<()> { + let refuse = |what: String| { + Err(EngineError::Params(format!("{what}: the block must meet the mesh face to face, with the same divisions and element kind"))) }; - for (bf, corners) in hex_faces.iter().enumerate() { - if corners.iter().all(|&c| mesh.corners.get(c).is_some_and(|&pt| joined(pt))) { - let nodes = pieces(&mesh.faces, bf, |k, f| k.faces().get(f.checked_sub(1)?).copied()); - if nodes.iter().any(|n| !merged.contains(n)) { - return Err(EngineError::Params(format!( - "block face {} lies on existing nodes at its corners but not at every node: the block must meet the mesh face to face (same divisions and element kind)", - bf + 1 - ))); - } - } + let corners_of = |k: ElementKind, nodes: &[u32], local: &[usize]| -> Vec { + key(local.iter().filter(|&&i| i < k.corner_count()).filter_map(|&i| nodes.get(i).copied())) + }; + // The block's boundary faces, by sorted corners, with their full node lists. + let mut new_faces: BTreeMap, Vec> = BTreeMap::new(); + for &(e, f, _) in &mesh.faces { + let (Some(nodes), Some(local)) = (elements.get(e as usize), usize::from(f).checked_sub(1).and_then(|i| kind.faces().get(i))) else { continue }; + new_faces.insert(corners_of(kind, nodes, local), key(local.iter().filter_map(|&i| nodes.get(i).copied()))); } - for (be, [a, b]) in modelcraft_mesh::block::EDGES.iter().enumerate() { - if [a, b].iter().all(|&&c| mesh.corners.get(c).is_some_and(|&pt| joined(pt))) { - let nodes = pieces(&mesh.edges, be, |k, f| k.edges().get(f.checked_sub(1)?).copied()); - if nodes.iter().any(|n| !merged.contains(n)) { - return Err(EngineError::Params(format!( - "the block edge from corner {} to {} ends on existing nodes but not every node on it does: the block must meet the mesh with the same divisions", - a + 1, - b + 1 - ))); - } - } - } - let new_faces: BTreeSet> = mesh - .faces - .iter() - .filter_map(|&(e, f, _)| { - let nodes = elements.get(e as usize)?; - let local = kind.faces().get(usize::from(f).checked_sub(1)?)?; - Some(key(local.iter().filter_map(|&i| nodes.get(i).copied()))) - }) - .collect(); - let mut old_faces = BTreeSet::new(); - for (label, e) in model.elements() { - if !e.nodes.iter().any(|n| merged.contains(n)) { - continue; - } + let mut old_faces: BTreeMap, Vec> = BTreeMap::new(); + let mut old_edges: BTreeSet<[u32; 2]> = BTreeSet::new(); + for e in model.elements().values().filter(|e| e.nodes.iter().any(|n| merged.contains(n))) { for local in e.kind.faces() { - let face = key(local.iter().filter_map(|&i| e.nodes.get(i).copied())); - if face.iter().all(|n| merged.contains(n)) { - if !new_faces.contains(&face) { - return Err(EngineError::Params(format!( - "element {label} has a face on the block that no block element shares: the block must meet the mesh face to face (same divisions and element kind)" - ))); - } - old_faces.insert(face); + old_faces.insert(corners_of(e.kind, &e.nodes, local), key(local.iter().filter_map(|&i| e.nodes.get(i).copied()))); + } + } + for (corners, nodes) in &new_faces { + if corners.iter().all(|n| merged.contains(n)) && old_faces.get(corners) != Some(nodes) { + return refuse(format!("the block face on nodes {corners:?} lies on existing nodes but is no element face there")); + } + } + for (corners, nodes) in &old_faces { + if corners.iter().all(|n| merged.contains(n)) && new_faces.get(corners) != Some(nodes) { + return refuse(format!("the element face on nodes {corners:?} lies on the block but no block element shares it")); + } + } + // Hanging nodes: boundary nodes of either side on the other side's element edges. + let xyz = |label: u32| model.node(label).map(|n| n.xyz); + let block_points: BTreeMap = + mesh.elements.iter().zip(elements).flat_map(|(pts, labels)| pts.iter().zip(labels).filter_map(|(&p, &l)| Some((l, *mesh.points.get(p as usize)?)))).collect(); + let mut new_edges: BTreeSet<[u32; 2]> = BTreeSet::new(); + for &(e, f, _) in &mesh.faces { + let (Some(nodes), Some(local)) = (elements.get(e as usize), usize::from(f).checked_sub(1).and_then(|i| kind.faces().get(i))) else { continue }; + let cs: Vec = local.iter().filter(|&&i| i < kind.corner_count()).filter_map(|&i| nodes.get(i).copied()).collect(); + for (i, &a) in cs.iter().enumerate() { + let b = cs.get((i + 1) % cs.len()).copied().unwrap_or(a); + new_edges.insert(if a < b { [a, b] } else { [b, a] }); + } + } + for e in model.elements().values().filter(|e| e.nodes.iter().any(|n| merged.contains(n))) { + for local in e.kind.edges() { + if let (Some(&a), Some(&b)) = (local.first().and_then(|&i| e.nodes.get(i)), local.get(1).and_then(|&i| e.nodes.get(i))) { + old_edges.insert(if a < b { [a, b] } else { [b, a] }); } } } - if let Some(f) = new_faces.iter().find(|f| f.iter().all(|n| merged.contains(n)) && !old_faces.contains(*f)) { - return Err(EngineError::Params(format!( - "the block face on nodes {f:?} lies on existing nodes but matches no element face there: the block must meet the mesh face to face (same divisions and element kind)" - ))); + let new_boundary: BTreeSet = new_faces.values().flatten().copied().filter(|n| !merged.contains(n)).collect(); + let on_segment = |p: [f64; 3], a: [f64; 3], b: [f64; 3]| { + let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; + let ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]]; + let len2 = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2]; + if !(len2 > 0.0) { + return false; + } + let t = (ap[0] * ab[0] + ap[1] * ab[1] + ap[2] * ab[2]) / len2; + let d = [ap[0] - t * ab[0], ap[1] - t * ab[1], ap[2] - t * ab[2]]; + let tol = merge / len2.sqrt(); + t > tol && t < 1.0 - tol && (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() <= merge + }; + for &n in &new_boundary { + let Some(&p) = block_points.get(&n) else { continue }; + for [a, b] in &old_edges { + if let (Some(xa), Some(xb)) = (xyz(*a), xyz(*b)) + && on_segment(p, xa, xb) + { + return refuse(format!("a block node at {p:?} lies on the edge between nodes {a} and {b}")); + } + } + } + let old_nodes: BTreeSet = old_faces.values().flatten().copied().filter(|n| !merged.contains(n)).collect(); + for n in old_nodes { + let Some(p) = xyz(n) else { continue }; + for [a, b] in &new_edges { + if let (Some(&xa), Some(&xb)) = (block_points.get(a), block_points.get(b)) + && on_segment(p, xa, xb) + { + return refuse(format!("node {n} at {p:?} lies on a block edge")); + } + } } Ok(()) } @@ -367,6 +378,22 @@ mod tests { let mut t = Session::default(); t.execute("mesh.block", &json!({"body": "A", "kind": "hex8", "corners": cube(0.0), "divisions": [2, 2, 2]})).unwrap(); assert!(t.execute("mesh.block", &json!({"body": "B", "kind": "hex8", "corners": cube(1.0), "divisions": [1, 1, 1]})).is_err()); + // Divisions that share no node inside the face: the hanging nodes on its edges are caught. + assert!(t.execute("mesh.block", &json!({"body": "B", "kind": "hex8", "corners": cube(1.0), "divisions": [3, 3, 3]})).is_err()); + // A ring closed by its last block touches existing nodes at the corners of faces it does + // not share (the ends): that is no conflict. + let mut ring = Session::default(); + for k in 0..4 { + let at = |r: f64, q: usize, z: f64| { + let a = std::f64::consts::FRAC_PI_2 * q as f64; + let c = |v: f64| if v.abs() < 1e-12 { 0.0 } else { v }; + json!([c(r * a.cos()), c(r * a.sin()), z]) + }; + let corners = json!([at(1.0, k, 0.0), at(2.0, k, 0.0), at(2.0, k + 1, 0.0), at(1.0, k + 1, 0.0), at(1.0, k, 1.0), at(2.0, k, 1.0), at(2.0, k + 1, 1.0), at(1.0, k + 1, 1.0)]); + let edges = json!([{"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 1]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 1]}}]); + ring.execute("mesh.block", &json!({"body": "Ring", "kind": "hex20", "corners": corners, "divisions": [2, 3, 2], "edges": edges})).unwrap(); + } + assert_eq!(ring.model.elements().len(), 48); assert!(t.execute("mesh.block", &json!({"body": "B", "kind": "hex8", "corners": cube(1.0), "divisions": [1, 1, 1], "merge": 0})).is_ok()); } diff --git a/crates/testkit/Cargo.toml b/crates/testkit/Cargo.toml index 495a49e..c6a6414 100644 --- a/crates/testkit/Cargo.toml +++ b/crates/testkit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "modelcraft-testkit" -description = "ModelCraft test tooling: the meshing test corpus (SolidCraft recipes and analytic shapes)" +description = "ModelCraft test tooling: the meshing test corpus (SolidCraft recipes and analytic shapes) and the verification oracle" version.workspace = true edition.workspace = true license.workspace = true @@ -12,5 +12,6 @@ workspace = true [dependencies] modelcraft-geometry = { workspace = true, features = ["scripts"] } modelcraft-mesh = { workspace = true } +modelcraft-engine = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/testkit/src/bin/oracle.rs b/crates/testkit/src/bin/oracle.rs new file mode 100644 index 0000000..70fc286 --- /dev/null +++ b/crates/testkit/src/bin/oracle.rs @@ -0,0 +1,43 @@ +//! `oracle [--case NAME]... [--bless NAME|all]... [--quick] [--no-write]`: see +//! [`modelcraft_testkit::oracle`]; run it as `cargo xtask oracle`. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +#![forbid(unsafe_code)] + +use std::process::ExitCode; + +use modelcraft_testkit::oracle::{Options, run}; + +const USAGE: &str = "usage: oracle [--case NAME]... [--bless NAME|all]... [--quick] [--no-write]"; + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + let mut options = Options { write: true, ..Options::default() }; + let mut it = args.iter(); + while let Some(a) = it.next() { + let value = |it: &mut std::slice::Iter<'_, String>| it.next().cloned().ok_or_else(|| format!("{a} needs a value\n{USAGE}")); + let step = match a.as_str() { + "--case" => value(&mut it).map(|v| options.cases.push(v)), + "--bless" => value(&mut it).map(|v| options.bless.push(v)), + "--quick" => { + options.quick = true; + Ok(()) + } + "--no-write" => { + options.write = false; + Ok(()) + } + other => Err(format!("unknown argument `{other}`\n{USAGE}")), + }; + if let Err(e) = step { + eprintln!("error: {e}"); + return ExitCode::FAILURE; + } + } + match run(&modelcraft_testkit::corpus::workspace_root(), &options) { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("error: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/crates/testkit/src/lib.rs b/crates/testkit/src/lib.rs index 0362ef3..9d1e89c 100644 --- a/crates/testkit/src/lib.rs +++ b/crates/testkit/src/lib.rs @@ -4,8 +4,12 @@ //! `tests/corpus//recipe.json`, and analytic shapes generated in code with closed-form //! volumes. Meshing tests load their geometry from here; [`runner`] (the `corpus` binary, //! `cargo xtask corpus`) builds and meshes every part and writes docs/meshing/corpus.md. +//! +//! [`oracle`] (the `oracle` binary, `cargo xtask oracle`) runs the verification and validation +//! cases of `tests/oracle` against their references and fixtures and writes docs/oracle.md. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] pub mod corpus; +pub mod oracle; pub mod runner; diff --git a/crates/testkit/src/oracle.rs b/crates/testkit/src/oracle.rs new file mode 100644 index 0000000..1939799 --- /dev/null +++ b/crates/testkit/src/oracle.rs @@ -0,0 +1,688 @@ +//! The verification and validation oracle (docs/verification.md): every case in +//! `tests/oracle//` is our own model of a problem with a known answer, run through the +//! command engine and compared with its reference values and with its frozen fixture. +//! +//! A case directory holds: +//! - `expected.json`: the case's kind (theory, experiment, benchmark), source, status and its +//! quantities: how each is measured, the reference value, the tolerance and the reason for it; +//! - `recipe.json`: the model as ModelCraft commands (a JSON array of `{"command", "params"}`, +//! each step optionally with an `id` that measures can read its output by); absent while a +//! case is pending; +//! - `design.json` (optional): a SolidCraft script that builds the geometry, opened first; +//! - `fixture.json`: our accepted values, written by `--bless` once the case passes; +//! - `README.md`: the problem in our own words. +//! +//! [`run`] (the `oracle` binary, `cargo xtask oracle`) runs the cases, prints and checks the +//! results and writes `docs/oracle.md` after a full run. + +use std::collections::BTreeMap; +use std::fmt::Write as _; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Instant; + +use modelcraft_engine::Session; +use serde_json::{Value, json}; + +/// Largest case file read. +const MAX_FILE_BYTES: u64 = 16 << 20; + +/// Fixture band: a result may move by this fraction of its size (and a thousandth of the +/// reference's absolute tolerance) before CI calls it a change. Results are deterministic on one +/// platform; the band only absorbs last-digit differences of the system maths library. +pub const FIXTURE_RELATIVE: f64 = 1e-9; + +/// The kinds of reference (docs/verification.md). +pub const KINDS: [&str; 3] = ["theory", "benchmark", "experiment"]; + +/// Options of one run. +#[derive(Debug, Default, Clone)] +pub struct Options { + /// Run only these cases (all when empty). + pub cases: Vec, + /// Write `fixture.json` for these cases when they pass (`all` for every passing case). + pub bless: Vec, + /// Skip cases marked `slow`. + pub quick: bool, + /// Write docs/oracle.md (only after a run of every case). + pub write: bool, +} + +/// A quantity's tolerance: |ours − reference| ≤ relative·|reference| + absolute. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Tolerance { + pub relative: f64, + pub absolute: f64, +} + +impl Tolerance { + fn from_json(v: &Value) -> Result { + let get = |k: &str| -> Result { + match v.get(k) { + None => Ok(0.0), + Some(x) => x.as_f64().filter(|x| x.is_finite() && *x >= 0.0).ok_or_else(|| format!("tolerance `{k}` must be a number ≥ 0")), + } + }; + let t = Tolerance { relative: get("relative")?, absolute: get("absolute")? }; + if t.relative == 0.0 && t.absolute == 0.0 && v.get("exact") != Some(&Value::Bool(true)) { + return Err("a tolerance gives `relative`, `absolute` or `\"exact\": true`".into()); + } + Ok(t) + } + + pub fn allows(&self, reference: f64, ours: f64) -> bool { + (ours - reference).abs() <= self.relative * reference.abs() + self.absolute + } + + fn describe(&self) -> String { + match (self.relative > 0.0, self.absolute > 0.0) { + (true, false) => fraction(self.relative), + (false, true) => format!("± {}", number(self.absolute)), + (true, true) => format!("{} + {}", fraction(self.relative), number(self.absolute)), + (false, false) => "exact".into(), + } + } +} + +/// One quantity of a case. +#[derive(Debug, Clone)] +pub struct Quantity { + pub id: String, + pub description: String, + pub measure: Value, + pub reference: Option, + pub tolerance: Tolerance, + pub reason: String, + pub known_failure: Option, +} + +/// One case's `expected.json`. +#[derive(Debug, Clone)] +pub struct Expected { + pub title: String, + pub kind: String, + pub source: String, + pub pending: Option, + pub slow: bool, + pub quantities: Vec, +} + +fn text(v: &Value, key: &str) -> Result { + v.get(key).and_then(Value::as_str).filter(|s| !s.trim().is_empty()).map(str::to_string).ok_or_else(|| format!("`{key}` must be a non-empty string")) +} + +impl Expected { + pub fn parse(v: &Value) -> Result { + let kind = text(v, "kind")?; + if !KINDS.contains(&kind.as_str()) { + return Err(format!("`kind` must be one of {KINDS:?}")); + } + let pending = match v.get("status").and_then(Value::as_str) { + None | Some("active") => None, + Some("pending") => Some(text(v, "requires")?), + Some(other) => return Err(format!("`status` must be active or pending, not `{other}`")), + }; + let list = v.get("quantities").and_then(Value::as_array).filter(|q| !q.is_empty()).ok_or("`quantities` must be a non-empty list")?; + let mut quantities = Vec::with_capacity(list.len()); + for q in list { + let id = text(q, "id")?; + let context = |e: String| format!("quantity `{id}`: {e}"); + let reference = match q.get("value") { + None | Some(Value::Null) if pending.is_some() => None, + Some(x) => Some(x.as_f64().filter(|x| x.is_finite()).ok_or_else(|| context("`value` must be a finite number".into()))?), + None => return Err(context("`value` is required".into())), + }; + let tolerance = match q.get("tolerance") { + None if pending.is_some() => Tolerance::default(), + Some(t) => Tolerance::from_json(t).map_err(context)?, + None => return Err(context("`tolerance` is required".into())), + }; + let reason = if pending.is_some() { q.get("reason").and_then(Value::as_str).unwrap_or_default().to_string() } else { text(q, "reason").map_err(context)? }; + let measure = q.get("measure").cloned().unwrap_or(Value::Null); + if pending.is_none() && measure.get("pointer").and_then(Value::as_str).is_none() { + return Err(context("`measure` needs a JSON `pointer` into a step's output or a command's".into())); + } + if quantities.iter().any(|x: &Quantity| x.id == id) { + return Err(format!("quantity `{id}` is listed twice")); + } + quantities.push(Quantity { + description: text(q, "quantity").map_err(context)?, + known_failure: q.get("known_failure").and_then(Value::as_str).map(str::to_string), + id, + measure, + reference, + tolerance, + reason, + }); + } + Ok(Expected { + title: text(v, "title")?, + kind, + source: text(v, "source")?, + pending, + slow: v.get("slow").and_then(Value::as_bool).unwrap_or(false), + quantities, + }) + } +} + +/// A frozen value and how far it may move. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Frozen { + pub value: f64, + pub band: f64, +} + +/// The band a fixture value gets when it is blessed. +pub fn fixture_band(value: f64, reference: f64, tolerance: Tolerance) -> f64 { + FIXTURE_RELATIVE * value.abs().max(reference.abs()) + 1e-3 * tolerance.absolute +} + +fn read_json(path: &Path) -> Result { + let size = std::fs::metadata(path).map_err(|e| format!("{}: {e}", path.display()))?.len(); + if size > MAX_FILE_BYTES { + return Err(format!("{}: larger than {MAX_FILE_BYTES} bytes", path.display())); + } + let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?; + serde_json::from_str(&text).map_err(|e| format!("{}: {e}", path.display())) +} + +fn read_fixture(path: &Path) -> Result>, String> { + if !path.is_file() { + return Ok(None); + } + let v = read_json(path)?; + let values = v.get("values").and_then(Value::as_object).ok_or_else(|| format!("{}: no `values`", path.display()))?; + values + .iter() + .map(|(k, f)| { + let value = f.get("value").and_then(Value::as_f64); + let band = f.get("band").and_then(Value::as_f64).filter(|b| *b >= 0.0); + match (value, band) { + (Some(value), Some(band)) => Ok((k.clone(), Frozen { value, band })), + _ => Err(format!("{}: `{k}` needs a `value` and a `band` ≥ 0", path.display())), + } + }) + .collect::>() + .map(Some) +} + +/// The result of measuring one quantity. +#[derive(Debug, Clone)] +pub struct Measured { + pub id: String, + pub value: Result, +} + +/// Run a case's recipe and measure its quantities. A failing command fails every quantity. +pub fn run_case(dir: &Path, expected: &Expected) -> Result, String> { + let mut session = Session::default(); + let design = dir.join("design.json"); + if design.is_file() { + let script = std::fs::read_to_string(&design).map_err(|e| format!("{}: {e}", design.display()))?; + let bytes = modelcraft_geometry::script_design(&script).map_err(|e| format!("design.json: {e}"))?; + let opened = modelcraft_geometry::open_design(&bytes).map_err(|e| format!("design.json: {e}"))?; + if let Some(e) = opened.errors.first() { + return Err(format!("design.json: feature {}: {}", e.feature, e.message)); + } + session.design = Some(Arc::new(opened)); + } + let recipe = read_json(&dir.join("recipe.json"))?; + let steps = recipe.as_array().ok_or("recipe.json must be a JSON array of steps")?; + let mut outputs: BTreeMap = BTreeMap::new(); + for (i, step) in steps.iter().enumerate() { + let command = step.get("command").and_then(Value::as_str).ok_or_else(|| format!("recipe step {}: no `command`", i + 1))?; + let params = step.get("params").cloned().unwrap_or_else(|| json!({})); + let out = session.execute(command, ¶ms).map_err(|e| format!("recipe step {} ({command}): {e}", i + 1))?; + if let Some(id) = step.get("id").and_then(Value::as_str) { + outputs.insert(id.to_string(), out); + } + } + Ok(expected + .quantities + .iter() + .map(|q| Measured { id: q.id.clone(), value: measure(&mut session, &outputs, &q.measure) }) + .collect()) +} + +/// Read one number: from a recipe step's output (`"from": id`) or a command run now +/// (`"command"`, `"params"`), at a JSON pointer. +fn measure(session: &mut Session, outputs: &BTreeMap, m: &Value) -> Result { + let pointer = m.get("pointer").and_then(Value::as_str).ok_or("no `pointer`")?; + let fresh; + let out = match (m.get("from").and_then(Value::as_str), m.get("command").and_then(Value::as_str)) { + (Some(id), None) => outputs.get(id).ok_or_else(|| format!("no recipe step has id `{id}`"))?, + (None, Some(command)) => { + let params = m.get("params").cloned().unwrap_or_else(|| json!({})); + fresh = session.execute(command, ¶ms).map_err(|e| format!("{command}: {e}"))?; + &fresh + } + _ => return Err("a measure gives either `from` (a recipe step id) or `command`".into()), + }; + let v = out.pointer(pointer).ok_or_else(|| format!("nothing at `{pointer}`"))?; + let x = v.as_f64().ok_or_else(|| format!("`{pointer}` is {v}, not a number"))?; + let x = x * m.get("scale").and_then(Value::as_f64).unwrap_or(1.0); + if x.is_finite() { Ok(x) } else { Err(format!("`{pointer}` is not finite")) } +} + +/// How one quantity came out. +#[derive(Debug, Clone, PartialEq)] +pub enum Verdict { + Pass, + Fail, + /// Fails, and expected.json says why. + Known, + /// Marked as a known failure but passes: remove the mark. + Fixed, + /// Could not be measured. + Error(String), + Pending, +} + +/// The fixture check of one quantity. +#[derive(Debug, Clone, PartialEq)] +pub enum FixtureCheck { + Matches, + Moved { frozen: f64 }, + Missing, + /// No fixture is kept (pending, failing or known-failing quantity). + NotKept, + Blessed, +} + +/// One row of the report. +#[derive(Debug, Clone)] +pub struct Row { + pub case: String, + pub quantity: Quantity, + pub ours: Option, + pub verdict: Verdict, + pub fixture: FixtureCheck, +} + +/// Judge a measured value against the reference and the fixture. +pub fn judge(q: &Quantity, ours: &Result, frozen: Option<&Frozen>) -> (Verdict, FixtureCheck) { + let (Some(reference), Ok(ours)) = (q.reference, ours) else { + return match ours { + Err(e) => (Verdict::Error(e.clone()), FixtureCheck::NotKept), + Ok(_) => (Verdict::Pending, FixtureCheck::NotKept), + }; + }; + let pass = q.tolerance.allows(reference, *ours); + let verdict = match (pass, &q.known_failure) { + (true, None) => Verdict::Pass, + (true, Some(_)) => Verdict::Fixed, + (false, None) => Verdict::Fail, + (false, Some(_)) => Verdict::Known, + }; + let fixture = match frozen { + Some(f) if (ours - f.value).abs() <= f.band => FixtureCheck::Matches, + Some(f) => FixtureCheck::Moved { frozen: f.value }, + None if verdict == Verdict::Pass => FixtureCheck::Missing, + None => FixtureCheck::NotKept, + }; + (verdict, fixture) +} + +/// The case directories under `tests/oracle`, sorted. +pub fn case_dirs(root: &Path) -> Result, String> { + let dir = root.join("tests").join("oracle"); + let mut cases: Vec = std::fs::read_dir(&dir) + .map_err(|e| format!("{}: {e}", dir.display()))? + .flatten() + .map(|e| e.path()) + .filter(|p| p.join("expected.json").is_file()) + .collect(); + cases.sort(); + Ok(cases) +} + +fn name_of(dir: &Path) -> String { + dir.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default() +} + +/// Run the oracle. Fails when a quantity fails its reference, a fixture moved or is missing, +/// a known failure now passes, or a case cannot run. +pub fn run(root: &Path, options: &Options) -> Result<(), String> { + let all = case_dirs(root)?; + for want in options.cases.iter().chain(options.bless.iter().filter(|b| *b != "all")) { + if !all.iter().any(|d| name_of(d) == *want) { + return Err(format!("no case `{want}` in tests/oracle")); + } + } + let selected: Vec<&PathBuf> = all.iter().filter(|d| options.cases.is_empty() || options.cases.contains(&name_of(d))).collect(); + let mut rows = Vec::new(); + let mut cases: Vec<(String, Expected)> = Vec::new(); + let mut problems = Vec::new(); + let mut skipped = Vec::new(); + for dir in selected { + let case = name_of(dir); + let expected = Expected::parse(&read_json(&dir.join("expected.json"))?).map_err(|e| format!("{case}/expected.json: {e}"))?; + if expected.slow && options.quick { + skipped.push(case); + continue; + } + let measured: Vec = if expected.pending.is_some() { + expected.quantities.iter().map(|q| Measured { id: q.id.clone(), value: Err("pending".into()) }).collect() + } else { + let start = Instant::now(); + let m = run_case(dir, &expected).unwrap_or_else(|e| expected.quantities.iter().map(|q| Measured { id: q.id.clone(), value: Err(e.clone()) }).collect()); + eprintln!("oracle: {case} ({:.2} s)", start.elapsed().as_secs_f64()); + m + }; + let fixture_path = dir.join("fixture.json"); + let fixture = read_fixture(&fixture_path)?; + let bless = options.bless.iter().any(|b| *b == case || b == "all"); + let mut case_rows = Vec::new(); + for (q, m) in expected.quantities.iter().zip(&measured) { + let (verdict, check) = if expected.pending.is_some() { + (Verdict::Pending, FixtureCheck::NotKept) + } else { + judge(q, &m.value, fixture.as_ref().and_then(|f| f.get(&q.id))) + }; + case_rows.push(Row { case: case.clone(), quantity: q.clone(), ours: m.value.as_ref().ok().copied(), verdict, fixture: check }); + } + if bless && expected.pending.is_none() { + let passing = case_rows.iter().all(|r| matches!(r.verdict, Verdict::Pass | Verdict::Known)); + if !passing { + problems.push(format!("{case}: not blessed, it does not pass against expected.json")); + } else { + let mut values = serde_json::Map::new(); + for r in case_rows.iter_mut().filter(|r| r.verdict == Verdict::Pass) { + if let (Some(ours), Some(reference)) = (r.ours, r.quantity.reference) { + values.insert(r.quantity.id.clone(), json!({"value": ours, "band": fixture_band(ours, reference, r.quantity.tolerance)})); + r.fixture = FixtureCheck::Blessed; + } + } + let text = serde_json::to_string_pretty(&json!({ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": values, + })) + .map_err(|e| e.to_string())?; + std::fs::write(&fixture_path, text + "\n").map_err(|e| format!("{}: {e}", fixture_path.display()))?; + } + } + for r in &case_rows { + let id = format!("{case}/{}", r.quantity.id); + match &r.verdict { + Verdict::Fail => problems.push(format!("{id}: {} vs reference {} ({})", show(r.ours), show(r.quantity.reference), r.quantity.tolerance.describe())), + Verdict::Fixed => problems.push(format!("{id}: passes now; remove its `known_failure` mark")), + Verdict::Error(e) => problems.push(format!("{id}: {e}")), + _ => {} + } + match &r.fixture { + FixtureCheck::Moved { frozen } => problems.push(format!("{id}: moved from the fixture {} to {}", number(*frozen), show(r.ours))), + FixtureCheck::Missing => problems.push(format!("{id}: no fixture; bless the case (`cargo xtask oracle --bless {case}`)")), + _ => {} + } + } + rows.extend(case_rows); + cases.push((case, expected)); + } + print_summary(&rows); + if options.write && options.cases.is_empty() && !options.quick { + let path = root.join("docs").join("oracle.md"); + std::fs::write(&path, report(&cases, &rows)).map_err(|e| format!("{}: {e}", path.display()))?; + eprintln!("oracle: wrote docs/oracle.md"); + } + if !skipped.is_empty() { + eprintln!("oracle: skipped slow cases: {}", skipped.join(", ")); + } + if problems.is_empty() { + Ok(()) + } else { + Err(format!("{} problem(s):\n {}", problems.len(), problems.join("\n "))) + } +} + +fn print_summary(rows: &[Row]) { + for r in rows { + if r.verdict == Verdict::Pending { + continue; + } + println!( + "{:<34} {:<22} ours {:<14} ref {:<14} err {:<12} {:<14} {}", + r.case, + r.quantity.id, + show(r.ours), + show(r.quantity.reference), + error_of(r), + verdict_text(&r.verdict), + fixture_text(&r.fixture) + ); + } +} + +/// A number with five significant digits. +pub fn number(x: f64) -> String { + if x == 0.0 { + return "0".into(); + } + let a = x.abs(); + if (1e-3..1e5).contains(&a) { + let digits = 4 - a.log10().floor() as i32; + let s = format!("{x:.*}", digits.max(0) as usize); + if s.contains('.') { s.trim_end_matches('0').trim_end_matches('.').to_string() } else { s } + } else { + format!("{x:.4e}") + } +} + +/// A fraction as a percentage with three significant digits. +fn percent(r: f64) -> String { + let p = r * 100.0; + if p == 0.0 { + return "0%".into(); + } + let digits = (2 - p.abs().log10().floor() as i32).max(0) as usize; + let s = format!("{p:.digits$}"); + let s = if s.contains('.') { s.trim_end_matches('0').trim_end_matches('.').to_string() } else { s }; + format!("{s}%") +} + +/// A tolerance's relative part: a percentage, or a plain fraction when it only admits rounding. +fn fraction(r: f64) -> String { + if r >= 1e-4 { percent(r) } else { format!("{r:.0e} rel.") } +} + +fn show(x: Option) -> String { + x.map(number).unwrap_or_else(|| "—".into()) +} + +/// The error against the reference: relative where the reference is not zero. +fn error_of(r: &Row) -> String { + let (Some(ours), Some(reference)) = (r.ours, r.quantity.reference) else { return "—".into() }; + let d = ours - reference; + if reference != 0.0 && r.quantity.tolerance.relative > 0.0 { + let rel = d / reference.abs(); + if rel.abs() < 1e-12 { "< 1e-10%".into() } else { percent(rel) } + } else if d.abs() < 1e-12 { + "< 1e-12".into() + } else { + format!("{d:.1e}") + } +} + +fn verdict_text(v: &Verdict) -> String { + match v { + Verdict::Pass => "pass".into(), + Verdict::Fail => "**FAIL**".into(), + Verdict::Known => "known failure".into(), + Verdict::Fixed => "**passes: remove known_failure**".into(), + Verdict::Error(_) => "**ERROR**".into(), + Verdict::Pending => "pending".into(), + } +} + +fn fixture_text(f: &FixtureCheck) -> &'static str { + match f { + FixtureCheck::Matches => "matches", + FixtureCheck::Moved { .. } => "**MOVED**", + FixtureCheck::Missing => "**missing**", + FixtureCheck::NotKept => "—", + FixtureCheck::Blessed => "blessed", + } +} + +fn cell(s: &str) -> String { + s.replace('|', "/").replace('\n', " ") +} + +/// docs/oracle.md. +pub fn report(cases: &[(String, Expected)], rows: &[Row]) -> String { + let active: Vec<&(String, Expected)> = cases.iter().filter(|(_, e)| e.pending.is_none()).collect(); + let count = |kind: &str| active.iter().filter(|(_, e)| e.kind == kind).count(); + let quantities = rows.iter().filter(|r| r.verdict != Verdict::Pending).count(); + let passing = rows.iter().filter(|r| r.verdict == Verdict::Pass).count(); + let known = rows.iter().filter(|r| r.verdict == Verdict::Known).count(); + let mut md = String::new(); + let _ = write!( + md, + "# Verification and validation oracle\n\n\ +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 ModelCraft 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.\n\n\ +**{passing} of {quantities} quantities pass** ({known} known failure(s)) in {} active cases: {} theory, {} published \ +benchmark, {} experiment; {} pending cases wait for capabilities still being built.\n\n", + active.len(), + count("theory"), + count("benchmark"), + count("experiment"), + cases.len() - active.len(), + ); + for kind in KINDS { + let in_kind: Vec<&&(String, Expected)> = active.iter().filter(|(_, e)| e.kind == kind).collect(); + if in_kind.is_empty() { + continue; + } + let heading = match kind { + "theory" => "Theory: closed-form solutions, patch tests and mesh invariants", + "benchmark" => "Published benchmarks", + _ => "Experiments", + }; + let _ = write!(md, "## {heading}\n\n| Case | Quantity | Reference | ModelCraft | Error | Tolerance | Result | Fixture |\n|---|---|---|---|---|---|---|---|\n"); + for (case, _) in &in_kind { + for r in rows.iter().filter(|r| &r.case == case) { + let mut result = verdict_text(&r.verdict); + if let Some(k) = &r.quantity.known_failure { + result = format!("{result}: {k}"); + } + if let Verdict::Error(e) = &r.verdict { + result = format!("{result}: {e}"); + } + let _ = writeln!( + md, + "| [{case}](../tests/oracle/{case}/) | {} | {} | {} | {} | {} | {} | {} |", + cell(&r.quantity.description), + show(r.quantity.reference), + show(r.ours), + error_of(r), + r.quantity.tolerance.describe(), + cell(&result), + fixture_text(&r.fixture), + ); + } + } + md.push('\n'); + } + md += "## Sources and tolerances\n\n"; + for (case, e) in &active { + let _ = write!(md, "- **[{case}](../tests/oracle/{case}/)** ({}): {}. Source: {}.", e.kind, cell(&e.title), cell(&e.source)); + let reasons: Vec = e.quantities.iter().map(|q| format!("{}: {}", q.id, cell(&q.reason))).collect(); + let _ = writeln!(md, " Tolerances — {}", reasons.join("; ")); + } + let pending: Vec<&(String, Expected)> = cases.iter().filter(|(_, e)| e.pending.is_some()).collect(); + if !pending.is_empty() { + md += "\n## Pending cases\n\nReference values already cited; each case gets its recipe when the capability it needs lands.\n\n| Case | Kind | Problem | Needs | Reference values | Source |\n|---|---|---|---|---|---|\n"; + for (case, e) in pending { + let values: Vec = + e.quantities.iter().map(|q| format!("{} = {}", cell(&q.description), q.reference.map(number).unwrap_or_else(|| "to be transcribed".into()))).collect(); + let _ = writeln!( + md, + "| [{case}](../tests/oracle/{case}/) | {} | {} | {} | {} | {} |", + e.kind, + cell(&e.title), + cell(e.pending.as_deref().unwrap_or_default()), + values.join("; "), + cell(&e.source) + ); + } + } + md +} + +#[cfg(test)] +mod tests { + use super::*; + + fn quantity(reference: f64, tolerance: Tolerance, known: Option<&str>) -> Quantity { + Quantity { + id: "q".into(), + description: "q".into(), + measure: json!({"from": "s", "pointer": "/x"}), + reference: Some(reference), + tolerance, + reason: "r".into(), + known_failure: known.map(str::to_string), + } + } + + #[test] + fn verdicts_and_fixtures() { + let t = Tolerance { relative: 0.01, absolute: 0.0 }; + let q = quantity(2.0, t, None); + let frozen = Frozen { value: 2.01, band: fixture_band(2.01, 2.0, t) }; + assert_eq!(judge(&q, &Ok(2.01), Some(&frozen)), (Verdict::Pass, FixtureCheck::Matches)); + assert_eq!(judge(&q, &Ok(2.011), Some(&frozen)), (Verdict::Pass, FixtureCheck::Moved { frozen: 2.01 })); + assert_eq!(judge(&q, &Ok(2.01), None), (Verdict::Pass, FixtureCheck::Missing)); + assert_eq!(judge(&q, &Ok(2.1), None).0, Verdict::Fail); + assert_eq!(judge(&quantity(2.0, t, Some("why")), &Ok(2.1), None), (Verdict::Known, FixtureCheck::NotKept)); + assert_eq!(judge(&quantity(2.0, t, Some("why")), &Ok(2.0), None).0, Verdict::Fixed); + assert!(matches!(judge(&q, &Err("boom".into()), None).0, Verdict::Error(_))); + // Zero references use the absolute tolerance, and the band keeps a floor from it. + let a = Tolerance { relative: 0.0, absolute: 1e-9 }; + assert!(a.allows(0.0, 5e-10) && !a.allows(0.0, 2e-9)); + assert!(fixture_band(1e-16, 0.0, a) >= 1e-12); + } + + #[test] + fn expected_files_are_checked() { + let good = json!({"title": "t", "kind": "theory", "source": "s", "quantities": [ + {"id": "a", "quantity": "a", "measure": {"from": "x", "pointer": "/a"}, "value": 1.0, "tolerance": {"relative": 0.1}, "reason": "r"}]}); + assert!(Expected::parse(&good).is_ok()); + let pending = json!({"title": "t", "kind": "benchmark", "source": "s", "status": "pending", "requires": "shells", + "quantities": [{"id": "a", "quantity": "a", "value": null}]}); + assert!(Expected::parse(&pending).unwrap().pending.is_some()); + for (key, bad) in [ + ("kind", json!("guess")), + ("quantities", json!([])), + ("status", json!("maybe")), + ("title", json!("")), + ] { + let mut v = good.clone(); + v[key] = bad; + assert!(Expected::parse(&v).is_err(), "{key}"); + } + for (key, bad) in [("tolerance", json!({})), ("tolerance", json!({"relative": -1})), ("value", json!("x")), ("reason", json!("")), ("measure", json!({}))] { + let mut v = good.clone(); + v["quantities"][0][key] = bad; + assert!(Expected::parse(&v).is_err(), "{key}"); + } + } + + #[test] + fn numbers_print_with_five_digits() { + assert_eq!(number(92.7), "92.7"); + assert_eq!(number(-4.0241234), "-4.0241"); + assert_eq!(number(0.30241), "0.30241"); + assert_eq!(number(1.8248e-5), "1.8248e-5"); + assert_eq!(number(210000.0), "2.1000e5"); + assert_eq!(number(0.0), "0"); + assert_eq!(percent(-0.0050317), "-0.503%"); + assert_eq!(percent(0.000132), "0.0132%"); + assert_eq!(percent(0.02), "2%"); + assert_eq!(fraction(1e-9), "1e-9 rel."); + } +} diff --git a/docs/oracle.md b/docs/oracle.md new file mode 100644 index 0000000..54e87f9 --- /dev/null +++ b/docs/oracle.md @@ -0,0 +1,169 @@ +# Verification and validation oracle + +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 ModelCraft 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. + +**100 of 106 quantities pass** (6 known failure(s)) in 28 active cases: 20 theory, 8 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built. + +## Theory: closed-form solutions, patch tests and mesh invariants + +| Case | Quantity | Reference | ModelCraft | Error | Tolerance | Result | Fixture | +|---|---|---|---|---|---|---|---| +| [bar-pressure-wave](../tests/oracle/bar-pressure-wave/) | displacement of the loaded end at t = 0.06 | -6.0000e-4 | -5.9981e-4 | 0.0315% | 1% | pass | matches | +| [bar-pressure-wave](../tests/oracle/bar-pressure-wave/) | particle velocity at x = 5, 20 elements behind the front, at t = 0.06 | -0.01 | -0.0098554 | 1.45% | 2% | pass | matches | +| [bar-pressure-wave](../tests/oracle/bar-pressure-wave/) | energy balance error | 0 | 0.0015271 | 1.5e-3 | ± 0.01 | pass | matches | +| [bar-rigid-wall-impact](../tests/oracle/bar-rigid-wall-impact/) | displacement of the struck end at t = 0.3 (in contact until 2L/c = 0.2, then leaving at v₀) | 0.01 | 0.0099497 | -0.503% | 3% | pass | matches | +| [bar-rigid-wall-impact](../tests/oracle/bar-rigid-wall-impact/) | displacement of the far end at t = 0.3 (−v₀L/c, then +v₀ after the reflection) | 0.01 | 0.0099463 | -0.537% | 3% | pass | matches | +| [bar-rigid-wall-impact](../tests/oracle/bar-rigid-wall-impact/) | kinetic energy after the rebound over the initial ½mv₀² | 1 | 0.99469 | -0.531% | 2% | pass | matches | +| [cantilever-frequencies](../tests/oracle/cantilever-frequencies/) | first bending frequency (modes 1 and 2 are the y and z pair) | 0.012771 | 0.012751 | -0.158% | 1% | pass | matches | +| [cantilever-frequencies](../tests/oracle/cantilever-frequencies/) | the other first bending frequency | 0.012771 | 0.012751 | -0.158% | 1% | pass | matches | +| [cantilever-frequencies](../tests/oracle/cantilever-frequencies/) | second bending frequency | 0.080034 | 0.079167 | -1.08% | 2% | pass | matches | +| [cantilever-hex20](../tests/oracle/cantilever-hex20/) | tip deflection u_z at (10, 0.5, 0.5) | -4.024 | -4.0235 | 0.0131% | 0.5% | pass | matches | +| [cantilever-hex20](../tests/oracle/cantilever-hex20/) | total reaction z | 1 | 1 | 0.00000000402% | 1e-9 rel. | pass | matches | +| [cantilever-hex8-incompatible](../tests/oracle/cantilever-hex8-incompatible/) | tip deflection u_z at (10, 0.5, 0.5) | -4.024 | -4.0175 | 0.162% | 0.5% | pass | matches | +| [cantilever-hex8-incompatible](../tests/oracle/cantilever-hex8-incompatible/) | total reaction z | 1 | 1 | -0.00000000116% | 1e-9 rel. | pass | matches | +| [cantilever-tet10](../tests/oracle/cantilever-tet10/) | tip deflection u_z at (10, 0.5, 0.5) | -4.024 | -4.0233 | 0.018% | 0.5% | pass | matches | +| [cantilever-tet10](../tests/oracle/cantilever-tet10/) | total reaction z | 1 | 1 | 0.00000000174% | 1e-9 rel. | pass | matches | +| [cdt-cylinder](../tests/oracle/cdt-cylinder/) | volume of the tetrahedra | 6267.5 | 6266.5 | -0.0152% | 0.25% | pass | matches | +| [cdt-cylinder](../tests/oracle/cdt-cylinder/) | elements that fail the certified Jacobian check | 0 | 0 | < 1e-12 | exact | pass | matches | +| [cdt-stepped-block](../tests/oracle/cdt-stepped-block/) | volume of the tetrahedra | 47000 | 47000 | < 1e-10% | 1e-12 rel. | pass | matches | +| [cdt-stepped-block](../tests/oracle/cdt-stepped-block/) | elements that fail the certified Jacobian check | 0 | 0 | < 1e-12 | exact | pass | matches | +| [cdt-stepped-block](../tests/oracle/cdt-stepped-block/) | elements the Jacobian check could not decide | 0 | 0 | < 1e-12 | exact | pass | matches | +| [free-beam-frequencies](../tests/oracle/free-beam-frequencies/) | sixth frequency (the last rigid-body mode) | 0 | 1.5486e-7 | 1.5e-7 | ± 8.1264e-5 | pass | matches | +| [free-beam-frequencies](../tests/oracle/free-beam-frequencies/) | first free–free bending frequency (mode 7) | 0.081264 | 0.080618 | -0.795% | 1.5% | pass | matches | +| [free-beam-frequencies](../tests/oracle/free-beam-frequencies/) | total mass | 20 | 20 | < 1e-10% | 1e-12 rel. | pass | matches | +| [kirsch-plate](../tests/oracle/kirsch-plate/) | σ_xx at the top of the hole (0, 1, 0) over the remote stress | 3 | 3.0121 | 0.402% | 2% | pass | matches | +| [kirsch-plate](../tests/oracle/kirsch-plate/) | σ_yy at the side of the hole (1, 0, 0) over the remote stress | -1 | -1.011 | -1.1% | 3% | pass | matches | +| [lame-cylinder](../tests/oracle/lame-cylinder/) | radial displacement at the bore (10, 0, 0) | 0.0090794 | 0.0090793 | -0.000267% | 0.1% | pass | matches | +| [lame-cylinder](../tests/oracle/lame-cylinder/) | radial displacement outside (20, 0, 0) | 0.0057778 | 0.0057778 | -0.000301% | 0.1% | pass | matches | +| [lame-cylinder](../tests/oracle/lame-cylinder/) | hoop stress σ_θ = σ_yy at the bore (10, 0, 0) | 166.67 | 166.36 | -0.183% | 1% | pass | matches | +| [patch-hex20](../tests/oracle/patch-hex20/) | u_x at the corner (1, 1, 1) | -2.5000e-4 | -2.5000e-4 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20](../tests/oracle/patch-hex20/) | u_y at the corner (1, 1, 1) | 0.001 | 0.001 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20](../tests/oracle/patch-hex20/) | u_z at the corner (1, 1, 1) | 0.00225 | 0.00225 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20](../tests/oracle/patch-hex20/) | σ_xx at the inner node (0.46, 0.55, 0.53) | 1 | 1 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20](../tests/oracle/patch-hex20/) | σ_yy at the inner node (0.46, 0.55, 0.53) | 2 | 2 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20](../tests/oracle/patch-hex20/) | σ_zz at the inner node (0.46, 0.55, 0.53) | 3 | 3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20](../tests/oracle/patch-hex20/) | σ_xy at the inner node | 0 | 5.8265e-16 | < 1e-12 | ± 1.0000e-9 | pass | matches | +| [patch-hex20-reduced](../tests/oracle/patch-hex20-reduced/) | u_x at the corner (1, 1, 1) | -2.5000e-4 | -2.5000e-4 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20-reduced](../tests/oracle/patch-hex20-reduced/) | u_y at the corner (1, 1, 1) | 0.001 | 1.0000e-3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20-reduced](../tests/oracle/patch-hex20-reduced/) | u_z at the corner (1, 1, 1) | 0.00225 | 0.00225 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20-reduced](../tests/oracle/patch-hex20-reduced/) | σ_xx at the inner node (0.46, 0.55, 0.53) | 1 | 1 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20-reduced](../tests/oracle/patch-hex20-reduced/) | σ_yy at the inner node (0.46, 0.55, 0.53) | 2 | 2 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20-reduced](../tests/oracle/patch-hex20-reduced/) | σ_zz at the inner node (0.46, 0.55, 0.53) | 3 | 3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex20-reduced](../tests/oracle/patch-hex20-reduced/) | σ_xy at the inner node | 0 | -3.7386e-16 | < 1e-12 | ± 1.0000e-9 | pass | matches | +| [patch-hex8](../tests/oracle/patch-hex8/) | u_x at the corner (1, 1, 1) | -2.5000e-4 | -2.5000e-4 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8](../tests/oracle/patch-hex8/) | u_y at the corner (1, 1, 1) | 0.001 | 1.0000e-3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8](../tests/oracle/patch-hex8/) | u_z at the corner (1, 1, 1) | 0.00225 | 0.00225 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8](../tests/oracle/patch-hex8/) | σ_xx at the inner node (0.46, 0.55, 0.53) | 1 | 1 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8](../tests/oracle/patch-hex8/) | σ_yy at the inner node (0.46, 0.55, 0.53) | 2 | 2 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8](../tests/oracle/patch-hex8/) | σ_zz at the inner node (0.46, 0.55, 0.53) | 3 | 3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8](../tests/oracle/patch-hex8/) | σ_xy at the inner node | 0 | -1.5987e-16 | < 1e-12 | ± 1.0000e-9 | pass | matches | +| [patch-hex8-incompatible](../tests/oracle/patch-hex8-incompatible/) | u_x at the corner (1, 1, 1) | -2.5000e-4 | -2.5000e-4 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8-incompatible](../tests/oracle/patch-hex8-incompatible/) | u_y at the corner (1, 1, 1) | 0.001 | 0.001 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8-incompatible](../tests/oracle/patch-hex8-incompatible/) | u_z at the corner (1, 1, 1) | 0.00225 | 0.00225 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8-incompatible](../tests/oracle/patch-hex8-incompatible/) | σ_xx at the inner node (0.46, 0.55, 0.53) | 1 | 1 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8-incompatible](../tests/oracle/patch-hex8-incompatible/) | σ_yy at the inner node (0.46, 0.55, 0.53) | 2 | 2 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8-incompatible](../tests/oracle/patch-hex8-incompatible/) | σ_zz at the inner node (0.46, 0.55, 0.53) | 3 | 3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-hex8-incompatible](../tests/oracle/patch-hex8-incompatible/) | σ_xy at the inner node | 0 | -2.1491e-16 | < 1e-12 | ± 1.0000e-9 | pass | matches | +| [patch-tet10](../tests/oracle/patch-tet10/) | u_x at the corner (1, 1, 1) | -2.5000e-4 | -2.5000e-4 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet10](../tests/oracle/patch-tet10/) | u_y at the corner (1, 1, 1) | 0.001 | 1.0000e-3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet10](../tests/oracle/patch-tet10/) | u_z at the corner (1, 1, 1) | 0.00225 | 0.00225 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet10](../tests/oracle/patch-tet10/) | σ_xx at the inner node (0.46, 0.55, 0.53) | 1 | 1 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet10](../tests/oracle/patch-tet10/) | σ_yy at the inner node (0.46, 0.55, 0.53) | 2 | 2 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet10](../tests/oracle/patch-tet10/) | σ_zz at the inner node (0.46, 0.55, 0.53) | 3 | 3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet10](../tests/oracle/patch-tet10/) | σ_xy at the inner node | 0 | 2.7299e-15 | < 1e-12 | ± 1.0000e-9 | pass | matches | +| [patch-tet4](../tests/oracle/patch-tet4/) | u_x at the corner (1, 1, 1) | -2.5000e-4 | -2.5000e-4 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet4](../tests/oracle/patch-tet4/) | u_y at the corner (1, 1, 1) | 0.001 | 1.0000e-3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet4](../tests/oracle/patch-tet4/) | u_z at the corner (1, 1, 1) | 0.00225 | 0.00225 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet4](../tests/oracle/patch-tet4/) | σ_xx at the inner node (0.46, 0.55, 0.53) | 1 | 1 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet4](../tests/oracle/patch-tet4/) | σ_yy at the inner node (0.46, 0.55, 0.53) | 2 | 2 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet4](../tests/oracle/patch-tet4/) | σ_zz at the inner node (0.46, 0.55, 0.53) | 3 | 3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-tet4](../tests/oracle/patch-tet4/) | σ_xy at the inner node | 0 | 2.3336e-17 | < 1e-12 | ± 1.0000e-9 | pass | matches | +| [patch-wedge6](../tests/oracle/patch-wedge6/) | u_x at the corner (1, 1, 1) | -2.5000e-4 | -2.5000e-4 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-wedge6](../tests/oracle/patch-wedge6/) | u_y at the corner (1, 1, 1) | 0.001 | 0.001 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-wedge6](../tests/oracle/patch-wedge6/) | u_z at the corner (1, 1, 1) | 0.00225 | 0.00225 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-wedge6](../tests/oracle/patch-wedge6/) | σ_xx at the inner node (0.46, 0.55, 0.53) | 1 | 1 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-wedge6](../tests/oracle/patch-wedge6/) | σ_yy at the inner node (0.46, 0.55, 0.53) | 2 | 2 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-wedge6](../tests/oracle/patch-wedge6/) | σ_zz at the inner node (0.46, 0.55, 0.53) | 3 | 3 | < 1e-10% | 1e-9 rel. | pass | matches | +| [patch-wedge6](../tests/oracle/patch-wedge6/) | σ_xy at the inner node | 0 | -8.3791e-17 | < 1e-12 | ± 1.0000e-9 | pass | matches | +| [pure-bending-coupling](../tests/oracle/pure-bending-coupling/) | bending stress σ_xx at mid-span, top fibre (10, 0.5, 1) | 15 | 15 | -0.0000103% | 0.5% | pass | matches | +| [pure-bending-coupling](../tests/oracle/pure-bending-coupling/) | rotation of the reference point about y | 0.3 | 0.30355 | 1.18% | 1% | known failure: 1.2% high: the distributing coupling weights every node of the end face equally, corner and mid-side nodes of hex20 alike, which is not the consistent linear traction; the end face deforms locally and the fitted rotation includes it. Area-consistent weights would remove it | — | +| [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 | + +## Published benchmarks + +| Case | Quantity | Reference | ModelCraft | Error | Tolerance | Result | Fixture | +|---|---|---|---|---|---|---|---| +| [mh-curved-cantilever](../tests/oracle/mh-curved-cantilever/) | tip displacement along the in-plane load | 0.08734 | 0.076833 | -12% | 2% | known failure: 0.880 of the theory, grade C: straight-sided hex8 on six elements over 90° (MacNeal–Harder's HEXA(8): 0.880, Table 9 p. 15) | — | +| [mh-curved-cantilever](../tests/oracle/mh-curved-cantilever/) | tip displacement along the out-of-plane load | 0.5022 | 0.40095 | -20.2% | 2% | known failure: 0.798 of the theory, grade D: out-of-plane bending with torsion on one element across (MacNeal–Harder's HEXA(8): 0.849) | — | +| [mh-solid-patch](../tests/oracle/mh-solid-patch/) | σ_xx at inner node 7 (0.788, 0.693, 0.644) | 2000 | 2000 | < 1e-10% | 1e-9 rel. | pass | matches | +| [mh-solid-patch](../tests/oracle/mh-solid-patch/) | σ_yy at inner node 7 (0.788, 0.693, 0.644) | 2000 | 2000 | < 1e-10% | 1e-9 rel. | pass | matches | +| [mh-solid-patch](../tests/oracle/mh-solid-patch/) | σ_zz at inner node 7 (0.788, 0.693, 0.644) | 2000 | 2000 | < 1e-10% | 1e-9 rel. | pass | matches | +| [mh-solid-patch](../tests/oracle/mh-solid-patch/) | τ_xy at inner node 7 | 400 | 400 | < 1e-10% | 1e-9 rel. | pass | matches | +| [mh-solid-patch](../tests/oracle/mh-solid-patch/) | τ_xz at inner node 7 | 400 | 400 | < 1e-10% | 1e-9 rel. | pass | matches | +| [mh-solid-patch](../tests/oracle/mh-solid-patch/) | τ_yz at inner node 7 | 400 | 400 | < 1e-10% | 1e-9 rel. | pass | matches | +| [mh-solid-patch](../tests/oracle/mh-solid-patch/) | u_x at inner node 7 | 0.0014565 | 0.0014565 | < 1e-10% | 1e-9 rel. | pass | matches | +| [mh-straight-cantilever](../tests/oracle/mh-straight-cantilever/) | tip displacement along the load, extension | 3.0000e-5 | 2.9629e-5 | -1.24% | 2% | pass | matches | +| [mh-straight-cantilever](../tests/oracle/mh-straight-cantilever/) | tip displacement along the load, in-plane shear (width 0.2) | 0.1081 | 0.10574 | -2.18% | 2% | known failure: 0.978 of the theory, grade B: one hex8 with incompatible modes through the depth of the prescribed mesh (MacNeal–Harder's HEXA(8): 0.981, Table 8(a) p. 15); refined meshes converge (cantilever-hex8-incompatible) | — | +| [mh-straight-cantilever](../tests/oracle/mh-straight-cantilever/) | tip displacement along the load, out-of-plane shear (depth 0.1) | 0.4321 | 0.42037 | -2.71% | 2% | known failure: 0.973 of the theory, grade B, as for in-plane shear (MacNeal–Harder's HEXA(8): 0.981) | — | +| [mh-straight-cantilever](../tests/oracle/mh-straight-cantilever/) | tip rotation under a unit twisting moment | 0.03208 | 0.028612 | -10.8% | 2% | known failure: 0.892 of the theory, grade C: one element across the section cannot represent the torsion warping (MacNeal–Harder's HEXA(8): 0.910, HEX20: 0.904) | — | +| [mh-thick-cylinder](../tests/oracle/mh-thick-cylinder/) | radial displacement at the inner radius, ν = 0.49 | 0.0050399 | 0.0049705 | -1.38% | 2% | pass | matches | +| [mh-thick-cylinder](../tests/oracle/mh-thick-cylinder/) | radial displacement at the inner radius, ν = 0.499 | 0.0050602 | 0.0049892 | -1.4% | 2% | pass | matches | +| [mh-thick-cylinder](../tests/oracle/mh-thick-cylinder/) | radial displacement at the inner radius, ν = 0.4999 | 0.0050623 | 0.0049911 | -1.41% | 2% | pass | matches | +| [nafems-fv32](../tests/oracle/nafems-fv32/) | frequency of mode 1 (Hz) | 44.623 | 44.623 | -0.000219% | 1% | pass | matches | +| [nafems-fv32](../tests/oracle/nafems-fv32/) | frequency of mode 2 (Hz) | 130.03 | 130.03 | 0.00356% | 1% | pass | matches | +| [nafems-fv32](../tests/oracle/nafems-fv32/) | frequency of mode 3 (Hz) | 162.7 | 162.7 | -0.00108% | 1% | pass | matches | +| [nafems-fv32](../tests/oracle/nafems-fv32/) | frequency of mode 4 (Hz) | 246.05 | 246.05 | 0.00159% | 1% | pass | matches | +| [nafems-fv32](../tests/oracle/nafems-fv32/) | frequency of mode 5 (Hz) | 379.9 | 379.9 | 0.000182% | 1% | pass | matches | +| [nafems-fv32](../tests/oracle/nafems-fv32/) | frequency of mode 6 (Hz) | 391.44 | 391.44 | -0.0012% | 1% | pass | matches | +| [nafems-le1](../tests/oracle/nafems-le1/) | σ_yy at D (2, 0) | 92.7 | 92.869 | 0.182% | 1% | pass | matches | +| [nafems-le10](../tests/oracle/nafems-le10/) | σ_yy at D (2, 0, 0.3) on the loaded face | -5.38 | -5.3339 | 0.857% | 2% | pass | matches | +| [scordelis-lo-roof-solid](../tests/oracle/scordelis-lo-roof-solid/) | vertical displacement at the midpoint of the free edge, mid-surface | -0.3024 | -0.3014 | 0.33% | 2% | pass | matches | + +## Sources and tolerances + +- **[bar-pressure-wave](../tests/oracle/bar-pressure-wave/)** (theory): Elastic wave in a bar from a pressure step (explicit). Source: d'Alembert's solution of the one-dimensional wave equation: c = √(E/ρ), particle velocity v = p/(ρc); Graff, Wave Motion in Elastic Solids, Dover 1991, §2.1. Tolerances — u_end: the end moves at the particle velocity p/(ρc) from the start; the discrete wave's ringing behind the front changes the end's displacement by well under 1%; v_mid: central differences ring behind a step front (dispersion); bulk viscosity damps it within a few elements, leaving about 1% twenty elements behind; balance: external work = kinetic + internal + dissipated energy; the solver warns above 1% +- **[bar-rigid-wall-impact](../tests/oracle/bar-rigid-wall-impact/)** (theory): Elastic bar striking a rigid wall (explicit). Source: One-dimensional elastic impact on a rigid wall: contact time 2L/c, rebound at the impact speed, contact stress ρcv₀; Graff, Wave Motion in Elastic Solids, Dover 1991, §2.3; Johnson, Impact Strength of Materials, Arnold 1972, §1.6. Tolerances — u_front: the contact ends at 2L/c in the elastic theory; the discrete release front is spread over a few elements, a few tenths of a percent of the time, and the rebound speed is v₀ to about 1%; u_back: as for the struck end; ke: an elastic impact keeps the kinetic energy; quadratic bulk viscosity in the compression front and the vibration left in the bar take about 1% +- **[cantilever-frequencies](../tests/oracle/cantilever-frequencies/)** (theory): Natural frequencies of a cantilever (Euler–Bernoulli). Source: Euler–Bernoulli beam: f_n = (β_n L)²/(2π L²)·√(EI/ρA), β₁L = 1.8751, β₂L = 4.6941; Timoshenko, Young and Weaver, Vibration Problems in Engineering, 4th ed., Wiley 1974, §5.3. Tolerances — f1: Euler–Bernoulli neglects shear and rotary inertia, which lower f1 by about 0.2% at L/h = 20; f1_pair: as f1: a square section bends alike in y and z; f2: shear and rotary inertia lower the second mode by about 1% at L/h = 20 (Timoshenko) +- **[cantilever-hex20](../tests/oracle/cantilever-hex20/)** (theory): Cantilever under a tip load, Timoshenko deflection (hex20). Source: Timoshenko and Gere, Mechanics of Materials (beam with shear deformation: δ = PL³/3EI + PL/κGA, κ = 5/6 for a rectangle); Cowper, J. Appl. Mech. 33 (1966) 335–340 for the shear coefficient. Tolerances — tip_uz: beam theory vs 3D elasticity: the shear coefficient and the fully clamped end change the tip deflection by a few tenths of a percent at L/h = 10; reaction: equilibrium holds to rounding +- **[cantilever-hex8-incompatible](../tests/oracle/cantilever-hex8-incompatible/)** (theory): Cantilever under a tip load, Timoshenko deflection (hex8-incompatible). Source: Timoshenko and Gere, Mechanics of Materials (beam with shear deformation: δ = PL³/3EI + PL/κGA, κ = 5/6 for a rectangle); Cowper, J. Appl. Mech. 33 (1966) 335–340 for the shear coefficient. Tolerances — tip_uz: beam theory vs 3D elasticity (a few tenths of a percent at L/h = 10); incompatible modes represent the bending exactly in the constant-moment part; reaction: equilibrium holds to rounding +- **[cantilever-tet10](../tests/oracle/cantilever-tet10/)** (theory): Cantilever under a tip load, Timoshenko deflection (tet10). Source: Timoshenko and Gere, Mechanics of Materials (beam with shear deformation: δ = PL³/3EI + PL/κGA, κ = 5/6 for a rectangle); Cowper, J. Appl. Mech. 33 (1966) 335–340 for the shear coefficient. Tolerances — tip_uz: beam theory vs 3D elasticity (a few tenths of a percent at L/h = 10); quadratic tetrahedra are converged on this mesh; reaction: equilibrium holds to rounding +- **[cdt-cylinder](../tests/oracle/cdt-cylinder/)** (theory): Constrained Delaunay mesh of a cylinder: volume within the chordal bound, valid elements. Source: mesh invariants: an inscribed polygon of sagitta s loses at most 2s/R of a circle's area (elementary geometry); constrained Delaunay tetrahedralization after Diazzi et al., ACM TOG 42(6) 2023. Tolerances — volume: the tessellation inscribes the cylinder within the chordal tolerance s = 0.05: the polygon section loses between 0 and 2s/R = 0.5% of the area; the reference is the middle of that band; invalid: every element of a valid mesh has positive volume +- **[cdt-stepped-block](../tests/oracle/cdt-stepped-block/)** (theory): Constrained Delaunay mesh of a stepped block: exact volume, valid elements. Source: mesh invariants: a tetrahedralization of a polyhedron fills its volume exactly (Si, TetGen paper ACM TOMS 41(2) 2015 for the definition of a CDT; Diazzi et al., ACM TOG 42(6) 2023); the volume is 60·40·10 + 40·40·10 + 20·40·10 − 10·10·10. Tolerances — volume: a planar boundary is tessellated exactly and the constrained Delaunay mesh fills it exactly: only rounding; invalid: every element of a valid mesh has positive volume; undetermined: a linear tetrahedron's sign is exact +- **[free-beam-frequencies](../tests/oracle/free-beam-frequencies/)** (theory): Natural frequencies of a free beam: rigid-body modes and the first bending mode. Source: Euler–Bernoulli free–free beam, β₁L = 4.7300; Timoshenko, Young and Weaver, Vibration Problems in Engineering, 4th ed., Wiley 1974, §5.3. Tolerances — rigid_6: a free body has six zero frequencies; numerical rigid-body frequencies below 10⁻³ of the first elastic one (eigenvalues below 10⁻⁶ of it) are rounding of the shifted solve; f7: Euler–Bernoulli neglects shear and rotary inertia, about 0.8% at L/h = 20 for the free–free mode; mass: the mass matrix integrates the density exactly +- **[kirsch-plate](../tests/oracle/kirsch-plate/)** (theory): Plate with a circular hole in tension (Kirsch). Source: Kirsch, Zeitschrift des VDI 42 (1898) 797–807; Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §35. Tolerances — kt: the reference is the infinite plate; the finite plate (W = 60a, Heywood's width correction about +0.1%, and the loaded ends 30a away) raises the peak by a few tenths of a percent, and the stress extrapolated to the hole surface adds up to about 1%; side: infinite-plate value −S; finite width and extrapolation as for Kt, on a smaller value +- **[lame-cylinder](../tests/oracle/lame-cylinder/)** (theory): Thick-walled cylinder under internal pressure, plane strain (Lamé). Source: Lamé's solution, Timoshenko and Goodier, Theory of Elasticity, 3rd ed., McGraw-Hill 1970, §28 (thick-walled cylinder). Tolerances — ur_a: quadratic elements on 8 radial divisions resolve u_r = A r + B/r to well under 0.1%; ur_b: as for the bore; hoop_a: stress extrapolated from the integration points to the bore surface and averaged over two elements: about 0.5% at the steepest gradient +- **[mh-curved-cantilever](../tests/oracle/mh-curved-cantilever/)** (benchmark): MacNeal–Harder curved cantilever, hex8 with incompatible modes. 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. 5 (p. 7) geometry, material and mesh; Table 3 (p. 10) theoretical tip displacements. Tolerances — in_plane: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14); out_of_plane: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14) +- **[mh-solid-patch](../tests/oracle/mh-solid-patch/)** (benchmark): MacNeal–Harder patch test for solids (hex8 with incompatible modes). 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. 3 (p. 6) geometry and inner node locations, Table 2(c) (p. 10) boundary displacements and theoretical solution. Tolerances — sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; txy: a patch test is passed exactly: the tolerance only admits rounding; txz: a patch test is passed exactly: the tolerance only admits rounding; tyz: a patch test is passed exactly: the tolerance only admits rounding; u7: a patch test is passed exactly: the tolerance only admits rounding +- **[mh-straight-cantilever](../tests/oracle/mh-straight-cantilever/)** (benchmark): MacNeal–Harder straight cantilever, regular hex8 mesh with incompatible modes. 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. 4(a) (p. 7) geometry, material and mesh; Table 3 (p. 10) theoretical tip displacements. Tolerances — extension: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14); in_plane: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14); out_of_plane: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14); twist: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14); the moment reaches the end through a distributing coupling +- **[mh-thick-cylinder](../tests/oracle/mh-thick-cylinder/)** (benchmark): MacNeal–Harder thick-walled cylinder, nearly incompressible (hex8 with incompatible modes). 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. 10 (p. 9) geometry, material and radial mesh; Table 5(c) (p. 11) radial displacements at the inner radius. Tolerances — ur_1: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14); ur_2: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14); ur_3: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14) +- **[nafems-fv32](../tests/oracle/nafems-fv32/)** (benchmark): NAFEMS FV32 cantilevered tapered membrane, free vibration (hex20 plane stress layer). Source: NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test FV32 (frequencies 44.623, 130.03, 162.70, 246.05, 379.90, 391.44 Hz); as reproduced in the Abaqus 2017 Benchmarks Guide, 'FV32: Cantilevered tapered membrane'. Tolerances — f1: targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%; f2: targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%; f3: targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%; f4: targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%; f5: targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%; f6: targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1% +- **[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-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) +- **[patch-hex20](../tests/oracle/patch-hex20/)** (theory): Constant-stress patch test, hex20. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1) +- **[patch-hex20-reduced](../tests/oracle/patch-hex20-reduced/)** (theory): Constant-stress patch test, hex20-reduced. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1) +- **[patch-hex8](../tests/oracle/patch-hex8/)** (theory): Constant-stress patch test, hex8. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1) +- **[patch-hex8-incompatible](../tests/oracle/patch-hex8-incompatible/)** (theory): Constant-stress patch test, hex8-incompatible. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1) +- **[patch-tet10](../tests/oracle/patch-tet10/)** (theory): Constant-stress patch test, tet10. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1) +- **[patch-tet4](../tests/oracle/patch-tet4/)** (theory): Constant-stress patch test, tet4. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1) +- **[patch-wedge6](../tests/oracle/patch-wedge6/)** (theory): Constant-stress patch test, wedge6. Source: Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7. Tolerances — ux: a patch test is passed exactly: the tolerance only admits rounding; uy: a patch test is passed exactly: the tolerance only admits rounding; uz: a patch test is passed exactly: the tolerance only admits rounding; sxx: a patch test is passed exactly: the tolerance only admits rounding; syy: a patch test is passed exactly: the tolerance only admits rounding; szz: a patch test is passed exactly: the tolerance only admits rounding; sxy: a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1) +- **[pure-bending-coupling](../tests/oracle/pure-bending-coupling/)** (theory): Pure bending through a distributing coupling. Source: Euler–Bernoulli pure bending, σ = M_y z/I, θ = ML/(EI) (exact in elasticity for ν = 0); Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §102. Tolerances — sigma_top: with ν = 0 pure bending is a quadratic displacement field that hex20 represents exactly; the coupling's nodal force pattern differs from the linear traction only within a depth or two of the end (Saint-Venant), five depths from the probe; rotation: the reference point follows the best-fit rigid motion of the end section, which the end zone's local deformation disturbs by well under 1% +- **[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) +- **[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 + +Reference values already cited; each case gets its recipe when the capability it needs lands. + +| Case | Kind | Problem | Needs | Reference values | Source | +|---|---|---|---|---|---| +| [cylinder-channel-2d1](../tests/oracle/cylinder-channel-2d1/) | benchmark | Flow around a cylinder, steady 2D-1 (Re = 20) | lattice Boltzmann CFD (M12) | drag coefficient (published interval 5.57–5.59) = 5.58; lift coefficient (interval 0.0104–0.0110) = 0.0107 | Schäfer and Turek, 'Benchmark computations of laminar flow around a cylinder', Notes on Numerical Fluid Mechanics 52 (1996) 547–566, Table 4 (reference intervals) | +| [lid-driven-cavity-re100](../tests/oracle/lid-driven-cavity-re100/) | benchmark | Lid-driven cavity, Re = 100 | lattice Boltzmann CFD (M12) | u/U at y = 0.4531 on the vertical centreline = -0.2109 | Ghia, Ghia and Shin, Journal of Computational Physics 48 (1982) 387–411, Table I (u along the vertical centreline) | +| [mh-twisted-beam](../tests/oracle/mh-twisted-beam/) | benchmark | MacNeal–Harder twisted beam | shell elements (M8) or mapped blocks with helical edges | tip displacement along the in-plane load = 0.005424; tip displacement along the out-of-plane load = 0.001754 | MacNeal and Harder, 'A proposed standard set of problems to test finite element accuracy', Finite Elements in Analysis and Design 1 (1985) 3–20, Fig. 6 (p. 8), Table 3 (p. 10) | +| [nafems-le1-plane-stress](../tests/oracle/nafems-le1-plane-stress/) | benchmark | NAFEMS LE1 elliptic membrane with 2D plane stress elements | 2D plane stress elements (docs/elements.md) | σ_yy at D (2, 0) = 92.7 | NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE1 | +| [nafems-le11](../tests/oracle/nafems-le11/) | benchmark | NAFEMS LE11 solid cylinder/taper/sphere under a temperature field | thermal loads (M8 heat transfer) | σ_zz at A = -105 | NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE11 (target σ_zz = −105 MPa at A) | +| [pinched-cylinder](../tests/oracle/pinched-cylinder/) | benchmark | Pinched cylinder with rigid diaphragms | shell elements (M8) | radial displacement under the load = -1.8248e-5 | Belytschko, Stolarski, Liu, Carpenter and Ong, 'Stress projection for membrane and shear locking in shell finite elements', Computer Methods in Applied Mechanics and Engineering 51 (1985) 221–258 (the shell obstacle course); reference 1.8248 × 10⁻⁵ under the load | +| [pinched-hemisphere](../tests/oracle/pinched-hemisphere/) | benchmark | Hemispherical shell with an 18° hole under alternating point loads | shell elements (M8), or mapped blocks that follow doubly curved faces | displacement under the load along the load = 0.094 | MacNeal and Harder, 'A proposed standard set of problems to test finite element accuracy', Finite Elements in Analysis and Design 1 (1985) 3–20, Fig. 9 (p. 9), Table 5(b) (p. 11): 0.0940 (lower bound 0.0924 from their [7]) | +| [plastic-thick-cylinder](../tests/oracle/plastic-thick-cylinder/) | theory | Elastic–perfectly plastic thick cylinder: collapse pressure | J2 plasticity (M8/M10 X2) | collapse pressure for a = 10, b = 20, σ_y = 250 = 200.09 | Hill, The Mathematical Theory of Plasticity, Oxford 1950, ch. V §2: plane strain von Mises collapse pressure p = (2/√3)σ_y ln(b/a) | +| [scordelis-lo-roof-shell](../tests/oracle/scordelis-lo-roof-shell/) | benchmark | Scordelis–Lo roof with shell elements | shell elements (MITC4, M8) | vertical displacement at the midpoint of the free edge = -0.3024 | MacNeal and Harder, 'A proposed standard set of problems to test finite element accuracy', Finite Elements in Analysis and Design 1 (1985) 3–20, Fig. 8 (p. 8), Table 5(a) (p. 11); Belytschko, Stolarski, Liu, Carpenter and Ong, 'Stress projection for membrane and shear locking in shell finite elements', Computer Methods in Applied Mechanics and Engineering 51 (1985) 221–258 (the shell obstacle course) | +| [taylor-bar](../tests/oracle/taylor-bar/) | experiment | Taylor bar impact (copper) | large-deformation plasticity and contact (M10 X2/X3) | final over initial length = to be transcribed | Wilkins and Guinan, 'Impact of cylinders on a rigid boundary', Journal of Applied Physics 44 (1973) 1200–1206 (final length and mushroom diameter vs impact speed) | diff --git a/docs/verification.md b/docs/verification.md index e4f86ea..959800a 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -27,3 +27,61 @@ Rules: (a better element, a finer default mesh), never to hide a regression. - The theory suite is the gate for every PR; experiment and benchmark cases run in CI too, with their cited tolerances. + +## The case format + +`cargo xtask oracle` builds `modelcraft-testkit`'s `oracle` binary (release) once and runs every +case through the command engine; the results are in [oracle.md](oracle.md). + +``` +cargo xtask oracle # every case; writes docs/oracle.md +cargo xtask oracle --case nafems-le1 # one case (repeatable); docs untouched +cargo xtask oracle --bless nafems-le1 # freeze a passing case's results (or --bless all) +cargo xtask oracle --quick # skip cases marked "slow" +``` + +- `recipe.json` is a script `modelcraft-cli run` also accepts: a JSON array of + `{"command", "params"}`; a step may carry an `"id"` so a quantity can read its output. Meshes + come from `mesh.block` (mapped hex, tet or wedge blocks with arcs and elliptical edges, named + faces, edges and vertices that sets select), `mesh.tet.generate`, or nodes and elements written + one by one; once structured and automatic meshers land, cases may use them. +- `design.json` (optional) is a SolidCraft script that builds the geometry, opened before the + recipe (the test corpus's recipe form). +- `expected.json`: + ```json + {"title": "…", "kind": "theory|benchmark|experiment", + "source": "document, section/table/figure, page", + "status": "active|pending", "requires": "the capability a pending case waits for", + "slow": false, + "quantities": [{"id": "syy_D", "quantity": "σ_yy at D (2, 0)", + "measure": {"command": "solve.probe", "params": {"step": "Load", "at": [[2, 0, 0]]}, "pointer": "/nodes/0/stress/1"}, + "value": 92.7, "tolerance": {"relative": 0.01}, "reason": "why this tolerance", + "known_failure": "optional: why ModelCraft misses the reference today"}]} + ``` + A measure reads a JSON pointer either from a recipe step's output (`"from": ""`) or from a + command run after the recipe (`"command"`, `"params"`); an optional `"scale"` multiplies it. + A tolerance is `relative` (of the reference), `absolute`, both, or `"exact": true`. A pending + case cites its reference values (or `null` until transcribed) and has no recipe yet. +- `fixture.json` holds each passing quantity's accepted `value` and its `band` + (10⁻⁹ of the larger of value and reference, plus 10⁻³ of an absolute tolerance): the result is + deterministic, the band only absorbs last-digit differences of the system maths library. + +The run fails when a quantity misses its reference (unless it is a `known_failure`, which is +listed in oracle.md with its reason), when a known failure starts passing (remove the mark), +when a result leaves its fixture band, when a passing quantity has no fixture, or when a case +cannot run. + +## Adding cases for a new capability + +Every capability that lands brings its oracle cases in the same pull request: + +1. Turn the pending cases that waited for it into active ones (write `recipe.json`, drop + `status`/`requires`, add `measure` and `tolerance` with its reason to each quantity), and add + theory cases for its closed-form solutions (patch test and convergence for an element; the + analytic problem for a load, constraint or procedure). +2. Choose meshes that are converged well inside the tolerance (show the refinement in the case's + README), or the mesh a benchmark prescribes. +3. Run `cargo xtask oracle --case `; when it passes, `--bless `; commit the case, its + fixture and the regenerated docs/oracle.md. +4. A case that misses its reference stays honest: keep the tolerance its reason supports and add + `known_failure` with what is missing. Never widen a tolerance to make a case pass. diff --git a/tests/oracle/bar-pressure-wave/README.md b/tests/oracle/bar-pressure-wave/README.md new file mode 100644 index 0000000..19f502d --- /dev/null +++ b/tests/oracle/bar-pressure-wave/README.md @@ -0,0 +1,4 @@ +# Elastic wave in a bar from a pressure step (explicit) + +A bar 10 × 0.1 × 0.1 of 200 one-point hex8 elements (E = 10⁴, ρ = 1, ν = 0, so c = 100) with a +pressure step of 1 on its end x = 10, solved explicitly to t = 0.06 (the front has travelled 6). diff --git a/tests/oracle/bar-pressure-wave/expected.json b/tests/oracle/bar-pressure-wave/expected.json new file mode 100644 index 0000000..bc8325f --- /dev/null +++ b/tests/oracle/bar-pressure-wave/expected.json @@ -0,0 +1,66 @@ +{ + "title": "Elastic wave in a bar from a pressure step (explicit)", + "kind": "theory", + "source": "d'Alembert's solution of the one-dimensional wave equation: c = √(E/ρ), particle velocity v = p/(ρc); Graff, Wave Motion in Elastic Solids, Dover 1991, §2.1", + "quantities": [ + { + "id": "u_end", + "quantity": "displacement of the loaded end at t = 0.06", + "measure": { + "command": "solve.probe", + "params": { + "step": "Wave", + "at": [ + [ + 10.0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": -0.0006, + "tolerance": { + "relative": 0.01 + }, + "reason": "the end moves at the particle velocity p/(ρc) from the start; the discrete wave's ringing behind the front changes the end's displacement by well under 1%" + }, + { + "id": "v_mid", + "quantity": "particle velocity at x = 5, 20 elements behind the front, at t = 0.06", + "measure": { + "command": "solve.probe", + "params": { + "step": "Wave", + "at": [ + [ + 5.0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/velocity/0" + }, + "value": -0.01, + "tolerance": { + "relative": 0.02 + }, + "reason": "central differences ring behind a step front (dispersion); bulk viscosity damps it within a few elements, leaving about 1% twenty elements behind" + }, + { + "id": "balance", + "quantity": "energy balance error", + "measure": { + "from": "solve", + "pointer": "/energy_balance" + }, + "value": 0.0, + "tolerance": { + "absolute": 0.01 + }, + "reason": "external work = kinetic + internal + dissipated energy; the solver warns above 1%" + } + ] +} diff --git a/tests/oracle/bar-pressure-wave/fixture.json b/tests/oracle/bar-pressure-wave/fixture.json new file mode 100644 index 0000000..8f0c548 --- /dev/null +++ b/tests/oracle/bar-pressure-wave/fixture.json @@ -0,0 +1,17 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "balance": { + "band": 0.00001000000152706151, + "value": 0.001527061509461723 + }, + "u_end": { + "band": 6e-13, + "value": -0.0005998107000086691 + }, + "v_mid": { + "band": 1.0000000000000001e-11, + "value": -0.00985543917252298 + } + } +} diff --git a/tests/oracle/bar-pressure-wave/recipe.json b/tests/oracle/bar-pressure-wave/recipe.json new file mode 100644 index 0000000..ab076e9 --- /dev/null +++ b/tests/oracle/bar-pressure-wave/recipe.json @@ -0,0 +1,10 @@ +[ + {"command": "mesh.block", "params": {"body": "Bar", "kind": "hex8", "corners": [[0, 0, 0], [10.0, 0, 0], [10.0, 0.1, 0], [0, 0.1, 0], [0, 0, 0.1], [10.0, 0, 0.1], [10.0, 0.1, 0.1], [0, 0.1, 0.1]], "divisions": [200, 1, 1], "faces": {"4": "end"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Bar"}]}}, + {"command": "model.set.create", "params": {"name": "end", "geometry": [{"body": "Bar", "face": "end"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 10000.0, "poisson": 0.0}, "density": 1.0}}, + {"command": "model.section.create", "params": {"name": "bar", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}}, + {"command": "model.load.create", "params": {"name": "p", "type": "pressure", "set": "end", "magnitude": 1.0}}, + {"command": "model.step.create", "params": {"name": "Wave", "procedure": {"type": "explicit_dynamic", "end_time": 0.06}, "boundary_conditions": [], "loads": ["p"]}}, + {"command": "solve.explicit", "params": {"step": "Wave"}, "id": "solve"} +] diff --git a/tests/oracle/bar-rigid-wall-impact/README.md b/tests/oracle/bar-rigid-wall-impact/README.md new file mode 100644 index 0000000..c661e13 --- /dev/null +++ b/tests/oracle/bar-rigid-wall-impact/README.md @@ -0,0 +1,5 @@ +# Elastic bar striking a rigid wall (explicit) + +A bar 10 × 0.1 × 0.1 of 200 one-point hex8 elements (E = 10⁴, ρ = 1, ν = 0, c = 100) moving at +v₀ = 0.1 towards a frictionless rigid wall at x = 0 (its end face is the contact set); linear bulk +viscosity off. Solved to t = 0.3: in contact from 0 to 2L/c = 0.2, then free at +v₀. diff --git a/tests/oracle/bar-rigid-wall-impact/expected.json b/tests/oracle/bar-rigid-wall-impact/expected.json new file mode 100644 index 0000000..81789e9 --- /dev/null +++ b/tests/oracle/bar-rigid-wall-impact/expected.json @@ -0,0 +1,67 @@ +{ + "title": "Elastic bar striking a rigid wall (explicit)", + "kind": "theory", + "source": "One-dimensional elastic impact on a rigid wall: contact time 2L/c, rebound at the impact speed, contact stress ρcv₀; Graff, Wave Motion in Elastic Solids, Dover 1991, §2.3; Johnson, Impact Strength of Materials, Arnold 1972, §1.6", + "quantities": [ + { + "id": "u_front", + "quantity": "displacement of the struck end at t = 0.3 (in contact until 2L/c = 0.2, then leaving at v₀)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Impact", + "at": [ + [ + 0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": 0.009999999999999998, + "tolerance": { + "relative": 0.03 + }, + "reason": "the contact ends at 2L/c in the elastic theory; the discrete release front is spread over a few elements, a few tenths of a percent of the time, and the rebound speed is v₀ to about 1%" + }, + { + "id": "u_back", + "quantity": "displacement of the far end at t = 0.3 (−v₀L/c, then +v₀ after the reflection)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Impact", + "at": [ + [ + 10.0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": 0.01, + "tolerance": { + "relative": 0.03 + }, + "reason": "as for the struck end" + }, + { + "id": "ke", + "quantity": "kinetic energy after the rebound over the initial ½mv₀²", + "measure": { + "from": "solve", + "pointer": "/energy/kinetic", + "scale": 1999.9999999999995 + }, + "value": 1.0, + "tolerance": { + "relative": 0.02 + }, + "reason": "an elastic impact keeps the kinetic energy; quadratic bulk viscosity in the compression front and the vibration left in the bar take about 1%" + } + ] +} diff --git a/tests/oracle/bar-rigid-wall-impact/fixture.json b/tests/oracle/bar-rigid-wall-impact/fixture.json new file mode 100644 index 0000000..3ac1d47 --- /dev/null +++ b/tests/oracle/bar-rigid-wall-impact/fixture.json @@ -0,0 +1,17 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "ke": { + "band": 1e-9, + "value": 0.9946927802814477 + }, + "u_back": { + "band": 1.0000000000000001e-11, + "value": 0.009946347263883909 + }, + "u_front": { + "band": 1e-11, + "value": 0.009949683285107395 + } + } +} diff --git a/tests/oracle/bar-rigid-wall-impact/recipe.json b/tests/oracle/bar-rigid-wall-impact/recipe.json new file mode 100644 index 0000000..efce52d --- /dev/null +++ b/tests/oracle/bar-rigid-wall-impact/recipe.json @@ -0,0 +1,9 @@ +[ + {"command": "mesh.block", "params": {"body": "Bar", "kind": "hex8", "corners": [[0, 0, 0], [10.0, 0, 0], [10.0, 0.1, 0], [0, 0.1, 0], [0, 0, 0.1], [10.0, 0, 0.1], [10.0, 0.1, 0.1], [0, 0.1, 0.1]], "divisions": [200, 1, 1], "faces": {"6": "front"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Bar"}]}}, + {"command": "model.set.create", "params": {"name": "front", "geometry": [{"body": "Bar", "face": "front"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 10000.0, "poisson": 0.0}, "density": 1.0}}, + {"command": "model.section.create", "params": {"name": "bar", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}}, + {"command": "model.step.create", "params": {"name": "Impact", "procedure": {"type": "explicit_dynamic", "end_time": 0.3, "initial_velocities": [{"set": "all", "velocity": [-0.1, 0, 0]}], "rigid_walls": [{"set": "front", "point": [0, 0, 0], "normal": [1, 0, 0]}], "bulk_viscosity": {"linear": 0.0, "quadratic": 1.5}}, "boundary_conditions": [], "loads": []}}, + {"command": "solve.explicit", "params": {"step": "Impact"}, "id": "solve"} +] diff --git a/tests/oracle/cantilever-frequencies/README.md b/tests/oracle/cantilever-frequencies/README.md new file mode 100644 index 0000000..506f74d --- /dev/null +++ b/tests/oracle/cantilever-frequencies/README.md @@ -0,0 +1,4 @@ +# Natural frequencies of a cantilever (Euler–Bernoulli) + +A 20 × 1 × 1 bar of 20 × 2 × 2 hex20 elements clamped at x = 0 (E = 1000, ν = 0, ρ = 1), consistent +mass. Euler–Bernoulli frequencies of a clamped–free beam. diff --git a/tests/oracle/cantilever-frequencies/expected.json b/tests/oracle/cantilever-frequencies/expected.json new file mode 100644 index 0000000..7f829d8 --- /dev/null +++ b/tests/oracle/cantilever-frequencies/expected.json @@ -0,0 +1,46 @@ +{ + "title": "Natural frequencies of a cantilever (Euler–Bernoulli)", + "kind": "theory", + "source": "Euler–Bernoulli beam: f_n = (β_n L)²/(2π L²)·√(EI/ρA), β₁L = 1.8751, β₂L = 4.6941; Timoshenko, Young and Weaver, Vibration Problems in Engineering, 4th ed., Wiley 1974, §5.3", + "quantities": [ + { + "id": "f1", + "quantity": "first bending frequency (modes 1 and 2 are the y and z pair)", + "measure": { + "from": "modes", + "pointer": "/modes/0/frequency" + }, + "value": 0.012770863695053732, + "tolerance": { + "relative": 0.01 + }, + "reason": "Euler–Bernoulli neglects shear and rotary inertia, which lower f1 by about 0.2% at L/h = 20" + }, + { + "id": "f1_pair", + "quantity": "the other first bending frequency", + "measure": { + "from": "modes", + "pointer": "/modes/1/frequency" + }, + "value": 0.012770863695053732, + "tolerance": { + "relative": 0.01 + }, + "reason": "as f1: a square section bends alike in y and z" + }, + { + "id": "f2", + "quantity": "second bending frequency", + "measure": { + "from": "modes", + "pointer": "/modes/2/frequency" + }, + "value": 0.08003363662360005, + "tolerance": { + "relative": 0.02 + }, + "reason": "shear and rotary inertia lower the second mode by about 1% at L/h = 20 (Timoshenko)" + } + ] +} diff --git a/tests/oracle/cantilever-frequencies/fixture.json b/tests/oracle/cantilever-frequencies/fixture.json new file mode 100644 index 0000000..61a1c5f --- /dev/null +++ b/tests/oracle/cantilever-frequencies/fixture.json @@ -0,0 +1,17 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "f1": { + "band": 1.2770863695053732e-11, + "value": 0.012750678058109033 + }, + "f1_pair": { + "band": 1.2770863695053732e-11, + "value": 0.012750678058529053 + }, + "f2": { + "band": 8.003363662360005e-11, + "value": 0.07916657526441415 + } + } +} diff --git a/tests/oracle/cantilever-frequencies/recipe.json b/tests/oracle/cantilever-frequencies/recipe.json new file mode 100644 index 0000000..2c9a54e --- /dev/null +++ b/tests/oracle/cantilever-frequencies/recipe.json @@ -0,0 +1,10 @@ +[ + {"command": "mesh.block", "params": {"body": "Beam", "kind": "hex20", "corners": [[0, 0, 0], [20.0, 0, 0], [20.0, 1.0, 0], [0, 1.0, 0], [0, 0, 1.0], [20.0, 0, 1.0], [20.0, 1.0, 1.0], [0, 1.0, 1.0]], "divisions": [20, 2, 2], "faces": {"6": "root"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Beam"}]}}, + {"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Beam", "face": "root"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.0}, "density": 1.0}}, + {"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "clamp", "type": "fixed", "target": {"set": "root"}}}, + {"command": "model.step.create", "params": {"name": "Modes", "procedure": {"type": "frequency", "modes": 4}, "boundary_conditions": ["clamp"], "loads": []}}, + {"command": "solve.frequency", "params": {"step": "Modes"}, "id": "modes"} +] diff --git a/tests/oracle/cantilever-hex20/README.md b/tests/oracle/cantilever-hex20/README.md new file mode 100644 index 0000000..3144a37 --- /dev/null +++ b/tests/oracle/cantilever-hex20/README.md @@ -0,0 +1,5 @@ +# Cantilever under a tip load, Timoshenko deflection (hex20) + +A 10 × 1 × 1 bar of hex20 elements (20 × 2 × 2) clamped at x = 0 with a total +force of 1 in −z spread over the free end (E = 1000, ν = 0, so no Poisson coupling at the clamp). +Timoshenko beam theory gives δ = PL³/(3EI) + PL/(κGA) = 4 + 0.024 = 4.024. diff --git a/tests/oracle/cantilever-hex20/expected.json b/tests/oracle/cantilever-hex20/expected.json new file mode 100644 index 0000000..56af8b6 --- /dev/null +++ b/tests/oracle/cantilever-hex20/expected.json @@ -0,0 +1,43 @@ +{ + "title": "Cantilever under a tip load, Timoshenko deflection (hex20)", + "kind": "theory", + "source": "Timoshenko and Gere, Mechanics of Materials (beam with shear deformation: δ = PL³/3EI + PL/κGA, κ = 5/6 for a rectangle); Cowper, J. Appl. Mech. 33 (1966) 335–340 for the shear coefficient", + "quantities": [ + { + "id": "tip_uz", + "quantity": "tip deflection u_z at (10, 0.5, 0.5)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Bend", + "at": [ + [ + 10.0, + 0.5, + 0.5 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": -4.024, + "tolerance": { + "relative": 0.005 + }, + "reason": "beam theory vs 3D elasticity: the shear coefficient and the fully clamped end change the tip deflection by a few tenths of a percent at L/h = 10" + }, + { + "id": "reaction", + "quantity": "total reaction z", + "measure": { + "from": "solve", + "pointer": "/reaction_total/2" + }, + "value": 1.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "equilibrium holds to rounding" + } + ] +} diff --git a/tests/oracle/cantilever-hex20/fixture.json b/tests/oracle/cantilever-hex20/fixture.json new file mode 100644 index 0000000..4c1d5f2 --- /dev/null +++ b/tests/oracle/cantilever-hex20/fixture.json @@ -0,0 +1,13 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "reaction": { + "band": 1.0000000000401866e-9, + "value": 1.0000000000401865 + }, + "tip_uz": { + "band": 4.024e-9, + "value": -4.023474075811461 + } + } +} diff --git a/tests/oracle/cantilever-hex20/recipe.json b/tests/oracle/cantilever-hex20/recipe.json new file mode 100644 index 0000000..3b7ede6 --- /dev/null +++ b/tests/oracle/cantilever-hex20/recipe.json @@ -0,0 +1,12 @@ +[ + {"command": "mesh.block", "params": {"body": "Beam", "kind": "hex20", "corners": [[0, 0, 0], [10.0, 0, 0], [10.0, 1.0, 0], [0, 1.0, 0], [0, 0, 1.0], [10.0, 0, 1.0], [10.0, 1.0, 1.0], [0, 1.0, 1.0]], "divisions": [20, 2, 2], "faces": {"6": "root", "4": "tip"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Beam"}]}}, + {"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Beam", "face": "root"}]}}, + {"command": "model.set.create", "params": {"name": "tip", "geometry": [{"body": "Beam", "face": "tip"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.0}}}, + {"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "clamp", "type": "fixed", "target": {"set": "root"}}}, + {"command": "model.load.create", "params": {"name": "P", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, -1]}}, + {"command": "model.step.create", "params": {"name": "Bend", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["P"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/cantilever-hex8-incompatible/README.md b/tests/oracle/cantilever-hex8-incompatible/README.md new file mode 100644 index 0000000..243ffa1 --- /dev/null +++ b/tests/oracle/cantilever-hex8-incompatible/README.md @@ -0,0 +1,5 @@ +# Cantilever under a tip load, Timoshenko deflection (hex8-incompatible) + +A 10 × 1 × 1 bar of hex8-incompatible elements (20 × 2 × 2) clamped at x = 0 with a total +force of 1 in −z spread over the free end (E = 1000, ν = 0, so no Poisson coupling at the clamp). +Timoshenko beam theory gives δ = PL³/(3EI) + PL/(κGA) = 4 + 0.024 = 4.024. diff --git a/tests/oracle/cantilever-hex8-incompatible/expected.json b/tests/oracle/cantilever-hex8-incompatible/expected.json new file mode 100644 index 0000000..50c12e3 --- /dev/null +++ b/tests/oracle/cantilever-hex8-incompatible/expected.json @@ -0,0 +1,43 @@ +{ + "title": "Cantilever under a tip load, Timoshenko deflection (hex8-incompatible)", + "kind": "theory", + "source": "Timoshenko and Gere, Mechanics of Materials (beam with shear deformation: δ = PL³/3EI + PL/κGA, κ = 5/6 for a rectangle); Cowper, J. Appl. Mech. 33 (1966) 335–340 for the shear coefficient", + "quantities": [ + { + "id": "tip_uz", + "quantity": "tip deflection u_z at (10, 0.5, 0.5)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Bend", + "at": [ + [ + 10.0, + 0.5, + 0.5 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": -4.024, + "tolerance": { + "relative": 0.005 + }, + "reason": "beam theory vs 3D elasticity (a few tenths of a percent at L/h = 10); incompatible modes represent the bending exactly in the constant-moment part" + }, + { + "id": "reaction", + "quantity": "total reaction z", + "measure": { + "from": "solve", + "pointer": "/reaction_total/2" + }, + "value": 1.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "equilibrium holds to rounding" + } + ] +} diff --git a/tests/oracle/cantilever-hex8-incompatible/fixture.json b/tests/oracle/cantilever-hex8-incompatible/fixture.json new file mode 100644 index 0000000..0800363 --- /dev/null +++ b/tests/oracle/cantilever-hex8-incompatible/fixture.json @@ -0,0 +1,13 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "reaction": { + "band": 1e-9, + "value": 0.9999999999883957 + }, + "tip_uz": { + "band": 4.024e-9, + "value": -4.017499999970641 + } + } +} diff --git a/tests/oracle/cantilever-hex8-incompatible/recipe.json b/tests/oracle/cantilever-hex8-incompatible/recipe.json new file mode 100644 index 0000000..aafeebe --- /dev/null +++ b/tests/oracle/cantilever-hex8-incompatible/recipe.json @@ -0,0 +1,12 @@ +[ + {"command": "mesh.block", "params": {"body": "Beam", "kind": "hex8", "corners": [[0, 0, 0], [10.0, 0, 0], [10.0, 1.0, 0], [0, 1.0, 0], [0, 0, 1.0], [10.0, 0, 1.0], [10.0, 1.0, 1.0], [0, 1.0, 1.0]], "divisions": [20, 2, 2], "faces": {"6": "root", "4": "tip"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Beam"}]}}, + {"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Beam", "face": "root"}]}}, + {"command": "model.set.create", "params": {"name": "tip", "geometry": [{"body": "Beam", "face": "tip"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.0}}}, + {"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"incompatible_modes": true}}}, + {"command": "model.boundary_condition.create", "params": {"name": "clamp", "type": "fixed", "target": {"set": "root"}}}, + {"command": "model.load.create", "params": {"name": "P", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, -1]}}, + {"command": "model.step.create", "params": {"name": "Bend", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["P"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/cantilever-tet10/README.md b/tests/oracle/cantilever-tet10/README.md new file mode 100644 index 0000000..97aa082 --- /dev/null +++ b/tests/oracle/cantilever-tet10/README.md @@ -0,0 +1,5 @@ +# Cantilever under a tip load, Timoshenko deflection (tet10) + +A 10 × 1 × 1 bar of tet10 elements (20 × 2 × 2) clamped at x = 0 with a total +force of 1 in −z spread over the free end (E = 1000, ν = 0, so no Poisson coupling at the clamp). +Timoshenko beam theory gives δ = PL³/(3EI) + PL/(κGA) = 4 + 0.024 = 4.024. diff --git a/tests/oracle/cantilever-tet10/expected.json b/tests/oracle/cantilever-tet10/expected.json new file mode 100644 index 0000000..cae5c70 --- /dev/null +++ b/tests/oracle/cantilever-tet10/expected.json @@ -0,0 +1,43 @@ +{ + "title": "Cantilever under a tip load, Timoshenko deflection (tet10)", + "kind": "theory", + "source": "Timoshenko and Gere, Mechanics of Materials (beam with shear deformation: δ = PL³/3EI + PL/κGA, κ = 5/6 for a rectangle); Cowper, J. Appl. Mech. 33 (1966) 335–340 for the shear coefficient", + "quantities": [ + { + "id": "tip_uz", + "quantity": "tip deflection u_z at (10, 0.5, 0.5)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Bend", + "at": [ + [ + 10.0, + 0.5, + 0.5 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": -4.024, + "tolerance": { + "relative": 0.005 + }, + "reason": "beam theory vs 3D elasticity (a few tenths of a percent at L/h = 10); quadratic tetrahedra are converged on this mesh" + }, + { + "id": "reaction", + "quantity": "total reaction z", + "measure": { + "from": "solve", + "pointer": "/reaction_total/2" + }, + "value": 1.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "equilibrium holds to rounding" + } + ] +} diff --git a/tests/oracle/cantilever-tet10/fixture.json b/tests/oracle/cantilever-tet10/fixture.json new file mode 100644 index 0000000..67fb136 --- /dev/null +++ b/tests/oracle/cantilever-tet10/fixture.json @@ -0,0 +1,13 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "reaction": { + "band": 1.0000000000174362e-9, + "value": 1.000000000017436 + }, + "tip_uz": { + "band": 4.024e-9, + "value": -4.0232748685235595 + } + } +} diff --git a/tests/oracle/cantilever-tet10/recipe.json b/tests/oracle/cantilever-tet10/recipe.json new file mode 100644 index 0000000..403e57c --- /dev/null +++ b/tests/oracle/cantilever-tet10/recipe.json @@ -0,0 +1,12 @@ +[ + {"command": "mesh.block", "params": {"body": "Beam", "kind": "tet10", "corners": [[0, 0, 0], [10.0, 0, 0], [10.0, 1.0, 0], [0, 1.0, 0], [0, 0, 1.0], [10.0, 0, 1.0], [10.0, 1.0, 1.0], [0, 1.0, 1.0]], "divisions": [20, 2, 2], "faces": {"6": "root", "4": "tip"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Beam"}]}}, + {"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Beam", "face": "root"}]}}, + {"command": "model.set.create", "params": {"name": "tip", "geometry": [{"body": "Beam", "face": "tip"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.0}}}, + {"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "clamp", "type": "fixed", "target": {"set": "root"}}}, + {"command": "model.load.create", "params": {"name": "P", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, -1]}}, + {"command": "model.step.create", "params": {"name": "Bend", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["P"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/cdt-cylinder/README.md b/tests/oracle/cdt-cylinder/README.md new file mode 100644 index 0000000..6dd1e9f --- /dev/null +++ b/tests/oracle/cdt-cylinder/README.md @@ -0,0 +1,5 @@ +# Constrained Delaunay mesh of a cylinder: volume within the chordal bound, valid elements + +A cylinder R = 10, H = 20 built with SolidCraft from `design.json`, tessellated within 0.05 and +tetrahedralized by `mesh.tet.generate`. The mesh fills the inscribed polyhedron, whose volume lies +between (1 − 2s/R)πR²H and πR²H. diff --git a/tests/oracle/cdt-cylinder/design.json b/tests/oracle/cdt-cylinder/design.json new file mode 100644 index 0000000..c859832 --- /dev/null +++ b/tests/oracle/cdt-cylinder/design.json @@ -0,0 +1,12 @@ +{ + "commands": [ + { + "command": "solid.cylinder", + "params": { + "radius": 10.0, + "height": 20.0, + "body_name": "Cylinder" + } + } + ] +} diff --git a/tests/oracle/cdt-cylinder/expected.json b/tests/oracle/cdt-cylinder/expected.json new file mode 100644 index 0000000..9113c57 --- /dev/null +++ b/tests/oracle/cdt-cylinder/expected.json @@ -0,0 +1,33 @@ +{ + "title": "Constrained Delaunay mesh of a cylinder: volume within the chordal bound, valid elements", + "kind": "theory", + "source": "mesh invariants: an inscribed polygon of sagitta s loses at most 2s/R of a circle's area (elementary geometry); constrained Delaunay tetrahedralization after Diazzi et al., ACM TOG 42(6) 2023", + "quantities": [ + { + "id": "volume", + "quantity": "volume of the tetrahedra", + "measure": { + "from": "tet", + "pointer": "/volume" + }, + "value": 6267.477343911638, + "tolerance": { + "relative": 0.0025 + }, + "reason": "the tessellation inscribes the cylinder within the chordal tolerance s = 0.05: the polygon section loses between 0 and 2s/R = 0.5% of the area; the reference is the middle of that band" + }, + { + "id": "invalid", + "quantity": "elements that fail the certified Jacobian check", + "measure": { + "command": "mesh.quality", + "pointer": "/invalid/count" + }, + "value": 0.0, + "tolerance": { + "exact": true + }, + "reason": "every element of a valid mesh has positive volume" + } + ] +} diff --git a/tests/oracle/cdt-cylinder/fixture.json b/tests/oracle/cdt-cylinder/fixture.json new file mode 100644 index 0000000..3a847e4 --- /dev/null +++ b/tests/oracle/cdt-cylinder/fixture.json @@ -0,0 +1,13 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "invalid": { + "band": 0.0, + "value": 0.0 + }, + "volume": { + "band": 6.267477343911638e-6, + "value": 6266.527341942973 + } + } +} diff --git a/tests/oracle/cdt-cylinder/recipe.json b/tests/oracle/cdt-cylinder/recipe.json new file mode 100644 index 0000000..3b618a2 --- /dev/null +++ b/tests/oracle/cdt-cylinder/recipe.json @@ -0,0 +1,3 @@ +[ + {"command": "mesh.tet.generate", "params": {"body": "Cylinder", "tolerance": 0.05}, "id": "tet"} +] diff --git a/tests/oracle/cdt-stepped-block/README.md b/tests/oracle/cdt-stepped-block/README.md new file mode 100644 index 0000000..491f751 --- /dev/null +++ b/tests/oracle/cdt-stepped-block/README.md @@ -0,0 +1,5 @@ +# Constrained Delaunay mesh of a stepped block: exact volume, valid elements + +The corpus part `stepped-block` (three boxes, a pocket cut through), built with SolidCraft from +`design.json`, tetrahedralized by `mesh.tet.generate`. Every boundary face is planar, so the +tessellation is exact and the mesh's volume must equal 47000 to rounding. diff --git a/tests/oracle/cdt-stepped-block/design.json b/tests/oracle/cdt-stepped-block/design.json new file mode 100644 index 0000000..5a440e6 --- /dev/null +++ b/tests/oracle/cdt-stepped-block/design.json @@ -0,0 +1,69 @@ +{ + "commands": [ + { + "command": "solid.box", + "params": { + "corner": [ + 0, + 0, + 0 + ], + "length": 60, + "width": 40, + "height": 10, + "body_name": "Steps" + } + }, + { + "command": "solid.box", + "params": { + "corner": [ + 0, + 0, + 10 + ], + "length": 40, + "width": 40, + "height": 10, + "operation": "join", + "targets": [ + "Steps" + ] + } + }, + { + "command": "solid.box", + "params": { + "corner": [ + 0, + 0, + 20 + ], + "length": 20, + "width": 40, + "height": 10, + "operation": "join", + "targets": [ + "Steps" + ] + } + }, + { + "command": "solid.box", + "params": { + "corner": [ + 45, + 15, + 0 + ], + "length": 10, + "width": 10, + "height": 10, + "operation": "cut", + "targets": [ + "Steps" + ] + } + } + ] +} diff --git a/tests/oracle/cdt-stepped-block/expected.json b/tests/oracle/cdt-stepped-block/expected.json new file mode 100644 index 0000000..6ef0289 --- /dev/null +++ b/tests/oracle/cdt-stepped-block/expected.json @@ -0,0 +1,46 @@ +{ + "title": "Constrained Delaunay mesh of a stepped block: exact volume, valid elements", + "kind": "theory", + "source": "mesh invariants: a tetrahedralization of a polyhedron fills its volume exactly (Si, TetGen paper ACM TOMS 41(2) 2015 for the definition of a CDT; Diazzi et al., ACM TOG 42(6) 2023); the volume is 60·40·10 + 40·40·10 + 20·40·10 − 10·10·10", + "quantities": [ + { + "id": "volume", + "quantity": "volume of the tetrahedra", + "measure": { + "from": "tet", + "pointer": "/volume" + }, + "value": 47000.0, + "tolerance": { + "relative": 1e-12 + }, + "reason": "a planar boundary is tessellated exactly and the constrained Delaunay mesh fills it exactly: only rounding" + }, + { + "id": "invalid", + "quantity": "elements that fail the certified Jacobian check", + "measure": { + "command": "mesh.quality", + "pointer": "/invalid/count" + }, + "value": 0.0, + "tolerance": { + "exact": true + }, + "reason": "every element of a valid mesh has positive volume" + }, + { + "id": "undetermined", + "quantity": "elements the Jacobian check could not decide", + "measure": { + "command": "mesh.quality", + "pointer": "/undetermined/count" + }, + "value": 0.0, + "tolerance": { + "exact": true + }, + "reason": "a linear tetrahedron's sign is exact" + } + ] +} diff --git a/tests/oracle/cdt-stepped-block/fixture.json b/tests/oracle/cdt-stepped-block/fixture.json new file mode 100644 index 0000000..d4a0ebe --- /dev/null +++ b/tests/oracle/cdt-stepped-block/fixture.json @@ -0,0 +1,17 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "invalid": { + "band": 0.0, + "value": 0.0 + }, + "undetermined": { + "band": 0.0, + "value": 0.0 + }, + "volume": { + "band": 0.000047000000000000004, + "value": 47000.0 + } + } +} diff --git a/tests/oracle/cdt-stepped-block/recipe.json b/tests/oracle/cdt-stepped-block/recipe.json new file mode 100644 index 0000000..064fa2e --- /dev/null +++ b/tests/oracle/cdt-stepped-block/recipe.json @@ -0,0 +1,3 @@ +[ + {"command": "mesh.tet.generate", "params": {"body": "Steps", "tolerance": 0.05}, "id": "tet"} +] diff --git a/tests/oracle/cylinder-channel-2d1/README.md b/tests/oracle/cylinder-channel-2d1/README.md new file mode 100644 index 0000000..999ab53 --- /dev/null +++ b/tests/oracle/cylinder-channel-2d1/README.md @@ -0,0 +1,3 @@ +# Flow around a cylinder, steady 2D-1 (Re = 20) + +Channel 2.2 × 0.41 with a cylinder of diameter 0.1, parabolic inflow, Re = 20. diff --git a/tests/oracle/cylinder-channel-2d1/expected.json b/tests/oracle/cylinder-channel-2d1/expected.json new file mode 100644 index 0000000..22bde90 --- /dev/null +++ b/tests/oracle/cylinder-channel-2d1/expected.json @@ -0,0 +1,19 @@ +{ + "title": "Flow around a cylinder, steady 2D-1 (Re = 20)", + "kind": "benchmark", + "source": "Schäfer and Turek, 'Benchmark computations of laminar flow around a cylinder', Notes on Numerical Fluid Mechanics 52 (1996) 547–566, Table 4 (reference intervals)", + "status": "pending", + "requires": "lattice Boltzmann CFD (M12)", + "quantities": [ + { + "id": "cd", + "quantity": "drag coefficient (published interval 5.57–5.59)", + "value": 5.58 + }, + { + "id": "cl", + "quantity": "lift coefficient (interval 0.0104–0.0110)", + "value": 0.0107 + } + ] +} diff --git a/tests/oracle/free-beam-frequencies/README.md b/tests/oracle/free-beam-frequencies/README.md new file mode 100644 index 0000000..d806ad7 --- /dev/null +++ b/tests/oracle/free-beam-frequencies/README.md @@ -0,0 +1,4 @@ +# Natural frequencies of a free beam: rigid-body modes and the first bending mode + +The same bar with no support: six rigid-body modes at zero, then the first bending pair at the +free–free Euler–Bernoulli frequency. diff --git a/tests/oracle/free-beam-frequencies/expected.json b/tests/oracle/free-beam-frequencies/expected.json new file mode 100644 index 0000000..b2a240a --- /dev/null +++ b/tests/oracle/free-beam-frequencies/expected.json @@ -0,0 +1,46 @@ +{ + "title": "Natural frequencies of a free beam: rigid-body modes and the first bending mode", + "kind": "theory", + "source": "Euler–Bernoulli free–free beam, β₁L = 4.7300; Timoshenko, Young and Weaver, Vibration Problems in Engineering, 4th ed., Wiley 1974, §5.3", + "quantities": [ + { + "id": "rigid_6", + "quantity": "sixth frequency (the last rigid-body mode)", + "measure": { + "from": "modes", + "pointer": "/modes/5/frequency" + }, + "value": 0.0, + "tolerance": { + "absolute": 8.1264203095911e-05 + }, + "reason": "a free body has six zero frequencies; numerical rigid-body frequencies below 10⁻³ of the first elastic one (eigenvalues below 10⁻⁶ of it) are rounding of the shifted solve" + }, + { + "id": "f7", + "quantity": "first free–free bending frequency (mode 7)", + "measure": { + "from": "modes", + "pointer": "/modes/6/frequency" + }, + "value": 0.08126420309591101, + "tolerance": { + "relative": 0.015 + }, + "reason": "Euler–Bernoulli neglects shear and rotary inertia, about 0.8% at L/h = 20 for the free–free mode" + }, + { + "id": "mass", + "quantity": "total mass", + "measure": { + "from": "modes", + "pointer": "/total_mass" + }, + "value": 20.0, + "tolerance": { + "relative": 1e-12 + }, + "reason": "the mass matrix integrates the density exactly" + } + ] +} diff --git a/tests/oracle/free-beam-frequencies/fixture.json b/tests/oracle/free-beam-frequencies/fixture.json new file mode 100644 index 0000000..818666c --- /dev/null +++ b/tests/oracle/free-beam-frequencies/fixture.json @@ -0,0 +1,17 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "f7": { + "band": 8.126420309591101e-11, + "value": 0.08061820502077137 + }, + "mass": { + "band": 2e-8, + "value": 19.999999999992152 + }, + "rigid_6": { + "band": 8.126420325076649e-8, + "value": 1.5485547377408235e-7 + } + } +} diff --git a/tests/oracle/free-beam-frequencies/recipe.json b/tests/oracle/free-beam-frequencies/recipe.json new file mode 100644 index 0000000..33ad5c7 --- /dev/null +++ b/tests/oracle/free-beam-frequencies/recipe.json @@ -0,0 +1,8 @@ +[ + {"command": "mesh.block", "params": {"body": "Beam", "kind": "hex20", "corners": [[0, 0, 0], [20.0, 0, 0], [20.0, 1.0, 0], [0, 1.0, 0], [0, 0, 1.0], [20.0, 0, 1.0], [20.0, 1.0, 1.0], [0, 1.0, 1.0]], "divisions": [20, 2, 2]}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Beam"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.0}, "density": 1.0}}, + {"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.step.create", "params": {"name": "Modes", "procedure": {"type": "frequency", "modes": 8}, "boundary_conditions": [], "loads": []}}, + {"command": "solve.frequency", "params": {"step": "Modes"}, "id": "modes"} +] diff --git a/tests/oracle/kirsch-plate/README.md b/tests/oracle/kirsch-plate/README.md new file mode 100644 index 0000000..1a40f68 --- /dev/null +++ b/tests/oracle/kirsch-plate/README.md @@ -0,0 +1,7 @@ +# Plate with a circular hole in tension (Kirsch) + +A quarter of a square plate of half-width 30 with a hole of radius 1 (W = 60a, so the infinite +plate's solution applies to a few tenths of a percent), thin (free faces, plane stress), with +symmetry on x = 0, y = 0 and the mid-plane, pulled by a traction of 1 on x = 30. Two hex20 blocks +around the hole (40 × 16 each), graded 40 : 1 towards it. Kirsch: σ_θ = 3S at the top of +the hole, −S at its side. diff --git a/tests/oracle/kirsch-plate/expected.json b/tests/oracle/kirsch-plate/expected.json new file mode 100644 index 0000000..b67b811 --- /dev/null +++ b/tests/oracle/kirsch-plate/expected.json @@ -0,0 +1,53 @@ +{ + "title": "Plate with a circular hole in tension (Kirsch)", + "kind": "theory", + "source": "Kirsch, Zeitschrift des VDI 42 (1898) 797–807; Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §35", + "quantities": [ + { + "id": "kt", + "quantity": "σ_xx at the top of the hole (0, 1, 0) over the remote stress", + "measure": { + "command": "solve.probe", + "params": { + "step": "Tension", + "at": [ + [ + 0, + 1.0, + 0 + ] + ] + }, + "pointer": "/nodes/0/stress/0" + }, + "value": 3.0, + "tolerance": { + "relative": 0.02 + }, + "reason": "the reference is the infinite plate; the finite plate (W = 60a, Heywood's width correction about +0.1%, and the loaded ends 30a away) raises the peak by a few tenths of a percent, and the stress extrapolated to the hole surface adds up to about 1%" + }, + { + "id": "side", + "quantity": "σ_yy at the side of the hole (1, 0, 0) over the remote stress", + "measure": { + "command": "solve.probe", + "params": { + "step": "Tension", + "at": [ + [ + 1.0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": -1.0, + "tolerance": { + "relative": 0.03 + }, + "reason": "infinite-plate value −S; finite width and extrapolation as for Kt, on a smaller value" + } + ] +} diff --git a/tests/oracle/kirsch-plate/fixture.json b/tests/oracle/kirsch-plate/fixture.json new file mode 100644 index 0000000..ec95459 --- /dev/null +++ b/tests/oracle/kirsch-plate/fixture.json @@ -0,0 +1,13 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "kt": { + "band": 3.0120654194508524e-9, + "value": 3.012065419450852 + }, + "side": { + "band": 1.0110362570446956e-9, + "value": -1.0110362570446956 + } + } +} diff --git a/tests/oracle/kirsch-plate/recipe.json b/tests/oracle/kirsch-plate/recipe.json new file mode 100644 index 0000000..53dbc8a --- /dev/null +++ b/tests/oracle/kirsch-plate/recipe.json @@ -0,0 +1,17 @@ +[ + {"command": "mesh.block", "params": {"body": "Plate", "kind": "hex20", "corners": [[1.0, 0, 0], [30.0, 0, 0], [30.0, 30.0, 0], [0.7071067811865476, 0.7071067811865476, 0], [1.0, 0, 0.1], [30.0, 0, 0.1], [30.0, 30.0, 0.1], [0.7071067811865476, 0.7071067811865476, 0.1]], "divisions": [40, 16, 1], "bias": [40, 1, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 0.1]}}], "faces": {"6": "hole", "3": "y0", "4": "load", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Plate", "kind": "hex20", "corners": [[0.7071067811865476, 0.7071067811865476, 0], [30.0, 30.0, 0], [0, 30.0, 0], [0, 1.0, 0], [0.7071067811865476, 0.7071067811865476, 0.1], [30.0, 30.0, 0.1], [0, 30.0, 0.1], [0, 1.0, 0.1]], "divisions": [40, 16, 1], "bias": [40, 1, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 0.1]}}], "faces": {"6": "hole", "5": "x0", "1": "z0"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Plate"}]}}, + {"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Plate", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Plate", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Plate", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "load", "geometry": [{"body": "Plate", "face": "load"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}}}, + {"command": "model.section.create", "params": {"name": "plate", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sx", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sy", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sz", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "S", "type": "traction", "set": "load", "vector": [1, 0, 0]}}, + {"command": "model.step.create", "params": {"name": "Tension", "procedure": {"type": "static"}, "boundary_conditions": ["sx", "sy", "sz"], "loads": ["S"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/lame-cylinder/README.md b/tests/oracle/lame-cylinder/README.md new file mode 100644 index 0000000..e1d97ce --- /dev/null +++ b/tests/oracle/lame-cylinder/README.md @@ -0,0 +1,6 @@ +# Thick-walled cylinder under internal pressure, plane strain (Lamé) + +A quarter of a cylinder a = 10, b = 20, one unit long, with symmetry planes x = 0 and y = 0 and +u_z = 0 on both ends (plane strain), and internal pressure p = 100 (E = 210000, ν = 0.3); +8 × 16 hex20 elements with mid-side nodes on the arcs. Lamé: u_r = (1 + ν)a²p/(E(b² − a²)) +((1 − 2ν)r + b²/r), σ_θ(a) = p(b² + a²)/(b² − a²). diff --git a/tests/oracle/lame-cylinder/expected.json b/tests/oracle/lame-cylinder/expected.json new file mode 100644 index 0000000..de0be8c --- /dev/null +++ b/tests/oracle/lame-cylinder/expected.json @@ -0,0 +1,76 @@ +{ + "title": "Thick-walled cylinder under internal pressure, plane strain (Lamé)", + "kind": "theory", + "source": "Lamé's solution, Timoshenko and Goodier, Theory of Elasticity, 3rd ed., McGraw-Hill 1970, §28 (thick-walled cylinder)", + "quantities": [ + { + "id": "ur_a", + "quantity": "radial displacement at the bore (10, 0, 0)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Pressure", + "at": [ + [ + 10.0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": 0.00907936507936508, + "tolerance": { + "relative": 0.001 + }, + "reason": "quadratic elements on 8 radial divisions resolve u_r = A r + B/r to well under 0.1%" + }, + { + "id": "ur_b", + "quantity": "radial displacement outside (20, 0, 0)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Pressure", + "at": [ + [ + 20.0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": 0.005777777777777778, + "tolerance": { + "relative": 0.001 + }, + "reason": "as for the bore" + }, + { + "id": "hoop_a", + "quantity": "hoop stress σ_θ = σ_yy at the bore (10, 0, 0)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Pressure", + "at": [ + [ + 10.0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": 166.66666666666666, + "tolerance": { + "relative": 0.01 + }, + "reason": "stress extrapolated from the integration points to the bore surface and averaged over two elements: about 0.5% at the steepest gradient" + } + ] +} diff --git a/tests/oracle/lame-cylinder/fixture.json b/tests/oracle/lame-cylinder/fixture.json new file mode 100644 index 0000000..f0382a1 --- /dev/null +++ b/tests/oracle/lame-cylinder/fixture.json @@ -0,0 +1,17 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "hoop_a": { + "band": 1.6666666666666668e-7, + "value": 166.36121907829693 + }, + "ur_a": { + "band": 9.07936507936508e-12, + "value": 0.009079340794555735 + }, + "ur_b": { + "band": 5.777777777777779e-12, + "value": 0.0057777604134337515 + } + } +} diff --git a/tests/oracle/lame-cylinder/recipe.json b/tests/oracle/lame-cylinder/recipe.json new file mode 100644 index 0000000..3fc666b --- /dev/null +++ b/tests/oracle/lame-cylinder/recipe.json @@ -0,0 +1,18 @@ +[ + {"command": "mesh.block", "params": {"body": "Pipe", "kind": "hex20", "corners": [[10.0, 0, 0], [20.0, 0, 0], [0, 20.0, 0], [0, 10.0, 0], [10.0, 0, 1], [20.0, 0, 1], [0, 20.0, 1], [0, 10.0, 1]], "divisions": [8, 16, 1], "edges": [{"corners": [2, 3], "arc": {"center": [0, 0, 0]}}, {"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 1]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 1]}}], "faces": {"6": "bore", "3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Pipe"}]}}, + {"command": "model.set.create", "params": {"name": "bore", "geometry": [{"body": "Pipe", "face": "bore"}]}}, + {"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Pipe", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Pipe", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Pipe", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Pipe", "face": "z1"}]}}, + {"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000.0, "poisson": 0.3}}}, + {"command": "model.section.create", "params": {"name": "pipe", "set": "all", "material": "Steel", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sx", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sy", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sz0", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sz1", "type": "symmetry", "target": {"set": "z1"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "p", "type": "pressure", "set": "bore", "magnitude": 100.0}}, + {"command": "model.step.create", "params": {"name": "Pressure", "procedure": {"type": "static"}, "boundary_conditions": ["sx", "sy", "sz0", "sz1"], "loads": ["p"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/lid-driven-cavity-re100/README.md b/tests/oracle/lid-driven-cavity-re100/README.md new file mode 100644 index 0000000..6b285b4 --- /dev/null +++ b/tests/oracle/lid-driven-cavity-re100/README.md @@ -0,0 +1,3 @@ +# Lid-driven cavity, Re = 100 + +The square cavity with a moving lid at Re = 100. diff --git a/tests/oracle/lid-driven-cavity-re100/expected.json b/tests/oracle/lid-driven-cavity-re100/expected.json new file mode 100644 index 0000000..f600362 --- /dev/null +++ b/tests/oracle/lid-driven-cavity-re100/expected.json @@ -0,0 +1,14 @@ +{ + "title": "Lid-driven cavity, Re = 100", + "kind": "benchmark", + "source": "Ghia, Ghia and Shin, Journal of Computational Physics 48 (1982) 387–411, Table I (u along the vertical centreline)", + "status": "pending", + "requires": "lattice Boltzmann CFD (M12)", + "quantities": [ + { + "id": "u_min", + "quantity": "u/U at y = 0.4531 on the vertical centreline", + "value": -0.2109 + } + ] +} diff --git a/tests/oracle/mh-curved-cantilever/README.md b/tests/oracle/mh-curved-cantilever/README.md new file mode 100644 index 0000000..293a25f --- /dev/null +++ b/tests/oracle/mh-curved-cantilever/README.md @@ -0,0 +1,6 @@ +# MacNeal–Harder curved cantilever, hex8 with incompatible modes + +Our model of the published problem: a quarter ring, inner radius 4.12, outer 4.32, thickness 0.1 +(E = 10⁷, ν = 0.25), of the prescribed 6 × 1 elements (hex8 with incompatible modes, corners on +the arcs), clamped at one end; a unit force at the free end, in the plane across the beam and out +of the plane, each a separate step. diff --git a/tests/oracle/mh-curved-cantilever/expected.json b/tests/oracle/mh-curved-cantilever/expected.json new file mode 100644 index 0000000..213ba99 --- /dev/null +++ b/tests/oracle/mh-curved-cantilever/expected.json @@ -0,0 +1,55 @@ +{ + "title": "MacNeal–Harder curved cantilever, hex8 with incompatible modes", + "kind": "benchmark", + "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. 5 (p. 7) geometry, material and mesh; Table 3 (p. 10) theoretical tip displacements", + "quantities": [ + { + "id": "in_plane", + "quantity": "tip displacement along the in-plane load", + "measure": { + "command": "solve.probe", + "params": { + "step": "In_plane", + "at": [ + [ + 0, + 4.12, + 0 + ] + ] + }, + "pointer": "/nodes/0/displacement/1" + }, + "value": 0.08734, + "tolerance": { + "relative": 0.02 + }, + "reason": "MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14)", + "known_failure": "0.880 of the theory, grade C: straight-sided hex8 on six elements over 90° (MacNeal–Harder's HEXA(8): 0.880, Table 9 p. 15)" + }, + { + "id": "out_of_plane", + "quantity": "tip displacement along the out-of-plane load", + "measure": { + "command": "solve.probe", + "params": { + "step": "Out_of_plane", + "at": [ + [ + 0, + 4.12, + 0 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": 0.5022, + "tolerance": { + "relative": 0.02 + }, + "reason": "MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14)", + "known_failure": "0.798 of the theory, grade D: out-of-plane bending with torsion on one element across (MacNeal–Harder's HEXA(8): 0.849)" + } + ] +} diff --git a/tests/oracle/mh-curved-cantilever/fixture.json b/tests/oracle/mh-curved-cantilever/fixture.json new file mode 100644 index 0000000..6344008 --- /dev/null +++ b/tests/oracle/mh-curved-cantilever/fixture.json @@ -0,0 +1,4 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": {} +} diff --git a/tests/oracle/mh-curved-cantilever/recipe.json b/tests/oracle/mh-curved-cantilever/recipe.json new file mode 100644 index 0000000..4b8b001 --- /dev/null +++ b/tests/oracle/mh-curved-cantilever/recipe.json @@ -0,0 +1,15 @@ +[ + {"command": "mesh.block", "params": {"body": "Beam", "kind": "hex8", "corners": [[4.12, 0, 0], [4.32, 0, 0], [0, 4.32, 0], [0, 4.12, 0], [4.12, 0, 0.1], [4.32, 0, 0.1], [0, 4.32, 0.1], [0, 4.12, 0.1]], "divisions": [1, 6, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 0.1]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 0.1]}}], "faces": {"3": "root", "5": "tip"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Beam"}]}}, + {"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Beam", "face": "root"}]}}, + {"command": "model.set.create", "params": {"name": "tip", "geometry": [{"body": "Beam", "face": "tip"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 10000000.0, "poisson": 0.25}}}, + {"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"incompatible_modes": true}}}, + {"command": "model.boundary_condition.create", "params": {"name": "clamp", "type": "fixed", "target": {"set": "root"}}}, + {"command": "model.load.create", "params": {"name": "in_plane", "type": "force", "target": {"set": "tip"}, "vector": [0, 1, 0]}}, + {"command": "model.load.create", "params": {"name": "out_of_plane", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, 1]}}, + {"command": "model.step.create", "params": {"name": "In_plane", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["in_plane"]}}, + {"command": "model.step.create", "params": {"name": "Out_of_plane", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["out_of_plane"]}}, + {"command": "solve.static", "params": {"step": "In_plane"}}, + {"command": "solve.static", "params": {"step": "Out_of_plane"}} +] diff --git a/tests/oracle/mh-solid-patch/README.md b/tests/oracle/mh-solid-patch/README.md new file mode 100644 index 0000000..0ecf2fd --- /dev/null +++ b/tests/oracle/mh-solid-patch/README.md @@ -0,0 +1,6 @@ +# MacNeal–Harder patch test for solids (hex8 with incompatible modes) + +Our model of the published problem: a unit cube cut into seven hexahedra around an irregular inner +hexahedron (inner node coordinates from Fig. 3), E = 10⁶, ν = 0.25, with the linear displacement +field of Table 2(c) prescribed at the eight corners. The published solution is the uniform strain +10⁻³ with σ_x = σ_y = σ_z = 2000 and τ = 400. diff --git a/tests/oracle/mh-solid-patch/expected.json b/tests/oracle/mh-solid-patch/expected.json new file mode 100644 index 0000000..0d9035c --- /dev/null +++ b/tests/oracle/mh-solid-patch/expected.json @@ -0,0 +1,168 @@ +{ + "title": "MacNeal–Harder patch test for solids (hex8 with incompatible modes)", + "kind": "benchmark", + "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. 3 (p. 6) geometry and inner node locations, Table 2(c) (p. 10) boundary displacements and theoretical solution", + "quantities": [ + { + "id": "sxx", + "quantity": "σ_xx at inner node 7 (0.788, 0.693, 0.644)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Patch", + "at": [ + [ + 0.788, + 0.693, + 0.644 + ] + ] + }, + "pointer": "/nodes/0/stress/0" + }, + "value": 2000.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "syy", + "quantity": "σ_yy at inner node 7 (0.788, 0.693, 0.644)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Patch", + "at": [ + [ + 0.788, + 0.693, + 0.644 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": 2000.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "szz", + "quantity": "σ_zz at inner node 7 (0.788, 0.693, 0.644)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Patch", + "at": [ + [ + 0.788, + 0.693, + 0.644 + ] + ] + }, + "pointer": "/nodes/0/stress/2" + }, + "value": 2000.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "txy", + "quantity": "τ_xy at inner node 7", + "measure": { + "command": "solve.probe", + "params": { + "step": "Patch", + "at": [ + [ + 0.788, + 0.693, + 0.644 + ] + ] + }, + "pointer": "/nodes/0/stress/3" + }, + "value": 400.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "txz", + "quantity": "τ_xz at inner node 7", + "measure": { + "command": "solve.probe", + "params": { + "step": "Patch", + "at": [ + [ + 0.788, + 0.693, + 0.644 + ] + ] + }, + "pointer": "/nodes/0/stress/4" + }, + "value": 400.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "tyz", + "quantity": "τ_yz at inner node 7", + "measure": { + "command": "solve.probe", + "params": { + "step": "Patch", + "at": [ + [ + 0.788, + 0.693, + 0.644 + ] + ] + }, + "pointer": "/nodes/0/stress/5" + }, + "value": 400.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "u7", + "quantity": "u_x at inner node 7", + "measure": { + "command": "solve.probe", + "params": { + "step": "Patch", + "at": [ + [ + 0.788, + 0.693, + 0.644 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": 0.0014565000000000001, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + } + ] +} diff --git a/tests/oracle/mh-solid-patch/fixture.json b/tests/oracle/mh-solid-patch/fixture.json new file mode 100644 index 0000000..1f8c715 --- /dev/null +++ b/tests/oracle/mh-solid-patch/fixture.json @@ -0,0 +1,33 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "sxx": { + "band": 2.000000000000001e-6, + "value": 2000.000000000001 + }, + "syy": { + "band": 2.000000000000003e-6, + "value": 2000.0000000000027 + }, + "szz": { + "band": 2.000000000000003e-6, + "value": 2000.0000000000027 + }, + "txy": { + "band": 4.00000000000001e-7, + "value": 400.00000000000097 + }, + "txz": { + "band": 4.0000000000000003e-7, + "value": 399.99999999999994 + }, + "tyz": { + "band": 4.0000000000000003e-7, + "value": 399.9999999999994 + }, + "u7": { + "band": 1.4565000000000005e-12, + "value": 0.0014565000000000003 + } + } +} diff --git a/tests/oracle/mh-solid-patch/recipe.json b/tests/oracle/mh-solid-patch/recipe.json new file mode 100644 index 0000000..c04af95 --- /dev/null +++ b/tests/oracle/mh-solid-patch/recipe.json @@ -0,0 +1,46 @@ +[ + {"command": "mesh.node.add", "params": {"label": 1, "xyz": [0, 0, 0]}}, + {"command": "mesh.node.add", "params": {"label": 2, "xyz": [1, 0, 0]}}, + {"command": "mesh.node.add", "params": {"label": 3, "xyz": [1, 1, 0]}}, + {"command": "mesh.node.add", "params": {"label": 4, "xyz": [0, 1, 0]}}, + {"command": "mesh.node.add", "params": {"label": 5, "xyz": [0, 0, 1]}}, + {"command": "mesh.node.add", "params": {"label": 6, "xyz": [1, 0, 1]}}, + {"command": "mesh.node.add", "params": {"label": 7, "xyz": [1, 1, 1]}}, + {"command": "mesh.node.add", "params": {"label": 8, "xyz": [0, 1, 1]}}, + {"command": "mesh.node.add", "params": {"label": 9, "xyz": [0.249, 0.342, 0.192]}}, + {"command": "mesh.node.add", "params": {"label": 10, "xyz": [0.826, 0.288, 0.288]}}, + {"command": "mesh.node.add", "params": {"label": 11, "xyz": [0.85, 0.649, 0.263]}}, + {"command": "mesh.node.add", "params": {"label": 12, "xyz": [0.273, 0.75, 0.23]}}, + {"command": "mesh.node.add", "params": {"label": 13, "xyz": [0.32, 0.186, 0.643]}}, + {"command": "mesh.node.add", "params": {"label": 14, "xyz": [0.677, 0.305, 0.683]}}, + {"command": "mesh.node.add", "params": {"label": 15, "xyz": [0.788, 0.693, 0.644]}}, + {"command": "mesh.node.add", "params": {"label": 16, "xyz": [0.165, 0.745, 0.702]}}, + {"command": "mesh.element.add", "params": {"label": 1, "kind": "hex8", "nodes": [9, 10, 11, 12, 13, 14, 15, 16]}}, + {"command": "mesh.element.add", "params": {"label": 2, "kind": "hex8", "nodes": [1, 2, 3, 4, 9, 10, 11, 12]}}, + {"command": "mesh.element.add", "params": {"label": 3, "kind": "hex8", "nodes": [13, 14, 15, 16, 5, 6, 7, 8]}}, + {"command": "mesh.element.add", "params": {"label": 4, "kind": "hex8", "nodes": [1, 2, 10, 9, 5, 6, 14, 13]}}, + {"command": "mesh.element.add", "params": {"label": 5, "kind": "hex8", "nodes": [12, 11, 3, 4, 16, 15, 7, 8]}}, + {"command": "mesh.element.add", "params": {"label": 6, "kind": "hex8", "nodes": [1, 9, 12, 4, 5, 13, 16, 8]}}, + {"command": "mesh.element.add", "params": {"label": 7, "kind": "hex8", "nodes": [10, 2, 3, 11, 14, 6, 7, 15]}}, + {"command": "model.set.create", "params": {"name": "all", "elements": [1, 2, 3, 4, 5, 6, 7]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000000.0, "poisson": 0.25}}}, + {"command": "model.section.create", "params": {"name": "cube", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"incompatible_modes": true}}}, + {"command": "model.set.create", "params": {"name": "corner1", "nodes": [1]}}, + {"command": "model.boundary_condition.create", "params": {"name": "u1", "type": "displacement", "target": {"set": "corner1"}, "values": [0.0, 0.0, 0.0, null, null, null]}}, + {"command": "model.set.create", "params": {"name": "corner2", "nodes": [2]}}, + {"command": "model.boundary_condition.create", "params": {"name": "u2", "type": "displacement", "target": {"set": "corner2"}, "values": [0.001, 0.0005, 0.0005, null, null, null]}}, + {"command": "model.set.create", "params": {"name": "corner3", "nodes": [3]}}, + {"command": "model.boundary_condition.create", "params": {"name": "u3", "type": "displacement", "target": {"set": "corner3"}, "values": [0.0015, 0.0015, 0.001, null, null, null]}}, + {"command": "model.set.create", "params": {"name": "corner4", "nodes": [4]}}, + {"command": "model.boundary_condition.create", "params": {"name": "u4", "type": "displacement", "target": {"set": "corner4"}, "values": [0.0005, 0.001, 0.0005, null, null, null]}}, + {"command": "model.set.create", "params": {"name": "corner5", "nodes": [5]}}, + {"command": "model.boundary_condition.create", "params": {"name": "u5", "type": "displacement", "target": {"set": "corner5"}, "values": [0.0005, 0.0005, 0.001, null, null, null]}}, + {"command": "model.set.create", "params": {"name": "corner6", "nodes": [6]}}, + {"command": "model.boundary_condition.create", "params": {"name": "u6", "type": "displacement", "target": {"set": "corner6"}, "values": [0.0015, 0.001, 0.0015, null, null, null]}}, + {"command": "model.set.create", "params": {"name": "corner7", "nodes": [7]}}, + {"command": "model.boundary_condition.create", "params": {"name": "u7", "type": "displacement", "target": {"set": "corner7"}, "values": [0.002, 0.002, 0.002, null, null, null]}}, + {"command": "model.set.create", "params": {"name": "corner8", "nodes": [8]}}, + {"command": "model.boundary_condition.create", "params": {"name": "u8", "type": "displacement", "target": {"set": "corner8"}, "values": [0.001, 0.0015, 0.0015, null, null, null]}}, + {"command": "model.step.create", "params": {"name": "Patch", "procedure": {"type": "static"}, "boundary_conditions": ["u1", "u2", "u3", "u4", "u5", "u6", "u7", "u8"], "loads": []}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/mh-straight-cantilever/README.md b/tests/oracle/mh-straight-cantilever/README.md new file mode 100644 index 0000000..fce13dd --- /dev/null +++ b/tests/oracle/mh-straight-cantilever/README.md @@ -0,0 +1,7 @@ +# MacNeal–Harder straight cantilever, regular hex8 mesh with incompatible modes + +Our model of the published problem: a beam 6.0 long, 0.2 wide, 0.1 deep (E = 10⁷, ν = 0.3) of the +prescribed 6 × 1 regular elements, here hex8 with incompatible modes, clamped at one end; unit +forces at the free end along the beam, across its width and across its depth (spread over the end +face by area), and a unit twisting moment through a distributing coupling, each a separate step. +Displacements are read at a corner of the free end. diff --git a/tests/oracle/mh-straight-cantilever/expected.json b/tests/oracle/mh-straight-cantilever/expected.json new file mode 100644 index 0000000..5ae9779 --- /dev/null +++ b/tests/oracle/mh-straight-cantilever/expected.json @@ -0,0 +1,92 @@ +{ + "title": "MacNeal–Harder straight cantilever, regular hex8 mesh with incompatible modes", + "kind": "benchmark", + "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. 4(a) (p. 7) geometry, material and mesh; Table 3 (p. 10) theoretical tip displacements", + "quantities": [ + { + "id": "extension", + "quantity": "tip displacement along the load, extension", + "measure": { + "command": "solve.probe", + "params": { + "step": "Extension", + "at": [ + [ + 6.0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": 3e-05, + "tolerance": { + "relative": 0.02 + }, + "reason": "MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14)" + }, + { + "id": "in_plane", + "quantity": "tip displacement along the load, in-plane shear (width 0.2)", + "measure": { + "command": "solve.probe", + "params": { + "step": "In_plane", + "at": [ + [ + 6.0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/displacement/1" + }, + "value": 0.1081, + "tolerance": { + "relative": 0.02 + }, + "reason": "MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14)", + "known_failure": "0.978 of the theory, grade B: one hex8 with incompatible modes through the depth of the prescribed mesh (MacNeal–Harder's HEXA(8): 0.981, Table 8(a) p. 15); refined meshes converge (cantilever-hex8-incompatible)" + }, + { + "id": "out_of_plane", + "quantity": "tip displacement along the load, out-of-plane shear (depth 0.1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Out_of_plane", + "at": [ + [ + 6.0, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": 0.4321, + "tolerance": { + "relative": 0.02 + }, + "reason": "MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14)", + "known_failure": "0.973 of the theory, grade B, as for in-plane shear (MacNeal–Harder's HEXA(8): 0.981)" + }, + { + "id": "twist", + "quantity": "tip rotation under a unit twisting moment", + "measure": { + "from": "twist", + "pointer": "/reference_points/0/displacement/3" + }, + "value": 0.03208, + "tolerance": { + "relative": 0.02 + }, + "reason": "MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14); the moment reaches the end through a distributing coupling", + "known_failure": "0.892 of the theory, grade C: one element across the section cannot represent the torsion warping (MacNeal–Harder's HEXA(8): 0.910, HEX20: 0.904)" + } + ] +} diff --git a/tests/oracle/mh-straight-cantilever/fixture.json b/tests/oracle/mh-straight-cantilever/fixture.json new file mode 100644 index 0000000..717d24a --- /dev/null +++ b/tests/oracle/mh-straight-cantilever/fixture.json @@ -0,0 +1,9 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "extension": { + "band": 3.0000000000000005e-14, + "value": 0.00002962884635719957 + } + } +} diff --git a/tests/oracle/mh-straight-cantilever/recipe.json b/tests/oracle/mh-straight-cantilever/recipe.json new file mode 100644 index 0000000..b9d589d --- /dev/null +++ b/tests/oracle/mh-straight-cantilever/recipe.json @@ -0,0 +1,23 @@ +[ + {"command": "mesh.block", "params": {"body": "Beam", "kind": "hex8", "corners": [[0, 0, 0], [6.0, 0, 0], [6.0, 0.2, 0], [0, 0.2, 0], [0, 0, 0.1], [6.0, 0, 0.1], [6.0, 0.2, 0.1], [0, 0.2, 0.1]], "divisions": [6, 1, 1], "faces": {"6": "root", "4": "tip"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Beam"}]}}, + {"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Beam", "face": "root"}]}}, + {"command": "model.set.create", "params": {"name": "tip", "geometry": [{"body": "Beam", "face": "tip"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 10000000.0, "poisson": 0.3}}}, + {"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"incompatible_modes": true}}}, + {"command": "model.boundary_condition.create", "params": {"name": "clamp", "type": "fixed", "target": {"set": "root"}}}, + {"command": "model.reference_point.create", "params": {"name": "RP", "xyz": [6.0, 0.1, 0.05]}}, + {"command": "model.coupling.create", "params": {"name": "end", "reference_point": "RP", "set": "tip", "kind": "distributing"}}, + {"command": "model.load.create", "params": {"name": "extension", "type": "force", "target": {"set": "tip"}, "vector": [1, 0, 0]}}, + {"command": "model.load.create", "params": {"name": "in_plane", "type": "force", "target": {"set": "tip"}, "vector": [0, 1, 0]}}, + {"command": "model.load.create", "params": {"name": "out_of_plane", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, 1]}}, + {"command": "model.load.create", "params": {"name": "twist", "type": "moment", "target": {"reference_point": "RP"}, "vector": [1, 0, 0]}}, + {"command": "model.step.create", "params": {"name": "Extension", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["extension"]}}, + {"command": "model.step.create", "params": {"name": "In_plane", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["in_plane"]}}, + {"command": "model.step.create", "params": {"name": "Out_of_plane", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["out_of_plane"]}}, + {"command": "model.step.create", "params": {"name": "Twist", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["twist"]}}, + {"command": "solve.static", "params": {"step": "Extension"}, "id": "extension"}, + {"command": "solve.static", "params": {"step": "In_plane"}, "id": "in_plane"}, + {"command": "solve.static", "params": {"step": "Out_of_plane"}, "id": "out_of_plane"}, + {"command": "solve.static", "params": {"step": "Twist"}, "id": "twist"} +] diff --git a/tests/oracle/mh-thick-cylinder/README.md b/tests/oracle/mh-thick-cylinder/README.md new file mode 100644 index 0000000..4a31518 --- /dev/null +++ b/tests/oracle/mh-thick-cylinder/README.md @@ -0,0 +1,7 @@ +# MacNeal–Harder thick-walled cylinder, nearly incompressible (hex8 with incompatible modes) + +Our model of the published problem: inner radius 3, outer 9, thickness 1, E = 1000, plane strain, +unit internal pressure, ν = 0.49, 0.499 and 0.4999 (three separate bodies in one model, solved +together). The radial element boundaries are those of Fig. 10 (3.0, 3.5, 4.2, 5.2, 6.75, 9.0); +we model a quarter with symmetry planes (nine 10° elements around) instead of one 10° slice, +which needs no skew support and is the same axisymmetric problem. diff --git a/tests/oracle/mh-thick-cylinder/expected.json b/tests/oracle/mh-thick-cylinder/expected.json new file mode 100644 index 0000000..54b7f39 --- /dev/null +++ b/tests/oracle/mh-thick-cylinder/expected.json @@ -0,0 +1,76 @@ +{ + "title": "MacNeal–Harder thick-walled cylinder, nearly incompressible (hex8 with incompatible modes)", + "kind": "benchmark", + "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. 10 (p. 9) geometry, material and radial mesh; Table 5(c) (p. 11) radial displacements at the inner radius", + "quantities": [ + { + "id": "ur_1", + "quantity": "radial displacement at the inner radius, ν = 0.49", + "measure": { + "command": "solve.probe", + "params": { + "step": "Pressure", + "at": [ + [ + 3.0, + 0, + 0.0 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": 0.0050399, + "tolerance": { + "relative": 0.02 + }, + "reason": "MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14)" + }, + { + "id": "ur_2", + "quantity": "radial displacement at the inner radius, ν = 0.499", + "measure": { + "command": "solve.probe", + "params": { + "step": "Pressure", + "at": [ + [ + 3.0, + 0, + 2.0 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": 0.0050602, + "tolerance": { + "relative": 0.02 + }, + "reason": "MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14)" + }, + { + "id": "ur_3", + "quantity": "radial displacement at the inner radius, ν = 0.4999", + "measure": { + "command": "solve.probe", + "params": { + "step": "Pressure", + "at": [ + [ + 3.0, + 0, + 4.0 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": 0.0050623, + "tolerance": { + "relative": 0.02 + }, + "reason": "MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14)" + } + ] +} diff --git a/tests/oracle/mh-thick-cylinder/fixture.json b/tests/oracle/mh-thick-cylinder/fixture.json new file mode 100644 index 0000000..cb9f0b1 --- /dev/null +++ b/tests/oracle/mh-thick-cylinder/fixture.json @@ -0,0 +1,17 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "ur_1": { + "band": 5.0399e-12, + "value": 0.0049705467068696585 + }, + "ur_2": { + "band": 5.0602000000000006e-12, + "value": 0.004989212096414709 + }, + "ur_3": { + "band": 5.0622999999999995e-12, + "value": 0.004991068698509793 + } + } +} diff --git a/tests/oracle/mh-thick-cylinder/recipe.json b/tests/oracle/mh-thick-cylinder/recipe.json new file mode 100644 index 0000000..ffdaf04 --- /dev/null +++ b/tests/oracle/mh-thick-cylinder/recipe.json @@ -0,0 +1,58 @@ +[ + {"command": "mesh.block", "params": {"body": "Cylinder1", "kind": "hex8", "corners": [[3.0, 0, 0.0], [3.5, 0, 0.0], [0, 3.5, 0.0], [0, 3.0, 0.0], [3.0, 0, 1.0], [3.5, 0, 1.0], [0, 3.5, 1.0], [0, 3.0, 1.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 1.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 1.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1", "6": "bore"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder1", "kind": "hex8", "corners": [[3.5, 0, 0.0], [4.2, 0, 0.0], [0, 4.2, 0.0], [0, 3.5, 0.0], [3.5, 0, 1.0], [4.2, 0, 1.0], [0, 4.2, 1.0], [0, 3.5, 1.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 1.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 1.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder1", "kind": "hex8", "corners": [[4.2, 0, 0.0], [5.2, 0, 0.0], [0, 5.2, 0.0], [0, 4.2, 0.0], [4.2, 0, 1.0], [5.2, 0, 1.0], [0, 5.2, 1.0], [0, 4.2, 1.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 1.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 1.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder1", "kind": "hex8", "corners": [[5.2, 0, 0.0], [6.75, 0, 0.0], [0, 6.75, 0.0], [0, 5.2, 0.0], [5.2, 0, 1.0], [6.75, 0, 1.0], [0, 6.75, 1.0], [0, 5.2, 1.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 1.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 1.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder1", "kind": "hex8", "corners": [[6.75, 0, 0.0], [9.0, 0, 0.0], [0, 9.0, 0.0], [0, 6.75, 0.0], [6.75, 0, 1.0], [9.0, 0, 1.0], [0, 9.0, 1.0], [0, 6.75, 1.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 1.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 1.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "model.set.create", "params": {"name": "all1", "geometry": [{"body": "Cylinder1"}]}}, + {"command": "model.set.create", "params": {"name": "x01", "geometry": [{"body": "Cylinder1", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "y01", "geometry": [{"body": "Cylinder1", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "z01", "geometry": [{"body": "Cylinder1", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "z11", "geometry": [{"body": "Cylinder1", "face": "z1"}]}}, + {"command": "model.set.create", "params": {"name": "bore1", "geometry": [{"body": "Cylinder1", "face": "bore"}]}}, + {"command": "model.material.create", "params": {"name": "Mat1", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.49}}}, + {"command": "model.section.create", "params": {"name": "s1", "set": "all1", "material": "Mat1", "kind": {"type": "solid"}, "formulation": {"incompatible_modes": true}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sx1", "type": "symmetry", "target": {"set": "x01"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sy1", "type": "symmetry", "target": {"set": "y01"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sza1", "type": "symmetry", "target": {"set": "z01"}, "plane": "z"}}, + {"command": "model.boundary_condition.create", "params": {"name": "szb1", "type": "symmetry", "target": {"set": "z11"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "p1", "type": "pressure", "set": "bore1", "magnitude": 1.0}}, + {"command": "mesh.block", "params": {"body": "Cylinder2", "kind": "hex8", "corners": [[3.0, 0, 2.0], [3.5, 0, 2.0], [0, 3.5, 2.0], [0, 3.0, 2.0], [3.0, 0, 3.0], [3.5, 0, 3.0], [0, 3.5, 3.0], [0, 3.0, 3.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 2.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 2.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 3.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 3.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1", "6": "bore"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder2", "kind": "hex8", "corners": [[3.5, 0, 2.0], [4.2, 0, 2.0], [0, 4.2, 2.0], [0, 3.5, 2.0], [3.5, 0, 3.0], [4.2, 0, 3.0], [0, 4.2, 3.0], [0, 3.5, 3.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 2.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 2.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 3.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 3.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder2", "kind": "hex8", "corners": [[4.2, 0, 2.0], [5.2, 0, 2.0], [0, 5.2, 2.0], [0, 4.2, 2.0], [4.2, 0, 3.0], [5.2, 0, 3.0], [0, 5.2, 3.0], [0, 4.2, 3.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 2.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 2.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 3.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 3.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder2", "kind": "hex8", "corners": [[5.2, 0, 2.0], [6.75, 0, 2.0], [0, 6.75, 2.0], [0, 5.2, 2.0], [5.2, 0, 3.0], [6.75, 0, 3.0], [0, 6.75, 3.0], [0, 5.2, 3.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 2.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 2.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 3.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 3.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder2", "kind": "hex8", "corners": [[6.75, 0, 2.0], [9.0, 0, 2.0], [0, 9.0, 2.0], [0, 6.75, 2.0], [6.75, 0, 3.0], [9.0, 0, 3.0], [0, 9.0, 3.0], [0, 6.75, 3.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 2.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 2.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 3.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 3.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "model.set.create", "params": {"name": "all2", "geometry": [{"body": "Cylinder2"}]}}, + {"command": "model.set.create", "params": {"name": "x02", "geometry": [{"body": "Cylinder2", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "y02", "geometry": [{"body": "Cylinder2", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "z02", "geometry": [{"body": "Cylinder2", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "z12", "geometry": [{"body": "Cylinder2", "face": "z1"}]}}, + {"command": "model.set.create", "params": {"name": "bore2", "geometry": [{"body": "Cylinder2", "face": "bore"}]}}, + {"command": "model.material.create", "params": {"name": "Mat2", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.499}}}, + {"command": "model.section.create", "params": {"name": "s2", "set": "all2", "material": "Mat2", "kind": {"type": "solid"}, "formulation": {"incompatible_modes": true}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sx2", "type": "symmetry", "target": {"set": "x02"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sy2", "type": "symmetry", "target": {"set": "y02"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sza2", "type": "symmetry", "target": {"set": "z02"}, "plane": "z"}}, + {"command": "model.boundary_condition.create", "params": {"name": "szb2", "type": "symmetry", "target": {"set": "z12"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "p2", "type": "pressure", "set": "bore2", "magnitude": 1.0}}, + {"command": "mesh.block", "params": {"body": "Cylinder3", "kind": "hex8", "corners": [[3.0, 0, 4.0], [3.5, 0, 4.0], [0, 3.5, 4.0], [0, 3.0, 4.0], [3.0, 0, 5.0], [3.5, 0, 5.0], [0, 3.5, 5.0], [0, 3.0, 5.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 4.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 4.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 5.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 5.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1", "6": "bore"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder3", "kind": "hex8", "corners": [[3.5, 0, 4.0], [4.2, 0, 4.0], [0, 4.2, 4.0], [0, 3.5, 4.0], [3.5, 0, 5.0], [4.2, 0, 5.0], [0, 4.2, 5.0], [0, 3.5, 5.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 4.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 4.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 5.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 5.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder3", "kind": "hex8", "corners": [[4.2, 0, 4.0], [5.2, 0, 4.0], [0, 5.2, 4.0], [0, 4.2, 4.0], [4.2, 0, 5.0], [5.2, 0, 5.0], [0, 5.2, 5.0], [0, 4.2, 5.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 4.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 4.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 5.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 5.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder3", "kind": "hex8", "corners": [[5.2, 0, 4.0], [6.75, 0, 4.0], [0, 6.75, 4.0], [0, 5.2, 4.0], [5.2, 0, 5.0], [6.75, 0, 5.0], [0, 6.75, 5.0], [0, 5.2, 5.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 4.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 4.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 5.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 5.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cylinder3", "kind": "hex8", "corners": [[6.75, 0, 4.0], [9.0, 0, 4.0], [0, 9.0, 4.0], [0, 6.75, 4.0], [6.75, 0, 5.0], [9.0, 0, 5.0], [0, 9.0, 5.0], [0, 6.75, 5.0]], "divisions": [1, 9, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 4.0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 4.0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 5.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 5.0]}}], "faces": {"3": "y0", "5": "x0", "1": "z0", "2": "z1"}}}, + {"command": "model.set.create", "params": {"name": "all3", "geometry": [{"body": "Cylinder3"}]}}, + {"command": "model.set.create", "params": {"name": "x03", "geometry": [{"body": "Cylinder3", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "y03", "geometry": [{"body": "Cylinder3", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "z03", "geometry": [{"body": "Cylinder3", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "z13", "geometry": [{"body": "Cylinder3", "face": "z1"}]}}, + {"command": "model.set.create", "params": {"name": "bore3", "geometry": [{"body": "Cylinder3", "face": "bore"}]}}, + {"command": "model.material.create", "params": {"name": "Mat3", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.4999}}}, + {"command": "model.section.create", "params": {"name": "s3", "set": "all3", "material": "Mat3", "kind": {"type": "solid"}, "formulation": {"incompatible_modes": true}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sx3", "type": "symmetry", "target": {"set": "x03"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sy3", "type": "symmetry", "target": {"set": "y03"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sza3", "type": "symmetry", "target": {"set": "z03"}, "plane": "z"}}, + {"command": "model.boundary_condition.create", "params": {"name": "szb3", "type": "symmetry", "target": {"set": "z13"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "p3", "type": "pressure", "set": "bore3", "magnitude": 1.0}}, + {"command": "model.step.create", "params": {"name": "Pressure", "procedure": {"type": "static"}, "boundary_conditions": ["sx1", "sy1", "sza1", "szb1", "sx2", "sy2", "sza2", "szb2", "sx3", "sy3", "sza3", "szb3"], "loads": ["p1", "p2", "p3"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/mh-twisted-beam/README.md b/tests/oracle/mh-twisted-beam/README.md new file mode 100644 index 0000000..8423baa --- /dev/null +++ b/tests/oracle/mh-twisted-beam/README.md @@ -0,0 +1,3 @@ +# MacNeal–Harder twisted beam + +Length 12, width 1.1, depth 0.32, twisted 90° root to tip, E = 29 × 10⁶, ν = 0.22, mesh 12 × 2, unit tip forces. diff --git a/tests/oracle/mh-twisted-beam/expected.json b/tests/oracle/mh-twisted-beam/expected.json new file mode 100644 index 0000000..9492348 --- /dev/null +++ b/tests/oracle/mh-twisted-beam/expected.json @@ -0,0 +1,19 @@ +{ + "title": "MacNeal–Harder twisted beam", + "kind": "benchmark", + "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. 6 (p. 8), Table 3 (p. 10)", + "status": "pending", + "requires": "shell elements (M8) or mapped blocks with helical edges", + "quantities": [ + { + "id": "in_plane", + "quantity": "tip displacement along the in-plane load", + "value": 0.005424 + }, + { + "id": "out_of_plane", + "quantity": "tip displacement along the out-of-plane load", + "value": 0.001754 + } + ] +} diff --git a/tests/oracle/nafems-fv32/README.md b/tests/oracle/nafems-fv32/README.md new file mode 100644 index 0000000..d367b0f --- /dev/null +++ b/tests/oracle/nafems-fv32/README.md @@ -0,0 +1,6 @@ +# NAFEMS FV32 cantilevered tapered membrane, free vibration (hex20 plane stress layer) + +Our model of the published problem: a membrane 10 m long tapering symmetrically from 5 m at the +clamped edge to 1 m at the free end, 0.05 m thick, E = 200 GPa, ν = 0.3, ρ = 8000 kg/m³; +u_x = u_y = 0 on the root. In-plane (plane stress) vibration is modelled by half the thickness of +hex20 elements (16 × 8 × 1) with u_z = 0 on the mid-surface and the other face free. diff --git a/tests/oracle/nafems-fv32/expected.json b/tests/oracle/nafems-fv32/expected.json new file mode 100644 index 0000000..803556a --- /dev/null +++ b/tests/oracle/nafems-fv32/expected.json @@ -0,0 +1,85 @@ +{ + "title": "NAFEMS FV32 cantilevered tapered membrane, free vibration (hex20 plane stress layer)", + "kind": "benchmark", + "source": "NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test FV32 (frequencies 44.623, 130.03, 162.70, 246.05, 379.90, 391.44 Hz); as reproduced in the Abaqus 2017 Benchmarks Guide, 'FV32: Cantilevered tapered membrane'", + "quantities": [ + { + "id": "f1", + "quantity": "frequency of mode 1 (Hz)", + "measure": { + "from": "modes", + "pointer": "/modes/0/frequency" + }, + "value": 44.623, + "tolerance": { + "relative": 0.01 + }, + "reason": "targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%" + }, + { + "id": "f2", + "quantity": "frequency of mode 2 (Hz)", + "measure": { + "from": "modes", + "pointer": "/modes/1/frequency" + }, + "value": 130.03, + "tolerance": { + "relative": 0.01 + }, + "reason": "targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%" + }, + { + "id": "f3", + "quantity": "frequency of mode 3 (Hz)", + "measure": { + "from": "modes", + "pointer": "/modes/2/frequency" + }, + "value": 162.7, + "tolerance": { + "relative": 0.01 + }, + "reason": "targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%" + }, + { + "id": "f4", + "quantity": "frequency of mode 4 (Hz)", + "measure": { + "from": "modes", + "pointer": "/modes/3/frequency" + }, + "value": 246.05, + "tolerance": { + "relative": 0.01 + }, + "reason": "targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%" + }, + { + "id": "f5", + "quantity": "frequency of mode 5 (Hz)", + "measure": { + "from": "modes", + "pointer": "/modes/4/frequency" + }, + "value": 379.9, + "tolerance": { + "relative": 0.01 + }, + "reason": "targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%" + }, + { + "id": "f6", + "quantity": "frequency of mode 6 (Hz)", + "measure": { + "from": "modes", + "pointer": "/modes/5/frequency" + }, + "value": 391.44, + "tolerance": { + "relative": 0.01 + }, + "reason": "targets published to five figures for 2D plane stress; a thin solid layer with a free face differs from plane stress by O((t/L)²) and the 16 × 8 quadratic mesh is converged to about 0.1%" + } + ] +} diff --git a/tests/oracle/nafems-fv32/fixture.json b/tests/oracle/nafems-fv32/fixture.json new file mode 100644 index 0000000..19411e3 --- /dev/null +++ b/tests/oracle/nafems-fv32/fixture.json @@ -0,0 +1,29 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "f1": { + "band": 4.4623e-8, + "value": 44.62290209745542 + }, + "f2": { + "band": 1.3003463312585388e-7, + "value": 130.03463312585387 + }, + "f3": { + "band": 1.627e-7, + "value": 162.69825051149476 + }, + "f4": { + "band": 2.460539171824971e-7, + "value": 246.0539171824971 + }, + "f5": { + "band": 3.7990069011853704e-7, + "value": 379.900690118537 + }, + "f6": { + "band": 3.9144e-7, + "value": 391.4352970715355 + } + } +} diff --git a/tests/oracle/nafems-fv32/recipe.json b/tests/oracle/nafems-fv32/recipe.json new file mode 100644 index 0000000..4b8b551 --- /dev/null +++ b/tests/oracle/nafems-fv32/recipe.json @@ -0,0 +1,12 @@ +[ + {"command": "mesh.block", "params": {"body": "Membrane", "kind": "hex20", "corners": [[0, 0, 0], [10, 2, 0], [10, 3, 0], [0, 5, 0], [0, 0, 0.025], [10, 2, 0.025], [10, 3, 0.025], [0, 5, 0.025]], "divisions": [16, 8, 1], "faces": {"6": "root", "1": "mid"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Membrane"}]}}, + {"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Membrane", "face": "root"}]}}, + {"command": "model.set.create", "params": {"name": "mid", "geometry": [{"body": "Membrane", "face": "mid"}]}}, + {"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 200000000000.0, "poisson": 0.3}, "density": 8000.0}}, + {"command": "model.section.create", "params": {"name": "membrane", "set": "all", "material": "Steel", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "root", "type": "displacement", "target": {"set": "root"}, "values": [0, 0, null, null, null, null]}}, + {"command": "model.boundary_condition.create", "params": {"name": "mid", "type": "symmetry", "target": {"set": "mid"}, "plane": "z"}}, + {"command": "model.step.create", "params": {"name": "Modes", "procedure": {"type": "frequency", "modes": 6}, "boundary_conditions": ["root", "mid"], "loads": []}}, + {"command": "solve.frequency", "params": {"step": "Modes"}, "id": "modes"} +] diff --git a/tests/oracle/nafems-le1-plane-stress/README.md b/tests/oracle/nafems-le1-plane-stress/README.md new file mode 100644 index 0000000..75f2bdb --- /dev/null +++ b/tests/oracle/nafems-le1-plane-stress/README.md @@ -0,0 +1,3 @@ +# NAFEMS LE1 elliptic membrane with 2D plane stress elements + +The membrane of `nafems-le1` meshed with plane stress quadrilaterals. diff --git a/tests/oracle/nafems-le1-plane-stress/expected.json b/tests/oracle/nafems-le1-plane-stress/expected.json new file mode 100644 index 0000000..9db2e1d --- /dev/null +++ b/tests/oracle/nafems-le1-plane-stress/expected.json @@ -0,0 +1,14 @@ +{ + "title": "NAFEMS LE1 elliptic membrane with 2D plane stress elements", + "kind": "benchmark", + "source": "NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE1", + "status": "pending", + "requires": "2D plane stress elements (docs/elements.md)", + "quantities": [ + { + "id": "syy_D", + "quantity": "σ_yy at D (2, 0)", + "value": 92.7 + } + ] +} diff --git a/tests/oracle/nafems-le1/README.md b/tests/oracle/nafems-le1/README.md new file mode 100644 index 0000000..0743eb7 --- /dev/null +++ b/tests/oracle/nafems-le1/README.md @@ -0,0 +1,8 @@ +# NAFEMS LE1 elliptic membrane (hex20 plane stress layer) + +Our model of the published problem: the quarter membrane between the ellipses (x/2)² + y² = 1 and +(x/3.25)² + (y/2.75)² = 1, thickness 0.1, E = 210 GPa, ν = 0.3 (MPa and m), u_x = 0 on AB (x = 0), +u_y = 0 on CD (y = 0), an outward pressure of 10 MPa on the outer edge BC. Plane stress is +modelled by half the thickness of hex20 elements with a symmetry plane at the mid-surface and the +other face free: 24 × 48 elements graded 4 : 1 towards the hole, converged at D (32 × 64 with +the same grading changes σ_yy there by 0.003%). diff --git a/tests/oracle/nafems-le1/expected.json b/tests/oracle/nafems-le1/expected.json new file mode 100644 index 0000000..674a9b6 --- /dev/null +++ b/tests/oracle/nafems-le1/expected.json @@ -0,0 +1,30 @@ +{ + "title": "NAFEMS LE1 elliptic membrane (hex20 plane stress layer)", + "kind": "benchmark", + "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'", + "quantities": [ + { + "id": "syy_D", + "quantity": "σ_yy at D (2, 0)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 2, + 0, + 0 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": 92.7, + "tolerance": { + "relative": 0.01 + }, + "reason": "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" + } + ] +} diff --git a/tests/oracle/nafems-le1/fixture.json b/tests/oracle/nafems-le1/fixture.json new file mode 100644 index 0000000..34db2ba --- /dev/null +++ b/tests/oracle/nafems-le1/fixture.json @@ -0,0 +1,9 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "syy_D": { + "band": 9.286850287265874e-8, + "value": 92.86850287265874 + } + } +} diff --git a/tests/oracle/nafems-le1/recipe.json b/tests/oracle/nafems-le1/recipe.json new file mode 100644 index 0000000..3abec16 --- /dev/null +++ b/tests/oracle/nafems-le1/recipe.json @@ -0,0 +1,16 @@ +[ + {"command": "mesh.block", "params": {"body": "Membrane", "kind": "hex20", "corners": [[2, 0, 0], [3.25, 0, 0], [0, 2.75, 0], [0, 1, 0], [2, 0, 0.05], [3.25, 0, 0.05], [0, 2.75, 0.05], [0, 1, 0.05]], "divisions": [24, 48, 1], "bias": [4, 1, 1], "edges": [{"corners": [1, 4], "ellipse": {"center": [0, 0, 0], "axes": [[2, 0, 0], [0, 1, 0]]}}, {"corners": [2, 3], "ellipse": {"center": [0, 0, 0], "axes": [[3.25, 0, 0], [0, 2.75, 0]]}}, {"corners": [5, 8], "ellipse": {"center": [0, 0, 0.05], "axes": [[2, 0, 0], [0, 1, 0]]}}, {"corners": [6, 7], "ellipse": {"center": [0, 0, 0.05], "axes": [[3.25, 0, 0], [0, 2.75, 0]]}}], "faces": {"5": "AB", "3": "CD", "4": "BC", "1": "mid"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Membrane"}]}}, + {"command": "model.set.create", "params": {"name": "AB", "geometry": [{"body": "Membrane", "face": "AB"}]}}, + {"command": "model.set.create", "params": {"name": "CD", "geometry": [{"body": "Membrane", "face": "CD"}]}}, + {"command": "model.set.create", "params": {"name": "BC", "geometry": [{"body": "Membrane", "face": "BC"}]}}, + {"command": "model.set.create", "params": {"name": "mid", "geometry": [{"body": "Membrane", "face": "mid"}]}}, + {"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000.0, "poisson": 0.3}}}, + {"command": "model.section.create", "params": {"name": "membrane", "set": "all", "material": "Steel", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "ab", "type": "symmetry", "target": {"set": "AB"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "cd", "type": "symmetry", "target": {"set": "CD"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "mid", "type": "symmetry", "target": {"set": "mid"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "p", "type": "pressure", "set": "BC", "magnitude": -10.0}}, + {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["ab", "cd", "mid"], "loads": ["p"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/nafems-le10/README.md b/tests/oracle/nafems-le10/README.md new file mode 100644 index 0000000..28b32b5 --- /dev/null +++ b/tests/oracle/nafems-le10/README.md @@ -0,0 +1,6 @@ +# NAFEMS LE10 thick plate under pressure (hex20) + +Our model of the published problem: the elliptic plate of LE1 (inner (x/2)² + y² = 1, outer +(x/3.25)² + (y/2.75)² = 1), 0.6 thick, E = 210 GPa, ν = 0.3, 1 MPa pressure on the upper face; +u_y = 0 on y = 0, u_x = 0 on x = 0, u_x = u_y = 0 on the outer curved face, u_z = 0 on the line +where the outer face meets the mid-surface. Two layers of hex20 blocks (8 × 16 × 3 each). diff --git a/tests/oracle/nafems-le10/expected.json b/tests/oracle/nafems-le10/expected.json new file mode 100644 index 0000000..280f6af --- /dev/null +++ b/tests/oracle/nafems-le10/expected.json @@ -0,0 +1,30 @@ +{ + "title": "NAFEMS LE10 thick plate under pressure (hex20)", + "kind": "benchmark", + "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'", + "quantities": [ + { + "id": "syy_D", + "quantity": "σ_yy at D (2, 0, 0.3) on the loaded face", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 2, + 0, + 0.3 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": -5.38, + "tolerance": { + "relative": 0.02 + }, + "reason": "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)" + } + ] +} diff --git a/tests/oracle/nafems-le10/fixture.json b/tests/oracle/nafems-le10/fixture.json new file mode 100644 index 0000000..af70be2 --- /dev/null +++ b/tests/oracle/nafems-le10/fixture.json @@ -0,0 +1,9 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "syy_D": { + "band": 5.38e-9, + "value": -5.333890516372018 + } + } +} diff --git a/tests/oracle/nafems-le10/recipe.json b/tests/oracle/nafems-le10/recipe.json new file mode 100644 index 0000000..6fbd6bc --- /dev/null +++ b/tests/oracle/nafems-le10/recipe.json @@ -0,0 +1,19 @@ +[ + {"command": "mesh.block", "params": {"body": "Plate", "kind": "hex20", "corners": [[2, 0, -0.3], [3.25, 0, -0.3], [0, 2.75, -0.3], [0, 1, -0.3], [2, 0, 0.0], [3.25, 0, 0.0], [0, 2.75, 0.0], [0, 1, 0.0]], "divisions": [8, 16, 3], "edges": [{"corners": [1, 4], "ellipse": {"center": [0, 0, -0.3], "axes": [[2, 0, 0], [0, 1, 0]]}}, {"corners": [2, 3], "ellipse": {"center": [0, 0, -0.3], "axes": [[3.25, 0, 0], [0, 2.75, 0]]}}, {"corners": [5, 8], "ellipse": {"center": [0, 0, 0.0], "axes": [[2, 0, 0], [0, 1, 0]]}}, {"corners": [6, 7], "ellipse": {"center": [0, 0, 0.0], "axes": [[3.25, 0, 0], [0, 2.75, 0]]}, "name": "EE"}], "faces": {"5": "AB", "3": "CD", "4": "BC"}}}, + {"command": "mesh.block", "params": {"body": "Plate", "kind": "hex20", "corners": [[2, 0, 0.0], [3.25, 0, 0.0], [0, 2.75, 0.0], [0, 1, 0.0], [2, 0, 0.3], [3.25, 0, 0.3], [0, 2.75, 0.3], [0, 1, 0.3]], "divisions": [8, 16, 3], "edges": [{"corners": [1, 4], "ellipse": {"center": [0, 0, 0.0], "axes": [[2, 0, 0], [0, 1, 0]]}}, {"corners": [2, 3], "ellipse": {"center": [0, 0, 0.0], "axes": [[3.25, 0, 0], [0, 2.75, 0]]}}, {"corners": [5, 8], "ellipse": {"center": [0, 0, 0.3], "axes": [[2, 0, 0], [0, 1, 0]]}}, {"corners": [6, 7], "ellipse": {"center": [0, 0, 0.3], "axes": [[3.25, 0, 0], [0, 2.75, 0]]}}], "faces": {"5": "AB", "3": "CD", "4": "BC", "2": "top"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Plate"}]}}, + {"command": "model.set.create", "params": {"name": "AB", "geometry": [{"body": "Plate", "face": "AB"}]}}, + {"command": "model.set.create", "params": {"name": "CD", "geometry": [{"body": "Plate", "face": "CD"}]}}, + {"command": "model.set.create", "params": {"name": "BC", "geometry": [{"body": "Plate", "face": "BC"}]}}, + {"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Plate", "face": "top"}]}}, + {"command": "model.set.create", "params": {"name": "EE", "geometry": [{"body": "Plate", "edge": "EE"}]}}, + {"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000.0, "poisson": 0.3}}}, + {"command": "model.section.create", "params": {"name": "plate", "set": "all", "material": "Steel", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "ab", "type": "symmetry", "target": {"set": "AB"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "cd", "type": "symmetry", "target": {"set": "CD"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "bc", "type": "displacement", "target": {"set": "BC"}, "values": [0, 0, null, null, null, null]}}, + {"command": "model.boundary_condition.create", "params": {"name": "ee", "type": "displacement", "target": {"set": "EE"}, "values": [null, null, 0, null, null, null]}}, + {"command": "model.load.create", "params": {"name": "p", "type": "pressure", "set": "top", "magnitude": 1.0}}, + {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["ab", "cd", "bc", "ee"], "loads": ["p"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/nafems-le11/README.md b/tests/oracle/nafems-le11/README.md new file mode 100644 index 0000000..9d00ce6 --- /dev/null +++ b/tests/oracle/nafems-le11/README.md @@ -0,0 +1,3 @@ +# NAFEMS LE11 solid cylinder/taper/sphere under a temperature field + +Thermal expansion under a linear temperature field; needs temperature loads. diff --git a/tests/oracle/nafems-le11/expected.json b/tests/oracle/nafems-le11/expected.json new file mode 100644 index 0000000..f4afbf6 --- /dev/null +++ b/tests/oracle/nafems-le11/expected.json @@ -0,0 +1,14 @@ +{ + "title": "NAFEMS LE11 solid cylinder/taper/sphere under a temperature field", + "kind": "benchmark", + "source": "NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE11 (target σ_zz = −105 MPa at A)", + "status": "pending", + "requires": "thermal loads (M8 heat transfer)", + "quantities": [ + { + "id": "szz_A", + "quantity": "σ_zz at A", + "value": -105.0 + } + ] +} diff --git a/tests/oracle/patch-hex20-reduced/README.md b/tests/oracle/patch-hex20-reduced/README.md new file mode 100644 index 0000000..1f75304 --- /dev/null +++ b/tests/oracle/patch-hex20-reduced/README.md @@ -0,0 +1,8 @@ +# Constant-stress patch test, hex20-reduced + +A unit cube of eight hex20-reduced cells whose shared points are moved off the regular grid (every +face, edge and centre point; the corners stay), so every element is distorted (one mesh.block per cell). Symmetry on the +faces x = 0, y = 0 and z = 0; tractions 1, 2 and 3 on the faces x = 1, y = 1 and z = 1 +(E = 1000, ν = 0.25). The exact solution is the uniform stress diag(1, 2, 3) with strains +ε_i = (σ_i − ν(σ_j + σ_k))/E, which every conforming element of a convergent formulation +reproduces exactly on any mesh. diff --git a/tests/oracle/patch-hex20-reduced/expected.json b/tests/oracle/patch-hex20-reduced/expected.json new file mode 100644 index 0000000..b3eed83 --- /dev/null +++ b/tests/oracle/patch-hex20-reduced/expected.json @@ -0,0 +1,168 @@ +{ + "title": "Constant-stress patch test, hex20-reduced", + "kind": "theory", + "source": "Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7", + "quantities": [ + { + "id": "ux", + "quantity": "u_x at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": -0.00025, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uy", + "quantity": "u_y at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/1" + }, + "value": 0.001, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uz", + "quantity": "u_z at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": 0.00225, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxx", + "quantity": "σ_xx at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/0" + }, + "value": 1.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "syy", + "quantity": "σ_yy at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": 2.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "szz", + "quantity": "σ_zz at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/2" + }, + "value": 3.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxy", + "quantity": "σ_xy at the inner node", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/3" + }, + "value": 0.0, + "tolerance": { + "absolute": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)" + } + ] +} diff --git a/tests/oracle/patch-hex20-reduced/fixture.json b/tests/oracle/patch-hex20-reduced/fixture.json new file mode 100644 index 0000000..c471fcc --- /dev/null +++ b/tests/oracle/patch-hex20-reduced/fixture.json @@ -0,0 +1,33 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "sxx": { + "band": 1.0000000000000003e-9, + "value": 1.0000000000000002 + }, + "sxy": { + "band": 1.000000000000374e-12, + "value": -3.738573156315291e-16 + }, + "syy": { + "band": 2.0000000000000026e-9, + "value": 2.0000000000000027 + }, + "szz": { + "band": 3.000000000000002e-9, + "value": 3.0000000000000018 + }, + "ux": { + "band": 2.5000000000000005e-13, + "value": -0.0002499999999999936 + }, + "uy": { + "band": 1.0000000000000002e-12, + "value": 0.0009999999999999957 + }, + "uz": { + "band": 2.25e-12, + "value": 0.0022499999999999964 + } + } +} diff --git a/tests/oracle/patch-hex20-reduced/recipe.json b/tests/oracle/patch-hex20-reduced/recipe.json new file mode 100644 index 0000000..0a6a2f4 --- /dev/null +++ b/tests/oracle/patch-hex20-reduced/recipe.json @@ -0,0 +1,27 @@ +[ + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.0, 0.0, 0.0], [0.46, 0.0, 0.0], [0.55, 0.47, 0.0], [0.0, 0.46, 0.0], [0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.46, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.55, 0.0], [0.55, 0.47, 0.0], [0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.0, 0.46, 0.0], [0.55, 0.47, 0.0], [0.56, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.55, 0.47, 0.0], [1.0, 0.55, 0.0], [1.0, 1.0, 0.0], [0.56, 1.0, 0.0], [0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54], [0.0, 0.0, 1.0], [0.55, 0.0, 1.0], [0.56, 0.46, 1.0], [0.0, 0.55, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53], [0.55, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 0.47, 1.0], [0.56, 0.46, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45], [0.0, 0.55, 1.0], [0.56, 0.46, 1.0], [0.46, 1.0, 1.0], [0.0, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54], [0.56, 0.46, 1.0], [1.0, 0.47, 1.0], [1.0, 1.0, 1.0], [0.46, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "2": "z1"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}}, + {"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}}, + {"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "y1", "geometry": [{"body": "Cube", "face": "y1"}]}}, + {"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Cube", "face": "z1"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.25}}}, + {"command": "model.section.create", "params": {"name": "solid", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_x", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_y", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_z", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "tx", "type": "traction", "set": "x1", "vector": [1.0, 0, 0]}}, + {"command": "model.load.create", "params": {"name": "ty", "type": "traction", "set": "y1", "vector": [0, 2.0, 0]}}, + {"command": "model.load.create", "params": {"name": "tz", "type": "traction", "set": "z1", "vector": [0, 0, 3.0]}}, + {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["sym_x", "sym_y", "sym_z"], "loads": ["tx", "ty", "tz"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/patch-hex20/README.md b/tests/oracle/patch-hex20/README.md new file mode 100644 index 0000000..880497e --- /dev/null +++ b/tests/oracle/patch-hex20/README.md @@ -0,0 +1,8 @@ +# Constant-stress patch test, hex20 + +A unit cube of eight hex20 cells whose shared points are moved off the regular grid (every +face, edge and centre point; the corners stay), so every element is distorted (one mesh.block per cell). Symmetry on the +faces x = 0, y = 0 and z = 0; tractions 1, 2 and 3 on the faces x = 1, y = 1 and z = 1 +(E = 1000, ν = 0.25). The exact solution is the uniform stress diag(1, 2, 3) with strains +ε_i = (σ_i − ν(σ_j + σ_k))/E, which every conforming element of a convergent formulation +reproduces exactly on any mesh. diff --git a/tests/oracle/patch-hex20/expected.json b/tests/oracle/patch-hex20/expected.json new file mode 100644 index 0000000..c0ff625 --- /dev/null +++ b/tests/oracle/patch-hex20/expected.json @@ -0,0 +1,168 @@ +{ + "title": "Constant-stress patch test, hex20", + "kind": "theory", + "source": "Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7", + "quantities": [ + { + "id": "ux", + "quantity": "u_x at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": -0.00025, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uy", + "quantity": "u_y at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/1" + }, + "value": 0.001, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uz", + "quantity": "u_z at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": 0.00225, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxx", + "quantity": "σ_xx at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/0" + }, + "value": 1.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "syy", + "quantity": "σ_yy at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": 2.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "szz", + "quantity": "σ_zz at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/2" + }, + "value": 3.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxy", + "quantity": "σ_xy at the inner node", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/3" + }, + "value": 0.0, + "tolerance": { + "absolute": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)" + } + ] +} diff --git a/tests/oracle/patch-hex20/fixture.json b/tests/oracle/patch-hex20/fixture.json new file mode 100644 index 0000000..ea11715 --- /dev/null +++ b/tests/oracle/patch-hex20/fixture.json @@ -0,0 +1,33 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "sxx": { + "band": 1.0000000000000034e-9, + "value": 1.0000000000000033 + }, + "sxy": { + "band": 1.0000000000005828e-12, + "value": 5.826462044860843e-16 + }, + "syy": { + "band": 2.0000000000000026e-9, + "value": 2.0000000000000027 + }, + "szz": { + "band": 3.0000000000000025e-9, + "value": 3.000000000000002 + }, + "ux": { + "band": 2.500000000000003e-13, + "value": -0.0002500000000000003 + }, + "uy": { + "band": 1.000000000000001e-12, + "value": 0.0010000000000000009 + }, + "uz": { + "band": 2.2500000000000027e-12, + "value": 0.0022500000000000024 + } + } +} diff --git a/tests/oracle/patch-hex20/recipe.json b/tests/oracle/patch-hex20/recipe.json new file mode 100644 index 0000000..f3c975d --- /dev/null +++ b/tests/oracle/patch-hex20/recipe.json @@ -0,0 +1,27 @@ +[ + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.0, 0.0, 0.0], [0.46, 0.0, 0.0], [0.55, 0.47, 0.0], [0.0, 0.46, 0.0], [0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.46, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.55, 0.0], [0.55, 0.47, 0.0], [0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.0, 0.46, 0.0], [0.55, 0.47, 0.0], [0.56, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.55, 0.47, 0.0], [1.0, 0.55, 0.0], [1.0, 1.0, 0.0], [0.56, 1.0, 0.0], [0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54], [0.0, 0.0, 1.0], [0.55, 0.0, 1.0], [0.56, 0.46, 1.0], [0.0, 0.55, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53], [0.55, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 0.47, 1.0], [0.56, 0.46, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45], [0.0, 0.55, 1.0], [0.56, 0.46, 1.0], [0.46, 1.0, 1.0], [0.0, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex20", "corners": [[0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54], [0.56, 0.46, 1.0], [1.0, 0.47, 1.0], [1.0, 1.0, 1.0], [0.46, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "2": "z1"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}}, + {"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}}, + {"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "y1", "geometry": [{"body": "Cube", "face": "y1"}]}}, + {"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Cube", "face": "z1"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.25}}}, + {"command": "model.section.create", "params": {"name": "solid", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_x", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_y", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_z", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "tx", "type": "traction", "set": "x1", "vector": [1.0, 0, 0]}}, + {"command": "model.load.create", "params": {"name": "ty", "type": "traction", "set": "y1", "vector": [0, 2.0, 0]}}, + {"command": "model.load.create", "params": {"name": "tz", "type": "traction", "set": "z1", "vector": [0, 0, 3.0]}}, + {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["sym_x", "sym_y", "sym_z"], "loads": ["tx", "ty", "tz"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/patch-hex8-incompatible/README.md b/tests/oracle/patch-hex8-incompatible/README.md new file mode 100644 index 0000000..c367b73 --- /dev/null +++ b/tests/oracle/patch-hex8-incompatible/README.md @@ -0,0 +1,8 @@ +# Constant-stress patch test, hex8-incompatible + +A unit cube of eight hex8-incompatible cells whose shared points are moved off the regular grid (every +face, edge and centre point; the corners stay), so every element is distorted (one mesh.block per cell). Symmetry on the +faces x = 0, y = 0 and z = 0; tractions 1, 2 and 3 on the faces x = 1, y = 1 and z = 1 +(E = 1000, ν = 0.25). The exact solution is the uniform stress diag(1, 2, 3) with strains +ε_i = (σ_i − ν(σ_j + σ_k))/E, which every conforming element of a convergent formulation +reproduces exactly on any mesh. diff --git a/tests/oracle/patch-hex8-incompatible/expected.json b/tests/oracle/patch-hex8-incompatible/expected.json new file mode 100644 index 0000000..fe6652b --- /dev/null +++ b/tests/oracle/patch-hex8-incompatible/expected.json @@ -0,0 +1,168 @@ +{ + "title": "Constant-stress patch test, hex8-incompatible", + "kind": "theory", + "source": "Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7", + "quantities": [ + { + "id": "ux", + "quantity": "u_x at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": -0.00025, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uy", + "quantity": "u_y at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/1" + }, + "value": 0.001, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uz", + "quantity": "u_z at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": 0.00225, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxx", + "quantity": "σ_xx at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/0" + }, + "value": 1.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "syy", + "quantity": "σ_yy at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": 2.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "szz", + "quantity": "σ_zz at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/2" + }, + "value": 3.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxy", + "quantity": "σ_xy at the inner node", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/3" + }, + "value": 0.0, + "tolerance": { + "absolute": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)" + } + ] +} diff --git a/tests/oracle/patch-hex8-incompatible/fixture.json b/tests/oracle/patch-hex8-incompatible/fixture.json new file mode 100644 index 0000000..c4104f1 --- /dev/null +++ b/tests/oracle/patch-hex8-incompatible/fixture.json @@ -0,0 +1,33 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "sxx": { + "band": 1.0000000000000005e-9, + "value": 1.0000000000000004 + }, + "sxy": { + "band": 1.000000000000215e-12, + "value": -2.1490555370965863e-16 + }, + "syy": { + "band": 2.0000000000000018e-9, + "value": 2.0000000000000018 + }, + "szz": { + "band": 3.0000000000000025e-9, + "value": 3.000000000000002 + }, + "ux": { + "band": 2.5000000000000005e-13, + "value": -0.0002499999999999985 + }, + "uy": { + "band": 1.0000000000000002e-12, + "value": 0.001 + }, + "uz": { + "band": 2.25e-12, + "value": 0.0022499999999999985 + } + } +} diff --git a/tests/oracle/patch-hex8-incompatible/recipe.json b/tests/oracle/patch-hex8-incompatible/recipe.json new file mode 100644 index 0000000..aad0a01 --- /dev/null +++ b/tests/oracle/patch-hex8-incompatible/recipe.json @@ -0,0 +1,27 @@ +[ + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.0, 0.0, 0.0], [0.46, 0.0, 0.0], [0.55, 0.47, 0.0], [0.0, 0.46, 0.0], [0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.46, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.55, 0.0], [0.55, 0.47, 0.0], [0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.0, 0.46, 0.0], [0.55, 0.47, 0.0], [0.56, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.55, 0.47, 0.0], [1.0, 0.55, 0.0], [1.0, 1.0, 0.0], [0.56, 1.0, 0.0], [0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54], [0.0, 0.0, 1.0], [0.55, 0.0, 1.0], [0.56, 0.46, 1.0], [0.0, 0.55, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53], [0.55, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 0.47, 1.0], [0.56, 0.46, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45], [0.0, 0.55, 1.0], [0.56, 0.46, 1.0], [0.46, 1.0, 1.0], [0.0, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54], [0.56, 0.46, 1.0], [1.0, 0.47, 1.0], [1.0, 1.0, 1.0], [0.46, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "2": "z1"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}}, + {"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}}, + {"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "y1", "geometry": [{"body": "Cube", "face": "y1"}]}}, + {"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Cube", "face": "z1"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.25}}}, + {"command": "model.section.create", "params": {"name": "solid", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"incompatible_modes": true}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_x", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_y", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_z", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "tx", "type": "traction", "set": "x1", "vector": [1.0, 0, 0]}}, + {"command": "model.load.create", "params": {"name": "ty", "type": "traction", "set": "y1", "vector": [0, 2.0, 0]}}, + {"command": "model.load.create", "params": {"name": "tz", "type": "traction", "set": "z1", "vector": [0, 0, 3.0]}}, + {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["sym_x", "sym_y", "sym_z"], "loads": ["tx", "ty", "tz"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/patch-hex8/README.md b/tests/oracle/patch-hex8/README.md new file mode 100644 index 0000000..e176a6c --- /dev/null +++ b/tests/oracle/patch-hex8/README.md @@ -0,0 +1,8 @@ +# Constant-stress patch test, hex8 + +A unit cube of eight hex8 cells whose shared points are moved off the regular grid (every +face, edge and centre point; the corners stay), so every element is distorted (one mesh.block per cell). Symmetry on the +faces x = 0, y = 0 and z = 0; tractions 1, 2 and 3 on the faces x = 1, y = 1 and z = 1 +(E = 1000, ν = 0.25). The exact solution is the uniform stress diag(1, 2, 3) with strains +ε_i = (σ_i − ν(σ_j + σ_k))/E, which every conforming element of a convergent formulation +reproduces exactly on any mesh. diff --git a/tests/oracle/patch-hex8/expected.json b/tests/oracle/patch-hex8/expected.json new file mode 100644 index 0000000..3d90c6d --- /dev/null +++ b/tests/oracle/patch-hex8/expected.json @@ -0,0 +1,168 @@ +{ + "title": "Constant-stress patch test, hex8", + "kind": "theory", + "source": "Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7", + "quantities": [ + { + "id": "ux", + "quantity": "u_x at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": -0.00025, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uy", + "quantity": "u_y at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/1" + }, + "value": 0.001, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uz", + "quantity": "u_z at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": 0.00225, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxx", + "quantity": "σ_xx at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/0" + }, + "value": 1.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "syy", + "quantity": "σ_yy at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": 2.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "szz", + "quantity": "σ_zz at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/2" + }, + "value": 3.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxy", + "quantity": "σ_xy at the inner node", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/3" + }, + "value": 0.0, + "tolerance": { + "absolute": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)" + } + ] +} diff --git a/tests/oracle/patch-hex8/fixture.json b/tests/oracle/patch-hex8/fixture.json new file mode 100644 index 0000000..ed0c520 --- /dev/null +++ b/tests/oracle/patch-hex8/fixture.json @@ -0,0 +1,33 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "sxx": { + "band": 1e-9, + "value": 0.9999999999999999 + }, + "sxy": { + "band": 1.0000000000001601e-12, + "value": -1.5987023023967256e-16 + }, + "syy": { + "band": 2.000000000000001e-9, + "value": 2.000000000000001 + }, + "szz": { + "band": 3.0000000000000004e-9, + "value": 2.9999999999999987 + }, + "ux": { + "band": 2.5000000000000005e-13, + "value": -0.00024999999999999865 + }, + "uy": { + "band": 1.0000000000000002e-12, + "value": 0.0009999999999999998 + }, + "uz": { + "band": 2.25e-12, + "value": 0.0022499999999999977 + } + } +} diff --git a/tests/oracle/patch-hex8/recipe.json b/tests/oracle/patch-hex8/recipe.json new file mode 100644 index 0000000..8473f21 --- /dev/null +++ b/tests/oracle/patch-hex8/recipe.json @@ -0,0 +1,27 @@ +[ + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.0, 0.0, 0.0], [0.46, 0.0, 0.0], [0.55, 0.47, 0.0], [0.0, 0.46, 0.0], [0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.46, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.55, 0.0], [0.55, 0.47, 0.0], [0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.0, 0.46, 0.0], [0.55, 0.47, 0.0], [0.56, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.55, 0.47, 0.0], [1.0, 0.55, 0.0], [1.0, 1.0, 0.0], [0.56, 1.0, 0.0], [0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54], [0.0, 0.0, 1.0], [0.55, 0.0, 1.0], [0.56, 0.46, 1.0], [0.0, 0.55, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53], [0.55, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 0.47, 1.0], [0.56, 0.46, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45], [0.0, 0.55, 1.0], [0.56, 0.46, 1.0], [0.46, 1.0, 1.0], [0.0, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "hex8", "corners": [[0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54], [0.56, 0.46, 1.0], [1.0, 0.47, 1.0], [1.0, 1.0, 1.0], [0.46, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "2": "z1"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}}, + {"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}}, + {"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "y1", "geometry": [{"body": "Cube", "face": "y1"}]}}, + {"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Cube", "face": "z1"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.25}}}, + {"command": "model.section.create", "params": {"name": "solid", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_x", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_y", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_z", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "tx", "type": "traction", "set": "x1", "vector": [1.0, 0, 0]}}, + {"command": "model.load.create", "params": {"name": "ty", "type": "traction", "set": "y1", "vector": [0, 2.0, 0]}}, + {"command": "model.load.create", "params": {"name": "tz", "type": "traction", "set": "z1", "vector": [0, 0, 3.0]}}, + {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["sym_x", "sym_y", "sym_z"], "loads": ["tx", "ty", "tz"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/patch-tet10/README.md b/tests/oracle/patch-tet10/README.md new file mode 100644 index 0000000..1741768 --- /dev/null +++ b/tests/oracle/patch-tet10/README.md @@ -0,0 +1,8 @@ +# Constant-stress patch test, tet10 + +A unit cube of eight tet10 cells whose shared points are moved off the regular grid (every +face, edge and centre point; the corners stay), so every element is distorted (written node by node as Kuhn's six tetrahedra per cell with straight edges). Symmetry on the +faces x = 0, y = 0 and z = 0; tractions 1, 2 and 3 on the faces x = 1, y = 1 and z = 1 +(E = 1000, ν = 0.25). The exact solution is the uniform stress diag(1, 2, 3) with strains +ε_i = (σ_i − ν(σ_j + σ_k))/E, which every conforming element of a convergent formulation +reproduces exactly on any mesh. diff --git a/tests/oracle/patch-tet10/expected.json b/tests/oracle/patch-tet10/expected.json new file mode 100644 index 0000000..652c5cd --- /dev/null +++ b/tests/oracle/patch-tet10/expected.json @@ -0,0 +1,168 @@ +{ + "title": "Constant-stress patch test, tet10", + "kind": "theory", + "source": "Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7", + "quantities": [ + { + "id": "ux", + "quantity": "u_x at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": -0.00025, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uy", + "quantity": "u_y at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/1" + }, + "value": 0.001, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uz", + "quantity": "u_z at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": 0.00225, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxx", + "quantity": "σ_xx at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/0" + }, + "value": 1.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "syy", + "quantity": "σ_yy at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": 2.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "szz", + "quantity": "σ_zz at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/2" + }, + "value": 3.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxy", + "quantity": "σ_xy at the inner node", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/3" + }, + "value": 0.0, + "tolerance": { + "absolute": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)" + } + ] +} diff --git a/tests/oracle/patch-tet10/fixture.json b/tests/oracle/patch-tet10/fixture.json new file mode 100644 index 0000000..9e67f02 --- /dev/null +++ b/tests/oracle/patch-tet10/fixture.json @@ -0,0 +1,33 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "sxx": { + "band": 1e-9, + "value": 0.9999999999999987 + }, + "sxy": { + "band": 1.0000000000027301e-12, + "value": 2.7299366180215153e-15 + }, + "syy": { + "band": 2e-9, + "value": 1.9999999999999964 + }, + "szz": { + "band": 3.0000000000000004e-9, + "value": 2.9999999999999925 + }, + "ux": { + "band": 2.5000000000000005e-13, + "value": -0.000249999999999999 + }, + "uy": { + "band": 1.0000000000000002e-12, + "value": 0.0009999999999999816 + }, + "uz": { + "band": 2.250000000000011e-12, + "value": 0.0022500000000000107 + } + } +} diff --git a/tests/oracle/patch-tet10/recipe.json b/tests/oracle/patch-tet10/recipe.json new file mode 100644 index 0000000..18a9e7d --- /dev/null +++ b/tests/oracle/patch-tet10/recipe.json @@ -0,0 +1,192 @@ +[ + {"command": "mesh.node.add", "params": {"label": 1, "xyz": [0.0, 0.0, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 2, "xyz": [0.46, 0.0, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 3, "xyz": [0.55, 0.47, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 4, "xyz": [0.46, 0.55, 0.53]}}, + {"command": "mesh.node.add", "params": {"label": 5, "xyz": [0.23, 0.0, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 6, "xyz": [0.505, 0.235, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 7, "xyz": [0.275, 0.235, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 8, "xyz": [0.23, 0.275, 0.265]}}, + {"command": "mesh.node.add", "params": {"label": 9, "xyz": [0.46, 0.275, 0.265]}}, + {"command": "mesh.node.add", "params": {"label": 10, "xyz": [0.505, 0.51, 0.265]}}, + {"command": "mesh.node.add", "params": {"label": 11, "xyz": [0.56, 0.0, 0.45]}}, + {"command": "mesh.node.add", "params": {"label": 12, "xyz": [0.28, 0.0, 0.225]}}, + {"command": "mesh.node.add", "params": {"label": 13, "xyz": [0.51, 0.0, 0.225]}}, + {"command": "mesh.node.add", "params": {"label": 14, "xyz": [0.51, 0.275, 0.49]}}, + {"command": "mesh.node.add", "params": {"label": 15, "xyz": [0.0, 0.46, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 16, "xyz": [0.275, 0.46499999999999997, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 17, "xyz": [0.0, 0.23, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 18, "xyz": [0.23, 0.505, 0.265]}}, + {"command": "mesh.node.add", "params": {"label": 19, "xyz": [0.0, 0.47, 0.54]}}, + {"command": "mesh.node.add", "params": {"label": 20, "xyz": [0.0, 0.46499999999999997, 0.27]}}, + {"command": "mesh.node.add", "params": {"label": 21, "xyz": [0.0, 0.235, 0.27]}}, + {"command": "mesh.node.add", "params": {"label": 22, "xyz": [0.23, 0.51, 0.535]}}, + {"command": "mesh.node.add", "params": {"label": 23, "xyz": [0.0, 0.0, 0.53]}}, + {"command": "mesh.node.add", "params": {"label": 24, "xyz": [0.0, 0.0, 0.265]}}, + {"command": "mesh.node.add", "params": {"label": 25, "xyz": [0.28, 0.0, 0.49]}}, + {"command": "mesh.node.add", "params": {"label": 26, "xyz": [0.23, 0.275, 0.53]}}, + {"command": "mesh.node.add", "params": {"label": 27, "xyz": [0.0, 0.235, 0.535]}}, + {"command": "mesh.node.add", "params": {"label": 28, "xyz": [1.0, 0.0, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 29, "xyz": [1.0, 0.55, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 30, "xyz": [1.0, 0.46, 0.45]}}, + {"command": "mesh.node.add", "params": {"label": 31, "xyz": [0.73, 0.0, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 32, "xyz": [1.0, 0.275, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 33, "xyz": [0.73, 0.275, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 34, "xyz": [0.73, 0.23, 0.225]}}, + {"command": "mesh.node.add", "params": {"label": 35, "xyz": [1.0, 0.23, 0.225]}}, + {"command": "mesh.node.add", "params": {"label": 36, "xyz": [1.0, 0.505, 0.225]}}, + {"command": "mesh.node.add", "params": {"label": 37, "xyz": [1.0, 0.0, 0.54]}}, + {"command": "mesh.node.add", "params": {"label": 38, "xyz": [0.73, 0.0, 0.27]}}, + {"command": "mesh.node.add", "params": {"label": 39, "xyz": [1.0, 0.0, 0.27]}}, + {"command": "mesh.node.add", "params": {"label": 40, "xyz": [1.0, 0.23, 0.495]}}, + {"command": "mesh.node.add", "params": {"label": 41, "xyz": [0.775, 0.51, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 42, "xyz": [0.775, 0.46499999999999997, 0.225]}}, + {"command": "mesh.node.add", "params": {"label": 43, "xyz": [0.73, 0.505, 0.49]}}, + {"command": "mesh.node.add", "params": {"label": 44, "xyz": [0.78, 0.0, 0.495]}}, + {"command": "mesh.node.add", "params": {"label": 45, "xyz": [0.78, 0.23, 0.45]}}, + {"command": "mesh.node.add", "params": {"label": 46, "xyz": [0.56, 1.0, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 47, "xyz": [0.55, 1.0, 0.54]}}, + {"command": "mesh.node.add", "params": {"label": 48, "xyz": [0.555, 0.735, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 49, "xyz": [0.28, 0.73, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 50, "xyz": [0.275, 0.73, 0.27]}}, + {"command": "mesh.node.add", "params": {"label": 51, "xyz": [0.55, 0.735, 0.27]}}, + {"command": "mesh.node.add", "params": {"label": 52, "xyz": [0.555, 1.0, 0.27]}}, + {"command": "mesh.node.add", "params": {"label": 53, "xyz": [0.505, 0.775, 0.535]}}, + {"command": "mesh.node.add", "params": {"label": 54, "xyz": [0.0, 1.0, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 55, "xyz": [0.28, 1.0, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 56, "xyz": [0.0, 0.73, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 57, "xyz": [0.275, 1.0, 0.27]}}, + {"command": "mesh.node.add", "params": {"label": 58, "xyz": [0.0, 1.0, 0.45]}}, + {"command": "mesh.node.add", "params": {"label": 59, "xyz": [0.0, 1.0, 0.225]}}, + {"command": "mesh.node.add", "params": {"label": 60, "xyz": [0.0, 0.73, 0.225]}}, + {"command": "mesh.node.add", "params": {"label": 61, "xyz": [0.275, 1.0, 0.495]}}, + {"command": "mesh.node.add", "params": {"label": 62, "xyz": [0.275, 0.735, 0.54]}}, + {"command": "mesh.node.add", "params": {"label": 63, "xyz": [0.0, 0.735, 0.495]}}, + {"command": "mesh.node.add", "params": {"label": 64, "xyz": [1.0, 1.0, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 65, "xyz": [1.0, 1.0, 0.53]}}, + {"command": "mesh.node.add", "params": {"label": 66, "xyz": [1.0, 0.775, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 67, "xyz": [0.775, 0.735, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 68, "xyz": [0.775, 0.735, 0.265]}}, + {"command": "mesh.node.add", "params": {"label": 69, "xyz": [1.0, 0.775, 0.265]}}, + {"command": "mesh.node.add", "params": {"label": 70, "xyz": [1.0, 1.0, 0.265]}}, + {"command": "mesh.node.add", "params": {"label": 71, "xyz": [1.0, 0.73, 0.49]}}, + {"command": "mesh.node.add", "params": {"label": 72, "xyz": [0.78, 1.0, 0.0]}}, + {"command": "mesh.node.add", "params": {"label": 73, "xyz": [0.78, 1.0, 0.265]}}, + {"command": "mesh.node.add", "params": {"label": 74, "xyz": [0.775, 1.0, 0.535]}}, + {"command": "mesh.node.add", "params": {"label": 75, "xyz": [0.73, 0.775, 0.53]}}, + {"command": "mesh.node.add", "params": {"label": 76, "xyz": [0.56, 0.46, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 77, "xyz": [0.28, 0.23, 0.765]}}, + {"command": "mesh.node.add", "params": {"label": 78, "xyz": [0.56, 0.23, 0.725]}}, + {"command": "mesh.node.add", "params": {"label": 79, "xyz": [0.51, 0.505, 0.765]}}, + {"command": "mesh.node.add", "params": {"label": 80, "xyz": [0.55, 0.0, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 81, "xyz": [0.275, 0.0, 0.765]}}, + {"command": "mesh.node.add", "params": {"label": 82, "xyz": [0.555, 0.0, 0.725]}}, + {"command": "mesh.node.add", "params": {"label": 83, "xyz": [0.555, 0.23, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 84, "xyz": [0.28, 0.46499999999999997, 0.77]}}, + {"command": "mesh.node.add", "params": {"label": 85, "xyz": [0.0, 0.55, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 86, "xyz": [0.0, 0.51, 0.77]}}, + {"command": "mesh.node.add", "params": {"label": 87, "xyz": [0.0, 0.275, 0.765]}}, + {"command": "mesh.node.add", "params": {"label": 88, "xyz": [0.28, 0.505, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 89, "xyz": [0.0, 0.0, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 90, "xyz": [0.0, 0.0, 0.765]}}, + {"command": "mesh.node.add", "params": {"label": 91, "xyz": [0.275, 0.0, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 92, "xyz": [0.28, 0.23, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 93, "xyz": [0.0, 0.275, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 94, "xyz": [1.0, 0.47, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 95, "xyz": [0.78, 0.235, 0.725]}}, + {"command": "mesh.node.add", "params": {"label": 96, "xyz": [1.0, 0.235, 0.77]}}, + {"command": "mesh.node.add", "params": {"label": 97, "xyz": [1.0, 0.46499999999999997, 0.725]}}, + {"command": "mesh.node.add", "params": {"label": 98, "xyz": [1.0, 0.0, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 99, "xyz": [0.78, 0.0, 0.725]}}, + {"command": "mesh.node.add", "params": {"label": 100, "xyz": [1.0, 0.0, 0.77]}}, + {"command": "mesh.node.add", "params": {"label": 101, "xyz": [1.0, 0.235, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 102, "xyz": [0.73, 0.51, 0.765]}}, + {"command": "mesh.node.add", "params": {"label": 103, "xyz": [0.78, 0.46499999999999997, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 104, "xyz": [0.775, 0.0, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 105, "xyz": [0.775, 0.235, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 106, "xyz": [0.46, 1.0, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 107, "xyz": [0.23, 0.735, 0.77]}}, + {"command": "mesh.node.add", "params": {"label": 108, "xyz": [0.46, 0.775, 0.765]}}, + {"command": "mesh.node.add", "params": {"label": 109, "xyz": [0.505, 1.0, 0.77]}}, + {"command": "mesh.node.add", "params": {"label": 110, "xyz": [0.51, 0.73, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 111, "xyz": [0.23, 1.0, 0.725]}}, + {"command": "mesh.node.add", "params": {"label": 112, "xyz": [0.0, 1.0, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 113, "xyz": [0.0, 1.0, 0.725]}}, + {"command": "mesh.node.add", "params": {"label": 114, "xyz": [0.0, 0.735, 0.77]}}, + {"command": "mesh.node.add", "params": {"label": 115, "xyz": [0.23, 1.0, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 116, "xyz": [0.23, 0.775, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 117, "xyz": [0.0, 0.775, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 118, "xyz": [1.0, 1.0, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 119, "xyz": [0.73, 0.775, 0.765]}}, + {"command": "mesh.node.add", "params": {"label": 120, "xyz": [1.0, 0.73, 0.725]}}, + {"command": "mesh.node.add", "params": {"label": 121, "xyz": [1.0, 1.0, 0.765]}}, + {"command": "mesh.node.add", "params": {"label": 122, "xyz": [1.0, 0.735, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 123, "xyz": [0.775, 1.0, 0.77]}}, + {"command": "mesh.node.add", "params": {"label": 124, "xyz": [0.73, 1.0, 1.0]}}, + {"command": "mesh.node.add", "params": {"label": 125, "xyz": [0.78, 0.73, 1.0]}}, + {"command": "mesh.element.add", "params": {"label": 1, "kind": "tet10", "nodes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}}, + {"command": "mesh.element.add", "params": {"label": 2, "kind": "tet10", "nodes": [1, 11, 2, 4, 12, 13, 5, 8, 14, 9]}}, + {"command": "mesh.element.add", "params": {"label": 3, "kind": "tet10", "nodes": [1, 3, 15, 4, 7, 16, 17, 8, 10, 18]}}, + {"command": "mesh.element.add", "params": {"label": 4, "kind": "tet10", "nodes": [1, 15, 19, 4, 17, 20, 21, 8, 18, 22]}}, + {"command": "mesh.element.add", "params": {"label": 5, "kind": "tet10", "nodes": [1, 23, 11, 4, 24, 25, 12, 8, 26, 14]}}, + {"command": "mesh.element.add", "params": {"label": 6, "kind": "tet10", "nodes": [1, 19, 23, 4, 21, 27, 24, 8, 22, 26]}}, + {"command": "mesh.element.add", "params": {"label": 7, "kind": "tet10", "nodes": [2, 28, 29, 30, 31, 32, 33, 34, 35, 36]}}, + {"command": "mesh.element.add", "params": {"label": 8, "kind": "tet10", "nodes": [2, 37, 28, 30, 38, 39, 31, 34, 40, 35]}}, + {"command": "mesh.element.add", "params": {"label": 9, "kind": "tet10", "nodes": [2, 29, 3, 30, 33, 41, 6, 34, 36, 42]}}, + {"command": "mesh.element.add", "params": {"label": 10, "kind": "tet10", "nodes": [2, 3, 4, 30, 6, 10, 9, 34, 42, 43]}}, + {"command": "mesh.element.add", "params": {"label": 11, "kind": "tet10", "nodes": [2, 11, 37, 30, 13, 44, 38, 34, 45, 40]}}, + {"command": "mesh.element.add", "params": {"label": 12, "kind": "tet10", "nodes": [2, 4, 11, 30, 9, 14, 13, 34, 43, 45]}}, + {"command": "mesh.element.add", "params": {"label": 13, "kind": "tet10", "nodes": [15, 3, 46, 47, 16, 48, 49, 50, 51, 52]}}, + {"command": "mesh.element.add", "params": {"label": 14, "kind": "tet10", "nodes": [15, 4, 3, 47, 18, 10, 16, 50, 53, 51]}}, + {"command": "mesh.element.add", "params": {"label": 15, "kind": "tet10", "nodes": [15, 46, 54, 47, 49, 55, 56, 50, 52, 57]}}, + {"command": "mesh.element.add", "params": {"label": 16, "kind": "tet10", "nodes": [15, 54, 58, 47, 56, 59, 60, 50, 57, 61]}}, + {"command": "mesh.element.add", "params": {"label": 17, "kind": "tet10", "nodes": [15, 19, 4, 47, 20, 22, 18, 50, 62, 53]}}, + {"command": "mesh.element.add", "params": {"label": 18, "kind": "tet10", "nodes": [15, 58, 19, 47, 60, 63, 20, 50, 61, 62]}}, + {"command": "mesh.element.add", "params": {"label": 19, "kind": "tet10", "nodes": [3, 29, 64, 65, 41, 66, 67, 68, 69, 70]}}, + {"command": "mesh.element.add", "params": {"label": 20, "kind": "tet10", "nodes": [3, 30, 29, 65, 42, 36, 41, 68, 71, 69]}}, + {"command": "mesh.element.add", "params": {"label": 21, "kind": "tet10", "nodes": [3, 64, 46, 65, 67, 72, 48, 68, 70, 73]}}, + {"command": "mesh.element.add", "params": {"label": 22, "kind": "tet10", "nodes": [3, 46, 47, 65, 48, 52, 51, 68, 73, 74]}}, + {"command": "mesh.element.add", "params": {"label": 23, "kind": "tet10", "nodes": [3, 4, 30, 65, 10, 43, 42, 68, 75, 71]}}, + {"command": "mesh.element.add", "params": {"label": 24, "kind": "tet10", "nodes": [3, 47, 4, 65, 51, 53, 10, 68, 74, 75]}}, + {"command": "mesh.element.add", "params": {"label": 25, "kind": "tet10", "nodes": [23, 11, 4, 76, 25, 14, 26, 77, 78, 79]}}, + {"command": "mesh.element.add", "params": {"label": 26, "kind": "tet10", "nodes": [23, 80, 11, 76, 81, 82, 25, 77, 83, 78]}}, + {"command": "mesh.element.add", "params": {"label": 27, "kind": "tet10", "nodes": [23, 4, 19, 76, 26, 22, 27, 77, 79, 84]}}, + {"command": "mesh.element.add", "params": {"label": 28, "kind": "tet10", "nodes": [23, 19, 85, 76, 27, 86, 87, 77, 84, 88]}}, + {"command": "mesh.element.add", "params": {"label": 29, "kind": "tet10", "nodes": [23, 89, 80, 76, 90, 91, 81, 77, 92, 83]}}, + {"command": "mesh.element.add", "params": {"label": 30, "kind": "tet10", "nodes": [23, 85, 89, 76, 87, 93, 90, 77, 88, 92]}}, + {"command": "mesh.element.add", "params": {"label": 31, "kind": "tet10", "nodes": [11, 37, 30, 94, 44, 40, 45, 95, 96, 97]}}, + {"command": "mesh.element.add", "params": {"label": 32, "kind": "tet10", "nodes": [11, 98, 37, 94, 99, 100, 44, 95, 101, 96]}}, + {"command": "mesh.element.add", "params": {"label": 33, "kind": "tet10", "nodes": [11, 30, 4, 94, 45, 43, 14, 95, 97, 102]}}, + {"command": "mesh.element.add", "params": {"label": 34, "kind": "tet10", "nodes": [11, 4, 76, 94, 14, 79, 78, 95, 102, 103]}}, + {"command": "mesh.element.add", "params": {"label": 35, "kind": "tet10", "nodes": [11, 80, 98, 94, 82, 104, 99, 95, 105, 101]}}, + {"command": "mesh.element.add", "params": {"label": 36, "kind": "tet10", "nodes": [11, 76, 80, 94, 78, 83, 82, 95, 103, 105]}}, + {"command": "mesh.element.add", "params": {"label": 37, "kind": "tet10", "nodes": [19, 4, 47, 106, 22, 53, 62, 107, 108, 109]}}, + {"command": "mesh.element.add", "params": {"label": 38, "kind": "tet10", "nodes": [19, 76, 4, 106, 84, 79, 22, 107, 110, 108]}}, + {"command": "mesh.element.add", "params": {"label": 39, "kind": "tet10", "nodes": [19, 47, 58, 106, 62, 61, 63, 107, 109, 111]}}, + {"command": "mesh.element.add", "params": {"label": 40, "kind": "tet10", "nodes": [19, 58, 112, 106, 63, 113, 114, 107, 111, 115]}}, + {"command": "mesh.element.add", "params": {"label": 41, "kind": "tet10", "nodes": [19, 85, 76, 106, 86, 88, 84, 107, 116, 110]}}, + {"command": "mesh.element.add", "params": {"label": 42, "kind": "tet10", "nodes": [19, 112, 85, 106, 114, 117, 86, 107, 115, 116]}}, + {"command": "mesh.element.add", "params": {"label": 43, "kind": "tet10", "nodes": [4, 30, 65, 118, 43, 71, 75, 119, 120, 121]}}, + {"command": "mesh.element.add", "params": {"label": 44, "kind": "tet10", "nodes": [4, 94, 30, 118, 102, 97, 43, 119, 122, 120]}}, + {"command": "mesh.element.add", "params": {"label": 45, "kind": "tet10", "nodes": [4, 65, 47, 118, 75, 74, 53, 119, 121, 123]}}, + {"command": "mesh.element.add", "params": {"label": 46, "kind": "tet10", "nodes": [4, 47, 106, 118, 53, 109, 108, 119, 123, 124]}}, + {"command": "mesh.element.add", "params": {"label": 47, "kind": "tet10", "nodes": [4, 76, 94, 118, 79, 103, 102, 119, 125, 122]}}, + {"command": "mesh.element.add", "params": {"label": 48, "kind": "tet10", "nodes": [4, 106, 76, 118, 108, 110, 79, 119, 124, 125]}}, + {"command": "model.set.create", "params": {"name": "all", "elements": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48]}}, + {"command": "model.set.create", "params": {"name": "x0", "faces": [{"element": 4, "face": 1}, {"element": 6, "face": 1}, {"element": 16, "face": 1}, {"element": 18, "face": 1}, {"element": 28, "face": 1}, {"element": 30, "face": 1}, {"element": 40, "face": 1}, {"element": 42, "face": 1}]}}, + {"command": "model.set.create", "params": {"name": "x1", "faces": [{"element": 7, "face": 3}, {"element": 8, "face": 3}, {"element": 19, "face": 3}, {"element": 20, "face": 3}, {"element": 31, "face": 3}, {"element": 32, "face": 3}, {"element": 43, "face": 3}, {"element": 44, "face": 3}]}}, + {"command": "model.set.create", "params": {"name": "y0", "faces": [{"element": 2, "face": 1}, {"element": 5, "face": 1}, {"element": 8, "face": 1}, {"element": 11, "face": 1}, {"element": 26, "face": 1}, {"element": 29, "face": 1}, {"element": 32, "face": 1}, {"element": 35, "face": 1}]}}, + {"command": "model.set.create", "params": {"name": "y1", "faces": [{"element": 15, "face": 3}, {"element": 16, "face": 3}, {"element": 21, "face": 3}, {"element": 22, "face": 3}, {"element": 39, "face": 3}, {"element": 40, "face": 3}, {"element": 45, "face": 3}, {"element": 46, "face": 3}]}}, + {"command": "model.set.create", "params": {"name": "z0", "faces": [{"element": 1, "face": 1}, {"element": 3, "face": 1}, {"element": 7, "face": 1}, {"element": 9, "face": 1}, {"element": 13, "face": 1}, {"element": 15, "face": 1}, {"element": 19, "face": 1}, {"element": 21, "face": 1}]}}, + {"command": "model.set.create", "params": {"name": "z1", "faces": [{"element": 29, "face": 3}, {"element": 30, "face": 3}, {"element": 35, "face": 3}, {"element": 36, "face": 3}, {"element": 41, "face": 3}, {"element": 42, "face": 3}, {"element": 47, "face": 3}, {"element": 48, "face": 3}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.25}}}, + {"command": "model.section.create", "params": {"name": "solid", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_x", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_y", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_z", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "tx", "type": "traction", "set": "x1", "vector": [1.0, 0, 0]}}, + {"command": "model.load.create", "params": {"name": "ty", "type": "traction", "set": "y1", "vector": [0, 2.0, 0]}}, + {"command": "model.load.create", "params": {"name": "tz", "type": "traction", "set": "z1", "vector": [0, 0, 3.0]}}, + {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["sym_x", "sym_y", "sym_z"], "loads": ["tx", "ty", "tz"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/patch-tet4/README.md b/tests/oracle/patch-tet4/README.md new file mode 100644 index 0000000..f08eb47 --- /dev/null +++ b/tests/oracle/patch-tet4/README.md @@ -0,0 +1,8 @@ +# Constant-stress patch test, tet4 + +A unit cube of eight tet4 cells whose shared points are moved off the regular grid (every +face, edge and centre point; the corners stay), so every element is distorted (one mesh.block per cell). Symmetry on the +faces x = 0, y = 0 and z = 0; tractions 1, 2 and 3 on the faces x = 1, y = 1 and z = 1 +(E = 1000, ν = 0.25). The exact solution is the uniform stress diag(1, 2, 3) with strains +ε_i = (σ_i − ν(σ_j + σ_k))/E, which every conforming element of a convergent formulation +reproduces exactly on any mesh. diff --git a/tests/oracle/patch-tet4/expected.json b/tests/oracle/patch-tet4/expected.json new file mode 100644 index 0000000..3aa989c --- /dev/null +++ b/tests/oracle/patch-tet4/expected.json @@ -0,0 +1,168 @@ +{ + "title": "Constant-stress patch test, tet4", + "kind": "theory", + "source": "Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7", + "quantities": [ + { + "id": "ux", + "quantity": "u_x at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": -0.00025, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uy", + "quantity": "u_y at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/1" + }, + "value": 0.001, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uz", + "quantity": "u_z at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": 0.00225, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxx", + "quantity": "σ_xx at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/0" + }, + "value": 1.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "syy", + "quantity": "σ_yy at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": 2.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "szz", + "quantity": "σ_zz at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/2" + }, + "value": 3.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxy", + "quantity": "σ_xy at the inner node", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/3" + }, + "value": 0.0, + "tolerance": { + "absolute": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)" + } + ] +} diff --git a/tests/oracle/patch-tet4/fixture.json b/tests/oracle/patch-tet4/fixture.json new file mode 100644 index 0000000..59e2f40 --- /dev/null +++ b/tests/oracle/patch-tet4/fixture.json @@ -0,0 +1,33 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "sxx": { + "band": 1e-9, + "value": 0.9999999999999988 + }, + "sxy": { + "band": 1.0000000000000236e-12, + "value": 2.3335757696855974e-17 + }, + "syy": { + "band": 2e-9, + "value": 1.9999999999999982 + }, + "szz": { + "band": 3.0000000000000004e-9, + "value": 2.999999999999998 + }, + "ux": { + "band": 2.5000000000000035e-13, + "value": -0.00025000000000000033 + }, + "uy": { + "band": 1.0000000000000002e-12, + "value": 0.0009999999999999987 + }, + "uz": { + "band": 2.25e-12, + "value": 0.0022499999999999977 + } + } +} diff --git a/tests/oracle/patch-tet4/recipe.json b/tests/oracle/patch-tet4/recipe.json new file mode 100644 index 0000000..9ed49a2 --- /dev/null +++ b/tests/oracle/patch-tet4/recipe.json @@ -0,0 +1,27 @@ +[ + {"command": "mesh.block", "params": {"body": "Cube", "kind": "tet4", "corners": [[0.0, 0.0, 0.0], [0.46, 0.0, 0.0], [0.55, 0.47, 0.0], [0.0, 0.46, 0.0], [0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "tet4", "corners": [[0.46, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.55, 0.0], [0.55, 0.47, 0.0], [0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "tet4", "corners": [[0.0, 0.46, 0.0], [0.55, 0.47, 0.0], [0.56, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "tet4", "corners": [[0.55, 0.47, 0.0], [1.0, 0.55, 0.0], [1.0, 1.0, 0.0], [0.56, 1.0, 0.0], [0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "tet4", "corners": [[0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54], [0.0, 0.0, 1.0], [0.55, 0.0, 1.0], [0.56, 0.46, 1.0], [0.0, 0.55, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "tet4", "corners": [[0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53], [0.55, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 0.47, 1.0], [0.56, 0.46, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "tet4", "corners": [[0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45], [0.0, 0.55, 1.0], [0.56, 0.46, 1.0], [0.46, 1.0, 1.0], [0.0, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "tet4", "corners": [[0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54], [0.56, 0.46, 1.0], [1.0, 0.47, 1.0], [1.0, 1.0, 1.0], [0.46, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "2": "z1"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}}, + {"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}}, + {"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "y1", "geometry": [{"body": "Cube", "face": "y1"}]}}, + {"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Cube", "face": "z1"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.25}}}, + {"command": "model.section.create", "params": {"name": "solid", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_x", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_y", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_z", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "tx", "type": "traction", "set": "x1", "vector": [1.0, 0, 0]}}, + {"command": "model.load.create", "params": {"name": "ty", "type": "traction", "set": "y1", "vector": [0, 2.0, 0]}}, + {"command": "model.load.create", "params": {"name": "tz", "type": "traction", "set": "z1", "vector": [0, 0, 3.0]}}, + {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["sym_x", "sym_y", "sym_z"], "loads": ["tx", "ty", "tz"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/patch-wedge6/README.md b/tests/oracle/patch-wedge6/README.md new file mode 100644 index 0000000..6753f44 --- /dev/null +++ b/tests/oracle/patch-wedge6/README.md @@ -0,0 +1,8 @@ +# Constant-stress patch test, wedge6 + +A unit cube of eight wedge6 cells whose shared points are moved off the regular grid (every +face, edge and centre point; the corners stay), so every element is distorted (one mesh.block per cell). Symmetry on the +faces x = 0, y = 0 and z = 0; tractions 1, 2 and 3 on the faces x = 1, y = 1 and z = 1 +(E = 1000, ν = 0.25). The exact solution is the uniform stress diag(1, 2, 3) with strains +ε_i = (σ_i − ν(σ_j + σ_k))/E, which every conforming element of a convergent formulation +reproduces exactly on any mesh. diff --git a/tests/oracle/patch-wedge6/expected.json b/tests/oracle/patch-wedge6/expected.json new file mode 100644 index 0000000..a8912e2 --- /dev/null +++ b/tests/oracle/patch-wedge6/expected.json @@ -0,0 +1,168 @@ +{ + "title": "Constant-stress patch test, wedge6", + "kind": "theory", + "source": "Irons and Razzaque, 'Experience with the patch test for convergence of finite elements', in Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, pp. 557–587; MacNeal and Harder, Finite Elements in Analysis and Design 1 (1985) 3–20, constant-stress loading, p. 14 Table 7", + "quantities": [ + { + "id": "ux", + "quantity": "u_x at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/0" + }, + "value": -0.00025, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uy", + "quantity": "u_y at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/1" + }, + "value": 0.001, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "uz", + "quantity": "u_z at the corner (1, 1, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 1, + 1, + 1 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": 0.00225, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxx", + "quantity": "σ_xx at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/0" + }, + "value": 1.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "syy", + "quantity": "σ_yy at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/1" + }, + "value": 2.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "szz", + "quantity": "σ_zz at the inner node (0.46, 0.55, 0.53)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/2" + }, + "value": 3.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding" + }, + { + "id": "sxy", + "quantity": "σ_xy at the inner node", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0.46, + 0.55, + 0.53 + ] + ] + }, + "pointer": "/nodes/0/stress/3" + }, + "value": 0.0, + "tolerance": { + "absolute": 1e-09 + }, + "reason": "a patch test is passed exactly: the tolerance only admits rounding (stresses of order 1)" + } + ] +} diff --git a/tests/oracle/patch-wedge6/fixture.json b/tests/oracle/patch-wedge6/fixture.json new file mode 100644 index 0000000..ae46c68 --- /dev/null +++ b/tests/oracle/patch-wedge6/fixture.json @@ -0,0 +1,33 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "sxx": { + "band": 1.0000000000000007e-9, + "value": 1.0000000000000007 + }, + "sxy": { + "band": 1.000000000000084e-12, + "value": -8.37913026900662e-17 + }, + "syy": { + "band": 2.0000000000000005e-9, + "value": 2.0000000000000004 + }, + "szz": { + "band": 3.0000000000000004e-9, + "value": 2.9999999999999987 + }, + "ux": { + "band": 2.5000000000000005e-13, + "value": -0.00024999999999999784 + }, + "uy": { + "band": 1.0000000000000016e-12, + "value": 0.0010000000000000015 + }, + "uz": { + "band": 2.25e-12, + "value": 0.002249999999999995 + } + } +} diff --git a/tests/oracle/patch-wedge6/recipe.json b/tests/oracle/patch-wedge6/recipe.json new file mode 100644 index 0000000..95b11e5 --- /dev/null +++ b/tests/oracle/patch-wedge6/recipe.json @@ -0,0 +1,27 @@ +[ + {"command": "mesh.block", "params": {"body": "Cube", "kind": "wedge6", "corners": [[0.0, 0.0, 0.0], [0.46, 0.0, 0.0], [0.55, 0.47, 0.0], [0.0, 0.46, 0.0], [0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "wedge6", "corners": [[0.46, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.55, 0.0], [0.55, 0.47, 0.0], [0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "wedge6", "corners": [[0.0, 0.46, 0.0], [0.55, 0.47, 0.0], [0.56, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "wedge6", "corners": [[0.55, 0.47, 0.0], [1.0, 0.55, 0.0], [1.0, 1.0, 0.0], [0.56, 1.0, 0.0], [0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "1": "z0"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "wedge6", "corners": [[0.0, 0.0, 0.53], [0.56, 0.0, 0.45], [0.46, 0.55, 0.53], [0.0, 0.47, 0.54], [0.0, 0.0, 1.0], [0.55, 0.0, 1.0], [0.56, 0.46, 1.0], [0.0, 0.55, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "wedge6", "corners": [[0.56, 0.0, 0.45], [1.0, 0.0, 0.54], [1.0, 0.46, 0.45], [0.46, 0.55, 0.53], [0.55, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 0.47, 1.0], [0.56, 0.46, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "3": "y0", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "wedge6", "corners": [[0.0, 0.47, 0.54], [0.46, 0.55, 0.53], [0.55, 1.0, 0.54], [0.0, 1.0, 0.45], [0.0, 0.55, 1.0], [0.56, 0.46, 1.0], [0.46, 1.0, 1.0], [0.0, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"6": "x0", "5": "y1", "2": "z1"}}}, + {"command": "mesh.block", "params": {"body": "Cube", "kind": "wedge6", "corners": [[0.46, 0.55, 0.53], [1.0, 0.46, 0.45], [1.0, 1.0, 0.53], [0.55, 1.0, 0.54], [0.56, 0.46, 1.0], [1.0, 0.47, 1.0], [1.0, 1.0, 1.0], [0.46, 1.0, 1.0]], "divisions": [1, 1, 1], "faces": {"4": "x1", "5": "y1", "2": "z1"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Cube"}]}}, + {"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Cube", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "x1", "geometry": [{"body": "Cube", "face": "x1"}]}}, + {"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Cube", "face": "y0"}]}}, + {"command": "model.set.create", "params": {"name": "y1", "geometry": [{"body": "Cube", "face": "y1"}]}}, + {"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Cube", "face": "z0"}]}}, + {"command": "model.set.create", "params": {"name": "z1", "geometry": [{"body": "Cube", "face": "z1"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.25}}}, + {"command": "model.section.create", "params": {"name": "solid", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_x", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_y", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sym_z", "type": "symmetry", "target": {"set": "z0"}, "plane": "z"}}, + {"command": "model.load.create", "params": {"name": "tx", "type": "traction", "set": "x1", "vector": [1.0, 0, 0]}}, + {"command": "model.load.create", "params": {"name": "ty", "type": "traction", "set": "y1", "vector": [0, 2.0, 0]}}, + {"command": "model.load.create", "params": {"name": "tz", "type": "traction", "set": "z1", "vector": [0, 0, 3.0]}}, + {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["sym_x", "sym_y", "sym_z"], "loads": ["tx", "ty", "tz"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/pinched-cylinder/README.md b/tests/oracle/pinched-cylinder/README.md new file mode 100644 index 0000000..3a00e7a --- /dev/null +++ b/tests/oracle/pinched-cylinder/README.md @@ -0,0 +1,3 @@ +# Pinched cylinder with rigid diaphragms + +Radius 300, length 600, thickness 3, E = 3 × 10⁶, ν = 0.3, rigid end diaphragms, two opposite unit loads at mid-length. diff --git a/tests/oracle/pinched-cylinder/expected.json b/tests/oracle/pinched-cylinder/expected.json new file mode 100644 index 0000000..cc7bdee --- /dev/null +++ b/tests/oracle/pinched-cylinder/expected.json @@ -0,0 +1,14 @@ +{ + "title": "Pinched cylinder with rigid diaphragms", + "kind": "benchmark", + "source": "Belytschko, Stolarski, Liu, Carpenter and Ong, 'Stress projection for membrane and shear locking in shell finite elements', Computer Methods in Applied Mechanics and Engineering 51 (1985) 221–258 (the shell obstacle course); reference 1.8248 × 10⁻⁵ under the load", + "status": "pending", + "requires": "shell elements (M8)", + "quantities": [ + { + "id": "u", + "quantity": "radial displacement under the load", + "value": -1.8248e-05 + } + ] +} diff --git a/tests/oracle/pinched-hemisphere/README.md b/tests/oracle/pinched-hemisphere/README.md new file mode 100644 index 0000000..48b5140 --- /dev/null +++ b/tests/oracle/pinched-hemisphere/README.md @@ -0,0 +1,3 @@ +# Hemispherical shell with an 18° hole under alternating point loads + +Radius 10, thickness 0.04, E = 6.825 × 10⁷, ν = 0.3, loads of 2.0 alternating in sign at 90° on the equator (one quadrant modelled). diff --git a/tests/oracle/pinched-hemisphere/expected.json b/tests/oracle/pinched-hemisphere/expected.json new file mode 100644 index 0000000..ca62eb7 --- /dev/null +++ b/tests/oracle/pinched-hemisphere/expected.json @@ -0,0 +1,14 @@ +{ + "title": "Hemispherical shell with an 18° hole under alternating point loads", + "kind": "benchmark", + "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. 9 (p. 9), Table 5(b) (p. 11): 0.0940 (lower bound 0.0924 from their [7])", + "status": "pending", + "requires": "shell elements (M8), or mapped blocks that follow doubly curved faces", + "quantities": [ + { + "id": "u", + "quantity": "displacement under the load along the load", + "value": 0.094 + } + ] +} diff --git a/tests/oracle/plastic-thick-cylinder/README.md b/tests/oracle/plastic-thick-cylinder/README.md new file mode 100644 index 0000000..daa8eeb --- /dev/null +++ b/tests/oracle/plastic-thick-cylinder/README.md @@ -0,0 +1,3 @@ +# Elastic–perfectly plastic thick cylinder: collapse pressure + +The Lamé cylinder of `lame-cylinder`, elastic–perfectly plastic, loaded to collapse. diff --git a/tests/oracle/plastic-thick-cylinder/expected.json b/tests/oracle/plastic-thick-cylinder/expected.json new file mode 100644 index 0000000..0b84b94 --- /dev/null +++ b/tests/oracle/plastic-thick-cylinder/expected.json @@ -0,0 +1,14 @@ +{ + "title": "Elastic–perfectly plastic thick cylinder: collapse pressure", + "kind": "theory", + "source": "Hill, The Mathematical Theory of Plasticity, Oxford 1950, ch. V §2: plane strain von Mises collapse pressure p = (2/√3)σ_y ln(b/a)", + "status": "pending", + "requires": "J2 plasticity (M8/M10 X2)", + "quantities": [ + { + "id": "p_collapse", + "quantity": "collapse pressure for a = 10, b = 20, σ_y = 250", + "value": 200.0944 + } + ] +} diff --git a/tests/oracle/pure-bending-coupling/README.md b/tests/oracle/pure-bending-coupling/README.md new file mode 100644 index 0000000..f28d7a1 --- /dev/null +++ b/tests/oracle/pure-bending-coupling/README.md @@ -0,0 +1,5 @@ +# Pure bending through a distributing coupling + +A 20 × 1 × 2 bar of 20 × 2 × 4 hex20 elements clamped at x = 0; its end face is coupled to a +reference point by a distributing coupling, and a moment of 10 about y acts on the point +(E = 1000, ν = 0). diff --git a/tests/oracle/pure-bending-coupling/expected.json b/tests/oracle/pure-bending-coupling/expected.json new file mode 100644 index 0000000..679900a --- /dev/null +++ b/tests/oracle/pure-bending-coupling/expected.json @@ -0,0 +1,44 @@ +{ + "title": "Pure bending through a distributing coupling", + "kind": "theory", + "source": "Euler–Bernoulli pure bending, σ = M_y z/I, θ = ML/(EI) (exact in elasticity for ν = 0); Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §102", + "quantities": [ + { + "id": "sigma_top", + "quantity": "bending stress σ_xx at mid-span, top fibre (10, 0.5, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Bend", + "at": [ + [ + 10.0, + 0.5, + 1.0 + ] + ] + }, + "pointer": "/nodes/0/stress/0" + }, + "value": 15.0, + "tolerance": { + "relative": 0.005 + }, + "reason": "with ν = 0 pure bending is a quadratic displacement field that hex20 represents exactly; the coupling's nodal force pattern differs from the linear traction only within a depth or two of the end (Saint-Venant), five depths from the probe" + }, + { + "id": "rotation", + "quantity": "rotation of the reference point about y", + "measure": { + "from": "solve", + "pointer": "/reference_points/0/displacement/4" + }, + "value": 0.30000000000000004, + "tolerance": { + "relative": 0.01 + }, + "reason": "the reference point follows the best-fit rigid motion of the end section, which the end zone's local deformation disturbs by well under 1%", + "known_failure": "1.2% high: the distributing coupling weights every node of the end face equally, corner and mid-side nodes of hex20 alike, which is not the consistent linear traction; the end face deforms locally and the fitted rotation includes it. Area-consistent weights would remove it" + } + ] +} diff --git a/tests/oracle/pure-bending-coupling/fixture.json b/tests/oracle/pure-bending-coupling/fixture.json new file mode 100644 index 0000000..d22eeb7 --- /dev/null +++ b/tests/oracle/pure-bending-coupling/fixture.json @@ -0,0 +1,9 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "sigma_top": { + "band": 1.5000000000000002e-8, + "value": 14.99999845770873 + } + } +} diff --git a/tests/oracle/pure-bending-coupling/recipe.json b/tests/oracle/pure-bending-coupling/recipe.json new file mode 100644 index 0000000..b39ff3c --- /dev/null +++ b/tests/oracle/pure-bending-coupling/recipe.json @@ -0,0 +1,14 @@ +[ + {"command": "mesh.block", "params": {"body": "Beam", "kind": "hex20", "corners": [[0, 0, -1.0], [20.0, 0, -1.0], [20.0, 1.0, -1.0], [0, 1.0, -1.0], [0, 0, 1.0], [20.0, 0, 1.0], [20.0, 1.0, 1.0], [0, 1.0, 1.0]], "divisions": [20, 2, 4], "faces": {"6": "root", "4": "tip"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Beam"}]}}, + {"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Beam", "face": "root"}]}}, + {"command": "model.set.create", "params": {"name": "tip", "geometry": [{"body": "Beam", "face": "tip"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.0}}}, + {"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "clamp", "type": "fixed", "target": {"set": "root"}}}, + {"command": "model.reference_point.create", "params": {"name": "RP", "xyz": [20.0, 0.5, 0]}}, + {"command": "model.coupling.create", "params": {"name": "end", "reference_point": "RP", "set": "tip", "kind": "distributing"}}, + {"command": "model.load.create", "params": {"name": "M", "type": "moment", "target": {"reference_point": "RP"}, "vector": [0, 10.0, 0]}}, + {"command": "model.step.create", "params": {"name": "Bend", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["M"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/scordelis-lo-roof-shell/README.md b/tests/oracle/scordelis-lo-roof-shell/README.md new file mode 100644 index 0000000..1f390ab --- /dev/null +++ b/tests/oracle/scordelis-lo-roof-shell/README.md @@ -0,0 +1,3 @@ +# Scordelis–Lo roof with shell elements + +The roof of `scordelis-lo-roof-solid` meshed with shells. diff --git a/tests/oracle/scordelis-lo-roof-shell/expected.json b/tests/oracle/scordelis-lo-roof-shell/expected.json new file mode 100644 index 0000000..15c994a --- /dev/null +++ b/tests/oracle/scordelis-lo-roof-shell/expected.json @@ -0,0 +1,14 @@ +{ + "title": "Scordelis–Lo roof with shell elements", + "kind": "benchmark", + "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), Table 5(a) (p. 11); Belytschko, Stolarski, Liu, Carpenter and Ong, 'Stress projection for membrane and shear locking in shell finite elements', Computer Methods in Applied Mechanics and Engineering 51 (1985) 221–258 (the shell obstacle course)", + "status": "pending", + "requires": "shell elements (MITC4, M8)", + "quantities": [ + { + "id": "uz", + "quantity": "vertical displacement at the midpoint of the free edge", + "value": -0.3024 + } + ] +} diff --git a/tests/oracle/scordelis-lo-roof-solid/README.md b/tests/oracle/scordelis-lo-roof-solid/README.md new file mode 100644 index 0000000..543d8db --- /dev/null +++ b/tests/oracle/scordelis-lo-roof-solid/README.md @@ -0,0 +1,7 @@ +# Scordelis–Lo roof with one layer of hex20 reduced-integration solids + +Our model of the published problem: a cylindrical roof of radius 25, length 50, thickness 0.25, +spanning ±40°, E = 4.32 × 10⁸, ν = 0, self-weight 90 per unit area (a body force 90/t), supported +by rigid diaphragms at its curved ends (u_y = u_z = 0 there) and free along its straight edges. +One quarter with symmetry planes, 16 × 16 hex20 reduced-integration elements, one through the +thickness (MacNeal and Harder list the roof as a solid-element test, Table 1). diff --git a/tests/oracle/scordelis-lo-roof-solid/expected.json b/tests/oracle/scordelis-lo-roof-solid/expected.json new file mode 100644 index 0000000..3cfc063 --- /dev/null +++ b/tests/oracle/scordelis-lo-roof-solid/expected.json @@ -0,0 +1,30 @@ +{ + "title": "Scordelis–Lo roof with one layer of hex20 reduced-integration solids", + "kind": "benchmark", + "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)", + "quantities": [ + { + "id": "uz", + "quantity": "vertical displacement at the midpoint of the free edge, mid-surface", + "measure": { + "command": "solve.probe", + "params": { + "step": "Load", + "at": [ + [ + 0, + 16.069690242163, + 19.151111077974 + ] + ] + }, + "pointer": "/nodes/0/displacement/2" + }, + "value": -0.3024, + "tolerance": { + "relative": 0.02 + }, + "reason": "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)" + } + ] +} diff --git a/tests/oracle/scordelis-lo-roof-solid/fixture.json b/tests/oracle/scordelis-lo-roof-solid/fixture.json new file mode 100644 index 0000000..3b1fbbf --- /dev/null +++ b/tests/oracle/scordelis-lo-roof-solid/fixture.json @@ -0,0 +1,9 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "uz": { + "band": 3.024e-10, + "value": -0.3014029226397275 + } + } +} diff --git a/tests/oracle/scordelis-lo-roof-solid/recipe.json b/tests/oracle/scordelis-lo-roof-solid/recipe.json new file mode 100644 index 0000000..e57667f --- /dev/null +++ b/tests/oracle/scordelis-lo-roof-solid/recipe.json @@ -0,0 +1,15 @@ +[ + {"command": "mesh.block", "params": {"body": "Roof", "kind": "hex20", "corners": [[0, 0.0, 24.875], [25.0, 0.0, 24.875], [25.0, 15.989341790953, 19.055355522585], [0, 15.989341790953, 19.055355522585], [0, 0.0, 25.125], [25.0, 0.0, 25.125], [25.0, 16.150038693374, 19.246866633364], [0, 16.150038693374, 19.246866633364]], "divisions": [16, 16, 1], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [2, 3], "arc": {"center": [25.0, 0, 0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 0]}}, {"corners": [6, 7], "arc": {"center": [25.0, 0, 0]}}], "faces": {"6": "x0", "3": "crown", "4": "diaphragm"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Roof"}]}}, + {"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Roof", "face": "x0"}]}}, + {"command": "model.set.create", "params": {"name": "crown", "geometry": [{"body": "Roof", "face": "crown"}]}}, + {"command": "model.set.create", "params": {"name": "diaphragm", "geometry": [{"body": "Roof", "face": "diaphragm"}]}}, + {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 432000000.0, "poisson": 0.0}}}, + {"command": "model.section.create", "params": {"name": "roof", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "sx", "type": "symmetry", "target": {"set": "x0"}, "plane": "x"}}, + {"command": "model.boundary_condition.create", "params": {"name": "sy", "type": "symmetry", "target": {"set": "crown"}, "plane": "y"}}, + {"command": "model.boundary_condition.create", "params": {"name": "diaphragm", "type": "displacement", "target": {"set": "diaphragm"}, "values": [null, 0, 0, null, null, null]}}, + {"command": "model.load.create", "params": {"name": "weight", "type": "body_force", "set": "all", "vector": [0, 0, -360.0]}}, + {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["sx", "sy", "diaphragm"], "loads": ["weight"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/tests/oracle/taylor-bar/README.md b/tests/oracle/taylor-bar/README.md new file mode 100644 index 0000000..7316f6e --- /dev/null +++ b/tests/oracle/taylor-bar/README.md @@ -0,0 +1,3 @@ +# Taylor bar impact (copper) + +A copper cylinder fired at a rigid wall; the measured final shapes are transcribed when the case is built. diff --git a/tests/oracle/taylor-bar/expected.json b/tests/oracle/taylor-bar/expected.json new file mode 100644 index 0000000..3d565e8 --- /dev/null +++ b/tests/oracle/taylor-bar/expected.json @@ -0,0 +1,14 @@ +{ + "title": "Taylor bar impact (copper)", + "kind": "experiment", + "source": "Wilkins and Guinan, 'Impact of cylinders on a rigid boundary', Journal of Applied Physics 44 (1973) 1200–1206 (final length and mushroom diameter vs impact speed)", + "status": "pending", + "requires": "large-deformation plasticity and contact (M10 X2/X3)", + "quantities": [ + { + "id": "final_length", + "quantity": "final over initial length", + "value": null + } + ] +} diff --git a/tests/oracle/tube-torsion-coupling/README.md b/tests/oracle/tube-torsion-coupling/README.md new file mode 100644 index 0000000..6f94148 --- /dev/null +++ b/tests/oracle/tube-torsion-coupling/README.md @@ -0,0 +1,5 @@ +# Torsion of a tube through a kinematically coupled reference point + +A tube a = 8, b = 10, L = 40 (four hex20 blocks around, 4 × 8 × 8 each) clamped at z = 0; its end +face is coupled kinematically to a reference point on the axis, which carries a moment +T = 10⁶ about z (E = 200000, ν = 0.3). diff --git a/tests/oracle/tube-torsion-coupling/expected.json b/tests/oracle/tube-torsion-coupling/expected.json new file mode 100644 index 0000000..e076cb4 --- /dev/null +++ b/tests/oracle/tube-torsion-coupling/expected.json @@ -0,0 +1,43 @@ +{ + "title": "Torsion of a tube through a kinematically coupled reference point", + "kind": "theory", + "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", + "quantities": [ + { + "id": "twist", + "quantity": "rotation of the reference point about z", + "measure": { + "from": "solve", + "pointer": "/reference_points/0/displacement/5" + }, + "value": 0.05607084715974633, + "tolerance": { + "relative": 0.005 + }, + "reason": "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%" + }, + { + "id": "shear", + "quantity": "surface shear stress τ_θz = σ_yz at (10, 0, 20)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Twist", + "at": [ + [ + 10.0, + 0, + 20.0 + ] + ] + }, + "pointer": "/nodes/0/stress/5" + }, + "value": 1078.285522302814, + "tolerance": { + "relative": 0.01 + }, + "reason": "stress extrapolated to the outer surface and averaged where two blocks meet: about 0.3% with four radial quadratic elements (two gave 1.1%)" + } + ] +} diff --git a/tests/oracle/tube-torsion-coupling/fixture.json b/tests/oracle/tube-torsion-coupling/fixture.json new file mode 100644 index 0000000..030f421 --- /dev/null +++ b/tests/oracle/tube-torsion-coupling/fixture.json @@ -0,0 +1,13 @@ +{ + "note": "ModelCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "shear": { + "band": 1.0813925570161819e-6, + "value": 1081.3925570161819 + }, + "twist": { + "band": 5.6071194158263346e-11, + "value": 0.056071194158263345 + } + } +} diff --git a/tests/oracle/tube-torsion-coupling/recipe.json b/tests/oracle/tube-torsion-coupling/recipe.json new file mode 100644 index 0000000..f9f3da1 --- /dev/null +++ b/tests/oracle/tube-torsion-coupling/recipe.json @@ -0,0 +1,17 @@ +[ + {"command": "mesh.block", "params": {"body": "Tube", "kind": "hex20", "corners": [[8.0, 0.0, 0], [10.0, 0.0, 0], [0.0, 10.0, 0], [0.0, 8.0, 0], [8.0, 0.0, 40.0], [10.0, 0.0, 40.0], [0.0, 10.0, 40.0], [0.0, 8.0, 40.0]], "divisions": [4, 8, 8], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 40.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 40.0]}}], "faces": {"1": "root", "2": "tip"}}}, + {"command": "mesh.block", "params": {"body": "Tube", "kind": "hex20", "corners": [[0.0, 8.0, 0], [0.0, 10.0, 0], [-10.0, 0.0, 0], [-8.0, 0.0, 0], [0.0, 8.0, 40.0], [0.0, 10.0, 40.0], [-10.0, 0.0, 40.0], [-8.0, 0.0, 40.0]], "divisions": [4, 8, 8], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 40.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 40.0]}}], "faces": {"1": "root", "2": "tip"}}}, + {"command": "mesh.block", "params": {"body": "Tube", "kind": "hex20", "corners": [[-8.0, 0.0, 0], [-10.0, 0.0, 0], [0.0, -10.0, 0], [0.0, -8.0, 0], [-8.0, 0.0, 40.0], [-10.0, 0.0, 40.0], [0.0, -10.0, 40.0], [0.0, -8.0, 40.0]], "divisions": [4, 8, 8], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 40.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 40.0]}}], "faces": {"1": "root", "2": "tip"}}}, + {"command": "mesh.block", "params": {"body": "Tube", "kind": "hex20", "corners": [[0.0, -8.0, 0], [0.0, -10.0, 0], [10.0, 0.0, 0], [8.0, 0.0, 0], [0.0, -8.0, 40.0], [0.0, -10.0, 40.0], [10.0, 0.0, 40.0], [8.0, 0.0, 40.0]], "divisions": [4, 8, 8], "edges": [{"corners": [1, 4], "arc": {"center": [0, 0, 0]}}, {"corners": [2, 3], "arc": {"center": [0, 0, 0]}}, {"corners": [5, 8], "arc": {"center": [0, 0, 40.0]}}, {"corners": [6, 7], "arc": {"center": [0, 0, 40.0]}}], "faces": {"1": "root", "2": "tip"}}}, + {"command": "model.set.create", "params": {"name": "all", "geometry": [{"body": "Tube"}]}}, + {"command": "model.set.create", "params": {"name": "root", "geometry": [{"body": "Tube", "face": "root"}]}}, + {"command": "model.set.create", "params": {"name": "tip", "geometry": [{"body": "Tube", "face": "tip"}]}}, + {"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 200000.0, "poisson": 0.3}}}, + {"command": "model.section.create", "params": {"name": "tube", "set": "all", "material": "Steel", "kind": {"type": "solid"}}}, + {"command": "model.boundary_condition.create", "params": {"name": "clamp", "type": "fixed", "target": {"set": "root"}}}, + {"command": "model.reference_point.create", "params": {"name": "RP", "xyz": [0, 0, 40.0]}}, + {"command": "model.coupling.create", "params": {"name": "end", "reference_point": "RP", "set": "tip", "kind": "kinematic"}}, + {"command": "model.load.create", "params": {"name": "T", "type": "moment", "target": {"reference_point": "RP"}, "vector": [0, 0, 1000000.0]}}, + {"command": "model.step.create", "params": {"name": "Twist", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["T"]}}, + {"command": "solve.static", "params": {}, "id": "solve"} +] diff --git a/xtask/src/main.rs b/xtask/src/main.rs index fcaee0d..5defcdc 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -37,6 +37,11 @@ commands: corpus [--part NAME] [--no-write] build every meshing test corpus part (tests/corpus, analytic shapes), tessellate and mesh it, and write the results to docs/meshing/corpus.md (release build) + oracle [--case NAME]... [--bless NAME|all]... [--quick] [--no-write] + run the verification and validation cases (tests/oracle, docs/verification.md) + against their references and fixtures; write docs/oracle.md after a full run; + --bless freezes a passing case's results in its fixture.json; --quick skips + cases marked slow (release build) wasm cargo check every library crate for wasm32-unknown-unknown ci fmt --check, assets, licences --check, layers, clippy -D warnings, then the tests (not the heavy ones) with the wasm check alongside (stops at the first failing @@ -72,6 +77,12 @@ fn main() -> ExitCode { c.args(["run", "--release", "-p", "modelcraft-testkit", "--bin", "corpus", "--"]).args(&rest); run(c, "corpus") } + Some("oracle") => { + // A binary of the testkit, which has the engine and SolidCraft scripts for geometry. + let mut c = cargo(); + c.args(["run", "--release", "-p", "modelcraft-testkit", "--bin", "oracle", "--"]).args(&rest); + run(c, "oracle") + } Some("wasm") => cmd_wasm(), Some("-h" | "--help" | "help") | None => { print!("{USAGE}");