docs: oracle in architecture and roadmap; fmt

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-13 21:42:19 -04:00
1 parent 6d9452ad84
commit cbff230734
4 files changed
+75 -33

No files matched your search

+1 -1
View File
@@ -33,5 +33,5 @@ hex meshes today and it gives the best elements where stresses are read.
| M12 CFD: lattice Boltzmann | D3Q19/D3Q27 on grids voxelised from the face-tagged surface; TRT, regularised and cumulant collision; interpolated walls, inlets and outlets; LES; octree refinement shared with the hex mesher; wgpu backend (deterministic per device, checked against the CPU) (CPU core: done — `modelcraft-lbm` with D3Q19 TRT and Guo forcing, halfway bounce-back and moving walls, slip and periodic faces, Zou–He velocity and pressure faces, exact voxelisation of obstacle bodies, momentum-exchange forces per face; fluid materials, fluid domains and fluid steps in the model; `model.fluid_domain.*`, `solve.fluid`; verified by Poiseuille channel and pipe, Couette, Taylor–Green decay, sphere-array Stokes drag and mass conservation; Schäfer–Turek 2D-1 as a benchmark) | started |
| M13 Fluid–structure interaction | Immersed-boundary coupling of the lattice Boltzmann fluid with the explicit or implicit structure; interface force transfer; sub-cycling; Aitken then interface quasi-Newton coupling; free-surface flow | planned |
| M14 Blast and impact fluids | EOS-driven fluids in the explicit solver: ALE multi-material or SPH, with fluid–structure contact | planned |
| Continuous: verification and validation | Every capability checked against theory (closed forms), experiments (published measurements) and published benchmarks (NAFEMS, vendor benchmark and verification manuals such as Abaqus's), each case our own model in `tests/oracle/` with its cited reference and tolerance; `cargo xtask oracle` runs them and writes docs/oracle.md; once a case matches, our result is frozen as a fixture so fidelity cannot regress (docs/verification.md) | started |
| Continuous: verification and validation | Every capability checked against theory (closed forms), experiments (published measurements) and published benchmarks (NAFEMS, vendor benchmark and verification manuals such as Abaqus's), each case our own model in `tests/oracle/` with its cited reference and tolerance; `cargo xtask oracle` runs them and writes docs/oracle.md; once a case matches, our result is frozen as a fixture so fidelity cannot regress (docs/verification.md). Runner, fixtures and CI job: done; 20 theory cases (patch tests of every solid element, cantilevers, Lamé, Kirsch, beam frequencies, couplings, explicit waves and impact, CDT invariants), 8 published benchmarks (MacNeal–Harder solid tests, Scordelis–Lo with solids, NAFEMS LE1, LE10, FV32) and 10 pending cases with citations (shells, 2D, thermal, plasticity, Taylor bar, CFD); `mesh.block` mapped blocks build their meshes | started |
| Continuous: desktop and agents | Desktop app (viewport, model tree, commands, control channel, the open design drawn, `ui.capture` of the whole window offscreen: done; toolbar of every capability from the catalogue, ready or coming soon with its milestone, icons drawn with VectorCraft, docs/features.md: done), MCP server, browser build, command schema as the public API | started |
+24 -6
View File
@@ -249,7 +249,9 @@ fn check_conforming(model: &Model, kind: ElementKind, mesh: &BlockMesh, elements
// The block's boundary faces, by sorted corners, with their full node lists.
let mut new_faces: BTreeMap<Vec<u32>, Vec<u32>> = 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 };
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())));
}
let mut old_faces: BTreeMap<Vec<u32>, Vec<u32>> = BTreeMap::new();
@@ -271,11 +273,17 @@ fn check_conforming(model: &Model, kind: ElementKind, mesh: &BlockMesh, elements
}
// 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<u32, [f64; 3]> =
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 block_points: BTreeMap<u32, [f64; 3]> = 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 (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<u32> = 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);
@@ -389,9 +397,19 @@ mod tests {
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 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();
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());
+33 -25
View File
@@ -108,7 +108,11 @@ pub struct Expected {
}
fn text(v: &Value, key: &str) -> Result<String, String> {
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"))
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 {
@@ -137,7 +141,11 @@ impl Expected {
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 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()));
@@ -238,11 +246,7 @@ pub fn run_case(dir: &Path, expected: &Expected) -> Result<Vec<Measured>, String
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())
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
@@ -283,7 +287,9 @@ pub enum Verdict {
#[derive(Debug, Clone, PartialEq)]
pub enum FixtureCheck {
Matches,
Moved { frozen: f64 },
Moved {
frozen: f64,
},
Missing,
/// No fixture is kept (pending, failing or known-failing quantity).
NotKept,
@@ -366,7 +372,8 @@ pub fn run(root: &Path, options: &Options) -> Result<(), String> {
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());
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
};
@@ -405,7 +412,9 @@ pub fn run(root: &Path, options: &Options) -> Result<(), String> {
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::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}")),
_ => {}
@@ -428,11 +437,7 @@ pub fn run(root: &Path, options: &Options) -> Result<(), String> {
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 ")))
}
if problems.is_empty() { Ok(()) } else { Err(format!("{} problem(s):\n {}", problems.len(), problems.join("\n "))) }
}
fn print_summary(rows: &[Row]) {
@@ -562,7 +567,10 @@ benchmark, {} experiment; {} pending cases wait for capabilities still being bui
"benchmark" => "Published benchmarks",
_ => "Experiments",
};
let _ = write!(md, "## {heading}\n\n| Case | Quantity | Reference | ModelCraft | Error | Tolerance | Result | Fixture |\n|---|---|---|---|---|---|---|---|\n");
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);
@@ -597,8 +605,11 @@ benchmark, {} experiment; {} pending cases wait for capabilities still being bui
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<String> =
e.quantities.iter().map(|q| format!("{} = {}", cell(&q.description), q.reference.map(number).unwrap_or_else(|| "to be transcribed".into()))).collect();
let values: Vec<String> = 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}/) | {} | {} | {} | {} | {} |",
@@ -655,17 +666,14 @@ mod tests {
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!("")),
] {
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!({}))] {
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}");
+17 -1
View File
@@ -79,7 +79,13 @@ Rules beyond the layer order:
recombination (Lee and Lo 1994) and one uniform split that lands on the boundary nodes, then
smoothing and untangling; every quad is checked with exact predicates. One `SizeField` trait
(`modelcraft-mesh::size`) sets interior sizes and edge divisions, with even counts per loop.
Mapped and swept hex build on these face meshes.
Mapped and swept hex build on these face meshes. Without geometry, `mesh.block`
(`modelcraft-mesh::block`) meshes one six-sided block from its eight corners and optional
circular or elliptical edges by the same edge-based transfinite interpolation, as hex8, hex20,
tet4/tet10 (Kuhn's six tetrahedra per cell) or wedge6, with grading; blocks join where their
nodes coincide, face to face (mismatched divisions or kinds are refused), and their named
faces, edges and vertices become geometry tags that sets select. It meshes the analytic
shapes of the verification cases.
6. **Quality.** Scaled Jacobian, aspect ratio, dihedral angles; every generated mesh is checked
for positive Jacobians and conserved volume before it reaches the model.
@@ -303,3 +309,13 @@ geometry is stored. `cargo xtask corpus` (a release build of the testkit's `corp
builds every part, tessellates it and runs each stage of `modelcraft_testkit::runner`, writing
docs/meshing/corpus.md. A mesher joins by implementing `runner::Stage` (its columns, and a run
over the part's tessellated surfaces) and adding itself to `runner::stages()`.
## Verification oracle
`tests/oracle/<case>/` holds the verification and validation cases (docs/verification.md): our
own model of each problem as a command recipe (`recipe.json`, optionally a SolidCraft
`design.json`), its cited reference values with tolerances and reasons (`expected.json`), and our
frozen results (`fixture.json`). `cargo xtask oracle` runs the testkit's `oracle` binary
(`modelcraft_testkit::oracle`): one `Session` per case, quantities read by JSON pointer from step
outputs or commands such as `solve.probe {"at": [[x, y, z]]}`, compared with the reference and the
fixture; it writes docs/oracle.md. CI runs it as its own job.