8e6ee31596
ci / test (push) Successful in 6m18s
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.
396 lines
18 KiB
Python
396 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""The strip ACROSS the width, between the inserts of one pick row.
|
|
|
|
The beam solve calls each support line rigid. It is not: the inserts in a
|
|
row are discrete points, and the panel between two of them carries its
|
|
share sideways to reach one. These tests hold the ROW-FLEX arithmetic to
|
|
hand numbers on the ASPIRE panel -- 10 ft wide, 8.75 in thick, two lines
|
|
of two at the Figure 8.3.2 positions -- where every value can be written
|
|
on one line.
|
|
"""
|
|
import dataclasses
|
|
import re
|
|
|
|
from conftest import LENGTH_FT, WIDTH_FT, THICKNESS_IN, make_panel
|
|
from panellift import checks, pci, statics
|
|
from panellift.model import Blockout, Lifter, Rigging, Stage
|
|
|
|
|
|
WEIGHT_LB = 35000.0 # 32 ft x 10 ft x 8.75 in of 150 pcf concrete
|
|
|
|
|
|
def _panel():
|
|
return dataclasses.replace(make_panel(), weight_lb=WEIGHT_LB)
|
|
|
|
|
|
def _findings(panel, stage, rolling):
|
|
a = statics.analyse(panel, stage, rolling, role='strip')
|
|
return checks.check_row_transverse_flexure(a)
|
|
|
|
|
|
def _codes(findings):
|
|
return [f.code for f in findings]
|
|
|
|
|
|
def _f_t_psi(finding):
|
|
m = re.search(r'f_t ([0-9.]+) psi', finding.message)
|
|
return float(m.group(1))
|
|
|
|
|
|
def _expected_stress(stripping, gap_ft, depth_ft):
|
|
"""Hand arithmetic for the ASPIRE pick, straight from the definition.
|
|
|
|
Two symmetric lines under an equalising rig: each carries half the
|
|
factored weight. Spread over the width that is w; the strip between or
|
|
past the inserts is w*gap^2/8 inside, /2 at an overhang; the web depth
|
|
is the longitudinal concrete beside the row.
|
|
"""
|
|
w_lbft = WEIGHT_LB * stripping.multiplier / 2.0 / WIDTH_FT
|
|
m_lbft = w_lbft * gap_ft ** 2 / 8.0
|
|
s_in3 = THICKNESS_IN * (depth_ft * 12.0) ** 2 / 6.0
|
|
return m_lbft * 12.0 / s_in3
|
|
|
|
|
|
# ------------------------------------------------------------------- solid
|
|
|
|
def test_the_strip_is_noise_on_a_solid_panel(stripping, rolling):
|
|
"""Same deep-beam stiffness that makes EDGE-PATH ~1% of allowable."""
|
|
found = _findings(_panel(), stripping, rolling)
|
|
assert len(found) == 1
|
|
assert found[0].severity == 'OK'
|
|
assert _f_t_psi(found[0]) < 5.0
|
|
|
|
|
|
def test_the_arithmetic_is_locked_to_a_hand_number(stripping, rolling):
|
|
left, right = pci.optimal_two_line_supports(LENGTH_FT)
|
|
# Interior gap is WIDTH/2; the web beside either row is the inner span.
|
|
expected = _expected_stress(stripping, WIDTH_FT / 2.0, right - left)
|
|
found = _findings(_panel(), stripping, rolling)[0]
|
|
assert abs(_f_t_psi(found) - expected) < 0.05
|
|
|
|
|
|
def test_the_multiplier_is_on_the_load():
|
|
"""Concrete stress against concrete allowable: factored, unlike INSERT."""
|
|
roll = Rigging(mode='rolling', sling_angle_deg=90.0)
|
|
gentle = _findings(_panel(), Stage('a', 1.0, 4000.0), roll)[0]
|
|
harsh = _findings(_panel(), Stage('b', 2.0, 4000.0), roll)[0]
|
|
assert abs(_f_t_psi(harsh) - 2.0 * _f_t_psi(gentle)) <= 0.15
|
|
|
|
|
|
def test_an_overhang_past_the_end_insert_governs_when_it_must(stripping,
|
|
rolling):
|
|
"""Both inserts of each row pushed to mid-width: the strips hanging off
|
|
the ends are 4.9 ft cantilevers against a 0.2 ft interior span."""
|
|
panel = _panel()
|
|
panel = dataclasses.replace(panel, lifters=[
|
|
dataclasses.replace(l, y=4.9 + (i % 2) * 0.2)
|
|
for i, l in enumerate(panel.lifters)])
|
|
found = _findings(panel, stripping, rolling)[0]
|
|
assert 'overhang' in found.message
|
|
left, right = pci.optimal_two_line_supports(LENGTH_FT)
|
|
w_lbft = WEIGHT_LB * stripping.multiplier / 2.0 / WIDTH_FT
|
|
m_lbft = w_lbft * 4.9 ** 2 / 2.0
|
|
s_in3 = THICKNESS_IN * ((right - left) * 12.0) ** 2 / 6.0
|
|
assert abs(_f_t_psi(found) - m_lbft * 12.0 / s_in3) < 0.05
|
|
|
|
|
|
# ------------------------------------------------------- openings cut the web
|
|
|
|
def _row_opening(depth_in=None):
|
|
"""Straddles the left pick row and reaches into the insert-to-insert
|
|
strip, leaving 1.0 ft of web on each side of the row."""
|
|
left, _ = pci.optimal_two_line_supports(LENGTH_FT)
|
|
return Blockout(name='BIG', x0=left - 1.0, y0=4.0, length=2.0,
|
|
width=3.0, depth_in=depth_in)
|
|
|
|
|
|
def _side_opening(offset_ft, length_ft=4.0, depth_in=None):
|
|
"""A full-depth opening wholly on one side of the left pick row.
|
|
|
|
offset_ft is the clear distance from the row to the opening's near face:
|
|
positive puts it inboard (to the right of the row), negative outboard.
|
|
"""
|
|
left, _ = pci.optimal_two_line_supports(LENGTH_FT)
|
|
x0 = (left + offset_ft) if offset_ft > 0 else (left + offset_ft - length_ft)
|
|
return Blockout(name='SIDE', x0=x0, y0=3.0, length=length_ft, width=4.0,
|
|
depth_in=depth_in)
|
|
|
|
|
|
def test_an_opening_on_one_side_leaves_the_other_side_untouched(stripping,
|
|
rolling):
|
|
"""The web is the concrete BESIDE the row, not the part of the hole that
|
|
happens to lie on each side of it. An opening entirely outboard of the
|
|
row does not touch the concrete inboard of it, and the deeper side is
|
|
what the strip spans on."""
|
|
solid = _findings(_panel(), stripping, rolling)[0]
|
|
panel = dataclasses.replace(_panel(), blockouts=[_side_opening(-0.25)])
|
|
found = _findings(panel, stripping, rolling)[0]
|
|
assert found.severity == 'OK'
|
|
assert abs(_f_t_psi(found) - _f_t_psi(solid)) < 0.05
|
|
|
|
|
|
def test_an_opening_inboard_of_the_row_leaves_only_the_outboard_end(
|
|
stripping, rolling):
|
|
"""Mirror of the above: the inboard web is starved to 3 in, so the strip
|
|
spans on the outboard end of the piece instead."""
|
|
left, _ = pci.optimal_two_line_supports(LENGTH_FT)
|
|
panel = dataclasses.replace(_panel(), blockouts=[_side_opening(0.25)])
|
|
found = _findings(panel, stripping, rolling)[0]
|
|
assert 'opening SIDE' in found.message
|
|
assert '{0:.1f} in'.format(left * 12.0) in found.message
|
|
|
|
|
|
def test_a_bigger_hole_through_the_row_never_reads_as_a_deeper_web(
|
|
stripping, rolling):
|
|
"""The failure this replaces: the web was measured to the opening's FAR
|
|
face, so lengthening a hole that runs through the pick row made the strip
|
|
deeper and the stress lower -- monotonically backwards."""
|
|
left, _ = pci.optimal_two_line_supports(LENGTH_FT)
|
|
depths = []
|
|
for length in (1.0, 2.0, 4.0, 8.0):
|
|
panel = dataclasses.replace(_panel(), blockouts=[Blockout(
|
|
name='GROW', x0=left - length / 2.0, y0=3.0, length=length,
|
|
width=4.0)])
|
|
found = _findings(panel, stripping, rolling)
|
|
codes = _codes(found)
|
|
assert 'ROW-FLEX-INTERRUPTED' in codes
|
|
depths.append(codes)
|
|
# Every one of them is interrupted; none of them is rated as a beam that
|
|
# got deeper. The old code produced 12, 24, 48 and 96 in of "web".
|
|
assert all('ROW-FLEX-INTERRUPTED' in c for c in depths)
|
|
|
|
|
|
def test_a_row_running_through_an_opening_is_not_rated_at_all(stripping,
|
|
rolling):
|
|
"""No concrete beside the row means no strip. Rating it would invent a
|
|
section; failing it would condemn a piece this model cannot speak about."""
|
|
panel = dataclasses.replace(_panel(), blockouts=[_row_opening()])
|
|
found = _findings(panel, stripping, rolling)
|
|
notes = [f for f in found if f.code == 'ROW-FLEX-INTERRUPTED']
|
|
assert len(notes) == 1 # one per row and opening, not gap
|
|
assert notes[0].severity == 'WARN'
|
|
assert 'runs through opening BIG' in notes[0].message
|
|
assert 'cannot rate it' in notes[0].message
|
|
# The other row is solid and is still rated.
|
|
rated = [f for f in found if f.code == 'ROW-FLEX']
|
|
assert len(rated) == 1 and rated[0].severity == 'OK'
|
|
|
|
|
|
def test_an_opening_clear_of_the_strip_across_the_width_does_not_sever_it(
|
|
stripping, rolling):
|
|
"""Six inches clear is clear. A tolerance that says otherwise turns
|
|
concrete that is there into a finding, 184 times over."""
|
|
left, _ = pci.optimal_two_line_supports(LENGTH_FT)
|
|
solid = _findings(_panel(), stripping, rolling)[0]
|
|
# Inserts at y = 2.5 and 7.5; this sits between 7.9 and 9.9, clear of the
|
|
# 2.5-7.5 strip by 0.4 ft, and it straddles the row longitudinally.
|
|
panel = dataclasses.replace(_panel(), blockouts=[Blockout(
|
|
name='CLEAR', x0=left - 1.0, y0=7.9, length=2.0, width=2.0)])
|
|
found = _findings(panel, stripping, rolling)
|
|
interrupting = [f for f in found if f.code == 'ROW-FLEX-INTERRUPTED']
|
|
# It interrupts only the overhang it actually crosses, and says where.
|
|
assert len(interrupting) == 1
|
|
assert '7.90 to 9.90 ft' in interrupting[0].message
|
|
# The strip it is 0.4 ft clear of is not severed: what is still rated
|
|
# stays down at solid-panel noise rather than reporting a cut web.
|
|
rated = [f for f in found if f.code == 'ROW-FLEX']
|
|
assert len(rated) == 1 and rated[0].severity == 'OK'
|
|
assert _f_t_psi(rated[0]) < 2.0 * _f_t_psi(solid)
|
|
|
|
|
|
def test_a_partial_depth_void_is_not_a_web_cut(stripping, rolling):
|
|
"""House rule: partial-depth blockouts do not reduce section properties.
|
|
The finding must come out numerically identical to the solid panel's."""
|
|
solid = _findings(_panel(), stripping, rolling)[0]
|
|
panel = dataclasses.replace(_panel(),
|
|
blockouts=[_row_opening(depth_in=4.0)])
|
|
found = _findings(panel, stripping, rolling)[0]
|
|
assert 'opening' not in found.message
|
|
assert abs(_f_t_psi(found) - _f_t_psi(solid)) < 0.001
|
|
|
|
|
|
# ------------------------------------------------------------------ guards
|
|
|
|
def test_a_stage_standing_on_its_edge_belongs_to_edge_path(stripping,
|
|
rolling):
|
|
"""Within-row gaps along the anchored top edge are EDGE-PATH's concrete;
|
|
checking them here too would report the same strip under two codes."""
|
|
found = _findings(_panel(), Stage('e', 1.4, 4000.0, span='width'),
|
|
rolling)
|
|
assert found == []
|
|
|
|
|
|
def test_a_dunnage_stage_has_no_pick_rows(shipping, rolling):
|
|
panel = dataclasses.replace(_panel())
|
|
stage = Stage(name='transportation', multiplier=1.35, fc_psi=5000.0,
|
|
supports=[5.0, LENGTH_FT - 5.0])
|
|
assert _findings(panel, stage, rolling) == []
|
|
|
|
|
|
def test_no_weight_says_so_instead_of_passing(stripping, rolling):
|
|
found = _findings(make_panel(), stripping, rolling) # weight_lb=None
|
|
assert _codes(found) == ['ROW-FLEX']
|
|
assert found[0].severity == 'WARN'
|
|
|
|
|
|
def test_no_concrete_strength_is_not_a_pass(rolling):
|
|
found = _findings(_panel(), Stage('e', 1.52, 0.0), rolling)
|
|
assert _codes(found) == ['ROW-FLEX-UNRATED']
|
|
assert found[0].severity == 'WARN'
|
|
|
|
|
|
def test_run_all_carries_the_check(stripping, rolling):
|
|
a = statics.analyse(_panel(), stripping, rolling, role='strip')
|
|
assert 'ROW-FLEX' in _codes(checks.run_all(a, rolling))
|
|
|
|
|
|
# ------------------------------- a line that is not a row is not rated
|
|
|
|
def _staggered_panel():
|
|
"""A real shape out of the model: the two inserts of one row sit 0.5 ft
|
|
apart along the piece, which is more than handling.group_tolerance_ft, so
|
|
the solve reads them as two support lines each holding one insert."""
|
|
panel = _panel()
|
|
left, right = pci.optimal_two_line_supports(LENGTH_FT)
|
|
return dataclasses.replace(panel, lifters=[
|
|
dataclasses.replace(panel.lifters[0], x=left, y=2.5),
|
|
dataclasses.replace(panel.lifters[1], x=left + 0.5, y=7.5),
|
|
dataclasses.replace(panel.lifters[2], x=right, y=2.5),
|
|
dataclasses.replace(panel.lifters[3], x=right, y=7.5)])
|
|
|
|
|
|
def test_a_line_holding_one_insert_is_not_rated_as_a_row(stripping, rolling):
|
|
"""One insert has no strip BETWEEN inserts -- the width hangs off a
|
|
point. Rating it cantilevered the whole half-width off that point and,
|
|
with an opening beside it, reported 33,491 psi on a piece that ships."""
|
|
found = _findings(_staggered_panel(), stripping, rolling)
|
|
notes = [f for f in found if f.code == 'ROW-FLEX-NOT-A-ROW']
|
|
assert notes and all(f.severity == 'WARN' for f in notes)
|
|
assert 'one insert' in notes[0].message
|
|
assert 'group_tolerance_ft' in notes[0].message
|
|
assert not [f for f in found if f.severity == 'FAIL']
|
|
|
|
|
|
def test_a_line_being_pulled_up_is_not_rated_either(stripping):
|
|
"""Two support lines half a foot apart make the static solve lift one of
|
|
them: -12,621 lb against +38,138 lb next to it. Taking abs() of that and
|
|
rating a strip against the result is arithmetic on a number that means
|
|
the opposite of what it says.
|
|
|
|
Static, not rolling, because that is the mode that produces it: an
|
|
equalising rig shares the load and never lifts a line, so under rolling
|
|
this case cannot arise and a test written on it would pass without
|
|
running the code it names.
|
|
"""
|
|
static = Rigging(mode='static')
|
|
a = statics.analyse(_staggered_panel(), stripping, static, role='strip')
|
|
lifted = [l for l in a.rig.lines if l.reaction_lb <= 0.0]
|
|
assert lifted, 'the fixture stopped producing an uplift reaction'
|
|
found = checks.check_row_transverse_flexure(a)
|
|
assert not [f for f in found if f.severity == 'FAIL']
|
|
# This geometry trips 'one insert' first, so force a two-insert line to
|
|
# be the one lifted and check the uplift branch on its own terms.
|
|
panel = _panel()
|
|
left, right = pci.optimal_two_line_supports(LENGTH_FT)
|
|
panel = dataclasses.replace(panel, lifters=[
|
|
dataclasses.replace(panel.lifters[0], x=left, y=2.5),
|
|
dataclasses.replace(panel.lifters[1], x=left, y=7.5),
|
|
dataclasses.replace(panel.lifters[2], x=left + 0.5, y=2.5),
|
|
dataclasses.replace(panel.lifters[3], x=left + 0.5, y=7.5)])
|
|
a = statics.analyse(panel, stripping, static, role='strip')
|
|
assert any(l.reaction_lb <= 0.0 and len(l.lifters) == 2
|
|
for l in a.rig.lines), 'no two-insert line was lifted'
|
|
found = checks.check_row_transverse_flexure(a)
|
|
assert any('pulled UP' in f.message for f in found)
|
|
assert not [f for f in found if f.severity == 'FAIL']
|
|
|
|
|
|
def test_a_severed_band_does_not_become_a_zero_depth_failure(stripping,
|
|
rolling):
|
|
"""WP156's shape: one opening crosses the row inside the strip, another
|
|
sits far away on the same strip. The crossing one severs a band -- the
|
|
load goes around it -- and the far one must not turn that into a
|
|
'no section at all' FAIL naming the wrong opening."""
|
|
left, right = pci.optimal_two_line_supports(LENGTH_FT)
|
|
panel = dataclasses.replace(_panel(), blockouts=[
|
|
# Squeeze the concrete beside the row to 6 in on both sides...
|
|
Blockout(name='OUTBOARD', x0=left - 1.17, y0=4.0, length=0.67,
|
|
width=3.0),
|
|
Blockout(name='INBOARD', x0=left + 0.5, y0=4.0, length=0.67,
|
|
width=3.0),
|
|
# ...and cut the strip itself with a third that crosses the row.
|
|
Blockout(name='CROSSING', x0=left - 0.3, y0=4.0, length=0.67,
|
|
width=3.0)])
|
|
found = _findings(panel, stripping, rolling)
|
|
notes = [f for f in found if f.code == 'ROW-FLEX-INTERRUPTED']
|
|
assert len(notes) == 1
|
|
assert 'opening CROSSING' in notes[0].message
|
|
assert 'INBOARD' not in notes[0].message
|
|
# The severed strip must not ALSO be rated behind the note. Carrying on
|
|
# with the two squeezing openings rates that row on 6 in of web and
|
|
# reports 1,750 psi against 296 -- a FAIL invented for a strip the model
|
|
# has just said it cannot speak about.
|
|
assert not [f for f in found if f.severity == 'FAIL']
|
|
rated = [f for f in found if f.code == 'ROW-FLEX']
|
|
assert len(rated) == 1
|
|
assert abs(rated[0].at_ft - right) < 0.01
|
|
|
|
|
|
# --------------------------- where the pinch sits is most of the answer
|
|
|
|
def _pinched(y0, y1):
|
|
"""Two openings, one each side of the left pick row, squeezing the web to
|
|
6 in over the band y0..y1 of the strip between the inserts at 2.5 and 7.5.
|
|
"""
|
|
left, _ = pci.optimal_two_line_supports(LENGTH_FT)
|
|
return dataclasses.replace(_panel(), blockouts=[
|
|
Blockout(name='L', x0=left - 1.17, y0=y0, length=0.67, width=y1 - y0),
|
|
Blockout(name='R', x0=left + 0.5, y0=y0, length=0.67, width=y1 - y0)])
|
|
|
|
|
|
def test_a_pinch_beside_an_insert_costs_less_than_one_at_midspan(stripping,
|
|
rolling):
|
|
"""A strip between two inserts carries its largest moment at midspan and
|
|
none at all at the inserts. Concrete missing where the moment is zero is
|
|
not the same as concrete missing where it peaks -- and taking the
|
|
smallest web anywhere and applying it to the whole strip said it was,
|
|
reporting 522 psi against 444 on a piece whose midspan is solid.
|
|
"""
|
|
at_end = _f_t_psi(_findings(_pinched(2.6, 3.1), stripping, rolling)[0])
|
|
at_mid = _f_t_psi(_findings(_pinched(4.75, 5.25), stripping, rolling)[0])
|
|
assert at_mid > 2.0 * at_end
|
|
|
|
|
|
def test_the_pinched_band_is_rated_at_its_own_moment(stripping, rolling):
|
|
"""Closed form for the band beside the insert: the strip spans 2.5 to
|
|
7.5, the band ends at 3.1, and a simply supported span carries
|
|
w*(y-a)*(b-y)/2 there -- not w*L^2/8, which belongs to midspan.
|
|
|
|
w comes off the solved line rather than half the weight: the section is
|
|
6 in deep, so a per-cent of reaction is tens of psi here and an
|
|
approximate w would make this test about the approximation.
|
|
"""
|
|
panel = _pinched(2.6, 3.1)
|
|
a = statics.analyse(panel, stripping, rolling, role='strip')
|
|
line = min(a.rig.lines, key=lambda l: l.x)
|
|
w_lbft = line.reaction_lb / WIDTH_FT
|
|
m_lbft = w_lbft * (3.1 - 2.5) * (7.5 - 3.1) / 2.0
|
|
s_in3 = THICKNESS_IN * 6.0 ** 2 / 6.0 # 6 in of web
|
|
found = checks.check_row_transverse_flexure(a)[0]
|
|
assert abs(_f_t_psi(found) - m_lbft * 12.0 / s_in3) < 0.2
|
|
|
|
|
|
def test_a_pinch_over_the_whole_strip_still_governs_at_midspan(stripping,
|
|
rolling):
|
|
"""The uniform case has to survive the change: squeeze the whole strip
|
|
and the answer goes back to w*L^2/8 on the reduced section."""
|
|
panel = _pinched(2.5, 7.5) # exactly the strip, not the overhangs
|
|
a = statics.analyse(panel, stripping, rolling, role='strip')
|
|
line = min(a.rig.lines, key=lambda l: l.x)
|
|
w_lbft = line.reaction_lb / WIDTH_FT
|
|
m_lbft = w_lbft * (WIDTH_FT / 2.0) ** 2 / 8.0
|
|
s_in3 = THICKNESS_IN * 6.0 ** 2 / 6.0
|
|
found = checks.check_row_transverse_flexure(a)[0]
|
|
assert 'overhang' not in found.message
|
|
assert abs(_f_t_psi(found) - m_lbft * 12.0 / s_in3) < 0.2
|