Files
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

197 lines
8.9 KiB
Python

# -*- 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)