Add structural calculation worksheets
Collection of engineering calculation projects (Python + Typst), each with input, calc script, tests, results, and generated PDF where available.
This commit is contained in:
commit
d5ac3fca7e
81 changed files with 18668 additions and 0 deletions
163
wood-stud-column/calc.py
Normal file
163
wood-stud-column/calc.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
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())
|
||||
Loading…
Reference in a new issue