calcs/wood-stud-column/calc.py

163 lines
5.9 KiB
Python
Raw Normal View History

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("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 factor(value, name: str) -> float:
magnitude = float(value)
if magnitude <= 0:
raise ValueError(f"{name} must be positive")
return magnitude
# NDS 2018 constants for sawn-lumber columns (NDS 3.7.1.5, Eqs. 3.7-1 and 3.7-2).
KC_EULER = 0.822 # Euler buckling coefficient for a rectangular section about its
# weak axis (pi^2 / 12); appears directly in Eq. 3.7-2.
C_SAWN = 0.8 # Column-stability denominator coefficient c for sawn lumber
# (Eq. 3.7-1). c = 0.85 for round timber, 0.9 for glulam/SCL/CLT.
def _column_stability_factor(Fc_star: float, Emin_prime: float, Le: float, d: float):
"""NDS 3.7.1.5, Eq. 3.7-1 — column stability factor Cp for one buckling axis."""
FcE = KC_EULER * Emin_prime / (Le / d) ** 2
ratio = FcE / Fc_star
Cp = (1.0 + ratio) / (2.0 * C_SAWN) - math.sqrt(((1.0 + ratio) / (2.0 * C_SAWN)) ** 2 - ratio / C_SAWN)
if not math.isfinite(Cp) or Cp <= 0:
raise ValueError(f"Cp = {Cp:.4f} is not finite/positive; column stability invalid (NDS 3.7.1.4)")
return min(Cp, 1.0), FcE, ratio
def compute(inp: dict) -> dict:
# Geometry
b_in = quantity(inp["width"], "in", "width")
d_in = quantity(inp["depth"], "in", "depth")
L_in = quantity(inp["length"], "in", "length")
Ke = factor(inp["Ke"], "Ke")
# Material (NDS Supplement reference values)
Fc_psi = quantity(inp["Fc"], "psi", "Fc")
E_psi = quantity(inp["E"], "psi", "E")
Emin_psi = quantity(inp["E_min"], "psi", "E_min")
species_grade = str(inp["species_grade"])
# Adjustment factors (NDS Table 4.3.1)
CD = factor(inp["CD"], "CD")
CM = factor(inp["CM"], "CM")
Ct = factor(inp["Ct"], "Ct")
CF = factor(inp["CF"], "CF")
Ci = factor(inp["Ci"], "Ci")
# Checked axial demand (ASD)
P_lbf = quantity(inp["P"], "lbf", "P")
A_in2 = b_in * d_in
Le_in = Ke * L_in
d_least = min(b_in, d_in)
d_great = max(b_in, d_in)
# Adjusted reference compression (without Cp), NDS 3.7.1.5
Fc_star = Fc_psi * CD * CM * Ct * CF * Ci
Emin_prime = Emin_psi * CM * Ct * Ci
# Column stability factor about each axis; the weak axis governs.
Cp_weak, FcE_weak, ratio_weak = _column_stability_factor(Fc_star, Emin_prime, Le_in, d_least)
Cp_strong, FcE_strong, ratio_strong = _column_stability_factor(Fc_star, Emin_prime, Le_in, d_great)
governing_axis = "weak" if Cp_weak <= Cp_strong else "strong"
Cp = min(Cp_weak, Cp_strong)
FcE = FcE_weak if governing_axis == "weak" else FcE_strong
# Adjusted compression design value, NDS Table 4.3.1
Fc_prime = Fc_star * Cp
fc_psi = P_lbf / A_in2
Pa_lbf = Fc_prime * A_in2
def q(value: float) -> float:
return round(value, 6)
values = {
"P_lbf": q(P_lbf),
"L_in": q(L_in), "b_in": q(b_in), "d_in": q(d_in),
"A_in2": q(A_in2), "Ke": q(Ke), "Le_in": q(Le_in),
"d_least_in": q(d_least), "d_great_in": q(d_great),
"Le_d_weak": q(Le_in / d_least), "Le_d_strong": q(Le_in / d_great),
"species_grade": species_grade,
"Fc_psi": q(Fc_psi), "E_psi": q(E_psi), "Emin_psi": q(Emin_psi),
"CD": q(CD), "CM": q(CM), "Ct": q(Ct), "CF": q(CF), "Ci": q(Ci),
"kc_euler": q(KC_EULER), "c_sawn": q(C_SAWN),
"Fc_star_psi": q(Fc_star), "Emin_prime_psi": q(Emin_prime),
"FcE_weak_psi": q(FcE_weak), "FcE_strong_psi": q(FcE_strong),
"ratio_weak": q(ratio_weak), "ratio_strong": q(ratio_strong),
"Cp_weak": q(Cp_weak), "Cp_strong": q(Cp_strong),
"Cp": q(Cp), "governing_axis": governing_axis,
"Fc_prime_psi": q(Fc_prime), "fc_psi": q(fc_psi), "Pa_lbf": q(Pa_lbf),
}
checks = {
"axial": {"demand": q(P_lbf), "capacity": q(Pa_lbf), "ok": P_lbf <= Pa_lbf},
}
return {
"tool": "wood_stud_column",
"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 wood stud column axial capacity 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())