route.py: Manhattan distance, the rectilinear minimum spanning tree (Prim over |dx| + |dy|, exact on the points given) and the x-then-y route. pipe_rows turns the P&ID's runs into rows by room x service x diameter. length_source auto measures on the P&ID only when the scale check calls it to scale; otherwise each run is the RMST of its tags' layout positions plus vertical_allowance per connection, with the drawn length kept beside it as schematic_m. length_source=pid on a schematic P&ID is refused with the statistics unless force=True. Status A / B / C; a C run goes to control with its schematic length, counted and flagged. Routed lengths are shared among the layout rooms their route crosses (track F's face finder); the allowance is its own column. Lengths are metres from the unit the geometry implies: a layout declaring inches over millimetres is read in mm and warned about, never multiplied by 25.4. The takeoff carries narrow stand-ins for the unit inference, the plan-copy clusters and the scale check (group U's modules are on another branch); a scale result passed in is used first. Tags written twice in the chosen cluster are ambiguous, never picked. Beyond the plan: the scale stand-in calls a non-positive median ratio insufficient (as group U's scale_check does) instead of to_scale with a factor of 0; an INSUNITS code the reader does not convert is named in the unit warning instead of being called "no unit"; wall layers are classified once per layer, not once per record. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
49 lines
1.6 KiB
Python
49 lines
1.6 KiB
Python
"""Orthogonal lengths: Manhattan distance, the rectilinear MST, the x-then-y route.
|
|
|
|
Every expected value is worked by hand in the comment beside it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from engineering.understand.route import manhattan, rmst, route_legs
|
|
|
|
EPS = 1e-9
|
|
|
|
|
|
def test_manhattan_is_the_sum_of_the_axis_distances():
|
|
assert manhattan((0.0, 0.0), (3.0, -4.0)) == 7.0 # 3 + 4, not the diagonal 5
|
|
|
|
|
|
def test_fewer_than_two_points_have_no_tree():
|
|
assert rmst([]) == (0.0, ())
|
|
assert rmst([(5.0, 5.0)]) == (0.0, ())
|
|
|
|
|
|
def test_a_two_ended_run_is_dx_plus_dy():
|
|
length, edges = rmst([(1000.0, 3000.0), (29000.0, 15000.0)])
|
|
assert abs(length - 40000.0) < EPS # 28000 + 12000
|
|
assert edges == ((0, 1),)
|
|
|
|
|
|
def test_a_square_of_four_tags_is_three_sides():
|
|
# Prim from point 0: (0,1) at 10 (tie with 3 goes to the lower index),
|
|
# then (1,2) at 10, then (0,3) at 10 -> 30, never the 40 of the full ring
|
|
length, edges = rmst([(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)])
|
|
assert abs(length - 30.0) < EPS
|
|
assert edges == ((0, 1), (1, 2), (0, 3))
|
|
|
|
|
|
def test_three_tags_in_a_row_chain_instead_of_a_star():
|
|
length, edges = rmst([(0.0, 0.0), (100.0, 0.0), (200.0, 0.0)])
|
|
assert abs(length - 200.0) < EPS # 100 + 100, not 100 + 200
|
|
assert edges == ((0, 1), (1, 2))
|
|
|
|
|
|
def test_the_route_runs_along_x_first_then_y():
|
|
assert route_legs((0.0, 0.0), (30.0, 40.0)) == (
|
|
((0.0, 0.0), (30.0, 0.0)),
|
|
((30.0, 0.0), (30.0, 40.0)),
|
|
)
|
|
assert route_legs((0.0, 0.0), (0.0, 40.0)) == (((0.0, 0.0), (0.0, 40.0)),)
|
|
assert route_legs((1.0, 1.0), (1.0, 1.0)) == ()
|