Building a Beam Analyzer
import numpy as np
import matplotlib.pyplot as plt
from dataclasses import dataclass
from typing import List, Tuple
@dataclass
class PointLoad:
position: float # mm from left
magnitude: float # N (positive = downward)
@dataclass
class DistributedLoad:
start: float # mm from left
end: float # mm from left
w_start: float # N/mm at start (positive = downward)
w_end: float # N/mm at end (for varying loads)
@dataclass
class PointMoment:
position: float # mm from left
magnitude: float # N·mm (positive = CCW)
class BeamAnalyzer:
"""Analyze beams and generate SFD/BMD."""
def __init__(self, length: float, support_type: str = 'simply_supported'):
"""
Parameters:
-----------
length : float - Beam length in mm
support_type : str - 'simply_supported', 'cantilever', 'fixed_fixed'
"""
self.L = length
self.support_type = support_type
self.point_loads: List[PointLoad] = []
self.distributed_loads: List[DistributedLoad] = []
self.point_moments: List[PointMoment] = []
self.reactions = {}
def add_point_load(self, position: float, magnitude: float):
"""Add a point load (positive = downward)."""
self.point_loads.append(PointLoad(position, magnitude))
def add_distributed_load(self, start: float, end: float,
w_start: float, w_end: float = None):
"""Add distributed load (positive = downward)."""
if w_end is None:
w_end = w_start
self.distributed_loads.append(DistributedLoad(start, end, w_start, w_end))
def add_moment(self, position: float, magnitude: float):
"""Add point moment (positive = CCW)."""
self.point_moments.append(PointMoment(position, magnitude))
def calculate_reactions(self):
"""Calculate support reactions."""
# Total load from point loads
total_P = sum(pl.magnitude for pl in self.point_loads)
# Total load from distributed loads
total_W = 0
moment_W = 0 # Moment about left support
for dl in self.distributed_loads:
length = dl.end - dl.start
# For linearly varying load
avg_w = (dl.w_start + dl.w_end) / 2
W = avg_w * length
total_W += W
# Centroid of load from left support
if dl.w_start == dl.w_end: # Uniform
centroid = dl.start + length / 2
else: # Triangular/trapezoidal
# For trapezoid: centroid = start + L/3 * (w1 + 2*w2)/(w1 + w2)
centroid = dl.start + length/3 * (dl.w_start + 2*dl.w_end) / (dl.w_start + dl.w_end)
moment_W += W * centroid
# Moments from point loads about left support
moment_P = sum(pl.magnitude * pl.position for pl in self.point_loads)
# External moments
total_M = sum(pm.magnitude for pm in self.point_moments)
if self.support_type == 'simply_supported':
# ΣM_A = 0: R_B * L = moment_P + moment_W + total_M
# ΣFy = 0: R_A + R_B = total_P + total_W
R_B = (moment_P + moment_W + total_M) / self.L
R_A = total_P + total_W - R_B
self.reactions = {
'R_A': R_A,
'R_B': R_B,
'M_A': 0,
'M_B': 0
}
elif self.support_type == 'cantilever':
# Fixed at left end (A)
R_A = total_P + total_W
M_A = -(moment_P + moment_W + total_M) # Moment at fixed end
self.reactions = {
'R_A': R_A,
'M_A': M_A
}
return self.reactions
def shear_at(self, x: float) -> float:
"""Calculate shear force at position x."""
V = 0
# Reaction at left
if self.support_type in ['simply_supported', 'cantilever']:
if x > 0:
V += self.reactions['R_A']
# Point loads to the left of x
for pl in self.point_loads:
if pl.position < x:
V -= pl.magnitude
# Distributed loads
for dl in self.distributed_loads:
if x > dl.start:
# Portion of distributed load to the left of x
x_eff = min(x, dl.end)
length = x_eff - dl.start
if dl.w_start == dl.w_end: # Uniform
V -= dl.w_start * length
else: # Varying
# Linear interpolation of w at x_eff
ratio = length / (dl.end - dl.start)
w_at_x = dl.w_start + ratio * (dl.w_end - dl.w_start)
avg_w = (dl.w_start + w_at_x) / 2
V -= avg_w * length
return V
def moment_at(self, x: float) -> float:
"""Calculate bending moment at position x."""
M = 0
# Reaction moment at left (for cantilever)
if self.support_type == 'cantilever':
M += self.reactions.get('M_A', 0)
# Reaction moment
if self.support_type in ['simply_supported', 'cantilever']:
if x > 0:
M += self.reactions['R_A'] * x
# Point loads
for pl in self.point_loads:
if pl.position < x:
M -= pl.magnitude * (x - pl.position)
# Point moments
for pm in self.point_moments:
if pm.position < x:
M += pm.magnitude
# Distributed loads
for dl in self.distributed_loads:
if x > dl.start:
x_eff = min(x, dl.end)
length = x_eff - dl.start
if dl.w_start == dl.w_end: # Uniform
W = dl.w_start * length
centroid = dl.start + length / 2
M -= W * (x - centroid)
else: # Varying - more complex
# Simplified for uniform case
ratio = length / (dl.end - dl.start)
w_at_x = dl.w_start + ratio * (dl.w_end - dl.w_start)
avg_w = (dl.w_start + w_at_x) / 2
W = avg_w * length
# Approximate centroid
centroid = dl.start + length / 2
M -= W * (x - centroid)
return M
def generate_diagrams(self, num_points: int = 500):
"""Generate SFD and BMD data."""
self.calculate_reactions()
x = np.linspace(0, self.L, num_points)
V = np.array([self.shear_at(xi) for xi in x])
M = np.array([self.moment_at(xi) for xi in x])
return x, V, M
def plot(self, num_points: int = 500):
"""Plot beam loading, SFD, and BMD."""
x, V, M = self.generate_diagrams(num_points)
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
# 1. Loading diagram
ax1 = axes[0]
ax1.axhline(y=0, color='black', lw=3) # Beam
# Supports
if self.support_type == 'simply_supported':
ax1.scatter([0, self.L], [0, 0], s=300, marker='^',
color='green', zorder=5)
elif self.support_type == 'cantilever':
ax1.fill_between([-self.L*0.02, 0], [-10, -10], [10, 10],
color='gray', alpha=0.5)
# Point loads
for pl in self.point_loads:
ax1.annotate('', xy=(pl.position, 0),
xytext=(pl.position, 50 if pl.magnitude > 0 else -50),
arrowprops=dict(arrowstyle='->', lw=2, color='red'))
ax1.text(pl.position, 55 if pl.magnitude > 0 else -55,
f'{pl.magnitude/1000:.1f}kN', ha='center', fontsize=10, color='red')
# Distributed loads (simplified visualization)
for dl in self.distributed_loads:
xs = np.linspace(dl.start, dl.end, 20)
for xi in xs:
ax1.annotate('', xy=(xi, 0), xytext=(xi, 30),
arrowprops=dict(arrowstyle='->', lw=1, color='blue', alpha=0.5))
ax1.text((dl.start + dl.end)/2, 35,
f'{dl.w_start:.1f} N/mm', ha='center', fontsize=9, color='blue')
# Reactions
ax1.annotate('', xy=(0, 0), xytext=(0, -40),
arrowprops=dict(arrowstyle='->', lw=2, color='green'))
ax1.text(0, -50, f"R_A={self.reactions['R_A']/1000:.2f}kN",
ha='center', fontsize=10, color='green')
if self.support_type == 'simply_supported':
ax1.annotate('', xy=(self.L, 0), xytext=(self.L, -40),
arrowprops=dict(arrowstyle='->', lw=2, color='green'))
ax1.text(self.L, -50, f"R_B={self.reactions['R_B']/1000:.2f}kN",
ha='center', fontsize=10, color='green')
ax1.set_xlim(-self.L*0.1, self.L*1.1)
ax1.set_ylim(-80, 80)
ax1.set_title('Loading Diagram', fontsize=12)
ax1.set_ylabel('Load')
ax1.axis('off')
# 2. Shear Force Diagram
ax2 = axes[1]
ax2.fill_between(x, 0, V/1000, where=(V >= 0), alpha=0.3, color='blue')
ax2.fill_between(x, 0, V/1000, where=(V < 0), alpha=0.3, color='red')
ax2.plot(x, V/1000, 'k-', lw=2)
ax2.axhline(y=0, color='k', lw=0.5)
# Mark max/min
V_max, V_min = np.max(V), np.min(V)
ax2.axhline(y=V_max/1000, color='blue', linestyle='--', alpha=0.5)
ax2.axhline(y=V_min/1000, color='red', linestyle='--', alpha=0.5)
ax2.set_xlabel('x (mm)')
ax2.set_ylabel('V (kN)')
ax2.set_title(f'Shear Force Diagram (Vmax={V_max/1000:.2f}kN, Vmin={V_min/1000:.2f}kN)')
ax2.grid(True, alpha=0.3)
# 3. Bending Moment Diagram
ax3 = axes[2]
ax3.fill_between(x, 0, M/1e6, where=(M >= 0), alpha=0.3, color='green')
ax3.fill_between(x, 0, M/1e6, where=(M < 0), alpha=0.3, color='orange')
ax3.plot(x, M/1e6, 'k-', lw=2)
ax3.axhline(y=0, color='k', lw=0.5)
# Mark max/min
M_max, M_min = np.max(M), np.min(M)
idx_max = np.argmax(np.abs(M))
ax3.scatter([x[idx_max]], [M[idx_max]/1e6], s=100, color='red', zorder=5)
ax3.annotate(f'Mmax={M[idx_max]/1e6:.2f} kN·m',
xy=(x[idx_max], M[idx_max]/1e6),
xytext=(x[idx_max]+100, M[idx_max]/1e6 + 0.5),
fontsize=10)
ax3.set_xlabel('x (mm)')
ax3.set_ylabel('M (kN·m)')
ax3.set_title('Bending Moment Diagram')
ax3.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
return {'V_max': V_max, 'V_min': V_min, 'M_max': M_max, 'M_min': M_min}
Example: Simply Supported Beam
# Create beam
beam = BeamAnalyzer(length=6000, support_type='simply_supported')
# Add loads
beam.add_point_load(2000, 15e3) # 15 kN at 2m from left
beam.add_point_load(4000, 10e3) # 10 kN at 4m from left
beam.add_distributed_load(0, 6000, 5, 5) # 5 N/mm UDL over entire span
# Calculate and plot
results = beam.plot()
print("\nResults Summary:")
print(f" Reactions: R_A = {beam.reactions['R_A']/1000:.2f} kN")
print(f" R_B = {beam.reactions['R_B']/1000:.2f} kN")
print(f" Max Shear: {results['V_max']/1000:.2f} kN")
print(f" Max Moment: {results['M_max']/1e6:.2f} kN·m")
Example: Cantilever Beam
# Cantilever with various loads
cantilever = BeamAnalyzer(length=3000, support_type='cantilever')
cantilever.add_point_load(3000, 8e3) # 8 kN at free end
cantilever.add_distributed_load(0, 2000, 4, 4) # 4 N/mm over first 2m
results = cantilever.plot()
print("\nCantilever Results:")
print(f" Reaction: R_A = {cantilever.reactions['R_A']/1000:.2f} kN")
print(f" Fixed-end moment: M_A = {cantilever.reactions['M_A']/1e6:.2f} kN·m")
Quick Reference: Standard Cases
def standard_cases_summary():
"""Print summary of standard SFD/BMD cases."""
cases = [
{
'name': 'Simply Supported - Center Point Load P',
'V_max': 'P/2 (at supports)',
'M_max': 'PL/4 (at center)'
},
{
'name': 'Simply Supported - UDL w',
'V_max': 'wL/2 (at supports)',
'M_max': 'wL²/8 (at center)'
},
{
'name': 'Cantilever - End Point Load P',
'V_max': 'P (entire length)',
'M_max': 'PL (at fixed end)'
},
{
'name': 'Cantilever - UDL w',
'V_max': 'wL (at fixed end)',
'M_max': 'wL²/2 (at fixed end)'
},
{
'name': 'Fixed-Fixed - Center Point Load P',
'V_max': 'P/2 (at supports)',
'M_max': 'PL/8 (at center & supports)'
},
{
'name': 'Fixed-Fixed - UDL w',
'V_max': 'wL/2 (at supports)',
'M_max': 'wL²/12 (at supports), wL²/24 (at center)'
}
]
print("Standard SFD/BMD Cases")
print("=" * 60)
for case in cases:
print(f"\n{case['name']}:")
print(f" Vmax = {case['V_max']}")
print(f" Mmax = {case['M_max']}")
standard_cases_summary()
Key Takeaways
- Relationships: $dV/dx = -w$, $dM/dx = V$
- Shear jumps at point loads, Moment jumps at point moments
- UDL creates linear shear, parabolic moment
- Point loads create constant shear between loads
- Maximum moment occurs where shear crosses zero
Next lesson: We'll explore torsion in shafts.