Collection of engineering calculation projects (Python + Typst), each with input, calc script, tests, results, and generated PDF where available.
163 lines
5.9 KiB
Python
163 lines
5.9 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("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())
|