# -*- coding: utf-8 -*- """Prestress, transfer length, and what a blockout does to both. The numbers in `test_matches_a_commercial_check` are from an independent run of the same piece through Eriksson Wall v5.2.0. That program was not consulted while this was written -- its inputs were read off its own file and its outputs off its own report -- so agreement to the last printed digit is worth having, and disagreement is worth stopping for. The piece: 38'-7.5" x 10'-7.5" x 8", one opening 111.25 x 40.25 set 16 in. from one edge, twelve 1/2 in. 270k strands in two rows. """ import pytest from panellift import checks, prestress, section from panellift.model import Blockout, Panel, Prestress, Strand PULL = 0.153 * 270_000 * 0.75 # 75% pull on a 1/2 in. 270k strand POSITIONS_IN = [3.5, 16.5, 40.5, 59.5, 87.5, 109.5] ROWS_IN = [1.75, 6.25] def sample_panel(loss_pct=1.58, blockout=True): strands = [Strand(y=y / 12.0, z_in=z, area_in2=0.153, initial_pull_lb=PULL) for z in ROWS_IN for y in POSITIONS_IN] blockouts = [] if blockout: blockouts.append(Blockout(name='OPENING 1', x0=330.0 / 12.0, y0=16.0 / 12.0, length=111.25 / 12.0, width=40.25 / 12.0)) return Panel( mark='sample', length_ft=463.5 / 12.0, width_ft=127.5 / 12.0, thickness_in=8.0, fc_psi=6000.0, fci_psi=3500.0, blockouts=blockouts, prestress=Prestress(strands=strands, loss_pct=loss_pct, diameter_in=0.5)) # ------------------------------------------------------------------ transfer def test_transfer_length_follows_the_effective_stress(): panel = sample_panel() # f_se = 0.75 * 270 * (1 - 0.0158) = 199.3 ksi; lt = f_se * db / 3 assert prestress.transfer_length_in(panel.prestress) == \ pytest.approx(33.2, abs=0.1) def test_a_strand_picks_its_force_up_over_the_transfer_length(): panel = sample_panel() lt = prestress.transfer_length_in(panel.prestress) / 12.0 strand = panel.prestress.strands[0] # y = 3.5 in, never cut assert prestress.developed(strand, panel, 0.0, lt) == 0.0 assert prestress.developed(strand, panel, lt / 2, lt) == pytest.approx(0.5) assert prestress.developed(strand, panel, lt, lt) == pytest.approx(1.0) assert prestress.developed(strand, panel, 10.0, lt) == pytest.approx(1.0) def test_the_far_end_is_a_free_end_too(): panel = sample_panel() lt = prestress.transfer_length_in(panel.prestress) / 12.0 strand = panel.prestress.strands[0] assert prestress.developed(strand, panel, panel.length_ft, lt) == 0.0 assert prestress.developed( strand, panel, panel.length_ft - lt / 2, lt) == pytest.approx(0.5) # ------------------------------------------------------- blockouts cut strands def test_a_blockout_cuts_only_the_strands_it_passes_over(): panel = sample_panel() cut = [s for s in panel.prestress.strands if prestress.cuts_for(s, panel)] # the opening spans y 16 -> 56.25 in, which is 16.5 and 40.5 assert len(cut) == 4 assert sorted(set(round(s.y * 12.0, 2) for s in cut)) == [16.5, 40.5] def test_area_inside_the_opening_is_the_uncut_strands_only(): panel = sample_panel() state = prestress.state_at(panel, 28.97) # mid-opening assert state.area_in2 == pytest.approx(1.22, abs=0.01) def test_area_ramps_down_approaching_the_cut(): """Eriksson reports 1.84 / 1.75 / 1.33 / 1.22 at these four stations.""" panel = sample_panel() for x_ft, expected in ((22.62, 1.84), (25.11, 1.75), (27.04, 1.33), (27.50, 1.22)): state = prestress.state_at(panel, x_ft) assert state.area_in2 == pytest.approx(expected, abs=0.01), x_ft def test_a_strand_the_opening_misses_keeps_its_force_through_it(): panel = sample_panel() lt = prestress.transfer_length_in(panel.prestress) / 12.0 outer = [s for s in panel.prestress.strands if round(s.y * 12.0, 2) == 109.5][0] assert prestress.developed(outer, panel, 28.97, lt) == pytest.approx(1.0) # ------------------------------------------------------------- net section def test_an_off_centre_opening_moves_the_centroid(): panel = sample_panel() net = section.NetSection(panel, 28.97) assert net.area_in2 == pytest.approx(698.0, abs=1.0) assert net.cy_in == pytest.approx(76.49, abs=0.05) assert net.sz_in3 == pytest.approx(930.7, abs=1.0) assert net.iy_in4 == pytest.approx(979_000, rel=0.005) def test_a_solid_section_is_centred(): panel = sample_panel(blockout=False) net = section.NetSection(panel, 19.0) assert net.cy_in == pytest.approx(63.75, abs=0.01) assert net.area_in2 == pytest.approx(1020.0, abs=0.5) # ------------------------------------------------- the number that matters def _corners(panel, x, moment_lbft): net = section.NetSection(panel, x) return [c[0] for c in checks._corner_stresses(panel, x, moment_lbft, net)] def _corner_max(panel, x, moment_lbft): return max(_corners(panel, x, moment_lbft)) def test_matches_a_commercial_check(): """417.6 psi at 28.97 ft, from Eriksson Wall on the same piece. Their factored service moment there is 48.17 k-ft. Feed the same moment into the same section and the answer has to be the same, or one of us is wrong about the mechanics rather than about the panel. """ panel = sample_panel(loss_pct=1.58) got = _corner_max(panel, 28.97, 48_170.0) assert got == pytest.approx(417.6, abs=2.0) def test_without_prestress_the_same_piece_reads_far_higher(): """The bug this whole feature exists to fix, kept as a number.""" panel = sample_panel() bare = Panel(**{**panel.__dict__, 'prestress': None}) assert _corner_max(bare, 28.97, 48_170.0) == pytest.approx(621.0, abs=2.0) def test_precompression_wipes_out_a_light_stage(): """Our own stripping moment on this piece, which is larger than theirs. 23.83 k-ft of bending on the solid section against ~359 psi of precompression: 210 psi of it goes, and the piece comes back well under the limit instead of at 47% of it. """ panel = sample_panel() assert _corner_max(panel, 10.94, 23_830.0) < 40.0 def test_precompression_and_compression_both_match_at_stripping(): """Eriksson's stripping at 0.25L: tension 0.0 psi, compression 667.4. Their moment there is 14.10 k-ft, not ours -- they idealise a static lift and this idealises rolling blocks, so the two disagree about the reactions before either says anything about stress. Feeding THEIR moment in isolates the section from the rigging, and both faces then have to agree, which is a stronger check than the tension alone: the tension is zero either way once there is enough precompression, but the compression is not, and it pins P/A and the eccentricity together. """ panel = sample_panel(loss_pct=1.58) corners = _corners(panel, 9.66, 14_100.0) assert max(corners) <= 0.0 assert -min(corners) == pytest.approx(667.4, abs=2.0) def test_a_bigger_loss_gives_back_tension(): """The loss assumption is a real lever, so it has to move the answer.""" lean = _corner_max(sample_panel(loss_pct=1.58), 28.97, 48_170.0) fat = _corner_max(sample_panel(loss_pct=8.0), 28.97, 48_170.0) assert fat > lean + 10.0 # --------------------------------------------------------- sign conventions def _one_row_panel(z_in): strands = [Strand(y=y / 12.0, z_in=z_in, initial_pull_lb=PULL) for y in [15.0, 45.0, 75.0, 105.0]] return Panel(mark='s', length_ft=30.0, width_ft=10.0, thickness_in=8.0, fc_psi=6000.0, prestress=Prestress(strands=strands, loss_pct=0.0)) def test_a_low_strand_helps_the_face_that_sagging_puts_in_tension(): """Sagging puts tension on the form face, z = 0. A strand below mid-depth hogs the piece and takes tension off that face; the same strand above mid-depth adds to it. Nothing in the commercial comparison can catch this: that piece has two symmetric rows, so its eccentricity is exactly zero and the term this tests does not exist. It was written wrong the first time and this is what found it. """ low = _corners(_one_row_panel(1.0), 15.0, 20_000.0)[:2] # cz = -t/2 mid = _corners(_one_row_panel(4.0), 15.0, 20_000.0)[:2] high = _corners(_one_row_panel(7.0), 15.0, 20_000.0)[:2] assert max(low) < max(mid) < max(high)