Files
mechanical-compiler/tests/test_stock.py
T
TheRON 70787ea17d stock: records delegates to the descriptor, and the descriptor stops gating
geom.records no longer computes the section, cavity or laminae. It calls mechcomp.stock, so the shape of a piece of stock is defined once. All 123 oracle cases unmoved: 482 passed. stock.py also becomes a leaf module, ending an import cycle with geom that resolved only by accident of ordering.

Three defects in f5651d3 corrected. Provenance raised on an empty source, which made an unattributed dimension unrepresentable and blocked the ordinary use of the tool: type what the caliper reads, print, measure the print, adjust. Provenance now records and travels with the output. Fit refused negative clearance on the argument that interference is not assemblable, which is a design judgement and not the compilers to make. Interference is now computed and reported. CATALOGUE read as a whitelist and is documented as starting points, with a test asserting an entry built from nothing is as valid as one pulled from the dict.

emt_template takes the diameter, fit, designation and note from the caller. There is no standards table and no lookup. A parametric compiler cannot require its subject to be catalogued before it will run.

STOCK.md section 5 amended, since the refusals were implementing it. An entry without provenance no longer fails to ship, it ships labelled unattributed. The principle that a number must not appear from nowhere looking authoritative survives; the door does not.

Tests compare records and stock against a hand transcription of the reference rather than against each other, which would be tautological after delegation. Mutation testing found four gaps before landing: a dropped lamina stacking offset, emt_template silently ignoring its fit argument, describe discarding the note exactly when provenance was unverified, and a guard on float arithmetic that asserted a tautology.
2026-08-23 10:24:39 -05:00

427 lines
17 KiB
Python

"""
Proof that the stock descriptor reproduces the existing geometry exactly.
STOCK.md section 6: expressing the strap through the descriptor rather than
beside it is the migration that proves the abstraction. If the descriptor
produces the same paths ``geom.records`` already produces, it is faithful and
the geometry layer can be rewired onto it. If it does not, it is wrong, and the
second catalogue entry would have inherited the flaw.
The comparison is EXACT -- ``==`` on floats, not ``approx``. The point is
byte-identical output, because the next step replaces one with the other and the
frozen oracle must not move by so much as a last-place unit.
Nothing here imports the build path. ``mechcomp.stock`` is not yet used by any
profile, so these tests cannot disturb the oracle even if they fail.
"""
from __future__ import annotations
import math
import pytest
from mechcomp.geom.records import Geo, Member, cavity_path, strap_layer_paths, strap_path
from mechcomp.stock import (
CATALOGUE,
Fit,
Provenance,
RectStock,
RoundStock,
inscribed_radius,
placed,
)
# The oracle's own parameter space: defaults plus every override that appears in
# a case label -- width13.4, steel0.79, bundle2, bundle3.
WIDTHS = [15.875, 13.4]
THICKNESSES = [0.508, 0.79]
COUNTS = [1, 2, 3]
CLEARANCES = [0.25, 0.0, 0.4]
# Placements chosen to exercise rotation, translation and every face mode.
PLACEMENTS = [
(0.0, 0.0, 0.0),
(3.5, -2.25, 90.0),
(-7.125, 11.0, 33.0),
(0.0, -9.5, -45.0),
(12.75, 12.75, 137.0), # the dihedral angle ROADMAP calls the real gap
]
FACES = [0, 1, -1]
def _geo(width, thickness, count, clearance):
return Geo(width=width, strap_t=thickness, count=count, clearance=clearance,
wall_inside=1.20, wall_outside=1.20, wall_edge=1.20, min_wall=1.20)
def _stock(width, thickness, count):
return RectStock(designation="test", width=width, thickness=thickness,
count=count,
provenance=Provenance(source="test", recorded="2026-08-22"))
def _cases():
for w in WIDTHS:
for t in THICKNESSES:
for n in COUNTS:
for c in CLEARANCES:
yield w, t, n, c
# ---------------------------------------------------------------------------
# Independent transcription of the reference
# ---------------------------------------------------------------------------
#
# geom.records now DELEGATES to mechcomp.stock, so comparing the two against
# each other proves nothing -- it would be the same code twice. Both are
# therefore compared against these, transcribed by hand from
# legacy/openscad/lib/sb-geom.scad and deliberately not importing either
# module. If this file and the port ever disagree, one of them is wrong and
# the suite says so instead of going quietly green.
def _ref_place(cx, cy, angle, path):
ca, sa = math.cos(math.radians(angle)), math.sin(math.radians(angle))
return [(p[0] * ca - p[1] * sa + cx, p[0] * sa + p[1] * ca + cy)
for p in path]
def _ref_local_rect(half_lead, half_trail, up, down):
return [(half_lead, -down), (half_lead, up),
(-half_trail, up), (-half_trail, -down)]
def _ref_cavity(cx, cy, angle, width, thickness, count, clearance):
cavity_w = width + 2.0 * clearance
cavity_t = count * thickness + 2.0 * clearance
return _ref_place(cx, cy, angle,
_ref_local_rect(cavity_w / 2.0, cavity_w / 2.0,
cavity_t / 2.0, cavity_t / 2.0))
def _ref_section(cx, cy, angle, width, thickness, count):
bundle_t = count * thickness
return _ref_place(cx, cy, angle,
_ref_local_rect(width / 2.0, width / 2.0,
bundle_t / 2.0, bundle_t / 2.0))
def _ref_laminae(cx, cy, angle, width, thickness, count):
out = []
for i in range(count):
y = (i - (count - 1) / 2.0) * thickness
t = thickness / 2.0
rect = _ref_local_rect(width / 2.0, width / 2.0, t, t)
out.append(_ref_place(cx, cy, angle,
[(p[0], p[1] + y) for p in rect]))
return out
def test_both_implementations_match_the_reference_transcription():
"""
The check that survives delegation.
Asserts records AND stock each equal a hand transcription of the
reference formula, exactly. This is what stops the faithfulness proof
from becoming a comparison of one module with itself.
"""
for w, t, n, c in _cases():
g = _geo(w, t, n, c)
s = _stock(w, t, n)
fit = Fit(clearance=c)
for cx, cy, angle in PLACEMENTS:
ref = _ref_cavity(cx, cy, angle, w, t, n, c)
assert cavity_path(Member(cx, cy, angle, 0), g) == ref, (
"geom.records diverged from the reference at "
"width=%s thickness=%s count=%s clearance=%s" % (w, t, n, c))
assert placed(s.cavity(fit), cx, cy, angle) == ref, (
"mechcomp.stock diverged from the reference at "
"width=%s thickness=%s count=%s clearance=%s" % (w, t, n, c))
ref_s = _ref_section(cx, cy, angle, w, t, n)
assert strap_path(Member(cx, cy, angle, 0), g) == ref_s
assert placed(s.section(), cx, cy, angle) == ref_s
# Laminae need the same independent check. Comparing stock
# against records here would be one function compared with
# itself, and a dropped stacking offset would go unnoticed --
# it did, in the first cut of this step.
ref_l = _ref_laminae(cx, cy, angle, w, t, n)
assert strap_layer_paths(Member(cx, cy, angle, 0), g) == ref_l, (
"geom.records laminae diverged at width=%s thickness=%s "
"count=%s" % (w, t, n))
assert [placed(p, cx, cy, angle) for p in s.laminae()] == ref_l, (
"mechcomp.stock laminae diverged at width=%s thickness=%s "
"count=%s" % (w, t, n))
def test_cavity_expression_forms_agree_across_the_operating_range():
"""
``(w + 2c)/2`` versus ``w/2 + c``, and why the reference form is kept.
Step 2 changed ``rect_cavity`` to the reference's expression on the
suspicion that the two could differ in IEEE 754 and so move the frozen
oracle after delegation. MEASURED: they cannot, for any operand this
compiler will ever see. ``2*c`` and ``x/2`` are exact scalings, so both
forms reduce to the correctly rounded sum of the same two reals,
halved. They diverge only where scaling changes the exponent regime --
subnormals and overflow -- and neither is a stock dimension. Four
million random draws in range produced no divergence.
The reference form is kept regardless, because matching the source
character for character is worth having when that source is the only
evidence the geometry is right. But it is kept for fidelity, not for
numerical necessity, and this test says so instead of pretending to
guard something unfalsifiable.
Consequence for anyone mutation-testing this file: swapping the two
forms back is a mutation that SHOULD survive. It is a bad mutation,
not a test gap -- HANDOFF section 8.
"""
import random
from mechcomp.stock import rect_cavity
rng = random.Random(20260823)
for _ in range(20000):
w = rng.uniform(1e-6, 1e5)
c = rng.uniform(0.0, 1e3)
assert (w + 2.0 * c) / 2.0 == w / 2.0 + c, (
"the two forms diverged in the operating range at w=%r c=%r -- "
"if this ever fires, the choice of expression is load-bearing "
"and every stock dimension needs auditing" % (w, c))
assert rect_cavity(w, w, c)[0][0] == (w + 2.0 * c) / 2.0
# The regimes where they genuinely do differ, so the claim above is
# grounded in a demonstration rather than in an argument.
assert (1.5e-323 + 2.0 * 5e-324) / 2.0 != 1.5e-323 / 2.0 + 5e-324
assert (1e308 + 2.0 * 1e308) / 2.0 != 1e308 / 2.0 + 1e308
def test_stock_degree_helpers_match_geoms_bit_for_bit():
"""
stock.py defines its own cos_d/sin_d so it can stay a leaf module with no
mechcomp imports. Duplication is only safe while the two agree exactly.
"""
from mechcomp.geom.primitives import cos_d as geom_cos, sin_d as geom_sin
from mechcomp.stock import cos_d as stock_cos, sin_d as stock_sin
for a in [0, 1, 30, 33, 45, 60, 90, 120, 137, 180, 270, 359.9, -45, 720]:
assert stock_cos(a) == geom_cos(a), "cos_d diverged at %s" % a
assert stock_sin(a) == geom_sin(a), "sin_d diverged at %s" % a
# ---------------------------------------------------------------------------
# Faithfulness
# ---------------------------------------------------------------------------
def test_cavity_matches_records_exactly():
"""RectStock.cavity, placed, equals geom.records.cavity_path bit for bit."""
for w, t, n, c in _cases():
g = _geo(w, t, n, c)
s = _stock(w, t, n)
fit = Fit(clearance=c)
for cx, cy, angle in PLACEMENTS:
for face in FACES:
want = cavity_path(Member(cx, cy, angle, face), g)
got = placed(s.cavity(fit), cx, cy, angle)
assert got == want, (
"cavity diverged at width=%s thickness=%s count=%s "
"clearance=%s placement=(%s,%s,%s)\n records: %r\n stock: %r"
% (w, t, n, c, cx, cy, angle, want, got))
def test_section_matches_records_exactly():
"""RectStock.section, placed, equals geom.records.strap_path bit for bit."""
for w, t, n, c in _cases():
g = _geo(w, t, n, c)
s = _stock(w, t, n)
for cx, cy, angle in PLACEMENTS:
want = strap_path(Member(cx, cy, angle, 0), g)
got = placed(s.section(), cx, cy, angle)
assert got == want, (
"section diverged at width=%s thickness=%s count=%s "
"placement=(%s,%s,%s)" % (w, t, n, cx, cy, angle))
def test_laminae_match_records_exactly():
"""Per-layer paths match, including the count>1 stacking offset."""
for w, t, n, c in _cases():
g = _geo(w, t, n, c)
s = _stock(w, t, n)
for cx, cy, angle in PLACEMENTS:
want = strap_layer_paths(Member(cx, cy, angle, 0), g)
got = [placed(p, cx, cy, angle) for p in s.laminae()]
assert got == want, (
"laminae diverged at width=%s thickness=%s count=%s" % (w, t, n))
def test_stack_matches_geo_bundle_t():
for w, t, n, c in _cases():
assert _stock(w, t, n).stack == _geo(w, t, n, c).bundle_t
# ---------------------------------------------------------------------------
# Round stock -- the outward approximation
# ---------------------------------------------------------------------------
def test_round_cavity_is_never_smaller_than_required():
"""
The tightest point of a round cavity is at least the required radius.
This is the property that decides whether conduit goes in. A vertices-on-
circle polygon -- what OpenSCAD's circle() gives -- fails it, so the failure
is asserted too, to show the test can distinguish the two.
"""
for diameter in (17.93, 23.42, 6.0, 50.0):
for facets in (12, 24, 48, 96):
for clearance in (0.0, 0.25, 0.5):
stock = RoundStock(
designation="test", diameter=diameter, facets=facets,
provenance=Provenance(source="test", recorded="2026-08-22"))
fit = Fit(clearance=clearance)
required = stock.cavity_tight_radius(fit)
tight = inscribed_radius(stock.cavity(fit))
assert tight >= required - 1e-9, (
"cavity is tighter than required at d=%s n=%s clr=%s: "
"%.9f < %.9f -- the stock would not go in"
% (diameter, facets, clearance, tight, required))
assert tight == pytest.approx(required, abs=1e-9), (
"cavity is looser than it needs to be at d=%s n=%s: "
"%.9f vs %.9f" % (diameter, facets, tight, required))
def test_inscribed_polygon_would_fail_the_same_check():
"""
The naive approximation really is too small, by the expected amount.
Guards against the outward-growth test passing vacuously. At 48 facets a
9 mm radius hole drawn the naive way is ~9.6 um undersized -- small, and
entirely capable of stopping a press fit.
"""
from mechcomp.stock import _polygon
for r in (3.0, 9.0, 25.0):
for n in (12, 48):
tight = inscribed_radius(_polygon(r, n))
expected = r * math.cos(math.radians(180.0 / n))
assert tight == pytest.approx(expected, rel=1e-12)
assert tight < r
# ---------------------------------------------------------------------------
# Provenance and fit are enforced, not decorative
# ---------------------------------------------------------------------------
def test_unattributed_dimensions_are_representable_and_say_so():
"""
The ordinary case must work: type the number, print, measure, adjust.
An earlier version raised here, which made a parametric compiler refuse
to run until someone had done paperwork. Provenance records; it does not
gate.
"""
bare = Provenance()
assert bare.verified is False
assert "unverified" in bare.describe()
stock = RoundStock(designation="whatever conduit", diameter=23.4,
provenance=bare)
assert stock.cavity(Fit(clearance=0.25))
cited = Provenance(source="caliper", recorded="2026-08-23", note="3 samples")
assert cited.verified is True
assert "caliper" in cited.describe() and "3 samples" in cited.describe()
def test_interference_fit_is_allowed_and_reported():
"""
Negative clearance is an interference fit, not an error.
Whether the parts press together is the operator's call. The compiler's
job is to compute the cavity and state what it did.
"""
tight = Fit(clearance=-0.05)
assert tight.is_interference
assert tight.interference == 0.05
assert "interference" in tight.describe()
loose = Fit(clearance=0.25)
assert not loose.is_interference
assert loose.interference == 0.0
assert "clearance" in loose.describe()
assert "line-to-line" in Fit(clearance=0.0).describe()
# The cavity really is smaller than the stock under interference.
stock = RoundStock(designation="rod", diameter=20.0,
provenance=Provenance())
assert inscribed_radius(stock.cavity(tight)) < 10.0
assert stock.cavity_tight_radius(tight) == pytest.approx(9.95)
def test_any_diameter_is_accepted_and_fine_tuning_moves_the_cavity():
"""
The tuning loop, asserted: a small change in the input must produce a
correspondingly small, monotonic change in the hole.
"""
from mechcomp.stock import emt_template
previous = 0.0
for diameter in (12.7, 17.93, 21.0, 23.4, 26.65, 40.0, 63.5):
stock = emt_template(diameter)
r = inscribed_radius(stock.cavity(Fit(clearance=0.25)))
assert r == pytest.approx(diameter / 2.0 + 0.25, abs=1e-9)
assert r > previous
previous = r
base = emt_template(23.4)
for step in (0.05, 0.10, 0.15):
loose = inscribed_radius(base.cavity(Fit(clearance=0.25 + step)))
tight = inscribed_radius(base.cavity(Fit(clearance=0.25 - step)))
assert loose - tight == pytest.approx(2 * step, abs=1e-9)
# The template must carry every argument through, not just the diameter.
# A default_fit that ignores its argument would leave the entry's own
# suggested fit permanently wrong while every explicit Fit still worked.
for fit in (0.0, 0.1, 0.35, -0.05):
assert emt_template(23.4, fit=fit).default_fit == fit
named = emt_template(23.4, designation="EMT 1/2 in, measured 2026-08",
note="3 samples, mid-run")
assert named.designation == "EMT 1/2 in, measured 2026-08"
assert "3 samples" in named.provenance.describe()
assert named.provenance.verified is False
def test_catalogue_entries_are_starting_points_not_a_whitelist():
"""
Membership means nothing. Entries exist so common cases need not be
retyped, and every one of them is overridable.
"""
import dataclasses
assert CATALOGUE
for name, entry in CATALOGUE.items():
assert entry.designation == name
assert entry.provenance.describe()
strap = CATALOGUE["PET strap 15.875 x 0.508"]
wider = dataclasses.replace(strap, width=19.0, designation="wider strap")
assert wider.width == 19.0
assert strap.width == 15.875, "the catalogue entry was mutated"
# An entry built from nothing is as valid as one pulled from the dict.
adhoc = RectStock(designation="scrap banding", width=11.1, thickness=0.4,
provenance=Provenance())
assert adhoc.cavity(Fit(clearance=0.2))
def test_catalogue_strap_matches_the_reference_defaults():
"""The first entry is the strap rev 8.0.0 was built around."""
strap = CATALOGUE["PET strap 15.875 x 0.508"]
assert strap.width == 15.875
assert strap.thickness == 0.508
assert strap.count == 1
assert strap.default_fit == 0.25