calcs/concentric-footing/tasks/002_numerical_tests.md
smillmorel d5ac3fca7e Add structural calculation worksheets
Collection of engineering calculation projects (Python + Typst), each with
input, calc script, tests, results, and generated PDF where available.
2026-09-21 12:19:20 -04:00

5.5 KiB

Task 002 — Numerical lock: test_concentric_footing.py (compute() only)

Goal

Lock the corrected Blavatnik benchmark in pytest so the calculator cannot drift, and guard Pint units and applicability limits.

Background

Depends on Task 001 contract. This task is purely numerical: it imports calc.py:compute and asserts pytest.approx on every value name. It does not require Typst. The reference PDF contains known inconsistencies (68in vs 92in punching perimeter, plate vs column area, 66in textual dimension vs 36in computed Af); tests lock the ACI-correct values documented in PROJECT_STATE.md.

All assertions target values produced by calc.py:compute() sub-blocks: Pressures, One-way shear, Two-way shear, Flexure, Concrete bearing.

Files to Modify

  • calcs/concentric-footing/test_concentric_footing.py — create.
  • calcs/concentric-footing/calc.py — read-only, do not edit.
  • calcs/concentric-footing/input.yaml — read-only.
  • calcs/concentric-footing/results.json — read-only.

Implementation

Create calcs/concentric-footing/test_concentric_footing.py following calcs/wood-joist/test_wood_joist.py structure:

  • Imports: pathlib Path, importlib.util, json, subprocess, sys, pytest, yaml
  • Load calc module via importlib.util.spec_from_file_location("concentric_footing_calc", HERE/"calc.py"); compute = calc_module.compute
  • Helper load_input() reads HERE/"input.yaml" with yaml.safe_load
  • @pytest.fixture def result(): return compute(load_input())

Tests to implement (each as def test_* function)

  1. test_example_pressures_and_geometry(result) — asserts:

    • Af_ft2 == 9.0 approx
    • Bf_ft == 3.0, Bf_in == 36.0
    • d_in == 9.0
    • q_psf == pytest.approx(2044.44, rel=0.01) (18.4k/9) — allow 1% because Ps 18.4 vs derived 18.36
    • qu_psf == pytest.approx(2911.11, rel=0.01) (26.2/9)
    • qa_psf == 2500.0
  2. test_example_one_way_shear(result) — asserts:

    • L1_in == pytest.approx(2.0, abs=0.01) ((36-14)/2 -9 =2)
    • Vu_one_way_kip == pytest.approx(1.455, abs=0.05) (quBfL1)
    • Vc_one_way_kip == pytest.approx(35.45, rel=0.01) (2*sqrt(3000)369/1000)
    • phiVc_one_way_kip == pytest.approx(26.59, rel=0.01)
    • checks["one_way_shear"]["ok"] is True
    • D/C approx 0.055
  3. test_example_two_way_shear(result) — asserts corrected ACI values:

    • bo_in == pytest.approx(92.0) (4*(14+9))
    • vc_psi == pytest.approx(219.089, rel=1e-3) (4*sqrt(3000))
    • Vc_two_way_kip == pytest.approx(181.4, rel=0.02) (vcbod)
    • phiVn_two_way_kip == pytest.approx(136.0, rel=0.02)
    • Vu_two_way_kip == pytest.approx(15.51, abs=0.1) (qu*(9 - (23/12)^2))
    • checks["two_way_shear"]["ok"] is True
    • Document that reference PDF reports bo 68in and Vc 134kip; this test locks ACI-correct 92in.
  4. test_example_flexure(result) — asserts:

    • Lc_in == pytest.approx(11.0) ((36-14)/2)
    • Mu_kipft == pytest.approx(3.68, abs=0.1) (quBfLc^2/2)
    • a_in == pytest.approx(0.524, abs=0.02) (Asfy/(0.85fc*B))
    • As_in2 == pytest.approx(0.785, abs=0.02) (4*#4 -> 4*0.196=0.785; reference rounds to 0.8, accept both with rel 0.03 but assert within 0.785±0.02)
    • Mn_kipft == pytest.approx(34.27, abs=0.5)
    • phiMn_kipft == pytest.approx(30.84, abs=0.5)
    • rho == pytest.approx(0.00242, rel=0.02)
    • checks["flexure"]["ok"] is True
    • checks["minimum_steel"]["ok"] is True (rho >=0.0018)
  5. test_example_bearing(result) — asserts:

    • A1_in2 == pytest.approx(36.0) (6*6)
    • A2_in2 == pytest.approx(1296.0) (36*36)
    • sqrt_ratio == pytest.approx(6.0, abs=0.01) capped at 2 for Bn actually but store raw ratio and report capped separately? Store raw 6.0 and separately use capped 2 for Bn; test asserts stored sqrt_ratio is 6.0 and Bn uses capped 2.
    • Bn_kip == pytest.approx(183.6, abs=0.5) (0.85300036*2/1000)
    • phiBn_kip == pytest.approx(119.34, abs=0.5)
    • checks["bearing"]["ok"] is True
  6. test_all_checks_pass(result) — asserts all six checks ok is True.

  7. test_alternate_units_match_default(result) — copies input, replaces Bf with "36 in", fc with "3 ksi", fy with "60000 psi", Ps with "18400 lbf", Pu with "26200 lbf" etc, computes converted, asserts for keys ("Af_ft2","q_psf","Mu_kipft","Vu_one_way_kip","phiBn_kip") approx equal rel 1e-6.

  8. test_wrong_dimension_is_rejected() — sets Bf: "3 kip" and expects ValueError match "Bf".

  9. test_effective_depth_validation() — sets cover "13 in" with Df "12 in" (d negative) expects ValueError.

  10. test_zero_footing_size_rejected() — sets Bf "0 ft" expects ValueError.

If any additional guard is implemented (lambda >1, N zero) add matching tests, but at minimum the 10 above.

Each test that reads values should use v = result["values"] and pytest.approx. The file must be runnable with python -m pytest calcs/concentric-footing/test_concentric_footing.py -v.

Acceptance Criteria

  1. python -m pytest calcs/concentric-footing/test_concentric_footing.py -v shows at least 10 tests, all passed.
  2. Tests lock the corrected benchmark within stated tolerances; changing any equation in calc.py causes at least one failure.
  3. Wrong-dimension and validation tests raise ValueError with correct field name.
  4. Alternate units test demonstrates Pint equivalence.
  5. No modification to calc.py, input.yaml, results.json.

Tests

Builder runs:

python -m pip install -r requirements.txt
python -m pytest calcs/concentric-footing/test_concentric_footing.py -v

Expected: 10 passed (or more if extra guards).

Dependencies

Task 001 must be DONE.