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,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Put beamcalc/src first on sys.path. Run from the repo root, the bare
|
||||
`beamcalc/` directory (which holds fixtures/, tools/, tests/ and src/) is
|
||||
what `import beamcalc` finds -- an implicit namespace package with no
|
||||
modules in it -- and every test here fails to import. `pip install -e
|
||||
beamcalc` would fix it too, but the suite must run on a fresh checkout
|
||||
with nothing installed, same as the rest of this repo."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
_SRC = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'src')
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
_mod = sys.modules.get('beamcalc')
|
||||
if _mod is not None and getattr(_mod, '__file__', None) is None:
|
||||
del sys.modules['beamcalc'] # the namespace-package placeholder
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Shear-zone accounting: `av_provided_per_ft` sums each covering zone once
|
||||
(a symmetric zone overlapping its own mirror at midspan is not doubled, but
|
||||
two identical zone rows -- the doubled-stirrup detail -- each count), the raw
|
||||
Vs reported per station vs the 8·√f'c·bw·d cap of 22.5.1.2 (`vs_over_max` and
|
||||
its note), the displaced-concrete flexure station, and a full RBA1 run."""
|
||||
import inspect
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from beamcalc import model, solve, shear
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
RBA5 = os.path.join(HERE, '..', 'fixtures', 'rba5_16x22.json')
|
||||
RBA1 = os.path.join(HERE, '..', 'fixtures', 'rba1_8x48.json')
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def rba5():
|
||||
return model.load_job(RBA5)
|
||||
|
||||
|
||||
def test_av_provided_per_ft_counts_each_zone_once(rba5):
|
||||
# (beam, x_in) -> (total in^2/ft, tightest spacing or None)
|
||||
assert list(inspect.signature(shear.av_provided_per_ft).parameters) \
|
||||
== ['beam', 'x_in']
|
||||
# 107.5" is the beam midpoint: the symmetric outer zone's mirrored half
|
||||
# overlaps its own direct span there, and must count only once.
|
||||
mid, s_mid = shear.av_provided_per_ft(rba5, 107.5)
|
||||
off_mid, s_off = shear.av_provided_per_ft(rba5, 100.0)
|
||||
assert mid == pytest.approx(off_mid, abs=1e-9)
|
||||
assert s_mid == s_off
|
||||
|
||||
|
||||
def test_av_provided_per_ft_doubles_the_tight_zone(rba5):
|
||||
# RBA5's transverse list carries the tight end zone (first 2", s = 4")
|
||||
# twice: the doubled-stirrup detail, and each identical row counts once.
|
||||
tight = [z for z in rba5.transverse if z.spacing_in == 4.0]
|
||||
assert len(tight) == 2
|
||||
expected = 2 * (tight[0].area * 12.0 / tight[0].spacing_in)
|
||||
av_prov, s_min = shear.av_provided_per_ft(rba5, 6.0)
|
||||
assert av_prov == pytest.approx(expected, abs=1e-9)
|
||||
assert s_min == 4.0
|
||||
|
||||
|
||||
def test_raw_vs_and_22_5_1_2_flag(rba5):
|
||||
a = solve.analyse(rba5, extra_stations_in=[0.33 * 12, 0.90 * 12,
|
||||
8.96 * 12])
|
||||
cases = [(0.33 * 12, 153.6, True), (0.90 * 12, 233.9, True),
|
||||
(8.96 * 12, 96.0, False)]
|
||||
for x, vs_exp, over in cases:
|
||||
row = min(a.shr, key=lambda s: abs(s.x_in - x))
|
||||
assert abs(row.x_in - x) < 1e-6, 'no station at x = {}'.format(x)
|
||||
msg = ('at x={0:.2f}: vs_prov={1:.4f} (expect ~{2}), '
|
||||
'vs_over_max={3}, note={4!r}').format(
|
||||
row.x_in, row.r.vs_prov, vs_exp, row.r.vs_over_max,
|
||||
row.r.note)
|
||||
assert row.r.vs_prov == pytest.approx(vs_exp, rel=0.01), msg
|
||||
assert row.r.vs_over_max is over, msg
|
||||
assert ('22.5.1.2' in row.r.note) is over, msg
|
||||
|
||||
|
||||
def test_displaced_concrete_flexure_row(rba5):
|
||||
a = solve.analyse(rba5, extra_stations_in=[1.43 * 12])
|
||||
row = min(a.flex, key=lambda f: abs(f.x_ft - 1.43))
|
||||
msg = ('at x_ft={0}: c={1:.4f} (expect ~2.67), '
|
||||
'phi_mn={2:.4f} (expect ~224.0)').format(
|
||||
row.x_ft, row.c, row.phi_mn)
|
||||
# phi_mn reproduces the package's 224.0. c does NOT yet: the package
|
||||
# prints 2.67 here and the engine gives 2.49 (2.42 before the
|
||||
# displaced-concrete trigger moved from a to c). This pins the current
|
||||
# value as a regression guard so a change to the section solve is
|
||||
# noticed -- it is not a claim of agreement. When c reaches 2.67 within
|
||||
# 3%, replace the pin with that. Open item, PROJECT.md §4 / RBA5 notes.
|
||||
assert row.c == pytest.approx(2.4928, rel=0.02), msg
|
||||
assert abs(row.phi_mn) == pytest.approx(224.0, rel=0.01), msg
|
||||
|
||||
|
||||
def test_rba1_analysis_runs():
|
||||
a = solve.analyse(model.load_job(RBA1))
|
||||
assert len(a.shr) > 10
|
||||
|
||||
|
||||
def test_grade_factor_psi_g_follows_aci_table_25_4_2_5():
|
||||
from beamcalc import develop
|
||||
assert develop.psi_g_for(60.0) == 1.0
|
||||
assert develop.psi_g_for(80.0) == 1.15
|
||||
assert develop.psi_g_for(100.0) == 1.3
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""`Result.shear_jump` returns the exact one-sided shear limits at x --
|
||||
grid-independent and right even when x sits just before a support or point
|
||||
load (the old code reported the neighbouring grid node's post-jump value)."""
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from beamcalc import model, solve, statics
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
JOB = os.path.join(HERE, '..', 'fixtures', 'rba5_16x22.json')
|
||||
EXTRA = [1.90, 1.92, 2.0, 2.04, 212.92, 213.0, 213.08]
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def job():
|
||||
return model.load_job(JOB)
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def analysis(job):
|
||||
return solve.analyse(job, extra_stations_in=EXTRA)
|
||||
|
||||
|
||||
def _row(analysis, x_in):
|
||||
return next(r for r in analysis.strength if abs(r.x_in - x_in) < 1e-6)
|
||||
|
||||
|
||||
def test_strength_rows_see_the_overhang_side_before_a_support(analysis):
|
||||
# 1.92" is 0.08" before the left support centreline at 2.0": both
|
||||
# limits there are the small overhang shear, not the 44 kip span side.
|
||||
assert _row(analysis, 1.92).vu < 2.0
|
||||
assert _row(analysis, 2.04).vu > 40.0
|
||||
# right support at 213.0": 212.92" is span-side, 213.08" is overhang.
|
||||
assert _row(analysis, 212.92).vu > 18.0
|
||||
assert _row(analysis, 213.08).vu < 1.0
|
||||
|
||||
|
||||
def test_diagram_shear_jump_at_support():
|
||||
a = solve.analyse(model.load_job(JOB), extra_stations_in=EXTRA)
|
||||
# The governing combo puts 44.1 kip on the span side of the left support.
|
||||
d = a.diagrams['1.2D + 1.6L + 0.5R [pattern 3]']
|
||||
left, right = d.shear_jump(2.0)
|
||||
assert abs(left) < 2.0
|
||||
assert abs(right) > 40.0
|
||||
|
||||
|
||||
def test_diagram_shear_jump_is_continuous_off_loads(analysis):
|
||||
d = analysis.diagrams['1.4D']
|
||||
left, right = d.shear_jump(100.0) # no load or support sits at 100.0
|
||||
assert left == pytest.approx(right, abs=1e-9)
|
||||
assert left == pytest.approx(d.shear_at(100.0), abs=1e-6)
|
||||
|
||||
|
||||
def test_right_limit_at_a_node_is_the_stored_value(analysis):
|
||||
d = analysis.diagrams['1.2D + 1.6L + 0.5R [pattern 3]']
|
||||
for x, v in zip(d.xs, d.shear):
|
||||
assert d.shear_jump(x)[1] == pytest.approx(v, abs=1e-9)
|
||||
Reference in New Issue
Block a user