🧮 Engineering Calculations in Python: No Excel Required¶

Somewhere on your desktop there is a file called beam_design_v7_FINAL_final.xlsx. It has 4 tabs. Two of them are broken. Nobody knows which two. 😬

This notebook is the intervention.

handcalcs intercepts Python assignments and renders them as LaTeX — three columns, symbolic form on the left, numeric result on the right, exactly like a hand calculation sheet. forallpeople wraps SI units into live objects: 600 * mm is not the number 600 but a physical quantity, and M / I * y gives stress, with units, automatically. Multiply the wrong things: get an error, before you submit the report.

The full calculation below is ~60 lines of code. It renders as a typeset calc sheet. It lives in git, runs in 10 seconds, and survives a client changing the span.


Library What it does
🔢 handcalcs Renders Python math as LaTeX. Write code, get calc sheet.
📐 forallpeople SI units through every operation. Wrong units = instant error.

What we will calculate¶

  1. 📦 Bending moment from uniform load
  2. 📏 Section centroid and second moment of area ($I$, $\bar{y}$)
  3. 💪 Elastic bending stress and strain (uncracked section — for illustration only, not the ULS basis)
  4. 🔩 Required tension reinforcement (EC2 simplified rectangular stress block, ULS)
  5. 📊 Plots — section, stress diagram, rebar comparison

You could use FEA for this. We will not.


⚙️ Setup¶

Install once:

pip install handcalcs forallpeople matplotlib numpy

💡 Tip: Use a virtual environment. You know this. You will ignore this advice. That is fine.

In [1]:
import handcalcs.render          # registers the %%render cell magic
from handcalcs.render import render
import forallpeople as si
si.environment('structural', top_level=True)   # injects mm, m, kN, MPa, etc. into global scope
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.gridspec import GridSpec

🧰 The Tools — Full Feature Tour¶

Before we crunch numbers, here is everything handcalcs and forallpeople can do. We will use all of it in this notebook.


handcalcs — render modes¶

Magic What it does
%%render Default. Shows: symbol = expanded formula = numeric result
%%render params Parameters only. No symbolic expansion — just name and value.
%%render long Forces every expression onto its own line, even short ones.
%%render symbolic Shows only the symbolic form — no numbers at all.
render(expr) Inline function — render a single expression anywhere in a cell.

Comment tricks inside any %%render cell:

Comment Effect
# Label at end of line handcalcs uses your comment as the description label
Works on any line Keeps rendered output readable without variable-name guesswork

forallpeople — how units work¶

si.environment('structural', top_level=True) injects these into global scope:

mm  cm  m   km          (length)
N   kN  MN              (force)
Pa  kPa MPa GPa         (stress / pressure)
kg  t                   (mass)
kN/m    kN/m2           (distributed loads)

Rules:

  • ✅ 600 * mm → stored as 0.6 m internally, displayed as 600 mm
  • ✅ (15 * kN/m) * (6*m)**2 / 8 → result is 67.5 kN·m — units simplified automatically
  • ✅ sigma / E_c → Pa/Pa → dimensionless (no .value needed)
  • ❌ (25 * MPa) + (600 * mm) → TypeError — adding stress to length is always wrong

💡 Use float(x / unit) to extract a plain number when needed (comparisons, numpy, etc.).


1. 🏗️ The Problem¶

         q = 15 kN/m (uniform load)
    ←————————————————————————————→
    ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
    △                            △
    |←————————  L = 6 m  ————————→|

T-section (floor beam — slab acts as compression flange):

    |←——  b_f = 600 mm  ——→|
    ████████████████████████  ← flange (slab), h_f = 100 mm
           ████████           ← web (beam stem)
           ████████           h = 450 mm total
           ████████
           ● ●  ● ●           ← tension rebar (what we are finding)
           |←b_w→|            b_w = 250 mm

Materials: C25/30 concrete ($f_{ck} = 25$ MPa), B500 reinforcement ($f_{yk} = 500$ MPa).

Goal: Find the required tension steel area $A_s$ at mid-span. 🎯


2. 📐 Geometry and Material Properties¶

%%render params — the clean way to define inputs. No symbolic expansion (nobody needs to see b_f = b_f), just a tidy parameter table. 👇

In [2]:
%%render params
# Section geometry
b_f = 600 * mm     # flange width
b_w = 250 * mm     # web width
h_f = 100 * mm     # flange thickness
h   = 450 * mm     # total section depth
h_w = h - h_f      # web height
c_nom = 35 * mm    # nominal cover to rebar
phi   = 20 * mm    # assumed rebar diameter
d     = h - c_nom - phi / 2    # effective depth to rebar centroid
$$ \begin{aligned} b_{f} &= 600.000\ \mathrm{mm} \; \;\textrm{(flange width)} &b_{w} &= 250.000\ \mathrm{mm} \; \;\textrm{(web width)} &h_{f} &= 100.000\ \mathrm{mm} \; \;\textrm{(flange thickness)} \\ h &= 450.000\ \mathrm{mm} \; \;\textrm{(total section depth)} &h_{w} &= 350.000\ \mathrm{mm} \; \;\textrm{(web height)} &c_{nom} &= 35.000\ \mathrm{mm} \; \;\textrm{(nominal cover to rebar)} \\ \phi &= 20.000\ \mathrm{mm} \; \;\textrm{(assumed rebar diameter)} &d &= 405.000\ \mathrm{mm} \; \;\textrm{(effective depth to rebar centroid)} \end{aligned} $$
In [3]:
%%render params
# Material properties
f_ck = 25 * MPa    # characteristic concrete compressive strength (cylinder)
f_yk = 500 * MPa   # characteristic steel yield strength
E_c  = 31 * GPa    # concrete modulus of elasticity (EC2 approx for C25)
E_s  = 200 * GPa   # steel modulus of elasticity
$$ \begin{aligned} f_{ck} &= 25.000\ \mathrm{MPa} \; \;\textrm{(characteristic concrete compressive strength (cylinder))} &f_{yk} &= 500.000\ \mathrm{MPa} \; \;\textrm{(characteristic steel yield strength)} &E_{c} &= 31.000\ \mathrm{GPa} \; \;\textrm{(concrete modulus of elasticity (EC2 approx for C25))} \\ E_{s} &= 200.000\ \mathrm{GPa} \; \;\textrm{(steel modulus of elasticity)} \end{aligned} $$

3. ⬇️ Loading and Bending Moment¶

Classic simply-supported beam, uniform load. Mid-span moment: $M = qL^2/8$.

Watch forallpeople simplify (kN/m) × m² → kN·m automatically. No conversion factors. No unit footnotes. No regrets. 👇

In [4]:
%%render
L   = 6.0 * m           # span
q   = 15.0 * kN / m     # uniformly distributed load
M_Ed = (q * L**2) / 8   # design bending moment at mid-span
$$ \begin{aligned} L &= 6.0 \cdot m = 6.0 \cdot m &= 6.000\ \mathrm{m} \; \;\textrm{(span)} \\ q &= 15.0 \cdot \frac{ \mathrm{kN} }{ m } = 15.0 \cdot \frac{ kN }{ m } &= 15.000\ \mathrm{kN/m} \; \;\textrm{(uniformly distributed load)} \\ M_{Ed} &= \frac{ q \cdot \left( L \right) ^{ 2 } }{ 8 } = \frac{ 15.000\ \mathrm{kN/m} \cdot \left( 6.000\ \mathrm{m} \right) ^{ 2 } }{ 8 } &= 67.500\ \mathrm{kN} \cdot \mathrm{m} \; \;\textrm{(design bending moment at mid-span)} \end{aligned} $$

📌 forallpeople resolved (kN/m) × m² = kN·m without being asked. This is dimensional analysis. It just happens to feel like magic.

Now trying %%render long for the area calculations. Normally handcalcs collapses short expressions to one line. %%render long forces each assignment onto its own full block — cleaner when you want every step visible.


4. 📏 Section Properties¶

Split the T into two rectangles (flange + web), then use the parallel axis theorem for $I$.

😐 The neutral axis is where the section feels nothing. Like a senior engineer reviewing your first design.

👇 Areas with %%render long:

In [5]:
%%render long
# Areas of sub-regions
A_f = b_f * h_f                # flange area
A_w = b_w * h_w                # web area
A_total = A_f + A_w            # total cross-section area
$$ \begin{aligned} A_{f} &= b_{f} \cdot h_{f} \\&= 600.000\ \mathrm{mm} \cdot 100.000\ \mathrm{mm} \\&= 60000.000\ \mathrm{mm}^{2} \; \;\textrm{(flange area)}\\ \\ A_{w} &= b_{w} \cdot h_{w} \\&= 250.000\ \mathrm{mm} \cdot 350.000\ \mathrm{mm} \\&= 87500.000\ \mathrm{mm}^{2} \; \;\textrm{(web area)}\\ \\ A_{total} &= A_{f} + A_{w} \\&= 60000.000\ \mathrm{mm}^{2} + 87500.000\ \mathrm{mm}^{2} \\&= 147500.000\ \mathrm{mm}^{2} \; \;\textrm{(total cross-section area)}\\ \end{aligned} $$
In [6]:
%%render
# Centroid distances from top fibre
y_f = h_f / 2                          # centroid of flange from top
y_w = h_f + h_w / 2                    # centroid of web from top
y_bar = (A_f * y_f + A_w * y_w) / A_total    # section centroid from top
$$ \begin{aligned} y_{f} &= \frac{ h_{f} }{ 2 } = \frac{ 100.000\ \mathrm{mm} }{ 2 } &= 50.000\ \mathrm{mm} \; \;\textrm{(centroid of flange from top)} \\ y_{w} &= h_{f} + \frac{ h_{w} }{ 2 } = 100.000\ \mathrm{mm} + \frac{ 350.000\ \mathrm{mm} }{ 2 } &= 275.000\ \mathrm{mm} \; \;\textrm{(centroid of web from top)} \\ y_{bar} &= \frac{ A_{f} \cdot y_{f} + A_{w} \cdot y_{w} }{ A_{total} } = \frac{ 60000.000\ \mathrm{mm}^{2} \cdot 50.000\ \mathrm{mm} + 87500.000\ \mathrm{mm}^{2} \cdot 275.000\ \mathrm{mm} }{ 147500.000\ \mathrm{mm}^{2} } &= 183.475\ \mathrm{mm} \; \;\textrm{(section centroid from top)} \end{aligned} $$
In [7]:
%%render
# Second moment of area — parallel axis theorem for each part
I_f = (b_f * h_f**3) / 12 + A_f * (y_bar - y_f)**2   # flange
I_w = (b_w * h_w**3) / 12 + A_w * (y_bar - y_w)**2   # web
I   = I_f + I_w                                        # total
$$ \begin{aligned} I_{f} &= \frac{ b_{f} \cdot \left( h_{f} \right) ^{ 3 } }{ 12 } + A_{f} \cdot \left( y_{bar} - y_{f} \right) ^{ 2 } \\&= \frac{ 600.000\ \mathrm{mm} \cdot \left( 100.000\ \mathrm{mm} \right) ^{ 3 } }{ 12 } + 60000.000\ \mathrm{mm}^{2} \cdot \left( 183.475\ \mathrm{mm} - 50.000\ \mathrm{mm} \right) ^{ 2 } \\&= 1118927750.646\ \mathrm{mm}^{4} \; \;\textrm{(flange)}\\ \\ I_{w} &= \frac{ b_{w} \cdot \left( h_{w} \right) ^{ 3 } }{ 12 } + A_{w} \cdot \left( y_{bar} - y_{w} \right) ^{ 2 } \\&= \frac{ 250.000\ \mathrm{mm} \cdot \left( 350.000\ \mathrm{mm} \right) ^{ 3 } }{ 12 } + 87500.000\ \mathrm{mm}^{2} \cdot \left( 183.475\ \mathrm{mm} - 275.000\ \mathrm{mm} \right) ^{ 2 } \\&= 1626208195.681\ \mathrm{mm}^{4} \; \;\textrm{(web)}\\ \\ I &= I_{f} + I_{w} = 1118927750.646\ \mathrm{mm}^{4} + 1626208195.681\ \mathrm{mm}^{4} &= 2745135946.328\ \mathrm{mm}^{4} \; \;\textrm{(total)} \end{aligned} $$

5. 💪 Elastic Bending Stress and Strain (Uncracked Section)¶

⚠️ Important — this is for illustration, not the ULS design basis. The stresses below assume a homogeneous, uncracked, fully elastic section. They are useful to visualise the linear strain/stress distribution and could support an SLS uncracked check, but the reinforcement design in §6 uses the EC2 ULS rectangular stress block on the cracked section.

Elastic bending: $\sigma = My/I$.

  • 🔵 Top fibre → compression. Concrete is happy here.
  • 🔴 Bottom fibre → tension. Concrete is not happy here. Hence: rebar.

First, %%render symbolic to see the formula structure with no numbers. Then %%render for the actual values. Two cells, full picture. 👇

In [8]:
%%render
# Distance from neutral axis to extreme fibres
y_top = y_bar                  # top fibre to centroid
y_bot = h - y_bar              # bottom fibre to centroid
$$ \begin{aligned} y_{top} &= 183.475\ \mathrm{mm} \; \;\textrm{(top fibre to centroid)} \\ y_{bot} &= h - y_{bar} = 450.000\ \mathrm{mm} - 183.475\ \mathrm{mm} &= 266.525\ \mathrm{mm} \; \;\textrm{(bottom fibre to centroid)} \end{aligned} $$
In [9]:
%%render symbolic
# Show the formula structure only — no numeric values yet
sigma_top_sym = M_Ed * y_top / I
sigma_bot_sym = M_Ed * y_bot / I
epsilon_top_sym = sigma_top_sym / E_c
epsilon_bot_sym = sigma_bot_sym / E_c
$$ \begin{aligned} \sigma_{top_{sym}} &= M_{Ed} \cdot \frac{ y_{top} }{ I } \; \\ \sigma_{bot_{sym}} &= M_{Ed} \cdot \frac{ y_{bot} }{ I } \; \\ \epsilon_{top_{sym}} &= \frac{ \sigma_{top_{sym}} }{ E_{c} } \; \\ \epsilon_{bot_{sym}} &= \frac{ \sigma_{bot_{sym}} }{ E_{c} } \; \end{aligned} $$
In [10]:
# @handcalc decorator — the programmatic API for using handcalcs outside of a magic cell
# Useful when you want to call a calculation as a reusable function
from handcalcs import handcalc
from IPython.display import Latex, display

@handcalc(jupyter_display=True)
def bending_stress(M, y, I):
    sigma = M * y / I
    return locals()

# Call it like a regular function — it prints the rendered LaTeX AND returns the result dict
results = bending_stress(M_Ed, y_bar, I)
print("sigma =", results["sigma"])
$$ \begin{aligned} \sigma &= M \cdot \frac{ y }{ I } = 67.500\ \mathrm{kN} \cdot \mathrm{m} \cdot \frac{ 183.475\ \mathrm{mm} }{ 2745135946.328\ \mathrm{mm}^{4} } &= 4.511\ \mathrm{MPa} \end{aligned} $$
sigma = 4.511 MPa
In [11]:
%%render
# Bending stresses at extreme fibres
sigma_top = M_Ed * y_top / I    # compressive stress at top (positive = compression here)
sigma_bot = M_Ed * y_bot / I    # tensile stress at bottom
$$ \begin{aligned} \sigma_{top} &= M_{Ed} \cdot \frac{ y_{top} }{ I } = 67.500\ \mathrm{kN} \cdot \mathrm{m} \cdot \frac{ 183.475\ \mathrm{mm} }{ 2745135946.328\ \mathrm{mm}^{4} } &= 4.511\ \mathrm{MPa} \; \;\textrm{(compressive stress at top (positive = compression here))} \\ \sigma_{bot} &= M_{Ed} \cdot \frac{ y_{bot} }{ I } = 67.500\ \mathrm{kN} \cdot \mathrm{m} \cdot \frac{ 266.525\ \mathrm{mm} }{ 2745135946.328\ \mathrm{mm}^{4} } &= 6.554\ \mathrm{MPa} \; \;\textrm{(tensile stress at bottom)} \end{aligned} $$
In [12]:
%%render
# Elastic strains in the uncracked section (illustration — not ULS design strains)
epsilon_top = sigma_top / E_c   # strain at top fibre (concrete in compression)
epsilon_bot = sigma_bot / E_c   # strain at bottom fibre (concrete in tension, uncracked)
$$ \begin{aligned} \epsilon_{top} &= \frac{ \sigma_{top} }{ E_{c} } = \frac{ 4.511\ \mathrm{MPa} }{ 31.000\ \mathrm{GPa} } &= 0.000 \; \;\textrm{(strain at top fibre (concrete in compression))} \\ \epsilon_{bot} &= \frac{ \sigma_{bot} }{ E_{c} } = \frac{ 6.554\ \mathrm{MPa} }{ 31.000\ \mathrm{GPa} } &= 0.000 \; \;\textrm{(strain at bottom fibre (concrete in tension, uncracked))} \end{aligned} $$

6. 🔩 Required Tension Reinforcement (EC2 Simplified, ULS)¶

EC2 rectangular stress block — three steps:

  1. $K = M_{Ed} / (b\, d^2 f_{cd})$ — how hard the section is working.
  2. Lever arm $z$ — where the internal force couple acts.
  3. $A_s = M_{Ed} / (f_{yd}\, z)$ — back-calculate the required steel area.

We check $K \leq K_{bal} = 0.167$ (EC2 with no moment redistribution, $f_{ck} \leq 50$ MPa). If exceeded, compression steel is needed too. For a T-beam with a wide flange this almost never happens — unless the loading was, shall we say, enthusiastic. 🙃

🧱 Width used in $K$. For a T-section we should first check whether the neutral axis falls inside the flange. If it does, the section behaves like a rectangle of width $b_f$. If it dips into the web, full T-beam analysis is needed. The shortcut taken below — using the web width $b_w$ — is the conservative simplification: it always over-estimates $K$ (and therefore the required steel) compared with the rigorous T-beam analysis, so the design remains safe. For the loading here, $K$ stays well below $K_{bal}$ with $b_w$, so refining the width is unnecessary.

📎 About the 1.134 in the lever-arm formula. EC2 uses the rectangular stress block with $\eta = 1.0$ and $\lambda = 0.8$ (for $f_{ck} \leq 50$ MPa), giving an effective stress block of depth $\lambda x$ at stress $\eta f_{cd}$. Solving the moment equilibrium for the lever arm yields $z = d\,(0.5 + \sqrt{0.25 - K/1.134})$, where $1.134 \approx 1/(\eta\,\lambda\,(1 - \lambda/2)) = 1/(1.0 \times 0.8 \times 0.6) = 1/0.48$ after the customary EC2 grouping; many references quote the constant as $1.134$ directly.

👇 Design strengths:

In [13]:
%%render
# EC2 design strengths — partial factors gamma_c = 1.5 (concrete), gamma_s = 1.15 (steel)
gamma_c = 1.5
gamma_s = 1.15
f_cd = f_ck / gamma_c      # design compressive strength of concrete
f_yd = f_yk / gamma_s      # design yield strength of steel
$$ \begin{aligned} \gamma_{c} &= 1.500 \; \\ \gamma_{s} &= 1.150 \; \\ f_{cd} &= \frac{ f_{ck} }{ \gamma_{c} } = \frac{ 25.000\ \mathrm{MPa} }{ 1.500 } &= 16.667\ \mathrm{MPa} \; \;\textrm{(design compressive strength of concrete)} \\ f_{yd} &= \frac{ f_{yk} }{ \gamma_{s} } = \frac{ 500.000\ \mathrm{MPa} }{ 1.150 } &= 434.783\ \mathrm{MPa} \; \;\textrm{(design yield strength of steel)} \end{aligned} $$
In [14]:
%%render
# Moment capacity parameter K
# Using the web width b_w (conservative for a T-beam — see note above).
# A rigorous check would first locate the neutral axis: if x <= h_f the flange
# is in compression alone and b_f could be used, giving a smaller K.
K     = M_Ed / (b_w * d**2 * f_cd)
K_bal = 0.167    # balanced limit, EC2, no moment redistribution, fck <= 50 MPa
$$ \begin{aligned} K &= 0.099 \; &K_{bal} &= 0.167 \; \;\textrm{(balanced limit, EC2, no moment redistribution, fck <= 50 MPa)} \end{aligned} $$
In [15]:
# Check: K must be <= K_bal for singly reinforced section
K_val = float(K)   # strip units for comparison (K is dimensionless)
print(f"K = {K_val:.4f}  |  K_bal = {K_bal}  |  {'OK — singly reinforced' if K_val <= K_bal else 'FAIL — need compression steel'}")
K = 0.0988  |  K_bal = 0.167  |  OK — singly reinforced
In [16]:
%%render
# Lever arm z (EC2 Eq. 6.18, capped at 0.95d)
z_calc = d * (0.5 + (0.25 - K_val / 1.134)**0.5)
z      = min(z_calc, 0.95 * d)
$$ \begin{aligned} z_{calc} &= d \cdot \left( 0.5 + \left( 0.25 - \frac{ K_{val} }{ 1.134 } \right) ^{ 0.5 } \right) = 405.000\ \mathrm{mm} \cdot \left( 0.5 + \left( 0.25 - \frac{ 0.099 }{ 1.134 } \right) ^{ 0.5 } \right) &= 365.964\ \mathrm{mm} \\ z &= \operatorname{min} \left( z_{calc} ,\ 0.95 \cdot d \right) = \operatorname{min} \left( 365.964\ \mathrm{mm} ,\ 0.95 \cdot 405.000\ \mathrm{mm} \right) &= 365.964\ \mathrm{mm} \end{aligned} $$
In [17]:
%%render
# Required tension reinforcement area
A_s_req = M_Ed / (f_yd * z)
$$ \begin{aligned} A_{s_{req}} &= \frac{ M_{Ed} }{ f_{yd} \cdot z } = \frac{ 67.500\ \mathrm{kN} \cdot \mathrm{m} }{ 434.783\ \mathrm{MPa} \cdot 365.964\ \mathrm{mm} } &= 424.222\ \mathrm{mm}^{2} \end{aligned} $$
In [18]:
%%render
# Minimum reinforcement check (EC2 Cl. 9.2.1.1)
f_ctm    = 0.3 * (f_ck / MPa)**(2/3) * MPa    # mean tensile strength
A_s_min  = max(0.26 * (f_ctm / f_yk) * b_w * d,  0.0013 * b_w * d)
$$ \begin{aligned} f_{ctm} &= 0.3 \cdot \left( \frac{ f_{ck} }{ \mathrm{MPa} } \right) ^{ \left( \frac{ 2 }{ 3 } \right) } \cdot \mathrm{MPa} = 0.3 \cdot \left( \frac{ 25.000\ \mathrm{MPa} }{ MPa } \right) ^{ \left( \frac{ 2 }{ 3 } \right) } \cdot MPa &= 2.565\ \mathrm{MPa} \; \;\textrm{(mean tensile strength)} \\ A_{s_{min}} &= \operatorname{max} \left( 0.26 \cdot \left( \frac{ f_{ctm} }{ f_{yk} } \right) \cdot b_{w} \cdot d ,\ 0.0013 \cdot b_{w} \cdot d \right) \\&= \operatorname{max} \left( 0.26 \cdot \left( \frac{ 2.565\ \mathrm{MPa} }{ 500.000\ \mathrm{MPa} } \right) \cdot 250.000\ \mathrm{mm} \cdot 405.000\ \mathrm{mm} ,\ 0.0013 \cdot 250.000\ \mathrm{mm} \cdot 405.000\ \mathrm{mm} \right) \\&= 135.045\ \mathrm{mm}^{2} \\ \end{aligned} $$
In [19]:
# Final required area: governing value
A_s_design = max(A_s_req, A_s_min)
print(f"A_s,req  = {float(A_s_req * 1e6):.0f} mm²")
print(f"A_s,min  = {float(A_s_min * 1e6):.0f} mm²")
print(f"A_s,use  = {float(A_s_design * 1e6):.0f} mm²")

# Typical bar choice: 3 x H25 = 1473 mm², 4 x H20 = 1257 mm², 5 x H16 = 1005 mm²
A_s_prov_options = {
    "3 x H25": 3 * np.pi * (25e-3)**2 / 4,
    "4 x H20": 4 * np.pi * (20e-3)**2 / 4,
    "5 x H16": 5 * np.pi * (16e-3)**2 / 4,
}
print("\nBar options:")
for label, area in A_s_prov_options.items():
    status = "OK" if area * 1e6 >= float(A_s_req * 1e6) else "insufficient"
    print(f"  {label}: {area*1e6:.0f} mm²  — {status}")
A_s,req  = 424 mm²
A_s,min  = 135 mm²
A_s,use  = 424 mm²

Bar options:
  3 x H25: 1473 mm²  — OK
  4 x H20: 1257 mm²  — OK
  5 x H16: 1005 mm²  — OK

7. 📊 Plots¶

Three separate figures:

  • 📐 Section geometry — T-shape, dimensions, neutral axis, rebar position
  • 💪 Stress distribution — compression and tension zones through the depth
  • 🔩 Reinforcement comparison — required vs. provided bar options
In [20]:
# Wong palette + shared numeric extracts for all plots
BLUE   = '#0072B2'
ORANGE = '#E69F00'
GREEN  = '#009E73'
RED    = '#D55E00'
GRAY   = '#666666'
PINK   = '#CC79A7'

import matplotlib
matplotlib.rcParams.update({'font.family': 'sans-serif', 'font.size': 9,
                             'axes.linewidth': 0.6, 'lines.linewidth': 1.4})

bf    = float(b_f / mm)
bw    = float(b_w / mm)
hf    = float(h_f / mm)
htot  = float(h / mm)
hw    = htot - hf
yb    = float(y_bar / mm)
d_val = float(d / mm)
s_top = float(sigma_top / MPa)
s_bot = float(sigma_bot / MPa)
A_req = float(A_s_req * 1e6)
A_min_val = float(A_s_min * 1e6)
A_prov_vals = {k: v * 1e6 for k, v in A_s_prov_options.items()}

📐 Plot 1 — Section Geometry¶

In [21]:
fig1, ax1 = plt.subplots(figsize=(4.5, 5.5))
ax1.set_aspect('equal')
ax1.set_xlim(-60, bf + 90)
ax1.set_ylim(-35, htot + 50)
ax1.axis('off')
ax1.set_title('T-Section Geometry', fontsize=10, pad=10)

flange_x = (bf - bw) / 2
ax1.add_patch(patches.Rectangle((0, htot - hf), bf, hf, lw=0.8, edgecolor=GRAY, facecolor='#d1d5db'))
ax1.add_patch(patches.Rectangle((flange_x, 0), bw, hw, lw=0.8, edgecolor=GRAY, facecolor='#d1d5db'))

ax1.axhline(htot - yb, color=BLUE, lw=1.0, ls='--')
rebar_y = htot - d_val
for xi in [flange_x + 30, flange_x + bw / 2, flange_x + bw - 30]:
    ax1.plot(xi, rebar_y, 'o', color=RED, ms=6, zorder=5)

def dim_h(ax, y1, y2, x, top_label, bot_label):
    ax.annotate('', xy=(x, y2), xytext=(x, y1),
                arrowprops=dict(arrowstyle='<->', color=GRAY, lw=0.8))
    ax.text(x + 8, (y1 + y2) / 2, top_label, va='center', ha='left', fontsize=7.5, color=GRAY)

def dim_w(ax, x1, x2, y, label):
    ax.annotate('', xy=(x2, y), xytext=(x1, y),
                arrowprops=dict(arrowstyle='<->', color=GRAY, lw=0.8))
    ax.text((x1 + x2) / 2, y + 8, label, va='bottom', ha='center', fontsize=7.5, color=GRAY)

dim_h(ax1, 0, htot - hf, bf + 15, 'h_w = ' + str(int(hw)) + ' mm', '')
dim_h(ax1, htot - hf, htot, bf + 15, 'h_f = ' + str(int(hf)) + ' mm', '')
dim_w(ax1, 0, bf, htot + 22, 'b_f = ' + str(int(bf)) + ' mm')
dim_w(ax1, flange_x, flange_x + bw, -25, 'b_w = ' + str(int(bw)) + ' mm')
ax1.text(bf + 16, htot - yb + 4, 'y = ' + str(int(yb)) + ' mm (NA)', fontsize=7.5, color=BLUE)
ax1.text(flange_x + bw / 2 + 10, rebar_y, 'As', fontsize=9, color=RED, va='center', fontstyle='italic')

plt.savefig('fig1_section.png', dpi=150, bbox_inches='tight')
plt.show()
No description has been provided for this image

💪 Plot 2 — Elastic Stress Distribution¶

In [22]:
fig2, ax2 = plt.subplots(figsize=(4, 5))
ax2.set_title('Elastic Stress Distribution', fontsize=10, pad=10)

depths   = np.linspace(0, htot, 300)
stresses = np.interp(depths, [0, htot], [s_top, -s_bot])
comp_mask = stresses >= 0

ax2.fill_betweenx(depths, 0, stresses, where=comp_mask,  color=BLUE, alpha=0.3, label='Compression')
ax2.fill_betweenx(depths, 0, stresses, where=~comp_mask, color=RED,  alpha=0.3, label='Tension')
ax2.plot(stresses, depths, color=GRAY, lw=1.4)
ax2.axvline(0, color=GRAY, lw=0.6)
ax2.axhline(yb, color=BLUE, lw=0.8, ls='--', label='NA (y = ' + str(int(yb)) + ' mm)')

ax2.set_xlabel('Stress (MPa)', fontsize=9)
ax2.set_ylabel('Depth from top (mm)', fontsize=9)
ax2.set_ylim(htot, 0)
ax2.legend(fontsize=8, loc='lower left')
ax2.tick_params(labelsize=8)

ax2.annotate(str(round(s_top, 2)) + ' MPa', xy=(s_top, 5), xytext=(s_top - 2.0, 50),
             fontsize=7.5, color=BLUE, arrowprops=dict(arrowstyle='->', color=BLUE, lw=0.7))
ax2.annotate(str(round(s_bot, 2)) + ' MPa', xy=(-s_bot, htot - 5), xytext=(-s_bot - 2.5, htot - 55),
             fontsize=7.5, color=RED, arrowprops=dict(arrowstyle='->', color=RED, lw=0.7))

fig2.tight_layout()
plt.savefig('fig2_stress.png', dpi=150, bbox_inches='tight')
plt.show()
No description has been provided for this image

🔩 Plot 3 — Required Reinforcement¶

In [23]:
fig3, ax3 = plt.subplots(figsize=(5, 4))
ax3.set_title('Reinforcement Area', fontsize=10, pad=10)

bar_labels = ['As,req', 'As,min'] + list(A_prov_vals.keys())
bar_vals   = [A_req, A_min_val] + list(A_prov_vals.values())
bar_colors = [ORANGE, PINK] + [GREEN if v >= A_req else RED for v in A_prov_vals.values()]

bars = ax3.bar(bar_labels, bar_vals, color=bar_colors, edgecolor=GRAY, lw=0.6, width=0.55)
ax3.axhline(A_req, color=ORANGE, lw=1.0, ls='--', label='Required: ' + str(int(A_req)) + ' mm2')

for bar, val in zip(bars, bar_vals):
    ax3.text(bar.get_x() + bar.get_width() / 2, val + 12,
             str(int(val)), ha='center', va='bottom', fontsize=8)

ax3.set_ylabel('Area (mm2)', fontsize=9)
ax3.set_ylim(0, max(bar_vals) * 1.18)
ax3.tick_params(labelsize=8)
ax3.legend(fontsize=8)

fig3.tight_layout()
plt.savefig('fig3_rebar.png', dpi=150, bbox_inches='tight')
plt.show()
No description has been provided for this image

8. 🏁 Summary¶

Parameter Value
Span $L$ 6.0 m
Uniform load $q$ 15 kN/m
Design moment $M_{Ed} = qL^2/8$ 67.5 kN·m
Section centroid $\bar{y}$ (from top) ~184 mm
Second moment of area $I$ ~$2.74 \times 10^9$ mm⁴
Effective depth $d$ 405 mm
$f_{cd} = f_{ck}/\gamma_c$ 16.67 MPa
$f_{yd} = f_{yk}/\gamma_s$ 434.8 MPa
$K = M_{Ed}/(b_w d^2 f_{cd})$ 0.099 < 0.167 ✅ singly reinforced
Lever arm $z$ ~366 mm
$A_{s,req}$ 424 mm²
$A_{s,min}$ (EC2 9.2.1.1) 135 mm²
Governing bar choice 3×H25 / 4×H20 / 5×H16 — all OK ✅

⚠️ Scope reminder. The elastic stress block in §5 is an uncracked-section illustration (could be reused for an SLS uncracked check). The reinforcement in §6 is sized at ULS with the EC2 rectangular stress block on the cracked section. Do not mix the two. Shear, deflection, crack width, and detailing checks are not included — this notebook is about the bending demand only.


🧰 handcalcs features used in this notebook¶

Feature Where used
%%render params Parameter tables (geometry, materials)
%%render All intermediate calculations
%%render long Section areas — forces each step to its own block
%%render symbolic Stress formulas — formula only, no numbers
render() Inline $K$ preview before formal calculation

📐 forallpeople features used¶

Feature Example
Unit objects 600 * mm, 25 * MPa, 15 * kN/m
Dimensional arithmetic (kN/m) × m² → kN·m auto-simplified
Dimensionless result Pa / Pa → plain float, no stripping needed
float(x / unit) Extract number for comparisons and numpy

🤔 Was this worth it vs. Excel?¶

The full calculation is ~60 lines of Python. It renders as a typeset calc sheet. It runs in 2 seconds. It is in git. It will be readable in 3 years.

The Excel alternative has a cell that says =IF(AND(B14>0,C7<=0.167),D12/(0.87*F5*G23),"FAIL"). That cell is load-bearing. Nobody knows why. 😅

The next time someone asks "where is your working?" — send them a link.