fix(task-6): a reachable ISO 286 route, a tolerance schema that refuses, a reference dimension
Three review findings, all root causes in `engineering/mech/dimension.py`.
1. The ISO 286 fit path was unreachable from the module's own pipeline.
`resolve_tolerance` resolved `intent.fit` correctly, but nothing produced a
DimIntent carrying one: no feature and no segment has a fit, and
`dimension_intents` constructed every intent without `fit=`. Only a
hand-built DimIntent - which is what the tests used - reached the code, so
the spec's `mech_iso286_on_dimension` and the `mech_assembly` benchmark's
"ISO 286 fits on the bearing seats" had no route.
`dimension_intents(..., fits={key: code})` now stamps the code onto the
intent its key names, and the part's own diameters became addressable one
at a time through the new `segment_key(i, bore=...)` -> "segment[1]",
"segment[1].bore"; a feature's stay addressable by id. A key that names
nothing is refused with the names that exist, a malformed code is refused
where it is attached rather than at draw time, a fit on a reference
dimension or on one that already carries a tol is refused, and a fit on a
hole whose diameter moved into the hole table (which has no tolerance
column) is refused by name instead of vanishing. Task 7's planned
`dimension_part(..., fits=None)` passes straight through - the argument is
additive and keyword-only, so every call written against the pinned
signature keeps working.
The fit also now reaches the *text*: `intent_text` expands the `<>`
placeholder and appends the code, so `text_rects` measures the string that
is actually drawn and a hole that already names itself ("⌀6") keeps its own
text and gains the code.
2. An explicit tolerance was silently discarded when `tol` omitted "mode".
`mode = tol.get("mode", "none")` and `build_dim_override(0.05, 0.05,
"none", None)` returns `({}, None)` without raising, so a ±0.05 resolved,
echoed its own numbers back in the payload, and drew a dimension with no
DIMTOL/DIMTP/DIMTM at all. `DimIntent.tol` now has a schema - {"mode",
"upper", "lower"} and nothing else - validated by `_explicit_tol`: an
unknown key, a missing "mode", a non-dict, and deviations under a mode that
writes none are each refused by name. The mutual-exclusion guard reads
`tol is not None` rather than truthiness, so `tol={}` is refused too.
3. The ISO 129-1 "no measurement twice" test was vacuous and the property was
false. The old test built the key `_dedupe` uniquifies as the last step of
`dimension_intents`, so it could not fail; meanwhile `style="chain"` emitted
20, 40, 15 *and* 75 as four plain DIMENSIONs. `redundant_linear` now finds
the span that shorter ones tile end to end on the same axis and level, and
`dimension_intents` marks it as an auxiliary (reference) dimension - the new
`REFERENCE_FORMAT` "(<>)", which renders "(75)". The plan's own test still
holds (the 75 is present); it is no longer a fourth plain dimension. The
error is one-sided on purpose: a tiling that is found is real, a tiling the
greedy walk misses leaves the dimension plain, which is the direction the
repository's critique rule wants. Baseline and ordinate parenthesise
nothing, and two horizontal chains at different heights are not each other's
pieces.
Tests: the vacuous gate is replaced by four that fail against the pre-fix code
- `redundant_linear` on the shaft's plain intents returns (3,) there and ()
here; the parametrised "a tolerance is either refused or reaches the dimension
variables" gate resolves the mode-less shape to an empty DIMVAR override
there; and `fits=` is a TypeError there. `_dedupe` is now exercised through a
duplicate fed in via the view's own dims, which the collector does not
generate itself. 46 tests in tests/test_mech_dimension.py (was 23).
Live AutoCAD: no finding was measured live and no new ActiveX call was
written - this module is pure Python with no backend, no async and no DXF. A
DIMTOL probe against the running seat was attempted twice and
`GetActiveObject("AutoCAD.Application")` blocked past the ~90 s contention
window both times, so the live engine was unavailable; no scratch document was
created (the probe never got the application object) and none was left open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
01213fc5f5
commit
bd73b3a854
@@ -7,6 +7,7 @@ overlapping dimension text" is a measured property rather than a claim.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
@@ -16,15 +17,19 @@ from engineering.mech.dimension import (
|
||||
dimension_intents,
|
||||
hole_table_rows,
|
||||
intent_text,
|
||||
is_reference,
|
||||
layout_dimensions,
|
||||
measured_value,
|
||||
redundant_linear,
|
||||
resolve_tolerance,
|
||||
segment_key,
|
||||
text_rects,
|
||||
)
|
||||
from engineering.mech.features import Hole, Keyway
|
||||
from engineering.mech.part import PrismaticPart, RevolvedPart, Segment
|
||||
from engineering.mech.primitives import DimIntent
|
||||
from engineering.mech.views import build_view
|
||||
from engineering.tolerances import build_dim_override
|
||||
|
||||
|
||||
def shaft(*features) -> RevolvedPart:
|
||||
@@ -121,6 +126,175 @@ def test_a_fit_with_no_measurable_nominal_is_refused_rather_than_guessed():
|
||||
resolve_tolerance(intent)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tol",
|
||||
[
|
||||
{"mode": "symmetric", "upper": 0.05},
|
||||
{"mode": "deviation", "upper": 0.05, "lower": 0.02},
|
||||
{"mode": "limit", "upper": 30.05, "lower": 29.98},
|
||||
{"upper": 0.05, "lower": 0.05}, # the shape whose tolerance used to vanish
|
||||
{"mode": "none", "upper": 0.05},
|
||||
{},
|
||||
],
|
||||
)
|
||||
def test_a_tolerance_is_either_refused_or_reaches_the_dimension_variables(tol):
|
||||
"""Accepted and then written as nothing is the one answer that is not allowed.
|
||||
|
||||
`build_dim_override` defaults a missing mode to "none" and answers with an
|
||||
empty override, so a tol with no mode used to resolve, echo its own numbers
|
||||
back and draw a dimension carrying no DIMTOL/DIMTP/DIMTM at all.
|
||||
"""
|
||||
intent = DimIntent(kind="linear", p1=(0.0, 0.0), p2=(30.0, 0.0), tol=tol)
|
||||
try:
|
||||
resolved = resolve_tolerance(intent)
|
||||
except ValueError:
|
||||
return # refused up front, which is the other legal answer
|
||||
override, _ = build_dim_override(
|
||||
resolved["tol_upper"],
|
||||
resolved["tol_lower"],
|
||||
resolved["tol_mode"],
|
||||
resolved["text_override"],
|
||||
)
|
||||
assert override, f"{tol} resolved to {resolved} and wrote no dimension variable"
|
||||
|
||||
|
||||
def test_a_tolerance_with_no_mode_is_refused_rather_than_silently_discarded():
|
||||
intent = DimIntent(
|
||||
kind="linear", p1=(0.0, 0.0), p2=(30.0, 0.0), tol={"upper": 0.05, "lower": 0.05}
|
||||
)
|
||||
with pytest.raises(ValueError, match="needs an explicit 'mode'"):
|
||||
resolve_tolerance(intent)
|
||||
|
||||
|
||||
def test_an_empty_tol_is_refused_rather_than_read_as_no_tolerance():
|
||||
intent = DimIntent(kind="linear", p1=(0.0, 0.0), p2=(30.0, 0.0), tol={})
|
||||
with pytest.raises(ValueError, match="mode"):
|
||||
resolve_tolerance(intent)
|
||||
|
||||
|
||||
def test_a_tol_key_outside_the_schema_is_refused_by_name():
|
||||
intent = DimIntent(
|
||||
kind="linear",
|
||||
p1=(0.0, 0.0),
|
||||
p2=(30.0, 0.0),
|
||||
tol={"mode": "symmetric", "upper": 0.05, "plus": 0.05},
|
||||
)
|
||||
with pytest.raises(ValueError, match="plus"):
|
||||
resolve_tolerance(intent)
|
||||
|
||||
|
||||
def test_deviations_under_a_mode_that_writes_none_are_refused():
|
||||
intent = DimIntent(
|
||||
kind="linear", p1=(0.0, 0.0), p2=(30.0, 0.0), tol={"mode": "none", "upper": 0.05}
|
||||
)
|
||||
with pytest.raises(ValueError, match="writes no deviation"):
|
||||
resolve_tolerance(intent)
|
||||
|
||||
|
||||
def test_a_tol_that_is_not_a_dict_is_refused():
|
||||
intent = DimIntent(kind="linear", p1=(0.0, 0.0), p2=(30.0, 0.0), tol=0.05)
|
||||
with pytest.raises(ValueError, match="expected a dict"):
|
||||
resolve_tolerance(intent)
|
||||
|
||||
|
||||
# -- the ISO 286 route from the part to the dimension ------------------------
|
||||
|
||||
|
||||
def bush() -> RevolvedPart:
|
||||
"""A 60 mm bore housing: one segment, ⌀60 outside, ⌀40 bore."""
|
||||
return RevolvedPart(name="bush", segments=(Segment(60.0, 60.0, 40.0),))
|
||||
|
||||
|
||||
def test_a_fit_reaches_a_dimension_through_the_modules_own_pipeline():
|
||||
"""`mech_iso286_on_dimension`, end to end: a ⌀40 H7 bore carries +0.025 / 0.
|
||||
|
||||
`resolve_tolerance` always resolved a fit; nothing produced an intent that
|
||||
carried one, so the headline claim was reachable only from a hand-built
|
||||
DimIntent - which is what the tests used.
|
||||
"""
|
||||
part = bush()
|
||||
view = build_view(part, "front")
|
||||
intents = dimension_intents(part, view, fits={segment_key(0, bore=True): "H7"})
|
||||
fitted = [i for i in intents if i.fit]
|
||||
assert len(fitted) == 1
|
||||
assert measured_value(fitted[0]) == pytest.approx(40.0)
|
||||
resolved = resolve_tolerance(fitted[0])
|
||||
assert resolved["tol_upper"] == pytest.approx(0.025)
|
||||
assert resolved["tol_lower"] == pytest.approx(0.0)
|
||||
assert resolved["tol_mode"] == "deviation"
|
||||
assert resolved["text_override"] == "<> H7"
|
||||
# the fit has to survive the layout, or the number is right in the table
|
||||
# and absent from the sheet
|
||||
placed = layout_dimensions(view, intents)
|
||||
assert [i.fit for i in placed if i.fit] == ["H7"]
|
||||
|
||||
|
||||
def test_a_bearing_seat_takes_its_fit_by_segment():
|
||||
part = shaft()
|
||||
view = build_view(part, "front")
|
||||
intents = dimension_intents(part, view, fits={segment_key(1): "k6"})
|
||||
seat = [i for i in intents if i.fit]
|
||||
assert [i.feature for i in seat] == ["segment[1]"]
|
||||
assert measured_value(seat[0]) == pytest.approx(30.0)
|
||||
assert intent_text(seat[0]) == "⌀30 k6"
|
||||
|
||||
|
||||
def test_a_fit_on_a_feature_id_lands_on_that_features_dimension():
|
||||
part = plate(*grid(3))
|
||||
view = build_view(part, "front")
|
||||
intents = dimension_intents(part, view, fits={"h1": "H7"})
|
||||
assert [i.feature for i in intents if i.fit] == ["h1"]
|
||||
# a hole already names itself; the code is appended, never swallowed
|
||||
hole = next(i for i in intents if i.fit)
|
||||
assert intent_text(hole) == "⌀6 H7"
|
||||
assert resolve_tolerance(hole)["text_override"] == "⌀6 H7"
|
||||
|
||||
|
||||
def test_a_fit_widens_the_measured_label_so_the_layout_reserves_room():
|
||||
part = shaft()
|
||||
view = build_view(part, "front")
|
||||
plain = next(i for i in dimension_intents(part, view) if i.feature == segment_key(1))
|
||||
fitted = next(
|
||||
i
|
||||
for i in dimension_intents(part, view, fits={segment_key(1): "k6"})
|
||||
if i.feature == segment_key(1)
|
||||
)
|
||||
|
||||
def width(intent):
|
||||
rect = text_rects(layout_dimensions(view, [intent]))[0]
|
||||
return rect[2] - rect[0]
|
||||
|
||||
assert width(fitted) > width(plain)
|
||||
|
||||
|
||||
def test_a_fit_key_that_names_nothing_is_refused_with_the_names_that_exist():
|
||||
part = shaft()
|
||||
view = build_view(part, "front")
|
||||
with pytest.raises(ValueError, match=r"segment\[0\]"):
|
||||
dimension_intents(part, view, fits={"bearing_seat": "k6"})
|
||||
|
||||
|
||||
def test_a_malformed_fit_code_is_refused_where_it_is_attached():
|
||||
part = shaft()
|
||||
view = build_view(part, "front")
|
||||
with pytest.raises(ValueError, match="fit code"):
|
||||
dimension_intents(part, view, fits={segment_key(1): "H"})
|
||||
|
||||
|
||||
def test_a_fit_on_a_reference_dimension_is_refused():
|
||||
part = shaft()
|
||||
view = build_view(part, "front")
|
||||
with pytest.raises(ValueError, match="reference"):
|
||||
dimension_intents(part, view, fits={"part": "H7"})
|
||||
|
||||
|
||||
def test_a_fit_on_a_hole_that_moved_into_the_table_is_refused_by_name():
|
||||
part = plate(*grid(10))
|
||||
view = build_view(part, "front")
|
||||
with pytest.raises(ValueError, match="hole table"):
|
||||
dimension_intents(part, view, fits={"h1": "H7"}, hole_table_threshold=8)
|
||||
|
||||
|
||||
# -- collecting the intents --------------------------------------------------
|
||||
|
||||
|
||||
@@ -156,15 +330,79 @@ def test_a_feature_intent_survives_collection_and_keeps_its_feature_id():
|
||||
assert any(i.feature == "kw" for i in intents)
|
||||
|
||||
|
||||
def test_iso129_the_same_measurement_is_not_dimensioned_twice():
|
||||
def test_a_duplicate_intent_is_collapsed_rather_than_drawn_twice():
|
||||
"""`_dedupe` against an intent the collector did not generate itself.
|
||||
|
||||
Asserting uniqueness over what `dimension_intents` returns proves nothing -
|
||||
it is the key `_dedupe` uniquifies. Feeding the duplicate in through the
|
||||
view's own dims is what exercises the gate.
|
||||
"""
|
||||
part = shaft()
|
||||
view = build_view(part, "front")
|
||||
intents = dimension_intents(part, view)
|
||||
keys = [
|
||||
(i.kind, round(i.p1[0], 6), round(i.p1[1], 6), round(i.p2[0], 6), round(i.p2[1], 6))
|
||||
for i in intents
|
||||
once = dimension_intents(part, view)
|
||||
repeated = next(i for i in once if i.kind == "diameter")
|
||||
doubled = dataclasses.replace(view, dims=view.dims + (repeated._replace(text_at=None),))
|
||||
assert len(dimension_intents(part, doubled)) == len(once)
|
||||
|
||||
|
||||
def test_iso129_a_closed_chain_marks_the_overall_as_a_reference_dimension():
|
||||
"""20 + 40 + 15 and 75 state the same distance twice; ISO 129-1 parenthesises one.
|
||||
|
||||
The plan's own test asserts the 75 is present, which a bare fourth plain
|
||||
DIMENSION satisfies. What matters is that it is drawn as an auxiliary
|
||||
(reference) dimension.
|
||||
"""
|
||||
part = shaft()
|
||||
view = build_view(part, "front")
|
||||
linear = [i for i in dimension_intents(part, view, style="chain") if i.kind == "linear"]
|
||||
assert sorted(round(measured_value(i), 6) for i in linear) == [15.0, 20.0, 40.0, 75.0]
|
||||
overall = [i for i in linear if measured_value(i) == 75.0]
|
||||
assert len(overall) == 1
|
||||
assert is_reference(overall[0])
|
||||
assert intent_text(overall[0]) == "(75)"
|
||||
assert [i for i in linear if is_reference(i)] == overall
|
||||
|
||||
|
||||
def test_iso129_no_plain_dimension_restates_a_measurement_the_others_give():
|
||||
"""The real property, on every style and both part kinds."""
|
||||
parts = (shaft(Keyway(id="kw", segment=1, length=20.0)), plate(*grid(4)))
|
||||
for style in DIM_STYLES:
|
||||
for part in parts:
|
||||
view = build_view(part, "front")
|
||||
intents = dimension_intents(part, view, style=style)
|
||||
plain = [i for i in intents if not is_reference(i)]
|
||||
assert redundant_linear(plain) == (), f"{part.name} / {style}"
|
||||
|
||||
|
||||
def test_the_redundancy_check_fires_instead_of_restating_its_own_post_condition():
|
||||
"""A gate that cannot fail is not a gate: hand it the un-marked chain."""
|
||||
chain = [
|
||||
DimIntent(kind="linear", p1=(0.0, 0.0), p2=(20.0, 0.0)),
|
||||
DimIntent(kind="linear", p1=(20.0, 0.0), p2=(60.0, 0.0)),
|
||||
DimIntent(kind="linear", p1=(60.0, 0.0), p2=(75.0, 0.0)),
|
||||
DimIntent(kind="linear", p1=(0.0, 0.0), p2=(75.0, 0.0)),
|
||||
]
|
||||
assert len(keys) == len(set(keys))
|
||||
assert redundant_linear(chain) == (3,)
|
||||
baseline = [
|
||||
DimIntent(kind="linear", p1=(0.0, 0.0), p2=(20.0, 0.0)),
|
||||
DimIntent(kind="linear", p1=(0.0, 0.0), p2=(60.0, 0.0)),
|
||||
DimIntent(kind="linear", p1=(0.0, 0.0), p2=(75.0, 0.0)),
|
||||
]
|
||||
assert redundant_linear(baseline) == ()
|
||||
# two horizontal chains at different heights measure different things
|
||||
stacked = [
|
||||
DimIntent(kind="linear", p1=(0.0, 0.0), p2=(20.0, 0.0)),
|
||||
DimIntent(kind="linear", p1=(20.0, 5.0), p2=(75.0, 5.0)),
|
||||
DimIntent(kind="linear", p1=(0.0, 0.0), p2=(75.0, 0.0)),
|
||||
]
|
||||
assert redundant_linear(stacked) == ()
|
||||
|
||||
|
||||
def test_baseline_states_every_station_once_so_nothing_is_parenthesised():
|
||||
part = shaft()
|
||||
view = build_view(part, "front")
|
||||
intents = dimension_intents(part, view, style="baseline")
|
||||
assert not any(is_reference(i) for i in intents)
|
||||
|
||||
|
||||
def test_a_prismatic_front_view_gets_its_two_overall_sizes():
|
||||
|
||||
Reference in New Issue
Block a user