Files
solvecraft/docs/robustness.md
T
jyviko 540aa0da1b Wave 185/509: engine: feature replies carry no boolean diagnostics (and 1 more)
2 commits, squashed; the originals stay reachable from tag archive/merge-main-develop.

- 88b1ba507 jyviko: doc: a feature result has no boolean diagnostics any more
- 1ecf00b5a jyviko: engine: feature replies carry no boolean diagnostics

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-10-22 07:36:40 -04:00

212 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kernel robustness
Real users hit edge cases the reference tests (docs/oracle.md) never see: faces that coincide
by accident, tangencies, seams lying where another body cuts, walls thinner than a shell. The
robustness runs build random but realistic designs and check every step.
## How it runs
```
solidcraft-cli fuzz --from 0 --count 1000 --jobs 8 --timeout 90 --out runs/ --report report.md
solidcraft-cli fuzz-one <seed> --minimise # one design, cut down to the steps that matter
```
`--count` seeds are run starting at `--from`. A range that exceeds `u64::MAX` is
rejected before any jobs or output directories are created. The maximum seed
(`18446744073709551615`) is valid with `--count 1`; `--count 0` runs no seeds.
- **Designs** (`crates/engine/src/fuzz.rs`): a seeded generator starts from a box, an extruded
sketch profile closed by an arc (sometimes notched), or a revolved section (partial or full
turns, sometimes touching the axis). It then adds up to eight steps: joins and cuts of
boxes, cylinders and spheres, fillets and chamfers on random edges, shells, holes (simple,
blind, drilled), and rectangular and circular patterns of the last feature. Placements snap
to a 2.5 mm grid and often to the body's own faces, so coincident faces and tangencies come
up all the time, as they do in real parts.
- **Checks after every step:** the command succeeds; every body is a sound solid (each shell
closed and consistently oriented, every face meshes, positive finite volume:
`Body::validity`); the volume moves the right way (a cut never adds, a join never removes).
- **Boolean identity** (`crates/kernel/src/fuzz.rs`): for two random primitives on the same
grid (boxes, cylinders, spheres, tori), union, intersection and cut each give a sound solid
and V(A∪B) + V(A∩B) = V(A) + V(B), V(A−B) = V(A) − V(A∩B), within 0.2 %.
- Each seed runs in its own process with a time limit, so hangs and hard crashes are recorded
too. A step the kernel refuses as **not supported yet** (a fillet between two curved faces,
a shell thicker than a wall) is undone and counted separately, and the design goes on: those
are known limitations, not failures. Booleans that fail count as failures even when their
message says "not supported".
- `cargo test` runs a fixed set of seeds that must stay clean
(`crates/engine/src/tests/robustness.rs`), next to the minimised cases fixed so far.
## Seeded goldens
`tests/robustness/goldens.json` records what each of seeds 0–95 builds (eight steps at most):
its steps, how it ended (ok, or the kind and the start of the error it was refused with), and
for the design it left the bodies' total volume and area (within a relative 1e-6) and their
face, edge and shell counts (exact). `tests::robustness_goldens` fails on any change, better or
worse, so a kernel change shows every seed it moves. Pull requests that can change them (the
kernel, engine, doc, sketch and geom crates) run every seed but the slowest few (`SLOW_SEEDS` in
`.github/workflows/ci.yml`, about four fifths of their time); the merge queue, pushes to `main`
and the nightly run them all. Each seed is its own test
(`seed_00` … `seed_95`), so the CI shards and their cores share them out.
Reference selection uses the compilation target's operating system and environment.
On Windows, `platforms.windows.seeds` contains the MSVC-verified records;
`platforms.windows-gnu.seeds`, when present, contains GNU records. An override
replaces the whole seed record. Seeds missing from the selected group use the shared
record, never another Windows group's record. Non-Windows targets keep their existing
OS-group selection and shared fallback. Unknown Windows environments compare with the
shared records and refuse recording until an explicit policy is defined.
All 96 seeds and the strict measurement and topology tolerances remain unchanged.
The Windows records for seeds 1, 23 and 34 were independently reproduced on unmodified
main `1e984c8e`; seed 94 was independently reproduced on unmodified main `c5992386`
after the extrusion rebase. Both match the extrusion branch. Seed 13's was recorded on
unmodified main `65e6d9da`; seed 23's was re-recorded after #1653. Seed 36's, recorded
on main `79a843c9`, holds a failure: on Windows (and macOS) its last step leaves a body that fails the
validity check, where Linux builds it ok (#3583). The record only makes a further change
show; it does not accept the failure. Recording on a platform with such entries updates its
changed seeds and preserves shared references. A platform record replaces the shared
one whole, so a change that moves a seed with a platform record needs that seed checked
on that platform too: seed 67's Windows record went stale this way after #2064 and
#2247 and was removed once Windows built the shared record again.
The Windows records were made with the MSVC target (`x86_64-pc-windows-msvc`) and all
six were checked again on main `e9490093`. GNU builds (`x86_64-pc-windows-gnu`)
use the shared records unless an explicit GNU override exists, so MSVC-only outcomes
do not leak into GNU validation (#4104).
To record an intended change, give the reason:
```
ROBUSTNESS_GOLDENS_BLESS="why it changed" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens::seed_
```
The changed seeds and the reason are added to the file's `notes`; say why in the pull request too.
Recording on Windows always writes to the current environment's own group, even when
that group does not exist yet. It preserves shared records and the other Windows
environment's records. Non-Windows recording keeps its existing OS-group policy.
Notes coalesce only when both the reason and destination match.
The generator feeds the current geometry's bounds and face/edge picks back into later
commands. The same RNG seed can therefore generate different command coordinates or picks
on different math libraries. The original inputs are retained, including near-contact cases.
Platform snapshots track repeatability; they do not certify cross-platform geometric
equivalence or resolve the numerical differences tracked in #924.
### Independent measurement checks
The seeded snapshots also have checks against geometric references. Seed 34 starts with a
quarter cylinder of radius 15 and height 12.5 (`x,y >= 0`, `-12.5 <= z <= 0`), cuts it with a
radius-2.5 sphere centred at (10, 12.5, 0), then drills an independent radius-2, depth-5 blind
hole from (0, 7.5, -6.25) along +X, with a 118-degree tip.
Subtracting the sphere/cylinder intersection and the drill gives **2134.404133 mm³** and
**1086.316691 mm²**. The drill has tip height `2 / tan(59 degrees)`; the spherical bite is
integrated over the intersection of the radius-15 disk and the sphere's projected radius-2.5
disk. Integrating the lower spherical hemisphere and the removed cylinder-wall patch gives
the surface reference. The test compares the chordal measurement with these independent
references at 5e-5 relative error.
A fixed command replay recorded from seed 67 checks two disconnected shells kept in one
body after a cut (#2247), with a combined analytic surface inventory of 22 planar, six
cylindrical and one conical face after merging redundant subdivisions. The commands are
fixed because the generator chooses later operations from the current body and edge layout;
changes to body grouping can therefore change the generated design. This preserves the
original geometric reference independently of those choices. These checks supplement the
snapshots' stricter repeatability comparisons of raw topology and tessellated measurements.
## Booleans
Booleans run in SMLib, with no retries or fallbacks: a Boolean SMLib cannot do exactly is an
error that names why, never a nudged or approximate result. The retry reports truck's Booleans
kept (`boolean_diagnostics` in `timeline.inspect` and `doc.inspect`) are gone with them.
## Results
1000 seeds (0–999), 8 processes on a heavily loaded machine (load average 60–80), 90 s per
seed. A "timeout" there is mostly a design whose booleans are very slow (seed 20 finishes in
2 min 8 s alone), not a hang. The "after" run had more of them: the machine was busier still
(test suites ran alongside), and failing booleans now try more fallbacks before giving up;
fewer designs fail outright (27.1 % against 36.3 %). Both figures are from before the last two
fixes below (sound face pushes, no panics in the boolean's final assembly).
| Designs | before | after |
|---|---|---|
| clean | 49.8 % | 53.8 % |
| failed | 36.3 % | 27.1 % |
| invalid body | 0.7 % | 0.9 % |
| volume the wrong way | 0.2 % | 0.2 % |
| over 90 s | 13.0 % | 18.0 % |
| Boolean identity | before | after |
|---|---|---|
| holds | 85.4 % | 80.8 % |
| a boolean failed | 1.6 % | 1.2 % |
| not run (the design ran out of time) | 13.0 % | 18.0 % |
### Fixed
- Back-to-back contact joins preserve intentional Split Face boundaries, including on
faces away from the contact. The glue pass retains their lines during healing and in
the resulting body for subsequent operations. Partial circular-contact regressions
cover offsets, rotation, operand order, volume, area and history; the seed-16 pattern
also runs in the ordinary engine suite with analytic volume and area checks.
Imprinting visits a shared analytic support only once: the two side faces of a
cylinder must not recut the same outline into redundant arcs.
- **Shell corners where a curved face meets another at an angle** (the biggest cause, 215 of
the 363 failed designs before). Moving a vertex took one linear step, exact only for planes;
a cylinder meeting a plane at an angle (a shallow arc side) landed off the moved surface.
Now refined by Newton steps onto the moved surfaces.
- **Shell edges that are neither lines nor arcs** (a hole's wall meeting a curved side): the
edge is rebuilt through its moved points.
- **Shells of bodies in several pieces** ("this shell is not connected"): one shell per piece.
- **Cylinder seams on the grid.** A cylinder's seam lines lay on its frame's axes, exactly
where faces placed on the same grid cut it, and booleans failed along the seam. Seams now
start at an odd angle: cylinders tangent to box faces from inside or out, and crossing
cylinders touching along a line, now work.
- **Partial revolves of profiles touching the axis** (a quarter or half turn of a rectangle
from the axis): the edge on the axis no longer sweeps a zero-area face, which every boolean
attempt failed on.
- **Booleans on bodies in several pieces** (a join that left two lumps): when the whole fails
or comes out empty, a piece at a time.
- **Pushing a face apart** (coincident faces) checks its result: the exact rebuild could leave a
cylinder unsound, and then the general offset is used. A boss standing on a plate joins again.
- **No panics in the boolean's final assembly.** Natively a panic inside the kernel is caught,
but the web build aborts on any panic; the Plastic Enclosure sample panicked 8 times building
its bosses and crashed the start page in the browser. The vendored shapeops now returns
`None` instead, and a test builds every sample with a panic counter. The runs count caught
panics per design ("Caught panics" in the report), since each one would crash the web build.
### The first tutorial (box, fillets, shell)
Found by QA on new users' first steps, each with an engine test against the analytic volume
(`crates/engine/src/tests/robustness.rs`):
- **Shell of a filleted box** (vertical edges r5, bottom loop r3, 2 mm, top open): the bottom
round runs round the corners as spindle tori (the tube wider than its circle), which the
offset now takes. Volume within 1e-5 of the analytic one.
- **Shell thicker than or equal to a round** (r2 rounds shelled 2 mm): the round's inside
collapses to a sharp edge, as intended, instead of "a cylinder shrinks to nothing".
- **Shell thinner than a round tangent to the open face** (1.5 mm): every face moves in
(tangencies keep) and the opening is the open face's copy swept out through the body.
- **Shell after a drilled hole** (its tip a cone).
- **Filleting a box's edges one at a time**: an edge that runs into an earlier round takes
that round off and rounds both together, with the corner where they meet (`blend_corner.rs`,
any radii).
### Speed
- Measuring a body of 5000 faces (a modelled thread): 13 s to 0.8 s.
### Known limitations (counted as "not supported", with a clear message)
- A shell thicker than a wall or a step: the moved faces would cross and the cavity turn
inside out. The intended behaviour fills such places solid; we need a topology-changing offset.
- Fillets and chamfers between two curved faces, or ending at faces that aren't planar.
### Still failing
- Partial unions of a cap and a face (a cylinder's end lying partly on a box face): pushing
one face into the other adds material where they don't overlap.
- Shells of partial annuli (a half ring) whose cavity truck's boolean rejects although the
same cavity built another way cuts cleanly.
- Very slow booleans on bodies with many patterned features.