# -*- coding: utf-8 -*- """The In-Place SERVICE slice: ASCE 2.4 combos, extreme-fibre stress vs the cracking / compression limits, deflection, and the P-Delta magnifier. Neutral fixtures only -- the parity against the reference tool's real pieces lives in the scratchpad harness (parity_service.py). These pin the identities any correct service answer must satisfy: * the service set is ASCE 7-16 2.4.1(1)-(5), T at 0.75, no 0.6D+0.6W; * SC5 = 1.0D + 0.6W assembles exactly 1.0*P_D and 0.6*M_W; * extreme-fibre tension = M/S - P/A (mild), util = tension / 7.5 sqrt(f'c), and a >1 util is REPORTED (the tool flags it, docs 6.1); * the P-Delta magnifier is 1/(1-gamma), gamma = Pu L^2 / (8 EI_eff), and an instability is NOT JUDGED, named -- never a fabricated number; * a deflection with no L/n limit is computed but its LIMIT check is NOT JUDGED, named. """ import math from panellift import capacity, inplace, section from panellift.model import Panel def plain_wall(length_ft=30.0, width_ft=10.0, thickness_in=12.0, fc_psi=6000.0): return Panel(mark='SVC-WALL', length_ft=length_ft, width_ft=width_ft, thickness_in=thickness_in, unit_weight_pcf=150.0, fc_psi=fc_psi, fci_psi=3500.0, prestress=None) def _service_cases(x_lo, x_hi, p_lb, m_lbft, wind_factor=0.6): """A cases dict where SC5 (1.0D + 0.6W) assembles to (p_lb, m_lbft): D carries the axial, W carries m/0.6 so 0.6*W = m.""" xs = [x_lo, x_hi] return { 'D': inplace.LoadField('D', 'Dead', xs, [p_lb, p_lb], [0.0, 0.0], [0.0, 0.0]), 'W': inplace.LoadField('W', 'Wind', xs, [0.0, 0.0], [m_lbft / wind_factor, m_lbft / wind_factor], [0.0, 0.0]), } def _stage(panel): return inplace.in_place_stage(panel, inplace.InPlaceInputs( restraint_xs=[0.0, panel.length_ft])) def _sc5(): return next(c for c in inplace.ASCE_7_16_SERVICE if c.name == 'SC5') # --------------------------------------------------------------- the combos def test_service_set_is_the_asce_2_4_basic_combinations(): names = [c.name for c in inplace.ASCE_7_16_SERVICE] assert names == ['SC1', 'SC2', 'SC3', 'SC4', 'SC5'] for c in inplace.ASCE_7_16_SERVICE: assert c.kind == 'service' assert c.factors.get('T') == 0.75 sc5 = _sc5() assert sc5.factors['D'] == 1.0 and sc5.factors['W'] == 0.6 # combination (7) 0.6D+0.6W is deliberately NOT in the reference set. assert not any(c.factors.get('D') == 0.6 for c in inplace.ASCE_7_16_SERVICE) def test_service_combo_assembles_the_right_demand(): """SC5 = 1.0D + 0.6W puts 1.0*P_D and 0.6*M_W, nothing else.""" panel = plain_wall() cases = _service_cases(0.0, panel.length_ft, p_lb=40000.0, m_lbft=100000.0) pu, mu, vu = inplace.combination_demand(cases, _sc5(), 12.0) assert abs(pu - 40000.0) < 1e-6 assert abs(mu - 100000.0) < 1e-6 assert abs(vu) < 1e-9 # --------------------------------------------------------------- the stress def test_service_tension_matches_hand_stress_and_cracking_util(): """WPA1.0's governing service tension row: a 12 in x 164 in solid mild wall at M = 260.99 k-ft, P = 36.9 k gives 776.9 psi vs 580.9 (7.5 sqrt 6000).""" panel = Panel(mark='SVC', length_ft=43.08, width_ft=164.0 / 12.0, thickness_in=12.0, unit_weight_pcf=150.0, fci_psi=3500.0, fc_psi=6000.0, prestress=None) cases = _service_cases(0.0, panel.length_ft, p_lb=36900.0, m_lbft=260990.0) st = inplace.service_station(panel, _stage(panel), cases, [_sc5()], 25.1) assert st.why is None assert abs(st.cracking_limit_psi - 580.9) < 0.5 assert abs(st.tension_psi - 776.9) < 1.0 assert abs(st.compression_psi - 814.4) < 1.0 # util = tension / cracking; over 1.0 here -- the tool FLAGS it, we report it assert st.tension_util > 1.0 assert abs(st.tension_util - 776.9 / 580.9) < 0.01 def test_prestress_precompression_term_is_the_reused_primitive(): """WPA5.0's developed-strand compression row: Pi = 123.3 k, M = 17.22 k-ft, P = 19.7 k -> 815.6 psi through capacity.extreme_fibre_stresses + (-P/A).""" A, i_in4, c_in = 313.5, 3161.0, 5.5 f_form, f_far = capacity.extreme_fibre_stresses( 123300.0, A, 0.0, c_in, i_in4, 17220.0) f_form -= 19700.0 / A f_far -= 19700.0 / A comp = max(0.0, -f_form, -f_far) assert abs(comp - 815.6) < 1.0 def test_no_section_dashes_with_a_reason_not_a_zero(): """A station where NetSection cannot be built is None+why, never a fake 0.""" panel = plain_wall(thickness_in=0.0) cases = _service_cases(0.0, panel.length_ft, 1000.0, 1000.0) st = inplace.service_station(panel, _stage(panel), cases, [_sc5()], 5.0) assert st.why is not None assert st.tension_util is None and st.compression_util is None def test_cracking_class_t_raises_the_limit_to_12_sqrt_fc(): stage = _stage(plain_wall(fc_psi=6000.0)) assert abs(inplace.cracking_limit_psi(stage, class_t=False) - 7.5 * math.sqrt(6000.0)) < 0.1 assert abs(inplace.cracking_limit_psi(stage, class_t=True) - 12.0 * math.sqrt(6000.0)) < 0.1 # -------------------------------------------------------------- the P-Delta def test_pdelta_magnifier_on_a_hand_case(): """delta = 1/(1-gamma), gamma = Pu L^2 / (8 EI_eff), EI_eff = 0.875 Ec Ig.""" panel = plain_wall(length_ft=30.0, width_ft=10.0, thickness_in=12.0, fc_psi=6000.0) pu, span_ft = 50000.0, 30.0 m = inplace.pdelta_magnifier(panel, _stage(panel), span_ft, pu, 15.0, beta_d=0.0) ec = inplace.ec_aci_psi(6000.0, 150.0) ig = section.NetSection(panel, 15.0).iz_in4 ei = 0.875 * ec * ig gamma = pu * (span_ft * 12.0) ** 2 / (8.0 * ei) assert abs(m.gamma - gamma) < 1e-9 assert abs(m.magnifier - 1.0 / (1.0 - gamma)) < 1e-9 assert m.magnifier > 1.0 def test_pdelta_reports_instability_not_a_fake_number(): panel = plain_wall(length_ft=60.0, width_ft=4.0, thickness_in=6.0, fc_psi=5000.0) m = inplace.pdelta_magnifier(panel, _stage(panel), 60.0, 5.0e6, 30.0) assert m.magnifier is None assert m.why and 'stability' in m.why def test_tension_axial_does_not_magnify(): panel = plain_wall(fc_psi=6000.0) m = inplace.pdelta_magnifier(panel, _stage(panel), panel.length_ft, -30000.0, 15.0) assert m.magnifier == 1.0 and m.gamma == 0.0 # ------------------------------------------------------------ deflection/run def test_run_populates_service_with_deflection_limit(): panel = plain_wall(length_ft=30.0, width_ft=10.0, fc_psi=6000.0) inputs = inplace.InPlaceInputs( restraint_xs=[0.0, 30.0], wind_zones=[inplace.LateralPressureZone(0.0, 30.0, 50.0, 'W')], deflection_limit_ratio=150.0) res = inplace.run(panel, inputs) assert res.service is not None and res.service.stations d = res.service.deflection assert d.combo == 'SC5' assert d.delta_wind_in > 0.0 assert d.delta_total_in is not None and d.delta_total_in >= d.delta_i_in assert abs(d.limit_in - 30.0 * 12.0 / 150.0) < 1e-6 assert d.util is not None and d.util > 0.0 def test_deflection_limit_not_supplied_is_not_judged_not_faked(): panel = plain_wall(length_ft=30.0, width_ft=10.0, fc_psi=6000.0) inputs = inplace.InPlaceInputs( restraint_xs=[0.0, 30.0], wind_zones=[inplace.LateralPressureZone(0.0, 30.0, 50.0, 'W')]) res = inplace.run(panel, inputs) d = res.service.deflection assert d.delta_total_in is not None # deflection still computed assert d.util is None # but not judged against a limit assert d.why and 'deflection limit' in d.why def test_deflection_recovers_the_simple_beam_closed_form(): """Uniform service wind, pinned-pinned: our double integration of the moment field gives 5 w L^4 / (384 EI_eff) at midspan.""" panel = plain_wall(length_ft=20.0, width_ft=8.0, thickness_in=10.0, fc_psi=6000.0) press = 40.0 inputs = inplace.InPlaceInputs( restraint_xs=[0.0, 20.0], wind_zones=[inplace.LateralPressureZone(0.0, 20.0, press, 'W')], deflection_limit_ratio=240.0) d = inplace.run(panel, inputs).service.deflection w_lb_per_in = (0.6 * press * panel.width_ft) / 12.0 l_in = 20.0 * 12.0 closed = 5.0 * w_lb_per_in * l_in ** 4 / (384.0 * d.ei_eff_lbin2) assert abs(d.delta_wind_in - closed) / closed < 0.01