# -*- coding: utf-8 -*- """The Tier-1 handling additions to `capacity`: the extreme-fibre STRESS check and the phi development RAMP. Both are ADDITIVE -- `capacity_at` and every type it returns are untouched, and their tests (test_capacity.py, test_station_table.py, ...) stand as they were. These pin the new functions on numbers a reviewer can reproduce on paper: * `extreme_fibre_stresses` is the task's Pi/A -/+ (Mg + Pi*e)*c/I, hand checked to the psi. * `handling_stress_at` builds those from a real panel/stage and divides by the STAGE's own allowables (tension_limit_sqrt_fc*sqrt(f'c), compression_limit_ratio*f'c) -- the settings that were read by nothing until this landed. A stage set to 5 vs 7.5 sqrt(f'c) moves the tension utilisation by exactly 1.5. * `phi_flexure_ramped` / `ramped_phi_mn` are the 0.75->0.90 ramp across the transfer/development length. """ import math import pytest from panellift import capacity, prestress from panellift.model import Panel, Prestress, Stage, Strand # --------------------------------------------------- the pure stress formula def test_extreme_fibre_stresses_hand_check(): """Pi=100000, A=1000, e=0, c=4, I=2000, Mg=10000 -> form +140, far -340. S = I/c = 500 in3; Pi/A = 100 psi (compression, negative); Mg/S = 240 psi. Form face (z=0): -100 + 240 = +140 (tension). Far face: -100 - 240 = -340. """ f_form, f_far = capacity.extreme_fibre_stresses( pi_lb=100000.0, area_in2=1000.0, e_in=0.0, c_in=4.0, i_in4=2000.0, moment_lbft=10000.0) assert f_form == pytest.approx(140.0) assert f_far == pytest.approx(-340.0) def test_extreme_fibre_stresses_eccentric_prestress(): """A low strand (e<0) hogs the piece: it ADDS compression on the form face. e = -2 in, Pi=120000, so Pi*e = -240000 lb-in. With A=1000, c=4, I=2000 (S=500) and Mg=0: form = -120 + (-240000/500) = -120 - 480 = -600; far = -120 + 480 = +360 (tension on the far face, which is the hogging a low strand causes). """ f_form, f_far = capacity.extreme_fibre_stresses( pi_lb=120000.0, area_in2=1000.0, e_in=-2.0, c_in=4.0, i_in4=2000.0, moment_lbft=0.0) assert f_form == pytest.approx(-600.0) assert f_far == pytest.approx(360.0) def test_extreme_fibre_stresses_degenerate_returns_none(): assert capacity.extreme_fibre_stresses(1.0, 0.0, 0.0, 4.0, 2000.0, 1.0) \ == (None, None) assert capacity.extreme_fibre_stresses(1.0, 1000.0, 0.0, 4.0, 0.0, 1.0) \ == (None, None) # ------------------------------------------------ handling_stress_at on panel def _solid_panel(): """A plain solid panel with two symmetric strands -- e_z = 0, no openings. t=8 in, so mid-depth is 4 in; strands at z=1.5 and z=6.5 (= 8-1.5) with equal force put the group centroid at mid-depth (e_z=0) and, being full width, leave the in-plane term e_y=0 too. So the fibre stress is exactly the 1-D Pi/A -/+ Mg*c/I with no prestress bending -- hand-reproducible. """ strands = [Strand(y=2.0, z_in=1.5, area_in2=0.153, initial_pull_lb=30000.0), Strand(y=2.0, z_in=6.5, area_in2=0.153, initial_pull_lb=30000.0)] pre = Prestress(strands=strands, loss_pct=0.0, diameter_in=0.5, fpu_ksi=270.0) return Panel(mark='S', 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) def test_handling_stress_at_matches_the_pure_formula(): panel = _solid_panel() stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0, tension_limit_sqrt_fc=7.5, compression_limit_ratio=0.6) x = 8.0 m = 12000.0 sp = capacity.handling_stress_at(panel, stage, x, m) # Rebuild the ingredients the way the function does, independently. from panellift import section net = section.NetSection(panel, x) state = prestress.state_at(panel, x) pi = state.force_lb assert pi > 0 assert sp.e_in == pytest.approx(0.0, abs=1e-9) # symmetric -> e_z = 0 f_form, f_far = capacity.extreme_fibre_stresses( pi, net.area_in2, 0.0, panel.thickness_in / 2.0, net.iz_in4, m) assert sp.f_form_psi == pytest.approx(f_form) assert sp.f_far_psi == pytest.approx(f_far) assert sp.in_plane_psi == pytest.approx(0.0, abs=1e-6) def test_handling_stress_utils_use_the_stages_allowables(): """The utilisation divides by the STAGE's own allowables -- the settings that nothing read before. tension over tension_limit_sqrt_fc*sqrt(f'c), compression over compression_limit_ratio*f'c.""" panel = _solid_panel() 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, 10.0, 15000.0) allow_t = 7.5 * math.sqrt(3500.0) allow_c = 0.6 * 3500.0 assert sp.tension_util == pytest.approx(sp.tension_psi / allow_t) assert sp.compression_util == pytest.approx(sp.compression_psi / allow_c) def test_handling_stress_limit_coefficient_moves_the_tension_util_by_1p5(): """5 vs 7.5 sqrt(f'c) is the whole 1.5x that the two schools argue about, and it lands on the tension utilisation and nowhere else.""" panel = _solid_panel() common = dict(multiplier=1.4, fc_psi=3500.0, compression_limit_ratio=0.6) five = Stage(name='a', tension_limit_sqrt_fc=5.0, **common) seven = Stage(name='b', tension_limit_sqrt_fc=7.5, **common) a = capacity.handling_stress_at(panel, five, 10.0, 15000.0) b = capacity.handling_stress_at(panel, seven, 10.0, 15000.0) assert a.tension_psi == pytest.approx(b.tension_psi) # same stress # 5 is the stricter school -> a higher utilisation, by exactly 1.5. assert a.tension_util == pytest.approx(b.tension_util * 1.5) def test_handling_stress_no_allowable_is_a_dash_not_a_zero(): """A stage stating no strength has no allowable to divide by -- util is None with a reason, never a fabricated 0, the same could-not-judge discipline as every ratio in this module.""" panel = _solid_panel() stage = Stage(name='s', multiplier=1.0, fc_psi=0.0) sp = capacity.handling_stress_at(panel, stage, 10.0, 15000.0) assert sp.tension_util is None and sp.compression_util is None assert sp.why # --------------------------------- phi from net tensile strain (Table 21.2.2) def test_phi_flexure_from_strain_boundaries_and_slope(): """ACI 318-14 Table 21.2.2, prestressed floor 0.65, eps_ty 0.002.""" assert capacity.phi_flexure_from_strain(0.001) == pytest.approx(0.65) assert capacity.phi_flexure_from_strain(0.002) == pytest.approx(0.65) # midway: 0.65 + 0.25*(0.0035-0.002)/(0.005-0.002) = 0.65 + 0.125 = 0.775 assert capacity.phi_flexure_from_strain(0.0035) == pytest.approx(0.775) assert capacity.phi_flexure_from_strain(0.005) == pytest.approx(0.90) assert capacity.phi_flexure_from_strain(0.010) == pytest.approx(0.90) def test_phi_flexure_from_strain_none_is_the_flat_value(): """No strain to judge on is not a reason to drop to the floor.""" assert capacity.phi_flexure_from_strain(None) == pytest.approx(0.90) def test_handling_flexure_at_applies_the_strain_phi_to_the_same_mn(): """M_n does not change; only phi moves, from the section's strain.""" pt = capacity.CapacityPoint(x=5.0) pt.phi = 0.90 pt.phi_mn_lbft = 9000.0 # M_n = 10000 lb-ft pt.strain_t = 0.0035 # transition -> phi 0.775 phi, phi_mn, ratio = capacity.handling_flexure_at(pt, moment_lbft=3100.0) assert phi == pytest.approx(0.775) assert phi_mn == pytest.approx(10000.0 * 0.775) # = 7750 assert ratio == pytest.approx(3100.0 / 7750.0) def test_handling_flexure_at_tension_controlled_is_flat_0p90(): pt = capacity.CapacityPoint(x=5.0) pt.phi = 0.90 pt.phi_mn_lbft = 9000.0 pt.strain_t = 0.020 # well tension-controlled phi, phi_mn, _ = capacity.handling_flexure_at(pt, moment_lbft=3000.0) assert phi == pytest.approx(0.90) assert phi_mn == pytest.approx(9000.0) # unchanged from flat def test_handling_flexure_at_rebuilds_mn_in_the_transition_region(): """Where capacity_at bailed (no phi*M_n) but left the equilibrium geometry, M_n is rebuilt from C = 0.85 f'c a b at lever arm (d_p - a/2).""" pt = capacity.CapacityPoint(x=5.0) # phi_mn_lbft stays None pt.a_in = 1.0 pt.b_in = 100.0 pt.dp_in = 6.0 pt.fc_psi = 3500.0 pt.strain_t = 0.003 # transition phi, phi_mn, ratio = capacity.handling_flexure_at(pt, moment_lbft=5000.0) tension = 0.85 * 3500.0 * 1.0 * 100.0 mn = tension * (6.0 - 0.5) / 12.0 assert phi == pytest.approx(0.65 + 0.25 * (0.003 - 0.002) / 0.003) assert phi_mn == pytest.approx(phi * mn) assert ratio == pytest.approx(5000.0 / (phi * mn)) def test_handling_flexure_at_no_strength_is_none_not_zero(): pt = capacity.CapacityPoint(x=5.0) # no phi_mn, no geometry, no eps assert capacity.handling_flexure_at(pt, moment_lbft=1000.0) \ == (None, None, None)