WillTools: precast shop-ticket QC and panel handling tools
ci / test (push) Has been cancelled

Public demo snapshot of an in-house pyRevit extension for precast concrete
shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel
handling solver (PCI Chapter 8).

- No client, job, plant or product data: knowledge layers ship as
  structural templates only.
- Ticket QC's model-read escalation targets a LAN model server endpoint
  (configurable), not a cloud provider.
- Built from the private development repo via tools/make_dist.py.
This commit is contained in:
2026-09-03 16:33:47 -05:00
commit 52d94303bc
281 changed files with 155656 additions and 0 deletions
+202
View File
@@ -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