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
BIN
steel-beam/aisc-shapes-database-v15.0.xlsx
Executable file
BIN
steel-beam/aisc-shapes-database-v15.0.xlsx
Executable file
Binary file not shown.
BIN
steel-beam/assets/logo.png
Executable file
BIN
steel-beam/assets/logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
82
steel-beam/assets/sheet.typ
Normal file
82
steel-beam/assets/sheet.typ
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#let navy = rgb("#1a3a5f")
|
||||
#let muted = rgb("#626b73")
|
||||
#let pass = rgb("#1f6b45")
|
||||
#let fail = rgb("#9b2c2c")
|
||||
|
||||
#let calcsheet(
|
||||
title: "Structural Calculation",
|
||||
project: "",
|
||||
prepared-by: "",
|
||||
body,
|
||||
) = {
|
||||
set document(title: title, author: prepared-by)
|
||||
set page(
|
||||
paper: "us-letter",
|
||||
margin: (x: 1in, top: 1.25in, bottom: 1in),
|
||||
header: context {
|
||||
grid(
|
||||
columns: (1fr, 1fr),
|
||||
align: (left, right),
|
||||
image("../assets/logo.png", height: 30pt),
|
||||
[#text(size: 9pt)[Project:] \
|
||||
#text(size: 10pt, weight: "bold")[#project]],
|
||||
)
|
||||
},
|
||||
footer: context {
|
||||
set text(size: 8.5pt, fill: muted)
|
||||
stack(
|
||||
spacing: 4pt,
|
||||
line(length: 100%, stroke: 0.5pt + muted),
|
||||
[#prepared-by],
|
||||
)
|
||||
},
|
||||
)
|
||||
set text(font: "Libertinus Serif", size: 10pt, lang: "en")
|
||||
set par(justify: true)
|
||||
set heading(numbering: none)
|
||||
show heading.where(level: 1): set text(size: 14pt, weight: "bold", fill: black)
|
||||
show heading.where(level: 2): set text(size: 11pt, weight: "bold", fill: black)
|
||||
show heading.where(level: 2): set block(above: 2em, below: 1em)
|
||||
body
|
||||
}
|
||||
|
||||
#let calcline(formula, note) = grid(
|
||||
columns: (1.7fr, 1fr),
|
||||
gutter: 4pt,
|
||||
align: (left, left),
|
||||
formula, text(size: 9pt, fill: muted, note),
|
||||
)
|
||||
|
||||
#let check(label, demand, capacity, unit: "", ok: auto, demand-label: "Demand", capacity-label: "Capacity") = {
|
||||
let utilization = demand / capacity
|
||||
let passes = if ok == auto { utilization <= 1 } else { ok }
|
||||
let color = if passes { pass } else { fail }
|
||||
block(
|
||||
breakable: false,
|
||||
width: 100%,
|
||||
stroke: 0.8pt + black,
|
||||
inset: 8pt,
|
||||
radius: 2pt,
|
||||
)[
|
||||
#grid(
|
||||
columns: (1fr, auto),
|
||||
[#text(weight: "bold")[#label]],
|
||||
box(stroke: 0.8pt + color, inset: (x: 6pt, y: 2pt))[
|
||||
#text(weight: "bold", fill: color)[#if passes { "OK" } else { "NOT OK" }]
|
||||
],
|
||||
)
|
||||
#v(4pt)
|
||||
#grid(
|
||||
columns: (1fr, auto),
|
||||
[
|
||||
#demand-label: #calc.round(demand, digits: 2) #unit #h(14pt)
|
||||
#capacity-label: #calc.round(capacity, digits: 2) #unit
|
||||
],
|
||||
[
|
||||
D/C: #calc.round(utilization, digits: 2)
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
255
steel-beam/calc.py
Normal file
255
steel-beam/calc.py
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
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
|
||||
DATABASE = HERE / "aisc-shapes-database-v15.0.xlsx"
|
||||
NS = {"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
|
||||
|
||||
ureg = UnitRegistry()
|
||||
ureg.define("kip = 1000 * force_pound")
|
||||
ureg.define("ksi = kip / inch ** 2")
|
||||
ureg.define("psf = force_pound / foot ** 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 _shared_strings(book: ZipFile) -> list[str]:
|
||||
root = ET.fromstring(book.read("xl/sharedStrings.xml"))
|
||||
return ["".join(t.text or "" for t in item.findall(".//m:t", NS)) for item in root.findall("m:si", NS)]
|
||||
|
||||
|
||||
def _cell_value(cell: ET.Element, shared: list[str]):
|
||||
value = cell.find("m:v", NS)
|
||||
if value is None:
|
||||
return None
|
||||
text = value.text or ""
|
||||
if cell.get("t") == "s":
|
||||
return shared[int(text)]
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return text
|
||||
|
||||
|
||||
def load_section(label: str, path: Path = DATABASE) -> dict[str, float | str]:
|
||||
with ZipFile(path) as book:
|
||||
shared = _shared_strings(book)
|
||||
root = ET.fromstring(book.read("xl/worksheets/sheet2.xml"))
|
||||
rows = root.findall(".//m:sheetData/m:row", NS)
|
||||
headers: dict[str, int] = {}
|
||||
for cell in rows[0].findall("m:c", NS):
|
||||
# The workbook repeats the metric section after the first 84 columns;
|
||||
# the first occurrence is the US customary section used here.
|
||||
headers.setdefault(str(_cell_value(cell, shared)), _column_number(cell.get("r", "A1")))
|
||||
label_column = headers["AISC_Manual_Label"]
|
||||
wanted = {
|
||||
# Use detailing dimensions for d and thicknesses, as shown in the
|
||||
# reference sheet; nominal dimensions are also retained in the source.
|
||||
"A": "A", "d": "ddet", "b": "bfdet", "tf": "tfdet", "tw": "twdet",
|
||||
"Ix": "Ix", "Zx": "Zx", "Sx": "Sx", "Iy": "Iy", "Zy": "Zy",
|
||||
"Sy": "Sy", "ry": "ry", "J": "J", "rts": "rts", "ho": "ho",
|
||||
"lambda": "h/tw",
|
||||
}
|
||||
for row in rows[1:]:
|
||||
values = {_column_number(cell.get("r", "A1")): _cell_value(cell, shared) for cell in row.findall("m:c", NS)}
|
||||
if values.get(label_column) == label:
|
||||
result: dict[str, float | str] = {"label": label}
|
||||
for name, header in wanted.items():
|
||||
raw = values.get(headers[header])
|
||||
if raw is None or raw == "–":
|
||||
raise ValueError(f"Section {label} has no usable {header} property")
|
||||
result[name] = float(raw)
|
||||
return result
|
||||
raise ValueError(f"Section {label!r} was not found in {path.name}")
|
||||
|
||||
|
||||
def _column_number(reference: str) -> int:
|
||||
letters = "".join(ch for ch in reference if ch.isalpha())
|
||||
number = 0
|
||||
for letter in letters:
|
||||
number = number * 26 + ord(letter.upper()) - ord("A") + 1
|
||||
return number
|
||||
|
||||
|
||||
def compute(inp: dict, database: Path = DATABASE) -> dict:
|
||||
beam_length = quantity(inp["beam_length"], "ft", "beam_length")
|
||||
unbraced_length = quantity(inp["unbraced_length"], "in", "unbraced_length")
|
||||
moment_kipft = quantity(inp["Mu"], "kip * ft", "Mu")
|
||||
shear_kip = quantity(inp["Vu"], "kip", "Vu")
|
||||
E = quantity(inp["steel_modulus"], "ksi", "steel_modulus")
|
||||
Fy = quantity(inp["steel_yield"], "ksi", "steel_yield")
|
||||
service_load = quantity(inp["service_load"], "lbf/ft", "service_load")
|
||||
Cb = float(inp.get("cb", 1))
|
||||
c = float(inp.get("c", 1))
|
||||
if Cb <= 0 or c <= 0:
|
||||
raise ValueError("cb and c must be positive")
|
||||
section = load_section(str(inp["section"]), database)
|
||||
|
||||
# The entered demands are the factored moment and the factored reaction on
|
||||
# the connector (used here as the factored shear). They arrive from the load
|
||||
# determination, which is based on a uniform gravity load, so the equivalent
|
||||
# factored uniform load is recovered from the shear: w_u = 2 V_u / L.
|
||||
factored_uniform_load_kipft = 2.0 * shear_kip / beam_length
|
||||
|
||||
Lb = unbraced_length
|
||||
Lp = 1.76 * float(section["ry"]) * math.sqrt(E / Fy)
|
||||
rts = float(section["rts"])
|
||||
Sx = float(section["Sx"])
|
||||
ho = float(section["ho"])
|
||||
J = float(section["J"])
|
||||
Lr = 1.95 * rts * E / (0.7 * Fy) * math.sqrt(J * c / (Sx * ho) + math.sqrt((J * c / (Sx * ho)) ** 2 + 6.76 * (0.7 * Fy / E) ** 2))
|
||||
Fcr = Cb * math.pi**2 * E / (Lb / rts) ** 2 * math.sqrt(1 + 0.078 * J * c / (Sx * ho) * (Lb / rts) ** 2)
|
||||
Mp = Fy * float(section["Zx"]) / 12.0
|
||||
if Lb <= Lp:
|
||||
Mn_ltb = Mp
|
||||
ltb_mode = "yielding"
|
||||
elif Lb <= Lr:
|
||||
Mn_ltb = Cb * (Mp - (Mp - 0.7 * Fy * Sx / 12.0) * (Lb - Lp) / (Lr - Lp))
|
||||
ltb_mode = "inelastic LTB"
|
||||
else:
|
||||
Mn_ltb = Fcr * Sx / 12.0
|
||||
ltb_mode = "elastic LTB"
|
||||
Mn = min(Mp, Mn_ltb)
|
||||
phi_mn = 0.9 * Mn
|
||||
|
||||
Aw = float(section["d"]) * float(section["tw"])
|
||||
lambda_lim = 2.24 * math.sqrt(E / Fy)
|
||||
kv = 5.34
|
||||
lambda_web = float(section["lambda"])
|
||||
cv1 = 1.0 if lambda_web <= 1.10 * math.sqrt(kv * E / Fy) else 1.10 * math.sqrt(kv * E / Fy) / lambda_web
|
||||
phi_v = 1.0 if lambda_web <= lambda_lim else 0.9
|
||||
phi_vn = phi_v * 0.6 * Fy * Aw * cv1
|
||||
|
||||
# Serviceability deflection under the service uniform load (L/240 limit).
|
||||
L_in = beam_length * 12.0
|
||||
w_serv_lbf_in = service_load / 12.0
|
||||
delta_limit = L_in / 240.0
|
||||
delta = 5.0 * w_serv_lbf_in * L_in ** 4 / (384.0 * (E * 1000.0) * float(section["Ix"]))
|
||||
|
||||
def q(value: float) -> float:
|
||||
return round(value, 6)
|
||||
|
||||
values = {
|
||||
"beam_length_ft": q(beam_length),
|
||||
"unbraced_length_in": q(Lb),
|
||||
"factored_uniform_load_kipft": q(factored_uniform_load_kipft),
|
||||
"service_load_lbf_ft": q(service_load),
|
||||
"moment_kipft": q(moment_kipft),
|
||||
"shear_kip": q(shear_kip),
|
||||
"E_ksi": q(E),
|
||||
"Fy_ksi": q(Fy),
|
||||
"Lp_in": q(Lp),
|
||||
"Lr_ft": q(Lr / 12),
|
||||
"Lb_ft": q(Lb / 12),
|
||||
"rts_in": q(rts),
|
||||
"Fcr_ksi": q(Fcr),
|
||||
"Mp_kipft": q(Mp),
|
||||
"MnLTB_kipft": q(Mn_ltb),
|
||||
"Mn_kipft": q(Mn),
|
||||
"phiMn_kipft": q(phi_mn),
|
||||
"Aw_in2": q(Aw),
|
||||
"lambda": q(lambda_web),
|
||||
"lambda_lim": q(lambda_lim),
|
||||
"kv": q(kv),
|
||||
"Cv1": q(cv1),
|
||||
"phi_v": q(phi_v),
|
||||
"phiVn_kip": q(phi_vn),
|
||||
"delta_limit_in": q(delta_limit),
|
||||
"delta_in": q(delta),
|
||||
"ltb_mode": ltb_mode,
|
||||
}
|
||||
values.update({f"section_{key}": q(float(value)) for key, value in section.items() if key != "label"})
|
||||
return {
|
||||
"tool": "steel_beam",
|
||||
"version": "0.1",
|
||||
"project": inp.get("project", ""),
|
||||
"prepared_by": inp.get("prepared_by", ""),
|
||||
"section": section["label"],
|
||||
"values": values,
|
||||
"checks": {
|
||||
"flexure": {"demand": q(moment_kipft), "capacity": q(phi_mn), "ok": moment_kipft <= phi_mn},
|
||||
"shear": {"demand": q(shear_kip), "capacity": q(phi_vn), "ok": shear_kip <= phi_vn},
|
||||
"deflection": {"demand": q(delta), "capacity": q(delta_limit), "ok": delta <= delta_limit},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def summary(result: dict) -> str:
|
||||
values = result["values"]
|
||||
checks = result["checks"]
|
||||
|
||||
def status(name: str) -> str:
|
||||
return "OK" if checks[name]["ok"] else "NOT OK"
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
"Steel Beam Design Summary",
|
||||
f"Project: {result['project']}",
|
||||
f"Section: {result['section']}",
|
||||
f"Span: {values['beam_length_ft']:.2f} ft",
|
||||
"",
|
||||
"Demands",
|
||||
f" Factored moment, Mu: {values['moment_kipft']:.3f} kip-ft",
|
||||
f" Factored shear, Vu: {values['shear_kip']:.3f} kip",
|
||||
f" Service load: {values.get('service_load_lbf_ft', 'see input')} lbf/ft",
|
||||
"",
|
||||
"Strength",
|
||||
f" Flexure: {status('flexure')} ({values['phiMn_kipft']:.3f} kip-ft capacity, D/C {values['moment_kipft'] / values['phiMn_kipft']:.3f})",
|
||||
f" Shear: {status('shear')} ({values['phiVn_kip']:.3f} kip capacity, D/C {values['shear_kip'] / values['phiVn_kip']:.3f})",
|
||||
f" LTB mode: {values['ltb_mode']}",
|
||||
"",
|
||||
"Serviceability",
|
||||
f" Deflection: {status('deflection')} ({values['delta_in']:.3f} in / {values['delta_limit_in']:.3f} in limit, D/C {values['delta_in'] / values['delta_limit_in']:.3f})",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Calculate the steel beam design 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 a human-readable design summary to stdout")
|
||||
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(summary(result) + "\n")
|
||||
else:
|
||||
output_path.write_text(serialized, encoding="utf-8")
|
||||
print(output_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
25
steel-beam/input.yaml
Normal file
25
steel-beam/input.yaml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Project name shown in the Typst report header.
|
||||
project: "Casablanca Hotel - 16 Steel Joists Replacement"
|
||||
# Person or organization shown in the Typst report footer.
|
||||
prepared_by: "Conemco Engineering"
|
||||
|
||||
# Beam span used for the service deflection limit.
|
||||
beam_length: "9 ft"
|
||||
# Unbraced length of the compression flange used for lateral-torsional buckling.
|
||||
unbraced_length: "9 ft"
|
||||
# Factored major-axis bending demand taken from load-determination.typ.
|
||||
Mu: "8.124 kip * ft"
|
||||
# Factored reaction on the connector, used here as the factored shear demand.
|
||||
Vu: "3.611 kip"
|
||||
# AISC Shapes Database v15.0 label. Section properties are read from the XLSX.
|
||||
section: "W10X15"
|
||||
# Steel yield stress used for flexure and shear (AISC A992).
|
||||
steel_yield: "50 ksi"
|
||||
# Steel elastic modulus used for LTB and deflection.
|
||||
steel_modulus: "29000 ksi"
|
||||
# LTB moment-gradient factor. Use the applicable AISC Cb value.
|
||||
cb: 1
|
||||
# AISC coefficient c for the selected I-shape LTB equation.
|
||||
c: 1
|
||||
# Service uniform load for the deflection check (lbf/ft).
|
||||
service_load: "548.98 lbf/ft"
|
||||
69
steel-beam/results.json
Normal file
69
steel-beam/results.json
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"tool": "steel_beam",
|
||||
"version": "0.1",
|
||||
"project": "Casablanca Hotel - 16 Steel Joists Replacement",
|
||||
"prepared_by": "Conemco Engineering",
|
||||
"section": "W10X15",
|
||||
"values": {
|
||||
"beam_length_ft": 9.0,
|
||||
"unbraced_length_in": 108.0,
|
||||
"factored_uniform_load_kipft": 0.802444,
|
||||
"service_load_lbf_ft": 548.98,
|
||||
"moment_kipft": 8.124,
|
||||
"shear_kip": 3.611,
|
||||
"E_ksi": 29000.0,
|
||||
"Fy_ksi": 50.0,
|
||||
"Lp_in": 34.332994,
|
||||
"Lr_ft": 8.608917,
|
||||
"Lb_ft": 9.0,
|
||||
"rts_in": 1.01,
|
||||
"Fcr_ksi": 32.555772,
|
||||
"Mp_kipft": 66.666667,
|
||||
"MnLTB_kipft": 37.439137,
|
||||
"Mn_kipft": 37.439137,
|
||||
"phiMn_kipft": 33.695224,
|
||||
"Aw_in2": 2.5,
|
||||
"lambda": 38.5,
|
||||
"lambda_lim": 53.946344,
|
||||
"kv": 5.34,
|
||||
"Cv1": 1.0,
|
||||
"phi_v": 1.0,
|
||||
"phiVn_kip": 75.0,
|
||||
"delta_limit_in": 0.45,
|
||||
"delta_in": 0.040559,
|
||||
"ltb_mode": "elastic LTB",
|
||||
"section_A": 4.41,
|
||||
"section_d": 10.0,
|
||||
"section_b": 4.0,
|
||||
"section_tf": 0.25,
|
||||
"section_tw": 0.25,
|
||||
"section_Ix": 68.9,
|
||||
"section_Zx": 16.0,
|
||||
"section_Sx": 13.8,
|
||||
"section_Iy": 2.89,
|
||||
"section_Zy": 2.3,
|
||||
"section_Sy": 1.45,
|
||||
"section_ry": 0.81,
|
||||
"section_J": 0.104,
|
||||
"section_rts": 1.01,
|
||||
"section_ho": 9.72,
|
||||
"section_lambda": 38.5
|
||||
},
|
||||
"checks": {
|
||||
"flexure": {
|
||||
"demand": 8.124,
|
||||
"capacity": 33.695224,
|
||||
"ok": true
|
||||
},
|
||||
"shear": {
|
||||
"demand": 3.611,
|
||||
"capacity": 75.0,
|
||||
"ok": true
|
||||
},
|
||||
"deflection": {
|
||||
"demand": 0.040559,
|
||||
"capacity": 0.45,
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
steel-beam/steel-beam-ref.pdf
Executable file
BIN
steel-beam/steel-beam-ref.pdf
Executable file
Binary file not shown.
1751
steel-beam/steel-beam.pdf
Normal file
1751
steel-beam/steel-beam.pdf
Normal file
File diff suppressed because it is too large
Load diff
84
steel-beam/steel-beam.typ
Normal file
84
steel-beam/steel-beam.typ
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#import "assets/sheet.typ": calcline, calcsheet, check
|
||||
|
||||
#let data = json("results.json")
|
||||
#let n = data.values
|
||||
#let checks = data.checks
|
||||
#let round(value, digits: 2) = calc.round(value, digits: digits)
|
||||
|
||||
#show: calcsheet.with(
|
||||
title: "Steel Beam Design",
|
||||
project: data.project,
|
||||
prepared-by: data.prepared_by,
|
||||
)
|
||||
|
||||
= Steel Beam Design
|
||||
|
||||
Steel beam design for the Casablanca Hotel steel-joist replacement. Demands determined assuming
|
||||
the new beam takes all load, non-composite action with concrete slab.
|
||||
|
||||
== Factored Demands
|
||||
As determined in "Load Determination section"
|
||||
|
||||
#let Mu_lbf = n.moment_kipft * 1000
|
||||
#let Vu_lbf = n.shear_kip * 1000
|
||||
#let L = n.beam_length_ft
|
||||
#let wu_kipft = n.factored_uniform_load_kipft
|
||||
#let wu_lbf = wu_kipft * 1000
|
||||
|
||||
#calcline([$M_u = #round(n.moment_kipft, digits: 2) "kip·ft"$], [Factored moment from load determination])
|
||||
#calcline([$V_u = #round(n.shear_kip, digits: 2) "kip"$], [Factored reaction on connector (factored shear)])
|
||||
#calcline([$L = #L "ft"$], [Beam span])
|
||||
|
||||
#metadata((Mu_kipft: n.moment_kipft, Vu_kip: n.shear_kip)) <load-demands>
|
||||
|
||||
== Material And Section Properties
|
||||
|
||||
#calcline([$E = #n.E_ksi "ksi"$], [Steel Young's modulus])
|
||||
#calcline([$F_y = #n.Fy_ksi "ksi"$], [Steel yield strength])
|
||||
#calcline([$"Section" = #data.section$], [AISC W-shape])
|
||||
#calcline([$d = #n.section_d "in"$, $b = #n.section_b "in"$], [Depth and flange width])
|
||||
#calcline([$t_f = #n.section_tf "in"$, $t_w = #n.section_tw "in"$], [Flange and web thickness])
|
||||
#calcline([$A = #n.section_A "in"^2$], [Section area])
|
||||
#calcline([$r_y = #n.section_ry "in"$], [Weak-axis radius of gyration])
|
||||
#calcline([$S_x = #n.section_Sx "in"^3$, $Z_x = #n.section_Zx "in"^3$], [Elastic and plastic major-axis modulus])
|
||||
#calcline([$S_y = #n.section_Sy "in"^3$, $Z_y = #n.section_Zy "in"^3$], [Elastic and plastic minor-axis modulus])
|
||||
#calcline([$h_o = #n.section_ho "in"$, $J = #n.section_J "in"^4$], [Flange centroid distance and torsional constant])
|
||||
#calcline([$I_x = #n.section_Ix "in"^4$, $I_y = #n.section_Iy "in"^4$], [Major and minor-axis inertia])
|
||||
#calcline([$lambda = #n.lambda$], [Web slenderness ratio])
|
||||
|
||||
== Bending About Major Axis
|
||||
|
||||
#calcline([$L_b = #round(n.Lb_ft, digits: 2) "ft"$], [Unbraced length of compression flange])
|
||||
#calcline([$L_p = 1.76 r_y sqrt(E / F_y) = #round(n.Lp_in / 12, digits: 2) "ft"$], [Limit for yielding])
|
||||
#calcline([$C_b = 1$, $c = 1$], [Moment gradient and I-shape coefficient])
|
||||
#calcline([$r_"ts" = sqrt(I_y h_o / (2 S_x)) = #round(n.section_rts, digits: 2) "in"$], [Effective radius of gyration])
|
||||
#calcline([$L_r = 1.95 r_"ts" E / (0.7 F_y) sqrt((J c)/(S_x h_o) + sqrt(((J c)/(S_x h_o))^2 + 6.76 (0.7 F_y / E)^2)) = #round(n.Lr_ft, digits: 2) "ft"$], [Limit for inelastic torsional buckling])
|
||||
#calcline([$F_"cr" = (C_b pi^2 E)/(L_b/r_"ts")^2 sqrt(1 + 0.078 (J c)/(S_x h_o) (L_b/r_"ts")^2) = #round(n.Fcr_ksi, digits: 2) "ksi"$], [Elastic lateral-torsional-buckling stress])
|
||||
#calcline([$M_p = F_y Z_x = #round(n.Mp_kipft, digits: 2) "kip·ft"$], [Plastic moment])
|
||||
#calcline([$M_n("LTB") = M_p, "for" L_b <= L_p$], [LTB moment for yielding range])
|
||||
#calcline([$M_n("LTB") = C_b [M_p - (M_p - 0.7 F_y S_x)(L_b - L_p)/(L_r - L_p)], "for" L_p < L_b <= L_r$], [LTB moment for inelastic range])
|
||||
#calcline([$M_n("LTB") = F_"cr" S_x, "for" L_b > L_r$], [LTB moment for elastic range])
|
||||
#calcline([$M_n("LTB") = #round(n.MnLTB_kipft, digits: 2) "kip·ft"$], [Lateral-torsional-buckling strength])
|
||||
#calcline([$M_n = min(M_p, M_n("LTB")) = #round(n.Mn_kipft, digits: 2) "kip·ft"$], [Nominal bending capacity])
|
||||
#calcline([$phi M_n = 0.9 M_n = #round(n.phiMn_kipft, digits: 2) "kip·ft"$], [Design bending capacity])
|
||||
|
||||
#check("Flexure", checks.flexure.demand, checks.flexure.capacity, unit: "kip·ft", ok: checks.flexure.ok, demand-label: [$M_u$], capacity-label: [$phi M_n$])
|
||||
|
||||
== Shear Of Web
|
||||
|
||||
#calcline([$A_w = d t_w = #round(n.Aw_in2, digits: 3) "in"^2$], [Web area])
|
||||
#calcline([$lambda_lim = 2.24 sqrt(E / F_y) = #round(n.lambda_lim, digits: 3)$], [Limiting web slenderness])
|
||||
#calcline([$k_v = #n.kv$], [Web shear buckling coefficient])
|
||||
#calcline([$C_"v1" = #n.Cv1$], [Web shear strength coefficient])
|
||||
#calcline([$phi_v = #n.phi_v$], [Shear reduction factor])
|
||||
#calcline([$phi V_n = phi_v 0.6 F_y A_w C_"v1" = #round(n.phiVn_kip, digits: 3) "kip"$], [Design shear capacity])
|
||||
|
||||
#check("Shear", checks.shear.demand, checks.shear.capacity, unit: "kip", ok: checks.shear.ok, demand-label: [$V_u$], capacity-label: [$phi V_n$])
|
||||
|
||||
== Deflection
|
||||
|
||||
#calcline([$delta_max = L / 240 = #round(n.delta_limit_in, digits: 3) "in"$], [Maximum allowed deflection])
|
||||
#calcline([$delta = 5/384 (w L^4) / (E I_x) = #round(n.delta_in, digits: 3) "in"$], [Expected center deflection under service uniform load])
|
||||
|
||||
#check("Deflection", checks.deflection.demand, checks.deflection.capacity, unit: "in", ok: checks.deflection.ok, demand-label: [$delta$], capacity-label: [$delta_"max"$])
|
||||
|
||||
92
steel-beam/test_steel_beam.py
Normal file
92
steel-beam/test_steel_beam.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
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("steel_beam_calc", HERE / "calc.py")
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
|
||||
def result():
|
||||
with (HERE / "input.yaml").open(encoding="utf-8") as handle:
|
||||
return module.compute(yaml.safe_load(handle))
|
||||
|
||||
|
||||
def test_typst_load_demands_match_checked_inputs():
|
||||
subprocess.run([sys.executable, str(HERE / "calc.py")], check=True, cwd=HERE.parents[1])
|
||||
query = subprocess.run(
|
||||
[
|
||||
"typst", "eval", "query(<load-demands>)", "--root", ".",
|
||||
"--in", "calcs/steel-beam/steel-beam.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["Mu_kipft"] == pytest.approx(values["moment_kipft"], abs=0.001)
|
||||
assert derived["Vu_kip"] == pytest.approx(values["shear_kip"], abs=0.001)
|
||||
|
||||
|
||||
def test_ground_truth_section_properties():
|
||||
values = result()["values"]
|
||||
assert values["section_d"] == pytest.approx(5.875)
|
||||
assert values["section_A"] == pytest.approx(2.52)
|
||||
assert values["section_Sx"] == pytest.approx(5.1)
|
||||
assert values["section_Zx"] == pytest.approx(5.73)
|
||||
assert values["section_rts"] == pytest.approx(1.05)
|
||||
|
||||
|
||||
def test_ground_truth_demands_and_flexure():
|
||||
output = result()
|
||||
values = output["values"]
|
||||
assert values["point_load_lbf"] == pytest.approx(7500)
|
||||
assert values["moment_kipft"] == pytest.approx(9.375)
|
||||
assert values["shear_kip"] == pytest.approx(3.75)
|
||||
assert values["Lp_in"] == pytest.approx(37.7, abs=0.1)
|
||||
assert values["Lr_ft"] == pytest.approx(9.48, abs=0.01)
|
||||
assert values["Fcr_ksi"] == pytest.approx(99.57, abs=0.25)
|
||||
assert values["Mp_kipft"] == pytest.approx(23.875)
|
||||
assert values["MnLTB_kipft"] == pytest.approx(21.235, abs=0.01)
|
||||
assert values["phiMn_kipft"] == pytest.approx(19.1, abs=0.1)
|
||||
assert output["checks"]["flexure"]["ok"] is True
|
||||
|
||||
|
||||
def test_ground_truth_shear_and_deflection():
|
||||
output = result()
|
||||
values = output["values"]
|
||||
assert values["Aw_in2"] == pytest.approx(1.102, abs=0.001)
|
||||
assert values["Cv1"] == pytest.approx(1)
|
||||
assert values["phiVn_kip"] == pytest.approx(33.047, abs=0.1)
|
||||
assert values["delta_limit_in"] == pytest.approx(0.25)
|
||||
assert values["delta_in"] == pytest.approx(0.078, abs=0.001)
|
||||
assert all(output["checks"][name]["ok"] for name in ("shear", "deflection"))
|
||||
|
||||
|
||||
def test_invalid_section_is_rejected():
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
module.compute({**yaml.safe_load((HERE / "input.yaml").read_text()), "section": "W0X0"})
|
||||
|
||||
|
||||
def test_cli_can_write_json_to_stdout():
|
||||
completed = subprocess.run(
|
||||
[sys.executable, str(HERE / "calc.py"), "--input", str(HERE / "input.yaml"), "--stdout"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
output = json.loads(completed.stdout)
|
||||
assert output["section"] == "W6X8.5"
|
||||
assert output["values"]["moment_kipft"] == pytest.approx(9.375)
|
||||
assert completed.stderr == ""
|
||||
Loading…
Reference in a new issue