Files
willtools-demo/panellift/tests/test_developed_at_opening.py
T
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
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.
2026-09-09 08:35:15 -05:00

131 lines
6.6 KiB
Python

# -*- coding: utf-8 -*-
"""The reference tool's (Eriksson Wall) §1.3 developed-strand-at-opening rule:
a strand a blockout SAW-CUT re-develops over the DEVELOPMENT length from that
edge, while a strand's transfer from the PIECE END is unchanged.
docs/eriksson-wall-algorithms.md sec.1.3 / sec.9. ADDITIVE and OPT-IN: it fires
only when a caller passes `ld_ft` (the handling STRESS path,
`capacity.handling_stress_at`). `prestress.state_at` with no `ld_ft` -- which is
what Panel Lift Check's `checks.stress_profile` uses -- keeps the physically
correct transfer-length force model, so a clean solid panel is byte-for-byte
what it was (validated against Eriksson to ~1%).
"""
import pytest
from panellift import capacity, prestress
from panellift.model import Blockout, Panel, Prestress, Stage, Strand
def _panel_with_one_opening():
"""20 ft panel, a full-depth opening x = 8..12 across the strand's y band.
The single strand at y = 2 is cut by it, so it has two bonded runs,
[0, 8] and [12, 20]. l_t = 2 ft (stated), and the tests pass l_d = 5 ft.
"""
strand = Strand(y=2.0, z_in=1.5, area_in2=0.153, initial_pull_lb=30000.0)
pre = Prestress(strands=[strand], loss_pct=0.0, diameter_in=0.5,
fpu_ksi=270.0, transfer_length_in=24.0) # l_t = 2 ft
panel = Panel(mark='O', length_ft=20.0, width_ft=4.0, thickness_in=8.0,
unit_weight_pcf=150.0, fci_psi=3500.0, fc_psi=6000.0,
prestress=pre)
panel.blockouts = [Blockout(name='O1', x0=8.0, y0=0.0, length=4.0,
width=4.0)]
return panel, strand
def test_developed_uses_development_length_from_an_opening_edge():
"""At x = 6, 2 ft short of the opening edge at x = 8, over l_d = 5:
fraction = (8 - 6)/5 = 0.4 -- against 1.0 with transfer only.
The piece-end side (x = 6, l_t = 2) is already full, so the opening edge
governs.
"""
panel, strand = _panel_with_one_opening()
# transfer only: (8-6)/2 = 1.0 on the opening side -> full
assert prestress.developed(strand, panel, 6.0, lt_ft=2.0) == pytest.approx(1.0)
# development length from the opening edge: (8-6)/5 = 0.4
assert prestress.developed(strand, panel, 6.0, lt_ft=2.0,
ld_ft=5.0) == pytest.approx(0.4)
def test_developed_keeps_transfer_length_from_the_piece_end():
"""At x = 1, near the piece end (x = 0), the transfer length still governs
even when l_d is supplied: (1 - 0)/2 = 0.5, the same both ways. Releasing
at the bed transfers f_se over l_t; only a saw-cut rebuilds over l_d."""
panel, strand = _panel_with_one_opening()
assert prestress.developed(strand, panel, 1.0, lt_ft=2.0) == pytest.approx(0.5)
assert prestress.developed(strand, panel, 1.0, lt_ft=2.0,
ld_ft=5.0) == pytest.approx(0.5)
def test_developed_on_the_far_side_of_the_opening():
"""In the second run [12, 20], the near edge x = 12 is an OPENING edge, so
at x = 14: (14 - 12)/5 = 0.4 with l_d, 1.0 with transfer only."""
panel, strand = _panel_with_one_opening()
assert prestress.developed(strand, panel, 14.0, lt_ft=2.0) == pytest.approx(1.0)
assert prestress.developed(strand, panel, 14.0, lt_ft=2.0,
ld_ft=5.0) == pytest.approx(0.4)
def test_state_at_ld_reduces_pi_near_an_opening():
"""`state_at(..., ld_ft=5)` credits less prestress force approaching an
opening than the transfer-only default. Pi = 30000 * developed."""
panel, _ = _panel_with_one_opening()
s_transfer = prestress.state_at(panel, 6.0)
s_dev = prestress.state_at(panel, 6.0, ld_ft=5.0)
assert s_transfer.force_lb == pytest.approx(30000.0) # full
assert s_dev.force_lb == pytest.approx(30000.0 * 0.4) # 12000
def test_clean_panel_is_untouched_by_the_ld_rule():
"""A panel with NO openings has only piece-end boundaries, so passing l_d
changes nothing -- the WPA2.0 clean-panel match is preserved. Checked at
several stations, near end and midspan alike."""
strand = Strand(y=2.0, z_in=1.5, area_in2=0.153, initial_pull_lb=30000.0)
pre = Prestress(strands=[strand], loss_pct=0.0, diameter_in=0.5,
fpu_ksi=270.0, transfer_length_in=24.0)
panel = Panel(mark='C', length_ft=20.0, width_ft=4.0, thickness_in=8.0,
unit_weight_pcf=150.0, fci_psi=3500.0, fc_psi=6000.0,
prestress=pre) # no blockouts
for x in (0.5, 1.5, 5.0, 10.0, 18.5, 19.5):
a = prestress.state_at(panel, x)
b = prestress.state_at(panel, x, ld_ft=5.0)
assert a.force_lb == pytest.approx(b.force_lb)
assert a.cy_in == pytest.approx(b.cy_in)
def test_stress_development_length_is_built_on_the_transfer_length():
"""The representative l_d handling_stress_at uses is l_t + (f_ps - f_se) d_b
-- the ACI 25.4.8.1 form on the SAME transfer length the force model uses,
so it is always at least the transfer length."""
panel, _ = _panel_with_one_opening()
stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0)
ld = capacity.stress_development_length_ft(panel, stage)
lt = prestress.transfer_length_in(panel.prestress) / 12.0
assert ld >= lt
assert ld == pytest.approx(2.0, abs=1e-9) or ld > 2.0 # l_t was 2.0 ft
def test_handling_stress_solid_panel_unchanged_by_the_rule():
"""On a solid panel the §1.3 rule is a no-op, so the extreme-fibre stress
is exactly the transfer-model value -- the guard that the clean-panel
parity match cannot regress."""
strand_b = Strand(y=2.0, z_in=1.5, area_in2=0.153, initial_pull_lb=30000.0)
strand_t = Strand(y=2.0, z_in=6.5, area_in2=0.153, initial_pull_lb=30000.0)
pre = Prestress(strands=[strand_b, strand_t], loss_pct=0.0, diameter_in=0.5,
fpu_ksi=270.0)
panel = Panel(mark='C', length_ft=20.0, width_ft=4.0, thickness_in=8.0,
unit_weight_pcf=150.0, fci_psi=3500.0, fc_psi=6000.0,
prestress=pre)
stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0,
tension_limit_sqrt_fc=7.5, compression_limit_ratio=0.6)
sp = capacity.handling_stress_at(panel, stage, 8.0, 12000.0)
# rebuild with the plain transfer-model state, no ld gating
from panellift import section
net = section.NetSection(panel, 8.0)
st = prestress.state_at(panel, 8.0) # no ld_ft
f_form, f_far = capacity.extreme_fibre_stresses(
st.force_lb, net.area_in2, 0.0, panel.thickness_in / 2.0,
net.iz_in4, 12000.0)
assert sp.f_form_psi == pytest.approx(f_form)
assert sp.f_far_psi == pytest.approx(f_far)