The compiler describes off-the-shelf hardware and generates the printed part that encloses, interfaces with, or augments it. The pallet strap is not the subject of the library, it is the first entry, and it was inlined into Geo rather than described. STOCK.md states what every entry must declare: designation, section, nominal versus actual, fit, stock tolerance, provenance. Geo conflates three things. Width, thickness and count are the stock. Clearance is the fit, a property of the joint. The wall thicknesses are the printed part policy. This commit names the first two and leaves Geo untouched, so no profile imports the new module and the frozen oracle cannot move. test_stock.py proves faithfulness by exact float equality against geom.records across 36 parameter combinations, 5 placements and 3 face modes. Mutation tested before landing: reversed vertex order, halved clearance, dropped lamina offset and a naive round cavity are each caught. Round cavities are circumscribed rather than inscribed. A vertices on circle polygon lies inside the nominal diameter and bites into it by r times one minus cos 180 over n, about 9.6 micron at 9 mm radius and 48 facets, which is enough to stop a press fit. Conduit is deliberately absent from the catalogue until a measurement or citation exists.
202 lines
7.5 KiB
Python
202 lines
7.5 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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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_entry_cannot_be_built_without_provenance():
|
|
with pytest.raises(ValueError, match="source is required"):
|
|
Provenance(source="", recorded="2026-08-22")
|
|
with pytest.raises(ValueError, match="recorded is required"):
|
|
Provenance(source="caliper", recorded="")
|
|
|
|
|
|
def test_negative_clearance_is_refused():
|
|
with pytest.raises(ValueError, match="negative"):
|
|
Fit(clearance=-0.1)
|
|
|
|
|
|
def test_catalogue_entries_all_carry_provenance():
|
|
assert CATALOGUE, "the catalogue is empty"
|
|
for name, entry in CATALOGUE.items():
|
|
assert entry.designation == name
|
|
assert entry.provenance.source.strip()
|
|
assert entry.provenance.recorded.strip()
|
|
|
|
|
|
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
|