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,202 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The In-Place SERVICE slice: ASCE 2.4 combos, extreme-fibre stress vs the
|
||||
cracking / compression limits, deflection, and the P-Delta magnifier.
|
||||
|
||||
Neutral fixtures only -- the parity against the reference tool's real pieces
|
||||
lives in the scratchpad harness (parity_service.py). These pin the identities
|
||||
any correct service answer must satisfy:
|
||||
|
||||
* the service set is ASCE 7-16 2.4.1(1)-(5), T at 0.75, no 0.6D+0.6W;
|
||||
* SC5 = 1.0D + 0.6W assembles exactly 1.0*P_D and 0.6*M_W;
|
||||
* extreme-fibre tension = M/S - P/A (mild), util = tension / 7.5 sqrt(f'c),
|
||||
and a >1 util is REPORTED (the tool flags it, docs 6.1);
|
||||
* the P-Delta magnifier is 1/(1-gamma), gamma = Pu L^2 / (8 EI_eff), and an
|
||||
instability is NOT JUDGED, named -- never a fabricated number;
|
||||
* a deflection with no L/n limit is computed but its LIMIT check is NOT
|
||||
JUDGED, named.
|
||||
"""
|
||||
import math
|
||||
|
||||
from panellift import capacity, inplace, section
|
||||
from panellift.model import Panel
|
||||
|
||||
|
||||
def plain_wall(length_ft=30.0, width_ft=10.0, thickness_in=12.0,
|
||||
fc_psi=6000.0):
|
||||
return Panel(mark='SVC-WALL', length_ft=length_ft, width_ft=width_ft,
|
||||
thickness_in=thickness_in, unit_weight_pcf=150.0,
|
||||
fc_psi=fc_psi, fci_psi=3500.0, prestress=None)
|
||||
|
||||
|
||||
def _service_cases(x_lo, x_hi, p_lb, m_lbft, wind_factor=0.6):
|
||||
"""A cases dict where SC5 (1.0D + 0.6W) assembles to (p_lb, m_lbft): D
|
||||
carries the axial, W carries m/0.6 so 0.6*W = m."""
|
||||
xs = [x_lo, x_hi]
|
||||
return {
|
||||
'D': inplace.LoadField('D', 'Dead', xs, [p_lb, p_lb], [0.0, 0.0],
|
||||
[0.0, 0.0]),
|
||||
'W': inplace.LoadField('W', 'Wind', xs, [0.0, 0.0],
|
||||
[m_lbft / wind_factor, m_lbft / wind_factor],
|
||||
[0.0, 0.0]),
|
||||
}
|
||||
|
||||
|
||||
def _stage(panel):
|
||||
return inplace.in_place_stage(panel, inplace.InPlaceInputs(
|
||||
restraint_xs=[0.0, panel.length_ft]))
|
||||
|
||||
|
||||
def _sc5():
|
||||
return next(c for c in inplace.ASCE_7_16_SERVICE if c.name == 'SC5')
|
||||
|
||||
|
||||
# --------------------------------------------------------------- the combos
|
||||
|
||||
def test_service_set_is_the_asce_2_4_basic_combinations():
|
||||
names = [c.name for c in inplace.ASCE_7_16_SERVICE]
|
||||
assert names == ['SC1', 'SC2', 'SC3', 'SC4', 'SC5']
|
||||
for c in inplace.ASCE_7_16_SERVICE:
|
||||
assert c.kind == 'service'
|
||||
assert c.factors.get('T') == 0.75
|
||||
sc5 = _sc5()
|
||||
assert sc5.factors['D'] == 1.0 and sc5.factors['W'] == 0.6
|
||||
# combination (7) 0.6D+0.6W is deliberately NOT in the reference set.
|
||||
assert not any(c.factors.get('D') == 0.6 for c in inplace.ASCE_7_16_SERVICE)
|
||||
|
||||
|
||||
def test_service_combo_assembles_the_right_demand():
|
||||
"""SC5 = 1.0D + 0.6W puts 1.0*P_D and 0.6*M_W, nothing else."""
|
||||
panel = plain_wall()
|
||||
cases = _service_cases(0.0, panel.length_ft, p_lb=40000.0, m_lbft=100000.0)
|
||||
pu, mu, vu = inplace.combination_demand(cases, _sc5(), 12.0)
|
||||
assert abs(pu - 40000.0) < 1e-6
|
||||
assert abs(mu - 100000.0) < 1e-6
|
||||
assert abs(vu) < 1e-9
|
||||
|
||||
|
||||
# --------------------------------------------------------------- the stress
|
||||
|
||||
def test_service_tension_matches_hand_stress_and_cracking_util():
|
||||
"""WPA1.0's governing service tension row: a 12 in x 164 in solid mild wall
|
||||
at M = 260.99 k-ft, P = 36.9 k gives 776.9 psi vs 580.9 (7.5 sqrt 6000)."""
|
||||
panel = Panel(mark='SVC', length_ft=43.08, width_ft=164.0 / 12.0,
|
||||
thickness_in=12.0, unit_weight_pcf=150.0, fci_psi=3500.0,
|
||||
fc_psi=6000.0, prestress=None)
|
||||
cases = _service_cases(0.0, panel.length_ft, p_lb=36900.0, m_lbft=260990.0)
|
||||
st = inplace.service_station(panel, _stage(panel), cases, [_sc5()], 25.1)
|
||||
assert st.why is None
|
||||
assert abs(st.cracking_limit_psi - 580.9) < 0.5
|
||||
assert abs(st.tension_psi - 776.9) < 1.0
|
||||
assert abs(st.compression_psi - 814.4) < 1.0
|
||||
# util = tension / cracking; over 1.0 here -- the tool FLAGS it, we report it
|
||||
assert st.tension_util > 1.0
|
||||
assert abs(st.tension_util - 776.9 / 580.9) < 0.01
|
||||
|
||||
|
||||
def test_prestress_precompression_term_is_the_reused_primitive():
|
||||
"""WPA5.0's developed-strand compression row: Pi = 123.3 k, M = 17.22 k-ft,
|
||||
P = 19.7 k -> 815.6 psi through capacity.extreme_fibre_stresses + (-P/A)."""
|
||||
A, i_in4, c_in = 313.5, 3161.0, 5.5
|
||||
f_form, f_far = capacity.extreme_fibre_stresses(
|
||||
123300.0, A, 0.0, c_in, i_in4, 17220.0)
|
||||
f_form -= 19700.0 / A
|
||||
f_far -= 19700.0 / A
|
||||
comp = max(0.0, -f_form, -f_far)
|
||||
assert abs(comp - 815.6) < 1.0
|
||||
|
||||
|
||||
def test_no_section_dashes_with_a_reason_not_a_zero():
|
||||
"""A station where NetSection cannot be built is None+why, never a fake 0."""
|
||||
panel = plain_wall(thickness_in=0.0)
|
||||
cases = _service_cases(0.0, panel.length_ft, 1000.0, 1000.0)
|
||||
st = inplace.service_station(panel, _stage(panel), cases, [_sc5()], 5.0)
|
||||
assert st.why is not None
|
||||
assert st.tension_util is None and st.compression_util is None
|
||||
|
||||
|
||||
def test_cracking_class_t_raises_the_limit_to_12_sqrt_fc():
|
||||
stage = _stage(plain_wall(fc_psi=6000.0))
|
||||
assert abs(inplace.cracking_limit_psi(stage, class_t=False)
|
||||
- 7.5 * math.sqrt(6000.0)) < 0.1
|
||||
assert abs(inplace.cracking_limit_psi(stage, class_t=True)
|
||||
- 12.0 * math.sqrt(6000.0)) < 0.1
|
||||
|
||||
|
||||
# -------------------------------------------------------------- the P-Delta
|
||||
|
||||
def test_pdelta_magnifier_on_a_hand_case():
|
||||
"""delta = 1/(1-gamma), gamma = Pu L^2 / (8 EI_eff), EI_eff = 0.875 Ec Ig."""
|
||||
panel = plain_wall(length_ft=30.0, width_ft=10.0, thickness_in=12.0,
|
||||
fc_psi=6000.0)
|
||||
pu, span_ft = 50000.0, 30.0
|
||||
m = inplace.pdelta_magnifier(panel, _stage(panel), span_ft, pu, 15.0,
|
||||
beta_d=0.0)
|
||||
ec = inplace.ec_aci_psi(6000.0, 150.0)
|
||||
ig = section.NetSection(panel, 15.0).iz_in4
|
||||
ei = 0.875 * ec * ig
|
||||
gamma = pu * (span_ft * 12.0) ** 2 / (8.0 * ei)
|
||||
assert abs(m.gamma - gamma) < 1e-9
|
||||
assert abs(m.magnifier - 1.0 / (1.0 - gamma)) < 1e-9
|
||||
assert m.magnifier > 1.0
|
||||
|
||||
|
||||
def test_pdelta_reports_instability_not_a_fake_number():
|
||||
panel = plain_wall(length_ft=60.0, width_ft=4.0, thickness_in=6.0,
|
||||
fc_psi=5000.0)
|
||||
m = inplace.pdelta_magnifier(panel, _stage(panel), 60.0, 5.0e6, 30.0)
|
||||
assert m.magnifier is None
|
||||
assert m.why and 'stability' in m.why
|
||||
|
||||
|
||||
def test_tension_axial_does_not_magnify():
|
||||
panel = plain_wall(fc_psi=6000.0)
|
||||
m = inplace.pdelta_magnifier(panel, _stage(panel), panel.length_ft,
|
||||
-30000.0, 15.0)
|
||||
assert m.magnifier == 1.0 and m.gamma == 0.0
|
||||
|
||||
|
||||
# ------------------------------------------------------------ deflection/run
|
||||
|
||||
def test_run_populates_service_with_deflection_limit():
|
||||
panel = plain_wall(length_ft=30.0, width_ft=10.0, fc_psi=6000.0)
|
||||
inputs = inplace.InPlaceInputs(
|
||||
restraint_xs=[0.0, 30.0],
|
||||
wind_zones=[inplace.LateralPressureZone(0.0, 30.0, 50.0, 'W')],
|
||||
deflection_limit_ratio=150.0)
|
||||
res = inplace.run(panel, inputs)
|
||||
assert res.service is not None and res.service.stations
|
||||
d = res.service.deflection
|
||||
assert d.combo == 'SC5'
|
||||
assert d.delta_wind_in > 0.0
|
||||
assert d.delta_total_in is not None and d.delta_total_in >= d.delta_i_in
|
||||
assert abs(d.limit_in - 30.0 * 12.0 / 150.0) < 1e-6
|
||||
assert d.util is not None and d.util > 0.0
|
||||
|
||||
|
||||
def test_deflection_limit_not_supplied_is_not_judged_not_faked():
|
||||
panel = plain_wall(length_ft=30.0, width_ft=10.0, fc_psi=6000.0)
|
||||
inputs = inplace.InPlaceInputs(
|
||||
restraint_xs=[0.0, 30.0],
|
||||
wind_zones=[inplace.LateralPressureZone(0.0, 30.0, 50.0, 'W')])
|
||||
res = inplace.run(panel, inputs)
|
||||
d = res.service.deflection
|
||||
assert d.delta_total_in is not None # deflection still computed
|
||||
assert d.util is None # but not judged against a limit
|
||||
assert d.why and 'deflection limit' in d.why
|
||||
|
||||
|
||||
def test_deflection_recovers_the_simple_beam_closed_form():
|
||||
"""Uniform service wind, pinned-pinned: our double integration of the
|
||||
moment field gives 5 w L^4 / (384 EI_eff) at midspan."""
|
||||
panel = plain_wall(length_ft=20.0, width_ft=8.0, thickness_in=10.0,
|
||||
fc_psi=6000.0)
|
||||
press = 40.0
|
||||
inputs = inplace.InPlaceInputs(
|
||||
restraint_xs=[0.0, 20.0],
|
||||
wind_zones=[inplace.LateralPressureZone(0.0, 20.0, press, 'W')],
|
||||
deflection_limit_ratio=240.0)
|
||||
d = inplace.run(panel, inputs).service.deflection
|
||||
w_lb_per_in = (0.6 * press * panel.width_ft) / 12.0
|
||||
l_in = 20.0 * 12.0
|
||||
closed = 5.0 * w_lb_per_in * l_in ** 4 / (384.0 * d.ei_eff_lbin2)
|
||||
assert abs(d.delta_wind_in - closed) / closed < 0.01
|
||||
Reference in New Issue
Block a user