Files
willtools-demo/WillTools.extension/tests/test_strandmatrix.py
T
will 8e6ee31596
ci / test (push) Successful in 6m18s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
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.
2026-09-09 08:32:15 -05:00

446 lines
18 KiB
Python

# -*- coding: utf-8 -*-
"""Tests for vibe.strandmatrix -- reading the strand matrix off drawing geometry.
The three real cases below (numbers only -- see CLAUDE.md: no client/job
names, filenames or sheet text ever leaves the evidence file) come from
sample_job's FSA5 (slab, three rows split across two vertical chains), WPA2
(wall, full grid, two-dimension-run thickness) and WPA3 (wall, full grid,
single vertical chain) design evidence: symbol counts, dimension text and
totals match those summary views' `dimensions` / `components` exactly.
Coordinates are invented but internally consistent -- built the same way
`_dimension()` in designspec.py actually measures a drafting view (view y is
up, true scale, so `value_ft` and symbol x/y share units with the printed
dimension text).
FSA5's real, live-measured symbol geometry put its 19 FilledRegion symbols in
THREE y-clusters, not two: 11 at the bottom, 5 at +4" above it and 3 at +6"
above it -- the top horizontal dimension chain is a PLAN PROJECTION across
both of the upper rows, and the two vertical chains only give the row depths
in UNION ('2"|8"' gives 2", '6"|2"|2"' gives 6" and 8"; together, 2", 6", 8").
"""
from vibe import strandmatrix as sm
# --------------------------------------------------------------- fixture helpers
def _sym(x, y, kind='FilledRegion'):
return {'kind': kind, 'x': x, 'y': y, 'w': 0.05, 'h': 0.05}
def _seg(value, value_ft, x):
return {'value': value, 'value_ft': value_ft, 'x': x}
def _hchain(y, parts):
"""A multi-segment horizontal dimension: parts = [(text, value_ft, x), ...]."""
return {'axis': 'horizontal', 'y': y,
'segments': [_seg(v, vf, x) for (v, vf, x) in parts]}
def _vchain(parts):
"""A vertical dimension: parts = [(text, value_ft, y), ...], any order --
_vertical_candidates sorts by y (bottom to top) itself."""
return {'axis': 'vertical',
'segments': [{'value': v, 'value_ft': vf, 'x': 4.0, 'y': y}
for (v, vf, y) in parts]}
IN = 1.0 / 12.0 # one inch, in feet -- makes the fixtures read like the drawing
# ====================================================================== FSA5
# slab, three rows. qty 19, thickness 10", width 13'-8" = 164".
#
# 11 strands @ from_bottom 2" -- every column
# 5 strands @ from_bottom 6" -- columns 68, 92, 116, 140, 158
# 3 strands @ from_bottom 8" -- columns 2, 20, 44
def _fsa5_view():
x0 = 5.0
row0_cols = [2, 8, 20, 32, 44, 56, 68, 92, 116, 140, 158] # 2" -- all 11
row1_cols = [68, 92, 116, 140, 158] # 6" -- upper 5
row2_cols = [2, 20, 44] # 8" -- upper 3
y0 = 13.7
y1 = y0 + 4 * IN
y2 = y0 + 6 * IN
symbols = (
[_sym(x0 + p * IN, y0) for p in row0_cols] +
[_sym(x0 + p * IN, y1) for p in row1_cols] +
[_sym(x0 + p * IN, y2) for p in row2_cols]
)
# bottom (closed) chain: 12 segments across 3 multi-segment dims + 1
# single, banded at label y ~13.6, concatenated in ascending x. Total
# 164" -- closes to the far edge, matching the piece width.
bottom_cum = [2, 8, 20, 32, 44, 56, 68, 92, 116, 140, 158, 164]
bottom_dim1 = _hchain(13.58, [
('2"', 2 * IN, x0 + bottom_cum[0] * IN),
('6"', 6 * IN, x0 + bottom_cum[1] * IN),
("1' - 0\"", 12 * IN, x0 + bottom_cum[2] * IN),
("1' - 0\"", 12 * IN, x0 + bottom_cum[3] * IN),
("1' - 0\"", 12 * IN, x0 + bottom_cum[4] * IN),
])
bottom_dim2 = _hchain(13.59, [
("1' - 0\"", 12 * IN, x0 + bottom_cum[5] * IN),
("1' - 0\"", 12 * IN, x0 + bottom_cum[6] * IN),
("2' - 0\"", 24 * IN, x0 + bottom_cum[7] * IN),
])
bottom_single = _hchain(13.60, [("2' - 0\"", 24 * IN, x0 + bottom_cum[8] * IN)])
bottom_dim4 = _hchain(13.61, [
("2' - 0\"", 24 * IN, x0 + bottom_cum[9] * IN),
("1' - 6\"", 18 * IN, x0 + bottom_cum[10] * IN),
('6"', 6 * IN, x0 + bottom_cum[11] * IN),
])
# plan-projection (open) chain: single 8-segment dim, stops at the last
# strand -- 158" total, NOT the 164" width -- and covers 8 of the 11
# columns, the union of the 6" and 8" rows.
top_cum = [2, 20, 44, 68, 92, 116, 140, 158]
top_dim = _hchain(15.15, [
('2"', 2 * IN, x0 + top_cum[0] * IN),
("1' - 6\"", 18 * IN, x0 + top_cum[1] * IN),
("2' - 0\"", 24 * IN, x0 + top_cum[2] * IN),
("2' - 0\"", 24 * IN, x0 + top_cum[3] * IN),
("2' - 0\"", 24 * IN, x0 + top_cum[4] * IN),
("2' - 0\"", 24 * IN, x0 + top_cum[5] * IN),
("2' - 0\"", 24 * IN, x0 + top_cum[6] * IN),
("1' - 6\"", 18 * IN, x0 + top_cum[7] * IN),
])
# vertical: '2"|8"' gives interior point {2}; '6"|2"|2"' (bottom-up
# order 6, 2, 2) gives interior points {6, 8}. Union {2, 6, 8}.
vert_a = _vchain([('2"', 2 * IN, 13.50), ('8"', 8 * IN, 13.95)])
vert_b = _vchain([('6"', 6 * IN, 13.55), ('2"', 2 * IN, 13.75),
('2"', 2 * IN, 13.90)])
# decoys that must be ignored harmlessly: a mild-bar chain (its
# positions never land on real columns) and an unparseable
# blocking-diagram chain.
mild_bar = _hchain(30.0, [
('5"', 5 * IN, 20.0), ('9"', 9 * IN, 20.5),
("1' - 0\"", 12 * IN, 21.0), ("1' - 0\"", 12 * IN, 21.5),
("1' - 0\"", 12 * IN, 22.0), ("1' - 0\"", 12 * IN, 22.5),
("1' - 0\"", 12 * IN, 23.0), ("1' - 0\"", 12 * IN, 23.5),
])
blocking = _hchain(25.0, [
('.104L', 0.05, 24.0), ('.291L', 0.10, 24.5), ('', 0.05, 25.0),
('0.291L', 0.10, 25.5), ('.104L', 0.05, 26.0),
])
return {
'symbols': symbols,
'dimensions': [bottom_dim1, bottom_dim2, bottom_single, bottom_dim4,
top_dim, vert_a, vert_b, mild_bar, blocking],
}
def test_fsa5_slab_three_rows_split_across_two_vertical_chains():
view = _fsa5_view()
layout, why = sm.read_layout(view, qty=19, thickness_in=10,
width_ft=13.0 + 8 * IN)
assert why is None
assert layout is not None
assert layout['columns_in'] == [2, 8, 20, 32, 44, 56, 68, 92, 116, 140, 158]
assert layout['total_in'] == 164
assert len(layout['rows']) == 3
row0, row1, row2 = layout['rows']
assert row0['from_bottom_in'] == 2
assert row0['strands'] == [True] * 11
assert row1['from_bottom_in'] == 6
assert row1['strands'] == [False, False, False, False, False, False,
True, True, True, True, True]
assert row2['from_bottom_in'] == 8
assert row2['strands'] == [True, False, True, False, True, False, False,
False, False, False, False]
assert '19 strand symbols in 3 rows' in layout['note']
assert 'vertical chains' in layout['note']
assert '"2", 8"' in layout['note'] # the '2"|8"' chain, quoted
assert '"6", 2", 2"' in layout['note'] # the '6"|2"|2"' chain, quoted
# ====================================================================== WPA2
# wall, full grid. qty 12, thickness 8", width 14'-1 1/4" = 169.25".
def _wpa2_view():
x0 = 5.0
positions_in = [6, 16, 52, 88, 124, 160]
y_bottom, y_top = 10.0, 10.0 + 4 * IN
symbols = (
[_sym(x0 + p * IN, y_bottom) for p in positions_in] +
[_sym(x0 + p * IN, y_top) for p in positions_in]
)
cum = [6, 16, 52, 88, 124, 160, 169.25]
chain = _hchain(9.9, [
('6"', 6 * IN, x0 + cum[0] * IN),
('10"', 10 * IN, x0 + cum[1] * IN),
("3' - 0\"", 36 * IN, x0 + cum[2] * IN),
("3' - 0\"", 36 * IN, x0 + cum[3] * IN),
("3' - 0\"", 36 * IN, x0 + cum[4] * IN),
("3' - 0\"", 36 * IN, x0 + cum[5] * IN),
('9 1/4"', 9.25 * IN, x0 + cum[6] * IN),
])
vert = _vchain([('2"', 2 * IN, 9.5), ('4"', 4 * IN, 9.6),
('2"', 2 * IN, 9.7)])
return {'symbols': symbols, 'dimensions': [chain, vert]}
def test_wpa2_wall_full_grid():
view = _wpa2_view()
layout, why = sm.read_layout(view, qty=12, thickness_in=8,
width_ft=14.0 + 1 * IN + 0.25 * IN)
assert why is None
assert layout is not None
assert layout['columns_in'] == [6, 16, 52, 88, 124, 160]
assert layout['total_in'] == 169.25
assert len(layout['rows']) == 2
bottom, top = layout['rows']
assert bottom['from_bottom_in'] == 2
assert bottom['strands'] == [True] * 6
assert top['from_bottom_in'] == 6
assert top['strands'] == [True] * 6
# ====================================================================== WPA3
# wall, full grid, single vertical chain. qty 8, thickness 10", width 7'-11".
def _wpa3_view():
x0 = 5.0
positions_in = [6, 16, 52, 88]
y_bottom, y_top = 8.0, 8.0 + 6 * IN
symbols = (
[_sym(x0 + p * IN, y_bottom) for p in positions_in] +
[_sym(x0 + p * IN, y_top) for p in positions_in]
)
cum = [6, 16, 52, 88, 95]
chain = _hchain(7.4, [
('6"', 6 * IN, x0 + cum[0] * IN),
('10"', 10 * IN, x0 + cum[1] * IN),
("3' - 0\"", 36 * IN, x0 + cum[2] * IN),
("3' - 0\"", 36 * IN, x0 + cum[3] * IN),
('7"', 7 * IN, x0 + cum[4] * IN),
])
vert = _vchain([('2"', 2 * IN, 7.9), ('6"', 6 * IN, 8.0),
('2"', 2 * IN, 8.1)])
return {'symbols': symbols, 'dimensions': [chain, vert]}
def test_wpa3_wall_full_grid_single_vertical_chain():
view = _wpa3_view()
layout, why = sm.read_layout(view, qty=8, thickness_in=10,
width_ft=7.0 + 11 * IN)
assert why is None
assert layout is not None
assert layout['columns_in'] == [6, 16, 52, 88]
assert layout['total_in'] == 95
assert len(layout['rows']) == 2
bottom, top = layout['rows']
assert bottom['from_bottom_in'] == 2
assert bottom['strands'] == [True] * 4
assert top['from_bottom_in'] == 8
assert top['strands'] == [True] * 4
# =============================================================== refusals
def test_refuses_when_symbol_count_disagrees_with_qty():
view = _fsa5_view()
view['symbols'] = view['symbols'][:-1] # drop one -> 18 symbols, qty 19
layout, why = sm.read_layout(view, qty=19, thickness_in=10,
width_ft=13.0 + 8 * IN)
assert layout is None
assert '18' in why and '19' in why
def test_refuses_when_evidence_has_no_symbols_key():
view = _fsa5_view()
del view['symbols']
layout, why = sm.read_layout(view, qty=19, thickness_in=10, width_ft=None)
assert layout is None
assert why == 'evidence carries no symbol positions -- re-run Design Extract'
def test_refuses_when_qty_is_none():
view = _fsa5_view()
layout, why = sm.read_layout(view, qty=None)
assert layout is None
assert why is not None
assert 'qty' in why
def test_refuses_when_a_column_has_no_chain():
# three strands in one row, columns at 0", 10" and 25" relative to the
# leftmost -- but the only horizontal chain on the drawing measures the
# first two and never reaches the third.
x0 = 0.0
symbols = [_sym(x0, 0.0), _sym(x0 + 10 * IN, 0.0), _sym(x0 + 25 * IN, 0.0)]
chain = _hchain(-0.1, [('5"', 5 * IN, 0.2), ('10"', 10 * IN, 0.5),
('8"', 8 * IN, 0.8)])
view = {'symbols': symbols, 'dimensions': [chain]}
layout, why = sm.read_layout(view, qty=3, thickness_in=None, width_ft=None)
assert layout is None
assert 'no dimension chain measures' in why
assert '25' in why
def test_refuses_when_closing_chains_disagree_on_total():
# a single real column (one strand), measured by two differently-banded
# dimension elements that both close to "the far edge" but disagree on
# where that edge is -- and no width_ft is given to settle it.
symbols = [_sym(2 * IN, 0.0)]
chain_a = _hchain(-0.1, [('2"', 2 * IN, 0.3), ('13"', 13 * IN, 0.6)]) # 15"
chain_b = _hchain(5.0, [('2"', 2 * IN, 0.3), ('14 1/2"', 14.5 * IN, 0.6)]) # 16.5"
view = {'symbols': symbols, 'dimensions': [chain_a, chain_b]}
layout, why = sm.read_layout(view, qty=1, thickness_in=None, width_ft=None)
assert layout is None
assert 'disagree' in why and '15' in why and '16.5' in why
def test_refuses_when_a_closing_chain_disagrees_with_known_width():
# one real column; the only chain closes to the far edge (its position
# matches the column) but its own total does not match the width_ft
# given, and it has no columns left over to be read as 'open' instead.
symbols = [_sym(2 * IN, 0.0)]
chain = _hchain(-0.1, [('2"', 2 * IN, 0.3), ('13"', 13 * IN, 0.6)]) # 15"
view = {'symbols': symbols, 'dimensions': [chain]}
layout, why = sm.read_layout(view, qty=1, thickness_in=None,
width_ft=20 * IN) # 20", not 15"
assert layout is None
assert 'closes to the far edge' in why
assert '15' in why and '20' in why
def test_refuses_when_no_vertical_chain_present():
view = _fsa5_view()
view['dimensions'] = [d for d in view['dimensions']
if d.get('axis') != 'vertical']
layout, why = sm.read_layout(view, qty=19, thickness_in=10,
width_ft=13.0 + 8 * IN)
assert layout is None
assert 'vertical chains give 0 depths' in why
def test_refuses_when_vertical_union_spacing_disagrees_with_symbol_rows():
view = _fsa5_view()
view['dimensions'] = [d for d in view['dimensions']
if d.get('axis') != 'vertical']
# '2"|8"' still gives {2}; perturb the other chain to '5"|3"|2"' so its
# interior points are {5, 8} -- union {2, 5, 8} has the right COUNT (3)
# but the wrong spacing (rows are 4" then 2" apart, not 3" then 3").
vert_a = _vchain([('2"', 2 * IN, 13.50), ('8"', 8 * IN, 13.95)])
vert_bad = _vchain([('5"', 5 * IN, 13.55), ('3"', 3 * IN, 13.75),
('2"', 2 * IN, 13.90)])
view['dimensions'] = view['dimensions'] + [vert_a, vert_bad]
layout, why = sm.read_layout(view, qty=19, thickness_in=10,
width_ft=13.0 + 8 * IN)
assert layout is None
assert 'spaced' in why
# ============================================================ printed vs drawn
def test_printed_wins_over_disagreeing_drawn_value_and_note_says_so():
# one row, two strands 12" apart; the edge segment (whose value never
# has to match a gap directly) is printed 1'-0" but drawn as 1.0104 ft
# (12.1248") -- printed must win, and the note must say the drawing and
# the label disagree.
symbols = [_sym(1.0, 0.0), _sym(2.0, 0.0)]
chain = _hchain(-0.1, [
("1' - 0\"", 1.0104, 0.5), # edge: disagreement lives here
("1' - 0\"", 1.0, 1.5), # interior gap: must read as 12" exactly
("1' - 0\"", 1.0, 2.5), # edge: irrelevant, width_ft is None
])
vert = _vchain([('3"', 3 * IN, 0.0), ('3"', 3 * IN, 0.1)])
view = {'symbols': symbols, 'dimensions': [chain, vert]}
layout, why = sm.read_layout(view, qty=2, thickness_in=6, width_ft=None)
assert why is None
assert layout is not None
assert layout['columns_in'] == [12, 24]
assert 'disagrees with drawn' in layout['note']
assert 'used printed' in layout['note']
# =================================================================== helpers
def test_parse_inches():
assert sm.parse_inches('2"') == 2
assert sm.parse_inches('6"') == 6
assert sm.parse_inches("1' - 0\"") == 12
assert sm.parse_inches("1'-6\"") == 18
assert sm.parse_inches('9 1/4"') == 9.25
assert sm.parse_inches("2' - 0 1/2\"") == 24.5
assert sm.parse_inches("13' - 8\"") == 164
assert sm.parse_inches("14' - 1 1/4\"") == 169.25
assert sm.parse_inches("7' - 11\"") == 95
assert sm.parse_inches('3/4"') == 0.75
assert sm.parse_inches('.104L') is None
assert sm.parse_inches('0.291L') is None
assert sm.parse_inches('') is None
assert sm.parse_inches(None) is None
def test_cluster_groups_within_tolerance_and_chains():
values = [0.0, 0.03, 0.06, 5.0, 5.02]
groups = sm.cluster(values, 0.05)
# 0.0-0.03 (0.03 apart) and 0.03-0.06 (0.03 apart) chain into one group
# even though 0.0 and 0.06 are 0.06 apart -- chaining, not a fixed window.
assert groups == [[0, 1, 2], [3, 4]]
def test_cluster_sorted_ascending_by_group_mean():
values = [10.0, 0.0, 10.1, 0.1]
groups = sm.cluster(values, 0.2)
means = [sum(values[i] for i in g) / float(len(g)) for g in groups]
assert means == sorted(means)
def test_band_chains_splices_multiple_dims_and_a_single_by_x():
view = _fsa5_view()
chains = sm.band_chains(view['dimensions'])
bottom = [c for c in chains if abs(c['y'] - 13.595) < 0.05]
assert len(bottom) == 1
assert len(bottom[0]['segments']) == 12
values = [s['value'] for s in bottom[0]['segments']]
assert values == ['2"', '6"', "1' - 0\"", "1' - 0\"", "1' - 0\"",
"1' - 0\"", "1' - 0\"", "2' - 0\"", "2' - 0\"",
"2' - 0\"", "1' - 6\"", '6"']
def test_fsa5_reads_when_multi_segment_chains_carry_no_top_level_origin():
"""The LIVE scrape shape: a multi-segment dimension has no top-level x/y
(Dimension.Origin throws on a drafting view -- measured 2026-09-04 on
three real summaries), only each segment's own label point. The first
version of band_chains skipped every such dimension and refused all
three real designs; each segment must band by its OWN y."""
view = _fsa5_view()
for d in view['dimensions']:
if d.get('segments'):
y = d.pop('y', None)
for s in d['segments']:
s.setdefault('y', y if y is not None else 14.0)
layout, why = sm.read_layout(view, qty=19, thickness_in=10,
width_ft=13.0 + 8 * IN)
assert why is None, why
assert layout['columns_in'] == [2, 8, 20, 32, 44, 56, 68, 92, 116, 140, 158]
assert [r['from_bottom_in'] for r in layout['rows']] == [2, 6, 8]