WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user