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}");