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 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 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") concrete_density = quantity(inp["concrete_density"], "pcf", "concrete_density") 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") 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 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 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") 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), "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), }, "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 _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 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) section_path = HERE / "assets" / "beam-section.typ" _load_section_module().write_fragment(result["values"], section_path) print(section_path) return 0 if __name__ == "__main__": raise SystemExit(main())