calcs/concentric-footing/tasks/002_numerical_tests.md

110 lines
5.5 KiB
Markdown
Raw Normal View History

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