25592fa6c2
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.
303 lines
13 KiB
Python
303 lines
13 KiB
Python
# -*- 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
|