WillTools 2026-09-09 -- clean snapshot of the development repo for review
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:
2026-09-09 08:35:15 -05:00
commit 25592fa6c2
406 changed files with 223435 additions and 0 deletions
+302
View File
@@ -0,0 +1,302 @@
# -*- coding: utf-8 -*-
"""Erection is two conditions, and they use two different ratings.
the pick panel flat, face lifters and edge anchors together. Face
lifters are pulled out of the face (tension); the anchors are
edge-on to the line, which pulls across them (shear).
standing panel plumb. The face lifters have let go and carry nothing.
The whole piece hangs from the anchors alone, in tension.
Checking the anchors against tension in the first condition, or running the
second one through the beam model, both produce a report that looks complete
and answers a question the panel never asks.
"""
import dataclasses
import pytest
from conftest import LENGTH_FT, WIDTH_FT, make_panel
from panellift import checks, statics
from panellift.model import Lifter
def erection_panel(shear_lb=4010.0, tension_lb=7690.0, n=4, weight=35000.0):
"""The ASPIRE panel picked on its lower two lines plus a top edge row."""
panel = make_panel()
face = [dataclasses.replace(l, capacity_lb=12000.0)
for l in panel.lifters if l.x < 0.5 * LENGTH_FT]
edge = [Lifter(mark='E{0}'.format(i + 1), family='EDGE_ANCHOR',
x=LENGTH_FT, y=WIDTH_FT * (i + 1) / (n + 1.0),
role='strip', capacity_lb=tension_lb,
shear_capacity_lb=shear_lb, on_edge=True)
for i in range(n)]
return dataclasses.replace(panel, lifters=face + edge, weight_lb=weight)
def analyse(panel, stage, rolling):
return statics.analyse(panel, stage, rolling, role='strip')
def codes(findings):
return [f.code for f in findings]
def insert_lines(findings, family):
return [f for f in findings if f.code == 'INSERT' and family in f.message]
# --------------------------------------------- condition 1: the flat pick
def test_an_edge_anchor_is_checked_in_both_tension_and_shear(stripping, rolling):
"""2026-09-02 owner's ruling: the sling's line force is decomposed into
a vertical (tension) and a horizontal (shear) share and BOTH are
checked against the lifter's own capacity in that column -- `on_edge`
no longer selects a single axis to judge here (it still matters to
check_plumb_hang). An edge anchor carries a tension rating same as a
face lifter, and it is checked too; at this fixture's vertical
(90deg) rigging the shear share is zero, so the shear ratio comes back
a trivial 0% rather than being skipped."""
a = analyse(erection_panel(), stripping, rolling)
found = insert_lines(checks.check_insert_capacity(a, rolling),
'EDGE_ANCHOR')
assert found
assert 'shear' in found[0].message and 'tension' in found[0].message
assert '4,010' in found[0].message # the shear rating
assert '7,690' in found[0].message # the tension rating
def test_a_face_lifter_is_checked_in_tension(stripping, rolling):
a = analyse(erection_panel(), stripping, rolling)
found = insert_lines(checks.check_insert_capacity(a, rolling),
'TEST_LIFTER')
assert found
assert 'tension' in found[0].message
def test_an_edge_anchor_with_no_shear_rating_still_judges_tension(stripping,
rolling):
"""No shear rating means the shear half of the decomposition can't be
judged -- but the tension half is a separate question with its own
capacity, and it still judges rather than the whole lifter vanishing
from the report. The missing half is named, not silently dropped."""
a = analyse(erection_panel(shear_lb=None), stripping, rolling)
found = checks.check_insert_capacity(a, rolling)
assert 'INSERT-UNRATED' in codes(found)
lines = insert_lines(found, 'EDGE_ANCHOR')
assert lines
assert 'tension' in lines[0].message
assert 'NOT JUDGED' in lines[0].message
assert '7,690' in lines[0].message # the tension rating still judged
# ------------------------------------------ condition 2: standing plumb
def test_standing_plumb_hangs_the_whole_piece_on_the_anchors(stripping,
rolling):
a = analyse(erection_panel(n=4, weight=35000.0), stripping, rolling)
found = [f for f in checks.check_plumb_hang(a) if f.code == 'PLUMB']
assert len(found) == 1
assert '8,750' in found[0].message # 35,000 / 4
assert '7,690' in found[0].message # the tension rating
def test_standing_plumb_ignores_the_handling_multiplier_and_the_sling(rolling):
"""It is a piece hanging straight down off four anchors. There is no
distribution question in it, so nothing about the pick can change it."""
from panellift.model import Stage
panel = erection_panel()
gentle = checks.check_plumb_hang(analyse(panel, Stage('a', 1.0, 4000.0),
rolling))
harsh = checks.check_plumb_hang(analyse(panel, Stage('b', 1.7, 4000.0),
rolling))
assert gentle[0].message == harsh[0].message
def test_a_row_too_light_for_the_piece_fails(stripping, rolling):
a = analyse(erection_panel(tension_lb=2000.0), stripping, rolling)
found = [f for f in checks.check_plumb_hang(a) if f.code == 'PLUMB']
assert found and found[0].severity == 'FAIL'
def test_an_anchor_with_no_tension_rating_says_so(stripping, rolling):
a = analyse(erection_panel(tension_lb=None), stripping, rolling)
assert 'PLUMB-UNRATED' in codes(checks.check_plumb_hang(a))
def test_a_stage_with_no_edge_anchors_is_not_stood_up(stripping, rolling):
"""Stripping and transportation never put the piece on end, so there is
no plumb condition to report and no warning to give about it."""
a = analyse(make_panel(), stripping, rolling)
assert checks.check_plumb_hang(a) == []
def test_face_lifters_are_not_used_to_stand_the_piece_up(stripping, rolling):
"""They have a tension rating too. Using it here would report a condition
the panel never sees."""
panel = erection_panel()
panel = dataclasses.replace(panel, lifters=[
dataclasses.replace(l, on_edge=False) for l in panel.lifters])
a = analyse(panel, stripping, rolling)
assert checks.check_plumb_hang(a) == []
def test_run_all_reports_both_conditions(stripping, rolling):
a = analyse(erection_panel(), stripping, rolling)
found = codes(checks.run_all(a, rolling))
assert 'INSERT' in found and 'PLUMB' in found
# ----------------------------------- transverse edge-anchor path
def _transverse_panel(opening=False):
"""A horizontal panel erected on its long edge, top-row anchors along the
edge, in TRANSPOSED shape: length = erected span (11.5 ft), width = long
dimension (26.83 ft)."""
from panellift.model import Blockout, Panel
panel = Panel(mark='H', length_ft=11.5, width_ft=26.83, thickness_in=8.0,
weight_lb=30800.0, fci_psi=3500.0, fc_psi=6000.0)
for i, y in enumerate([3.5, 10.5, 16.33, 24.08]):
panel.lifters.append(Lifter(
mark='E{0}'.format(i + 1), family='EDGE_ANCHOR', x=11.5, y=y,
role='strip', capacity_lb=16000.0, shear_capacity_lb=4010.0,
on_edge=True))
if opening:
# Reaches the anchored edge (x0 + length = 11.5) inside the gap
# between the anchors at y = 10.5 and 16.33.
panel.blockouts.append(Blockout(name='BIG', x0=7.0, y0=14.0,
length=4.5, width=3.0))
return panel
def _edge_path(panel):
from panellift.model import Stage
return checks.check_edge_anchor_path(
panel, Stage('erection', 1.4, 6000.0, span='width'))
def _f_t_psi(finding):
m = __import__('re').search(r'f_t ([0-9.]+) psi', finding.message)
return float(m.group(1))
def test_transverse_edge_path_is_tiny_on_a_solid_horizontal_panel():
found = _edge_path(_transverse_panel())
ok = [f for f in found if f.code == 'EDGE-PATH']
assert ok and ok[0].severity == 'OK'
assert 'psi' in ok[0].message
assert _f_t_psi(ok[0]) < 20.0 # a few pounds per square inch
def test_transverse_edge_path_respects_an_opening_across_the_anchor_line():
solid = _edge_path(_transverse_panel())[0]
with_opening = _edge_path(_transverse_panel(opening=True))[0]
assert solid.code == 'EDGE-PATH'
assert with_opening.code == 'EDGE-PATH'
assert 'opening' in with_opening.message.lower()
assert _f_t_psi(with_opening) > _f_t_psi(solid)
def test_transverse_edge_path_ignores_length_stages_and_few_anchors():
import dataclasses as _dc
from panellift.model import Stage
panel = _transverse_panel()
assert checks.check_edge_anchor_path(
panel, Stage('s', 1.4, 6000.0, span='length')) == []
few = _dc.replace(panel, lifters=panel.lifters[:1])
assert checks.check_edge_anchor_path(
few, Stage('s', 1.4, 6000.0, span='width')) == []
# ----------------------- the anchored edge is read, not assumed
def _mirrored_panel():
"""The same piece with the anchor row on the OTHER long edge -- x == 0
after transposing -- and the opening cutting in from that edge.
Which of a horizontal panel's two long edges ends up on top is a fact
about the model. Assuming it is always x == length does not give a wrong
number here, it gives a clean OK: no opening is ever seen to reach the
anchor line, so the strip reports its full depth and the one thing this
check exists to catch goes unreported.
"""
import dataclasses as _dc
from panellift.model import Blockout
panel = _transverse_panel()
panel = _dc.replace(panel, lifters=[_dc.replace(l, x=0.0)
for l in panel.lifters])
panel.blockouts.append(Blockout(name='BIG', x0=0.0, y0=14.0,
length=4.5, width=3.0))
return panel
def test_the_anchored_edge_is_taken_from_the_anchors():
found = _edge_path(_mirrored_panel())[0]
assert 'opening BIG' in found.message
# 11.5 ft of span less the 4.5 ft the opening eats = 7.0 ft of web.
assert '84.0 in' in found.message
def test_mirrored_and_normal_read_the_same_strip():
"""Same geometry, mirrored. The stress must come out the same."""
normal = _f_t_psi(_edge_path(_transverse_panel(opening=True))[0])
mirrored = _f_t_psi(_edge_path(_mirrored_panel())[0])
assert abs(normal - mirrored) < 0.05
# ------------------------------ the load and the ends are not understated
def test_the_handling_multiplier_is_on_the_load():
"""This compares a concrete stress to a concrete allowable, so it is the
factored load -- the insert checks divide the multiplier back out because
an insert is rated against service load, and that is a different question.
"""
import dataclasses as _dc
from panellift.model import Stage
panel = _transverse_panel()
gentle = checks.check_edge_anchor_path(
panel, Stage('a', 1.0, 6000.0, span='width'))[0]
harsh = checks.check_edge_anchor_path(
panel, Stage('b', 2.0, 6000.0, span='width'))[0]
# the message rounds to a tenth of a psi, so compare at that grain
assert abs(_f_t_psi(harsh) - 2.0 * _f_t_psi(gentle)) <= 0.15
def test_the_strip_past_the_last_anchor_is_a_cantilever():
"""It hangs off the end anchor; there is nothing beyond it to lean on.
Calling it a simple span understates that moment four times over."""
import dataclasses as _dc
panel = _transverse_panel()
# One long overhang, and the two anchors close together so no interior
# gap can win: 26.83 ft of piece with anchors at 3.5 and 5.0.
panel = _dc.replace(panel, lifters=[
_dc.replace(panel.lifters[0], y=3.5),
_dc.replace(panel.lifters[1], y=5.0)])
found = _edge_path(panel)[0]
assert 'overhang' in found.message
w = 30800.0 * 1.4 / 26.83
gap = 26.83 - 5.0
s_in3 = 8.0 * (11.5 * 12.0) ** 2 / 6.0
assert abs(_f_t_psi(found) - w * gap ** 2 / 2.0 * 12.0 / s_in3) < 0.05
# --------------------------------- nothing checked never reads as a pass
def test_no_concrete_strength_is_not_a_pass():
from panellift.model import Stage
found = checks.check_edge_anchor_path(
_transverse_panel(), Stage('e', 1.4, 0.0, span='width'))
assert codes(found) == ['EDGE-PATH-UNRATED']
assert found[0].severity == 'WARN'
def test_an_opening_that_eats_the_whole_strip_fails():
import dataclasses as _dc
from panellift.model import Blockout
panel = _transverse_panel()
panel.blockouts.append(Blockout(name='SLOT', x0=0.0, y0=12.0,
length=11.5, width=6.0))
found = _edge_path(panel)[0]
assert found.severity == 'FAIL'
assert 'no section at all' in found.message