2026-09-21 12:19:20 -04:00
|
|
|
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")
|
|
|
|
|
|
2026-09-21 12:44:28 -04:00
|
|
|
try:
|
|
|
|
|
from pint import DimensionalityError, UndefinedUnitError, UnitRegistry
|
|
|
|
|
except ImportError:
|
|
|
|
|
raise SystemExit("Install dependencies: python -m pip install -r requirements.txt")
|
|
|
|
|
|
2026-09-21 12:19:20 -04:00
|
|
|
|
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
|
|
|
|
2026-09-21 12:44:28 -04:00
|
|
|
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")
|
2026-09-21 12:19:20 -04:00
|
|
|
|
2026-09-21 12:44:28 -04:00
|
|
|
|
|
|
|
|
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:
|
2026-09-21 12:19:20 -04:00
|
|
|
raise ValueError(f"{name} must be positive")
|
2026-09-21 12:44:28 -04:00
|
|
|
return magnitude
|
2026-09-21 12:19:20 -04:00
|
|
|
|
|
|
|
|
|
2026-09-21 14:25:16 -04:00
|
|
|
def count(value, name: str, minimum: int = 1) -> int:
|
|
|
|
|
try:
|
|
|
|
|
number = int(value)
|
|
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
|
|
raise ValueError(f"{name}: expected an integer, got {value!r}") from exc
|
|
|
|
|
if number < minimum:
|
|
|
|
|
raise ValueError(f"{name} must be >= {minimum}")
|
|
|
|
|
return number
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bar_number(value, name: str) -> int:
|
|
|
|
|
number = count(value, name, minimum=3)
|
|
|
|
|
if number > 18:
|
|
|
|
|
raise ValueError(f"{name} must be between 3 and 18")
|
|
|
|
|
return number
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 12:19:20 -04:00
|
|
|
def compute(inp: dict) -> dict:
|
2026-09-21 12:44:28 -04:00
|
|
|
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")
|
|
|
|
|
concrete_density = quantity(inp["concrete_density"], "pcf", "concrete_density")
|
2026-09-21 14:25:16 -04:00
|
|
|
cover = quantity(inp["cover"], "in", "cover")
|
|
|
|
|
stirrup_size = bar_number(inp["stirrup_size"], "stirrup_size")
|
|
|
|
|
stirrup_spacing = quantity(inp["stirrup_spacing"], "in", "stirrup_spacing")
|
|
|
|
|
bar_size = bar_number(inp["bar_size"], "bar_size")
|
|
|
|
|
n_bars = count(inp["n_bars"], "n_bars")
|
|
|
|
|
top_bar_size = bar_number(inp["top_bar_size"], "top_bar_size")
|
|
|
|
|
n_top_bars = count(inp["n_top_bars"], "n_top_bars")
|
2026-09-21 12:19:20 -04:00
|
|
|
|
2026-09-21 19:16:57 -04:00
|
|
|
stirrup_db = stirrup_size / 8.0
|
|
|
|
|
bar_db = bar_size / 8.0
|
|
|
|
|
top_bar_db = top_bar_size / 8.0
|
|
|
|
|
As = n_bars * math.pi * bar_db**2 / 4.0
|
|
|
|
|
|
2026-09-21 12:44:28 -04:00
|
|
|
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
|
2026-09-21 12:19:20 -04:00
|
|
|
|
2026-09-21 12:44:28 -04:00
|
|
|
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
|
2026-09-21 12:19:20 -04:00
|
|
|
if et >= 0.005:
|
|
|
|
|
phi = 0.90
|
|
|
|
|
else:
|
|
|
|
|
phi = max(0.65, min(0.90, 0.65 + (et - 0.002) * 250.0 / 3.0))
|
2026-09-21 12:44:28 -04:00
|
|
|
Mn = As * fy * (d - a / 2.0) / 12.0
|
|
|
|
|
phiMn = phi * Mn
|
2026-09-21 12:19:20 -04:00
|
|
|
|
2026-09-21 12:44:28 -04:00
|
|
|
rho_min = max(3.0 * math.sqrt(fc * 1000.0) / (fy * 1000.0), 200.0 / (fy * 1000.0))
|
|
|
|
|
As_min = rho_min * bw * d
|
2026-09-21 12:19:20 -04:00
|
|
|
|
2026-09-21 12:44:28 -04:00
|
|
|
Vc = 2.0 * math.sqrt(fc * 1000.0) * bw * d / 1000.0
|
|
|
|
|
phiVc = 0.75 * Vc
|
2026-09-21 12:19:20 -04:00
|
|
|
|
2026-09-21 14:25:16 -04:00
|
|
|
bottom_centroid = h - d
|
|
|
|
|
min_bottom_centroid = cover + stirrup_db + bar_db / 2.0
|
|
|
|
|
if bottom_centroid < min_bottom_centroid:
|
|
|
|
|
raise ValueError("effective depth d is too large for the specified cover and bars")
|
|
|
|
|
inner_width = bw - 2.0 * (cover + stirrup_db)
|
|
|
|
|
if n_bars * bar_db > inner_width:
|
|
|
|
|
raise ValueError("bottom bars do not fit within the stirrup width")
|
|
|
|
|
|
2026-09-21 12:19:20 -04:00
|
|
|
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": {
|
2026-09-21 12:44:28 -04:00
|
|
|
"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),
|
2026-09-21 12:19:20 -04:00
|
|
|
"et": q(et),
|
|
|
|
|
"phi": q(phi),
|
2026-09-21 12:44:28 -04:00
|
|
|
"Mn": q(Mn),
|
|
|
|
|
"phiMn": q(phiMn),
|
|
|
|
|
"As_min": q(As_min),
|
|
|
|
|
"Vc": q(Vc),
|
|
|
|
|
"phiVc": q(phiVc),
|
2026-09-21 14:25:16 -04:00
|
|
|
"cover": q(cover),
|
|
|
|
|
"stirrup_size": stirrup_size,
|
|
|
|
|
"stirrup_spacing": q(stirrup_spacing),
|
|
|
|
|
"stirrup_db": q(stirrup_db),
|
|
|
|
|
"bar_size": bar_size,
|
|
|
|
|
"n_bars": n_bars,
|
|
|
|
|
"bar_db": q(bar_db),
|
|
|
|
|
"top_bar_size": top_bar_size,
|
|
|
|
|
"n_top_bars": n_top_bars,
|
|
|
|
|
"top_bar_db": q(top_bar_db),
|
2026-09-21 12:19:20 -04:00
|
|
|
},
|
|
|
|
|
"checks": {
|
|
|
|
|
"flexure": {
|
2026-09-21 12:44:28 -04:00
|
|
|
"demand": q(Mu),
|
|
|
|
|
"capacity": q(phiMn),
|
|
|
|
|
"ok": Mu <= phiMn,
|
2026-09-21 12:19:20 -04:00
|
|
|
},
|
|
|
|
|
"minimum_steel": {
|
2026-09-21 12:44:28 -04:00
|
|
|
"demand": q(As_min),
|
|
|
|
|
"capacity": q(As),
|
|
|
|
|
"ok": As >= As_min,
|
2026-09-21 12:19:20 -04:00
|
|
|
},
|
|
|
|
|
"shear": {
|
2026-09-21 12:44:28 -04:00
|
|
|
"demand": q(Vu),
|
|
|
|
|
"capacity": q(phiVc),
|
|
|
|
|
"ok": Vu <= phiVc,
|
2026-09-21 12:19:20 -04:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_results(result: dict, path: Path) -> None:
|
|
|
|
|
path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 14:25:16 -04:00
|
|
|
def _load_section_module():
|
|
|
|
|
import importlib.util
|
|
|
|
|
|
|
|
|
|
spec = importlib.util.spec_from_file_location("beam_section", HERE / "beam_section.py")
|
|
|
|
|
if spec is None or spec.loader is None:
|
|
|
|
|
raise SystemExit("Could not load beam_section.py")
|
|
|
|
|
module = importlib.util.module_from_spec(spec)
|
|
|
|
|
spec.loader.exec_module(module)
|
|
|
|
|
return module
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 12:19:20 -04:00
|
|
|
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)
|
2026-09-21 14:25:16 -04:00
|
|
|
section_path = HERE / "assets" / "beam-section.typ"
|
|
|
|
|
_load_section_module().write_fragment(result["values"], section_path)
|
|
|
|
|
print(section_path)
|
2026-09-21 12:19:20 -04:00
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|