Collection of engineering calculation projects (Python + Typst), each with input, calc script, tests, results, and generated PDF where available.
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 definekip = 1000*force_pound,ksi = kip/inch**2,psf = force_pound/foot**2,pcf = force_pound/foot**3,plf,psiif missing.- Helper
quantity(value, unit, name) -> float:ureg.Quantity(value).to(unit).magnitudewith 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:
- 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.
-
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
-
Pressures: Ps_lbf=Ps_kip1000, Pu_lbf=Pu_kip1000, q_psf = Ps_lbf/Af_ft2, qu_psf = Pu_lbf/Af_ft2
-
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
-
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
-
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
-
Bearing: A1=bp^2, A2=Af_in2, sqrt_ratio = sqrt(A2/A1), capped 2.0, Bn=0.85fc_psiA1capped/1000, phiBn=0.65Bn
-
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).
-
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.
-
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/-idefault HERE/"input.yaml",--output/-odefault beside input,--stdoutflag. 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
python calcs/concentric-footing/calc.pyexits 0 and writesresults.jsonwithtool=="concentric_footing"andversion=="0.1".- Re-running with
--stdoutproduces identical JSON to the file (idempotent). - 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.
- Wrong dimension raises ValueError with field name (e.g.
Bf: "3 kip"). - Negative or zero values raise ValueError.
d = Df - covervalidation: if cover >= Df, ValueError.- 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.) - 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.