refactor(understand): one reading of units, plan copies, scale and rooms
Groups U and N were built side by side, so the takeoffs carried narrow stand-ins for what group U reads properly. On main they read through U: - describe.drawing_units is the one call every reader makes (kept records, labelled room faces as evidence); describe, scale_check and the takeoffs all use it. Without the room faces, a layout that declares inches over millimetres can tie three ways and fall to the header. - layout_tags takes its plan copies from describe.prepare, room_regions its rooms from describe.find_rooms, and pipe_rows its scale from scale_check, per-room groups included. scale_stand_in and the takeoff's own unit bands and cluster grid are gone. - test_understand_one_reading pins the agreement: one unit, one plan copy, one verdict and one set of rooms per drawing, across drawing_understand, drawing_scale_check and the two takeoffs. Co-Authored-By: Claude Opus 5.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.5
parent
c4759e696d
commit
8c1a6cb27c
@@ -0,0 +1,132 @@
|
||||
"""One reading of a drawing: the takeoffs see what drawing_understand and drawing_scale_check see.
|
||||
|
||||
Groups U and N were built side by side from one base, so Task 6 carried narrow
|
||||
stand-ins: its own unit bands, a 2 % grid of plan copies, a scale statistic
|
||||
without room groups, its own room faces. On main the takeoffs read units, plan
|
||||
copies, scale and rooms through group U's readers, so the four tools never
|
||||
give one drawing two units, two plan copies, two verdicts or two sets of rooms.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from engineering.understand import takeoff
|
||||
from engineering.understand.describe import describe, drawing_units, prepare
|
||||
from engineering.understand.network import build_network
|
||||
from engineering.understand.scale import scale_check
|
||||
from engineering.understand.snapshot import Snapshot, read_snapshot
|
||||
from tests.fixtures.plant_pair import build_plant_pair
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def plant(tmp_path_factory):
|
||||
truth = build_plant_pair(tmp_path_factory.mktemp("one_reading"))
|
||||
return truth, read_snapshot(truth["pid"]), read_snapshot(truth["layout"])
|
||||
|
||||
|
||||
def empty(insunits):
|
||||
return Snapshot(
|
||||
source="empty.dxf",
|
||||
insunits=insunits,
|
||||
extmin=None,
|
||||
extmax=None,
|
||||
layers={},
|
||||
layouts=(),
|
||||
records=(),
|
||||
)
|
||||
|
||||
|
||||
def test_one_unit_per_drawing(plant):
|
||||
_truth, pid, layout = plant
|
||||
check = scale_check(pid, layout)
|
||||
for snap, side in ((pid, "a"), (layout, "b")):
|
||||
unit = takeoff.unit_of(snap)
|
||||
assert unit["inferred"] == describe(snap)["units"]["inferred"]
|
||||
assert unit["inferred"] == check["units"][side]["unit"]
|
||||
assert takeoff.unit_of(layout)["inferred"] == "mm" # INSUNITS says inches: Review Focus 5
|
||||
|
||||
|
||||
def test_the_takeoff_unit_carries_drawing_units_evidence_and_warning(plant):
|
||||
_truth, pid, layout = plant
|
||||
for snap in (pid, layout):
|
||||
mine, theirs = takeoff.unit_of(snap), drawing_units(snap)
|
||||
assert mine["declared"] == theirs["declared"]["name"]
|
||||
assert mine["m_per_unit"] == pytest.approx(theirs["mm_per_unit"] / 1000.0)
|
||||
assert mine["evidence"] == theirs["evidence"]
|
||||
expected = f"{snap.source}: {theirs['warning']}" if theirs["warning"] else None
|
||||
assert mine["warning"] == expected
|
||||
|
||||
|
||||
def test_a_unit_nothing_measures_keeps_the_declared_one():
|
||||
unit = takeoff.unit_of(empty(5))
|
||||
assert (unit["declared"], unit["inferred"], unit["m_per_unit"], unit["warning"]) == (
|
||||
"cm",
|
||||
"cm",
|
||||
0.01,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def test_a_unitless_drawing_nothing_measures_is_read_in_mm_and_says_so():
|
||||
unit = takeoff.unit_of(empty(0))
|
||||
assert (unit["declared"], unit["inferred"], unit["m_per_unit"]) == (None, "mm", 0.001)
|
||||
assert unit["warning"] == (
|
||||
"empty.dxf: INSUNITS declares Unitless (0) and no measurement settles the unit; "
|
||||
"the takeoff reads it as mm."
|
||||
)
|
||||
|
||||
|
||||
def test_plan_copies_are_the_clusters_drawing_understand_reports(plant):
|
||||
truth, _pid, layout = plant
|
||||
found = takeoff.layout_tags(layout, truth["tag_positions"]["layout"])
|
||||
boxes = [tuple(cluster["box"]) for cluster in prepare(layout)["clusters"]]
|
||||
assert found["clusters"] == len(boxes) == truth["cluster_count"]
|
||||
assert tuple(found["cluster"]) in boxes
|
||||
|
||||
|
||||
def test_the_takeoff_scale_is_drawing_scale_check(plant):
|
||||
_truth, pid, layout = plant
|
||||
result = takeoff.pipe_rows(build_network(pid), pid, layout)
|
||||
check = scale_check(pid, layout)
|
||||
for key in ("verdict", "factor", "median", "within_10pct", "pair_count", "groups"):
|
||||
assert result["scale"][key] == check[key], key
|
||||
|
||||
|
||||
def test_rooms_are_the_rooms_drawing_understand_reports(plant):
|
||||
truth, _pid, layout = plant
|
||||
cluster = takeoff.layout_tags(layout, truth["tag_positions"]["layout"])["cluster"]
|
||||
regions = takeoff.room_regions(layout, cluster)
|
||||
assert sorted(key for key, _face in regions["faces"]) == sorted(
|
||||
str(room["number"]) for room in truth["rooms"]
|
||||
)
|
||||
reported = {
|
||||
(str(row["number"] or row["name"]), round(row["face"]["area"], 3))
|
||||
for row in describe(layout)["rooms"]
|
||||
if row["face"] is not None
|
||||
}
|
||||
for key, face in regions["faces"]:
|
||||
assert (key, round(face.area, 3)) in reported
|
||||
|
||||
|
||||
def test_no_second_reader_is_left():
|
||||
for name in (
|
||||
"scale_stand_in",
|
||||
"INSUNITS_NAMES",
|
||||
"TEXT_BAND_M",
|
||||
"SPAN_BAND_M",
|
||||
"TYPICAL_SPAN_M",
|
||||
"TYPICAL_TEXT_M",
|
||||
"CLUSTER_GAP_SHARE",
|
||||
"MIN_PAIR_MM",
|
||||
"MIN_TAGS",
|
||||
"SCALE_BAND",
|
||||
"TO_SCALE_SHARE",
|
||||
"SCHEMATIC_SHARE",
|
||||
"_model_points",
|
||||
"_robust_box",
|
||||
"_text_heights",
|
||||
"_mark_segment",
|
||||
"_clusters",
|
||||
):
|
||||
assert not hasattr(takeoff, name), name
|
||||
Reference in New Issue
Block a user