# -*- 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