calcs/concentric-footing/tasks/001_calc_and_input.md

131 lines
7.8 KiB
Markdown
Raw Normal View History

# Task 001 — Numerical core: calc.py + input.yaml + results.json
## Goal
Implement the ACI 318-19 concentric footing calculator as the single source of truth for soil bearing, one-way shear, two-way shear, flexure, minimum steel, and concrete bearing.
## Background
Architecture is hybrid Pint pattern per PROJECT_STATE.md. This task establishes the contract every later task depends on: input.yaml Pint quantities, calc.py compute() with 6 checks, and results.json {tool,version,project,prepared_by,values,checks}. Parent conventions from wood-joist and steel-beam apply. No Typst work in this task.
The reference is CONCENTRIC-FOOTING.pdf (Blavatnik). Corrected benchmark is pinned in PROJECT_STATE.md (Bf=3ft, Af=9ft2, d=9in, fc=3000psi, fy=60ksi, Ps~18.4kip Pu~26.2kip, etc.). The 68in punching perimeter in the PDF is inconsistent; implement ACI-correct bo=4*(c+d)=92in.
## Files to Modify
- `calcs/concentric-footing/calc.py` — create. Single module, no external deps beyond pyyaml/pint.
- `calcs/concentric-footing/input.yaml` — create. Pint-quoted strings for all dimensional inputs.
- `calcs/concentric-footing/results.json` — create (generated by running calc.py on the default input.yaml). Do not hand-edit.
If `calcs/concentric-footing/generated/` does not exist, do not create it here (Task 003 does).
## Implementation
### 1. input.yaml — defaults reproduce corrected Blavatnik example
Write YAML with these keys (order as listed, comments allowed):
```yaml
project: "Blavatnik"
prepared_by: "Conemco Engineering"
Ps: "18.4 kip" # service axial (checked demand)
Pu: "26.2 kip" # factored axial (checked demand, 1.2D+1.6L)
qa: "2500 psf" # allowable soil bearing (gross)
Bf: "3 ft" # square footing side
Df: "12 in" # total thickness
cover: "3 in" # to centroid of steel -> d = Df - cover
fc: "3000 psi" # f'c
fy: "60 ksi" # fy
lambda: 1 # lightweight factor (float, (0,1])
column_width: "14 in" # c, square column
base_plate_width: "6 in" # bp, square base plate (A1 = bp^2); if omitted in code defaults to c but YAML provides it
N: 4 # bars per direction
rebar_size: 4 # #4 -> db=0.5in
# Optional documentation keys (not used in compute, but keep for Typst derivation comments):
# DLr, LLr etc are NOT in input.yaml; Typst derives Ps/Pu there. This YAML holds checked demands only.
```
All quantities must be quoted strings so Pint parses them. `lambda`, `N`, `rebar_size` are unquoted numbers.
Accept alternative units via Pint (e.g. Bf as "36 in", fc as "3 ksi", Ps as "18400 lbf") — conversion handled in calc.py.
### 2. calc.py — implement compute(inp) -> dict
Create file at `calcs/concentric-footing/calc.py` with structure identical to `calcs/wood-joist/calc.py`:
- Imports: json, math, sys, argparse, pathlib Path, yaml, pint UnitRegistry, DimensionalityError etc.
- Define `HERE = Path(__file__).resolve().parent`
- `ureg = UnitRegistry()` and define `kip = 1000*force_pound`, `ksi = kip/inch**2`, `psf = force_pound/foot**2`, `pcf = force_pound/foot**3`, `plf`, `psi` if missing.
- Helper `quantity(value, unit, name) -> float`: `ureg.Quantity(value).to(unit).magnitude` with ValueError on bad dimension or <=0. Message must include field name.
- Helper `factor(value,name)` or inline validation for lambda (0<lambda<=1), integer checks for N and rebar_size.
- `def compute(inp: dict) -> dict:` implements pinned equations from PROJECT_STATE.md **verbatim**:
1. Parse:
```
Ps_kip = quantity(inp["Ps"],"kip","Ps")
Pu_kip = quantity(inp["Pu"],"kip","Pu")
qa_psf = quantity(inp["qa"],"psf","qa")
Bf_ft = quantity(inp["Bf"],"ft","Bf")
Df_in = quantity(inp["Df"],"in","Df")
cover_in = quantity(inp["cover"],"in","cover")
fc_psi = quantity(inp["fc"],"psi","fc")
fy_psi = quantity(inp["fy"],"psi","fy") # accept ksi via Pint -> psi
fy_ksi = fy_psi/1000
lambda_f = float(inp.get("lambda",1))
c_in = quantity(inp["column_width"],"in","column_width")
bp_in = quantity(inp.get("base_plate_width", inp["column_width"]),"in","base_plate_width")
N = int(inp["N"]); rebar_size = int(inp["rebar_size"])
```
Validate lambda (0,1], N>=1, rebar_size 3..18, d_in = Df_in - cover_in >0 else ValueError, Bf_in = Bf_ft*12, Af_ft2 = Bf_ft**2, Af_in2 = Af_ft2*144.
2. Derived rebar: db_in = rebar_size/8.0, As1_in2 = pi*db^2/4, As_in2 = N*As1_in2, rho = As_in2/(Bf_in*d_in), rho_min=0.0018
3. Pressures: Ps_lbf=Ps_kip*1000, Pu_lbf=Pu_kip*1000, q_psf = Ps_lbf/Af_ft2, qu_psf = Pu_lbf/Af_ft2
4. One-way: L1_in = (Bf_in - c_in)/2 - d_in; Vu_one = 0 if L1_in<=0 else qu_psf*Bf_ft*(L1_in/12)/1000 (kip); Vc_one_lbf = 2*lambda_f* sqrt(fc_psi) * Bf_in * d_in; phiVc =0.75*Vc/1000
5. Two-way: bo=4*(c_in+d_in); beta=1; alpha=40; vc1=4*lambda*sqrt(fc), vc2=(2+4/beta)*lambda*sqrt(fc), vc3=(2+alpha*d/bo)*lambda*sqrt(fc); vc=min(...); Vc_two = vc*bo*d/1000; phiVn=0.75*Vc; Apunch = (c+d)^2 /144 ft2; Vu_two = qu*(Af - Apunch)/1000
6. Flexure: Lc_in=(Bf_in - c_in)/2; Lc_ft = Lc_in/12. Mu_kipft = qu_psf * Bf_ft * Lc_ft**2 / 2 / 1000 (qu psf * Bf ft * Lc_ft^2 / 2 = lbf-ft, /1000 = kip-ft). a_in = As_in2*fy_psi/(0.85*fc_psi*Bf_in); beta1 = max(0.65, min(0.85, 0.85 - 0.05*max(0, (fc_psi-4000)/1000))); Mn_kipft = As_in2*fy_ksi*(d_in - a_in/2)/12; phiMn_kipft = 0.9*Mn_kipft
7. Bearing: A1=bp^2, A2=Af_in2, sqrt_ratio = sqrt(A2/A1), capped 2.0, Bn=0.85*fc_psi*A1*capped/1000, phiBn=0.65*Bn
8. Build values dict with keys listed in PROJECT_STATE.md, each q(value,6) rounded. Include N, rebar_size, db, As1, As etc. Use helper q=lambda v: round(float(v),6).
9. Build checks dict with 6 entries: soil_bearing, one_way_shear, two_way_shear, flexure, minimum_steel, bearing each {demand, capacity, ok}. For minimum_steel demand=rho_min capacity=rho.
10. Return {tool:"concentric_footing", version:"0.1", project: inp.get("project",""), prepared_by: inp.get("prepared_by",""), values:..., checks:...}
- Implement `main(argv)` with argparse `--input/-i` default HERE/"input.yaml", `--output/-o` default beside input, `--stdout` flag. Mirrors wood-joist: read yaml, compute, write json indent2, print path or stdout.
- `if __name__=="__main__": raise SystemExit(main())`
### 3. results.json
After writing calc.py and input.yaml, run `python calcs/concentric-footing/calc.py` (from worksheets root) to generate `calcs/concentric-footing/results.json`. Verify file exists and contains tool concentric_footing.
## Acceptance Criteria
1. `python calcs/concentric-footing/calc.py` exits 0 and writes `results.json` with `tool=="concentric_footing"` and `version=="0.1"`.
2. Re-running with `--stdout` produces identical JSON to the file (idempotent).
3. Converting alternative units yields same magnitudes: Bf "36 in" vs "3 ft", fc "3 ksi" vs "3000 psi", Ps "18400 lbf" vs "18.4 kip" within 1e-6 rel.
4. Wrong dimension raises ValueError with field name (e.g. `Bf: "3 kip"`).
5. Negative or zero values raise ValueError.
6. `d = Df - cover` validation: if cover >= Df, ValueError.
7. Values for default input within approx of corrected benchmark: q ~2044 psf (±30), qu~2911 psf, one-way phiVc~26.6 kip, two-way phiVn~136 kip, Mu~3.68 kip-ft, phiMn~30.8 kip-ft, rho~0.0024, phiBn~119 kip. (Exact locks in Task 002.)
8. No existing calcs are modified.
## Tests
Do not create tests in this task. Manual verification commands (builder runs, reviewer will run pytest after Task 002):
```
python -m pip install -r requirements.txt
python calcs/concentric-footing/calc.py
python calcs/concentric-footing/calc.py --stdout | python -m json.tool
python -c "import yaml, importlib.util; spec=importlib.util.spec_from_file_location('c','calcs/concentric-footing/calc.py'); m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m); print(m.compute(yaml.safe_load(open('calcs/concentric-footing/input.yaml'))))"
```
## Dependencies
None.