8e6ee31596
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.
111 lines
4.4 KiB
Python
111 lines
4.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""The beam solver must reproduce PCI Figure 8.3.2 from first principles.
|
|
|
|
This is the test the whole design rests on. Figure 8.3.2's coefficients are
|
|
not a separate method -- they are the closed-form solution of a uniformly
|
|
loaded beam with two overhangs, with the supports placed where the positive
|
|
and negative moments come out equal. If our solver cannot land on 0.0214 with
|
|
the supports there, it cannot be trusted with the supports somewhere else,
|
|
which is the only case anyone actually needs it for.
|
|
"""
|
|
import math
|
|
|
|
from panellift import pci, section, statics
|
|
from panellift.model import Stage
|
|
|
|
from conftest import ASPIRE, LENGTH_FT, WIDTH_FT
|
|
|
|
|
|
def test_optimal_support_fraction_is_the_root_of_the_quadratic():
|
|
a = pci.OPTIMAL_TWO_LINE_FRACTION
|
|
assert abs(4 * a ** 2 + 4 * a - 1) < 1e-12
|
|
assert abs(a - 0.20710678) < 1e-7
|
|
|
|
|
|
def test_closed_form_coefficient_is_pcis_0_0214():
|
|
"""M = w*(aL)^2/2 with a = (sqrt(2)-1)/2 gives 0.02145, printed as 0.0214."""
|
|
a = pci.OPTIMAL_TWO_LINE_FRACTION
|
|
coefficient = a ** 2 / 2.0
|
|
assert abs(coefficient - 0.0214) < 0.0001
|
|
|
|
|
|
def test_solver_reproduces_my_coefficient(solid_panel, rolling):
|
|
"""The numerical diagram must match 0.0214 w a b^2 to within 0.5%."""
|
|
stage = Stage(name='unfactored', multiplier=1.0, fc_psi=4000.0)
|
|
result = statics.analyse(solid_panel, stage, rolling,
|
|
reconcile_weight=False)
|
|
|
|
w_psf = section.unit_weight_psf(solid_panel)
|
|
_, expected = pci.fig_832_four_point(w_psf, WIDTH_FT, LENGTH_FT)
|
|
|
|
_, m_pos = result.diagram.max_positive
|
|
_, m_neg = result.diagram.max_negative
|
|
|
|
assert abs(m_pos - expected) / expected < 0.005
|
|
assert abs(abs(m_neg) - expected) / expected < 0.005
|
|
|
|
|
|
def test_positive_and_negative_moments_are_equal_at_the_optimum(solid_panel,
|
|
rolling):
|
|
"""That equality is the definition of the Figure 8.3.2 layout."""
|
|
stage = Stage(name='unfactored', multiplier=1.0, fc_psi=4000.0)
|
|
result = statics.analyse(solid_panel, stage, rolling,
|
|
reconcile_weight=False)
|
|
_, m_pos = result.diagram.max_positive
|
|
_, m_neg = result.diagram.max_negative
|
|
assert abs(m_pos - abs(m_neg)) / m_pos < 0.01
|
|
|
|
|
|
def test_unit_weight_matches_the_article(solid_panel):
|
|
assert abs(section.unit_weight_psf(solid_panel) -
|
|
ASPIRE['w_psf']) < 0.1
|
|
|
|
|
|
def _peak_abs_moment(result):
|
|
"""The governing moment, whichever sign it has.
|
|
|
|
Bunching the picks inboard turns the panel into two long cantilevers, and
|
|
then the whole interior hogs -- the largest positive moment is zero, at
|
|
the ends. Comparing only sagging would read that as an improvement.
|
|
"""
|
|
_, positive = result.diagram.max_positive
|
|
_, negative = result.diagram.max_negative
|
|
return max(abs(positive), abs(negative))
|
|
|
|
|
|
def test_moving_the_supports_inboard_raises_the_governing_moment(solid_panel,
|
|
rolling):
|
|
"""A sanity check that the solver is solving, not returning a constant."""
|
|
stage = Stage(name='unfactored', multiplier=1.0, fc_psi=4000.0)
|
|
optimum = _peak_abs_moment(
|
|
statics.analyse(solid_panel, stage, rolling, reconcile_weight=False))
|
|
|
|
for lifter in solid_panel.lifters:
|
|
lifter.x = LENGTH_FT / 2.0 + math.copysign(
|
|
4.0, lifter.x - LENGTH_FT / 2.0)
|
|
moved = _peak_abs_moment(
|
|
statics.analyse(solid_panel, stage, rolling, reconcile_weight=False))
|
|
|
|
assert moved > optimum * 1.5
|
|
|
|
|
|
def test_the_optimum_layout_really_is_the_optimum(solid_panel, rolling):
|
|
"""No other support position beats (sqrt(2)-1)/2. Worth proving once.
|
|
|
|
This is what makes the PCI layout a benchmark the report can measure a
|
|
detailer's actual layout against, rather than just another arrangement.
|
|
"""
|
|
stage = Stage(name='unfactored', multiplier=1.0, fc_psi=4000.0)
|
|
best = _peak_abs_moment(
|
|
statics.analyse(solid_panel, stage, rolling, reconcile_weight=False))
|
|
|
|
for fraction in (0.10, 0.15, 0.18, 0.23, 0.26, 0.30):
|
|
left = fraction * LENGTH_FT
|
|
for lifter in solid_panel.lifters:
|
|
lifter.x = left if lifter.x < LENGTH_FT / 2.0 else LENGTH_FT - left
|
|
trial = _peak_abs_moment(
|
|
statics.analyse(solid_panel, stage, rolling,
|
|
reconcile_weight=False))
|
|
assert trial >= best * 0.999, (
|
|
'fraction {0} beat the supposed optimum'.format(fraction))
|