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
181
wood-joist/test_wood_joist.py
Normal file
181
wood-joist/test_wood_joist.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
from pathlib import Path
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
spec = importlib.util.spec_from_file_location("wood_joist_calc", HERE / "calc.py")
|
||||
assert spec is not None and spec.loader is not None
|
||||
calc_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(calc_module)
|
||||
compute = calc_module.compute
|
||||
|
||||
|
||||
def load_input():
|
||||
with (HERE / "input.yaml").open(encoding="utf-8") as handle:
|
||||
return yaml.safe_load(handle)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def result():
|
||||
return compute(load_input())
|
||||
|
||||
|
||||
def test_example_loads_and_section(result):
|
||||
v = result["values"]
|
||||
assert v["w_plf"] == pytest.approx(975)
|
||||
assert v["wL_plf"] == pytest.approx(650)
|
||||
assert v["A_in2"] == pytest.approx(33.25)
|
||||
assert v["Ix_in4"] == pytest.approx(250.1, abs=0.1)
|
||||
assert v["Sx_in3"] == pytest.approx(52.6, abs=0.1)
|
||||
|
||||
|
||||
def test_example_demands(result):
|
||||
v = result["values"]
|
||||
assert v["M_ftlbf"] == pytest.approx(9871.88, abs=0.01)
|
||||
assert v["V_lbf"] == pytest.approx(4387.5)
|
||||
assert v["R_lbf"] == pytest.approx(4387.5)
|
||||
|
||||
|
||||
def test_example_flexure(result):
|
||||
v = result["values"]
|
||||
assert v["RB"] == pytest.approx(0.881, abs=0.001)
|
||||
assert v["Fb_star_psi"] == pytest.approx(2400)
|
||||
assert v["FbE_psi"] == pytest.approx(789157.9, rel=1e-3)
|
||||
assert v["CL"] == pytest.approx(0.9998, abs=0.001)
|
||||
assert v["Fb_prime_psi"] == pytest.approx(2399.6, abs=0.2)
|
||||
assert v["fb_psi"] == pytest.approx(2250.2, abs=0.1)
|
||||
assert v["Ma_ftlbf"] == pytest.approx(10527.6, abs=0.3)
|
||||
assert v["fb_psi"] / v["Fb_prime_psi"] == pytest.approx(0.938, abs=0.001)
|
||||
assert result["checks"]["flexure"]["ok"] is True
|
||||
|
||||
|
||||
def test_example_shear(result):
|
||||
v = result["values"]
|
||||
assert v["Fv_prime_psi"] == pytest.approx(300)
|
||||
assert v["fv_psi"] == pytest.approx(197.932, abs=0.001)
|
||||
assert v["fv_psi"] / v["Fv_prime_psi"] == pytest.approx(0.66, abs=0.005)
|
||||
assert result["checks"]["shear"]["ok"] is True
|
||||
|
||||
|
||||
def test_example_bearing(result):
|
||||
v = result["values"]
|
||||
assert v["bed_in"] == pytest.approx(3)
|
||||
assert v["Cb"] == pytest.approx(1.15)
|
||||
assert v["Fcp_prime_psi"] == pytest.approx(649.75, abs=0.01)
|
||||
assert v["Ab_in2"] == pytest.approx(8.75)
|
||||
assert v["fcp_psi"] == pytest.approx(501.429, abs=0.001)
|
||||
assert v["fcp_psi"] / v["Fcp_prime_psi"] == pytest.approx(0.772, abs=0.001)
|
||||
assert result["checks"]["bearing"]["ok"] is True
|
||||
|
||||
|
||||
def test_example_deflection(result):
|
||||
v = result["values"]
|
||||
assert v["delta_total_in"] == pytest.approx(0.339, abs=0.001)
|
||||
assert v["delta_live_in"] == pytest.approx(0.226, abs=0.001)
|
||||
assert v["delta_allow_total_in"] == pytest.approx(0.45)
|
||||
assert v["delta_allow_live_in"] == pytest.approx(0.30)
|
||||
assert result["checks"]["deflection_total"]["ok"] is True
|
||||
assert result["checks"]["deflection_live"]["ok"] is True
|
||||
|
||||
|
||||
def test_all_checks_pass(result):
|
||||
assert all(result["checks"][name]["ok"] for name in ("flexure", "shear", "bearing", "deflection_total", "deflection_live")) is True
|
||||
|
||||
|
||||
def test_alternate_units_match_default(result):
|
||||
alt = load_input()
|
||||
alt.update({
|
||||
"span": "108 in",
|
||||
"width": "3.5 in",
|
||||
"depth": "9.5 in",
|
||||
"M": "118462.5 lbf * in",
|
||||
"V": "4387.5 lbf",
|
||||
"R": "4387.5 lbf",
|
||||
})
|
||||
converted = compute(alt)
|
||||
for key in ("span_ft", "M_ftlbf", "V_lbf", "R_lbf", "fb_psi", "delta_total_in"):
|
||||
assert converted["values"][key] == pytest.approx(result["values"][key], rel=1e-6)
|
||||
|
||||
|
||||
def test_wrong_dimension_is_rejected():
|
||||
bad = load_input()
|
||||
bad["span"] = "9 kip"
|
||||
with pytest.raises(ValueError, match="span"):
|
||||
compute(bad)
|
||||
|
||||
|
||||
def test_rb_limit_enforced():
|
||||
bad = load_input()
|
||||
bad["unbraced_length"] = "4000 in"
|
||||
with pytest.raises(ValueError, match="50"):
|
||||
compute(bad)
|
||||
|
||||
|
||||
def test_cl_bounds_enforced():
|
||||
bad = load_input()
|
||||
# A very large E_min drives the beam stability factor to 0 (catastrophic
|
||||
# cancellation in the C_L equation), which is out of the valid (0, 1] range.
|
||||
bad["E_min"] = "1e100 psi"
|
||||
with pytest.raises(ValueError, match="CL"):
|
||||
compute(bad)
|
||||
|
||||
|
||||
def test_bearing_length_limit_enforced():
|
||||
bad = load_input()
|
||||
bad["bearing_length"] = "6 in"
|
||||
with pytest.raises(ValueError, match="6"):
|
||||
compute(bad)
|
||||
|
||||
|
||||
def test_bearing_end_distance_enforced():
|
||||
bad = load_input()
|
||||
bad["bearing_end_distance"] = "2.5 in"
|
||||
with pytest.raises(ValueError, match="3"):
|
||||
compute(bad)
|
||||
|
||||
|
||||
def test_typst_compiles_and_presents_python_numbers(result):
|
||||
subprocess.run([sys.executable, str(HERE / "calc.py")], check=True, cwd=HERE.parents[1])
|
||||
pdf = HERE / "generated" / "wood-joist.pdf"
|
||||
subprocess.run(
|
||||
["typst", "compile", "--root", ".", "calcs/wood-joist/wood-joist.typ", str(pdf)],
|
||||
check=True, cwd=HERE.parents[1],
|
||||
)
|
||||
assert pdf.exists()
|
||||
query = subprocess.run(
|
||||
["typst", "eval", "query(<wood-joist-results>)", "--root", ".",
|
||||
"--in", "calcs/wood-joist/wood-joist.typ", "--format", "json"],
|
||||
check=True, capture_output=True, text=True, cwd=HERE.parents[1],
|
||||
)
|
||||
published = json.loads(query.stdout)
|
||||
assert len(published) == 1
|
||||
meta = published[0]["value"]
|
||||
values = result["values"]
|
||||
assert meta["flexure_util"] == pytest.approx(values["fb_psi"] / values["Fb_prime_psi"], abs=0.001)
|
||||
assert meta["shear_util"] == pytest.approx(values["fv_psi"] / values["Fv_prime_psi"], abs=0.001)
|
||||
assert meta["bearing_util"] == pytest.approx(values["fcp_psi"] / values["Fcp_prime_psi"], abs=0.001)
|
||||
assert meta["delta_total_in"] == pytest.approx(values["delta_total_in"], abs=1e-6)
|
||||
assert meta["delta_live_in"] == pytest.approx(values["delta_live_in"], abs=1e-6)
|
||||
|
||||
|
||||
def test_typst_load_demands_match_checked_inputs(result):
|
||||
subprocess.run([sys.executable, str(HERE / "calc.py")], check=True, cwd=HERE.parents[1])
|
||||
query = subprocess.run(
|
||||
["typst", "eval", "query(<wood-joist-loads>)", "--root", ".",
|
||||
"--in", "calcs/wood-joist/wood-joist.typ", "--format", "json"],
|
||||
check=True, capture_output=True, text=True, cwd=HERE.parents[1],
|
||||
)
|
||||
published = json.loads(query.stdout)
|
||||
assert len(published) == 1
|
||||
derived = published[0]["value"]
|
||||
values = result["values"]
|
||||
assert derived["w_plf"] == pytest.approx(values["w_plf"], abs=0.001)
|
||||
assert derived["wL_plf"] == pytest.approx(values["wL_plf"], abs=0.001)
|
||||
assert derived["M_ftlbf"] == pytest.approx(values["M_ftlbf"], abs=0.01)
|
||||
assert derived["V_lbf"] == pytest.approx(values["V_lbf"], abs=0.001)
|
||||
assert derived["R_lbf"] == pytest.approx(values["R_lbf"], abs=0.001)
|
||||
Loading…
Reference in a new issue