# 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)` (qu*Bf*L1) - `Vc_one_way_kip == pytest.approx(35.45, rel=0.01)` (2*sqrt(3000)*36*9/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)` (vc*bo*d) - `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)` (qu*Bf*Lc^2/2) - `a_in == pytest.approx(0.524, abs=0.02)` (As*fy/(0.85*fc*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.85*3000*36*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.