calcs/concentric-footing/tasks/003_typst_sheet.md
smillmorel d5ac3fca7e Add structural calculation worksheets
Collection of engineering calculation projects (Python + Typst), each with
input, calc script, tests, results, and generated PDF where available.
2026-09-21 12:19:20 -04:00

10 KiB
Raw Permalink Blame History

Task 003 — Presentation: footing.typ + compile + Typst metadata tests

Goal

Present the checked numbers in a printable Typst sheet that also derives the Blavatnik gravity loads inline, emits queryable metadata, and compiles to PDF.

Background

Architecture: footing.typ imports ../../lib/sheet.typ and json("results.json"). It must not recompute capacities; it only presents results.json values via calc-line and check boxes. Load determination (roof DL/LL, tributary area, column self weight) is derived in Typst and reconciled to Python's checked Ps/Pu by the test suite — mirroring wood-joist and steel-beam where <wood-joist-loads> is queried.

The check, calc-line, and calculation-sheet helpers are defined in lib/sheet.typ (worksheets root). Signatures used here: check(label, demand, capacity, unit:, ok:), calc-line(content, note), and calculation-sheet.with(title:, project:, prepared-by:).

Files to Modify

  • calcs/concentric-footing/footing.typ — create.
  • calcs/concentric-footing/generated/footing.pdf — create via typst compile --root ..
  • calcs/concentric-footing/test_concentric_footing.py — append two Typst query tests (do not rewrite existing tests); create generated/ dir if needed.

Implementation

1. footing.typ structure

Follow calcs/wood-joist/beam.typ as template:

  • Header:

    #import "../../lib/sheet.typ": calc-line, calculation-sheet, 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: calculation-sheet.with(title: "Concentric Footing Analysis", project: data.project, prepared-by: data.prepared_by)
    = Concentric Footing Analysis
    Square spread footing under concentric axial load, ACI 318-19. Numbers come from `calc.py`; this sheet only presents them. Typst derives gravity loads below; checked Python demands are reconciled by the test suite.
    
  • Add a simple footing sketch using Typst box/rect/line drawing (no external image needed). Example: centered square footing with column on top, label Bf and Df. Wrap in #figure(..., caption: [Footing plan and section: #n.Bf_ft ft × #n.Bf_ft ft × #n.Df_in in, d=#n.d_in in.]). Keep geometry text as calc-line below.

  • Section == Loads Determination: Derive Blavatnik loads in Typst (visible arithmetic):

    #let DLr = 10  // psf
    #let LLr = 20  // psf
    #let Br = 18.9 // ft
    #let Lr = 27.5 // ft
    #let Ar = Br * Lr  // ft2
    #let bc = 14  // in
    #let Lc = 14  // ft
    #let gamma_c = 145 // pcf
    #let Wc_kip = bc * bc / 144 * Lc * gamma_c / 1000 // kip column weight
    #let Ps_typst = (DLr + LLr) * Ar / 1000 + Wc_kip // kip
    #let Pu_typst = 1.2*(DLr*Ar/1000 + Wc_kip) + 1.6*LLr*Ar/1000
    #metadata((Ps_kip: Ps_typst, Pu_kip: Pu_typst, Ar_ft2: Ar, Wc_kip: Wc_kip)) <concentric-footing-loads>
    

    Then emit calc-line rows:

    • Ar = Br·Lr = #round(Ar,2) ft2 (tributary area)
    • Wc = bc·bc·Lc·γc = #round(Wc_kip,2) kip (column self weight)
    • Ps = (DLr+LLr)·Ar + Wc = #round(Ps_typst,2) kip (Typst-derived)
    • Ps_checked = #round(n.Ps_kip,2) kip (Python-checked)
    • Pu = 1.2(DLr·Ar+Wc)+1.6·LLr·Ar = #round(Pu_typst,2) kip (Typst-derived)
    • Pu_checked = #round(n.Pu_kip,2) kip
    • Also show qu = Pu/Af = #round(n.qu_psf,1) psf for context but do not recompute Af.
  • Section == Geometry and Materials: calc-line for Bf, Df, cover, d, Af, c, bp, fc, fy, lambda, N, rebar_size, As, rho. Example: calc-line([$B_f = #n.Bf_ft " ft"$, $A_f = #n.Af_ft2 " ft"^2$], [Footing plan area]) etc. Show As1, As, rho, rho_min.

  • Section == Soil Bearing: calc-line([$q = P_s/A_f = #round(n.q_psf,1) " psf"$], [Acting service pressure]) calc-line([$q_a = #n.qa_psf " psf"$], [Allowable]) Then #check("Soil bearing", checks.soil_bearing.demand, checks.soil_bearing.capacity, unit: "psf", ok: checks.soil_bearing.ok) Also show qu as info line.

  • Section == One-Way Shear: calc-line([$L_1 = (B_f - c)/2 - d = #round(n.L1_in,2) " in"$], [Cantilever beyond d]) calc-line([$V_u = q_u B_f L_1 = #round(n.Vu_one_way_kip,2) " kip"$], [Demand at d]) calc-line([$V_c = 2 lambda sqrt(f'_c) B_f d = #round(n.Vc_one_way_kip,1) " kip"$], [ACI 22.5]) calc-line([$phi V_c = #round(n.phiVc_one_way_kip,1) " kip"$], [phi=0.75]) #check("One-way shear", checks.one_way_shear.demand, checks.one_way_shear.capacity, unit: "kip", ok: checks.one_way_shear.ok)

  • Section == Two-Way Shear (Punching): calc-line([$b_o = 4(c+d)= #n.bo_in " in"$], [Critical perimeter, d/2]) calc-line([$v_c = min(4,2+4/beta,2+alpha d/b_o) lambda sqrt(f'_c)= #round(n.vc_psi,1) " psi"$], [ACI 22.6]) calc-line([$V_c = v_c b_o d = #round(n.Vc_two_way_kip,1) " kip"$], []) calc-line([$V_u = q_u(A_f - (c+d)^2)= #round(n.Vu_two_way_kip,1) " kip"$], [Punch demand]) #check("Two-way shear", checks.two_way_shear.demand, checks.two_way_shear.capacity, unit: "kip", ok: checks.two_way_shear.ok)

  • Section == Flexure: calc-line([$L_c = (B_f - c)/2 = #round(n.Lc_in,1) " in"$], [Cantilever]) calc-line([$M_u = q_u B_f L_c^2/2 = #round(n.Mu_kipft,2) " kip·ft"$], []) calc-line([$a = A_s f_y/(0.85 f'_c B_f)= #round(n.a_in,3) " in"$], [Whitney block]) calc-line([$M_n = A_s f_y(d -a/2)= #round(n.Mn_kipft,1) " kip·ft"$], []) calc-line([$rho = A_s/(B_f d)= #round(n.rho,4)$], [vs rho_min 0.0018]) #check("Flexure", checks.flexure.demand, checks.flexure.capacity, unit: "kip·ft", ok: checks.flexure.ok) #check("Minimum steel", checks.minimum_steel.demand, checks.minimum_steel.capacity, unit: "", ok: checks.minimum_steel.ok) — for rho, show ratio but check helper expects demand/capacity; use demand=rho_min capacity=rho; label accordingly.

  • Section == Concrete Bearing: calc-line([$A_1 = b_p^2 = #round(n.A1_in2,1) " in"^2$, $A_2 = B_f^2 = #round(n.A2_in2,1) " in"^2$], [Plate and footing]) calc-line([$sqrt(A_2/A_1)= #round(n.sqrt_ratio,2)$, capped at 2], []) calc-line([$B_n = 0.85 f'_c A_1 sqrt(...)= #round(n.Bn_kip,1) " kip"$], [ACI 22.8]) calc-line([$phi B_n = #round(n.phiBn_kip,1) " kip"$], [phi=0.65]) #check("Concrete bearing", checks.bearing.demand, checks.bearing.capacity, unit: "kip", ok: checks.bearing.ok)

  • Section == Scope And Limitations: Text stating square footing only, interior concentric axial only, gross pressure, no moment/overturning/sliding, no settlement, d from cover to centroid, bo correction noted (reference 68in corrected to ACI 92in), plate vs column clarification, no deflection/crack/development, final design by engineer.

  • Footer metadata for reconciliation:

    #metadata((soil_util: checks.soil_bearing.demand/checks.soil_bearing.capacity, one_way_util: checks.one_way_shear.demand/checks.one_way_shear.capacity, two_way_util: checks.two_way_shear.demand/checks.two_way_shear.capacity, flexure_util: checks.flexure.demand/checks.flexure.capacity, bearing_util: checks.bearing.demand/checks.bearing.capacity, q_psf: n.q_psf, qu_psf: n.qu_psf)) <concentric-footing-results>
    

Ensure all n.* keys match Task 001 values dict exactly.

2. Compile

From worksheets/ run:

python calcs/concentric-footing/calc.py
typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf

Verify PDF exists and >50KB.

3. Append Typst query tests

Append to calcs/concentric-footing/test_concentric_footing.py two new tests (do not delete existing 10):

  • test_typst_compiles_and_presents_python_numbers(result) — runs calc.py with subprocess.run([sys.executable, str(HERE/"calc.py")], check=True, cwd=HERE.parents[1]), then typst compile ... check pdf exists, then typst query for <concentric-footing-results> via typst eval "query(<concentric-footing-results>)"? Actually use typst eval or typst query command as in wood-joist: subprocess.run(["typst","query", ...])? Wood-joist uses typst compile and typst eval query. Use same pattern as wood-joist test:

    subprocess.run(["typst","compile","--root",".",...], check=True, cwd=HERE.parents[1])
    query = subprocess.run(["typst","query","..."]?) 
    

    Check wood-joist: it runs ["typst","eval","query(<wood-joist-results>)","--root",...,"--in","calcs/wood-joist/beam.typ","--format","json"]. Mirror that: ["typst","query", str(HERE/"footing.typ"), "<concentric-footing-results>", "--root", "."] may vary; adapt to whatever works but assert exactly one entry and that flexure_util approx equals Python f. Specifically query the published metadata and assert meta["flexure_util"] approx values["Mu"]/values["phiMn"] etc for soil, one-way, two-way, bearing.

  • test_typst_load_demands_match_checked_inputs(result) — queries <concentric-footing-loads> and asserts derived Ps_kip approx values["Ps_kip"] within 0.1 kip (allow rounding of column weight 18.36 vs 18.4) and same for Pu_kip.

Implementation must be robust: if typst query syntax differs, use typst compile + typst eval as in wood-joist. Ensure tests pass on the CI image where typst is installed.

If generated/ does not exist, create it with Path(...).mkdir(parents=True, exist_ok=True).

Acceptance Criteria

  1. footing.typ imports only lib/sheet.typ and results.json; no arithmetic beyond load derivation and rounding.
  2. Every check box reflects checks.*.ok from JSON; D/C printed as demand/capacity.
  3. typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf succeeds and PDF exists.
  4. Two appended pytest tests pass, reconciling Typst-derived Ps/Pu to Python-checked Ps/Pu and Typst-presented utils to Python values.
  5. All prior 10 tests still pass (total 12).
  6. No existing calc files modified; generated/ is git-ignored regenerable.

Tests

Builder runs:

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 -v

Expected: 12 passed, PDF exists.

Dependencies

Tasks 001, 002 must be DONE.