diff --git a/.config/nextest.toml b/.config/nextest.toml index e1070ba4..5e98c2b7 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -10,6 +10,7 @@ not ( (package(solidcraft-engine) & ( test(=tests::hostile_params_never_panic) | test(/^tests::robustness::/) + | test(/^tests::robustness_goldens::/) | test(=pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen) | test(=pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs) | test(=cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 397ca115..a589b722 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: runs-on: ubuntu-24.04 outputs: native: ${{ steps.paths.outputs.native }} + kernel: ${{ steps.paths.outputs.kernel }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -59,12 +60,14 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | native=false + kernel=false if [ "$EVENT_NAME" != pull_request ]; then native=true else # Compare both old and new paths of renames; unknown paths run native checks. git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths" while IFS= read -r -d '' path; do + case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac case "$path" in .github/workflows/ci.yml) native=true ;; *.rs|Cargo.toml|Cargo.lock|*/Cargo.toml|.cargo/*|rust-toolchain*) native=true ;; @@ -74,6 +77,7 @@ jobs: done < "$RUNNER_TEMP/ci-paths" fi echo "native=$native" >> "$GITHUB_OUTPUT" + echo "kernel=$kernel" >> "$GITHUB_OUTPUT" checks: name: Format and repository checks @@ -150,7 +154,7 @@ jobs: tests: name: Native Linux tests (${{ matrix.shard }}/4) - needs: build-tests + needs: [changes, build-tests] runs-on: ubuntu-24.04 timeout-minutes: 60 strategy: @@ -177,6 +181,16 @@ jobs: # the build job's path, which is the same checkout path here. cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \ --profile ci --partition "slice:$SHARD/4" "${all[@]}" + - name: Robustness goldens sample (pull requests that touch the kernel) + # Every 8th seed of tests/robustness/goldens.json, a quarter per shard; pushes to main and + # the nightly run every seed in the step above. + if: github.event_name == 'pull_request' && needs.changes.outputs.kernel == 'true' + env: + SHARD: ${{ matrix.shard }} + run: | + cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \ + --profile ci --ignore-default-filter \ + -E "package(solidcraft-engine) & test(=tests::robustness_goldens::sample_$((SHARD - 1)))" - name: Upload test timings if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6270c57c..94977271 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -161,7 +161,8 @@ Pull requests skip a few heavy tests (one to four minutes each), which run on ev `main` and nightly; check the `main` run after merging. They are listed in `.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md): -- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness`, +- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and + `tests::robustness_goldens` (pull requests that touch `crates/kernel` run its `sample_*` part), `pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`, `pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`, `cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall` diff --git a/crates/engine/src/fuzz.rs b/crates/engine/src/fuzz.rs index ede0d55c..db78c2d8 100644 --- a/crates/engine/src/fuzz.rs +++ b/crates/engine/src/fuzz.rs @@ -289,6 +289,47 @@ pub fn run(seed: u64, max_steps: usize) -> Run { /// [`run`], telling `on_step` about each step before it runs (to see where a hang happened). pub fn run_with(seed: u64, max_steps: usize, on_step: &mut dyn FnMut(&Step)) -> Run { + run_session(seed, max_steps, on_step).0 +} + +/// What a seed builds, as recorded in the robustness goldens (`tests/robustness/goldens.json`): +/// the steps, how it ended (with the start of the message when it did not end ok) and, for the +/// design it left, its bodies' total volume and area and their face, edge and shell counts. +pub fn golden(seed: u64, max_steps: usize) -> Value { + let (run, s) = run_session(seed, max_steps, &mut |_| {}); + let st = s.model.state(); + let (mut volume, mut area, mut faces, mut edges, mut shells) = (0.0, 0.0, 0, 0, 0); + let mut measured = true; + for b in &st.bodies { + match solidcraft_kernel::measure(&b.body) { + Ok(m) => { + volume += m.volume; + area += m.area; + faces += m.faces; + edges += m.edges; + shells += m.shells; + } + Err(_) => measured = false, + } + } + let round = |x: f64| (x * 1e6).round() / 1e6; + json!({ + "steps": run.steps.iter().map(|st| st.kind).collect::>(), + "outcome": run.outcome.kind, + "error": (run.outcome.kind != "ok").then(|| gist(&run.outcome.detail)), + "unsupported": run.unsupported.len(), + "rejected": run.rejected, + "bodies": st.bodies.len(), + "volume": measured.then(|| round(volume)), + "area": measured.then(|| round(area)), + "faces": faces, + "edges": edges, + "shells": shells, + }) +} + +/// [`run_with`], and the session the run left. +fn run_session(seed: u64, max_steps: usize, on_step: &mut dyn FnMut(&Step)) -> (Run, Session) { let mut r = Rng::new(seed); let mut s = Session::default(); let mut run = Run { @@ -333,13 +374,13 @@ pub fn run_with(seed: u64, max_steps: usize, on_step: &mut dyn FnMut(&Step)) -> run.steps.push(st); if out.kind != "ok" { run.outcome = out; - return run; + return (run, s); } if matches!(kind, "join box" | "cut box" | "join cylinder" | "cut cylinder" | "cut sphere" | "hole") { last_feature = s.doc.features.last().map(|f| f.name.clone()); } } - run + (run, s) } /// Run fixed steps (a recorded or minimised design): the index and outcome of the first step diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 1633d3c2..0fd01055 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -175,6 +175,7 @@ mod component_frames; mod mirror_planes; mod primitive_points; mod robustness; +mod robustness_goldens; mod xz_plane; /// Plan area a round of radius `r` fills at the concave corner `x` where circles (c1, r1) and diff --git a/crates/engine/src/tests/robustness_goldens.rs b/crates/engine/src/tests/robustness_goldens.rs new file mode 100644 index 00000000..9cee7ef0 --- /dev/null +++ b/crates/engine/src/tests/robustness_goldens.rs @@ -0,0 +1,152 @@ +//! Seeded goldens: what every robustness seed builds (`tests/robustness/goldens.json`). +//! +//! Each seed's random design (`fuzz::run`) is recorded: its steps, how it ended (ok, or the +//! kind and start of the error it was refused with) and, for what it left, the bodies' total +//! volume and area (within a relative 1e-6) and their face, edge and shell counts (exact). Any +//! change fails, the better and the worse alike: a seed that used to be refused and now builds +//! is recorded on purpose, with the reason. To record: `ROBUSTNESS_GOLDENS_BLESS="why" cargo +//! test --profile ci -p solidcraft-engine --lib robustness_goldens:: -- --include-ignored` +//! (docs/robustness.md). The reason goes into the file's `notes` with the seeds it changed. +//! +//! Pull requests that touch `crates/kernel` run a sample (`sample_0` … `sample_3`, every 8th +//! seed, split so the shards share them); pushes to main and the nightly run every seed +//! (`all_0` … `all_3`). + +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::sync::Mutex; + +use serde_json::{Value, json}; + +use crate::fuzz; + +/// Seeds 0..SEEDS (about ten minutes for all of them on one core), each with up to this many steps (as `fixed_seeds_run_clean`). +const SEEDS: u64 = 96; +const MAX_STEPS: usize = 8; +const REL: f64 = 1e-6; + +/// Tests share the file when they record. +static FILE: Mutex<()> = Mutex::new(()); + +fn path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/robustness/goldens.json") +} + +fn load() -> Value { + std::fs::read(path()).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or_else(|| json!({})) +} + +/// How `got` differs from `want` (None when it matches within the tolerances). +fn differs(want: &Value, got: &Value) -> Option { + let mut out = Vec::new(); + for (k, g) in got.as_object().into_iter().flatten() { + let w = &want[k]; + let same = match (w.as_f64(), g.as_f64()) { + (Some(a), Some(b)) if matches!(k.as_str(), "volume" | "area") => (a - b).abs() <= REL * a.abs().max(b.abs()).max(1.0), + _ => w == g, + }; + if !same { + out.push(format!("{k}: {w} -> {g}")); + } + } + (!out.is_empty()).then(|| out.join("; ")) +} + +fn check(part: u64, parts: u64, every: u64) { + let seeds: Vec = (0..SEEDS).filter(|s| s % every == 0 && (s / every) % parts == part).collect(); + let got: BTreeMap = seeds.iter().map(|s| (*s, fuzz::golden(*s, MAX_STEPS))).collect(); + let _lock = FILE.lock().unwrap_or_else(|e| e.into_inner()); + let mut file = load(); + let changed: Vec<(u64, String)> = got + .iter() + .filter_map(|(seed, g)| { + let w = &file["seeds"][seed.to_string()]; + if w.is_null() { Some((*seed, "new".to_string())) } else { differs(w, g).map(|d| (*seed, d)) } + }) + .collect(); + if let Ok(reason) = std::env::var("ROBUSTNESS_GOLDENS_BLESS") { + assert!(!reason.trim().is_empty(), "ROBUSTNESS_GOLDENS_BLESS needs the reason for the change"); + if changed.is_empty() { + return; + } + if file.get("about").is_none() { + file["about"] = json!( + "What each robustness seed builds (crates/engine/src/tests/robustness_goldens.rs, docs/robustness.md). Record changes with ROBUSTNESS_GOLDENS_BLESS=\"why\"." + ); + } + for (seed, g) in &got { + file["seeds"][seed.to_string()] = g.clone(); + } + let mut notes = file["notes"].as_array().cloned().unwrap_or_default(); + let seeds: Vec = changed.iter().map(|(s, _)| *s).collect(); + // The parts of one record (the tests run side by side) share one note. + match notes.last_mut() { + Some(last) if last["reason"] == json!(reason) => { + let mut all: Vec = last["seeds"].as_array().into_iter().flatten().filter_map(Value::as_u64).chain(seeds).collect(); + all.sort_unstable(); + all.dedup(); + last["seeds"] = json!(all); + } + _ => notes.push(json!({"reason": reason, "seeds": seeds})), + } + file["notes"] = Value::Array(notes); + // Seeds in order, one per line, so a change is a small diff. + let seeds: BTreeMap = + file["seeds"].as_object().into_iter().flatten().filter_map(|(k, v)| Some((k.parse::().ok()?, v.clone()))).collect(); + let mut s = format!("{{\n \"about\": {},\n \"seeds\": {{\n", file["about"]); + let n = seeds.len(); + for (i, (k, v)) in seeds.iter().enumerate() { + s += &format!(" \"{k}\": {}{}\n", v, if i + 1 < n { "," } else { "" }); + } + let notes: Vec = file["notes"].as_array().into_iter().flatten().map(|n| format!(" {n}")).collect(); + s += &format!(" }},\n \"notes\": [\n{}\n ]\n}}\n", notes.join(",\n")); + std::fs::write(path(), s).unwrap_or_else(|e| panic!("{}: {e}", path().display())); + return; + } + assert!( + changed.is_empty(), + "{} seed(s) build differently from tests/robustness/goldens.json (if that is intended, record it with ROBUSTNESS_GOLDENS_BLESS=\"why\" and say why in the pull request):\n{}", + changed.len(), + changed.iter().map(|(s, d)| format!("seed {s}: {d}")).collect::>().join("\n") + ); +} + +#[test] +fn sample_0() { + check(0, 4, 8); +} + +#[test] +fn sample_1() { + check(1, 4, 8); +} + +#[test] +fn sample_2() { + check(2, 4, 8); +} + +#[test] +fn sample_3() { + check(3, 4, 8); +} + +#[test] +fn all_0() { + check(0, 4, 1); +} + +#[test] +fn all_1() { + check(1, 4, 1); +} + +#[test] +fn all_2() { + check(2, 4, 1); +} + +#[test] +fn all_3() { + check(3, 4, 1); +} diff --git a/docs/book/src/dev/testing.md b/docs/book/src/dev/testing.md index bf4b0055..aa458bea 100644 --- a/docs/book/src/dev/testing.md +++ b/docs/book/src/dev/testing.md @@ -76,7 +76,8 @@ On GitHub, one job builds the tests into a Pull requests skip a few heavy tests (each takes one to four minutes); they run on every push to `main`, nightly, and in `cargo xtask test`: -- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness`, +- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and + `tests::robustness_goldens` (pull requests that touch `crates/kernel` run its `sample_*` part), `pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`, `pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`, `cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall` diff --git a/docs/robustness.md b/docs/robustness.md index 70fa633d..31ebc181 100644 --- a/docs/robustness.md +++ b/docs/robustness.md @@ -36,6 +36,24 @@ rejected before any jobs or output directories are created. The maximum seed - `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 touch `crates/kernel` +run every 8th seed (`sample_0` … `sample_3`, about two minutes); pushes to `main` and the +nightly run them all (`all_0` … `all_3`). + +To record an intended change, give the reason: + +``` +ROBUSTNESS_GOLDENS_BLESS="why it changed" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens::all +``` + +The changed seeds and the reason are added to the file's `notes`; say why in the pull request too. + ## Successful Boolean retries When an accepted Boolean result needs a numerical retry, the feature shows a diff --git a/tests/robustness/goldens.json b/tests/robustness/goldens.json new file mode 100644 index 00000000..f0adb5f8 --- /dev/null +++ b/tests/robustness/goldens.json @@ -0,0 +1,104 @@ +{ + "about": "What each robustness seed builds (crates/engine/src/tests/robustness_goldens.rs, docs/robustness.md). Record changes with ROBUSTNESS_GOLDENS_BLESS=\"why\".", + "seeds": { + "0": {"area":4795.764399,"bodies":1,"edges":79,"error":null,"faces":29,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","hole","circular pattern","circular pattern","cut sphere","join cylinder","cut cylinder","fillet"],"unsupported":1,"volume":13466.472104}, + "1": {"area":2642.476675,"bodies":1,"edges":37,"error":null,"faces":15,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut cylinder","join cylinder"],"unsupported":0,"volume":6991.559474}, + "2": {"area":2493.96451,"bodies":1,"edges":59,"error":null,"faces":22,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut cylinder","fillet","cut sphere","rectangular pattern","cut sphere"],"unsupported":3,"volume":5020.244288}, + "3": {"area":6336.456108,"bodies":1,"edges":33,"error":null,"faces":13,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut box","hole","fillet","hole"],"unsupported":0,"volume":28037.146872}, + "4": {"area":5813.939544,"bodies":1,"edges":27,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut cylinder","fillet"],"unsupported":0,"volume":28462.605133}, + "5": {"area":1696.077144,"bodies":1,"edges":24,"error":"boolean Cut: the operation failed: no result; the operation ","faces":12,"outcome":"failed","rejected":1,"shells":1,"steps":["box","cut cylinder","hole","cut cylinder"],"unsupported":0,"volume":3520.994026}, + "6": {"area":9989.926765,"bodies":1,"edges":22,"error":"boolean Union: the operation failed: no result; the operatio","faces":9,"outcome":"failed","rejected":0,"shells":1,"steps":["box","hole","join box","join cylinder"],"unsupported":0,"volume":63836.414118}, + "7": {"area":7365.885875,"bodies":3,"edges":79,"error":null,"faces":34,"outcome":"ok","rejected":0,"shells":3,"steps":["extruded profile","join box","cut cylinder","join box","circular pattern","hole"],"unsupported":0,"volume":32867.700222}, + "8": {"area":2865.472238,"bodies":1,"edges":24,"error":null,"faces":10,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut cylinder"],"unsupported":0,"volume":10024.248065}, + "9": {"area":2434.461264,"bodies":1,"edges":38,"error":"lumps: shell boundaries intersect or touch; cavity ownership","faces":17,"outcome":"failed","rejected":0,"shells":2,"steps":["revolved section","chamfer","fillet","join box","cut sphere","rectangular pattern","rectangular pattern","cut box"],"unsupported":4,"volume":6547.123546}, + "10": {"area":2786.620097,"bodies":1,"edges":44,"error":null,"faces":18,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut box","chamfer","hole"],"unsupported":1,"volume":6668.980399}, + "11": {"area":7858.300481,"bodies":1,"edges":33,"error":null,"faces":13,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","hole","rectangular pattern","fillet"],"unsupported":0,"volume":34605.048259}, + "12": {"area":4668.664174,"bodies":2,"edges":62,"error":null,"faces":25,"outcome":"ok","rejected":0,"shells":2,"steps":["extruded profile","chamfer","join cylinder","join box","cut cylinder","circular pattern"],"unsupported":3,"volume":11874.471611}, + "13": {"area":1857.434659,"bodies":4,"edges":78,"error":null,"faces":34,"outcome":"ok","rejected":1,"shells":4,"steps":["revolved section","cut cylinder","join box","rectangular pattern","cut cylinder","cut box","fillet","hole"],"unsupported":0,"volume":2300.910644}, + "14": {"area":10553.267699,"bodies":1,"edges":43,"error":null,"faces":20,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut cylinder","shell","fillet"],"unsupported":0,"volume":10369.009812}, + "15": {"area":1974.784048,"bodies":1,"edges":40,"error":"boolean Cut: the operation failed: no result; the operation ","faces":18,"outcome":"failed","rejected":0,"shells":2,"steps":["revolved section","fillet","cut box","rectangular pattern","cut sphere","cut cylinder","cut sphere"],"unsupported":1,"volume":4781.224576}, + "16": {"area":8872.873884,"bodies":3,"edges":49,"error":null,"faces":23,"outcome":"ok","rejected":0,"shells":3,"steps":["box","join cylinder","circular pattern","hole"],"unsupported":0,"volume":44953.403148}, + "17": {"area":6048.425784,"bodies":1,"edges":56,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut box","shell","cut cylinder"],"unsupported":0,"volume":2968.531542}, + "18": {"area":5524.583818,"bodies":4,"edges":98,"error":"boolean Cut: the operation failed: no result; the operation ","faces":44,"outcome":"failed","rejected":0,"shells":4,"steps":["extruded profile","join cylinder","circular pattern","cut cylinder","hole","cut cylinder","hole","circular pattern"],"unsupported":0,"volume":21568.016708}, + "19": {"area":5938.978965,"bodies":1,"edges":74,"error":null,"faces":25,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut sphere","shell"],"unsupported":1,"volume":2942.886331}, + "20": {"area":5727.101213,"bodies":1,"edges":116,"error":null,"faces":45,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","fillet","cut box","cut cylinder","hole","circular pattern","cut box"],"unsupported":1,"volume":17142.726126}, + "21": {"area":5968.473044,"bodies":2,"edges":54,"error":null,"faces":21,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","join box","hole","fillet","hole","cut box"],"unsupported":1,"volume":18926.835174}, + "22": {"area":4634.464699,"bodies":2,"edges":108,"error":null,"faces":40,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","join box","join box","join box","rectangular pattern","cut cylinder"],"unsupported":0,"volume":7855.54666}, + "23": {"area":1795.696192,"bodies":1,"edges":16,"error":null,"faces":7,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut sphere"],"unsupported":0,"volume":3198.694598}, + "24": {"area":13432.401544,"bodies":1,"edges":86,"error":null,"faces":35,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer","hole","cut box","cut sphere","cut sphere","shell"],"unsupported":0,"volume":9985.175049}, + "25": {"area":6134.243434,"bodies":1,"edges":56,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","shell"],"unsupported":0,"volume":5828.585616}, + "26": {"area":3077.407407,"bodies":1,"edges":28,"error":"boolean Union: the operation failed: no result; the operatio","faces":14,"outcome":"failed","rejected":0,"shells":1,"steps":["revolved section","join box"],"unsupported":0,"volume":5890.433103}, + "27": {"area":11263.08174,"bodies":3,"edges":99,"error":null,"faces":42,"outcome":"ok","rejected":0,"shells":4,"steps":["box","chamfer","cut sphere","cut sphere","join box","circular pattern","hole","join box"],"unsupported":0,"volume":49451.079636}, + "28": {"area":2394.691384,"bodies":1,"edges":15,"error":null,"faces":9,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","join cylinder"],"unsupported":0,"volume":5902.121375}, + "29": {"area":6965.599057,"bodies":1,"edges":74,"error":null,"faces":30,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","shell","join cylinder"],"unsupported":0,"volume":3977.885339}, + "30": {"area":853.525389,"bodies":1,"edges":23,"error":null,"faces":10,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer"],"unsupported":0,"volume":1337.14821}, + "31": {"area":10195.468258,"bodies":3,"edges":75,"error":null,"faces":31,"outcome":"ok","rejected":1,"shells":3,"steps":["box","cut box","fillet","join box","circular pattern","cut box","circular pattern","hole"],"unsupported":1,"volume":56416.134415}, + "32": {"area":3220.802776,"bodies":1,"edges":51,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut sphere","cut box","cut box","join box"],"unsupported":0,"volume":7565.955387}, + "33": {"area":5269.06481,"bodies":2,"edges":64,"error":null,"faces":29,"outcome":"ok","rejected":0,"shells":3,"steps":["revolved section","fillet","join box","cut cylinder","join box","chamfer","fillet","fillet"],"unsupported":2,"volume":13840.242661}, + "34": {"area":1086.303472,"bodies":1,"edges":30,"error":null,"faces":14,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut sphere","hole"],"unsupported":0,"volume":2134.365275}, + "35": {"area":4605.630216,"bodies":1,"edges":26,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["box","fillet","join box"],"unsupported":0,"volume":13965.526982}, + "36": {"area":10408.82575,"bodies":1,"edges":60,"error":null,"faces":26,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","cut cylinder","join box","fillet","join cylinder"],"unsupported":0,"volume":55729.313461}, + "37": {"area":11714.822253,"bodies":1,"edges":44,"error":null,"faces":18,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut cylinder","join cylinder","cut sphere","circular pattern","fillet","circular pattern","rectangular pattern"],"unsupported":3,"volume":75556.602064}, + "38": {"area":11769.375817,"bodies":1,"edges":44,"error":"offset: a corner whose faces don't meet the same way after t","faces":12,"outcome":"failed","rejected":1,"shells":1,"steps":["box","hole","cut cylinder","shell"],"unsupported":0,"volume":78261.020973}, + "39": {"area":1892.050815,"bodies":1,"edges":27,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","chamfer","cut sphere","rectangular pattern"],"unsupported":1,"volume":4040.62787}, + "40": {"area":15548.516183,"bodies":1,"edges":49,"error":"offset: a corner whose faces don't meet the same way after t","faces":22,"outcome":"failed","rejected":0,"shells":1,"steps":["extruded profile","cut cylinder","shell","cut cylinder","join cylinder","shell"],"unsupported":0,"volume":16543.777645}, + "41": {"area":7611.393446,"bodies":1,"edges":99,"error":null,"faces":35,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut sphere","shell","fillet","cut cylinder","rectangular pattern","cut sphere","join cylinder"],"unsupported":2,"volume":5973.93328}, + "42": {"area":5486.881252,"bodies":1,"edges":45,"error":null,"faces":17,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut box","cut cylinder","rectangular pattern"],"unsupported":0,"volume":19095.110963}, + "43": {"area":2623.031137,"bodies":1,"edges":72,"error":"boolean Union: the operation failed: no result; the operatio","faces":26,"outcome":"failed","rejected":0,"shells":1,"steps":["extruded profile","cut cylinder","rectangular pattern","fillet","hole","rectangular pattern","join cylinder"],"unsupported":3,"volume":4325.21806}, + "44": {"area":7972.435565,"bodies":1,"edges":36,"error":null,"faces":15,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","shell","cut sphere"],"unsupported":0,"volume":7622.184369}, + "45": {"area":14416.876392,"bodies":2,"edges":62,"error":"boolean Union: implausible result volume ","faces":29,"outcome":"failed","rejected":0,"shells":3,"steps":["revolved section","join cylinder","fillet","shell","cut box","join box"],"unsupported":1,"volume":14286.791174}, + "46": {"area":3445.160599,"bodies":1,"edges":99,"error":null,"faces":31,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","join cylinder","hole","cut cylinder","rectangular pattern","cut box","cut cylinder"],"unsupported":0,"volume":6232.317431}, + "47": {"area":3098.575624,"bodies":1,"edges":42,"error":null,"faces":16,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","fillet","cut sphere","fillet"],"unsupported":0,"volume":8834.29745}, + "48": {"area":9651.031297,"bodies":1,"edges":66,"error":null,"faces":28,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","hole","circular pattern","join box","shell","cut box"],"unsupported":2,"volume":7044.773463}, + "49": {"area":7512.5,"bodies":1,"edges":24,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["box","join box"],"unsupported":0,"volume":40546.875}, + "50": {"area":9385.667614,"bodies":2,"edges":48,"error":null,"faces":21,"outcome":"ok","rejected":0,"shells":2,"steps":["extruded profile","fillet","join cylinder","join cylinder","fillet","cut box","cut cylinder"],"unsupported":2,"volume":40127.620224}, + "51": {"area":3316.022299,"bodies":1,"edges":51,"error":null,"faces":21,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut box","hole","rectangular pattern","chamfer","chamfer"],"unsupported":1,"volume":6812.582331}, + "52": {"area":7851.282527,"bodies":1,"edges":32,"error":null,"faces":14,"outcome":"ok","rejected":0,"shells":1,"steps":["box","shell","cut cylinder","chamfer","fillet"],"unsupported":0,"volume":5741.788461}, + "53": {"area":17168.464938,"bodies":1,"edges":110,"error":null,"faces":46,"outcome":"ok","rejected":0,"shells":4,"steps":["extruded profile","cut cylinder","shell","join cylinder","circular pattern","fillet"],"unsupported":1,"volume":16607.030819}, + "54": {"area":7080.555724,"bodies":4,"edges":90,"error":null,"faces":38,"outcome":"ok","rejected":0,"shells":4,"steps":["extruded profile","join box","circular pattern","cut cylinder","cut box","join cylinder","cut cylinder"],"unsupported":3,"volume":15737.224446}, + "55": {"area":6796.974648,"bodies":1,"edges":60,"error":"Body","faces":23,"outcome":"invalid","rejected":0,"shells":1,"steps":["box","fillet","cut sphere","circular pattern","rectangular pattern","cut sphere"],"unsupported":0,"volume":24741.239331}, + "56": {"area":4468.944174,"bodies":1,"edges":33,"error":null,"faces":13,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut box","chamfer"],"unsupported":0,"volume":11835.9375}, + "57": {"area":4678.619266,"bodies":1,"edges":63,"error":null,"faces":23,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut sphere","chamfer","circular pattern","cut sphere","cut sphere","cut cylinder"],"unsupported":2,"volume":16016.157732}, + "58": {"area":4197.04925,"bodies":1,"edges":42,"error":null,"faces":16,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","fillet","cut sphere"],"unsupported":0,"volume":12543.173615}, + "59": {"area":7770.5632,"bodies":1,"edges":36,"error":null,"faces":15,"outcome":"ok","rejected":0,"shells":1,"steps":["box","join box","join cylinder"],"unsupported":0,"volume":29735.209746}, + "60": {"area":3443.32863,"bodies":1,"edges":39,"error":null,"faces":15,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut sphere"],"unsupported":0,"volume":11779.204752}, + "61": {"area":5020.413157,"bodies":1,"edges":36,"error":"boolean Cut: the operation failed: no result; the operation ","faces":15,"outcome":"failed","rejected":0,"shells":1,"steps":["extruded profile","cut cylinder","shell","rectangular pattern"],"unsupported":0,"volume":4906.628359}, + "62": {"area":8771.003707,"bodies":1,"edges":60,"error":null,"faces":23,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut cylinder","circular pattern","cut box","cut box","join cylinder"],"unsupported":2,"volume":40107.001722}, + "63": {"area":3029.212486,"bodies":1,"edges":51,"error":"boolean Union: the bodies touch along faces that could not b","faces":19,"outcome":"failed","rejected":0,"shells":1,"steps":["revolved section","join box","cut cylinder","hole","cut cylinder","join box"],"unsupported":1,"volume":6571.859243}, + "64": {"area":4137.422404,"bodies":1,"edges":36,"error":null,"faces":14,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","join cylinder","hole"],"unsupported":1,"volume":8399.391489}, + "65": {"area":8469.64766,"bodies":1,"edges":30,"error":null,"faces":12,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut cylinder","fillet","rectangular pattern","circular pattern","circular pattern","fillet","cut box"],"unsupported":0,"volume":45143.068793}, + "66": {"area":9222.76638,"bodies":1,"edges":99,"error":null,"faces":37,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","join box","join box","join box","cut sphere","cut box"],"unsupported":1,"volume":35217.203139}, + "67": {"area":7942.099033,"bodies":2,"edges":85,"error":null,"faces":35,"outcome":"ok","rejected":0,"shells":2,"steps":["extruded profile","cut cylinder","hole","join cylinder","join box","hole","chamfer","rectangular pattern"],"unsupported":2,"volume":27607.71011}, + "68": {"area":7338.675126,"bodies":1,"edges":36,"error":null,"faces":14,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","hole","hole","rectangular pattern","rectangular pattern","fillet","circular pattern"],"unsupported":2,"volume":29780.742221}, + "69": {"area":6451.122768,"bodies":1,"edges":132,"error":"offset: a corner whose faces don't meet the same way after t","faces":27,"outcome":"failed","rejected":0,"shells":3,"steps":["revolved section","join box","shell","shell"],"unsupported":0,"volume":4761.477514}, + "70": {"area":3682.669157,"bodies":1,"edges":24,"error":"lumps: shell boundaries intersect or touch; cavity ownership","faces":12,"outcome":"failed","rejected":0,"shells":2,"steps":["extruded profile","join box","hole"],"unsupported":1,"volume":9155.027162}, + "71": {"area":3389.533577,"bodies":1,"edges":34,"error":"boolean Union: the operation failed: no result; the operatio","faces":14,"outcome":"failed","rejected":0,"shells":1,"steps":["revolved section","cut box","cut cylinder","hole","join cylinder"],"unsupported":0,"volume":14306.661547}, + "72": {"area":5770.270336,"bodies":2,"edges":46,"error":null,"faces":20,"outcome":"ok","rejected":0,"shells":2,"steps":["box","join cylinder","hole","join box","join box","cut box","cut cylinder","cut cylinder"],"unsupported":1,"volume":16081.801492}, + "73": {"area":7478.071654,"bodies":2,"edges":69,"error":"boolean Cut: implausible result volume ","faces":28,"outcome":"failed","rejected":0,"shells":2,"steps":["extruded profile","join box","hole","cut cylinder","hole","rectangular pattern"],"unsupported":4,"volume":34525.602795}, + "74": {"area":9294.57311,"bodies":1,"edges":22,"error":null,"faces":9,"outcome":"ok","rejected":0,"shells":1,"steps":["box","hole"],"unsupported":0,"volume":55866.183074}, + "75": {"area":7893.117931,"bodies":1,"edges":27,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut cylinder","chamfer","rectangular pattern","rectangular pattern","cut box"],"unsupported":0,"volume":38757.62083}, + "76": {"area":899.010133,"bodies":1,"edges":29,"error":null,"faces":13,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","fillet","cut cylinder"],"unsupported":0,"volume":1745.3677}, + "77": {"area":12157.059928,"bodies":1,"edges":126,"error":null,"faces":54,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","hole","circular pattern","rectangular pattern","circular pattern","join cylinder"],"unsupported":0,"volume":64306.616746}, + "78": {"area":2423.346733,"bodies":1,"edges":39,"error":"offset: a corner whose faces don't meet the same way after t","faces":15,"outcome":"failed","rejected":0,"shells":1,"steps":["revolved section","cut cylinder","cut box","shell"],"unsupported":0,"volume":3707.453171}, + "79": {"area":13585.252616,"bodies":1,"edges":27,"error":null,"faces":12,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut cylinder","join cylinder"],"unsupported":0,"volume":104810.763072}, + "80": {"area":8652.533342,"bodies":1,"edges":20,"error":null,"faces":9,"outcome":"ok","rejected":0,"shells":1,"steps":["box","fillet","fillet"],"unsupported":0,"volume":37387.449469}, + "81": {"area":4001.811798,"bodies":2,"edges":75,"error":null,"faces":29,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","cut sphere","circular pattern","rectangular pattern","hole","join cylinder","join box","cut sphere"],"unsupported":3,"volume":13139.562234}, + "82": {"area":3195.487113,"bodies":1,"edges":36,"error":null,"faces":13,"outcome":"ok","rejected":0,"shells":1,"steps":["box","hole","circular pattern","cut box"],"unsupported":0,"volume":5902.162954}, + "83": {"area":5125.0,"bodies":1,"edges":28,"error":"offset: a corner whose faces don't meet the same way after t","faces":12,"outcome":"failed","rejected":0,"shells":1,"steps":["box","join box","rectangular pattern","shell"],"unsupported":0,"volume":19250.0}, + "84": {"area":4906.192237,"bodies":1,"edges":67,"error":null,"faces":25,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","fillet","hole","hole","cut cylinder","cut box","fillet"],"unsupported":2,"volume":14219.865875}, + "85": {"area":1923.81126,"bodies":1,"edges":45,"error":"boolean Union: the operation failed: no result; the operatio","faces":19,"outcome":"failed","rejected":0,"shells":1,"steps":["revolved section","fillet","cut sphere","circular pattern","join cylinder","hole","join box"],"unsupported":1,"volume":4382.218633}, + "86": {"area":7885.25656,"bodies":1,"edges":66,"error":"offset: a corner whose faces don't meet the same way after t","faces":28,"outcome":"failed","rejected":0,"shells":3,"steps":["extruded profile","join cylinder","join cylinder","circular pattern","rectangular pattern","shell"],"unsupported":2,"volume":21498.106764}, + "87": {"area":4853.708989,"bodies":1,"edges":24,"error":null,"faces":10,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut box","rectangular pattern"],"unsupported":0,"volume":18568.611108}, + "88": {"area":3947.799648,"bodies":1,"edges":18,"error":null,"faces":8,"outcome":"ok","rejected":0,"shells":1,"steps":["box","hole"],"unsupported":0,"volume":12788.04706}, + "89": {"area":2512.5,"bodies":1,"edges":12,"error":null,"faces":6,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut box"],"unsupported":0,"volume":5906.25}, + "90": {"area":9703.747183,"bodies":1,"edges":30,"error":null,"faces":12,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","hole"],"unsupported":0,"volume":46947.022876}, + "91": {"area":4061.429312,"bodies":1,"edges":36,"error":null,"faces":14,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut cylinder","chamfer"],"unsupported":2,"volume":9885.654049}, + "92": {"area":2855.139226,"bodies":1,"edges":30,"error":null,"faces":12,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer","cut sphere"],"unsupported":0,"volume":9814.303984}, + "93": {"area":7266.404017,"bodies":7,"edges":115,"error":null,"faces":51,"outcome":"ok","rejected":0,"shells":7,"steps":["extruded profile","fillet","chamfer","join box","circular pattern","rectangular pattern","cut cylinder"],"unsupported":0,"volume":12734.393171}, + "94": {"area":3504.024629,"bodies":1,"edges":73,"error":null,"faces":27,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut cylinder","hole","rectangular pattern","cut box","cut box","join box"],"unsupported":2,"volume":5899.987584}, + "95": {"area":2339.366789,"bodies":1,"edges":27,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","hole","cut cylinder","cut sphere","chamfer","circular pattern"],"unsupported":1,"volume":6587.694591} + }, + "notes": [ + {"reason":"First record: what each seed builds on main at 1a0100cd","seeds":[0,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,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95]} + ] +}