WillTools 2026-09-09 -- clean snapshot of the development repo for review
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:
2026-09-09 08:35:15 -05:00
commit 25592fa6c2
406 changed files with 223435 additions and 0 deletions
+233
View File
@@ -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