forked from 1b-agent/fieldcraft-upstream
* mesh: one transfinite interpolation for mapped hex and mesh.block; stable oracle report mesh.block's lattice is now built from the structured hex mesher's transfinite code: faces by quad::transfinite_grid, the interior by the new quad::transfinite_volume (Gordon-Thiel, blended by the block's graded parameters), which hex::mapped now calls too. 2D patches are the transfinite_grid of their sides. Edge curves return their corners exactly. Oracle: a zero-reference quantity (rounding noise) keeps its absolute tolerance as its fixture band, and docs/oracle.md shows such noise and relative errors inside the fixture band as bounds, so the report no longer changes with the order of floating-point operations (it failed the 'docs/oracle.md is current' check on main after #50). Scordelis-Lo moved by 1.4e-9 relative (an ill-conditioned thin shell of solids: sub-ulp node changes) and is re-blessed. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * simplify: shared face filling and lattice index; faces blended like the volume; exact ends for every edge quad::transfinite_faces fills a block's six faces (with a per-face hook that hex::mapped uses to project onto its B-rep faces) and quad::lattice_index fixes the lattice layout for all three users. transfinite_grid_blended blends faces by the block's graded parameters, so faces and interior use one parametrisation and mesh.block reproduces its earlier Gordon-Hall lattice (the Scordelis-Lo fixture is back to its blessed value). Straight edges also return their corners exactly. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
5.8 KiB
5.8 KiB
Verification and validation
Every solver capability is checked against three kinds of reference, and every accepted result is frozen so it cannot drift.
| Kind | What it checks | Sources |
|---|---|---|
| Theory | The code solves the equations right | Closed-form solutions (beams, plates, Lamé, Kirsch, waves, shocks, Poiseuille…), patch tests, convergence rates, mesh invariants |
| Experiment | The model predicts reality | Measurements published in papers and reports (Taylor bar, cavity and cylinder flows, FSI experiments…) |
| Published benchmarks | Agreement with established codes on standard problems | NAFEMS benchmarks, vendor benchmark and verification manuals (e.g. Abaqus Benchmarks and Verification manuals), other published reference solutions |
Rules:
- Each case is our own model of the problem, built from the published description (geometry, material, loads, boundary conditions). We never run a commercial product and never copy its input files, manual text or figures; only the published numbers are used, with a citation.
- Each case lives in
tests/oracle/<case>/:recipe.json— the model as ModelCraft commands (or a SolidCraft recipe for geometry);expected.json— the reference values withkind(theory | experiment | benchmark),source(citation: document, table or figure, page),quantity,value,toleranceand the reason for that tolerance (mesh, measurement scatter, model differences);fixture.json— ModelCraft's accepted result, written bycargo xtask oracle --bless <case>only after the case passes againstexpected.json.
cargo xtask oracleruns every case, compares withexpected.json(pass/fail with error) and withfixture.json(any change beyond the fixture tolerance fails CI, even if the reference still passes), and writesdocs/oracle.mdwith the table of cases, sources, errors and status.- A fixture is updated only deliberately (
--bless), in a PR that says why the result moved (a better element, a finer default mesh), never to hide a regression. - The theory suite is the gate for every merge; 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.
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.jsonis a scriptmodelcraft-cli runalso accepts: a JSON array of{"command", "params"}; a step may carry an"id"so a quantity can read its output. Meshes come frommesh.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:A measure reads a JSON pointer either from a recipe step's output ({"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"}]}"from": "<id>") or from a command run after the recipe ("command","params"); an optional"scale"multiplies it. A tolerance isrelative(of the reference),absolute, both, or"exact": true. A pending case cites its reference values (ornulluntil transcribed) and has no recipe yet.fixture.jsonholds each passing quantity's acceptedvalueand itsband(10⁻⁹ of the larger of value and reference, plus 10⁻³ of an absolute tolerance; a quantity whose reference is zero, which is rounding noise, keeps its absolute tolerance as its band): 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:
- Turn the pending cases that waited for it into active ones (write
recipe.json, dropstatus/requires, addmeasureandtolerancewith 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). - Choose meshes that are converged well inside the tolerance (show the refinement in the case's README), or the mesh a benchmark prescribes.
- Run
cargo xtask oracle --case <name>; when it passes,--bless <name>; commit the case, its fixture and the regenerated docs/oracle.md. - A case that misses its reference stays honest: keep the tolerance its reason supports and add
known_failurewith what is missing. Never widen a tolerance to make a case pass.