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