# Project State: Concentric Footing Analysis Last updated: 2026-08-21 Project root: `calcs/concentric-footing/` Parent worksheets root: `/home/smill/Sync/worksheets` Reference: `CONCENTRIC-FOOTING.pdf` — Blavatnik concentric footing for steel column (square footing, ACI-based checks). Parent project conventions as documented in `worksheets/codemap.md` and `calcs/wood-joist/PROJECT_STATE.md`. ## Overview New hybrid (Typst + Python) calculation at `calcs/concentric-footing/` that checks a square, concentrically loaded, reinforced concrete spread footing under combined service and ultimate axial load per **ACI 318-19**. Five checks are covered: 1. Soil bearing (service, ASD) — `q = Ps/Af <= qa` 2. One-way (beam) shear — ACI 22.5 — `Vc = 2*lambda*sqrt(f'c)*Bf*d` 3. Two-way (punching) shear — ACI 22.6 — `vc = min(4, 2+4/beta, 2+alpha_s*d/bo)*lambda*sqrt(f'c)` 4. Flexure (bending) — ACI 22.5/7 — Whitney block `a = As*fy/(0.85*f'c*Bf)`, `Mn = As*fy*(d-a/2)` 5. Concrete bearing on footing — ACI 22.8 — `Bn = 0.85*f'c*A1*sqrt(A2/A1) <= 2*0.85*f'c*A1` Minimum reinforcement `rho = As/(Bf*d) >= 0.0018` is checked as `minimum_steel`. The default `input.yaml` reproduces the Blavatnik reference example subject to documented corrections (bearing plate clarification and ACI-correct punching perimeter). The pytest suite locks the corrected numbers. ## Architecture decisions - **Hybrid pattern** (same as `wood-joist` / `steel-beam`): `input.yaml` (Pint unit-bearing quantities, quoted strings) -> `calc.py` (`compute()` -> writes `results.json`) -> `footing.typ` (presents only, no recomputation) -> compiled PDF with `--root .` from `worksheets/`. - **`results.json` shape** (unchanged contract): `{tool, version, project, prepared_by, values, checks}` with `tool = "concentric_footing"`, `version = "0.1"`. - **Pint units**: all dimensional inputs are quoted strings (e.g. `"3000 psi"`, `"3 ft"`, `"18.4 kip"`). `calc.py` converts with a `quantity(value, unit, name)` helper identical to `wood-joist/calc.py`; dimensionless factors are plain floats. - **`calc.py` CLI** mirrors `wood-joist`/`steel-beam`: `--input`, `--output`, `--stdout`; runnable as `python calcs/concentric-footing/calc.py` with no args. - **ACI 318-19** is the governing standard and edition. All clause references are to ACI 318-19 Chapter 22 / 13. - **`footing.typ` imports** from `../../lib/sheet.typ` and reads `results.json`. It also **derives loads in Typst** (mirroring `wood-joist`/`steel-beam`): `DLr`, `LLr`, `Br`, `Lr`, `Ar`, column weight `Wc = bc*bc*Lc*gamma_c` -> `Ps_derived = (DLr+LLr)*Ar + Wc`, `Pu_derived = 1.2*(DLr*Ar+Wc)+1.6*LLr*Ar`. These are emitted as `` metadata and reconciled to the Python-checked `Ps`/`Pu` by the test suite. Capacity numbers are never recomputed in Typst. - **Sheet helpers**: flexure, shear, bearing, and soil bearing use `check` (Demand/Capacity D/C). No `check_service` variant is needed; soil bearing is presented as `q` vs `qa`. - **Compilation**: `typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf` so the shared logo at `assets/logo.png` resolves. ## Engineering decisions (pinned for the builder) All equations, units, and applicability limits are pinned here. The builder must not invent behavior. Tolerances and benchmark values are under "Reference example". ### Inputs and units Pint-parsed quantities (all positive, `ValueError` if <=0 or wrong dimension): - `Ps` -> kip (service axial load, column + roof) - `Pu` -> kip (factored axial load, 1.2D+1.6L) - `qa` -> psf (allowable soil bearing, gross) - `Bf` -> ft or in (square footing side; `Af = Bf^2` -> ft2, also `Bf_in = Bf_ft*12`) - `Df` -> in (total footing thickness) - `cover` -> in (to centroid of steel, so `d = Df - cover`; `d` is effective depth) - `fc` -> psi or ksi (concrete `f'c`) - `fy` -> psi or ksi (reinforcement yield) - `lambda` -> float (lightweight factor, 1.0 normal weight, (0,1]) - `column_width` (`c`) -> in (square column side) - `base_plate_width` (`bp`) -> in (square base plate side, `A1 = bp^2`; if omitted defaults to `c`) - `rebar_size` -> int (e.g. 4 means #4 -> db = rebar_size/8 in) - `N` -> int (number of bars per direction) Dimensionless / integers are validated: `N` integer >=1, `rebar_size` integer 3..18, `lambda` in (0,1], `rho_min` hardcoded 0.0018. Derived: - `db_in = rebar_size/8` - `As1_in2 = pi*db^2/4` - `As_in2 = N*As1` - `d_in = Df_in - cover_in` (cover to centroid per reference; `ValueError` if `d <=0` or `d > Df`) - `Af_ft2 = Bf_ft^2`, `Af_in2 = Af_ft2*144` - `A1_in2 = bp_in^2`, `A2_in2 = Af_in2`, `A2_ft2 = Af_ft2` - `Bf_in = Bf_ft*12`, `L_cant_ft = (Bf_in - c_in)/2/12`, `L_cant_in = (Bf_in - c_in)/2` ### Check 1 — Soil bearing (service) - `q_psf = Ps_lbf / Af_ft2` where `Ps_lbf = Ps_kip*1000` - `qu_psf = Pu_lbf / Af_ft2` (ultimate pressure for concrete checks) - `ok_soil = q_psf <= qa_psf` - Report `q_psf`, `qu_psf`, `qa_psf`, `Af_ft2`. - Note: footing self weight and soil surcharge are excluded (gross pressure follows reference). Scope note states this limitation. ### Check 2 — One-way (beam) shear — ACI 22.5.5, phi=0.75 - Critical section at distance `d` from column face. - `L1_in = (Bf_in - c_in)/2 - d_in` (cantilever beyond section). If `L1_in <=0` then `Vu_kip = 0` (no shear beyond section). - Otherwise `Vu_lbf = qu_psf * (Bf_ft) * (L1_in/12)` because `qu` (psf) * width (ft) * length (ft). So `Vu_kip = Vu_lbf/1000`. - `Vc_lbf = 2*lambda*sqrt(fc_psi)*Bf_in*d_in` (ACI 22.5.5.1, `lambda` factor). `Vc_kip = Vc_lbf/1000`. - `phiVc_kip = 0.75*Vc_kip` - `ok_one_way = Vu_kip <= phiVc_kip` - Also report `Vu/phiVc`. ### Check 3 — Two-way (punching) shear — ACI 22.6.5, phi=0.75 - `bo_in = 4*(c_in + d_in)` (interior square column; critical perimeter at d/2). Documented correction: reference shows 68in which is inconsistent with ACI; correct value for c=14,d=9 is 92in. - `beta = 1.0` (square). `alpha_s = 40` (interior per ACI 22.6.5.3). - `vc1 = 4*lambda*sqrt(fc_psi)` - `vc2 = (2 + 4/beta)*lambda*sqrt(fc_psi)` - `vc3 = (2 + alpha_s*d_in/bo_in)*lambda*sqrt(fc_psi)` - `vc_psi = min(vc1, vc2, vc3)` - `Vc_lbf = vc_psi*bo_in*d_in`, `Vc_kip = Vc_lbf/1000`, `phiVn_kip = 0.75*Vc_kip` - `Apunch_in2 = (c_in + d_in)^2`, `Apunch_ft2 = Apunch_in2/144` - `Vu_lbf = qu_psf*(Af_ft2 - Apunch_ft2)`, `Vu_kip = Vu_lbf/1000` - `ok_two_way = Vu_kip <= phiVn_kip` - Also report `vc_psi`, `bo_in`. ### Check 4 — Flexure — ACI 22.5 / 7, phi=0.90 - Cantilever length `Lc_in = (Bf_in - c_in)/2`, `Lc_ft = Lc_in/12` - `Mu_kipft = qu_psf * Bf_ft * Lc_ft^2 / 2` (qu as psf -> psf*ft*ft^2 = lbf*ft/1000 = kip*ft). Equivalent presentation: `Mu = qu*Bf*((Bf-c)/2)^2/2`. - `a_in = As_in2*fy_psi / (0.85*fc_psi*Bf_in)` - `c_block_in = a_in / beta1` where `beta1 = max(0.65, min(0.85, 0.85 - 0.05*max(0, (fc_psi-4000)/1000)))` (ACI 22.2.2.4.3). Computed for strain check but not required for phi (phi=0.9 tension-controlled assumed; still compute `et` for report). - `beta1` per above. - `Mn_kipft = As_in2*fy_ksi*(d_in - a_in/2)/12` (fy in ksi). Or `As*fy*(d-a/2)/12`. - `phiMn_kipft = 0.90*Mn_kipft` - `ok_flexure = Mu_kipft <= phiMn_kipft` - `rho = As_in2 / (Bf_in*d_in)`, `rho_min = 0.0018`, `ok_min_steel = rho >= rho_min` (separate check `minimum_steel` with demand `rho_min`, capacity `rho`). For `checks` dict, `minimum_steel` uses `demand = rho_min`, `capacity = rho`. - Also report `Mu/phiMn`, `a_in`, `rho`. ### Check 5 — Concrete bearing — ACI 22.8, phi=0.65 - `A1_in2 = bp_in^2`, `A2_in2 = Af_in2` - `sqrt_ratio = sqrt(A2_in2/A1_in2)`, capped at 2.0: `sqrt_ratio_capped = min(sqrt_ratio, 2.0)` - `Bn_lbf = 0.85*fc_psi*A1_in2*sqrt_ratio_capped`, but upper bound `2*0.85*fc_psi*A1_in2` already enforced by cap. - `Bn_kip = Bn_lbf/1000`, `phiBn_kip = 0.65*Bn_kip` - `ok_bearing = Pu_kip <= phiBn_kip` - Report `A1_in2`, `A2_in2`, `sqrt_ratio`, `Bn_kip`, `phiBn_kip`, `Pu/phiBn`. ### Values dictionary (all rounded to 6 decimals via q() helper, except labels) Keys in `results.json` `values` (units encoded in name): `Ps_kip, Pu_kip, qa_psf, q_psf, qu_psf, Af_ft2, Bf_in, Bf_ft, Df_in, cover_in, d_in, fc_psi, fy_psi, fy_ksi, lambda, c_in, bp_in, N, rebar_size, db_in, As1_in2, As_in2, rho, rho_min, L1_in, Vu_one_way_kip, Vc_one_way_kip, phiVc_one_way_kip, bo_in, vc_psi, Vu_two_way_kip, Vc_two_way_kip, phiVn_two_way_kip, Lc_in, Mu_kipft, a_in, beta1, Mn_kipft, phiMn_kipft, A1_in2, A2_in2, sqrt_ratio, Bn_kip, phiBn_kip` ### Checks dictionary Each entry `{demand, capacity, ok}` with appropriate units (kip, kip-ft, psf, or dimensionless for rho): - `soil_bearing`: demand `q_psf`, capacity `qa_psf` - `one_way_shear`: demand `Vu_one_way_kip`, capacity `phiVc_one_way_kip` - `two_way_shear`: demand `Vu_two_way_kip`, capacity `phiVn_two_way_kip` - `flexure`: demand `Mu_kipft`, capacity `phiMn_kipft` - `minimum_steel`: demand `rho_min`, capacity `rho` - `bearing`: demand `Pu_kip`, capacity `phiBn_kip` Overall ok requires all six true. ## Reference example (ground truth to lock, corrected) Project "Blavatnik", prepared_by "Conemco Engineering". Derived loads shown in Typst: `DLr=10 psf`, `LLr=20 psf`, `Br=18.9 ft`, `Lr=27.5 ft`, `Ar=519.75 ft2`, column `14 in x14 in x14 ft`, `gamma_c=145 pcf`, `Ps~18.4 kip`, `Pu~26.2 kip`. Footing assumed square `Bf=3 ft (36 in)`, `Af=9 ft2`, `Df=12 in`, `cover=3 in -> d=9 in`, `f'c=3000 psi`, `fy=60 ksi`, `lambda=1`, `N=4`, `rebar_size=4` -> `As=0.785 in2` (reference rounds to 0.8), `c=14 in`, `bp=6 in`, `qa=2500 psf`. Corrected benchmark (ACI-correct, Pint conversion, tolerance in test is approx): | Quantity | Value (rounded for display) | |---|---| | Ps, Pu | 18.4 kip, 26.2 kip (typst-derived 18.36/26.17) | | q, qu | 2044 psf, 2911 psf (reference 2039/2909 within rounding of Ps/Pu) | | soil D/C | 0.82 (q/qa) | | One-way Vu | 1.46 kip | | One-way Vc | 35.45 kip (2*sqrt(fc)*B*d) -> phiVc 26.59 kip, D/C 0.055 | | Two-way bo | 92 in (corrected from 68) | | vc | 219.1 psi (4*sqrt(fc)) | | Two-way Vc | 181.4 kip -> phiVn 136.0 kip, Vu 15.51 kip, D/C 0.114 | | Mu | 3.68 kip-ft | | a | 0.524 in | | Mn | 34.27 kip-ft -> phiMn 30.84 kip-ft, D/C 0.12 | | rho | 0.00242 (>0.0018) | | Bearing A1/A2 | 36 / 1296 in2, sqrt 6 capped 2 | | Bn | 183.6 kip -> phiBn 119.3 kip, D/C 0.22 | The reference PDF shows Vu one-way 1.5 kip, Vc 35.5 kip, phiVc 26.6 kip, Vu two-way 15.5 kip, Vc 134 kip (using 68in), phiVn 100.6 kip, Mu 3.7 kip-ft, Mn 34.3, phiMn 30.9. Differences are documented in `footing.typ` Scope: 68in perimeter corrected to ACI 92in and plate vs column clarification. Pinned tolerances for pytest.approx: psf within 1%, kip within 0.02 kip, inches within 0.01, phi capacities within 0.3 kip or rel 1e-3. ## Conventions (inherited) - `lib/sheet.typ` is shared. Do not modify; `footing.typ` uses `check` (not `check_service`) for all checks. - No existing calculation (`shore-post`, `concrete-beam`, `steel-beam`, `wood-joist`) may be modified; shared `README.md` is allowed to add the new calc entry. - Compile from `worksheets/` with `--root .`. - Lock the reference example in pytest with `pytest.approx` before treating tool as stable. ## Milestones - 001 DONE: `calc.py` + `input.yaml` + `results.json` — numerical core complete; corrected benchmark reproduced in results.json. - 002 DONE: `test_concentric_footing.py` locks corrected benchmark (10 tests pass; reviewer PASS). - 003 DONE: `footing.typ` + `generated/footing.pdf` + Typst metadata queries (12 tests pass; reviewer PASS skipped per user instruction). - 004 DONE: `README.md` + `codemap.md` refreshed (depends on 003). - 005 DONE: Reviewer PASS — engineering, deterministic evidence, docs, and simplify all verified (12 tests pass; PDF compiles; results.json idempotent). ## Final deliverables - `calcs/concentric-footing/calc.py` — ACI 318-19 footing checks, CLI --input/--output/--stdout - `calcs/concentric-footing/input.yaml` — Pint quantities, defaults reproduce Blavatnik - `calcs/concentric-footing/test_concentric_footing.py` — locks benchmark + units + error guards + Typst queries - `calcs/concentric-footing/footing.typ` — presents checked values, derives Ps/Pu in Typst, embeds sketch, compiled to `generated/footing.pdf` - `README.md`, `codemap.md` — indexed ## Known limitations - Square footing and square column/plate only; rectangular footings not checked. - Interior column only (alpha_s=40); edge/corner punching not covered. - Concentric axial load only; no moment or eccentricity, no overturning, no sliding. - Gross soil pressure (excludes footing self weight and overburden) per reference; net pressure option not provided. - One-way shear assumes uniform `qu` and prismatic width; beam shear Vc uses 2*sqrt(fc) only (no axial or size effect). - Bearing uses `sqrt(A2/A1) <=2` per ACI 22.8.3.2; confinement reinforcement not checked. - d = Df - cover (cover to centroid); bar diameter not subtracted separately. If cover is to clear, adjust input. - Deflection, crack control, development length, and settlement not checked. ## Dependencies - Python: `pyyaml`, `pytest`, `pint` (already in `requirements.txt`) - `typst` CLI for compile and metadata-query tests - No new third-party packages