222 lines
7.4 KiB
Python
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())
|