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