calcs/concentric-footing/calc.py
smillmorel d5ac3fca7e Add structural calculation worksheets
Collection of engineering calculation projects (Python + Typst), each with
input, calc script, tests, results, and generated PDF where available.
2026-09-21 12:19:20 -04:00

222 lines
7.4 KiB
Python

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