#!/usr/bin/env python3 """ Regenerate the frozen rev-8.0.0 acceptance oracle. WHAT THIS IS ------------ The oracle is the acceptance criterion for any reimplementation of the rev-8 generators. A port is correct when it reproduces these values -- not when it looks right. The ten rejected cases are part of the contract: a port that accepts them is wrong. WHY IT IS PINNED ---------------- Every case here was produced by OpenSCAD 2021.01 with BOSL2 at commit 92d697c2856de2fed93a33e858068589cefc2898. Those versions live in a container image (tools/reference-toolchain) and nowhere else. The running application has no OpenSCAD dependency at all, so the host's OpenSCAD version cannot drift into this file. HOW THE HASH WORKS ------------------ `fixtures_sha256` covers the document WITHOUT that field, serialised with indent=2 and sort_keys=True. Hashing the file itself will not reproduce it. `--verify` performs the correct comparison. USAGE ----- python3 make_fixtures.py --verify # check the committed oracle python3 make_fixtures.py --regenerate # rebuild it (needs OpenSCAD + BOSL2) Run --regenerate inside the reference toolchain image, never on a host whose OpenSCAD version is merely similar. """ from __future__ import annotations import argparse import datetime import hashlib import json import subprocess import sys from pathlib import Path REVISION = "8.0.0" OPENSCAD_VERSION = "2021.01" BOSL2_COMMIT = "92d697c2856de2fed93a33e858068589cefc2898" HERE = Path(__file__).resolve().parent REPO = HERE.parents[1] SCAD_DIR = REPO / "legacy" / "openscad" OUT = HERE / f"strap-beam-fixtures-{REVISION}.json" GEN_3X = "strap-beam-3x.scad" GEN_4X = "strap-beam-4x.scad" P3 = ["Equilateral Triangle", "General Triangle", "A Frame", "Y", "T", "Three-Fin"] P4 = ["Square", "Rectangle", "Diamond", "Cross", "Four-Fin"] # Report keys carrying numeric values. Everything else stays a string. NUM = set( """STRAP_WIDTH_MM STRAP_THICK_MM BUNDLE_COUNT BUNDLE_THICK_MM CLEARANCE_MM WALL_INSIDE_MM WALL_OUTSIDE_MM WALL_EDGE_MM MIN_WALL_SPEC_MM MIN_WALL_ACTUAL_MM SECTION_AREA_MM2 SECTION_PARTS STRAP_CHANNELS ENVELOPE_X_MM ENVELOPE_Y_MM LENGTH_MM VOLUME_MM3 MASS_G LENGTH_FT RING_SCALE RING_CORNER_WEB_MM RING_CORNER_R_MAX_MM SPOKE_RADIUS_MM SPOKE_WEB_MM FIN_CORE_SIDE_MM FIN_PROJECTION_MM FIN_SETBACK_MM FIN_BORE_SIDE_MM FIN_JUNCTION_WEB_MM AF_APEX_SETBACK_MM AF_CROSSBAR_DEPTH_MM AF_APEX_WEB_MM AF_CROSSBAR_WEB_MM T_FLANGE_SPAN_MM""".split() ) # --------------------------------------------------------------------------- # Case matrix # --------------------------------------------------------------------------- def build_cases() -> list[dict]: cases: list[dict] = [] def case(gen: str, profile: str, label: str, defs: list[str]) -> None: cases.append({"generator": gen, "profile": profile, "label": label, "defs": defs}) for p in P3: case(GEN_3X, p, "default", []) for p in P4: case(GEN_4X, p, "default", []) # A bundle is a stack of laminae, not a thicker strap. for p in P3: for b in (2, 3): case(GEN_3X, p, f"bundle{b}", [f"-Dbundle_count={b}"]) for p in P4: for b in (2, 3): case(GEN_4X, p, f"bundle{b}", [f"-Dbundle_count={b}"]) # Conditioned intermediate stock (Utility Two) is a first-class input, not # an edge case. Steel strap thickness likewise. for p in P3 + P4: g = GEN_3X if p in P3 else GEN_4X case(g, p, "width13.4", ["-Dstrap_width_mm=13.4"]) case(g, p, "steel0.79", ["-Dstrap_thickness_mm=0.79"]) # The chirality regression. for p in P3 + P4: g = GEN_3X if p in P3 else GEN_4X case(g, p, "wall_asym", [ "-Dinside_wall_thickness_mm=0.8", "-Doutside_wall_thickness_mm=2.4", "-Dmin_wall_mm=0.8", ]) # Profile ranges, including boundaries that MUST be rejected. for a in (20, 30, 45, 53, 55, 70): case(GEN_3X, "A Frame", f"leg{a}", [f"-Da_frame_leg_angle_deg={a}"]) for r in (0, 1, 2, 2.8, 2.9, 5): case(GEN_3X, "Equilateral Triangle", f"cornerR{r}", [f"-Dring_corner_radius_mm={r}"]) for w in (0.4, 1.2, 2.0, 4.0): case(GEN_3X, "Equilateral Triangle", f"web{w}", [f"-Dring_corner_web_mm={w}", f"-Dmin_wall_mm={min(w, 1.2)}"]) for f in (0, 2, 6.25, 12, 25): case(GEN_3X, "Three-Fin", f"fin{f}", [f"-Dthree_fin_fin_mm={f}"]) for r in (0, 1, 2, 8): case(GEN_3X, "Three-Fin", f"fillet{r}", [f"-Dthree_fin_junction_round_mm={r}"]) for b in (1, 7.5, 15, 30): case(GEN_3X, "Three-Fin", f"bore{b}", [f"-Dthree_fin_bore_side_mm={b}"]) for a in (0, 17, 33, 90): case(GEN_3X, "Three-Fin", f"rot{a}", [f"-Dthree_fin_rotation_deg={a}"]) for w in (0, 0.5, 1.2, 4.0): case(GEN_3X, "Y", f"ywebb{w}", [f"-Dy_junction_web_mm={w}", f"-Dmin_wall_mm={max(0.01, min(w, 1.2))}"]) for w in (0, 0.25, 1.2, 3.0): case(GEN_3X, "T", f"tweb{w}", [f"-Dt_stem_web_mm={w}", f"-Dmin_wall_mm={max(0.01, min(w, 1.2))}"]) for h in (4, 8, 15.5, 30): case(GEN_3X, "General Triangle", f"h{h}", [f"-Dgeneral_triangle_height={h}"]) for a in (1.0, 1.6, 2.5, 4.0): case(GEN_4X, "Rectangle", f"aspect{a}", [f"-Drectangle_aspect={a}"]) for f in (0, 2, 6.25, 15): case(GEN_4X, "Four-Fin", f"fin{f}", [f"-Dfour_fin_fin_mm={f}"]) # Isolation: a foreign parameter must never affect this profile. case(GEN_3X, "Y", "isolate_aframe", ["-Da_frame_leg_angle_deg=89"]) case(GEN_3X, "Y", "isolate_threefin", ["-Dthree_fin_fin_mm=0"]) case(GEN_3X, "T", "isolate_gentri", ["-Dgeneral_triangle_height=0.001"]) case(GEN_3X, "Equilateral Triangle", "isolate_t", ["-Dt_stem_web_mm=0"]) return cases # --------------------------------------------------------------------------- # Execution # --------------------------------------------------------------------------- def run_case(generator: str, defs: list[str]) -> tuple[dict, str | None]: """Run one generator invocation; return (report, rejection_message).""" cmd = ["openscad", "-o", "/dev/null", "--export-format=asciistl", *defs, generator] proc = subprocess.run(cmd, cwd=SCAD_DIR, capture_output=True, text=True, timeout=900) text = proc.stdout + proc.stderr report: dict = {} rejection: str | None = None for line in text.splitlines(): if line.startswith('ECHO: "SB_') and "=" in line: key, _, value = line[len('ECHO: "SB_'):].rstrip('"').partition("=") if key == "END": continue if key in NUM: try: value = float(value) except ValueError: pass report[key] = value elif "[strap-beam]" in line: rejection = line.split("] ", 1)[-1].strip() return report, rejection def regenerate() -> int: cases = build_cases() results = [] for i, c in enumerate(cases, 1): defs = [f'-Dprofile_type="{c["profile"]}"'] + c["defs"] report, rejection = run_case(c["generator"], defs) results.append({ **c, "defs": defs, "outcome": "ok" if report else "rejected", "report": report or None, "rejection": rejection, }) print(f" [{i:3d}/{len(cases)}] {c['profile']:22s} {c['label']:16s} " f"{'ok' if report else 'REJECTED'}", file=sys.stderr) accepted = sum(1 for r in results if r["outcome"] == "ok") doc = { "schema": "strap-beam.fixtures/1", "generator_revision": REVISION, "frozen": datetime.date.today().isoformat(), "toolchain": {"openscad": OPENSCAD_VERSION, "bosl2_commit": BOSL2_COMMIT}, "note": ( "Acceptance oracle for any reimplementation of the rev-8 generators. " "MIN_WALL_ACTUAL_MM, SECTION_PARTS and STRAP_CHANNELS are the invariants; " "the solved values (RING_*, FIN_*, AF_*, SPOKE_*) pin the placement solvers. " "Rejected cases are part of the contract: a port that accepts them is wrong." ), "tolerance": {"lengths_mm": 1e-4, "areas_mm2": 1e-3, "note": "counts are exact"}, "summary": {"cases": len(results), "accepted": accepted, "rejected": len(results) - accepted}, "cases": results, } doc["fixtures_sha256"] = hashlib.sha256( json.dumps(doc, indent=2, sort_keys=True).encode() ).hexdigest() OUT.write_text(json.dumps(doc, indent=2, sort_keys=True)) print(f"\n{len(results)} cases: {accepted} accepted, {len(results) - accepted} rejected") print("sha256:", doc["fixtures_sha256"]) return 0 def verify() -> int: if not OUT.exists(): print(f"MISSING: {OUT}", file=sys.stderr) return 2 doc = json.loads(OUT.read_text()) recorded = doc.pop("fixtures_sha256", None) actual = hashlib.sha256(json.dumps(doc, indent=2, sort_keys=True).encode()).hexdigest() print(f"file: {OUT.name}") print(f"revision: {doc.get('generator_revision')}") print(f"toolchain: openscad {doc['toolchain']['openscad']}, " f"bosl2 {doc['toolchain']['bosl2_commit'][:10]}") print(f"summary: {doc['summary']}") print(f"recorded: {recorded}") print(f"computed: {actual}") if recorded == actual: print("\nOK - oracle is intact.") return 0 print("\nFAIL - the oracle has been modified. A drifting oracle is worse " "than no oracle. Investigate before proceeding.", file=sys.stderr) return 1 def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) g = ap.add_mutually_exclusive_group(required=True) g.add_argument("--verify", action="store_true", help="check the committed oracle") g.add_argument("--regenerate", action="store_true", help="rebuild it; requires OpenSCAD and BOSL2") args = ap.parse_args() return regenerate() if args.regenerate else verify() if __name__ == "__main__": raise SystemExit(main())