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,208 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Prestress, transfer length, and what a blockout does to both.
|
||||
|
||||
The numbers in `test_matches_a_commercial_check` are from an independent
|
||||
run of the same piece through Eriksson Wall v5.2.0. That program was not
|
||||
consulted while this was written -- its inputs were read off its own file and
|
||||
its outputs off its own report -- so agreement to the last printed digit is
|
||||
worth having, and disagreement is worth stopping for.
|
||||
|
||||
The piece: 38'-7.5" x 10'-7.5" x 8", one opening 111.25 x 40.25 set 16 in.
|
||||
from one edge, twelve 1/2 in. 270k strands in two rows.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from panellift import checks, prestress, section
|
||||
from panellift.model import Blockout, Panel, Prestress, Strand
|
||||
|
||||
|
||||
PULL = 0.153 * 270_000 * 0.75 # 75% pull on a 1/2 in. 270k strand
|
||||
POSITIONS_IN = [3.5, 16.5, 40.5, 59.5, 87.5, 109.5]
|
||||
ROWS_IN = [1.75, 6.25]
|
||||
|
||||
|
||||
def sample_panel(loss_pct=1.58, blockout=True):
|
||||
strands = [Strand(y=y / 12.0, z_in=z, area_in2=0.153,
|
||||
initial_pull_lb=PULL)
|
||||
for z in ROWS_IN for y in POSITIONS_IN]
|
||||
blockouts = []
|
||||
if blockout:
|
||||
blockouts.append(Blockout(name='OPENING 1', x0=330.0 / 12.0,
|
||||
y0=16.0 / 12.0, length=111.25 / 12.0,
|
||||
width=40.25 / 12.0))
|
||||
return Panel(
|
||||
mark='sample', length_ft=463.5 / 12.0, width_ft=127.5 / 12.0,
|
||||
thickness_in=8.0, fc_psi=6000.0, fci_psi=3500.0,
|
||||
blockouts=blockouts,
|
||||
prestress=Prestress(strands=strands, loss_pct=loss_pct,
|
||||
diameter_in=0.5))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ transfer
|
||||
|
||||
def test_transfer_length_follows_the_effective_stress():
|
||||
panel = sample_panel()
|
||||
# f_se = 0.75 * 270 * (1 - 0.0158) = 199.3 ksi; lt = f_se * db / 3
|
||||
assert prestress.transfer_length_in(panel.prestress) == \
|
||||
pytest.approx(33.2, abs=0.1)
|
||||
|
||||
|
||||
def test_a_strand_picks_its_force_up_over_the_transfer_length():
|
||||
panel = sample_panel()
|
||||
lt = prestress.transfer_length_in(panel.prestress) / 12.0
|
||||
strand = panel.prestress.strands[0] # y = 3.5 in, never cut
|
||||
assert prestress.developed(strand, panel, 0.0, lt) == 0.0
|
||||
assert prestress.developed(strand, panel, lt / 2, lt) == pytest.approx(0.5)
|
||||
assert prestress.developed(strand, panel, lt, lt) == pytest.approx(1.0)
|
||||
assert prestress.developed(strand, panel, 10.0, lt) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_the_far_end_is_a_free_end_too():
|
||||
panel = sample_panel()
|
||||
lt = prestress.transfer_length_in(panel.prestress) / 12.0
|
||||
strand = panel.prestress.strands[0]
|
||||
assert prestress.developed(strand, panel, panel.length_ft, lt) == 0.0
|
||||
assert prestress.developed(
|
||||
strand, panel, panel.length_ft - lt / 2, lt) == pytest.approx(0.5)
|
||||
|
||||
|
||||
# ------------------------------------------------------- blockouts cut strands
|
||||
|
||||
def test_a_blockout_cuts_only_the_strands_it_passes_over():
|
||||
panel = sample_panel()
|
||||
cut = [s for s in panel.prestress.strands if prestress.cuts_for(s, panel)]
|
||||
# the opening spans y 16 -> 56.25 in, which is 16.5 and 40.5
|
||||
assert len(cut) == 4
|
||||
assert sorted(set(round(s.y * 12.0, 2) for s in cut)) == [16.5, 40.5]
|
||||
|
||||
|
||||
def test_area_inside_the_opening_is_the_uncut_strands_only():
|
||||
panel = sample_panel()
|
||||
state = prestress.state_at(panel, 28.97) # mid-opening
|
||||
assert state.area_in2 == pytest.approx(1.22, abs=0.01)
|
||||
|
||||
|
||||
def test_area_ramps_down_approaching_the_cut():
|
||||
"""Eriksson reports 1.84 / 1.75 / 1.33 / 1.22 at these four stations."""
|
||||
panel = sample_panel()
|
||||
for x_ft, expected in ((22.62, 1.84), (25.11, 1.75),
|
||||
(27.04, 1.33), (27.50, 1.22)):
|
||||
state = prestress.state_at(panel, x_ft)
|
||||
assert state.area_in2 == pytest.approx(expected, abs=0.01), x_ft
|
||||
|
||||
|
||||
def test_a_strand_the_opening_misses_keeps_its_force_through_it():
|
||||
panel = sample_panel()
|
||||
lt = prestress.transfer_length_in(panel.prestress) / 12.0
|
||||
outer = [s for s in panel.prestress.strands
|
||||
if round(s.y * 12.0, 2) == 109.5][0]
|
||||
assert prestress.developed(outer, panel, 28.97, lt) == pytest.approx(1.0)
|
||||
|
||||
|
||||
# ------------------------------------------------------------- net section
|
||||
|
||||
def test_an_off_centre_opening_moves_the_centroid():
|
||||
panel = sample_panel()
|
||||
net = section.NetSection(panel, 28.97)
|
||||
assert net.area_in2 == pytest.approx(698.0, abs=1.0)
|
||||
assert net.cy_in == pytest.approx(76.49, abs=0.05)
|
||||
assert net.sz_in3 == pytest.approx(930.7, abs=1.0)
|
||||
assert net.iy_in4 == pytest.approx(979_000, rel=0.005)
|
||||
|
||||
|
||||
def test_a_solid_section_is_centred():
|
||||
panel = sample_panel(blockout=False)
|
||||
net = section.NetSection(panel, 19.0)
|
||||
assert net.cy_in == pytest.approx(63.75, abs=0.01)
|
||||
assert net.area_in2 == pytest.approx(1020.0, abs=0.5)
|
||||
|
||||
|
||||
# ------------------------------------------------- the number that matters
|
||||
|
||||
def _corners(panel, x, moment_lbft):
|
||||
net = section.NetSection(panel, x)
|
||||
return [c[0] for c in checks._corner_stresses(panel, x, moment_lbft, net)]
|
||||
|
||||
|
||||
def _corner_max(panel, x, moment_lbft):
|
||||
return max(_corners(panel, x, moment_lbft))
|
||||
|
||||
|
||||
def test_matches_a_commercial_check():
|
||||
"""417.6 psi at 28.97 ft, from Eriksson Wall on the same piece.
|
||||
|
||||
Their factored service moment there is 48.17 k-ft. Feed the same moment
|
||||
into the same section and the answer has to be the same, or one of us is
|
||||
wrong about the mechanics rather than about the panel.
|
||||
"""
|
||||
panel = sample_panel(loss_pct=1.58)
|
||||
got = _corner_max(panel, 28.97, 48_170.0)
|
||||
assert got == pytest.approx(417.6, abs=2.0)
|
||||
|
||||
|
||||
def test_without_prestress_the_same_piece_reads_far_higher():
|
||||
"""The bug this whole feature exists to fix, kept as a number."""
|
||||
panel = sample_panel()
|
||||
bare = Panel(**{**panel.__dict__, 'prestress': None})
|
||||
assert _corner_max(bare, 28.97, 48_170.0) == pytest.approx(621.0, abs=2.0)
|
||||
|
||||
|
||||
def test_precompression_wipes_out_a_light_stage():
|
||||
"""Our own stripping moment on this piece, which is larger than theirs.
|
||||
|
||||
23.83 k-ft of bending on the solid section against ~359 psi of
|
||||
precompression: 210 psi of it goes, and the piece comes back well under
|
||||
the limit instead of at 47% of it.
|
||||
"""
|
||||
panel = sample_panel()
|
||||
assert _corner_max(panel, 10.94, 23_830.0) < 40.0
|
||||
|
||||
|
||||
def test_precompression_and_compression_both_match_at_stripping():
|
||||
"""Eriksson's stripping at 0.25L: tension 0.0 psi, compression 667.4.
|
||||
|
||||
Their moment there is 14.10 k-ft, not ours -- they idealise a static
|
||||
lift and this idealises rolling blocks, so the two disagree about the
|
||||
reactions before either says anything about stress. Feeding THEIR moment
|
||||
in isolates the section from the rigging, and both faces then have to
|
||||
agree, which is a stronger check than the tension alone: the tension is
|
||||
zero either way once there is enough precompression, but the compression
|
||||
is not, and it pins P/A and the eccentricity together.
|
||||
"""
|
||||
panel = sample_panel(loss_pct=1.58)
|
||||
corners = _corners(panel, 9.66, 14_100.0)
|
||||
assert max(corners) <= 0.0
|
||||
assert -min(corners) == pytest.approx(667.4, abs=2.0)
|
||||
|
||||
|
||||
def test_a_bigger_loss_gives_back_tension():
|
||||
"""The loss assumption is a real lever, so it has to move the answer."""
|
||||
lean = _corner_max(sample_panel(loss_pct=1.58), 28.97, 48_170.0)
|
||||
fat = _corner_max(sample_panel(loss_pct=8.0), 28.97, 48_170.0)
|
||||
assert fat > lean + 10.0
|
||||
|
||||
|
||||
# --------------------------------------------------------- sign conventions
|
||||
|
||||
def _one_row_panel(z_in):
|
||||
strands = [Strand(y=y / 12.0, z_in=z_in, initial_pull_lb=PULL)
|
||||
for y in [15.0, 45.0, 75.0, 105.0]]
|
||||
return Panel(mark='s', length_ft=30.0, width_ft=10.0, thickness_in=8.0,
|
||||
fc_psi=6000.0,
|
||||
prestress=Prestress(strands=strands, loss_pct=0.0))
|
||||
|
||||
|
||||
def test_a_low_strand_helps_the_face_that_sagging_puts_in_tension():
|
||||
"""Sagging puts tension on the form face, z = 0. A strand below mid-depth
|
||||
hogs the piece and takes tension off that face; the same strand above
|
||||
mid-depth adds to it.
|
||||
|
||||
Nothing in the commercial comparison can catch this: that piece has two
|
||||
symmetric rows, so its eccentricity is exactly zero and the term this
|
||||
tests does not exist. It was written wrong the first time and this is
|
||||
what found it.
|
||||
"""
|
||||
low = _corners(_one_row_panel(1.0), 15.0, 20_000.0)[:2] # cz = -t/2
|
||||
mid = _corners(_one_row_panel(4.0), 15.0, 20_000.0)[:2]
|
||||
high = _corners(_one_row_panel(7.0), 15.0, 20_000.0)[:2]
|
||||
assert max(low) < max(mid) < max(high)
|
||||
Reference in New Issue
Block a user