# -*- coding: utf-8 -*- """The detailed Vci/Vcw shear method: `capacity.detailed_shear_at`. Parity gap #6 -- our simplified V_c = 2*sqrt(f'c)*bw*d does not reproduce the reference tool's (Eriksson Wall) PCI detailed min(Vci, Vcw), which left the handling shear utilisation low by -35% (MWV1.0) to -54% (WPB3.0) on the parity sweep. The formulas are `docs/eriksson-wall-algorithms.md` sec.3, transcribed from that tool's QC document (QC p.37 prestressed, QC p.76 non-prestressed). Every number below is HAND COMPUTED, the arithmetic written into the comment beside it, on a rectangular prestressed section chosen so the closed forms are exact -- the same discipline `test_capacity.py` uses: b = 48 in. t = 8 in. L = 32 ft f'c = 4000 psi w_c = 150 pcf 4 strands, 0.153 in.^2 each, all at z = 2.0 in. above the form face jacked at 30,000 lb each, 10% loss, l_t stated at 30 in. f'c = 4000 puts sqrt(f'c) = 63.24555..., w_c = 150 puts lambda = 1.0, and a mid-span station (x = 16 ft, no blockout) develops every strand fully, so: Pi = 4 * 30000 * (1 - 0.10) = 108,000 lb A = 48 * 8 = 384 in^2 fpc = Pi/A = 281.25 psi (centroid, Vcw) dv = 0.8 * 8 = 6.40 in (ACI 22.5.8.3.1) b*d = 48 * 6.40 = 307.2 in^2 I = 48 * 8^3 / 12 = 2048 in^4 St = I / (t/2) = 2048 / 4 = 512 in^3 e = z_strand - t/2 = 2.0 - 4.0 = -2.0 in (low strand) fpe = Pi/A + Pi*|e|*(t/2)/I (compression at = 281.25 + 108000*2.0*4/2048 = 703.125 psi the sagging tension fibre, for Mcre) Mcre = St*(6*sqrt(f'c) + fpe) (PCI 5-26) = 512*(6*63.24555 + 703.125) = 554,290.4 lb-in = 46,190.86 lb-ft Vcw = (3.5*sqrt(f'c) + 0.3*fpc)*b*d (PCI 5-28) = (3.5*63.24555 + 0.3*281.25)*307.2 = (221.3594 + 84.375)*307.2 = 93,921.62 lb The pins are WRITTEN-DOWN goldens, not recomputations. A test that rebuilds both sides agrees with any refactor, including a wrong one -- PROJECT.md 3.7. """ import math from panellift import capacity, pci from panellift.model import Blockout, Panel, Prestress, Stage, Strand SQRT_4000 = math.sqrt(4000.0) # 63.245553... MID_X = 16.0 # mid-span: every strand fully developed def _pre_panel(blockouts=None): strands = [Strand(y=0.5 + i, z_in=2.0, area_in2=0.153, initial_pull_lb=30000.0) for i in range(4)] return Panel( mark='HS-PRE', length_ft=32.0, width_ft=48.0 / 12.0, thickness_in=8.0, unit_weight_pcf=150.0, fci_psi=4000.0, fc_psi=4000.0, blockouts=list(blockouts or []), prestress=Prestress(strands=strands, loss_pct=10.0, diameter_in=0.5, transfer_length_in=30.0)) def _stage(): return Stage(name='strip', multiplier=1.4, fc_psi=4000.0) # --------------------------------------------------------- the shared terms def test_section_terms_match_the_hand_calc(): d = capacity.detailed_shear_at(_pre_panel(), _stage(), MID_X, 10000.0, 8000.0) assert d.prestressed is True assert abs(d.dv_in - 6.40) < 1e-9 # 0.8 * 8 assert abs(d.bd_in2 - 307.2) < 1e-9 # 48 * 6.40 assert abs(d.fpc_psi - 281.25) < 1e-6 # Pi/A = 108000/384 assert abs(d.fpe_psi - 703.125) < 1e-6 # 281.25 + 108000*2*4/2048 assert abs(d.mcre_lbft - 46190.8616) < 1e-2 # St*(6 sqrt f'c + fpe)/12 assert abs(d.vcw_lb - 93921.6188) < 1e-2 # (3.5 sqrt f'c + 0.3 fpc)bd # --------------------------------------------------------- Vci.1 governs def test_vci_flexure_shear_term_governs_above_the_floor(): """M = 10,000 lb-ft, V = 8,000 lb -- Vci.1 clears the 1.7 floor. Vci.1 = 0.6*sqrt(f'c)*b*d + Vu*Mcre/Mmax (PCI 5-25) = 0.6*63.24555*307.2 + 8000*46190.86/10000 = 11,657.36 + 36,952.69 = 48,610.11 lb Vci.2 = 1.7*sqrt(f'c)*b*d = 1.7*63.24555*307.2 = 33,029.36 lb [floor] Vci = max(48,610.11, 33,029.36) = 48,610.11 lb Vcw = 93,921.62 lb ; Vc = min = 48,610.11 lb phiVn = 0.75 * 48,610.11 = 36,457.58 lb util = 8000 / 36,457.58 = 0.219433 """ d = capacity.detailed_shear_at(_pre_panel(), _stage(), MID_X, 10000.0, 8000.0) assert d.governed_by == 'Vci' assert abs(d.vci_lb - 48610.1097) < 1e-2 assert abs(d.vc_lb - 48610.1097) < 1e-2 assert abs(d.phi_vn_lb - 36457.5822) < 1e-2 assert abs(d.v_over_phi_vn - 0.2194331) < 1e-6 # --------------------------------------------------------- the 1.7 floor def test_vci_floor_governs_when_the_flexure_shear_term_is_small(): """M = 20,000 lb-ft, V = 8,000 lb -- Vci.1 falls under the floor. Vci.1 = 11,657.36 + 8000*46190.86/20000 = 11,657.36 + 18,476.34 = 30,133.70 lb Vci.2 = 33,029.36 lb [floor wins] Vci = max(30,133.70, 33,029.36) = 33,029.36 lb Vc = min(33,029.36, 93,921.62) = 33,029.36 lb phiVn = 0.75 * 33,029.36 = 24,772.02 lb util = 8000 / 24,772.02 = 0.322945 This is the 1.7*sqrt(f'c)*bw*d FLOOR (PCI 5-27), a max and not a cap -- the correction #4 in docs/eriksson-wall-algorithms.md. It is exactly this floor that lands MWV1.0 on Eriksson's 0.19 in the parity harness (the reduced end section where Vci.1 sits below it); dropping the floor overshoots. """ d = capacity.detailed_shear_at(_pre_panel(), _stage(), MID_X, 20000.0, 8000.0) assert d.governed_by == 'Vci' assert abs(d.vci_lb - 33029.3577) < 1e-2 # the floor, 1.7 sqrt f'c bd assert abs(d.vc_lb - 33029.3577) < 1e-2 assert abs(d.v_over_phi_vn - 0.3229450) < 1e-6 # ---------------------------------------------- zero moment -> Vcw governs def test_zero_moment_station_sends_vci_to_infinity_and_vcw_governs(): """M -> 0 : Vci.1 -> +inf, so Vci = +inf and Vc = Vcw (docs sec.3.1). This is the reference tool's printed "inf" in the Vci column at a zero-moment station (QC p.15/p.18). Vcw is independent of the moment: Vc = Vcw = 93,921.62 lb phiVn = 0.75 * 93,921.62 = 70,441.21 lb util = 8000 / 70,441.21 = 0.113570 """ d = capacity.detailed_shear_at(_pre_panel(), _stage(), MID_X, 0.0, 8000.0) assert d.vci_lb == float('inf') assert d.governed_by == 'Vcw' assert abs(d.vc_lb - 93921.6188) < 1e-2 assert abs(d.phi_vn_lb - 70441.2141) < 1e-2 assert abs(d.v_over_phi_vn - 0.1135699) < 1e-6 def test_a_moment_just_under_one_lbft_is_treated_as_zero(): """The Mmax->0 guard fires at the same 1 lb-ft floor the ratios use.""" d = capacity.detailed_shear_at(_pre_panel(), _stage(), MID_X, 0.5, 8000.0) assert d.vci_lb == float('inf') assert d.governed_by == 'Vcw' # --------------------------------------------------- non-prestressed path def test_non_prestressed_uses_the_aci_max_not_a_cap(): """ACI 318-14 22.5.6.1, phi*Vn = 0.75*max(Vc, Vc.min) -- QC p.76. Same rectangular section, no strand, no axial (Nu = 0): Vc = 2*lambda*(1 + 0)*sqrt(f'c)*bw*d = 2*63.24555*307.2 = 38,858.07 lb Vc.min = (4/3)*lambda*sqrt(f'c)*Ag = 1.33333*63.24555*384 = 32,381.72 lb Vc = max(38,858.07, 32,381.72) = 38,858.07 lb (Vc governs) phiVn = 0.75 * 38,858.07 = 29,143.55 lb It is a MAX, not a cap (docs correction #5, a genuine surprise). With dv = 0.8h the flexure term always clears the (4/3)Ag floor for a solid rectangle at Nu = 0, so Vc governs here; the branch is still a `max`, which is asserted directly below so a future edit to a `min`/cap is caught. """ panel = Panel(mark='HS-NP', length_ft=32.0, width_ft=48.0 / 12.0, thickness_in=8.0, unit_weight_pcf=150.0, fci_psi=4000.0, fc_psi=4000.0) d = capacity.detailed_shear_at(panel, _stage(), MID_X, 5000.0, 8000.0) assert d.prestressed is False assert d.governed_by == 'Vc' vc_flex = 2.0 * SQRT_4000 * 0.8 * 384.0 # 38,858.07 vc_min = (4.0 / 3.0) * SQRT_4000 * 384.0 # 32,381.72 assert abs(d.vc_lb - vc_flex) < 1e-2 assert abs(d.vc_lb - max(vc_flex, vc_min)) < 1e-9 # a MAX, never a cap assert d.vc_lb > vc_min # the floor did not win assert abs(d.phi_vn_lb - 29143.5509) < 1e-2 # -------------------------------------------------- additive: nothing moved def test_simplified_shear_capacity_is_untouched(): """`capacity_at` and its simplified V_c are what Panel Lift Check reads; the detailed path must not change them. V_c(simplified) = 2*lambda*sqrt(f'c)*bw*d, d = max(dp, 0.8h) Here dp(sagging) = t - z = 6.0 in, floored at 0.8h = 6.4 in, so d = 6.4: V_c = 2*63.24555*48*6.4 = 38,858.07 lb phiVn = 0.75 * 38,858.07 = 29,143.55 lb """ cap = capacity.capacity_at(_pre_panel(), _stage(), MID_X, 10000.0, 8000.0) assert abs(cap.phi_vn_lb - 29143.5509) < 1e-2 assert abs(cap.d_shear_in - 6.4) < 1e-9 # 0.8h floor, dp = 6.0 < 6.4 def test_detailed_and_simplified_disagree_by_design(): """The whole point of the gap: the two methods give different V_c, and the detailed one is what matches Eriksson. At this station Vci (48,610) beats the simplified V_c (38,858), so the detailed util is LOWER here -- but at a reduced section where the 1.7 floor bites (a blockout), the detailed util runs HIGHER, which is what closes MWV1.0's -35%.""" stage = _stage() det = capacity.detailed_shear_at(_pre_panel(), stage, MID_X, 10000.0, 8000.0) old = capacity.capacity_at(_pre_panel(), stage, MID_X, 10000.0, 8000.0) assert det.phi_vn_lb != old.phi_vn_lb # ------------------------------------------ prestress reduced at a blockout def test_undeveloped_strand_lowers_fpc_and_fpe(): """A blockout that cuts strands drops Pi at the station, so fpc and fpe fall with it -- the detailed shear reads the SAME developed prestress the stress and flexure paths do (prestress.state_at), never the full jacking force. A full-depth blockout over the whole width from x=10 to x=14 cuts every strand; just past it at x=15 the strands are still redeveloping.""" blocked = _pre_panel(blockouts=[Blockout( name='O', x0=10.0, y0=0.0, length=4.0, width=48.0 / 12.0)]) near = capacity.detailed_shear_at(blocked, _stage(), 15.0, 10000.0, 8000.0) far = capacity.detailed_shear_at(_pre_panel(), _stage(), MID_X, 10000.0, 8000.0) assert near.fpc_psi < far.fpc_psi # less strand developed here assert near.vcw_lb < far.vcw_lb # so Vcw is smaller too