calcs/concrete-beam/calc.py
smillmorel 8f91baee41 Use Pint and unit-free variable names in concrete-beam
Convert input parsing to Pint (matching the other calculations), carry
units in the YAML values, and drop unit suffixes from input keys, local
variables, results keys, the Typst sheet, and tests.
2026-09-21 12:44:28 -04:00

146 lines
4.2 KiB
Python

from __future__ import annotations
import json
import math
import sys
from pathlib import Path
try:
import yaml
except ImportError:
raise SystemExit("Install PyYAML: python -m pip install pyyaml")
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")
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:
span = quantity(inp["span"], "ft", "span")
tributary = quantity(inp["tributary"], "ft", "tributary")
D = quantity(inp["D"], "psf", "D")
L = quantity(inp["L"], "psf", "L")
bw = quantity(inp["bw"], "in", "bw")
h = quantity(inp["h"], "in", "h")
d = quantity(inp["d"], "in", "d")
fc = quantity(inp["fc"], "ksi", "fc")
fy = quantity(inp["fy"], "ksi", "fy")
As = quantity(inp["As"], "in**2", "As")
concrete_density = quantity(inp["concrete_density"], "pcf", "concrete_density")
self_weight = (bw * h / 144.0) * concrete_density / 1000.0
wD = D * tributary / 1000.0 + self_weight
wL = L * tributary / 1000.0
wu = 1.2 * wD + 1.6 * wL
Mu = wu * span**2 / 8.0
Vu = wu * span / 2.0
a = As * fy / (0.85 * fc * bw)
beta1 = max(0.65, min(0.85, 0.85 - 0.05 * max(0.0, (fc * 1000.0 - 4000.0) / 1000.0)))
c = a / beta1
et = 0.003 * (d - c) / c if c > 0 else 0.0
if et >= 0.005:
phi = 0.90
else:
phi = max(0.65, min(0.90, 0.65 + (et - 0.002) * 250.0 / 3.0))
Mn = As * fy * (d - a / 2.0) / 12.0
phiMn = phi * Mn
rho_min = max(3.0 * math.sqrt(fc * 1000.0) / (fy * 1000.0), 200.0 / (fy * 1000.0))
As_min = rho_min * bw * d
Vc = 2.0 * math.sqrt(fc * 1000.0) * bw * d / 1000.0
phiVc = 0.75 * Vc
def q(value: float) -> float:
return round(value, 6)
return {
"tool": "concrete_beam",
"version": "0.1",
"project": inp.get("project", ""),
"prepared_by": inp.get("prepared_by", ""),
"values": {
"span": q(span),
"tributary": q(tributary),
"D": q(D),
"L": q(L),
"self_weight": q(self_weight),
"wD": q(wD),
"wL": q(wL),
"wu": q(wu),
"Mu": q(Mu),
"Vu": q(Vu),
"bw": q(bw),
"h": q(h),
"d": q(d),
"fc": q(fc),
"fy": q(fy),
"As": q(As),
"a": q(a),
"et": q(et),
"phi": q(phi),
"Mn": q(Mn),
"phiMn": q(phiMn),
"As_min": q(As_min),
"Vc": q(Vc),
"phiVc": q(phiVc),
},
"checks": {
"flexure": {
"demand": q(Mu),
"capacity": q(phiMn),
"ok": Mu <= phiMn,
},
"minimum_steel": {
"demand": q(As_min),
"capacity": q(As),
"ok": As >= As_min,
},
"shear": {
"demand": q(Vu),
"capacity": q(phiVc),
"ok": Vu <= phiVc,
},
},
}
def write_results(result: dict, path: Path) -> None:
path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
def main() -> int:
input_path = Path(sys.argv[1]) if len(sys.argv) > 1 else HERE / "input.yaml"
output_path = Path(sys.argv[2]) if len(sys.argv) > 2 else input_path.with_name("results.json")
with input_path.open(encoding="utf-8") as handle:
inp = yaml.safe_load(handle)
result = compute(inp)
write_results(result, output_path)
print(output_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())