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