Add structural calculation worksheets
Collection of engineering calculation projects (Python + Typst), each with input, calc script, tests, results, and generated PDF where available.
This commit is contained in:
commit
d5ac3fca7e
81 changed files with 18668 additions and 0 deletions
BIN
concentric-footing/CONCENTRIC-FOOTING.pdf
Executable file
BIN
concentric-footing/CONCENTRIC-FOOTING.pdf
Executable file
Binary file not shown.
211
concentric-footing/PROJECT_STATE.md
Normal file
211
concentric-footing/PROJECT_STATE.md
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
# 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 `<concentric-footing-loads>` 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
|
||||
19
concentric-footing/TASKS.md
Normal file
19
concentric-footing/TASKS.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Tasks: Concentric Footing Analysis
|
||||
|
||||
Ordered, dependency-ordered plan. Each task has a spec in `tasks/`: `001_calc_and_input.md`, `002_numerical_tests.md`, `003_typst_sheet.md`, `004_docs_refresh.md`, `005_review.md`.
|
||||
|
||||
| # | Task | Files | Status | Depends on |
|
||||
|---|---|---|---|---|
|
||||
| 001 | Numerical core: `calc.py` + `input.yaml` + `results.json` | calcs/concentric-footing/calc.py, calcs/concentric-footing/input.yaml, calcs/concentric-footing/results.json | DONE | — |
|
||||
| 002 | Numerical lock: `test_concentric_footing.py` (compute() only) | calcs/concentric-footing/test_concentric_footing.py | DONE | 001 |
|
||||
| 003 | Presentation: `footing.typ` + compile + Typst metadata tests | calcs/concentric-footing/footing.typ, calcs/concentric-footing/generated/footing.pdf, calcs/concentric-footing/test_concentric_footing.py (append) | DONE | 001, 002 |
|
||||
| 004 | Documentation refresh: `README.md` + `codemap.md` | README.md (at worksheets/calcs/wood-joist/README.md and/or worksheets root), codemap.md | DONE | 003 |
|
||||
| 005 | Reviewer pass on the whole calculation | review notes only | DONE | 004 |
|
||||
|
||||
## Notes
|
||||
|
||||
- Task 001 is the numerical single source of truth; it pins every `results.json` value name and the exact ACI 318-19 equations. Tasks 002-005 depend on that contract.
|
||||
- The Typst metadata tests (soil/structural reconciliation) live with Task 003 because they need `footing.typ` to exist; Task 002 is purely numerical to keep the dependency graph acyclic.
|
||||
- `generated/` may not exist yet; Task 003 creates it before compiling.
|
||||
- All tasks use hybrid Pint pattern: quoted quantity strings in YAML, `quantity()` conversion in Python, values+checks contract, presentation-only Typst.
|
||||
- Load determination is derived in Typst (`DLr`, `LLr`, `Br`, `Lr`, `Ar`, column weight) and emitted as `<concentric-footing-loads>`; Python reads checked `Ps`/`Pu`. The suite reconciles them.
|
||||
BIN
concentric-footing/assets/logo.png
Executable file
BIN
concentric-footing/assets/logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
82
concentric-footing/assets/sheet.typ
Normal file
82
concentric-footing/assets/sheet.typ
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#let navy = rgb("#1a3a5f")
|
||||
#let muted = rgb("#626b73")
|
||||
#let pass = rgb("#1f6b45")
|
||||
#let fail = rgb("#9b2c2c")
|
||||
|
||||
#let calcsheet(
|
||||
title: "Structural Calculation",
|
||||
project: "",
|
||||
prepared-by: "",
|
||||
body,
|
||||
) = {
|
||||
set document(title: title, author: prepared-by)
|
||||
set page(
|
||||
paper: "us-letter",
|
||||
margin: (x: 1in, top: 1.25in, bottom: 1in),
|
||||
header: context {
|
||||
grid(
|
||||
columns: (1fr, 1fr),
|
||||
align: (left, right),
|
||||
image("../assets/logo.png", height: 30pt),
|
||||
[#text(size: 9pt)[Project:] \
|
||||
#text(size: 10pt, weight: "bold")[#project]],
|
||||
)
|
||||
},
|
||||
footer: context {
|
||||
set text(size: 8.5pt, fill: muted)
|
||||
stack(
|
||||
spacing: 4pt,
|
||||
line(length: 100%, stroke: 0.5pt + muted),
|
||||
[#prepared-by],
|
||||
)
|
||||
},
|
||||
)
|
||||
set text(font: "Libertinus Serif", size: 10pt, lang: "en")
|
||||
set par(justify: true)
|
||||
set heading(numbering: none)
|
||||
show heading.where(level: 1): set text(size: 14pt, weight: "bold", fill: black)
|
||||
show heading.where(level: 2): set text(size: 11pt, weight: "bold", fill: black)
|
||||
show heading.where(level: 2): set block(above: 2em, below: 1em)
|
||||
body
|
||||
}
|
||||
|
||||
#let calcline(formula, note) = grid(
|
||||
columns: (1.7fr, 1fr),
|
||||
gutter: 4pt,
|
||||
align: (left, left),
|
||||
formula, text(size: 9pt, fill: muted, note),
|
||||
)
|
||||
|
||||
#let check(label, demand, capacity, unit: "", ok: auto, demand-label: "Demand", capacity-label: "Capacity") = {
|
||||
let utilization = demand / capacity
|
||||
let passes = if ok == auto { utilization <= 1 } else { ok }
|
||||
let color = if passes { pass } else { fail }
|
||||
block(
|
||||
breakable: false,
|
||||
width: 100%,
|
||||
stroke: 0.8pt + black,
|
||||
inset: 8pt,
|
||||
radius: 2pt,
|
||||
)[
|
||||
#grid(
|
||||
columns: (1fr, auto),
|
||||
[#text(weight: "bold")[#label]],
|
||||
box(stroke: 0.8pt + color, inset: (x: 6pt, y: 2pt))[
|
||||
#text(weight: "bold", fill: color)[#if passes { "OK" } else { "NOT OK" }]
|
||||
],
|
||||
)
|
||||
#v(4pt)
|
||||
#grid(
|
||||
columns: (1fr, auto),
|
||||
[
|
||||
#demand-label: #calc.round(demand, digits: 2) #unit #h(14pt)
|
||||
#capacity-label: #calc.round(capacity, digits: 2) #unit
|
||||
],
|
||||
[
|
||||
D/C: #calc.round(utilization, digits: 2)
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
222
concentric-footing/calc.py
Normal file
222
concentric-footing/calc.py
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
raise SystemExit("Install dependencies: python -m pip install -r requirements.txt")
|
||||
|
||||
try:
|
||||
from pint import DimensionalityError, UndefinedUnitError, UnitRegistry
|
||||
except ImportError:
|
||||
raise SystemExit("Install dependencies: python -m pip install -r requirements.txt")
|
||||
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
ureg = UnitRegistry()
|
||||
ureg.define("kip = 1000 * force_pound")
|
||||
ureg.define("ksi = kip / inch ** 2")
|
||||
ureg.define("psf = force_pound / foot ** 2")
|
||||
ureg.define("pcf = force_pound / foot ** 3")
|
||||
ureg.define("plf = force_pound / foot")
|
||||
if "psi" not in ureg:
|
||||
ureg.define("psi = force_pound / inch ** 2")
|
||||
|
||||
|
||||
def quantity(value, unit: str, name: str) -> float:
|
||||
try:
|
||||
q = ureg.Quantity(value).to(unit)
|
||||
except (DimensionalityError, UndefinedUnitError, TypeError, ValueError) as exc:
|
||||
raise ValueError(f"{name}: expected {unit}, got {value!r}") from exc
|
||||
magnitude = float(q.magnitude)
|
||||
if magnitude <= 0:
|
||||
raise ValueError(f"{name} must be positive")
|
||||
return magnitude
|
||||
|
||||
|
||||
def compute(inp: dict) -> dict:
|
||||
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")
|
||||
fy_ksi = fy_psi / 1000.0
|
||||
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"])
|
||||
|
||||
if not (0 < lambda_f <= 1):
|
||||
raise ValueError("lambda must be in (0, 1]")
|
||||
if N < 1:
|
||||
raise ValueError("N must be >= 1")
|
||||
if not (3 <= rebar_size <= 18):
|
||||
raise ValueError("rebar_size must be an integer in [3, 18]")
|
||||
|
||||
d_in = Df_in - cover_in
|
||||
if d_in <= 0:
|
||||
raise ValueError("d = Df - cover must be positive")
|
||||
|
||||
Bf_in = Bf_ft * 12.0
|
||||
Af_ft2 = Bf_ft ** 2
|
||||
Af_in2 = Af_ft2 * 144.0
|
||||
|
||||
# Derived rebar properties
|
||||
db_in = rebar_size / 8.0
|
||||
As1_in2 = math.pi * db_in ** 2 / 4.0
|
||||
As_in2 = N * As1_in2
|
||||
rho = As_in2 / (Bf_in * d_in)
|
||||
rho_min = 0.0018
|
||||
|
||||
# Pressures
|
||||
Ps_lbf = Ps_kip * 1000.0
|
||||
Pu_lbf = Pu_kip * 1000.0
|
||||
q_psf = Ps_lbf / Af_ft2
|
||||
qu_psf = Pu_lbf / Af_ft2
|
||||
|
||||
# One-way shear — ACI 22.5.5, phi=0.75
|
||||
L1_in = (Bf_in - c_in) / 2.0 - d_in
|
||||
if L1_in <= 0:
|
||||
Vu_one_kip = 0.0
|
||||
else:
|
||||
Vu_one_lbf = qu_psf * Bf_ft * (L1_in / 12.0)
|
||||
Vu_one_kip = Vu_one_lbf / 1000.0
|
||||
Vc_one_lbf = 2.0 * lambda_f * math.sqrt(fc_psi) * Bf_in * d_in
|
||||
Vc_one_kip = Vc_one_lbf / 1000.0
|
||||
phiVc_one_kip = 0.75 * Vc_one_kip
|
||||
|
||||
# Two-way (punching) shear — ACI 22.6.5, phi=0.75
|
||||
bo_in = 4.0 * (c_in + d_in)
|
||||
beta = 1.0
|
||||
alpha_s = 40.0
|
||||
vc1 = 4.0 * lambda_f * math.sqrt(fc_psi)
|
||||
vc2 = (2.0 + 4.0 / beta) * lambda_f * math.sqrt(fc_psi)
|
||||
vc3 = (2.0 + alpha_s * d_in / bo_in) * lambda_f * math.sqrt(fc_psi)
|
||||
vc_psi = min(vc1, vc2, vc3)
|
||||
Vc_two_lbf = vc_psi * bo_in * d_in
|
||||
Vc_two_kip = Vc_two_lbf / 1000.0
|
||||
phiVn_two_kip = 0.75 * Vc_two_kip
|
||||
Apunch_in2 = (c_in + d_in) ** 2
|
||||
Apunch_ft2 = Apunch_in2 / 144.0
|
||||
Vu_two_lbf = qu_psf * (Af_ft2 - Apunch_ft2)
|
||||
Vu_two_kip = Vu_two_lbf / 1000.0
|
||||
|
||||
# Flexure — ACI 22.5 / 7, phi=0.90
|
||||
Lc_in = (Bf_in - c_in) / 2.0
|
||||
Lc_ft = Lc_in / 12.0
|
||||
Mu_kipft = qu_psf * Bf_ft * Lc_ft ** 2 / 2.0 / 1000.0
|
||||
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.0, (fc_psi - 4000.0) / 1000.0)))
|
||||
Mn_kipft = As_in2 * fy_ksi * (d_in - a_in / 2.0) / 12.0
|
||||
phiMn_kipft = 0.90 * Mn_kipft
|
||||
|
||||
# Concrete bearing — ACI 22.8, phi=0.65
|
||||
A1_in2 = bp_in ** 2
|
||||
A2_in2 = Af_in2
|
||||
sqrt_ratio = math.sqrt(A2_in2 / A1_in2)
|
||||
sqrt_ratio_capped = min(sqrt_ratio, 2.0)
|
||||
Bn_lbf = 0.85 * fc_psi * A1_in2 * sqrt_ratio_capped
|
||||
Bn_kip = Bn_lbf / 1000.0
|
||||
phiBn_kip = 0.65 * Bn_kip
|
||||
|
||||
def q(value: float) -> float:
|
||||
return round(value, 6)
|
||||
|
||||
values = {
|
||||
"Ps_kip": q(Ps_kip),
|
||||
"Pu_kip": q(Pu_kip),
|
||||
"qa_psf": q(qa_psf),
|
||||
"q_psf": q(q_psf),
|
||||
"qu_psf": q(qu_psf),
|
||||
"Af_ft2": q(Af_ft2),
|
||||
"Bf_in": q(Bf_in),
|
||||
"Bf_ft": q(Bf_ft),
|
||||
"Df_in": q(Df_in),
|
||||
"cover_in": q(cover_in),
|
||||
"d_in": q(d_in),
|
||||
"fc_psi": q(fc_psi),
|
||||
"fy_psi": q(fy_psi),
|
||||
"fy_ksi": q(fy_ksi),
|
||||
"lambda": q(lambda_f),
|
||||
"c_in": q(c_in),
|
||||
"bp_in": q(bp_in),
|
||||
"N": N,
|
||||
"rebar_size": rebar_size,
|
||||
"db_in": q(db_in),
|
||||
"As1_in2": q(As1_in2),
|
||||
"As_in2": q(As_in2),
|
||||
"rho": q(rho),
|
||||
"rho_min": q(rho_min),
|
||||
"L1_in": q(L1_in),
|
||||
"Vu_one_way_kip": q(Vu_one_kip),
|
||||
"Vc_one_way_kip": q(Vc_one_kip),
|
||||
"phiVc_one_way_kip": q(phiVc_one_kip),
|
||||
"bo_in": q(bo_in),
|
||||
"vc_psi": q(vc_psi),
|
||||
"Vu_two_way_kip": q(Vu_two_kip),
|
||||
"Vc_two_way_kip": q(Vc_two_kip),
|
||||
"phiVn_two_way_kip": q(phiVn_two_kip),
|
||||
"Lc_in": q(Lc_in),
|
||||
"Mu_kipft": q(Mu_kipft),
|
||||
"a_in": q(a_in),
|
||||
"beta1": q(beta1),
|
||||
"Mn_kipft": q(Mn_kipft),
|
||||
"phiMn_kipft": q(phiMn_kipft),
|
||||
"A1_in2": q(A1_in2),
|
||||
"A2_in2": q(A2_in2),
|
||||
"sqrt_ratio": q(sqrt_ratio),
|
||||
"Bn_kip": q(Bn_kip),
|
||||
"phiBn_kip": q(phiBn_kip),
|
||||
}
|
||||
|
||||
checks = {
|
||||
"soil_bearing": {"demand": q(q_psf), "capacity": q(qa_psf), "ok": q_psf <= qa_psf},
|
||||
"one_way_shear": {"demand": q(Vu_one_kip), "capacity": q(phiVc_one_kip), "ok": Vu_one_kip <= phiVc_one_kip},
|
||||
"two_way_shear": {"demand": q(Vu_two_kip), "capacity": q(phiVn_two_kip), "ok": Vu_two_kip <= phiVn_two_kip},
|
||||
"flexure": {"demand": q(Mu_kipft), "capacity": q(phiMn_kipft), "ok": Mu_kipft <= phiMn_kipft},
|
||||
"minimum_steel": {"demand": q(rho_min), "capacity": q(rho), "ok": rho >= rho_min},
|
||||
"bearing": {"demand": q(Pu_kip), "capacity": q(phiBn_kip), "ok": Pu_kip <= phiBn_kip},
|
||||
}
|
||||
|
||||
return {
|
||||
"tool": "concentric_footing",
|
||||
"version": "0.1",
|
||||
"project": inp.get("project", ""),
|
||||
"prepared_by": inp.get("prepared_by", ""),
|
||||
"values": values,
|
||||
"checks": checks,
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Calculate the concentric footing design from a YAML input file.")
|
||||
parser.add_argument("--input", "-i", type=Path, default=HERE / "input.yaml", help="YAML input path")
|
||||
parser.add_argument("--output", "-o", type=Path, help="JSON output path; defaults beside the input")
|
||||
parser.add_argument("--stdout", action="store_true", help="Write the complete JSON result to stdout instead of a file")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
input_path = args.input
|
||||
output_path = args.output or input_path.with_name("results.json")
|
||||
with input_path.open(encoding="utf-8") as handle:
|
||||
result = compute(yaml.safe_load(handle))
|
||||
serialized = json.dumps(result, indent=2) + "\n"
|
||||
if args.stdout:
|
||||
sys.stdout.write(serialized)
|
||||
else:
|
||||
output_path.write_text(serialized, encoding="utf-8")
|
||||
print(output_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
41
concentric-footing/codemap.md
Normal file
41
concentric-footing/codemap.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Codemap: concentric-footing
|
||||
|
||||
Hybrid (Typst + Python) structural calculation that checks a square, concentrically loaded reinforced-concrete spread footing per ACI 318-19: soil bearing, one-way shear, two-way (punching) shear, flexure, minimum steel, and concrete bearing. `calc.py` is the single source of truth; `footing.typ` presents only.
|
||||
|
||||
Generated: 2026-08-21
|
||||
Files indexed: 13 (all deliverables present)
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
calcs/concentric-footing/
|
||||
├── calc.py [logic] — ACI 318-19 footing checks; Pint YAML → compute() → results.json; CLI --input/--output/--stdout.
|
||||
├── input.yaml [config] — Pint-quoted quantities; Blavatnik defaults (Ps, Pu, qa, Bf, Df, cover, fc, fy, lambda, column_width, base_plate_width, N, rebar_size).
|
||||
├── results.json [state] — last calc output: {tool, version, project, prepared_by, values, checks}.
|
||||
├── test_concentric_footing.py [test] — pytest lock of corrected ACI 318-19 benchmark (12 tests); imports calc.py:compute via importlib.
|
||||
├── footing.typ [logic] — Typst presentation sheet; imports lib/sheet.typ + results.json, derives loads, emits <concentric-footing-loads>/<concentric-footing-results>.
|
||||
├── generated/footing.pdf [doc] — compiled artefact (~204 KB).
|
||||
├── CONCENTRIC-FOOTING.pdf [doc] — reference: Blavatnik square footing 36in×36in×12in, 3000 psi, 4-#4 (contains known 68in vs 92in perimeter typo).
|
||||
├── PROJECT_STATE.md [doc] — architecture, pinned equations, units, tolerances, corrected benchmark, known limitations.
|
||||
├── TASKS.md [doc] — 5-task plan (001 numerical core, 002 pytest lock, 003 Typst sheet, 004 docs, 005 review).
|
||||
└── tasks/
|
||||
├── 001_calc_and_input.md [doc] — numerical core spec (calc.py + input.yaml + results.json).
|
||||
├── 002_numerical_tests.md [doc] — pytest lock spec (test_concentric_footing.py, compute() only).
|
||||
├── 003_typst_sheet.md [doc] — Typst presentation spec (footing.typ + compile + metadata query tests).
|
||||
├── 004_docs_refresh.md [doc] — README + codemap refresh spec.
|
||||
└── 005_review.md [doc] — reviewer pass spec (engineering + deterministic evidence + simplify).
|
||||
```
|
||||
|
||||
(footing.typ and generated/footing.pdf are now present on disk — see layout above.)
|
||||
|
||||
## Hot Spots
|
||||
|
||||
- `calcs/concentric-footing/calc.py` — single source of truth for all six ACI 318-19 checks; any equation change must be reconciled with `test_concentric_footing.py` and the benchmark in `PROJECT_STATE.md`.
|
||||
- `calcs/concentric-footing/PROJECT_STATE.md` — pins every equation, unit, tolerance, and the corrected benchmark; the builder must not invent behavior beyond it.
|
||||
- `lib/sheet.typ` (in worksheets root, external) — shared `calc-line` / `check` helpers used by `footing.typ`; changing them breaks every sheet's PDF.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Hybrid Pint pattern: `input.yaml` (quoted quantity strings) → `calc.py:compute()` → `results.json` {tool, version, project, prepared_by, values, checks} → `footing.typ` (presents only, no recomputation) → PDF compiled with `--root .` from `worksheets/`.
|
||||
- Load determination (DL/LL, tributary area, column self weight) is derived in Typst and emitted as `<concentric-footing-loads>`; Python reads checked `Ps`/`Pu`. The test suite reconciles them.
|
||||
- Pin one hand-calculated example in `pytest.approx` before treating the tool as stable; correct reference typos in the Scope note rather than reproducing them.
|
||||
1964
concentric-footing/concentric-footing.pdf
Normal file
1964
concentric-footing/concentric-footing.pdf
Normal file
File diff suppressed because it is too large
Load diff
110
concentric-footing/concentric-footing.typ
Normal file
110
concentric-footing/concentric-footing.typ
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#import "assets/sheet.typ": calcline, calcsheet, check
|
||||
|
||||
#let data = json("results.json")
|
||||
#let n = data.values
|
||||
#let checks = data.checks
|
||||
#let round(value, digits: 2) = calc.round(value, digits: digits)
|
||||
|
||||
#show: calcsheet.with(
|
||||
title: "Concentric Footing Analysis",
|
||||
project: data.project,
|
||||
prepared-by: data.prepared_by,
|
||||
)
|
||||
|
||||
= Concentric Footing Analysis
|
||||
|
||||
Square spread footing under concentric axial load, ACI 318-19. Numbers come from
|
||||
`calc.py`; this sheet only presents them. Typst derives gravity loads below;
|
||||
checked Python demands are reconciled by the test suite.
|
||||
|
||||
#figure(
|
||||
align(center)[
|
||||
#box(width: 160pt, height: 130pt)[
|
||||
#place(rect(width: 120pt, height: 120pt, stroke: 1pt))
|
||||
#place(dx: 45pt, dy: 45pt, rect(width: 30pt, height: 30pt, fill: rgb("#cccccc"), stroke: 0.8pt))
|
||||
#place(dx: 55pt, dy: 2pt, text(size: 8pt)[$B_f$])
|
||||
#place(dx: 124pt, dy: 55pt, text(size: 8pt)[$B_f$])
|
||||
#place(dx: 56pt, dy: 56pt, text(size: 7pt)[$c$])
|
||||
]
|
||||
],
|
||||
caption: [Footing plan and section: #n.Bf_ft ft × #n.Bf_ft ft × #n.Df_in in, d=#n.d_in in.],
|
||||
)
|
||||
|
||||
== Loads Determination
|
||||
|
||||
#let DLr = 10 // psf
|
||||
#let LLr = 20 // psf
|
||||
#let Br = 18.9 // ft
|
||||
#let Lr = 27.5 // ft
|
||||
#let Ar = Br * Lr // ft2
|
||||
#let bc = 14 // in
|
||||
#let Lc = 14 // ft
|
||||
#let gamma_c = 145 // pcf
|
||||
#let Wc_kip = bc * bc / 144 * Lc * gamma_c / 1000 // kip column weight
|
||||
#let Ps_typst = (DLr + LLr) * Ar / 1000 + Wc_kip // kip
|
||||
#let Pu_typst = 1.2*(DLr*Ar/1000 + Wc_kip) + 1.6*LLr*Ar/1000
|
||||
|
||||
#metadata((Ps_kip: Ps_typst, Pu_kip: Pu_typst, Ar_ft2: Ar, Wc_kip: Wc_kip)) <concentric-footing-loads>
|
||||
|
||||
#calcline([$A_r = B_r L_r = #round(Ar, digits: 2) " ft"^2$], [Tributary area])
|
||||
#calcline([$W_c = b_c b_c L_c gamma_c = #round(Wc_kip, digits: 2) " kip"$], [Column self weight])
|
||||
#calcline([$P_s = (D_L_r + L_L_r) A_r + W_c = #round(Ps_typst, digits: 2) " kip"$], [Typst-derived service load])
|
||||
#calcline([$P_(s,"checked") = #round(n.Ps_kip, digits: 2) " kip"$], [Python-checked service load])
|
||||
#calcline([$P_u = 1.2(D_L_r A_r + W_c) + 1.6 L_L_r A_r = #round(Pu_typst, digits: 2) " kip"$], [Typst-derived factored load])
|
||||
#calcline([$P_(u,"checked") = #round(n.Pu_kip, digits: 2) " kip"$], [Factored axial load])
|
||||
#calcline([$q_u = P_u / A_f = #round(n.qu_psf, digits: 1) " psf"$], [Factored gross pressure])
|
||||
|
||||
== Geometry and Materials
|
||||
|
||||
#calcline([$B_f = #n.Bf_ft " ft"$, $A_f = #n.Af_ft2 " ft"^2$], [Footing plan dimensions])
|
||||
#calcline([$D_f = #n.Df_in " in"$, $"cover" = #n.cover_in " in"$, $d = D_f - "cover" = #n.d_in " in"$], [Effective depth])
|
||||
#calcline([$c = #n.c_in " in"$, $b_p = #n.bp_in " in"$], [Column and base-plate widths])
|
||||
#calcline([$f'_c = #n.fc_psi " psi"$, $f_y = #n.fy_ksi " ksi"$, $lambda = #n.lambda$], [Concrete and steel])
|
||||
#calcline([$N = #n.N$, #("#" + str(n.rebar_size) + " bars"), $A_(s,1) = #round(n.As1_in2, digits: 4) " in"^2$, $A_s = #round(n.As_in2, digits: 4) " in"^2$], [Reinforcement per direction])
|
||||
#calcline([$rho = A_s/(B_f d) = #round(n.rho, digits: 4)$, $rho_min = #n.rho_min$], [Reinforcement ratio])
|
||||
|
||||
== Soil Bearing
|
||||
|
||||
#calcline([$q = P_s/A_f = #round(n.q_psf, digits: 1) " psf"$], [Acting service pressure])
|
||||
#calcline([$q_a = #n.qa_psf " psf"$], [Allowable gross pressure])
|
||||
|
||||
#check("Soil bearing", checks.soil_bearing.demand, checks.soil_bearing.capacity, unit: "psf", ok: checks.soil_bearing.ok, demand-label: [$q$], capacity-label: [$q_a$])
|
||||
|
||||
== One-Way Shear
|
||||
|
||||
#calcline([$L_1 = (B_f - c)/2 - d = #round(n.L1_in, digits: 2) " in"$], [Cantilever beyond d])
|
||||
#calcline([$V_u = q_u B_f L_1 = #round(n.Vu_one_way_kip, digits: 2) " kip"$], [Demand at d])
|
||||
#calcline([$V_c = 2 lambda sqrt(f'_c) B_f d = #round(n.Vc_one_way_kip, digits: 1) " kip"$], [ACI 22.5])
|
||||
#calcline([$phi V_c = #round(n.phiVc_one_way_kip, digits: 1) " kip"$], [phi=0.75])
|
||||
|
||||
#check("One-way shear", checks.one_way_shear.demand, checks.one_way_shear.capacity, unit: "kip", ok: checks.one_way_shear.ok, demand-label: [$V_u$], capacity-label: [$phi V_c$])
|
||||
|
||||
== Two-Way Shear (Punching)
|
||||
|
||||
#calcline([$b_o = 4(c+d) = #n.bo_in " in"$], [Critical perimeter at d/2])
|
||||
#calcline([$v_c = min(4, 2+4/beta, 2+alpha_s d/b_o) lambda sqrt(f'_c) = #round(n.vc_psi, digits: 1) " psi"$], [ACI 22.6])
|
||||
#calcline([$V_c = v_c b_o d = #round(n.Vc_two_way_kip, digits: 1) " kip"$], [Concrete shear strength])
|
||||
#calcline([$V_u = q_u (A_f - (c+d)^2) = #round(n.Vu_two_way_kip, digits: 1) " kip"$], [Punch demand])
|
||||
|
||||
#check("Two-way shear", checks.two_way_shear.demand, checks.two_way_shear.capacity, unit: "kip", ok: checks.two_way_shear.ok, demand-label: [$V_u$], capacity-label: [$phi V_c$])
|
||||
|
||||
== Flexure
|
||||
|
||||
#calcline([$L_c = (B_f - c)/2 = #round(n.Lc_in, digits: 1) " in"$], [Cantilever])
|
||||
#calcline([$M_u = q_u B_f L_c^2/2 = #round(n.Mu_kipft, digits: 2) " kip·ft"$], [Demand])
|
||||
#calcline([$a = A_s f_y/(0.85 f'_c B_f) = #round(n.a_in, digits: 3) " in"$], [Whitney stress block])
|
||||
#calcline([$M_n = A_s f_y (d - a/2) = #round(n.Mn_kipft, digits: 1) " kip·ft"$], [Nominal moment strength])
|
||||
#calcline([$phi M_n = #round(n.phiMn_kipft, digits: 1) " kip·ft"$], [phi=0.90])
|
||||
#calcline([$rho = A_s/(B_f d) = #round(n.rho, digits: 4)$], [vs rho_min 0.0018])
|
||||
|
||||
#check("Flexure", checks.flexure.demand, checks.flexure.capacity, unit: "kip·ft", ok: checks.flexure.ok, demand-label: [$M_u$], capacity-label: [$phi M_n$])
|
||||
#check("Minimum steel", checks.minimum_steel.demand, checks.minimum_steel.capacity, unit: "", ok: checks.minimum_steel.ok, demand-label: [$rho$], capacity-label: [$rho_min$])
|
||||
|
||||
== Concrete Bearing
|
||||
|
||||
#calcline([$A_1 = b_p^2 = #round(n.A1_in2, digits: 1) " in"^2$, $A_2 = B_f^2 = #round(n.A2_in2, digits: 1) " in"^2$], [Plate and footing])
|
||||
#calcline([$sqrt(A_2/A_1) = #round(n.sqrt_ratio, digits: 2)$], [Uncapped ratio, capped at 2.0 for strength])
|
||||
#calcline([$B_n = 0.85 f'_c A_1 sqrt(...) = #round(n.Bn_kip, digits: 1) " kip"$], [ACI 22.8])
|
||||
#calcline([$phi B_n = #round(n.phiBn_kip, digits: 1) " kip"$], [phi=0.65])
|
||||
|
||||
#check("Concrete bearing", checks.bearing.demand, checks.bearing.capacity, unit: "kip", ok: checks.bearing.ok, demand-label: [$P_u$], capacity-label: [$phi B_n$])
|
||||
19
concentric-footing/input.yaml
Normal file
19
concentric-footing/input.yaml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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
|
||||
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.
|
||||
84
concentric-footing/results.json
Normal file
84
concentric-footing/results.json
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"tool": "concentric_footing",
|
||||
"version": "0.1",
|
||||
"project": "Blavatnik",
|
||||
"prepared_by": "Conemco Engineering",
|
||||
"values": {
|
||||
"Ps_kip": 18.4,
|
||||
"Pu_kip": 26.2,
|
||||
"qa_psf": 2500.0,
|
||||
"q_psf": 2044.444444,
|
||||
"qu_psf": 2911.111111,
|
||||
"Af_ft2": 9.0,
|
||||
"Bf_in": 36.0,
|
||||
"Bf_ft": 3.0,
|
||||
"Df_in": 12.0,
|
||||
"cover_in": 3.0,
|
||||
"d_in": 9.0,
|
||||
"fc_psi": 3000.0,
|
||||
"fy_psi": 60000.0,
|
||||
"fy_ksi": 60.0,
|
||||
"lambda": 1.0,
|
||||
"c_in": 14.0,
|
||||
"bp_in": 6.0,
|
||||
"N": 4,
|
||||
"rebar_size": 4,
|
||||
"db_in": 0.5,
|
||||
"As1_in2": 0.19635,
|
||||
"As_in2": 0.785398,
|
||||
"rho": 0.002424,
|
||||
"rho_min": 0.0018,
|
||||
"L1_in": 2.0,
|
||||
"Vu_one_way_kip": 1.455556,
|
||||
"Vc_one_way_kip": 35.492422,
|
||||
"phiVc_one_way_kip": 26.619316,
|
||||
"bo_in": 92.0,
|
||||
"vc_psi": 219.089023,
|
||||
"Vu_two_way_kip": 15.50571,
|
||||
"Vc_two_way_kip": 181.405711,
|
||||
"phiVn_two_way_kip": 136.054283,
|
||||
"Lc_in": 11.0,
|
||||
"Mu_kipft": 3.669213,
|
||||
"a_in": 0.513332,
|
||||
"beta1": 0.85,
|
||||
"Mn_kipft": 34.334992,
|
||||
"phiMn_kipft": 30.901493,
|
||||
"A1_in2": 36.0,
|
||||
"A2_in2": 1296.0,
|
||||
"sqrt_ratio": 6.0,
|
||||
"Bn_kip": 183.6,
|
||||
"phiBn_kip": 119.34
|
||||
},
|
||||
"checks": {
|
||||
"soil_bearing": {
|
||||
"demand": 2044.444444,
|
||||
"capacity": 2500.0,
|
||||
"ok": true
|
||||
},
|
||||
"one_way_shear": {
|
||||
"demand": 1.455556,
|
||||
"capacity": 26.619316,
|
||||
"ok": true
|
||||
},
|
||||
"two_way_shear": {
|
||||
"demand": 15.50571,
|
||||
"capacity": 136.054283,
|
||||
"ok": true
|
||||
},
|
||||
"flexure": {
|
||||
"demand": 3.669213,
|
||||
"capacity": 30.901493,
|
||||
"ok": true
|
||||
},
|
||||
"minimum_steel": {
|
||||
"demand": 0.0018,
|
||||
"capacity": 0.002424,
|
||||
"ok": true
|
||||
},
|
||||
"bearing": {
|
||||
"demand": 26.2,
|
||||
"capacity": 119.34,
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
}
|
||||
131
concentric-footing/tasks/001_calc_and_input.md
Normal file
131
concentric-footing/tasks/001_calc_and_input.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# 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.
|
||||
110
concentric-footing/tasks/002_numerical_tests.md
Normal file
110
concentric-footing/tasks/002_numerical_tests.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# 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.
|
||||
163
concentric-footing/tasks/003_typst_sheet.md
Normal file
163
concentric-footing/tasks/003_typst_sheet.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Task 003 — Presentation: footing.typ + compile + Typst metadata tests
|
||||
|
||||
## Goal
|
||||
|
||||
Present the checked numbers in a printable Typst sheet that also derives the Blavatnik gravity loads inline, emits queryable metadata, and compiles to PDF.
|
||||
|
||||
## Background
|
||||
|
||||
Architecture: `footing.typ` imports `../../lib/sheet.typ` and `json("results.json")`. It must not recompute capacities; it only presents `results.json` values via `calc-line` and `check` boxes. Load determination (roof DL/LL, tributary area, column self weight) is derived in Typst and reconciled to Python's checked `Ps`/`Pu` by the test suite — mirroring `wood-joist` and `steel-beam` where `<wood-joist-loads>` is queried.
|
||||
|
||||
The `check`, `calc-line`, and `calculation-sheet` helpers are defined in `lib/sheet.typ` (worksheets root). Signatures used here: `check(label, demand, capacity, unit:, ok:)`, `calc-line(content, note)`, and `calculation-sheet.with(title:, project:, prepared-by:)`.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
- `calcs/concentric-footing/footing.typ` — create.
|
||||
- `calcs/concentric-footing/generated/footing.pdf` — create via `typst compile --root .`.
|
||||
- `calcs/concentric-footing/test_concentric_footing.py` — append two Typst query tests (do not rewrite existing tests); create `generated/` dir if needed.
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. footing.typ structure
|
||||
|
||||
Follow `calcs/wood-joist/beam.typ` as template:
|
||||
|
||||
- Header:
|
||||
```
|
||||
#import "../../lib/sheet.typ": calc-line, calculation-sheet, check
|
||||
#let data = json("results.json")
|
||||
#let n = data.values
|
||||
#let checks = data.checks
|
||||
#let round(value, digits: 2) = calc.round(value, digits: digits)
|
||||
#show: calculation-sheet.with(title: "Concentric Footing Analysis", project: data.project, prepared-by: data.prepared_by)
|
||||
= Concentric Footing Analysis
|
||||
Square spread footing under concentric axial load, ACI 318-19. Numbers come from `calc.py`; this sheet only presents them. Typst derives gravity loads below; checked Python demands are reconciled by the test suite.
|
||||
```
|
||||
|
||||
- Add a simple footing sketch using Typst `box`/`rect`/`line` drawing (no external image needed). Example: centered square footing with column on top, label `Bf` and `Df`. Wrap in `#figure(..., caption: [Footing plan and section: #n.Bf_ft ft × #n.Bf_ft ft × #n.Df_in in, d=#n.d_in in.])`. Keep geometry text as `calc-line` below.
|
||||
|
||||
- Section `== Loads Determination`:
|
||||
Derive Blavatnik loads in Typst (visible arithmetic):
|
||||
```
|
||||
#let DLr = 10 // psf
|
||||
#let LLr = 20 // psf
|
||||
#let Br = 18.9 // ft
|
||||
#let Lr = 27.5 // ft
|
||||
#let Ar = Br * Lr // ft2
|
||||
#let bc = 14 // in
|
||||
#let Lc = 14 // ft
|
||||
#let gamma_c = 145 // pcf
|
||||
#let Wc_kip = bc * bc / 144 * Lc * gamma_c / 1000 // kip column weight
|
||||
#let Ps_typst = (DLr + LLr) * Ar / 1000 + Wc_kip // kip
|
||||
#let Pu_typst = 1.2*(DLr*Ar/1000 + Wc_kip) + 1.6*LLr*Ar/1000
|
||||
#metadata((Ps_kip: Ps_typst, Pu_kip: Pu_typst, Ar_ft2: Ar, Wc_kip: Wc_kip)) <concentric-footing-loads>
|
||||
```
|
||||
Then emit `calc-line` rows:
|
||||
- `Ar = Br·Lr = #round(Ar,2) ft2` (tributary area)
|
||||
- `Wc = bc·bc·Lc·γc = #round(Wc_kip,2) kip` (column self weight)
|
||||
- `Ps = (DLr+LLr)·Ar + Wc = #round(Ps_typst,2) kip (Typst-derived)`
|
||||
- `Ps_checked = #round(n.Ps_kip,2) kip (Python-checked)`
|
||||
- `Pu = 1.2(DLr·Ar+Wc)+1.6·LLr·Ar = #round(Pu_typst,2) kip (Typst-derived)`
|
||||
- `Pu_checked = #round(n.Pu_kip,2) kip`
|
||||
- Also show `qu = Pu/Af = #round(n.qu_psf,1) psf` for context but do not recompute Af.
|
||||
|
||||
- Section `== Geometry and Materials`:
|
||||
`calc-line` for Bf, Df, cover, d, Af, c, bp, fc, fy, lambda, N, rebar_size, As, rho. Example:
|
||||
`calc-line([$B_f = #n.Bf_ft " ft"$, $A_f = #n.Af_ft2 " ft"^2$], [Footing plan area])` etc. Show As1, As, rho, rho_min.
|
||||
|
||||
- Section `== Soil Bearing`:
|
||||
`calc-line([$q = P_s/A_f = #round(n.q_psf,1) " psf"$], [Acting service pressure])`
|
||||
`calc-line([$q_a = #n.qa_psf " psf"$], [Allowable])`
|
||||
Then `#check("Soil bearing", checks.soil_bearing.demand, checks.soil_bearing.capacity, unit: "psf", ok: checks.soil_bearing.ok)`
|
||||
Also show `qu` as info line.
|
||||
|
||||
- Section `== One-Way Shear`:
|
||||
`calc-line([$L_1 = (B_f - c)/2 - d = #round(n.L1_in,2) " in"$], [Cantilever beyond d])`
|
||||
`calc-line([$V_u = q_u B_f L_1 = #round(n.Vu_one_way_kip,2) " kip"$], [Demand at d])`
|
||||
`calc-line([$V_c = 2 lambda sqrt(f'_c) B_f d = #round(n.Vc_one_way_kip,1) " kip"$], [ACI 22.5])`
|
||||
`calc-line([$phi V_c = #round(n.phiVc_one_way_kip,1) " kip"$], [phi=0.75])`
|
||||
`#check("One-way shear", checks.one_way_shear.demand, checks.one_way_shear.capacity, unit: "kip", ok: checks.one_way_shear.ok)`
|
||||
|
||||
- Section `== Two-Way Shear (Punching)`:
|
||||
`calc-line([$b_o = 4(c+d)= #n.bo_in " in"$], [Critical perimeter, d/2])`
|
||||
`calc-line([$v_c = min(4,2+4/beta,2+alpha d/b_o) lambda sqrt(f'_c)= #round(n.vc_psi,1) " psi"$], [ACI 22.6])`
|
||||
`calc-line([$V_c = v_c b_o d = #round(n.Vc_two_way_kip,1) " kip"$], [])`
|
||||
`calc-line([$V_u = q_u(A_f - (c+d)^2)= #round(n.Vu_two_way_kip,1) " kip"$], [Punch demand])`
|
||||
`#check("Two-way shear", checks.two_way_shear.demand, checks.two_way_shear.capacity, unit: "kip", ok: checks.two_way_shear.ok)`
|
||||
|
||||
- Section `== Flexure`:
|
||||
`calc-line([$L_c = (B_f - c)/2 = #round(n.Lc_in,1) " in"$], [Cantilever])`
|
||||
`calc-line([$M_u = q_u B_f L_c^2/2 = #round(n.Mu_kipft,2) " kip·ft"$], [])`
|
||||
`calc-line([$a = A_s f_y/(0.85 f'_c B_f)= #round(n.a_in,3) " in"$], [Whitney block])`
|
||||
`calc-line([$M_n = A_s f_y(d -a/2)= #round(n.Mn_kipft,1) " kip·ft"$], [])`
|
||||
`calc-line([$rho = A_s/(B_f d)= #round(n.rho,4)$], [vs rho_min 0.0018])`
|
||||
`#check("Flexure", checks.flexure.demand, checks.flexure.capacity, unit: "kip·ft", ok: checks.flexure.ok)`
|
||||
`#check("Minimum steel", checks.minimum_steel.demand, checks.minimum_steel.capacity, unit: "", ok: checks.minimum_steel.ok)` — for rho, show ratio but check helper expects demand/capacity; use demand=rho_min capacity=rho; label accordingly.
|
||||
|
||||
- Section `== Concrete Bearing`:
|
||||
`calc-line([$A_1 = b_p^2 = #round(n.A1_in2,1) " in"^2$, $A_2 = B_f^2 = #round(n.A2_in2,1) " in"^2$], [Plate and footing])`
|
||||
`calc-line([$sqrt(A_2/A_1)= #round(n.sqrt_ratio,2)$, capped at 2], [])`
|
||||
`calc-line([$B_n = 0.85 f'_c A_1 sqrt(...)= #round(n.Bn_kip,1) " kip"$], [ACI 22.8])`
|
||||
`calc-line([$phi B_n = #round(n.phiBn_kip,1) " kip"$], [phi=0.65])`
|
||||
`#check("Concrete bearing", checks.bearing.demand, checks.bearing.capacity, unit: "kip", ok: checks.bearing.ok)`
|
||||
|
||||
- Section `== Scope And Limitations`:
|
||||
Text stating square footing only, interior concentric axial only, gross pressure, no moment/overturning/sliding, no settlement, d from cover to centroid, bo correction noted (reference 68in corrected to ACI 92in), plate vs column clarification, no deflection/crack/development, final design by engineer.
|
||||
|
||||
- Footer metadata for reconciliation:
|
||||
```
|
||||
#metadata((soil_util: checks.soil_bearing.demand/checks.soil_bearing.capacity, one_way_util: checks.one_way_shear.demand/checks.one_way_shear.capacity, two_way_util: checks.two_way_shear.demand/checks.two_way_shear.capacity, flexure_util: checks.flexure.demand/checks.flexure.capacity, bearing_util: checks.bearing.demand/checks.bearing.capacity, q_psf: n.q_psf, qu_psf: n.qu_psf)) <concentric-footing-results>
|
||||
```
|
||||
|
||||
Ensure all `n.*` keys match Task 001 values dict exactly.
|
||||
|
||||
### 2. Compile
|
||||
|
||||
From `worksheets/` run:
|
||||
```
|
||||
python calcs/concentric-footing/calc.py
|
||||
typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf
|
||||
```
|
||||
Verify PDF exists and >50KB.
|
||||
|
||||
### 3. Append Typst query tests
|
||||
|
||||
Append to `calcs/concentric-footing/test_concentric_footing.py` two new tests (do not delete existing 10):
|
||||
|
||||
- `test_typst_compiles_and_presents_python_numbers(result)` — runs `calc.py` with `subprocess.run([sys.executable, str(HERE/"calc.py")], check=True, cwd=HERE.parents[1])`, then `typst compile ...` check pdf exists, then `typst query` for `<concentric-footing-results>` via `typst eval "query(<concentric-footing-results>)"`? Actually use `typst eval` or `typst query` command as in wood-joist: `subprocess.run(["typst","query", ...])`? Wood-joist uses `typst compile` and `typst eval query`. Use same pattern as wood-joist test:
|
||||
```
|
||||
subprocess.run(["typst","compile","--root",".",...], check=True, cwd=HERE.parents[1])
|
||||
query = subprocess.run(["typst","query","..."]?)
|
||||
```
|
||||
Check wood-joist: it runs `["typst","eval","query(<wood-joist-results>)","--root",...,"--in","calcs/wood-joist/beam.typ","--format","json"]`. Mirror that: `["typst","query", str(HERE/"footing.typ"), "<concentric-footing-results>", "--root", "."]` may vary; adapt to whatever works but assert exactly one entry and that flexure_util approx equals Python f. Specifically query the published metadata and assert `meta["flexure_util"] approx values["Mu"]/values["phiMn"]` etc for soil, one-way, two-way, bearing.
|
||||
|
||||
- `test_typst_load_demands_match_checked_inputs(result)` — queries `<concentric-footing-loads>` and asserts `derived Ps_kip approx values["Ps_kip"]` within 0.1 kip (allow rounding of column weight 18.36 vs 18.4) and same for `Pu_kip`.
|
||||
|
||||
Implementation must be robust: if `typst query` syntax differs, use `typst compile` + `typst eval` as in wood-joist. Ensure tests pass on the CI image where typst is installed.
|
||||
|
||||
If `generated/` does not exist, create it with `Path(...).mkdir(parents=True, exist_ok=True)`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `footing.typ` imports only `lib/sheet.typ` and `results.json`; no arithmetic beyond load derivation and rounding.
|
||||
2. Every `check` box reflects `checks.*.ok` from JSON; D/C printed as demand/capacity.
|
||||
3. `typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf` succeeds and PDF exists.
|
||||
4. Two appended pytest tests pass, reconciling Typst-derived Ps/Pu to Python-checked Ps/Pu and Typst-presented utils to Python values.
|
||||
5. All prior 10 tests still pass (total 12).
|
||||
6. No existing calc files modified; `generated/` is git-ignored regenerable.
|
||||
|
||||
## Tests
|
||||
|
||||
Builder runs:
|
||||
|
||||
```
|
||||
python calcs/concentric-footing/calc.py
|
||||
typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf
|
||||
python -m pytest calcs/concentric-footing/test_concentric_footing.py -v
|
||||
```
|
||||
|
||||
Expected: 12 passed, PDF exists.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Tasks 001, 002 must be DONE.
|
||||
73
concentric-footing/tasks/004_docs_refresh.md
Normal file
73
concentric-footing/tasks/004_docs_refresh.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Task 004 — Documentation refresh: README.md + codemap.md
|
||||
|
||||
## Goal
|
||||
|
||||
Index the new concentric footing calculation in the shared documentation so the next engineer can discover and compile it without reading source.
|
||||
|
||||
## Background
|
||||
|
||||
Depends on footing.typ existing. This task is typst-builder-local (documentation only). Conventions per PROJECT_STATE.md: compile with --root ., hybrid pattern, no modification to existing calcs.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
- `README.md` — the shared worksheets readme that lists calcs. At present this file lives at `calcs/wood-joist/README.md` which serves as the de-facto worksheets README (it documents all sheets). Also check for `README.md` at `worksheets/` root if it exists. Update whichever exists; if both exist update both with same concentric-footing entry. If only `calcs/wood-joist/README.md` exists, that is the file to edit.
|
||||
- `codemap.md` — update at `worksheets/codemap.md` (and copy to `calcs/concentric-footing/codemap.md` if the per-calc map is expected). Regenerate the whole map rather than editing in place per codemap rules.
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. README.md
|
||||
|
||||
Add a new section `## Concentric footing` after the Wood joist section, following the same style. Include:
|
||||
|
||||
- One-sentence description: "Square concentric spread footing under axial load per ACI 318-19 — soil bearing, one-way/two-way shear, flexure, minimum steel, concrete bearing."
|
||||
- Compile commands:
|
||||
```
|
||||
python calcs/concentric-footing/calc.py
|
||||
typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf
|
||||
python -m pytest calcs/concentric-footing/test_concentric_footing.py
|
||||
```
|
||||
- Stdout variant:
|
||||
```
|
||||
python calcs/concentric-footing/calc.py --input calcs/concentric-footing/input.yaml --stdout
|
||||
```
|
||||
and `--output` note.
|
||||
- Note on load derivation: `input.yaml` supplies checked `Ps`/`Pu` (Pint quantities) and footing geometry/materials; `footing.typ` derives `Ps`/`Pu` inline from `DLr=10psf, LLr=20psf, Br=18.9ft, Lr=27.5ft, column 14in×14ft @145pcf` and reconciles via `<concentric-footing-loads>` query; bearing plate width `bp=6in` governs `A1`.
|
||||
- Update the top index code block to add line:
|
||||
```
|
||||
calcs/concentric-footing/ YAML quantities → Pint → JSON → Typst (ACI 318-19)
|
||||
```
|
||||
- Ensure the 5-step New calculation list remains unchanged.
|
||||
|
||||
If editing `worksheets/README.md` vs `calcs/wood-joist/README.md`, keep both in sync; prefer editing the file that actually exists and then copying the change to the other if both are present.
|
||||
|
||||
### 2. codemap.md
|
||||
|
||||
Regenerate at `worksheets/codemap.md` (and also write copy to `calcs/concentric-footing/codemap.md` if needed for per-calc root check):
|
||||
|
||||
- Re-run classification for all 37+ files including the new `calcs/concentric-footing/calc.py [logic]`, `input.yaml [config]`, `footing.typ [logic]`, `test_concentric_footing.py [test]`, `results.json [state]`, `generated/footing.pdf [doc]`, `CONCENTRIC-FOOTING.pdf [doc]`.
|
||||
- The layout section must now show `calcs/concentric-footing/` expanded with its 5 planned + 3 reference entries.
|
||||
- Hot Spots must now include `calcs/concentric-footing/calc.py` as single source of truth.
|
||||
- Conventions section must mention the new sheet's hybrid Pint pattern and Typst load derivation.
|
||||
- Timestamp updated to now, Files indexed incremented.
|
||||
|
||||
Do not write summaries from imagination; if uncertain read first 20 lines.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `README.md` contains a new `## Concentric footing` section with compile, stdout, and load-derivation notes; top index code block lists the new calc.
|
||||
2. `codemap.md` at `worksheets/codemap.md` exists, is well-formed markdown with header, layout, hot spots, conventions; it lists `calcs/concentric-footing/` and its files.
|
||||
3. `python -m pytest calcs/concentric-footing/test_concentric_footing.py -v` still passes (12 tests).
|
||||
4. No existing calc logic files modified; only docs/maps changed.
|
||||
|
||||
## Tests
|
||||
|
||||
```
|
||||
python -m pytest calcs/concentric-footing/test_concentric_footing.py -v
|
||||
typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf
|
||||
```
|
||||
|
||||
Both succeed; PDF still compiles.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Task 003 must be DONE.
|
||||
69
concentric-footing/tasks/005_review.md
Normal file
69
concentric-footing/tasks/005_review.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Task 005 — Reviewer pass on the whole calculation
|
||||
|
||||
## Goal
|
||||
|
||||
Verify engineering correctness, deterministic evidence, and documentation for the concentric footing analysis before marking the project done.
|
||||
|
||||
## Background
|
||||
|
||||
This task is reviewer-local (reasoning) with simplify skill pass. It must not modify source except via review report; if issues are found the orchestrator will send them back to the builder. The reviewer must check ACI 318-19 clause references, units, applicability limits, benchmark tolerances, and that the Typst sheet presents only (no recomputation) per PROJECT_STATE.md.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
No files to modify in this task. The reviewer writes a review note (verbal output) and runs the simplify skill as part of the standard review pass. If STATUS: FAIL, the orchestrator updates TASKS.md and re-delegates.
|
||||
|
||||
If the reviewer finds fixable issues, they must be reported as a structured list with file and line references.
|
||||
|
||||
## Implementation
|
||||
|
||||
Reviewer steps (read-only, then run commands via delegation tool if needed — but orchestrator handles command execution; reviewer describes what to run and checks output):
|
||||
|
||||
1. Read `PROJECT_STATE.md`, `TASKS.md`, `calcs/concentric-footing/calc.py`, `input.yaml`, `results.json`, `footing.typ`, `test_concentric_footing.py`, `README.md`, `codemap.md`, and `CONCENTRIC-FOOTING.pdf`.
|
||||
2. Verify engineering spec. Formula → code-section map (all in `calc.py:compute()`):
|
||||
- Soil bearing `q=Ps/Af`, `qu=Pu/Af`: Pressures block → `q_psf`, `qu_psf`.
|
||||
- One-way shear `Vc=2·λ·√fc·B·d`, `Vu=qu·B·L1`: One-way shear block → `L1_in`, `Vu_one_way_kip`, `Vc_one_way_kip`, `phiVc_one_way_kip`.
|
||||
- Two-way shear `bo=4(c+d)`, `vc=min(4,2+4/β,2+α·d/bo)·λ·√fc`, `Vu=qu·(Af-(c+d)²)`: Two-way shear block → `bo_in`, `vc_psi`, `Vu_two_way_kip`, `Vc_two_way_kip`, `phiVn_two_way_kip`.
|
||||
- Flexure `Mu=qu·B·Lc²/2`, `a=As·fy/(0.85·fc·B)`, `Mn=As·fy·(d-a/2)`: Flexure block → `Lc_in`, `Mu_kipft`, `a_in`, `beta1`, `Mn_kipft`, `phiMn_kipft`.
|
||||
- Bearing `Bn=0.85·fc·A1·min(√(A2/A1),2)`: Concrete bearing block → `A1_in2`, `A2_in2`, `sqrt_ratio`, `Bn_kip`, `phiBn_kip`.
|
||||
- Soil bearing `q=Ps/Af` and `qu=Pu/Af` correctly use Af=Bf^2 in ft2, Ps/Pu in kip, qa in psf, D/C correct.
|
||||
- One-way shear Vc=2*lambda*sqrt(fc)*B*d with phi 0.75, L1 = (B-c)/2 - d, Vu = qu*B*L1, correct unit conversion lbf->kip, handles L1<=0 edge.
|
||||
- Two-way shear bo=4*(c+d), vc=min(4,2+4/beta,2+alpha*d/bo)*lambda*sqrt(fc) with alpha 40 interior, beta 1, phi 0.75, Vu=qu*(Af - (c+d)^2), bo correction documented.
|
||||
- Flexure Mu=qu*B*Lc^2/2, a=As*fy/(0.85*fc*B), Mn=As*fy*(d-a/2), phi 0.9, beta1 formula per ACI, rho vs 0.0018.
|
||||
- Bearing Bn=0.85*fc*A1*min(sqrt(A2/A1),2), phi 0.65, A1=bp^2, A2=Bf^2, sqrt cap.
|
||||
- d = Df - cover to centroid, validation, lambda bounds, rebar area from #size.
|
||||
3. Verify deterministic evidence:
|
||||
- Run `python -m pytest calcs/concentric-footing/test_concentric_footing.py -v` and confirm 12 passed, no skipped.
|
||||
- Run `python calcs/concentric-footing/calc.py --stdout | python -m json.tool` and confirm idempotent vs results.json (diff empty).
|
||||
- Run `typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf` and confirm PDF exists and size >50KB.
|
||||
- Run Typst queries for `<concentric-footing-loads>` and `<concentric-footing-results>` and confirm reconciliation within tolerances.
|
||||
4. Check scope/limitations note in footing.typ mentions square only, interior only, gross pressure, no moment, bo correction, plate clarification, d definition, and that existing calc files were not touched.
|
||||
5. Run simplify skill pass on the diff (calc.py + footing.typ + test file) for readability, no behavior change.
|
||||
6. Return STATUS: PASS if all 8 checks below are satisfied, else STATUS: FAIL with enumerated issues.
|
||||
|
||||
## Acceptance Criteria (all must be true for PASS)
|
||||
|
||||
1. All six checks pass for default input; D/C values within expected ranges (soil ~0.82, one-way ~0.05, two-way ~0.11, flexure ~0.12, bearing ~0.22).
|
||||
2. Corrected punching perimeter bo=92in is used; review notes the PDF's 68in deviation and that the sheet's Scope documents it.
|
||||
3. Bearing plate vs column clarified (A1=36in2, A2=1296in2, Bn 183.6kip); alternative use of column size would be noted.
|
||||
4. Pint unit conversions tested (alternate units test passes) and wrong-dimension guards raise ValueError with field name.
|
||||
5. Effective depth d = Df - cover correctly validated; zero/negative d raises ValueError.
|
||||
6. Typst sheet presents only (no capacity recomputation), derives loads inline, emits both `<concentric-footing-loads>` and `<concentric-footing-results>` metadata, and compiles.
|
||||
7. README and codemap refreshed and list the new calculation; no existing calc files were modified (git diff or file timestamps check).
|
||||
8. Pytest 12 passed, PDF compiles, results.json idempotent, simplify pass clean (no behavior change).
|
||||
|
||||
## Tests
|
||||
|
||||
Reviewer (via orchestrator delegation) runs:
|
||||
|
||||
```
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pytest calcs/concentric-footing/test_concentric_footing.py -v
|
||||
python calcs/concentric-footing/calc.py --stdout
|
||||
typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf
|
||||
```
|
||||
|
||||
All succeed; 12 passed.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Task 004 must be DONE.
|
||||
170
concentric-footing/test_concentric_footing.py
Normal file
170
concentric-footing/test_concentric_footing.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
from pathlib import Path
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
spec = importlib.util.spec_from_file_location("concentric_footing_calc", HERE / "calc.py")
|
||||
assert spec is not None and spec.loader is not None
|
||||
calc_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(calc_module)
|
||||
compute = calc_module.compute
|
||||
|
||||
|
||||
def load_input():
|
||||
with (HERE / "input.yaml").open(encoding="utf-8") as handle:
|
||||
return yaml.safe_load(handle)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def result():
|
||||
return compute(load_input())
|
||||
|
||||
|
||||
def test_example_pressures_and_geometry(result):
|
||||
v = result["values"]
|
||||
assert v["Af_ft2"] == pytest.approx(9.0)
|
||||
assert v["Bf_ft"] == pytest.approx(3.0)
|
||||
assert v["Bf_in"] == pytest.approx(36.0)
|
||||
assert v["d_in"] == pytest.approx(9.0)
|
||||
assert v["q_psf"] == pytest.approx(2044.44, rel=0.01)
|
||||
assert v["qu_psf"] == pytest.approx(2911.11, rel=0.01)
|
||||
assert v["qa_psf"] == 2500.0
|
||||
|
||||
|
||||
def test_example_one_way_shear(result):
|
||||
v = result["values"]
|
||||
assert v["L1_in"] == pytest.approx(2.0, abs=0.01)
|
||||
assert v["Vu_one_way_kip"] == pytest.approx(1.455, abs=0.05)
|
||||
assert v["Vc_one_way_kip"] == pytest.approx(35.45, rel=0.01)
|
||||
assert v["phiVc_one_way_kip"] == pytest.approx(26.59, rel=0.01)
|
||||
assert result["checks"]["one_way_shear"]["ok"] is True
|
||||
dc = v["Vu_one_way_kip"] / v["phiVc_one_way_kip"]
|
||||
assert dc == pytest.approx(0.055, abs=0.001)
|
||||
|
||||
|
||||
def test_example_two_way_shear(result):
|
||||
v = result["values"]
|
||||
# Reference PDF used 68 in; ACI-correct is 92 in (4*(c+d) for interior square column).
|
||||
# PDF's 68 in perimeter yields ~134 kip Vc; ACI-correct 92 in yields ~181.4 kip Vc (phiVn ~136 kip).
|
||||
assert v["bo_in"] == pytest.approx(92.0)
|
||||
assert v["vc_psi"] == pytest.approx(219.089, rel=1e-3)
|
||||
assert v["Vc_two_way_kip"] == pytest.approx(181.4, rel=0.02)
|
||||
assert v["phiVn_two_way_kip"] == pytest.approx(136.0, rel=0.02)
|
||||
assert v["Vu_two_way_kip"] == pytest.approx(15.51, abs=0.1)
|
||||
assert result["checks"]["two_way_shear"]["ok"] is True
|
||||
dc = v["Vu_two_way_kip"] / v["phiVn_two_way_kip"]
|
||||
assert dc == pytest.approx(0.114, abs=0.002)
|
||||
|
||||
|
||||
def test_example_flexure(result):
|
||||
v = result["values"]
|
||||
assert v["Lc_in"] == pytest.approx(11.0)
|
||||
assert v["Mu_kipft"] == pytest.approx(3.68, abs=0.1)
|
||||
assert v["a_in"] == pytest.approx(0.524, abs=0.02)
|
||||
assert v["As_in2"] == pytest.approx(0.785, abs=0.02)
|
||||
assert v["Mn_kipft"] == pytest.approx(34.27, abs=0.5)
|
||||
assert v["phiMn_kipft"] == pytest.approx(30.84, abs=0.5)
|
||||
assert v["rho"] == pytest.approx(0.00242, rel=0.02)
|
||||
assert result["checks"]["flexure"]["ok"] is True
|
||||
assert result["checks"]["minimum_steel"]["ok"] is True
|
||||
dc = v["Mu_kipft"] / v["phiMn_kipft"]
|
||||
assert dc == pytest.approx(0.12, abs=0.01)
|
||||
|
||||
|
||||
def test_example_bearing(result):
|
||||
v = result["values"]
|
||||
assert v["A1_in2"] == pytest.approx(36.0)
|
||||
assert v["A2_in2"] == pytest.approx(1296.0)
|
||||
# Raw ratio stored; Bn uses capped 2.0.
|
||||
assert v["sqrt_ratio"] == pytest.approx(6.0, abs=0.01)
|
||||
assert v["Bn_kip"] == pytest.approx(183.6, abs=0.5)
|
||||
assert v["phiBn_kip"] == pytest.approx(119.34, abs=0.5)
|
||||
assert result["checks"]["bearing"]["ok"] is True
|
||||
dc = v["Pu_kip"] / v["phiBn_kip"]
|
||||
assert dc == pytest.approx(0.22, abs=0.01)
|
||||
|
||||
|
||||
def test_all_checks_pass(result):
|
||||
for k in ("soil_bearing", "one_way_shear", "two_way_shear", "flexure", "minimum_steel", "bearing"):
|
||||
assert result["checks"][k]["ok"] is True
|
||||
|
||||
|
||||
def test_alternate_units_match_default(result):
|
||||
alt = load_input()
|
||||
alt.update({
|
||||
"Bf": "36 in",
|
||||
"fc": "3 ksi",
|
||||
"fy": "60000 psi",
|
||||
"Ps": "18400 lbf",
|
||||
"Pu": "26200 lbf",
|
||||
})
|
||||
converted = compute(alt)
|
||||
for key in ("Af_ft2", "q_psf", "Mu_kipft", "Vu_one_way_kip", "phiBn_kip"):
|
||||
assert converted["values"][key] == pytest.approx(result["values"][key], rel=1e-6)
|
||||
|
||||
|
||||
def test_wrong_dimension_is_rejected():
|
||||
bad = load_input()
|
||||
bad["Bf"] = "3 kip"
|
||||
with pytest.raises(ValueError, match="Bf"):
|
||||
compute(bad)
|
||||
|
||||
|
||||
def test_effective_depth_validation():
|
||||
bad = load_input()
|
||||
bad["cover"] = "13 in"
|
||||
bad["Df"] = "12 in"
|
||||
with pytest.raises(ValueError, match="d"):
|
||||
compute(bad)
|
||||
|
||||
|
||||
def test_zero_footing_size_rejected():
|
||||
bad = load_input()
|
||||
bad["Bf"] = "0 ft"
|
||||
with pytest.raises(ValueError, match="Bf"):
|
||||
compute(bad)
|
||||
|
||||
|
||||
def test_typst_compiles_and_presents_python_numbers(result):
|
||||
subprocess.run([sys.executable, str(HERE / "calc.py")], check=True, cwd=HERE.parents[1])
|
||||
pdf = HERE / "generated" / "footing.pdf"
|
||||
pdf.parent.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(
|
||||
["typst", "compile", "--root", ".", "calcs/concentric-footing/footing.typ", str(pdf)],
|
||||
check=True, cwd=HERE.parents[1],
|
||||
)
|
||||
assert pdf.exists()
|
||||
query = subprocess.run(
|
||||
["typst", "eval", "query(<concentric-footing-results>)", "--root", ".",
|
||||
"--in", "calcs/concentric-footing/footing.typ", "--format", "json"],
|
||||
check=True, capture_output=True, text=True, cwd=HERE.parents[1],
|
||||
)
|
||||
published = json.loads(query.stdout)
|
||||
assert len(published) == 1
|
||||
meta = published[0]["value"]
|
||||
values = result["values"]
|
||||
assert meta["soil_util"] == pytest.approx(values["q_psf"] / values["qa_psf"], abs=0.001)
|
||||
assert meta["one_way_util"] == pytest.approx(values["Vu_one_way_kip"] / values["phiVc_one_way_kip"], abs=0.001)
|
||||
assert meta["two_way_util"] == pytest.approx(values["Vu_two_way_kip"] / values["phiVn_two_way_kip"], abs=0.001)
|
||||
assert meta["flexure_util"] == pytest.approx(values["Mu_kipft"] / values["phiMn_kipft"], abs=0.001)
|
||||
assert meta["bearing_util"] == pytest.approx(values["Pu_kip"] / values["phiBn_kip"], abs=0.001)
|
||||
|
||||
|
||||
def test_typst_load_demands_match_checked_inputs(result):
|
||||
subprocess.run([sys.executable, str(HERE / "calc.py")], check=True, cwd=HERE.parents[1])
|
||||
query = subprocess.run(
|
||||
["typst", "eval", "query(<concentric-footing-loads>)", "--root", ".",
|
||||
"--in", "calcs/concentric-footing/footing.typ", "--format", "json"],
|
||||
check=True, capture_output=True, text=True, cwd=HERE.parents[1],
|
||||
)
|
||||
published = json.loads(query.stdout)
|
||||
assert len(published) == 1
|
||||
derived = published[0]["value"]
|
||||
values = result["values"]
|
||||
assert derived["Ps_kip"] == pytest.approx(values["Ps_kip"], abs=0.1)
|
||||
assert derived["Pu_kip"] == pytest.approx(values["Pu_kip"], abs=0.1)
|
||||
Loading…
Reference in a new issue