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,203 @@
|
||||
# -*- 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)
|
||||
Reference in New Issue
Block a user