WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for `vibe.pa_reinf` -- the Panel Analysis Reinforcement pre-fill.
|
||||
|
||||
Runs on plain CPython via `tests/conftest.py`'s import trick (no Revit, no
|
||||
pyRevit -- Revit is NOT running for this suite). `pa_reinf` reaches for
|
||||
`vibe.knowledge` / `vibe.ticketqc` only inside the functions that need them,
|
||||
so it imports cleanly here even though `ticketqc` itself cannot (it pulls in
|
||||
`Autodesk.Revit.DB` at module scope).
|
||||
|
||||
Covers: strand grouping by depth, the pull-pct suffix appearing only when a
|
||||
group's pull differs from the modal pull, that `z_in` flows straight into
|
||||
`depth_in` with no thickness-based conversion, the rebar (size, type)
|
||||
summary and its total area, and a bar whose type name will not yield a size.
|
||||
"""
|
||||
from vibe import pa_reinf
|
||||
|
||||
|
||||
# --------------------------------------------------------------- strands
|
||||
|
||||
def test_no_prestress_is_empty():
|
||||
assert pa_reinf.strand_prefill(None, 8.0, None) == {}
|
||||
assert pa_reinf.strand_prefill({}, 8.0, None) == {}
|
||||
assert pa_reinf.strand_prefill({'strands': []}, 8.0, None) == {}
|
||||
|
||||
|
||||
def test_strand_count_and_diameter():
|
||||
prestress = {
|
||||
'diameter_in': 0.5,
|
||||
'loss_pct': 8.0,
|
||||
'strands': [
|
||||
{'area_in2': 0.153, 'initial_pull_lb': 33800.0, 'y': 0.5,
|
||||
'z_in': 2.0},
|
||||
{'area_in2': 0.153, 'initial_pull_lb': 33800.0, 'y': 1.5,
|
||||
'z_in': 2.0},
|
||||
],
|
||||
}
|
||||
out = pa_reinf.strand_prefill(prestress, 10.0, None)
|
||||
assert out['ps_count'] == 2
|
||||
assert out['ps_dia_in'] == '0.5'
|
||||
assert out['ps_loss_pct'] == 8.0
|
||||
assert out['ps_pull_lb'] == 33800.0
|
||||
|
||||
|
||||
def test_groups_by_depth_one_line_per_distinct_depth():
|
||||
prestress = {
|
||||
'diameter_in': 0.5,
|
||||
'strands': (
|
||||
[{'area_in2': 0.153, 'initial_pull_lb': 33800.0, 'y': i,
|
||||
'z_in': 2.0} for i in range(4)] +
|
||||
[{'area_in2': 0.153, 'initial_pull_lb': 33800.0, 'y': i,
|
||||
'z_in': 8.0} for i in range(3)]
|
||||
),
|
||||
}
|
||||
out = pa_reinf.strand_prefill(prestress, 10.0, None)
|
||||
rows = out['ps_rows'].splitlines()
|
||||
assert rows == ['4 @ 2', '3 @ 8']
|
||||
|
||||
|
||||
def test_depths_within_an_eighth_inch_collapse_to_one_group():
|
||||
# Floating-point noise off a real geometry read (2.0000000001 vs
|
||||
# 1.9999999998) must not split one physical row of strands in two.
|
||||
prestress = {
|
||||
'diameter_in': 0.5,
|
||||
'strands': [
|
||||
{'area_in2': 0.153, 'initial_pull_lb': 30000.0, 'y': 0,
|
||||
'z_in': 2.0000000001},
|
||||
{'area_in2': 0.153, 'initial_pull_lb': 30000.0, 'y': 1,
|
||||
'z_in': 1.9999999998},
|
||||
],
|
||||
}
|
||||
out = pa_reinf.strand_prefill(prestress, 10.0, None)
|
||||
assert out['ps_rows'] == '2 @ 2'
|
||||
|
||||
|
||||
def test_pull_pct_only_when_group_differs_from_modal():
|
||||
fpu = 270.0
|
||||
area = 0.153
|
||||
modal_pull = 0.75 * fpu * 1000.0 * area # 75% -- the majority
|
||||
other_pull = 0.70 * fpu * 1000.0 * area # 70% -- one odd group
|
||||
prestress = {
|
||||
'diameter_in': 0.5,
|
||||
'strands': (
|
||||
[{'area_in2': area, 'initial_pull_lb': modal_pull, 'y': i,
|
||||
'z_in': 2.0} for i in range(4)] +
|
||||
[{'area_in2': area, 'initial_pull_lb': other_pull, 'y': i,
|
||||
'z_in': 6.0} for i in range(2)]
|
||||
),
|
||||
}
|
||||
out = pa_reinf.strand_prefill(prestress, 10.0, fpu)
|
||||
assert out['ps_pull_lb'] == modal_pull
|
||||
rows = out['ps_rows'].splitlines()
|
||||
# The modal group carries no percentage -- it IS the default pull.
|
||||
assert rows[0] == '4 @ 2'
|
||||
# The odd group out states its own pull, as a percentage of fpu.
|
||||
assert rows[1] == '2 @ 6 @ 70'
|
||||
|
||||
|
||||
def test_z_in_maps_straight_into_depth_in_no_thickness_conversion():
|
||||
# z_in is "up from the form face" (liftexport.read_prestress) and
|
||||
# ps_rows' depth_in is "how deep from the bottom" -- the same axis, same
|
||||
# zero. Changing thickness_in must not move the reported depth.
|
||||
prestress = {
|
||||
'diameter_in': 0.5,
|
||||
'strands': [{'area_in2': 0.153, 'initial_pull_lb': 30000.0,
|
||||
'y': 0, 'z_in': 3.0}],
|
||||
}
|
||||
thin = pa_reinf.strand_prefill(prestress, 4.0, None)
|
||||
thick = pa_reinf.strand_prefill(prestress, 24.0, None)
|
||||
none_thickness = pa_reinf.strand_prefill(prestress, None, None)
|
||||
assert thin['ps_rows'] == '1 @ 3'
|
||||
assert thick['ps_rows'] == '1 @ 3'
|
||||
assert none_thickness['ps_rows'] == '1 @ 3'
|
||||
|
||||
|
||||
def test_strand_missing_z_in_is_dropped_from_grouping_not_fatal():
|
||||
prestress = {
|
||||
'diameter_in': 0.5,
|
||||
'strands': [
|
||||
{'area_in2': 0.153, 'initial_pull_lb': 30000.0, 'y': 0,
|
||||
'z_in': 2.0},
|
||||
{'area_in2': 0.153, 'initial_pull_lb': 30000.0, 'y': 1,
|
||||
'z_in': None},
|
||||
],
|
||||
}
|
||||
out = pa_reinf.strand_prefill(prestress, 10.0, None)
|
||||
assert out['ps_count'] == 2
|
||||
assert out['ps_rows'] == '1 @ 2'
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- rebar
|
||||
|
||||
def _box(family, mark=None, reinf_family=True, element=None, type_name=None):
|
||||
b = {'family': family, 'mark': mark, 'category': 'Specialty Equipment',
|
||||
'lo': [0, 0, 0], 'hi': [1, 1, 1]}
|
||||
if element is not None:
|
||||
b['element'] = element
|
||||
if type_name is not None:
|
||||
b['type'] = type_name
|
||||
return b
|
||||
|
||||
|
||||
class _FakeKnow(dict):
|
||||
pass
|
||||
|
||||
|
||||
def _know(patterns=None, wwf_prefix='WWF'):
|
||||
return {
|
||||
'families': {'strand_include': ['STRAND'], 'wwf_prefix': wwf_prefix},
|
||||
'drawings': {'reinforcing_family_patterns': patterns or []},
|
||||
}
|
||||
|
||||
|
||||
class _FakeElement(object):
|
||||
def __init__(self, name):
|
||||
self.Name = name
|
||||
|
||||
|
||||
def test_no_boxes_is_empty():
|
||||
assert pa_reinf.rebar_prefill([], _know()) == {}
|
||||
assert pa_reinf.rebar_prefill(None, _know()) == {}
|
||||
|
||||
|
||||
def test_rebar_summary_by_size_and_type_with_area():
|
||||
know = _know(patterns=[r'^BARFAM_\(SCHEDULED\)$'])
|
||||
boxes = [
|
||||
_box('BARFAM_(SCHEDULED)', mark='m1', type_name='T04'),
|
||||
_box('BARFAM_(SCHEDULED)', mark='m2', type_name='T04'),
|
||||
_box('BARFAM_(SCHEDULED)', mark='m3', type_name='T05'),
|
||||
]
|
||||
out = pa_reinf.rebar_prefill(boxes, know)
|
||||
assert out['mild_as'] == '2 x #4 (T04), 1 x #5 (T05)'
|
||||
assert out['mild_as_in2'] == '{0:.2f}'.format(0.20 * 2 + 0.31 * 1)
|
||||
assert 'reinf_basis' not in out
|
||||
|
||||
|
||||
def test_sorted_by_size_then_type():
|
||||
know = _know(patterns=[r'^BARFAM_\(SCHEDULED\)$'])
|
||||
boxes = [
|
||||
_box('BARFAM_(SCHEDULED)', type_name='ZZ-05'),
|
||||
_box('BARFAM_(SCHEDULED)', type_name='AA-04'),
|
||||
_box('BARFAM_(SCHEDULED)', type_name='BB-04'),
|
||||
]
|
||||
out = pa_reinf.rebar_prefill(boxes, know)
|
||||
assert out['mild_as'] == '1 x #4 (AA-04), 1 x #4 (BB-04), 1 x #5 (ZZ-05)'
|
||||
|
||||
|
||||
def test_unparseable_size_counted_and_named_in_basis():
|
||||
know = _know(patterns=[r'^BARFAM_\(SCHEDULED\)$'])
|
||||
boxes = [
|
||||
_box('BARFAM_(SCHEDULED)', type_name='ODDNAME'),
|
||||
_box('BARFAM_(SCHEDULED)', type_name='ODDNAME'),
|
||||
_box('BARFAM_(SCHEDULED)', type_name='T06'),
|
||||
]
|
||||
out = pa_reinf.rebar_prefill(boxes, know)
|
||||
assert '2 x ? (ODDNAME)' in out['mild_as']
|
||||
assert '1 x #6 (T06)' in out['mild_as']
|
||||
# Only the sized bar contributes area.
|
||||
assert out['mild_as_in2'] == '{0:.2f}'.format(0.44)
|
||||
assert len(out['reinf_basis']) == 1
|
||||
assert 'ODDNAME' in out['reinf_basis'][0]
|
||||
|
||||
|
||||
def test_type_name_read_off_element_when_box_has_none():
|
||||
know = _know(patterns=[r'^BARFAM_\(SCHEDULED\)$'])
|
||||
boxes = [
|
||||
_box('BARFAM_(SCHEDULED)', element=_FakeElement('T04'), type_name=None),
|
||||
]
|
||||
out = pa_reinf.rebar_prefill(boxes, know)
|
||||
assert out['mild_as'] == '1 x #4 (T04)'
|
||||
|
||||
|
||||
def test_no_type_name_at_all_falls_back_to_unknown_type():
|
||||
know = _know(patterns=[r'^BARFAM_\(SCHEDULED\)$'])
|
||||
boxes = [_box('BARFAM_(SCHEDULED)', type_name=None)]
|
||||
out = pa_reinf.rebar_prefill(boxes, know)
|
||||
assert out['mild_as'] == '1 x ? (unknown type)'
|
||||
|
||||
|
||||
def test_strands_and_non_reinforcing_families_excluded_from_rebar():
|
||||
know = _know(patterns=[r'^BARFAM_\(SCHEDULED\)$'])
|
||||
boxes = [
|
||||
_box('STRAND_SPECIAL', type_name='T04'), # is_strand -> skip
|
||||
_box('SOME_HARDWARE', type_name='T05'), # not reinforcing
|
||||
_box('BARFAM_(SCHEDULED)', type_name='T06'),
|
||||
]
|
||||
out = pa_reinf.rebar_prefill(boxes, know)
|
||||
assert out['mild_as'] == '1 x #6 (T06)'
|
||||
|
||||
|
||||
def test_mesh_family_names_deduplicated():
|
||||
know = _know(wwf_prefix='WWF')
|
||||
boxes = [
|
||||
_box('WWF_SAMPLE'), _box('WWF_SAMPLE'), _box('WWF_OTHER'),
|
||||
]
|
||||
out = pa_reinf.rebar_prefill(boxes, know)
|
||||
assert out['mesh'] == 'WWF_SAMPLE, WWF_OTHER'
|
||||
assert 'mild_as' not in out
|
||||
|
||||
|
||||
def test_custom_bar_size_pattern_from_plant():
|
||||
know = _know(patterns=[r'^BARFAM_\(SCHEDULED\)$'])
|
||||
know['panelanalysis'] = {'bar_size_pattern': r'^X(\d{2})'}
|
||||
boxes = [_box('BARFAM_(SCHEDULED)', type_name='X05-EXTRA')]
|
||||
out = pa_reinf.rebar_prefill(boxes, know)
|
||||
assert out['mild_as'] == '1 x #5 (X05-EXTRA)'
|
||||
|
||||
|
||||
# ----------------------------------------------------- merged entry point
|
||||
|
||||
def test_read_reinforcement_prefill_never_raises_without_revit():
|
||||
# vibe.ticketqc cannot even import here (no Autodesk.Revit.DB on host),
|
||||
# so the rebar half must fail silently while the strand half still runs.
|
||||
prestress = {
|
||||
'diameter_in': 0.5,
|
||||
'strands': [{'area_in2': 0.153, 'initial_pull_lb': 30000.0,
|
||||
'y': 0, 'z_in': 2.0}],
|
||||
}
|
||||
out = pa_reinf.read_reinforcement_prefill(
|
||||
object(), object(), _know(), prestress=prestress, fpu_ksi=None)
|
||||
assert out['ps_count'] == 1
|
||||
assert out['ps_rows'] == '1 @ 2'
|
||||
|
||||
|
||||
def test_read_reinforcement_prefill_defensive_on_bad_inputs():
|
||||
assert pa_reinf.read_reinforcement_prefill(
|
||||
None, None, {}, prestress=None, fpu_ksi=None) == {}
|
||||
|
||||
|
||||
# ------------------------------------------------------ cluster_split (2026-09-05)
|
||||
#
|
||||
# `mild_bar_rows_prefill` needs a live `el`/`asm` (through `_member_axes`)
|
||||
# and cannot run on this host suite at all -- see the module docstring's own
|
||||
# admission. `cluster_split` is the one piece of the WPA655 fix
|
||||
# (a (size, depth, direction) group's `spacing_in` being fooled by a
|
||||
# bimodal position spread -- two openings' own diagonal edge bars sharing a
|
||||
# group key) that is pure arithmetic, pulled to module level specifically so
|
||||
# it has a test reachable from here.
|
||||
|
||||
def test_cluster_split_separates_two_distant_pairs():
|
||||
# The exact shape read off WPA655: 4 members, two tight pairs ~3in
|
||||
# apart, the pairs themselves ~37 ft apart -- the median-gap statistic
|
||||
# over all 4 read as "spacing 3in" while from/to spanned the whole
|
||||
# panel. Splitting must keep each pair on its own.
|
||||
members = [
|
||||
{'along_length_in': 0.75},
|
||||
{'along_length_in': 3.75}, # 3in from the first
|
||||
{'along_length_in': 448.0}, # ~37.3 ft from the start
|
||||
{'along_length_in': 451.0}, # 3in from the previous
|
||||
]
|
||||
clusters = pa_reinf.cluster_split(members, 'along_length_in')
|
||||
assert len(clusters) == 2
|
||||
assert [m['along_length_in'] for m in clusters[0]] == [0.75, 3.75]
|
||||
assert [m['along_length_in'] for m in clusters[1]] == [448.0, 451.0]
|
||||
|
||||
|
||||
def test_cluster_split_keeps_one_evenly_spaced_run_together():
|
||||
# 12 bars at a genuine 36in (3 ft) pitch -- gaps are all equal, so
|
||||
# nothing should split even though the run covers most of the panel.
|
||||
members = [{'along_length_in': 24.5 + 36.0 * i} for i in range(12)]
|
||||
clusters = pa_reinf.cluster_split(members, 'along_length_in')
|
||||
assert len(clusters) == 1
|
||||
assert len(clusters[0]) == 12
|
||||
|
||||
|
||||
def test_cluster_split_leaves_small_groups_alone():
|
||||
# Fewer than 3 members: no spacing pattern to protect, never split.
|
||||
members = [{'along_length_in': 0.0}, {'along_length_in': 400.0}]
|
||||
assert pa_reinf.cluster_split(members, 'along_length_in') == [members]
|
||||
|
||||
|
||||
def test_cluster_split_tolerates_ordinary_production_variance():
|
||||
# Real gaps are never bit-identical -- a run at "3in" nominal that
|
||||
# actually measures 2.9/3.1/3.0/2.95 must not fragment into four
|
||||
# singleton clusters just because the smallest gap is not literally
|
||||
# the mode.
|
||||
positions = [0.0, 2.9, 6.0, 8.95, 11.95]
|
||||
members = [{'along_length_in': p} for p in positions]
|
||||
clusters = pa_reinf.cluster_split(members, 'along_length_in')
|
||||
assert len(clusters) == 1
|
||||
assert len(clusters[0]) == 5
|
||||
Reference in New Issue
Block a user