WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The insulated-sandwich SECTION path in `section.py` / `capacity.py`.
|
||||
|
||||
Every expected number here is arithmetic done by hand, written out beside it,
|
||||
and -- where a golden figure exists -- pinned to the value Eriksson Wall's own
|
||||
section table prints, NOT to the code's own expression. A pin that recomputes
|
||||
both sides agrees with any refactor, including a wrong one.
|
||||
|
||||
THE GOLDEN PIECE is WPB3.0 Run 1 (docs/eriksson-wall-algorithms.md sec.12 and
|
||||
the extracted `WPB3.0.md`): a horizontal insulated wall panel, layup
|
||||
|
||||
3 in. concrete + 4 in. insulation + 4 in. concrete = 11 in. envelope
|
||||
|
||||
at a clear width of 24 ft = 288 in (no opening at the checked station).
|
||||
Eriksson reports, for this section (p.11 of that packet):
|
||||
|
||||
Area 2016.00 in2 Cy 5.79 in Izz 29955.43 in4 Sb 5177.48 St 5744.88
|
||||
|
||||
which is the FULLY-COMPOSITE transformed section of the two concrete wythes
|
||||
about their common centroid, with the insulation EXCLUDED. That is what
|
||||
`section.NetSection` must reproduce for a sandwich, and it is what closes the
|
||||
"treated as a solid 11 in prism" defect: the solid idealisation would give
|
||||
Izz = 288*11^3/12 = 31944 and S = 288*11^2/6 = 5808, both larger than the real
|
||||
section (unconservative), and its shear b*d would be 288*0.8*11, a third too
|
||||
large -- see `test_shear_bd_drops_by_the_36_percent_the_parity_run_found`.
|
||||
|
||||
The task framed Eriksson's treatment as NON-composite; the golden figures do
|
||||
not bear that out (Area 2016 is BOTH wythes, and Izz 29955 is their composite
|
||||
inertia, not one wythe's), so this implements and pins the composite section
|
||||
Eriksson actually reports. The %composite knock-down Eriksson then applies
|
||||
(95% for stress/strength, 35% for in-place deflection) and the strand
|
||||
development layer are out of scope here; the more conservative non-composite /
|
||||
structural-wythe-alone bounds live in `sandwich.py`.
|
||||
"""
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from panellift import capacity, section
|
||||
from panellift.model import Panel, Stage
|
||||
|
||||
|
||||
ENVELOPE = 11.0
|
||||
WIDTH_FT = 24.0
|
||||
B_IN = WIDTH_FT * 12.0 # 288 in
|
||||
|
||||
|
||||
def wpb3_source():
|
||||
"""`source` as the exporter writes it for the WPB3.0 layup.
|
||||
|
||||
3 in wythe at the form face (z 0-3), 4 in insulation (z 3-7), 4 in wythe
|
||||
(z 7-11). The order does not change Area / Izz / the governing S; it only
|
||||
labels which face is Sb and which is St.
|
||||
"""
|
||||
return {
|
||||
'solid_parts': [
|
||||
{'counted': True, 'z0_in': 0.0, 'z1_in': 3.0,
|
||||
'material': 'WYTHE 3IN', 'density_pcf': 150.0, 'volume_ft3': 1.0},
|
||||
{'counted': False, 'z0_in': 3.0, 'z1_in': 7.0,
|
||||
'material': 'INSULATION', 'density_pcf': 2.0, 'volume_ft3': 1.0},
|
||||
{'counted': True, 'z0_in': 7.0, 'z1_in': 11.0,
|
||||
'material': 'WYTHE 4IN', 'density_pcf': 150.0, 'volume_ft3': 1.0},
|
||||
],
|
||||
'envelope_thickness_in': ENVELOPE,
|
||||
}
|
||||
|
||||
|
||||
def wpb3_panel():
|
||||
return Panel(mark='WPB3', length_ft=24.0, width_ft=WIDTH_FT,
|
||||
thickness_in=ENVELOPE, unit_weight_pcf=150.0,
|
||||
fci_psi=3500.0, fc_psi=6000.0, source=wpb3_source())
|
||||
|
||||
|
||||
def _mid_net(panel):
|
||||
return section.NetSection(panel, panel.length_ft / 2.0)
|
||||
|
||||
|
||||
# ------------------------------------------------ the section, vs the golden
|
||||
|
||||
def test_area_is_both_wythes_with_the_insulation_excluded():
|
||||
# A = b * (t1 + t2) = 288 * (3 + 4) = 2016.00 in2, NOT 288*11 = 3168.
|
||||
net = _mid_net(wpb3_panel())
|
||||
assert net.is_sandwich is True
|
||||
assert net.area_in2 == pytest.approx(2016.0, abs=1e-6)
|
||||
|
||||
|
||||
def test_centroid_is_the_composite_neutral_axis():
|
||||
# zbar = (A1*z1 + A2*z2)/A, wythes at z=1.5 and z=9:
|
||||
# = (3*1.5 + 4*9)/7 = (4.5 + 36)/7 = 5.785714 in -> Eriksson Cy 5.79
|
||||
net = _mid_net(wpb3_panel())
|
||||
assert net.zbar_in == pytest.approx(5.7857143, abs=1e-6)
|
||||
|
||||
|
||||
def test_through_thickness_inertia_matches_the_reported_izz():
|
||||
# Iz = b*[ t1^3/12 + t1*(z1 - zbar)^2 + t2^3/12 + t2*(z2 - zbar)^2 ]
|
||||
# = 288*[ 3^3/12 + 3*(1.5-5.785714)^2 + 4^3/12 + 4*(9-5.785714)^2 ]
|
||||
# = 288*[ 2.25 + 55.1020 + 5.3333 + 41.3265 ] = 288*104.0119 = 29955.4
|
||||
net = _mid_net(wpb3_panel())
|
||||
assert net.iz_in4 == pytest.approx(29955.43, abs=0.5) # reported 29955.43
|
||||
|
||||
|
||||
def test_the_two_faces_have_the_unequal_moduli_eriksson_prints():
|
||||
# c_form = 5.785714 (to z=0), c_far = 11 - 5.785714 = 5.214286 (to z=11)
|
||||
# Sb = 29955.43 / 5.785714 = 5177.5 (Eriksson 5177.48)
|
||||
# St = 29955.43 / 5.214286 = 5744.9 (Eriksson 5744.88)
|
||||
net = _mid_net(wpb3_panel())
|
||||
assert net.c_form_in == pytest.approx(5.7857143, abs=1e-6)
|
||||
assert net.c_far_in == pytest.approx(5.2142857, abs=1e-6)
|
||||
sb = net.iz_in4 / net.c_form_in
|
||||
st = net.iz_in4 / net.c_far_in
|
||||
assert sb == pytest.approx(5177.48, abs=1.0)
|
||||
assert st == pytest.approx(5744.88, abs=1.0)
|
||||
# sz_in3 is the GOVERNING (smaller) modulus, the one that decides tension.
|
||||
assert net.sz_in3 == pytest.approx(min(sb, st), abs=1e-6)
|
||||
|
||||
|
||||
def test_the_solid_idealisation_would_have_overstated_this_section():
|
||||
"""The number the refusal was built on: the solid 11 in prism is bigger
|
||||
than the real composite section, both in I and S -- unconservative."""
|
||||
net = _mid_net(wpb3_panel())
|
||||
solid_iz = B_IN * ENVELOPE ** 3 / 12.0 # 31944.0
|
||||
solid_sz = B_IN * ENVELOPE ** 2 / 6.0 # 5808.0
|
||||
assert solid_iz > net.iz_in4 # 31944 > 29955
|
||||
assert solid_sz > net.sz_in3 # 5808 > 5177
|
||||
|
||||
|
||||
# --------------------------------------------------------------- shear b*d
|
||||
|
||||
def test_shear_bd_drops_by_the_36_percent_the_parity_run_found():
|
||||
"""docs sec.3.1: for a sandwich d_v = 0.8*th uses the CONCRETE depth (the
|
||||
summed wythes), not the envelope. Here that is 0.8*7 vs 0.8*11, so the
|
||||
shear area is 7/11 of the solid one -- a 36.4% drop, which is exactly the
|
||||
WPB3.0 shear residual the harness traced to b*d not excluding insulation.
|
||||
"""
|
||||
net = _mid_net(wpb3_panel())
|
||||
assert net.shear_thickness_in == pytest.approx(7.0) # concrete, not 11
|
||||
stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0)
|
||||
pt = capacity.detailed_shear_at(wpb3_panel(), stage,
|
||||
x=12.0, moment_lbft=5000.0, shear_lb=8000.0)
|
||||
solid_bd = net.width_in * capacity.pci.DV_OVER_H_SHEAR * ENVELOPE
|
||||
assert pt.bd_in2 == pytest.approx(solid_bd * 7.0 / 11.0, rel=1e-9)
|
||||
drop = 1.0 - pt.bd_in2 / solid_bd
|
||||
assert drop == pytest.approx(4.0 / 11.0, abs=1e-6) # 0.3636...
|
||||
|
||||
|
||||
# --------------------------------------- contiguous two-layer -> merged solid
|
||||
|
||||
def test_a_contiguous_two_concrete_body_is_the_merged_solid():
|
||||
"""An architectural face cast monolithically on a structural wythe (no
|
||||
insulation gap) is not a hole in the middle -- its composite section is
|
||||
simply the solid over the whole concrete span, and that is what comes
|
||||
out. 4 in (z0-4) + 12 in (z4-16) contiguous -> a solid 16 in section."""
|
||||
src = {'solid_parts': [
|
||||
{'counted': True, 'z0_in': 4.0, 'z1_in': 16.0, 'material': 'STRUCT',
|
||||
'density_pcf': 150.0, 'volume_ft3': 1.0},
|
||||
{'counted': True, 'z0_in': 0.0, 'z1_in': 4.0, 'material': 'ARCH',
|
||||
'density_pcf': 150.0, 'volume_ft3': 1.0}],
|
||||
'envelope_thickness_in': 16.0}
|
||||
panel = Panel(mark='C', length_ft=20.0, width_ft=5.0, thickness_in=12.0,
|
||||
source=src)
|
||||
net = section.NetSection(panel, 10.0)
|
||||
b = 5.0 * 12.0
|
||||
assert net.area_in2 == pytest.approx(b * 16.0) # all 16 in concrete
|
||||
assert net.zbar_in == pytest.approx(8.0) # mid-depth: solid
|
||||
assert net.iz_in4 == pytest.approx(b * 16.0 ** 3 / 12.0)
|
||||
assert net.shear_thickness_in == pytest.approx(16.0)
|
||||
|
||||
|
||||
# --------------------------------------------- the solid path does not move
|
||||
|
||||
def test_a_solid_panel_is_untouched():
|
||||
"""No source -> not a sandwich -> the familiar b*t, b*t^2/6, b*t^3/12,
|
||||
d_v = 0.8*t, centroid t/2. Byte-for-byte what it always was."""
|
||||
panel = Panel(mark='S', length_ft=20.0, width_ft=5.0, thickness_in=8.0)
|
||||
net = section.NetSection(panel, 10.0)
|
||||
b = 5.0 * 12.0
|
||||
assert net.is_sandwich is False
|
||||
assert net.area_in2 == pytest.approx(b * 8.0)
|
||||
assert net.zbar_in == pytest.approx(4.0)
|
||||
assert net.c_form_in == pytest.approx(4.0)
|
||||
assert net.c_far_in == pytest.approx(4.0)
|
||||
assert net.iz_in4 == pytest.approx(b * 8.0 ** 3 / 12.0)
|
||||
assert net.sz_in3 == pytest.approx(b * 8.0 ** 2 / 6.0)
|
||||
assert net.shear_thickness_in == pytest.approx(8.0)
|
||||
|
||||
|
||||
def test_the_free_functions_follow_the_same_section():
|
||||
"""`section.area_in2` / `section_modulus_in3` / `inertia_in4` are what
|
||||
`statics` (EI) and `layout` read; they must be the sandwich section too."""
|
||||
panel = wpb3_panel()
|
||||
x = 12.0
|
||||
net = section.NetSection(panel, x)
|
||||
assert section.area_in2(panel, x) == pytest.approx(net.area_in2, abs=1e-6)
|
||||
assert section.inertia_in4(panel, x) == pytest.approx(net.iz_in4, abs=1e-6)
|
||||
assert section.section_modulus_in3(panel, x) == pytest.approx(net.sz_in3,
|
||||
abs=1e-6)
|
||||
Reference in New Issue
Block a user