Files
willtools-demo/panellift/tests/test_handling_stress.py
T
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
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.
2026-09-09 08:35:15 -05:00

204 lines
9.0 KiB
Python

# -*- coding: utf-8 -*-
"""The Tier-1 handling additions to `capacity`: the extreme-fibre STRESS check
and the phi development RAMP.
Both are ADDITIVE -- `capacity_at` and every type it returns are untouched, and
their tests (test_capacity.py, test_station_table.py, ...) stand as they were.
These pin the new functions on numbers a reviewer can reproduce on paper:
* `extreme_fibre_stresses` is the task's Pi/A -/+ (Mg + Pi*e)*c/I, hand
checked to the psi.
* `handling_stress_at` builds those from a real panel/stage and divides by
the STAGE's own allowables (tension_limit_sqrt_fc*sqrt(f'c),
compression_limit_ratio*f'c) -- the settings that were read by nothing
until this landed. A stage set to 5 vs 7.5 sqrt(f'c) moves the tension
utilisation by exactly 1.5.
* `phi_flexure_ramped` / `ramped_phi_mn` are the 0.75->0.90 ramp across the
transfer/development length.
"""
import math
import pytest
from panellift import capacity, prestress
from panellift.model import Panel, Prestress, Stage, Strand
# --------------------------------------------------- the pure stress formula
def test_extreme_fibre_stresses_hand_check():
"""Pi=100000, A=1000, e=0, c=4, I=2000, Mg=10000 -> form +140, far -340.
S = I/c = 500 in3; Pi/A = 100 psi (compression, negative); Mg/S = 240 psi.
Form face (z=0): -100 + 240 = +140 (tension). Far face: -100 - 240 = -340.
"""
f_form, f_far = capacity.extreme_fibre_stresses(
pi_lb=100000.0, area_in2=1000.0, e_in=0.0, c_in=4.0,
i_in4=2000.0, moment_lbft=10000.0)
assert f_form == pytest.approx(140.0)
assert f_far == pytest.approx(-340.0)
def test_extreme_fibre_stresses_eccentric_prestress():
"""A low strand (e<0) hogs the piece: it ADDS compression on the form face.
e = -2 in, Pi=120000, so Pi*e = -240000 lb-in. With A=1000, c=4, I=2000
(S=500) and Mg=0: form = -120 + (-240000/500) = -120 - 480 = -600;
far = -120 + 480 = +360 (tension on the far face, which is the hogging a
low strand causes).
"""
f_form, f_far = capacity.extreme_fibre_stresses(
pi_lb=120000.0, area_in2=1000.0, e_in=-2.0, c_in=4.0,
i_in4=2000.0, moment_lbft=0.0)
assert f_form == pytest.approx(-600.0)
assert f_far == pytest.approx(360.0)
def test_extreme_fibre_stresses_degenerate_returns_none():
assert capacity.extreme_fibre_stresses(1.0, 0.0, 0.0, 4.0, 2000.0, 1.0) \
== (None, None)
assert capacity.extreme_fibre_stresses(1.0, 1000.0, 0.0, 4.0, 0.0, 1.0) \
== (None, None)
# ------------------------------------------------ handling_stress_at on panel
def _solid_panel():
"""A plain solid panel with two symmetric strands -- e_z = 0, no openings.
t=8 in, so mid-depth is 4 in; strands at z=1.5 and z=6.5 (= 8-1.5) with
equal force put the group centroid at mid-depth (e_z=0) and, being full
width, leave the in-plane term e_y=0 too. So the fibre stress is exactly
the 1-D Pi/A -/+ Mg*c/I with no prestress bending -- hand-reproducible.
"""
strands = [Strand(y=2.0, z_in=1.5, area_in2=0.153, initial_pull_lb=30000.0),
Strand(y=2.0, z_in=6.5, area_in2=0.153, initial_pull_lb=30000.0)]
pre = Prestress(strands=strands, loss_pct=0.0, diameter_in=0.5,
fpu_ksi=270.0)
return Panel(mark='S', length_ft=20.0, width_ft=4.0, thickness_in=8.0,
unit_weight_pcf=150.0, fci_psi=3500.0, fc_psi=6000.0,
prestress=pre)
def test_handling_stress_at_matches_the_pure_formula():
panel = _solid_panel()
stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0,
tension_limit_sqrt_fc=7.5, compression_limit_ratio=0.6)
x = 8.0
m = 12000.0
sp = capacity.handling_stress_at(panel, stage, x, m)
# Rebuild the ingredients the way the function does, independently.
from panellift import section
net = section.NetSection(panel, x)
state = prestress.state_at(panel, x)
pi = state.force_lb
assert pi > 0
assert sp.e_in == pytest.approx(0.0, abs=1e-9) # symmetric -> e_z = 0
f_form, f_far = capacity.extreme_fibre_stresses(
pi, net.area_in2, 0.0, panel.thickness_in / 2.0, net.iz_in4, m)
assert sp.f_form_psi == pytest.approx(f_form)
assert sp.f_far_psi == pytest.approx(f_far)
assert sp.in_plane_psi == pytest.approx(0.0, abs=1e-6)
def test_handling_stress_utils_use_the_stages_allowables():
"""The utilisation divides by the STAGE's own allowables -- the settings
that nothing read before. tension over tension_limit_sqrt_fc*sqrt(f'c),
compression over compression_limit_ratio*f'c."""
panel = _solid_panel()
stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0,
tension_limit_sqrt_fc=7.5, compression_limit_ratio=0.6)
sp = capacity.handling_stress_at(panel, stage, 10.0, 15000.0)
allow_t = 7.5 * math.sqrt(3500.0)
allow_c = 0.6 * 3500.0
assert sp.tension_util == pytest.approx(sp.tension_psi / allow_t)
assert sp.compression_util == pytest.approx(sp.compression_psi / allow_c)
def test_handling_stress_limit_coefficient_moves_the_tension_util_by_1p5():
"""5 vs 7.5 sqrt(f'c) is the whole 1.5x that the two schools argue about,
and it lands on the tension utilisation and nowhere else."""
panel = _solid_panel()
common = dict(multiplier=1.4, fc_psi=3500.0, compression_limit_ratio=0.6)
five = Stage(name='a', tension_limit_sqrt_fc=5.0, **common)
seven = Stage(name='b', tension_limit_sqrt_fc=7.5, **common)
a = capacity.handling_stress_at(panel, five, 10.0, 15000.0)
b = capacity.handling_stress_at(panel, seven, 10.0, 15000.0)
assert a.tension_psi == pytest.approx(b.tension_psi) # same stress
# 5 is the stricter school -> a higher utilisation, by exactly 1.5.
assert a.tension_util == pytest.approx(b.tension_util * 1.5)
def test_handling_stress_no_allowable_is_a_dash_not_a_zero():
"""A stage stating no strength has no allowable to divide by -- util is
None with a reason, never a fabricated 0, the same could-not-judge
discipline as every ratio in this module."""
panel = _solid_panel()
stage = Stage(name='s', multiplier=1.0, fc_psi=0.0)
sp = capacity.handling_stress_at(panel, stage, 10.0, 15000.0)
assert sp.tension_util is None and sp.compression_util is None
assert sp.why
# --------------------------------- phi from net tensile strain (Table 21.2.2)
def test_phi_flexure_from_strain_boundaries_and_slope():
"""ACI 318-14 Table 21.2.2, prestressed floor 0.65, eps_ty 0.002."""
assert capacity.phi_flexure_from_strain(0.001) == pytest.approx(0.65)
assert capacity.phi_flexure_from_strain(0.002) == pytest.approx(0.65)
# midway: 0.65 + 0.25*(0.0035-0.002)/(0.005-0.002) = 0.65 + 0.125 = 0.775
assert capacity.phi_flexure_from_strain(0.0035) == pytest.approx(0.775)
assert capacity.phi_flexure_from_strain(0.005) == pytest.approx(0.90)
assert capacity.phi_flexure_from_strain(0.010) == pytest.approx(0.90)
def test_phi_flexure_from_strain_none_is_the_flat_value():
"""No strain to judge on is not a reason to drop to the floor."""
assert capacity.phi_flexure_from_strain(None) == pytest.approx(0.90)
def test_handling_flexure_at_applies_the_strain_phi_to_the_same_mn():
"""M_n does not change; only phi moves, from the section's strain."""
pt = capacity.CapacityPoint(x=5.0)
pt.phi = 0.90
pt.phi_mn_lbft = 9000.0 # M_n = 10000 lb-ft
pt.strain_t = 0.0035 # transition -> phi 0.775
phi, phi_mn, ratio = capacity.handling_flexure_at(pt, moment_lbft=3100.0)
assert phi == pytest.approx(0.775)
assert phi_mn == pytest.approx(10000.0 * 0.775) # = 7750
assert ratio == pytest.approx(3100.0 / 7750.0)
def test_handling_flexure_at_tension_controlled_is_flat_0p90():
pt = capacity.CapacityPoint(x=5.0)
pt.phi = 0.90
pt.phi_mn_lbft = 9000.0
pt.strain_t = 0.020 # well tension-controlled
phi, phi_mn, _ = capacity.handling_flexure_at(pt, moment_lbft=3000.0)
assert phi == pytest.approx(0.90)
assert phi_mn == pytest.approx(9000.0) # unchanged from flat
def test_handling_flexure_at_rebuilds_mn_in_the_transition_region():
"""Where capacity_at bailed (no phi*M_n) but left the equilibrium geometry,
M_n is rebuilt from C = 0.85 f'c a b at lever arm (d_p - a/2)."""
pt = capacity.CapacityPoint(x=5.0) # phi_mn_lbft stays None
pt.a_in = 1.0
pt.b_in = 100.0
pt.dp_in = 6.0
pt.fc_psi = 3500.0
pt.strain_t = 0.003 # transition
phi, phi_mn, ratio = capacity.handling_flexure_at(pt, moment_lbft=5000.0)
tension = 0.85 * 3500.0 * 1.0 * 100.0
mn = tension * (6.0 - 0.5) / 12.0
assert phi == pytest.approx(0.65 + 0.25 * (0.003 - 0.002) / 0.003)
assert phi_mn == pytest.approx(phi * mn)
assert ratio == pytest.approx(5000.0 / (phi * mn))
def test_handling_flexure_at_no_strength_is_none_not_zero():
pt = capacity.CapacityPoint(x=5.0) # no phi_mn, no geometry, no eps
assert capacity.handling_flexure_at(pt, moment_lbft=1000.0) \
== (None, None, None)