calcs/concentric-footing/tasks/001_calc_and_input.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

7.8 KiB

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, Ps18.4kip Pu26.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):

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_ft12, Af_ft2 = Bf_ft**2, Af_in2 = Af_ft2144.

  1. Derived rebar: db_in = rebar_size/8.0, As1_in2 = pidb^2/4, As_in2 = NAs1_in2, rho = As_in2/(Bf_in*d_in), rho_min=0.0018

  2. Pressures: Ps_lbf=Ps_kip1000, Pu_lbf=Pu_kip1000, q_psf = Ps_lbf/Af_ft2, qu_psf = Pu_lbf/Af_ft2

  3. One-way: L1_in = (Bf_in - c_in)/2 - d_in; Vu_one = 0 if L1_in<=0 else qu_psfBf_ft(L1_in/12)/1000 (kip); Vc_one_lbf = 2lambda_f sqrt(fc_psi) * Bf_in * d_in; phiVc =0.75*Vc/1000

  4. Two-way: bo=4*(c_in+d_in); beta=1; alpha=40; vc1=4lambdasqrt(fc), vc2=(2+4/beta)lambdasqrt(fc), vc3=(2+alphad/bo)lambdasqrt(fc); vc=min(...); Vc_two = vcbod/1000; phiVn=0.75Vc; Apunch = (c+d)^2 /144 ft2; Vu_two = qu*(Af - Apunch)/1000

  5. 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_in2fy_psi/(0.85fc_psiBf_in); beta1 = max(0.65, min(0.85, 0.85 - 0.05max(0, (fc_psi-4000)/1000))); Mn_kipft = As_in2fy_ksi(d_in - a_in/2)/12; phiMn_kipft = 0.9*Mn_kipft

  6. Bearing: A1=bp^2, A2=Af_in2, sqrt_ratio = sqrt(A2/A1), capped 2.0, Bn=0.85fc_psiA1capped/1000, phiBn=0.65Bn

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

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

  9. 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), qu2911 psf, one-way phiVc26.6 kip, two-way phiVn136 kip, Mu3.68 kip-ft, phiMn30.8 kip-ft, rho0.0024, phiBn119 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.