Files
mechanical-compiler/fixtures/strap-beam-8.0.0/make_fixtures.py
T
civicus-build c7e32d8e07 Seed repository: rev-8.0.0 reference, frozen oracle, toolchain, test harness
Reference implementation of the strap-beam generators at revision 8.0.0, kept
so the acceptance oracle can be regenerated. Not a live target; the running
application has no OpenSCAD dependency.

The oracle holds 123 frozen cases, 113 accepted and 10 rejected, produced by
OpenSCAD 2021.01 with BOSL2 at 92d697c2. The ten rejections are part of the
contract: a port that accepts them is wrong.

tests/test_oracle.py specifies the port API and was written before the port,
so the interface follows from what must be verified rather than what is
convenient to implement. Proven by adversarial stub: a build() that rejects
everything passes all 10 rejection tests and fails all 226 acceptance tests.
2026-08-18 07:30:17 -05:00

258 lines
10 KiB
Python

#!/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())