8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
141 lines
4.9 KiB
Python
141 lines
4.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""`pa_supports.support_positions` -- embeds as structural-model supports.
|
|
|
|
Only the pure function is tested here (no Revit, no live model): given
|
|
`ticketqc.member_boxes`-shaped boxes, a knowledge tree, and an `x_of_box`
|
|
callable, does it pick the right members, dedupe, sort, and say why? A
|
|
neutral family name (`PLATE_A`) stands in for the real plant plate marks
|
|
(P328 / P362) -- see CLAUDE.md, no client/job/plant naming in tracked files.
|
|
`read_supports` needs a live Revit `el`/`asm` and is exercised only through a
|
|
bridge dry-run, not here.
|
|
"""
|
|
from vibe import knowledge # noqa: F401 (registers the vibe stub)
|
|
from vibe import pa_supports as S
|
|
|
|
|
|
def _know(families):
|
|
tree = knowledge._deep_copy(knowledge.DEFAULTS)
|
|
tree.setdefault('panelanalysis', {})['supports'] = {'families': families}
|
|
return tree
|
|
|
|
|
|
def _box(family, x):
|
|
"""A minimal member_boxes-shaped box; x_of_box below just echoes it."""
|
|
return {'family': family, 'mark': None, 'category': 'Specialty Equipment',
|
|
'id': 1, 'lo': [0, 0, 0], 'hi': [0, 0, 0], '_x': x}
|
|
|
|
|
|
def _echo_x(box):
|
|
return box['_x']
|
|
|
|
|
|
def test_matches_plant_family_and_reports_why():
|
|
know = _know(['PLATE_A'])
|
|
boxes = [_box('PLATE_A', 2.0), _box('PLATE_A', 20.0)]
|
|
positions, why = S.support_positions(boxes, know, 25.0, _echo_x)
|
|
assert positions == [2.0, 20.0]
|
|
assert '2 matching member' in why
|
|
assert 'PLATE_A' in why
|
|
|
|
|
|
def test_match_is_case_insensitive_substring():
|
|
know = _know(['plate_a'])
|
|
boxes = [_box('GPLATE_A', 3.0), _box('PLATE_A_GALV', 15.0)]
|
|
positions, _why = S.support_positions(boxes, know, 25.0, _echo_x)
|
|
assert positions == [3.0, 15.0]
|
|
|
|
|
|
def test_non_matching_families_are_ignored():
|
|
know = _know(['PLATE_A'])
|
|
boxes = [_box('PLATE_A', 5.0), _box('QL062', 5.1), _box('MCC', 12.0)]
|
|
positions, _why = S.support_positions(boxes, know, 25.0, _echo_x)
|
|
assert positions == [5.0]
|
|
|
|
|
|
def test_dedupes_positions_within_quarter_foot_keeping_the_mean():
|
|
know = _know(['PLATE_A'])
|
|
# 5.00 and 5.20 are 0.20 ft apart (inside DEDUPE_FT) -> one support at
|
|
# their mean; 5.20 and 12.00 are far apart -> stays separate.
|
|
boxes = [_box('PLATE_A', 5.00), _box('PLATE_A', 5.20),
|
|
_box('PLATE_A', 12.00)]
|
|
positions, why = S.support_positions(boxes, know, 25.0, _echo_x)
|
|
assert positions == [5.10, 12.00]
|
|
assert '2 support position' in why
|
|
assert '3 matching member' in why
|
|
|
|
|
|
def test_dedupe_chains_through_a_group_not_just_the_first_pair():
|
|
know = _know(['PLATE_A'])
|
|
# Each successive gap is 0.2 ft (< 0.25), so all four chain into one
|
|
# group even though the first and last are 0.6 ft apart.
|
|
boxes = [_box('PLATE_A', x) for x in (1.0, 1.2, 1.4, 1.6)]
|
|
positions, _why = S.support_positions(boxes, know, 25.0, _echo_x)
|
|
assert positions == [1.3]
|
|
|
|
|
|
def test_positions_come_back_sorted_regardless_of_box_order():
|
|
know = _know(['PLATE_A'])
|
|
boxes = [_box('PLATE_A', 20.0), _box('PLATE_A', 2.0),
|
|
_box('PLATE_A', 11.0)]
|
|
positions, _why = S.support_positions(boxes, know, 25.0, _echo_x)
|
|
assert positions == [2.0, 11.0, 20.0]
|
|
|
|
|
|
def test_no_match_returns_empty_list_and_names_the_reason():
|
|
know = _know(['PLATE_A'])
|
|
boxes = [_box('QL062', 5.0), _box('MCC', 8.0)]
|
|
positions, why = S.support_positions(boxes, know, 25.0, _echo_x)
|
|
assert positions == []
|
|
assert 'no member matched' in why
|
|
assert 'panelanalysis.supports.families' in why
|
|
assert 'PLATE_A' in why
|
|
|
|
|
|
def test_empty_plant_list_returns_empty_list_and_says_so():
|
|
know = _know([])
|
|
boxes = [_box('PLATE_A', 5.0)]
|
|
positions, why = S.support_positions(boxes, know, 25.0, _echo_x)
|
|
assert positions == []
|
|
assert 'no member matched' in why
|
|
assert 'panelanalysis.supports.families' in why
|
|
|
|
|
|
def test_no_families_key_at_all_defaults_to_empty_and_no_match():
|
|
tree = knowledge._deep_copy(knowledge.DEFAULTS)
|
|
boxes = [_box('PLATE_A', 5.0)]
|
|
positions, why = S.support_positions(boxes, tree, 25.0, _echo_x)
|
|
assert positions == []
|
|
assert 'no member matched' in why
|
|
|
|
|
|
def test_empty_boxes_list_is_no_match():
|
|
know = _know(['PLATE_A'])
|
|
positions, why = S.support_positions([], know, 25.0, _echo_x)
|
|
assert positions == []
|
|
assert 'no member matched' in why
|
|
|
|
|
|
def test_x_of_box_returning_none_is_skipped_not_fatal():
|
|
know = _know(['PLATE_A'])
|
|
|
|
def flaky_x(box):
|
|
return None if box['_x'] < 0 else box['_x']
|
|
|
|
boxes = [_box('PLATE_A', -1.0), _box('PLATE_A', 4.0),
|
|
_box('PLATE_A', 9.0)]
|
|
positions, _why = S.support_positions(boxes, know, 25.0, flaky_x)
|
|
assert positions == [4.0, 9.0]
|
|
|
|
|
|
def test_x_of_box_raising_is_skipped_not_fatal():
|
|
know = _know(['PLATE_A'])
|
|
|
|
def raising_x(box):
|
|
if box['_x'] == 'boom':
|
|
raise ValueError('nope')
|
|
return box['_x']
|
|
|
|
boxes = [_box('PLATE_A', 'boom'), _box('PLATE_A', 6.0)]
|
|
positions, _why = S.support_positions(boxes, know, 25.0, raising_x)
|
|
assert positions == [6.0]
|