feat(task-8): ISO fastener and ISO 15 bearing tables, transcribed and pinned

engineering/mech/standards/fasteners.py carries ISO 4014:2011 / ISO 4017:2014
head dimensions (s, k), ISO 4032:2012 nut heights, ISO 7089:2000 washer
d1/d2/h and ISO 4762:2004 socket head cap screws, plus the ISO 261:1998 coarse
pitch column. bearings.py carries the ISO 15:2017 deep groove boundary
dimensions for 6000-6010, 6200-6210 and 6300-6310.

Only the ISO 261 first-choice sizes ship. M14/M18/M22/M27/M33, everything above
M36 (above M24 for ISO 4762), the 618x/619x/16xx bearing series, bore numbers
above 10 and every suffixed bearing variant are refused by name rather than
interpolated: a narrow table is merely narrow, a wrong row ships a wrong
workshop drawing. Across-corners e is derived geometry (2s/sqrt(3)), so no e
column had to be transcribed at all, and the ISO 8826-1 cross and ISO 8826-2
element-symbol fractions are declared drawing conventions, not table values.

No ActiveX member is called by this task, so no live AutoCAD probe was needed;
the two modules are pure tables and pure arithmetic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-22 18:53:17 +03:00
co-authored by Claude Opus 5
parent 2bb8044144
commit 49b972f007
4 changed files with 705 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
"""ISO 15 deep groove ball bearings, plus the ISO 8826 representation rules.
SOURCE
------
* ``DEEP_GROOVE`` - ISO 15:2017 Table 4: boundary dimensions ``d`` (bore),
``D`` (outside diameter) and ``B`` (width) for the radial bearings of
dimension series 10 (600x), 02 (620x) and 03 (630x).
* ``SOURCE_ISO_8826_1`` - ISO 8826-1:1989, general simplified representation:
the bearing is drawn as its envelope outline with an upright cross inside
that does not touch the outline.
* ``SOURCE_ISO_8826_2`` - ISO 8826-2:1994, particular simplified
representation: the envelope outline plus a symbol for the rolling element.
COVERAGE
--------
600x, 620x and 630x, bore numbers 00..10 (d = 10..50 mm) - 33 rows. Bore
numbers 00/01/02/03 are 10/12/15/17 mm by the ISO 15 rule; 04 and up are
``5 x bore number``. Deliberately **not** transcribed, and refused by name:
bore numbers above 10, the wide series 622x/623x, the light series 618x/619x
and 16xx, the inch series, and every suffixed variant (``6205-2RS``,
``6205-Z``) - a suffix changes width and sealing, not a number this module may
guess.
The chamfer dimensions ``r_s`` are not transcribed either: neither simplified
representation needs them, so no row carries a value nobody checked.
CROSS_FRACTION and BALL_SYMBOL_FRACTION are **declared drawing conventions**,
not transcribed dimensions. ISO 8826-1 fixes the *form* of the cross and
requires only that it not touch the outline; ISO 15 carries no ball diameter
at all, so the element symbol is sized as a fraction of the ring gap rather
than invented as a real ball.
"""
from __future__ import annotations
from . import Coverage, register
SOURCE_ISO_15 = (
"ISO 15:2017 Table 4 - rolling bearings, radial bearings, boundary dimensions "
"(dimension series 10, 02, 03)"
)
SOURCE_ISO_8826_1 = (
"ISO 8826-1:1989 - technical drawings, rolling bearings, general simplified representation"
)
SOURCE_ISO_8826_2 = (
"ISO 8826-2:1994 - technical drawings, rolling bearings, particular simplified representation"
)
#: Fraction of the half-width / half-gap used by the ISO 8826-1 upright cross.
#: A declared convention (see the module docstring), never a table value.
CROSS_FRACTION = 0.6
#: Fraction of the half-gap used by the ISO 8826-2 rolling-element symbol.
#: A declared convention, never a ball diameter.
BALL_SYMBOL_FRACTION = 0.6
BEARING_SERIES = ("600x", "620x", "630x")
_SERIES_PREFIX = {"60": "600x", "62": "620x", "63": "630x"}
#: ISO 15:2017 Table 4 - d, D, B in millimetres.
DEEP_GROOVE: dict[str, dict[str, float]] = {
# dimension series 10 (600x)
"6000": {"d": 10.0, "D": 26.0, "B": 8.0},
"6001": {"d": 12.0, "D": 28.0, "B": 8.0},
"6002": {"d": 15.0, "D": 32.0, "B": 9.0},
"6003": {"d": 17.0, "D": 35.0, "B": 10.0},
"6004": {"d": 20.0, "D": 42.0, "B": 12.0},
"6005": {"d": 25.0, "D": 47.0, "B": 12.0},
"6006": {"d": 30.0, "D": 55.0, "B": 13.0},
"6007": {"d": 35.0, "D": 62.0, "B": 14.0},
"6008": {"d": 40.0, "D": 68.0, "B": 15.0},
"6009": {"d": 45.0, "D": 75.0, "B": 16.0},
"6010": {"d": 50.0, "D": 80.0, "B": 16.0},
# dimension series 02 (620x)
"6200": {"d": 10.0, "D": 30.0, "B": 9.0},
"6201": {"d": 12.0, "D": 32.0, "B": 10.0},
"6202": {"d": 15.0, "D": 35.0, "B": 11.0},
"6203": {"d": 17.0, "D": 40.0, "B": 12.0},
"6204": {"d": 20.0, "D": 47.0, "B": 14.0},
"6205": {"d": 25.0, "D": 52.0, "B": 15.0},
"6206": {"d": 30.0, "D": 62.0, "B": 16.0},
"6207": {"d": 35.0, "D": 72.0, "B": 17.0},
"6208": {"d": 40.0, "D": 80.0, "B": 18.0},
"6209": {"d": 45.0, "D": 85.0, "B": 19.0},
"6210": {"d": 50.0, "D": 90.0, "B": 20.0},
# dimension series 03 (630x)
"6300": {"d": 10.0, "D": 35.0, "B": 11.0},
"6301": {"d": 12.0, "D": 37.0, "B": 12.0},
"6302": {"d": 15.0, "D": 42.0, "B": 13.0},
"6303": {"d": 17.0, "D": 47.0, "B": 14.0},
"6304": {"d": 20.0, "D": 52.0, "B": 15.0},
"6305": {"d": 25.0, "D": 62.0, "B": 17.0},
"6306": {"d": 30.0, "D": 72.0, "B": 19.0},
"6307": {"d": 35.0, "D": 80.0, "B": 21.0},
"6308": {"d": 40.0, "D": 90.0, "B": 23.0},
"6309": {"d": 45.0, "D": 100.0, "B": 25.0},
"6310": {"d": 50.0, "D": 110.0, "B": 27.0},
}
def bearing(designation) -> dict:
"""Boundary dimensions of one deep groove ball bearing.
Returns ``{"designation", "series", "source", "d", "D", "B"}``. A
designation outside the transcribed table is refused by name; nothing is
interpolated between series or bore numbers.
"""
key = " ".join(str(designation).split()).upper()
if key not in DEEP_GROOVE:
raise ValueError(
"ISO 15 deep groove ball bearings are transcribed here for the 600x, 620x "
f"and 630x series, bore numbers 00-10 (d = 10-50 mm); {key!r} is outside "
f"that table and is never interpolated ({SOURCE_ISO_15})."
)
return {
"designation": key,
"series": _SERIES_PREFIX[key[:2]],
"source": SOURCE_ISO_15,
**DEEP_GROOVE[key],
}
def list_bearings(series: str | None = None) -> tuple[dict, ...]:
"""Every transcribed bearing, optionally restricted to one series."""
rows = tuple(bearing(name) for name in DEEP_GROOVE)
if series is None:
return rows
name = " ".join(str(series).split()).lower()
if name not in BEARING_SERIES:
raise ValueError(
f"list_bearings: series must be one of {', '.join(BEARING_SERIES)}, got {series!r}."
)
return tuple(row for row in rows if row["series"] == name)
register("ISO 15", DEEP_GROOVE, Coverage(10.0, 50.0), SOURCE_ISO_15)
+269
View File
@@ -0,0 +1,269 @@
"""ISO metric fastener tables - transcribed from the standards, never derived.
SOURCE
------
* ``HEX_HEAD`` ISO 4014:2011 Table 1 (hexagon head bolts, product grades A
and B) and ISO 4017:2014 Table 1 (hexagon head screws, fully threaded). The
two standards carry the same width across flats ``s`` and head height ``k``
for every size shipped here, so one table serves both and the standard is a
property of the *part*, not a second copy of the table.
* ``HEX_NUT`` ISO 4032:2012 Table 1 (hexagon regular nuts, style 1):
width across flats ``s``, nut height ``m`` (max).
* ``WASHER`` ISO 7089:2000 Table 1 (plain washers, normal series, product
grade A): inside diameter ``d1``, outside diameter ``d2``, thickness ``h``.
* ``SOCKET_HEAD`` ISO 4762:2004 Table 1 (hexagon socket head cap screws): head
diameter ``dk`` (max), head height ``k`` (max), hexagon socket width across
flats ``s``, thread length ``b``.
* ``COARSE_PITCH`` ISO 261:1998, coarse pitch series.
COVERAGE (deliberately narrow - the table rule of the mech spec, section 5)
---------------------------------------------------------------------------
Only ISO 261 **first-choice** sizes are shipped:
* ISO 4014 / 4017 / 4032 / 7089 : M5 M6 M8 M10 M12 M16 M20 M24 M30 M36
* ISO 4762 : M3 M4 M5 M6 M8 M10 M12 M16 M20 M24
Second-choice sizes (M14, M18, M22, M27, M33) and everything beyond those
ranges are **not** shipped and are refused by name. A narrow table is merely
narrow; a wrong across-flats ships a wrong workshop drawing.
Two columns are deliberately absent:
* across-corners ``e`` - :func:`across_corners` returns the *nominal*
hexagon's ``2s/sqrt(3)`` instead. The standards' own ``e`` is a minimum
computed from ``s`` min and is product-grade dependent; a drawing of the
nominal part wants the nominal hexagon, so no ``e`` column is transcribed.
* the per-size nominal length ranges of ISO 4014 Table 1 - ``l`` is validated
as a finite positive number and the useful thread length comes from the
standard's own ``b`` rule in :func:`thread_length`.
"""
from __future__ import annotations
import math
from . import Coverage, register
SOURCE_ISO_4014 = "ISO 4014:2011 Table 1 - hexagon head bolts, product grades A and B"
SOURCE_ISO_4017 = "ISO 4017:2014 Table 1 - hexagon head screws, fully threaded"
SOURCE_ISO_4032 = "ISO 4032:2012 Table 1 - hexagon regular nuts, style 1"
SOURCE_ISO_7089 = "ISO 7089:2000 Table 1 - plain washers, normal series, product grade A"
SOURCE_ISO_4762 = "ISO 4762:2004 Table 1 - hexagon socket head cap screws"
SOURCE_ISO_261 = "ISO 261:1998 - ISO general purpose metric screw threads, coarse pitch series"
#: ISO 261:1998 coarse pitch, one entry per size any table below carries.
COARSE_PITCH: dict[str, float] = {
"M3": 0.5,
"M4": 0.7,
"M5": 0.8,
"M6": 1.0,
"M8": 1.25,
"M10": 1.5,
"M12": 1.75,
"M16": 2.0,
"M20": 2.5,
"M24": 3.0,
"M30": 3.5,
"M36": 4.0,
}
#: ISO 4014:2011 / ISO 4017:2014 Table 1 - s = across flats, k = head height.
HEX_HEAD: dict[str, dict[str, float]] = {
"M5": {"s": 8.0, "k": 3.5},
"M6": {"s": 10.0, "k": 4.0},
"M8": {"s": 13.0, "k": 5.3},
"M10": {"s": 16.0, "k": 6.4},
"M12": {"s": 18.0, "k": 7.5},
"M16": {"s": 24.0, "k": 10.0},
"M20": {"s": 30.0, "k": 12.5},
"M24": {"s": 36.0, "k": 15.0},
"M30": {"s": 46.0, "k": 18.7},
"M36": {"s": 55.0, "k": 22.5},
}
#: ISO 4032:2012 Table 1 - s = across flats, m = nut height (max).
HEX_NUT: dict[str, dict[str, float]] = {
"M5": {"s": 8.0, "m": 4.7},
"M6": {"s": 10.0, "m": 5.2},
"M8": {"s": 13.0, "m": 6.8},
"M10": {"s": 16.0, "m": 8.4},
"M12": {"s": 18.0, "m": 10.8},
"M16": {"s": 24.0, "m": 14.8},
"M20": {"s": 30.0, "m": 18.0},
"M24": {"s": 36.0, "m": 21.5},
"M30": {"s": 46.0, "m": 25.6},
"M36": {"s": 55.0, "m": 31.0},
}
#: ISO 7089:2000 Table 1 - d1 = bore, d2 = outside diameter, h = thickness.
WASHER: dict[str, dict[str, float]] = {
"M5": {"d1": 5.3, "d2": 10.0, "h": 1.0},
"M6": {"d1": 6.4, "d2": 12.0, "h": 1.6},
"M8": {"d1": 8.4, "d2": 16.0, "h": 1.6},
"M10": {"d1": 10.5, "d2": 20.0, "h": 2.0},
"M12": {"d1": 13.0, "d2": 24.0, "h": 2.5},
"M16": {"d1": 17.0, "d2": 30.0, "h": 3.0},
"M20": {"d1": 21.0, "d2": 37.0, "h": 3.0},
"M24": {"d1": 25.0, "d2": 44.0, "h": 4.0},
"M30": {"d1": 31.0, "d2": 56.0, "h": 4.0},
"M36": {"d1": 37.0, "d2": 66.0, "h": 5.0},
}
#: ISO 4762:2004 Table 1 - dk = head diameter (max), k = head height (max),
#: s = hexagon socket across flats, b = thread length.
SOCKET_HEAD: dict[str, dict[str, float]] = {
"M3": {"dk": 5.5, "k": 3.0, "s": 2.5, "b": 18.0},
"M4": {"dk": 7.0, "k": 4.0, "s": 3.0, "b": 20.0},
"M5": {"dk": 8.5, "k": 5.0, "s": 4.0, "b": 22.0},
"M6": {"dk": 10.0, "k": 6.0, "s": 5.0, "b": 24.0},
"M8": {"dk": 13.0, "k": 8.0, "s": 6.0, "b": 28.0},
"M10": {"dk": 16.0, "k": 10.0, "s": 8.0, "b": 32.0},
"M12": {"dk": 18.0, "k": 12.0, "s": 10.0, "b": 36.0},
"M16": {"dk": 24.0, "k": 16.0, "s": 14.0, "b": 44.0},
"M20": {"dk": 30.0, "k": 20.0, "s": 17.0, "b": 52.0},
"M24": {"dk": 36.0, "k": 24.0, "s": 19.0, "b": 60.0},
}
_TABLES: dict[str, tuple[dict[str, dict[str, float]], str]] = {
"ISO 4014": (HEX_HEAD, SOURCE_ISO_4014),
"ISO 4017": (HEX_HEAD, SOURCE_ISO_4017),
"ISO 4032": (HEX_NUT, SOURCE_ISO_4032),
"ISO 7089": (WASHER, SOURCE_ISO_7089),
"ISO 4762": (SOCKET_HEAD, SOURCE_ISO_4762),
}
THREAD_LENGTH_STANDARDS = ("ISO 4014", "ISO 4017", "ISO 4762")
def _key(size) -> str:
key = str(size).strip().upper()
if not key:
raise ValueError("fastener size must be a non-empty string such as 'M12'")
return key
def _row(table: dict, standard: str, source: str, key: str) -> dict:
if key not in table:
raise ValueError(
f"{standard} is transcribed here for {', '.join(table)} only; {key!r} is "
f"outside that table and is never interpolated ({source})."
)
return dict(table[key])
def nominal_diameter(size) -> float:
"""Nominal thread diameter in mm, e.g. ``'M12' -> 12.0``."""
key = _key(size)
if key not in COARSE_PITCH:
raise ValueError(
f"{SOURCE_ISO_261} is transcribed here for {', '.join(COARSE_PITCH)}; "
f"{key!r} is outside it."
)
return float(key[1:])
def thread_pitch(size) -> float:
"""ISO 261 coarse pitch in mm."""
key = _key(size)
if key not in COARSE_PITCH:
raise ValueError(
f"{SOURCE_ISO_261} is transcribed here for {', '.join(COARSE_PITCH)}; "
f"{key!r} is outside it."
)
return COARSE_PITCH[key]
def hex_head(size) -> dict:
"""ISO 4014 / ISO 4017 head row plus the nominal diameter and pitch."""
key = _key(size)
row = _row(HEX_HEAD, "ISO 4014 / ISO 4017", SOURCE_ISO_4014, key)
return {"size": key, "d": nominal_diameter(key), "p": thread_pitch(key), **row}
def hex_nut(size) -> dict:
"""ISO 4032 nut row plus the nominal diameter and pitch."""
key = _key(size)
row = _row(HEX_NUT, "ISO 4032", SOURCE_ISO_4032, key)
return {"size": key, "d": nominal_diameter(key), "p": thread_pitch(key), **row}
def washer(size) -> dict:
"""ISO 7089 washer row plus the mating thread's nominal diameter and pitch."""
key = _key(size)
row = _row(WASHER, "ISO 7089", SOURCE_ISO_7089, key)
return {"size": key, "d": nominal_diameter(key), "p": thread_pitch(key), **row}
def socket_head(size) -> dict:
"""ISO 4762 socket head cap screw row plus the nominal diameter and pitch."""
key = _key(size)
row = _row(SOCKET_HEAD, "ISO 4762", SOURCE_ISO_4762, key)
return {"size": key, "d": nominal_diameter(key), "p": thread_pitch(key), **row}
def thread_length(size, length: float, standard: str = "ISO 4014") -> float:
"""Useful thread length ``b`` for a nominal length ``l``, per the part standard.
ISO 4014 fixes ``b`` by rule rather than by table: ``2d + 6`` for
``l <= 125``, ``2d + 12`` for ``125 < l <= 200`` and ``2d + 25`` for
``l > 200``. ISO 4017 screws are threaded over their whole length. ISO 4762
tabulates ``b`` per size. In every case a screw shorter than its own ``b``
is threaded to the head, so the result is capped at ``l``.
"""
key = _key(size)
name = " ".join(str(standard).split())
if name not in THREAD_LENGTH_STANDARDS:
raise ValueError(
f"thread_length: standard must be one of {', '.join(THREAD_LENGTH_STANDARDS)}, "
f"got {standard!r}."
)
if isinstance(length, bool) or not isinstance(length, (int, float)):
raise ValueError(f"thread_length: length must be a number, got {length!r}")
l_mm = float(length)
if not math.isfinite(l_mm) or l_mm <= 0.0:
raise ValueError(f"thread_length: length must be finite and > 0, got {length!r}")
if name == "ISO 4017":
hex_head(key)
return l_mm
if name == "ISO 4762":
return min(socket_head(key)["b"], l_mm)
diameter = nominal_diameter(key)
hex_head(key) # ISO 4014 coverage: refuse a size the head table does not carry
if l_mm <= 125.0:
b = 2.0 * diameter + 6.0
elif l_mm <= 200.0:
b = 2.0 * diameter + 12.0
else:
b = 2.0 * diameter + 25.0
return min(b, l_mm)
def across_corners(s: float) -> float:
"""Across-corners width of the *nominal* hexagon: ``2s/sqrt(3)``.
Derived geometry, not a transcribed column - see the module docstring.
"""
if isinstance(s, bool) or not isinstance(s, (int, float)):
raise ValueError(f"across_corners: s must be a number, got {s!r}")
width = float(s)
if not math.isfinite(width) or width <= 0.0:
raise ValueError(f"across_corners: s must be finite and > 0, got {s!r}")
return 2.0 * width / math.sqrt(3.0)
def sizes(standard: str) -> tuple[str, ...]:
"""The transcribed sizes of one fastener standard, in table order."""
name = " ".join(str(standard).split()).upper()
if name not in _TABLES:
raise ValueError(
f"sizes: {standard!r} is not a fastener standard in this module; "
f"have {', '.join(_TABLES)}."
)
return tuple(_TABLES[name][0])
register("ISO 4014", HEX_HEAD, Coverage(5.0, 36.0), SOURCE_ISO_4014)
register("ISO 4017", HEX_HEAD, Coverage(5.0, 36.0), SOURCE_ISO_4017)
register("ISO 4032", HEX_NUT, Coverage(5.0, 36.0), SOURCE_ISO_4032)
register("ISO 7089", WASHER, Coverage(5.0, 36.0), SOURCE_ISO_7089)
register("ISO 4762", SOCKET_HEAD, Coverage(3.0, 24.0), SOURCE_ISO_4762)
+111
View File
@@ -0,0 +1,111 @@
"""ISO 15 boundary dimensions and the ISO 8826 representation conventions."""
from __future__ import annotations
import pytest
from engineering.mech.standards import standards
from engineering.mech.standards.bearings import (
BALL_SYMBOL_FRACTION,
BEARING_SERIES,
CROSS_FRACTION,
DEEP_GROOVE,
SOURCE_ISO_15,
SOURCE_ISO_8826_1,
SOURCE_ISO_8826_2,
bearing,
list_bearings,
)
@pytest.mark.parametrize(
("designation", "bore", "outer", "width"),
[
("6000", 10.0, 26.0, 8.0),
("6004", 20.0, 42.0, 12.0),
("6006", 30.0, 55.0, 13.0),
("6010", 50.0, 80.0, 16.0),
("6200", 10.0, 30.0, 9.0),
("6203", 17.0, 40.0, 12.0),
("6205", 25.0, 52.0, 15.0),
("6208", 40.0, 80.0, 18.0),
("6210", 50.0, 90.0, 20.0),
("6300", 10.0, 35.0, 11.0),
("6305", 25.0, 62.0, 17.0),
("6309", 45.0, 100.0, 25.0),
("6310", 50.0, 110.0, 27.0),
],
)
def test_named_rows_match_iso15(designation, bore, outer, width):
row = bearing(designation)
assert (row["d"], row["D"], row["B"]) == (bore, outer, width)
assert row["designation"] == designation
assert row["source"] == SOURCE_ISO_15
def test_bore_number_rule_holds_across_every_series():
"""ISO 15: bore 00/01/02/03 = 10/12/15/17 mm; 04 and up = 5 x bore number."""
for series in ("60", "62", "63"):
assert bearing(f"{series}00")["d"] == 10.0
assert bearing(f"{series}01")["d"] == 12.0
assert bearing(f"{series}02")["d"] == 15.0
assert bearing(f"{series}03")["d"] == 17.0
for n in range(4, 11):
assert bearing(f"{series}{n:02d}")["d"] == 5.0 * n
def test_outside_diameter_and_width_grow_with_the_series():
for n in range(0, 11):
light = bearing(f"60{n:02d}")
medium = bearing(f"62{n:02d}")
heavy = bearing(f"63{n:02d}")
assert light["d"] == medium["d"] == heavy["d"]
assert light["D"] < medium["D"] < heavy["D"]
assert light["B"] < medium["B"] < heavy["B"]
def test_coverage_refusals_name_the_table():
with pytest.raises(ValueError, match="ISO 15"):
bearing("6011")
with pytest.raises(ValueError, match="ISO 15"):
bearing("6211")
with pytest.raises(ValueError, match="ISO 15"):
bearing("6311")
with pytest.raises(ValueError, match="ISO 15"):
bearing("61805") # the 618x light series is not transcribed
with pytest.raises(ValueError, match="ISO 15"):
bearing("6205-2RS") # a seal suffix is a variant this table does not carry
def test_representation_fractions_are_declared_conventions_not_table_values():
assert 0.0 < CROSS_FRACTION < 1.0
assert 0.0 < BALL_SYMBOL_FRACTION < 1.0
assert "ISO 8826-1" in SOURCE_ISO_8826_1
assert "ISO 8826-2" in SOURCE_ISO_8826_2
assert "ISO 15:2017" in SOURCE_ISO_15
def test_the_element_symbol_always_fits_inside_the_ring_rectangle():
"""The declared fractions must never draw a symbol outside the envelope."""
for name in DEEP_GROOVE:
row = bearing(name)
half_width = row["B"] / 2.0
half_gap = (row["D"] - row["d"]) / 4.0
assert BALL_SYMBOL_FRACTION * half_gap < half_width
assert CROSS_FRACTION * half_width < half_width
assert CROSS_FRACTION * half_gap < half_gap
def test_list_bearings_filters_by_series():
assert len(DEEP_GROOVE) == 33
assert len(list_bearings()) == 33
assert BEARING_SERIES == ("600x", "620x", "630x")
assert {row["designation"] for row in list_bearings("620x")} == {
f"62{n:02d}" for n in range(11)
}
with pytest.raises(ValueError, match="620x"):
list_bearings("622x")
def test_the_table_registers_itself():
assert "ISO 15" in standards()
+189
View File
@@ -0,0 +1,189 @@
"""Provenance and coverage of the ISO fastener tables (the table rule, spec 5).
Every value asserted here is a named row of a named table. A row that could not
be verified against the standard is not in the module at all, so the coverage
refusals below are as load-bearing as the values.
"""
from __future__ import annotations
import math
import pytest
from engineering.mech.standards import standards
from engineering.mech.standards.fasteners import (
COARSE_PITCH,
HEX_NUT,
SOCKET_HEAD,
SOURCE_ISO_4014,
across_corners,
hex_head,
hex_nut,
nominal_diameter,
sizes,
socket_head,
thread_length,
thread_pitch,
washer,
)
def test_iso4014_named_rows_match_the_standard():
"""ISO 4014:2011 Table 1 - width across flats s and head height k."""
assert (hex_head("M6")["s"], hex_head("M6")["k"]) == (10.0, 4.0)
assert (hex_head("M8")["s"], hex_head("M8")["k"]) == (13.0, 5.3)
assert (hex_head("M10")["s"], hex_head("M10")["k"]) == (16.0, 6.4)
assert (hex_head("M12")["s"], hex_head("M12")["k"]) == (18.0, 7.5)
assert (hex_head("M16")["s"], hex_head("M16")["k"]) == (24.0, 10.0)
assert (hex_head("M20")["s"], hex_head("M20")["k"]) == (30.0, 12.5)
assert (hex_head("M24")["s"], hex_head("M24")["k"]) == (36.0, 15.0)
assert (hex_head("M30")["s"], hex_head("M30")["k"]) == (46.0, 18.7)
assert (hex_head("M36")["s"], hex_head("M36")["k"]) == (55.0, 22.5)
def test_iso4017_shares_the_iso4014_head():
"""ISO 4017 screws and ISO 4014 bolts have the same head at every size here."""
assert sizes("ISO 4017") == sizes("ISO 4014")
def test_iso4032_named_rows_match_the_standard():
"""ISO 4032:2012 Table 1 - nut height m (max)."""
assert hex_nut("M6")["m"] == 5.2
assert hex_nut("M8")["m"] == 6.8
assert hex_nut("M10")["m"] == 8.4
assert hex_nut("M12")["m"] == 10.8
assert hex_nut("M16")["m"] == 14.8
assert hex_nut("M20")["m"] == 18.0
assert hex_nut("M24")["m"] == 21.5
assert hex_nut("M30")["m"] == 25.6
assert hex_nut("M36")["m"] == 31.0
for size in HEX_NUT:
assert hex_nut(size)["s"] == hex_head(size)["s"]
def test_iso7089_named_rows_match_the_standard():
"""ISO 7089:2000 Table 1 - d1, d2, h (nominal), normal series, grade A."""
assert (washer("M6")["d1"], washer("M6")["d2"], washer("M6")["h"]) == (6.4, 12.0, 1.6)
assert (washer("M8")["d1"], washer("M8")["d2"], washer("M8")["h"]) == (8.4, 16.0, 1.6)
assert (washer("M10")["d1"], washer("M10")["d2"], washer("M10")["h"]) == (10.5, 20.0, 2.0)
assert (washer("M12")["d1"], washer("M12")["d2"], washer("M12")["h"]) == (13.0, 24.0, 2.5)
assert (washer("M16")["d1"], washer("M16")["d2"], washer("M16")["h"]) == (17.0, 30.0, 3.0)
assert (washer("M20")["d1"], washer("M20")["d2"], washer("M20")["h"]) == (21.0, 37.0, 3.0)
assert (washer("M24")["d1"], washer("M24")["d2"], washer("M24")["h"]) == (25.0, 44.0, 4.0)
assert (washer("M30")["d1"], washer("M30")["d2"], washer("M30")["h"]) == (31.0, 56.0, 4.0)
assert (washer("M36")["d1"], washer("M36")["d2"], washer("M36")["h"]) == (37.0, 66.0, 5.0)
def test_iso4762_named_rows_match_the_standard():
"""ISO 4762:2004 Table 1 - head dk (max), socket s, thread length b."""
assert (
socket_head("M3")["dk"],
socket_head("M3")["s"],
socket_head("M3")["b"],
) == (5.5, 2.5, 18.0)
assert (
socket_head("M5")["dk"],
socket_head("M5")["s"],
socket_head("M5")["b"],
) == (8.5, 4.0, 22.0)
assert (
socket_head("M8")["dk"],
socket_head("M8")["s"],
socket_head("M8")["b"],
) == (13.0, 6.0, 28.0)
assert (
socket_head("M12")["dk"],
socket_head("M12")["s"],
socket_head("M12")["b"],
) == (18.0, 10.0, 36.0)
assert (
socket_head("M20")["dk"],
socket_head("M20")["s"],
socket_head("M20")["b"],
) == (30.0, 17.0, 52.0)
assert (
socket_head("M24")["dk"],
socket_head("M24")["s"],
socket_head("M24")["b"],
) == (36.0, 19.0, 60.0)
# ISO 4762 head height equals the nominal thread diameter at every size here.
for size in SOCKET_HEAD:
assert socket_head(size)["k"] == nominal_diameter(size)
def test_iso261_coarse_pitch_column():
assert COARSE_PITCH == {
"M3": 0.5,
"M4": 0.7,
"M5": 0.8,
"M6": 1.0,
"M8": 1.25,
"M10": 1.5,
"M12": 1.75,
"M16": 2.0,
"M20": 2.5,
"M24": 3.0,
"M30": 3.5,
"M36": 4.0,
}
assert thread_pitch("M12") == 1.75
assert nominal_diameter("M12") == 12.0
def test_coverage_is_refused_at_both_ends_by_name():
with pytest.raises(ValueError, match="ISO 4014"):
hex_head("M3")
with pytest.raises(ValueError, match="ISO 4014"):
hex_head("M42")
with pytest.raises(ValueError, match="ISO 4032"):
hex_nut("M42")
with pytest.raises(ValueError, match="ISO 7089"):
washer("M3")
with pytest.raises(ValueError, match="ISO 4762"):
socket_head("M2")
with pytest.raises(ValueError, match="ISO 4762"):
socket_head("M30")
def test_second_choice_sizes_are_absent_not_interpolated():
for absent in ("M14", "M18", "M22", "M27", "M33"):
with pytest.raises(ValueError):
hex_head(absent)
with pytest.raises(ValueError):
hex_nut(absent)
def test_thread_length_follows_the_standards_own_b_rule():
assert thread_length("M12", 60.0) == pytest.approx(30.0) # 2d + 6, l <= 125
assert thread_length("M12", 150.0) == pytest.approx(36.0) # 2d + 12, 125 < l <= 200
assert thread_length("M12", 260.0) == pytest.approx(49.0) # 2d + 25, l > 200
assert thread_length("M12", 25.0) == pytest.approx(25.0) # never longer than the bolt
assert thread_length("M12", 60.0, "ISO 4017") == pytest.approx(60.0)
assert thread_length("M8", 30.0, "ISO 4762") == pytest.approx(28.0)
assert thread_length("M8", 20.0, "ISO 4762") == pytest.approx(20.0)
with pytest.raises(ValueError):
thread_length("M12", 0.0)
with pytest.raises(ValueError):
thread_length("M12", float("inf"))
with pytest.raises(ValueError, match="ISO 4014"):
thread_length("M12", 60.0, "DIN 931")
def test_across_corners_is_derived_geometry_not_a_transcribed_column():
assert across_corners(18.0) == pytest.approx(2.0 * 18.0 / math.sqrt(3.0))
assert across_corners(18.0) == pytest.approx(20.7846, abs=1e-4)
with pytest.raises(ValueError):
across_corners(0.0)
def test_every_table_registers_itself_with_the_shared_registry():
for name in ("ISO 4014", "ISO 4017", "ISO 4032", "ISO 7089", "ISO 4762"):
assert name in standards()
assert "ISO 4014:2011" in SOURCE_ISO_4014
def test_sizes_refuses_a_standard_this_module_does_not_carry():
assert sizes("ISO 4014")[0] == "M5"
with pytest.raises(ValueError, match="ISO 4014"):
sizes("ISO 15")