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
+153
View File
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
"""The PCI Design Aid 15.2.3 strand power curve and the strain-compatibility
flexure it feeds -- docs/eriksson-wall-algorithms.md sec.4.2.
ADDITIVE. `pci.fps_approximate_ksi` (the ACI approximate f_ps that the shared
`capacity_at`/`phi_mn_lbft` and Panel Lift Check keep) is untouched; these pin
the more exact law the reference tool (Eriksson Wall) uses and that the additive
`handling_flexure_at` (Panel Analysis) now prefers.
"""
import pytest
from panellift import capacity, pci
from panellift.model import Panel, Prestress, Stage, Strand
# ------------------------------------------------------- the pure power curve
def test_fps_power_curve_elastic_branch():
"""eps_ps <= 0.0085 is elastic: f_ps = Ep*eps_ps.
0.005 * 28500 = 142.5 ksi. At the limit itself, 0.0085 * 28500 = 242.25.
"""
assert pci.fps_power_curve_ksi(0.005, 270.0) == pytest.approx(142.5)
assert pci.fps_power_curve_ksi(0.0085, 270.0) == pytest.approx(242.25)
def test_fps_power_curve_power_branch():
"""eps_ps > 0.0085: f_ps = f_pu - 0.04/(eps_ps - 0.007).
At 0.010: 270 - 0.04/(0.010 - 0.007) = 270 - 13.3333 = 256.6667 ksi.
At 0.020: 270 - 0.04/0.013 = 270 - 3.0769 = 266.9231 ksi.
"""
assert pci.fps_power_curve_ksi(0.010, 270.0) == pytest.approx(256.6667,
abs=1e-3)
assert pci.fps_power_curve_ksi(0.020, 270.0) == pytest.approx(266.9231,
abs=1e-3)
def test_fps_power_curve_is_nearly_continuous_at_the_knee():
"""The two branches meet within ~1 ksi at eps_ps = 0.0085 -- one smooth
law, not a step. Elastic 242.25; power 270 - 0.04/0.0015 = 243.33."""
elastic = pci.fps_power_curve_ksi(0.0085, 270.0)
power = 270.0 - 0.04 / (0.0085 - 0.007)
assert abs(power - elastic) < 1.1
def test_fps_power_curve_caps_at_fpu_and_floors_at_zero():
# The power branch approaches f_pu from below but never reaches it: at
# eps = 1.0, 270 - 0.04/0.993 = 269.96 ksi, under f_pu.
assert pci.fps_power_curve_ksi(1.0, 270.0) == pytest.approx(269.96, abs=0.01)
assert pci.fps_power_curve_ksi(1.0, 270.0) < 270.0
# The cap DOES bind if the elastic branch is pushed past f_pu (an out-of-
# range Ep): 0.005 * 100000 = 500 ksi -> capped to f_pu = 270.
assert pci.fps_power_curve_ksi(0.005, 270.0, ep_ksi=100000.0) \
== pytest.approx(270.0)
# Non-positive strain is no stress, not a negative one.
assert pci.fps_power_curve_ksi(0.0, 270.0) == 0.0
assert pci.fps_power_curve_ksi(-0.001, 270.0) == 0.0
# --------------------------------------- strain-compat Mn on a solid panel
def _solid_prestressed_panel():
"""A plain solid panel, one bottom strand row, no openings -- so the
power-curve solve has a single fully-developed tension layer and can be
reproduced by an independent bisection in the test."""
strands = [Strand(y=2.0, z_in=1.5, area_in2=0.153, initial_pull_lb=28900.0),
Strand(y=6.0, z_in=1.5, area_in2=0.153, initial_pull_lb=28900.0)]
pre = Prestress(strands=strands, loss_pct=0.0, diameter_in=0.5,
fpu_ksi=270.0)
return Panel(mark='S', length_ft=24.0, width_ft=8.0, thickness_in=8.0,
unit_weight_pcf=150.0, fci_psi=3500.0, fc_psi=6000.0,
prestress=pre)
def test_power_curve_mn_matches_an_independent_strain_compat_solve():
"""`capacity_at` stores a power-curve M_n; reproduce it from the same law.
Midspan, fully developed: every strand sees the full power-curve f_ps at
its own strain, and equilibrium 0.85 f'ci beta1 c b = sum(A f_ps) fixes c.
The test re-solves that balance independently and checks the stored M_n.
"""
panel = _solid_prestressed_panel()
stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0)
x = 12.0
cap = capacity.capacity_at(panel, stage, x, moment_lbft=1.0, shear_lb=0.0)
assert cap.mn_powercurve_lbft is not None
# Independent solve, same inputs: two identical strands at d = 8 - 1.5 =
# 6.5 in, f_se = 28900/0.153/1000 = 188.9 ksi, eps_se = f_se/28500.
import math
fc = 3500.0
b = panel.thickness_in and (panel.width_ft * 12.0) # solid, full width
beta1 = pci.beta1(fc)
ep = pci.STRAND_ELASTIC_MODULUS_KSI
d = 8.0 - 1.5
area = 0.153 * 2
fse = 28900.0 / 0.153 / 1000.0
eps_se = fse / ep
def tension(c):
eps = eps_se + 0.003 * (d - c) / c
return area * pci.fps_power_curve_ksi(eps, 270.0, ep) * 1000.0
lo, hi = 1e-4, d
for _ in range(80):
mid = 0.5 * (lo + hi)
if 0.85 * fc * beta1 * mid * b - tension(mid) > 0.0:
hi = mid
else:
lo = mid
c = 0.5 * (lo + hi)
a = beta1 * c
mn = tension(c) * (d - a / 2.0) / 12.0
assert cap.mn_powercurve_lbft == pytest.approx(mn, rel=1e-3)
def test_handling_flexure_prefers_the_power_curve_mn():
"""handling_flexure_at reads the power-curve M_n, not the ACI-approximate
one, when the point carries it -- and applies the strain-based phi to it."""
panel = _solid_prestressed_panel()
stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0)
x = 12.0
cap = capacity.capacity_at(panel, stage, x, moment_lbft=4000.0, shear_lb=0.0)
phi, phi_mn, ratio = capacity.handling_flexure_at(cap, 4000.0)
# Fully developed, low rho -> tension-controlled -> phi 0.90, and phi*M_n
# is 0.90 times the stored power-curve M_n, NOT the approximate phi_mn_lbft.
assert phi == pytest.approx(0.90)
assert phi_mn == pytest.approx(0.90 * cap.mn_powercurve_lbft)
assert phi_mn != pytest.approx(cap.phi_mn_lbft) # differs from approximate
assert ratio == pytest.approx(4000.0 / phi_mn)
def test_power_curve_mn_is_none_without_developed_strand():
"""A free end has no developed area, so no power-curve M_n is stored."""
panel = _solid_prestressed_panel()
stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0)
cap = capacity.capacity_at(panel, stage, x=0.0, moment_lbft=1.0,
shear_lb=0.0)
assert cap.mn_powercurve_lbft is None
def test_power_curve_free_end_flexure_is_not_a_full_phi_mn():
"""The development cap keeps a station near a free end from claiming the
full midspan phi*M_n: at x just inside the transfer length the strand has
only begun to develop, so phi*M_n there is well below the midspan value."""
panel = _solid_prestressed_panel()
stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0)
mid = capacity.capacity_at(panel, stage, 12.0, 1.0, 0.0)
near = capacity.capacity_at(panel, stage, 0.5, 1.0, 0.0)
assert mid.mn_powercurve_lbft is not None
if near.mn_powercurve_lbft is not None:
assert near.mn_powercurve_lbft < mid.mn_powercurve_lbft