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:
smillmorel 2026-09-21 12:19:20 -04:00
commit d5ac3fca7e
81 changed files with 18668 additions and 0 deletions

207
wood-joist/PROJECT_STATE.md Normal file
View file

@ -0,0 +1,207 @@
# Project State: Wood Joist Analysis
Last updated: 2026-08-20
## Overview
New hybrid (Typst + Python) calculation at `calcs/wood-joist/` that analyzes an
existing wood joist under uniform gravity loads per **NDS 2018 Allowable Stress
Design (ASD)**. Four checks are covered:
1. Flexure (bending) — NDS 3.3 (beam stability factor `C_L`)
2. Shear — NDS 3.4
3. Bearing (compression perpendicular to grain) — NDS 3.10
4. Serviceability (deflection) — NDS 3.5, limits L/240 (total) and L/360 (live)
The default `input.yaml` reproduces a reference worked example ("Analysis for
existing wood joists under shoring loads", Conemco Engineering, project
"Pompano Beach") exactly. The pytest suite locks those numbers.
## Architecture decisions
- **Hybrid pattern** (same as `steel-beam`): `input.yaml` (Pint unit-bearing
quantities) → `calc.py` (computes, writes `results.json`) → `beam.typ`
(presents only, no recomputation) → compiled PDF.
- **`results.json` shape** (unchanged contract): `{tool, version, project,
prepared_by, values, checks}`. `tool = "wood_joist"`, `version = "0.1"`.
- **Pint units**: quantities are quoted strings in YAML (e.g. `"25 psf"`,
`"9 ft"`). `calc.py` converts with a `quantity()` helper identical in spirit
to `steel-beam/calc.py`. Dimensionless adjustment factors are plain floats.
- **`calc.py` CLI** mirrors `steel-beam`: `--input`, `--output`, `--stdout`
flags; runnable as `python calcs/wood-joist/calc.py` with no args.
- **NDS 2018** is the governing standard and edition. All clause references are
to NDS 2018.
- **`beam.typ` imports** from `../../lib/sheet.typ` and reads `results.json`
(no recomputation). Compiled with `--root .` from the worksheets root so the
shared logo resolves.
## 2026-08-20 change: load determination moved to Typst (user-approved)
This change moves wood-joist gravity-load determination from Python to Typst,
mirroring the `steel-beam` pattern. No engineering equations changed — only
where load determination lives. The Pompano Beach benchmark values are
unchanged.
- **`beam.typ` determines loads inline.** It binds `DL = 25 psf`, `LL = 50 psf`,
span `L = 9 ft`, spacing `B = 13 ft` as Typst `#let` values, then derives
`w = (DL + LL)·B`, `wL = LL·B`, `M = w·L²/8`, `V = w·L/2`, `R = V`. These are
emitted as a Typst metadata label `<wood-joist-loads>` with fields
`w_plf`, `wL_plf`, `M_ftlbf`, `V_lbf`, `R_lbf`. `beam.typ` presents both the
Typst load-derived values and the Python-checked values (like steel-beam's
`M_u,load` vs `M_u,design`), and the test suite reconciles them via the
metadata query.
- **New `input.yaml` contract.** `DL`, `LL`, and `spacing` are removed. The
checked demands are supplied as Pint-quantity strings: `w: "975 plf"`,
`wL: "650 plf"`, `M: "9871.875 ft * lbf"`, `V: "4387.5 lbf"`,
`R: "4387.5 lbf"`. `span` is kept (Python needs it for the L/240 and L/360
deflection limits). All other keys are unchanged (`width`, `depth`,
`bearing_length`, `bearing_end_distance`, `unbraced_length`, `species_grade`,
NDS reference values, adjustment factors, `project`, `prepared_by`).
- **`calc.py` reads demands.** It no longer performs load-path arithmetic.
`w_plf`, `wL_plf`, `M_ftlbf`, `V_lbf`, `R_lbf` come from `quantity()` on the
YAML demand keys. Section properties, all NDS 2018 ASD checks, and the
`{tool, version, project, prepared_by, values, checks}` contract are
unchanged (`tool = "wood_joist"`). `values` no longer includes `DL_psf`,
`LL_psf`, or `spacing_ft`.
- **`check_service` added to `lib/sheet.typ`** as a sibling of `check`.
Signature `check_service(label, acting, allowed, unit: "", ok: auto)`.
Identical layout (label, OK/NOT OK badge, colored box, ratio) but the left
column reads **Acting** / **Allowed** (instead of Demand / Capacity) and the
ratio is labeled **Utilization** (instead of D/C). Used only by the two
deflection checks in `calcs/wood-joist/beam.typ`; flexure, shear, and bearing
keep `check`.
- **Convention change.** The inherited "`lib/sheet.typ` must not be modified"
convention is superseded by this explicit user request. The change is
**additive only**: `check` is unchanged and no other sheet
(`steel-beam`, `concrete-beam`, `concrete-beam2`, `shore-post`) is modified.
Pinned benchmark (unchanged): `w` 975 plf, `wL` 650 plf, `M` 9871.88 ft·lbf
(exact 9871.875), `V` 4387.5 lbf, `R` 4387.5 lbf; flexure util 0.938, shear
util 0.66, bearing util 0.772, Δ_total 0.339 in, Δ_live 0.226 in.
## Engineering decisions (pinned for the builder)
The equations are fully pinned by the reference worked example (every
intermediate value and formula is supplied). **No separate engineering
specifier pass was required**; the equations reproduce NDS 2018 directly and
the clause references are recorded here and in `tasks/001`. The reviewer task
(`tasks/005`) still verifies the engineering specification.
### Section properties (rectangular joist)
- `A = b·d`
- `I_x = b·d³ / 12`
- `S_x = b·d² / 6` (= `2·I_x / d`)
### Check 1 — Flexure (NDS 3.3)
- Slenderness: `R_B = sqrt(l_e · d / b²)` (NDS 3.3.3.7). Require `R_B < 50`.
If `R_B >= 50`, `calc.py` raises `ValueError` (member needs lateral bracing;
the `C_L` equation is out of scope).
- `F*_b = F_b · C_D · C_M · C_t · C_F · C_i · C_r` (all factors except `C_L`, `C_fu`).
- `F_bE = 1.20 · E'_min / R_B²`
- `C_L = (1 + F_bE/F*_b)/1.9 − sqrt(((1 + F_bE/F*_b)/1.9)² − (F_bE/F*_b)/0.95)`
(NDS 3.3.3, `c_b = 0.95` for sawn lumber). Formula yields `C_L ≤ 1.0`.
- `F'_b = F*_b · C_L · C_fu`
- `f_b = M / S_x` (M in lbf·in)
- Allowable moment `M_a = S_x · F'_b` (reported in ft·lbf).
### Check 2 — Shear (NDS 3.4)
- `F'_v = F_v · C_D · C_M · C_t · C_vr`
- `f_v = 3·V / (2·A)` (rectangular section, NDS 3.4.1 Eq. 3.4-1)
### Check 3 — Bearing (NDS 3.10, compression perpendicular to grain)
- Bearing area factor `C_b = (l_b + 0.375) / l_b` (NDS 3.10.4; bearing < 6 in
and ≥ 3 in from member end).
- `F'_c⊥ = F_c⊥ · C_M · C_t · C_i · C_b`
- Bearing area `A_b = l_b · b`
- `f_c⊥ = R / A_b` (`R = w·L/2`)
### Check 4 — Serviceability (NDS 3.5)
- `Δ_total = 5·w·L⁴ / (384·E·I_x)`
- `Δ_live = 5·w_L·L⁴ / (384·E·I_x)`
- Limits `L/240` (total) and `L/360` (live). **Hardcoded constants** in
`calc.py` (`240`, `360`) — not YAML inputs — matching the reference.
## Reference example (ground truth to lock)
Project "Pompano Beach", prepared_by "Conemco Engineering".
| Quantity | Value |
|---|---|
| DL, LL | 25 psf, 50 psf |
| span L, spacing B | 9 ft, 13 ft |
| w, w_L | 975 plf, 650 plf |
| b × d | 3.5 in × 9.5 in |
| A, I_x, S_x | 33.25 in², 250.1 in⁴, 52.6 in³ |
| F_b, F_v, F_c⊥, E, E'_min | 2400, 300, 565, 1,700,000, 510,000 psi |
| All adjustment factors | 1.0 |
| M, V, R | 9871.88 ft·lbf, 4387.5 lbf, 4387.5 lbf |
| Flexure util / shear util / bearing util | 0.938 / 0.66 / 0.772 |
| Δ_total, Δ_live | 0.339 in, 0.226 in |
Corrected vs. the source PDF: the shear section label "acting bending stress"
becomes "acting shear stress", and the blank page 3 is omitted. Corrections are
noted in the sheet's Scope section.
## Conventions (inherited)
- `lib/sheet.typ` is shared. It must not be modified except where an explicit
user request authorizes an **additive-only** change (see the 2026-08-20
change: `check_service` added; `check` unchanged).
- No existing calculation (`steel-beam`, `concrete-beam`, `concrete-beam2`,
`shore-post`) may be modified.
- Compile from `worksheets/` with `--root .`.
- Lock the reference example in pytest with `pytest.approx` before treating the
tool as stable.
## Milestones
- 2026-08-19 — Task 001 DONE: `calc.py` + `input.yaml` + `results.json` created.
All five checks pass for the default "Pompano Beach" input; values match the
reference example (flexure 0.938, shear 0.66, bearing 0.772, Δ_total 0.339 in,
Δ_live 0.226 in). See `TASKS.md`.
- 2026-08-19 — Task 002 DONE: `test_wood_joist.py` created with 10 numerical
lock tests; all pass (`10 passed`).
- 2026-08-19 — Task 003 DONE: `beam.typ` created, compiled to
`generated/wood-joist.pdf` (206 KB), and the Typst metadata test appended;
all 11 tests pass (`11 passed`).
- 2026-08-19 — Task 004 DONE: `README.md` and `codemap.md` refreshed with the
wood-joist entry; all 11 tests still pass.
- 2026-08-19 — Task 005 DONE: Reviewer PASS after one fix cycle. Issues found
and fixed: (1) C_L ≤ 1.0 now explicitly enforced with regression test;
(2) bearing applicability limits enforced (l_b < 6 in, end distance ≥ 3 in)
with new `bearing_end_distance` input and tests. Final state: 14 tests pass,
PDF compiles, results.json idempotent. Calculation approved.
- 2026-08-20 — Tasks 006–008 DONE: load determination moved to Typst (mirroring
steel-beam), `check_service` added to `lib/sheet.typ` (Acting/Allowed/
Utilization) and used for the two deflection checks, `input.yaml` switched to
checked demands (`w`, `wL`, `M`, `V`, `R`), tests updated with the
`<wood-joist-loads>` reconciliation test, and README/codemap refreshed.
Reviewer PASS (all 8 checklist items, deterministic evidence verified);
simplify pass clean. Final state: 15 tests pass, PDF compiles, results.json
idempotent, benchmark values unchanged (flexure 0.938, shear 0.66, bearing
0.772, Δ_total 0.339 in, Δ_live 0.226 in).
## Final deliverables
- `calcs/wood-joist/calc.py` — NDS 2018 ASD wood joist analysis (flexure with
C_L, shear, bearing, deflection). CLI: `--input`, `--output`, `--stdout`.
- `calcs/wood-joist/input.yaml` — Pint-quantity inputs; defaults reproduce the
"Pompano Beach" reference example.
- `calcs/wood-joist/test_wood_joist.py` — 14 tests locking the reference values
plus validation guards and the Typst metadata query test.
- `calcs/wood-joist/beam.typ` — presentation sheet (no recomputation), compiled
to `calcs/wood-joist/generated/wood-joist.pdf`.
- `README.md`, `codemap.md` — documented and indexed.
## Known limitations
- Single-span, uniformly loaded, rectangular-section joist only.
- No repetitive-member live-load reduction beyond the explicit `C_r` factor.
- No lateral-torsional restraint beyond the single unbraced length `l_e`.
- Deflection limits hardcoded to L/240 (total) and L/360 (live).
## Dependencies
- Python: `pyyaml`, `pytest`, `pint` (already in `requirements.txt`).
- `typst` CLI for compile and the metadata-query test.
- No new third-party Python packages beyond `requirements.txt`.

160
wood-joist/README.md Normal file
View file

@ -0,0 +1,160 @@
# Worksheets
Typst-first calculation sheets. Use Python only when the arithmetic is no longer comfortable in Typst.
```text
lib/sheet.typ Shared letterhead, calc-line, check, check_service
calcs/shore-post/ Typst-only completed calculation
calcs/concrete-beam/ YAML numbers → Python → JSON → Typst
calcs/concrete-beam2/ YAML quantities → Pint → JSON → Typst
calcs/steel-beam/ YAML quantities → Pint + AISC XLSX → JSON → Typst
calcs/wood-joist/ YAML quantities → Pint → JSON → Typst (NDS 2018 ASD)
calcs/concentric-footing/ YAML quantities → Pint → JSON → Typst (ACI 318-19)
```
Compile every sheet from this folder so the shared logo resolves.
## Typst-only
Edit numbers in `calcs/shore-post/shore-post.typ`.
```bash
typst compile --root . calcs/shore-post/shore-post.typ calcs/shore-post/generated/shore-post.pdf
typst watch --root . calcs/shore-post/shore-post.typ calcs/shore-post/generated/shore-post.pdf
```
## Hybrid
Edit `calcs/concrete-beam/input.yaml`, then:
```bash
python -m pip install -r requirements.txt
python calcs/concrete-beam/calc.py
typst compile --root . calcs/concrete-beam/beam.typ calcs/concrete-beam/generated/beam.pdf
```
Optional output path: `python calcs/concrete-beam/calc.py path/to/input.yaml path/to/results.json`
```bash
python -m pytest calcs/concrete-beam/test_concrete_beam.py
```
`calc.py` writes named numbers. `beam.typ` does not recompute the design.
## Hybrid with units (Pint)
Same calculation pattern with unit-bearing YAML values. Write `span: "16 ft"`
or `span: "192 in"`. Quote every quantity.
```bash
python calcs/concrete-beam2/calc.py
typst compile --root . calcs/concrete-beam2/beam.typ calcs/concrete-beam2/generated/beam.pdf
python -m pytest calcs/concrete-beam2/test_concrete_beam2.py
```
## Templates
Copy one of these folders into `calcs/` and rename it. Compile from the
`worksheets/` directory so the shared logo resolves.
Typst-only:
```bash
typst compile --root . template/typst-only/main.typ template/typst-only/generated/calculation.pdf
```
Typst + Python:
```bash
python template/typst-python/calc.py
typst compile --root . template/typst-python/main.typ template/typst-python/generated/calculation.pdf
python -m pytest template/typst-python/test_template.py
```
## Steel beam
The steel beam sheet reads section properties directly from
`aisc-shapes-database-v15.0.xlsx`. Edit `calcs/steel-beam/input.yaml`, then:
```bash
python calcs/steel-beam/calc.py
typst compile --root . calcs/steel-beam/beam.typ calcs/steel-beam/generated/beam.pdf
python -m pytest calcs/steel-beam/test_steel_beam.py
```
For a standalone JSON result on stdout, use:
```bash
python calcs/steel-beam/calc.py --input calcs/steel-beam/input.yaml --stdout
```
Use `--output path/to/results.json` to select a JSON output path. The reusable
Python function is `compute(input_dict)` in `calcs/steel-beam/calc.py`.
`input.yaml` supplies the checked `Mu` and `Vu` demands. `beam.typ` determines
and displays the gravity loads that produce those demands, and the test suite
queries Typst metadata to verify the values match. The calculation covers
major-axis AISC lateral torsional buckling, web shear, and point-load
deflection. The test fixture locks the supplied `steel-beam.pdf` example for
W6X8.5.
## Wood joist
The wood joist sheet reads reference design values and adjustment factors from
`calcs/wood-joist/input.yaml`, then computes capacity checks per NDS 2018 ASD.
```bash
python calcs/wood-joist/calc.py
typst compile --root . calcs/wood-joist/beam.typ calcs/wood-joist/generated/wood-joist.pdf
python -m pytest calcs/wood-joist/test_wood_joist.py
```
For a standalone JSON result on stdout, use:
```bash
python calcs/wood-joist/calc.py --input calcs/wood-joist/input.yaml --stdout
```
Use `--output path/to/results.json` to select a JSON output path. The reusable
Python function is `compute(input_dict)` in `calcs/wood-joist/calc.py`.
`input.yaml` supplies the checked demands (`w`, `wL`, `M`, `V`, `R` as
Pint-quantity strings), `span`, section dimensions, bearing geometry, NDS
reference design values, and adjustment factors — and no longer supplies `DL`,
`LL`, or `spacing`. `beam.typ` determines the gravity loads inline (`DL = 25
psf`, `LL = 50 psf`, span 9 ft, spacing 13 ft), derives `w`, `wL`, `M`, `V`,
`R`, and presents them beside the Python-checked demands; the test suite
reconciles the two via the `<wood-joist-loads>` metadata query. `calc.py` reads
the demands and computes section properties, flexure (with `C_L`), shear,
bearing, and L/240 + L/360 deflection. The two deflection checks use the shared
`check_service` helper (Acting/Allowed/Utilization); flexure, shear, and
bearing use `check`.
## Concentric footing
Square concentric spread footing under axial load per ACI 318-19 — soil bearing, one-way/two-way shear, flexure, minimum steel, concrete bearing.
```bash
python calcs/concentric-footing/calc.py
typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf
python -m pytest calcs/concentric-footing/test_concentric_footing.py
```
For a standalone JSON result on stdout, use:
```bash
python calcs/concentric-footing/calc.py --input calcs/concentric-footing/input.yaml --stdout
```
Use `--output path/to/results.json` to select a JSON output path. The reusable
Python function is `compute(input_dict)` in `calcs/concentric-footing/calc.py`.
`input.yaml` supplies checked `Ps`/`Pu` (Pint quantities) and footing geometry/materials; `footing.typ` derives `Ps`/`Pu` inline from `DLr=10psf, LLr=20psf, Br=18.9ft, Lr=27.5ft, column 14in×14ft @145pcf` and reconciles via `<concentric-footing-loads>` query; bearing plate width `bp=6in` governs `A1`.
## New calculation
1. Copy the nearer example.
2. Stay in Typst if it is arithmetic you want to see.
3. If you need branching or iteration, write a small `calc.py` that reads YAML and writes `results.json` (`values` and `checks`).
4. Lock one hand example in a pytest before treating the tool as stable.
5. Extract a shared Python module only the second time you need the same procedure.

29
wood-joist/TASKS.md Normal file
View file

@ -0,0 +1,29 @@
# Tasks: Wood Joist Analysis
Ordered, dependency-ordered plan. Each task has a spec in `tasks/NNN_name.md`.
| # | Task | Files | Status | Depends on |
|---|---|---|---|---|
| 001 | Numerical core: `calc.py` + `input.yaml` + `results.json` | calcs/wood-joist/calc.py, calcs/wood-joist/input.yaml, calcs/wood-joist/results.json | DONE | — |
| 002 | Numerical lock: `test_wood_joist.py` (compute() only) | calcs/wood-joist/test_wood_joist.py | DONE | 001 |
| 003 | Presentation: `beam.typ` + compile + Typst metadata test | calcs/wood-joist/beam.typ, calcs/wood-joist/generated/wood-joist.pdf, calcs/wood-joist/test_wood_joist.py (append) | DONE | 001, 002 |
| 004 | Documentation refresh: `README.md` + `codemap.md` | README.md, codemap.md | DONE | 003 |
| 005 | Reviewer pass on the whole calculation | review notes only | DONE | 004 |
| 006 | `check_service` + move load determination to Typst | lib/sheet.typ, calcs/wood-joist/input.yaml, calcs/wood-joist/calc.py, calcs/wood-joist/beam.typ, calcs/wood-joist/results.json | DONE | 001–005 |
| 007 | Test updates + Typst load-demand reconciliation | calcs/wood-joist/test_wood_joist.py | DONE | 006 |
| 008 | Docs refresh (`README.md` + `codemap.md`) + reviewer pass | README.md, codemap.md | DONE | 006, 007 |
## Notes
- Task 001 is the numerical single source of truth; it pins every `results.json`
value name and the exact equations. Tasks 002–005 depend on that contract.
- The Typst metadata test (steel-beam query pattern) lives with Task 003 because
it needs `beam.typ` to exist; Task 002 is purely numerical to keep the
dependency graph acyclic.
- `generated/` may not exist yet; Task 003 creates it before compiling.
- Tasks 006–008 implement the user-approved "move load determination to Typst"
change. Task 006 changes the shared `lib/sheet.typ` (additive `check_service`
only) and the wood-joist contract; Task 007 adapts the tests and adds the
`<wood-joist-loads>` reconciliation test; Task 008 refreshes docs and runs the
reviewer pass verifying deterministic benchmark evidence. The Pompano Beach
benchmark values must stay identical.

Binary file not shown.

BIN
wood-joist/assets/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View 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)
],
)
]
}

187
wood-joist/calc.py Normal file
View file

@ -0,0 +1,187 @@
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("psf = force_pound / foot ** 2")
ureg.define("plf = force_pound / foot") # pounds per linear foot
if "psi" not in ureg:
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
def compute(inp: dict) -> dict:
span_ft = quantity(inp["span"], "ft", "span")
b_in = quantity(inp["width"], "in", "width")
d_in = quantity(inp["depth"], "in", "depth")
lb_in = quantity(inp["bearing_length"], "in", "bearing_length")
bed_in = quantity(inp["bearing_end_distance"], "in", "bearing_end_distance")
le_in = quantity(inp["unbraced_length"], "in", "unbraced_length")
Fb_psi = quantity(inp["Fb"], "psi", "Fb")
Fv_psi = quantity(inp["Fv"], "psi", "Fv")
Fcp_psi = quantity(inp["Fcp"], "psi", "Fcp")
E_psi = quantity(inp["E"], "psi", "E")
Emin_psi = quantity(inp["E_min"], "psi", "E_min")
species_grade = str(inp["species_grade"])
CD = factor(inp["CD"], "CD")
CM = factor(inp["CM"], "CM")
Ct = factor(inp["Ct"], "Ct")
CF = factor(inp["CF"], "CF")
Cr = factor(inp["Cr"], "Cr")
Cfu = factor(inp["Cfu"], "Cfu")
Ci = factor(inp["Ci"], "Ci")
Cvr = factor(inp["Cvr"], "Cvr")
# Checked demands supplied by YAML. beam.typ derives the same values and the
# test suite reconciles them via the <wood-joist-loads> metadata label.
w_plf = quantity(inp["w"], "plf", "w")
wL_plf = quantity(inp["wL"], "plf", "wL")
M_ftlbf = quantity(inp["M"], "ft * lbf", "M")
V_lbf = quantity(inp["V"], "lbf", "V")
R_lbf = quantity(inp["R"], "lbf", "R")
A_in2 = b_in * d_in
Ix_in4 = b_in * d_in ** 3 / 12.0
Sx_in3 = b_in * d_in ** 2 / 6.0
# Flexure (NDS 3.3.3)
RB = math.sqrt(le_in * d_in / b_in ** 2)
if RB >= 50.0:
raise ValueError(f"RB = {RB:.3f} >= 50; member requires additional "
"lateral bracing (NDS 3.3.3)")
Fb_star_psi = Fb_psi * CD * CM * Ct * CF * Ci * Cr
FbE_psi = 1.20 * Emin_psi / RB ** 2
ratio = FbE_psi / Fb_star_psi
CL = (1.0 + ratio) / 1.9 - math.sqrt(((1.0 + ratio) / 1.9) ** 2 - ratio / 0.95)
if not math.isfinite(CL) or CL > 1.0 or CL <= 0:
raise ValueError(f"CL = {CL:.4f} out of range (0, 1]; beam stability "
"factor invalid (NDS 3.3.3)")
Fb_prime_psi = Fb_star_psi * CL * Cfu
fb_psi = M_ftlbf * 12.0 / Sx_in3
Ma_ftlbf = Sx_in3 * Fb_prime_psi / 12.0
# Shear (NDS 3.4)
Fv_prime_psi = Fv_psi * CD * CM * Ct * Cvr
fv_psi = 1.5 * V_lbf / A_in2
# Bearing (NDS 3.10)
if lb_in >= 6.0:
raise ValueError("bearing_length must be < 6 in for the bearing area "
"factor (NDS 3.10.4)")
if bed_in < 3.0:
raise ValueError("bearing_end_distance must be >= 3 in for the bearing "
"area factor (NDS 3.10.4)")
Cb = (lb_in + 0.375) / lb_in
Fcp_prime_psi = Fcp_psi * CM * Ct * Ci * Cb
Ab_in2 = lb_in * b_in
fcp_psi = R_lbf / Ab_in2
# Serviceability (NDS 3.5). Limits hardcoded per reference: L/240, L/360.
L_in = span_ft * 12.0
delta_total_in = 5.0 * (w_plf / 12.0) * L_in ** 4 / (384.0 * E_psi * Ix_in4)
delta_live_in = 5.0 * (wL_plf / 12.0) * L_in ** 4 / (384.0 * E_psi * Ix_in4)
delta_allow_total_in = L_in / 240.0
delta_allow_live_in = L_in / 360.0
def q(value: float) -> float:
return round(value, 6)
values = {
"span_ft": q(span_ft),
"w_plf": q(w_plf), "wL_plf": q(wL_plf),
"b_in": q(b_in), "d_in": q(d_in),
"A_in2": q(A_in2), "Ix_in4": q(Ix_in4), "Sx_in3": q(Sx_in3),
"M_ftlbf": q(M_ftlbf), "V_lbf": q(V_lbf), "R_lbf": q(R_lbf),
"species_grade": species_grade,
"Fb_psi": q(Fb_psi), "Fv_psi": q(Fv_psi), "Fcp_psi": q(Fcp_psi),
"E_psi": q(E_psi), "Emin_psi": q(Emin_psi),
"CD": q(CD), "CM": q(CM), "Ct": q(Ct), "CF": q(CF),
"Cr": q(Cr), "Cfu": q(Cfu), "Ci": q(Ci), "Cvr": q(Cvr),
"le_in": q(le_in), "RB": q(RB),
"Fb_star_psi": q(Fb_star_psi), "FbE_psi": q(FbE_psi),
"CL": q(CL), "Fb_prime_psi": q(Fb_prime_psi),
"fb_psi": q(fb_psi), "Ma_ftlbf": q(Ma_ftlbf),
"Fv_prime_psi": q(Fv_prime_psi), "fv_psi": q(fv_psi),
"lb_in": q(lb_in), "bed_in": q(bed_in), "Cb": q(Cb), "Ab_in2": q(Ab_in2),
"Fcp_prime_psi": q(Fcp_prime_psi), "fcp_psi": q(fcp_psi),
"delta_total_in": q(delta_total_in), "delta_live_in": q(delta_live_in),
"delta_allow_total_in": q(delta_allow_total_in),
"delta_allow_live_in": q(delta_allow_live_in),
}
checks = {
"flexure": {"demand": q(fb_psi), "capacity": q(Fb_prime_psi), "ok": fb_psi <= Fb_prime_psi},
"shear": {"demand": q(fv_psi), "capacity": q(Fv_prime_psi), "ok": fv_psi <= Fv_prime_psi},
"bearing": {"demand": q(fcp_psi), "capacity": q(Fcp_prime_psi), "ok": fcp_psi <= Fcp_prime_psi},
"deflection_total": {"demand": q(delta_total_in), "capacity": q(delta_allow_total_in), "ok": delta_total_in <= delta_allow_total_in},
"deflection_live": {"demand": q(delta_live_in), "capacity": q(delta_allow_live_in), "ok": delta_live_in <= delta_allow_live_in},
}
return {
"tool": "wood_joist",
"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 joist 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 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())

76
wood-joist/codemap.md Normal file
View file

@ -0,0 +1,76 @@
# Codemap: worksheets
Typst-first structural calculation sheets. Hybrid calculations parse YAML inputs
through Pint, compute design values in Python, and present them in Typst.
Generated: 2026-08-19
Files indexed: 24
## Layout
```
worksheets/
├── README.md [doc] — project conventions, template usage, hybrid pattern docs.
├── aisc-shapes-database-v15.0.xlsx [asset] — AISC section-property workbook consumed by steel-beam.
├── requirements.txt [config] — Python deps: pyyaml, pytest, pint.
├── steel-beam.pdf [doc] — reference worked example for the W6X8.5 lock case.
├── WOOD-JOISTS-BENDING-SHEAR-BEARING-DEFLECTION.pdf [doc] — reference sheet for the new wood-joist calculation.
├── assets/
│ └── logo.png [asset] — shared letterhead image resolved from the worksheets root.
├── lib/
│ └── sheet.typ [doc] — shared letterhead, calc-line, check, check_service helpers; no calculation logic.
├── template/
│ ├── typst-only/
│ │ ├── README.md [doc] — pure-Typst template instructions.
│ │ └── main.typ [logic] — pure-Typst sample: arithmetic shown inline, no results.json.
│ └── typst-python/
│ ├── README.md [doc] — hybrid template instructions.
│ ├── calc.py [logic] — minimal YAML-driven compute(); writes results.json with values+checks.
│ ├── test_template.py [test] — locks one hand example with pytest.approx.
│ └── input.yaml [config] — template fixture inputs.
├── calcs/
│ ├── shore-post/ — pure-Typst reference calculation.
│ │ ├── shore-post.typ [logic] — tributary area, axial demand, AS550 shore-post capacity check.
│ │ ├── assets/tributary-areas.png [asset] — figure used by shore-post.typ.
│ │ └── generated/shore-post.pdf [doc] — last compiled artefact (regenerable).
│ ├── concrete-beam/ — hybrid YAML numbers → Python → JSON → Typst.
│ │ ├── calc.py [logic] — span, demand, Whitney block, phiMn, phiVc, min steel.
│ │ ├── test_concrete_beam.py [test] — locks example demands and capacities.
│ │ ├── input.yaml [config] — plain-number inputs.
│ │ ├── beam.typ [logic] — presents calc.py values; embeds beam sketch.
│ │ └── results.json [state] — last calc.py output (regenerable).
│ ├── concrete-beam2/ — hybrid YAML quantities + Pint unit conversion.
│ │ ├── calc.py [logic] — Pint-parses quantities; same procedure as concrete-beam.
│ │ ├── test_concrete_beam2.py [test] — locks Pint-parsed demands and capacities.
│ │ ├── input.yaml [config] — unit-bearing inputs ("16 ft", "3000 psi").
│ │ ├── beam.typ [logic] — presents calc.py values; embeds beam sketch.
│ │ └── results.json [state] — last calc.py output (regenerable).
│ ├── wood-joist/ — hybrid YAML quantities + Pint → JSON → Typst (NDS 2018 ASD).
│ │ ├── calc.py [logic] — section props, flexure (C_L), shear, bearing, deflection; reads YAML demands.
│ │ ├── test_wood_joist.py [test] — locks Pompano Beach example; queries <wood-joist-loads> and <wood-joist-results>.
│ │ ├── input.yaml [config] — Pint-quantity checked demands (w, wL, M, V, R), span, geometry, NDS design values.
│ │ ├── beam.typ [logic] — derives gravity loads (w, wL, M, V, R) and presents Python-checked values.
│ │ ├── results.json [state] — last calc.py output (regenerable).
│ │ └── generated/wood-joist.pdf [doc] — last compiled artefact (regenerable).
│ └── steel-beam/ — hybrid YAML quantities + Pint + AISC XLSX loader.
│ ├── calc.py [logic] — loads section from XLSX; AISC LTB, web shear, point-load deflection.
│ ├── test_steel_beam.py [test] — runs typst query(<load-demands>); locks W6X8.5 ground truth.
│ ├── input.yaml [config] — Pint-quantity inputs (span, Mu, Vu, section label).
│ ├── beam.typ [logic] — load determination shown in Typst; LTB/shear/deflection from Python.
│ └── results.json [state] — last calc.py output (regenerable).
└── tasks/ [doc] — Architect-written task specifications; one per builder task.
```
## Hot Spots
- `lib/sheet.typ` — shared by every sheet. Changing `calculation-sheet`, `calc-line`, or `check` breaks every compiled PDF.
- `aisc-shapes-database-v15.0.xlsx` — steel-beam fixture; column mapping in `calc.py:load_section` is brittle to schema changes.
- `calcs/steel-beam/calc.py` — single-source of truth for the steel-beam capacity numbers; test suite asserts exact values.
- `calcs/wood-joist/calc.py` — single source of truth for the NDS wood-joist capacity numbers; the test suite asserts the locked Pompano Beach values.
- `README.md` — documents the hybrid contract (Python computes, Typst presents). Keep the contract consistent across new sheets.
## Conventions
- Hybrid calculations: `input.yaml` (units via Pint) → `calc.py` (computes) → `results.json` (values + checks) → `beam.typ` (presents only). Wood-joist now derives loads in Typst (like steel-beam) while `calc.py` reads the checked demands. `check_service` is a presentation helper.
- Pure-Typst sheets are reserved for arithmetic the engineer wants to see inline; calculations with branching or iteration move to Python.
- The shared logo resolves only when sheets are compiled with `--root .` from the worksheets directory.

46
wood-joist/input.yaml Normal file
View file

@ -0,0 +1,46 @@
# Project name shown in the Typst report header.
project: "Pompano Beach"
# Person or organization shown in the Typst report footer.
prepared_by: "Conemco Engineering"
# Checked demands (ASD) supplied for verification. beam.typ derives the same
# gravity loads and the test suite reconciles them to these values.
w: "975 plf" # total uniform line load (w = (D + LL) B)
wL: "650 plf" # live uniform line load (wL = LL B)
M: "9871.875 ft * lbf" # maximum moment (M = w L^2 / 8)
V: "4_387.5 lbf" # support shear (V = w L / 2)
R: "4387.5 lbf" # support reaction (R = V)
# Joist geometry: span (needed for the L/240 and L/360 deflection limits).
span: "9 ft"
# Joist cross-section (actual dimensions).
width: "3.5 in" # b
depth: "9.5 in" # d
# Bearing length at each support (wood beam).
bearing_length: "2.5 in" # l_b
# Distance from member end to near edge of bearing; must be >= 3 in for C_b to apply (NDS 3.10.4).
bearing_end_distance: "3 in" # l_end
# Unbraced length of the compression edge used for beam stability.
unbraced_length: "1 in" # l_e
# Species and grade label shown in the report.
species_grade: "SP No. 2"
# NDS 2018 reference design values (Spruce-Pine No. 2).
Fb: "2400 psi" # bending (F_b)
Fv: "300 psi" # shear (F_v)
Fcp: "565 psi" # compression perpendicular to grain (F_c-perp)
E: "1700000 psi" # modulus of elasticity (E)
E_min: "510000 psi" # adjusted modulus for stability (E'_min)
# Adjustment factors (NDS Table 4.3.1). All 1.0 for this example.
CD: 1 # load duration factor
CM: 1 # wet service factor
Ct: 1 # temperature factor
CF: 1 # size factor (bending)
Cr: 1 # repetitive member factor
Cfu: 1 # flat use factor (bending)
Ci: 1 # incising factor
Cvr: 1 # shear reduction factor (NDS 3.4.3)

80
wood-joist/results.json Normal file
View file

@ -0,0 +1,80 @@
{
"tool": "wood_joist",
"version": "0.1",
"project": "Pompano Beach",
"prepared_by": "Conemco Engineering",
"values": {
"span_ft": 9.0,
"w_plf": 975.0,
"wL_plf": 650.0,
"b_in": 3.5,
"d_in": 9.5,
"A_in2": 33.25,
"Ix_in4": 250.067708,
"Sx_in3": 52.645833,
"M_ftlbf": 9871.875,
"V_lbf": 4387.5,
"R_lbf": 4387.5,
"species_grade": "SP No. 2",
"Fb_psi": 2400.0,
"Fv_psi": 300.0,
"Fcp_psi": 565.0,
"E_psi": 1700000.0,
"Emin_psi": 510000.0,
"CD": 1.0,
"CM": 1.0,
"Ct": 1.0,
"CF": 1.0,
"Cr": 1.0,
"Cfu": 1.0,
"Ci": 1.0,
"Cvr": 1.0,
"le_in": 1.0,
"RB": 0.880631,
"Fb_star_psi": 2400.0,
"FbE_psi": 789157.894737,
"CL": 0.999848,
"Fb_prime_psi": 2399.634053,
"fb_psi": 2250.178077,
"Ma_ftlbf": 10527.561199,
"Fv_prime_psi": 300.0,
"fv_psi": 197.932331,
"lb_in": 2.5,
"bed_in": 3.0,
"Cb": 1.15,
"Ab_in2": 8.75,
"Fcp_prime_psi": 649.75,
"fcp_psi": 501.428571,
"delta_total_in": 0.338572,
"delta_live_in": 0.225714,
"delta_allow_total_in": 0.45,
"delta_allow_live_in": 0.3
},
"checks": {
"flexure": {
"demand": 2250.178077,
"capacity": 2399.634053,
"ok": true
},
"shear": {
"demand": 197.932331,
"capacity": 300.0,
"ok": true
},
"bearing": {
"demand": 501.428571,
"capacity": 649.75,
"ok": true
},
"deflection_total": {
"demand": 0.338572,
"capacity": 0.45,
"ok": true
},
"deflection_live": {
"demand": 0.225714,
"capacity": 0.3,
"ok": true
}
}
}

View 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)

2090
wood-joist/wood-joist.pdf Normal file

File diff suppressed because it is too large Load diff

108
wood-joist/wood-joist.typ Normal file
View file

@ -0,0 +1,108 @@
#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: "Wood Joist Analysis",
project: data.project,
prepared-by: data.prepared_by,
)
= Wood Joist Analysis
Simple-span wood joist under uniform gravity load, analyzed per NDS 2018 ASD.
Pint parses `input.yaml`, `calc.py` computes the member capacities and checks.
Typst determines the gravity-load demands below; the checked Python inputs
are reconciled to those values by the test suite.
== Loads Determination
#let DL = 25
#let LL = 50
#let L = 9
#let B = 13
#let w = (DL + LL) * B
#let wL = LL * B
#let M = w * L * L / 8
#let V = w * L / 2
#let R = V
#metadata((w_plf: w, wL_plf: wL, M_ftlbf: M, V_lbf: V, R_lbf: R)) <wood-joist-loads>
#calcline([$L = #L " ft"$], [Simple span])
#calcline([$B = #B " ft"$], [Tributary spacing, on-center])
#calcline([$D = #DL " psf"$], [Dead load])
#calcline([$L_L = #LL " psf"$], [Live load])
#calcline([$w = (D + L_L) B = #w " plf"$], [Total uniform line load, Typst-derived])
#calcline([$w_("checked") = #round(n.w_plf, digits: 1) " plf"$], [Total uniform line load, checked input])
#calcline([$w_L = L_L B = #wL " plf"$], [Live uniform line load, Typst-derived])
#calcline([$w_("L,checked") = #round(n.wL_plf, digits: 1) " plf"$], [Live uniform line load, checked input])
#calcline([$M = w L^2 / 8 = #round(M, digits: 2) " ft·lbf"$], [Maximum moment, Typst-derived])
#calcline([$M_("checked") = #round(n.M_ftlbf, digits: 2) " ft·lbf"$], [Maximum moment, checked input])
#calcline([$V = w L / 2 = #V " lbf"$], [Support shear, Typst-derived])
#calcline([$V_("checked") = #round(n.V_lbf, digits: 1) " lbf"$], [Support shear, checked input])
#calcline([$R = V = #R " lbf"$], [Support reaction, Typst-derived])
#calcline([$R_("checked") = #round(n.R_lbf, digits: 1) " lbf"$], [Support reaction, checked input])
== Section Properties
#calcline([$b = #n.b_in " in"$, $d = #n.d_in " in"$], [Joist width and depth])
#calcline([$A = b d = #round(n.A_in2, digits: 2) " in"^2$], [Section area])
#calcline([$I_x = b d^3 / 12 = #round(n.Ix_in4, digits: 1) " in"^4$], [Moment of inertia])
#calcline([$S_x = b d^2 / 6 = #round(n.Sx_in3, digits: 1) " in"^3$], [Elastic section modulus])
== Material And Adjustment Factors
#calcline([$"Species/grade" = #n.species_grade$], [NDS Supplement reference values])
#calcline([$F_b = #n.Fb_psi " psi"$, $F_v = #n.Fv_psi " psi"$], [Reference bending and shear])
#calcline([$F_(c"⊥") = #n.Fcp_psi " psi"$], [Reference compression perpendicular to grain])
#calcline([$E = #n.E_psi " psi"$, $E'_"min" = #n.Emin_psi " psi"$], [Moduli of elasticity])
#calcline([$C_D = #n.CD$, $C_M = #n.CM$, $C_t = #n.Ct$, $C_F = #n.CF$], [Load duration, wet service, temperature, size])
#calcline([$C_r = #n.Cr$, $C_"fu" = #n.Cfu$, $C_i = #n.Ci$], [Repetitive member, flat use, incising])
== Flexure
#calcline([$l_e = #n.le_in " in"$], [Unbraced length of compression edge])
#calcline([$R_B = sqrt(l_e d / b^2) = #round(n.RB, digits: 3)$], [Slenderness ratio, NDS 3.3.3])
#calcline([$F^*_b = F_b C_D C_M C_t C_F C_i C_r = #round(n.Fb_star_psi, digits: 1) " psi"$], [Adjusted reference bending])
#calcline([$F_("bE") = 1.20 E'_"min" / R_B^2 = #round(n.FbE_psi, digits: 1) " psi"$], [Critical buckling stress])
#calcline([$C_L = (1 + F_("bE")/F^*_b)/1.9 - sqrt(...) = #round(n.CL, digits: 4)$], [Beam stability factor, NDS 3.3.3])
#calcline([$F'_b = F^*_b C_L C_"fu" = #round(n.Fb_prime_psi, digits: 1) " psi"$], [Adjusted bending])
#calcline([$f_b = M / S_x = #round(n.fb_psi, digits: 1) " psi"$], [Acting bending stress])
#calcline([$M_a = S_x F'_b = #round(n.Ma_ftlbf, digits: 1) " ft·lbf"$], [Allowable moment])
#v(7pt)
#check("Flexure", checks.flexure.demand, checks.flexure.capacity, unit: "psi", ok: checks.flexure.ok, demand-label: [$f_b$], capacity-label: [$F'_b$])
== Shear
#calcline([$F'_v = F_v C_D C_M C_t C_"vr" = #round(n.Fv_prime_psi, digits: 1) " psi"$], [Adjusted shear])
#calcline([$f_v = 3 V / (2 A) = #round(n.fv_psi, digits: 3) " psi"$], [Acting shear stress])
#v(7pt)
#check("Shear", checks.shear.demand, checks.shear.capacity, unit: "psi", ok: checks.shear.ok, demand-label: [$f_v$], capacity-label: [$F'_v$])
== Bearing
#calcline([$l_b = #n.lb_in " in"$], [Bearing length])
#calcline([$l_("end") = #n.bed_in " in"$], [Distance from member end to near edge of bearing, >= 3 in])
#calcline([$C_b = (l_b + 0.375) / l_b = #round(n.Cb, digits: 3)$], [Bearing area factor, NDS 3.10.4])
#calcline([$F'_("c⊥") = F_("c⊥") C_M C_t C_i C_b = #round(n.Fcp_prime_psi, digits: 2) " psi"$], [Adjusted bearing])
#calcline([$A_b = l_b b = #round(n.Ab_in2, digits: 2) " in"^2$], [Bearing area])
#calcline([$f_("c⊥") = R / A_b = #round(n.fcp_psi, digits: 2) " psi"$], [Acting bearing stress])
#v(7pt)
#check("Bearing", checks.bearing.demand, checks.bearing.capacity, unit: "psi", ok: checks.bearing.ok, demand-label: [$f_("c⊥")$], capacity-label: [$F'_("c⊥")$])
== Deflection
#calcline([$Delta_"total" = 5 w L^4 / (384 E I_x) = #round(n.delta_total_in, digits: 3) " in"$], [Total-load deflection])
#calcline([$Delta_"live" = 5 w_L L^4 / (384 E I_x) = #round(n.delta_live_in, digits: 3) " in"$], [Live-load deflection])
#calcline([$L/240 = #n.delta_allow_total_in " in"$], [Total-load limit])
#calcline([$L/360 = #n.delta_allow_live_in " in"$], [Live-load limit])
#v(7pt)
#check("Deflection (total)", checks.deflection_total.demand, checks.deflection_total.capacity, unit: "in", ok: checks.deflection_total.ok, demand-label: [$Delta_"total"$], capacity-label: [$L/240$])
#check("Deflection (live)", checks.deflection_live.demand, checks.deflection_live.capacity, unit: "in", ok: checks.deflection_live.ok, demand-label: [$Delta_"live"$], capacity-label: [$L/360$])