RCBeamdesign / continuous_beam.py
Sompote's picture
Upload 7 files
157b77e verified
import numpy as np
import matplotlib
matplotlib.use('Agg') # Use non-interactive backend for cloud deployment
import matplotlib.pyplot as plt
from typing import List, Dict, Tuple
import math
# Configure matplotlib for cloud deployment
plt.ioff() # Turn off interactive mode
plt.rcParams['figure.dpi'] = 80 # Lower DPI for faster rendering
plt.rcParams['savefig.dpi'] = 80
class ContinuousBeam:
"""
Continuous beam analysis and RC design according to ACI code
"""
def __init__(self):
self.spans = []
self.loads = []
self.supports = []
self.moments = []
self.shears = []
self.fc = 280 # Concrete compressive strength (ksc) - converted from 28 MPa
self.fy = 4000 # Steel yield strength (ksc) - Thai standard
self.beam_width = 300 # mm
self.beam_depth = 500 # mm
self.cover = 40 # mm
self.d = self.beam_depth - self.cover # Effective depth
def add_span(self, length: float, distributed_load: float = 0, point_loads: List[Tuple[float, float]] = None):
"""
Add a span to the continuous beam
length: span length in meters
distributed_load: uniformly distributed load in kN/m
point_loads: list of (position, load) tuples in (m, kN)
"""
span = {
'length': length,
'distributed_load': distributed_load,
'point_loads': point_loads or []
}
self.spans.append(span)
def create_beam_element_stiffness(self, length, E, I):
"""
Create local stiffness matrix for a beam element
[k] = EI/L^3 * [[12, 6L, -12, 6L],
[6L, 4L^2, -6L, 2L^2],
[-12, -6L, 12, -6L],
[6L, 2L^2, -6L, 4L^2]]
DOFs: [v1, θ1, v2, θ2] where v=deflection, θ=rotation
"""
L = length
EI_L3 = E * I / (L**3)
k = EI_L3 * np.array([
[12, 6*L, -12, 6*L],
[6*L, 4*L**2, -6*L, 2*L**2],
[-12, -6*L, 12, -6*L],
[6*L, 2*L**2, -6*L, 4*L**2]
])
return k
def create_distributed_load_vector(self, length, w):
"""
Create equivalent nodal force vector for distributed load
For uniformly distributed load w:
[F] = wL/12 * [6, L, 6, -L]
"""
L = length
wL_12 = w * L / 12
f = wL_12 * np.array([6, L, 6, -L])
return f
def create_point_load_vector(self, length, point_loads):
"""
Create equivalent nodal force vector for point loads
"""
L = length
f = np.zeros(4)
for pos, load in point_loads:
a = pos # distance from left node
b = L - pos # distance from right node
# Shape functions at load position
xi = pos / L # normalized position
# Equivalent nodal forces using shape functions
N1 = 1 - 3*xi**2 + 2*xi**3
N2 = L * (xi - 2*xi**2 + xi**3)
N3 = 3*xi**2 - 2*xi**3
N4 = L * (-xi**2 + xi**3)
f += load * np.array([N1, N2, N3, N4])
return f
def finite_element_analysis(self):
"""
Perform finite element analysis of continuous beam
"""
if len(self.spans) == 0:
raise ValueError("No spans defined")
# Material properties (assumed values for analysis)
E = 30000 # MPa (typical for concrete)
# Calculate moment of inertia from beam dimensions
b = self.beam_width / 1000 # Convert mm to m
h = self.beam_depth / 1000 # Convert mm to m
I = b * h**3 / 12 # m^4
# Create mesh - optimize for cloud deployment
# Reduce elements per span for better performance on limited resources
max_elements_per_span = min(8, max(4, int(40 / len(self.spans)))) # Scale down for many spans
elements_per_span = max_elements_per_span
total_elements = len(self.spans) * elements_per_span
total_nodes = total_elements + 1
# Node coordinates
node_coords = []
current_x = 0
for span in self.spans:
span_length = span['length']
element_length = span_length / elements_per_span
for i in range(elements_per_span):
node_coords.append(current_x + i * element_length)
current_x += span_length
# Add final node
node_coords.append(current_x)
node_coords = np.array(node_coords)
# Global stiffness matrix (2 DOFs per node: deflection and rotation)
n_dofs = 2 * total_nodes
K_global = np.zeros((n_dofs, n_dofs))
F_global = np.zeros(n_dofs)
# Assembly process
for elem_idx in range(total_elements):
# Element properties
span_idx = elem_idx // elements_per_span
local_elem_idx = elem_idx % elements_per_span
span = self.spans[span_idx]
element_length = span['length'] / elements_per_span
# Local stiffness matrix
k_local = self.create_beam_element_stiffness(element_length, E, I)
# Global DOF indices for this element
node1 = elem_idx
node2 = elem_idx + 1
dofs = [2*node1, 2*node1+1, 2*node2, 2*node2+1] # [v1, θ1, v2, θ2]
# Assemble into global matrix
for i in range(4):
for j in range(4):
K_global[dofs[i], dofs[j]] += k_local[i, j]
# Create load vector for this element
# Distributed load
w = span['distributed_load'] * 1000 # Convert kN/m to N/m
f_dist = self.create_distributed_load_vector(element_length, w)
# Point loads (only if they fall within this element)
point_loads = span.get('point_loads', [])
f_point = np.zeros(4)
span_start = sum(self.spans[j]['length'] for j in range(span_idx))
elem_start = span_start + local_elem_idx * element_length
elem_end = elem_start + element_length
for pos, load in point_loads:
global_pos = span_start + pos
if elem_start <= global_pos <= elem_end:
local_pos = global_pos - elem_start
f_point += self.create_point_load_vector(element_length, [(local_pos, load * 1000)])
f_total = f_dist + f_point
# Assemble into global force vector
for i in range(4):
F_global[dofs[i]] += f_total[i]
# Apply boundary conditions (pin supports at all support locations)
# Support locations are at the ends of each span
support_nodes = [0] # First support
current_node = 0
for span in self.spans:
current_node += elements_per_span
support_nodes.append(current_node)
# Create arrays for free DOFs (removing constrained deflections)
constrained_dofs = [2 * node for node in support_nodes] # Vertical deflections at supports
free_dofs = [i for i in range(n_dofs) if i not in constrained_dofs]
# Extract free DOF matrices
K_free = K_global[np.ix_(free_dofs, free_dofs)]
F_free = F_global[free_dofs]
# Solve for displacements
try:
U_free = np.linalg.solve(K_free, F_free)
except np.linalg.LinAlgError:
# Fallback to least squares if matrix is singular
U_free = np.linalg.lstsq(K_free, F_free, rcond=None)[0]
# Reconstruct full displacement vector
U_global = np.zeros(n_dofs)
U_global[free_dofs] = U_free
# Store results for post-processing
self.node_coords = node_coords
self.displacements = U_global
self.elements_per_span = elements_per_span
self.element_properties = {'E': E, 'I': I}
self.K_global = K_global # Store for reaction calculation
return node_coords, U_global
def calculate_element_forces(self):
"""
Calculate internal forces (moment and shear) for each element
"""
if not hasattr(self, 'displacements'):
self.finite_element_analysis()
E = self.element_properties['E']
I = self.element_properties['I']
moments = []
shears = []
x_coords = []
# Calculate reactions first for proper shear calculation
reactions = self.calculate_reactions()
elem_idx = 0
for span_idx, span in enumerate(self.spans):
span_length = span['length']
element_length = span_length / self.elements_per_span
span_start_x = sum(self.spans[j]['length'] for j in range(span_idx))
for local_elem in range(self.elements_per_span):
# Element nodes
node1 = elem_idx
node2 = elem_idx + 1
# Element displacements
u1 = self.displacements[2*node1] # deflection at node 1
theta1 = self.displacements[2*node1+1] # rotation at node 1
u2 = self.displacements[2*node2] # deflection at node 2
theta2 = self.displacements[2*node2+1] # rotation at node 2
# Calculate forces at multiple points within element
# Reduce points for cloud deployment performance
n_points = 5 # Reduced from 10 to 5
for i in range(n_points):
xi = i / (n_points - 1) # 0 to 1
x_local = xi * element_length
x_global = span_start_x + local_elem * element_length + x_local
# Shape function derivatives for moment calculation
# M = -EI * d²v/dx²
d2N1_dx2 = (-6 + 12*xi) / element_length**2
d2N2_dx2 = (-4 + 6*xi) / element_length
d2N3_dx2 = (6 - 12*xi) / element_length**2
d2N4_dx2 = (-2 + 6*xi) / element_length
curvature = (d2N1_dx2 * u1 + d2N2_dx2 * theta1 +
d2N3_dx2 * u2 + d2N4_dx2 * theta2)
moment = -E * I * curvature / 1000 # Convert to kN-m
# Calculate shear using equilibrium method (more reliable)
shear = self.calculate_shear_at_position(x_global, reactions)
x_coords.append(x_global)
moments.append(moment)
shears.append(shear)
elem_idx += 1
return np.array(x_coords), np.array(moments), np.array(shears)
def calculate_reactions(self):
"""
Calculate support reactions from finite element solution
"""
if not hasattr(self, 'K_global') or not hasattr(self, 'displacements'):
self.finite_element_analysis()
# Get support node indices
support_nodes = [0] # First support
current_node = 0
for span in self.spans:
current_node += self.elements_per_span
support_nodes.append(current_node)
# Calculate reactions using R = K*U - F for constrained DOFs
reactions = []
# Build complete force vector including applied loads
n_dofs = len(self.displacements)
F_complete = np.zeros(n_dofs)
# Assemble applied load vector (same as in FE analysis)
elem_idx = 0
for span_idx, span in enumerate(self.spans):
element_length = span['length'] / self.elements_per_span
for local_elem_idx in range(self.elements_per_span):
# Global DOF indices for this element
node1 = elem_idx
node2 = elem_idx + 1
dofs = [2*node1, 2*node1+1, 2*node2, 2*node2+1]
# Create load vector for this element
w = span['distributed_load'] * 1000 # Convert to N/m
f_dist = self.create_distributed_load_vector(element_length, w)
# Point loads (only if they fall within this element)
point_loads = span.get('point_loads', [])
f_point = np.zeros(4)
span_start = sum(self.spans[j]['length'] for j in range(span_idx))
elem_start = span_start + local_elem_idx * element_length
elem_end = elem_start + element_length
for pos, load in point_loads:
global_pos = span_start + pos
if elem_start <= global_pos <= elem_end:
local_pos = global_pos - elem_start
f_point += self.create_point_load_vector(element_length, [(local_pos, load * 1000)])
f_total = f_dist + f_point
# Assemble into global force vector
for i in range(4):
F_complete[dofs[i]] += f_total[i]
elem_idx += 1
# Calculate reactions at each support
for support_node in support_nodes:
# Vertical DOF for this support
dof = 2 * support_node
# Reaction = K*U - F at constrained DOF
# Since displacement is zero at support, reaction = -F_applied + K*U_other
reaction_force = 0
# Sum contributions from all DOFs
for j in range(n_dofs):
reaction_force += self.K_global[dof, j] * self.displacements[j]
# Subtract applied force (if any) at this DOF
reaction_force -= F_complete[dof]
# Convert to kN and store (positive = upward reaction)
# Note: FE convention may give negative values for upward reactions
reactions.append(-reaction_force / 1000)
# Store for debugging
self.reactions = reactions
return reactions
def calculate_shear_at_position(self, x_global, reactions):
"""
Calculate shear force at any position using equilibrium
"""
shear = 0
current_pos = 0
# Add reaction at first support
if len(reactions) > 0:
shear += reactions[0]
# Subtract loads to the left of current position
support_idx = 1
for span_idx, span in enumerate(self.spans):
span_start = current_pos
span_end = current_pos + span['length']
if x_global <= span_start:
break
# Check if we passed a support
if x_global > span_end and support_idx < len(reactions):
shear += reactions[support_idx]
support_idx += 1
# Calculate how much of this span is to the left of current position
span_length_to_left = min(x_global - span_start, span['length'])
if span_length_to_left > 0:
# Distributed load effect
w = span['distributed_load']
shear -= w * span_length_to_left
# Point load effects
point_loads = span.get('point_loads', [])
for pos, load in point_loads:
if pos <= span_length_to_left:
shear -= load
current_pos += span['length']
return shear
def analyze_moments(self):
"""
Analyze continuous beam using finite element method
"""
num_spans = len(self.spans)
if num_spans == 0:
raise ValueError("No spans defined")
# Perform finite element analysis
self.finite_element_analysis()
# Calculate detailed forces along the beam
x_coords, moments_detailed, shears_detailed = self.calculate_element_forces()
# Extract critical moments and shears for each span (for compatibility with existing code)
self.moments = []
self.shears = []
current_pos = 0
for i, span in enumerate(self.spans):
span_length = span['length']
span_start = current_pos
span_mid = current_pos + span_length / 2
span_end = current_pos + span_length
# Find indices closest to critical points
start_idx = np.argmin(np.abs(x_coords - span_start))
mid_idx = np.argmin(np.abs(x_coords - span_mid))
end_idx = np.argmin(np.abs(x_coords - span_end))
# Extract moments and shears at critical points
M_start = moments_detailed[start_idx]
M_mid = moments_detailed[mid_idx]
M_end = moments_detailed[end_idx]
V_start = shears_detailed[start_idx]
V_mid = shears_detailed[mid_idx]
V_end = shears_detailed[end_idx]
# Store for span (maintaining compatibility with existing design methods)
self.moments.append([M_start, M_mid, M_end])
self.shears.append([V_start, V_mid, V_end])
current_pos += span_length
# Store detailed results for plotting
self.detailed_x = x_coords
self.detailed_moments = moments_detailed
self.detailed_shears = shears_detailed
def calculate_required_reinforcement(self, moment: float, beam_type: str = "rectangular"):
"""
Calculate required area of reinforcement according to ACI code
moment: Design moment in kN-m
beam_type: Type of beam section
"""
if moment == 0:
return 0
# Convert moment to N-mm
Mu = abs(moment) * 1e6
# Material properties - convert from ksc to MPa
fc = self.fc / 10.2 # Convert ksc to MPa (1 ksc ≈ 0.098 MPa)
fy = self.fy / 10.2 # Convert ksc to MPa
b = self.beam_width # mm
d = self.d # mm
# Strength reduction factor
phi = 0.9
# Calculate required reinforcement
# Using simplified rectangular stress block
beta1 = 0.85 if fc <= 28 else max(0.65, 0.85 - 0.05 * (fc - 28) / 7)
# Calculate Rn
Rn = Mu / (phi * b * d**2)
# Calculate reinforcement ratio
# Check for domain error in sqrt
discriminant = 1 - 2 * Rn / (0.85 * fc)
if discriminant < 0:
# Moment exceeds capacity - increase beam size or use compression reinforcement
raise ValueError(f"Moment exceeds beam capacity. Increase beam size or use compression reinforcement.")
rho = (0.85 * fc / fy) * (1 - math.sqrt(discriminant))
# Minimum reinforcement ratio
rho_min = max(1.4 / fy, 0.25 * math.sqrt(fc) / fy)
# Maximum reinforcement ratio (75% of balanced ratio)
rho_b = (0.85 * fc * beta1 * 600) / (fy * (600 + fy))
rho_max = 0.75 * rho_b
# Check limits
rho = max(rho, rho_min)
if rho > rho_max:
raise ValueError(f"Required reinforcement ratio {rho:.4f} exceeds maximum {rho_max:.4f}")
# Calculate required area
As_required = rho * b * d
return As_required
def calculate_shear_reinforcement(self, shear: float):
"""
Calculate shear reinforcement (stirrups) according to ACI code
shear: Design shear force in kN
"""
if shear == 0:
return {"stirrup_spacing": "No stirrups required", "Av": 0}
# Convert shear to N
Vu = abs(shear) * 1000
# Material properties - convert from ksc to MPa
fc = self.fc / 10.2 # Convert ksc to MPa
fy = self.fy / 10.2 # Convert ksc to MPa (for stirrups)
b = self.beam_width # mm
d = self.d # mm
# Strength reduction factor for shear
phi_v = 0.75
# Concrete shear capacity
Vc = 0.17 * math.sqrt(fc) * b * d # N
# Check if shear reinforcement is required
if Vu <= phi_v * Vc / 2:
return {"stirrup_spacing": "No stirrups required", "Av": 0}
# Calculate required shear reinforcement
Vs = Vu / phi_v - Vc # Required steel shear capacity
# Maximum shear that can be carried by steel
Vs_max = 0.66 * math.sqrt(fc) * b * d
if Vs > Vs_max:
raise ValueError("Shear exceeds maximum capacity - increase beam size")
# Calculate required stirrup area
# Use RB9 or RB6 stirrups based on shear demand
if Vs > 150000: # High shear - use RB9
stirrup_dia = 9
Av = 2 * math.pi * (9/2)**2 # 2-leg RB9 stirrups = 2 × 63.6 = 127 mm²
stirrup_designation = "RB9"
else: # Lower shear - use RB6
stirrup_dia = 6
Av = 2 * math.pi * (6/2)**2 # 2-leg RB6 stirrups = 2 × 28.3 = 57 mm²
stirrup_designation = "RB6"
# Calculate required spacing
s_required = Av * fy * d / Vs # mm
# Maximum spacing limits
s_max = min(d / 2, 600) # mm
# Minimum stirrup requirements
if Vu > phi_v * Vc:
Av_min = 0.35 * b * s_required / fy
s_max_min = min(d / 4, 300) # More restrictive for high shear
s_required = min(s_required, s_max_min)
s_required = min(s_required, s_max)
s_required = max(s_required, 50) # Minimum practical spacing
return {
"stirrup_spacing": f"{stirrup_designation} @ {s_required:.0f} mm c/c",
"Av": Av,
"Vs": Vs / 1000, # Convert back to kN
"Vc": Vc / 1000, # Convert back to kN
"stirrup_type": stirrup_designation
}
def design_beam(self):
"""
Complete beam design including flexural and shear design
"""
if not self.moments:
self.analyze_moments()
design_results = []
for i, (moments, shears) in enumerate(zip(self.moments, self.shears)):
span_design = {
'span': i + 1,
'length': self.spans[i]['length'],
'moments': moments,
'shears': shears,
'reinforcement': [],
'stirrups': []
}
# Design for each critical section
moment_locations = ['Left Support', 'Mid-span', 'Right Support']
for j, (moment, shear) in enumerate(zip(moments, shears)):
# Flexural design
if moment != 0:
As_required = self.calculate_required_reinforcement(moment)
# Select reinforcement bars - Thai DB bars with spacing check
bar_data = {
12: {'area': 113, 'diameter': 12}, # DB12
16: {'area': 201, 'diameter': 16}, # DB16
20: {'area': 314, 'diameter': 20}, # DB20
24: {'area': 452, 'diameter': 24}, # DB24
32: {'area': 804, 'diameter': 32} # DB32
}
# Calculate minimum spacing requirements
cover = self.cover
stirrup_dia = 9 # Assume RB9 stirrups
# Try different bar sizes with spacing check
selected = False
for bar_size in sorted(bar_data.keys()):
bar_info = bar_data[bar_size]
bar_area = bar_info['area']
bar_diameter = bar_info['diameter']
num_bars = math.ceil(As_required / bar_area)
# Check practical limits
if num_bars > 8: # Too many bars
continue
if num_bars < 2: # Minimum 2 bars
num_bars = 2
# Calculate required spacing
# Available width = beam_width - 2×cover - 2×stirrup_dia
available_width = self.beam_width - 2*cover - 2*stirrup_dia
# Required spacing = (available_width - num_bars×bar_diameter) / (num_bars-1)
if num_bars > 1:
required_spacing = (available_width - num_bars * bar_diameter) / (num_bars - 1)
else:
required_spacing = available_width # Single bar case
# Minimum spacing = max(25mm, bar_diameter, aggregate_size)
# Use conservative 25mm minimum
min_spacing = max(25, bar_diameter)
# Check if spacing is adequate
if required_spacing >= min_spacing:
As_provided = num_bars * bar_area
selected = True
break
if not selected:
# If no bar size works, use largest bars and warn
bar_size = 32
bar_area = bar_data[32]['area']
bar_diameter = bar_data[32]['diameter']
num_bars = max(2, math.ceil(As_required / bar_area))
As_provided = num_bars * bar_area
# Calculate actual spacing for warning
available_width = self.beam_width - 2*cover - 2*stirrup_dia
if num_bars > 1:
actual_spacing = (available_width - num_bars * bar_diameter) / (num_bars - 1)
else:
actual_spacing = available_width
if actual_spacing < 25:
print(f"Warning: Tight bar spacing ({actual_spacing:.1f}mm) at {moment_locations[j]}. Consider increasing beam width.")
# Calculate final spacing for display
available_width = self.beam_width - 2*cover - 2*stirrup_dia
if num_bars > 1:
final_spacing = (available_width - num_bars * bar_diameter) / (num_bars - 1)
else:
final_spacing = available_width
reinforcement = {
'location': moment_locations[j],
'moment': moment,
'As_required': As_required,
'As_provided': As_provided,
'bars': f"{num_bars}-DB{bar_size}",
'spacing': f"{final_spacing:.0f}mm",
'ratio': As_provided / (self.beam_width * self.d) * 100
}
else:
reinforcement = {
'location': moment_locations[j],
'moment': 0,
'As_required': 0,
'As_provided': 0,
'bars': "No reinforcement",
'spacing': "N/A",
'ratio': 0
}
span_design['reinforcement'].append(reinforcement)
# Shear design
stirrup_design = self.calculate_shear_reinforcement(shear)
stirrup_design['location'] = moment_locations[j]
stirrup_design['shear'] = shear
span_design['stirrups'].append(stirrup_design)
design_results.append(span_design)
return design_results
def generate_report(self, design_results):
"""
Generate design report
"""
report = []
report.append("="*60)
report.append("CONTINUOUS BEAM RC DESIGN REPORT")
report.append("According to ACI Code")
report.append("="*60)
report.append(f"Beam dimensions: {self.beam_width}mm × {self.beam_depth}mm")
report.append(f"Concrete strength (f'c): {self.fc} MPa")
report.append(f"Steel strength (fy): {self.fy} MPa")
report.append(f"Effective depth (d): {self.d} mm")
report.append("")
for span_data in design_results:
report.append(f"SPAN {span_data['span']} - Length: {span_data['length']} m")
report.append("-" * 40)
# Moments and reinforcement
report.append("FLEXURAL DESIGN:")
for reinf in span_data['reinforcement']:
if reinf['moment'] != 0:
report.append(f" {reinf['location']}:")
report.append(f" Moment: {reinf['moment']:.2f} kN-m")
report.append(f" As required: {reinf['As_required']:.0f} mm²")
report.append(f" As provided: {reinf['As_provided']:.0f} mm²")
report.append(f" Reinforcement: {reinf['bars']}")
report.append(f" Bar spacing: {reinf['spacing']}")
report.append(f" Reinforcement ratio: {reinf['ratio']:.2f}%")
report.append("")
# Shear and stirrups
report.append("SHEAR DESIGN:")
for stirrup in span_data['stirrups']:
if stirrup['shear'] != 0:
report.append(f" {stirrup['location']}:")
report.append(f" Shear: {stirrup['shear']:.2f} kN")
if 'Vs' in stirrup:
report.append(f" Vc: {stirrup['Vc']:.2f} kN")
report.append(f" Vs: {stirrup['Vs']:.2f} kN")
report.append(f" Stirrup spacing: {stirrup['stirrup_spacing']}")
report.append("")
report.append("")
return "\n".join(report)
def plot_bmd_sfd(self, design_results=None):
"""
Generate BMD and SFD plots
"""
if design_results is None:
design_results = self.design_beam()
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
# Calculate total beam length and positions
total_length = 0
span_positions = [0]
for span in self.spans:
total_length += span['length']
span_positions.append(total_length)
# Use detailed FE results if available, otherwise fall back to approximate method
if hasattr(self, 'detailed_x') and hasattr(self, 'detailed_moments'):
# Use finite element results
x_coords = self.detailed_x
moments_detailed = self.detailed_moments
shears_detailed = self.detailed_shears
else:
# Fallback to approximate method for backwards compatibility
x_coords = []
moments_detailed = []
shears_detailed = []
for i, span_data in enumerate(design_results):
span_length = span_data['length']
start_pos = span_positions[i]
# Create x coordinates for this span
x_span = np.linspace(start_pos, start_pos + span_length, 100)
# Get moments and shears for this span
moments = span_data['moments'] # [left, mid, right]
shears = span_data['shears']
# Simple interpolation between critical points
moment_curve = np.interp(x_span,
[start_pos, start_pos + span_length/2, start_pos + span_length],
moments)
shear_curve = np.interp(x_span,
[start_pos, start_pos + span_length/2, start_pos + span_length],
shears)
x_coords.extend(x_span)
moments_detailed.extend(moment_curve)
shears_detailed.extend(shear_curve)
# Plot BMD
ax1.plot(x_coords, moments_detailed, 'b-', linewidth=2, label='Bending Moment')
ax1.fill_between(x_coords, moments_detailed, alpha=0.3, color='blue')
ax1.axhline(y=0, color='k', linestyle='-', alpha=0.3)
ax1.set_ylabel('Bending Moment (kN-m)', fontsize=12)
ax1.set_title('Bending Moment Diagram (BMD)', fontsize=14, fontweight='bold')
ax1.grid(True, alpha=0.3)
ax1.legend()
# Add support symbols
for pos in span_positions:
ax1.axvline(x=pos, color='red', linestyle='--', alpha=0.7)
ax1.plot(pos, 0, 'rs', markersize=8, label='Support' if pos == span_positions[0] else "")
# Plot SFD
ax2.plot(x_coords, shears_detailed, 'r-', linewidth=2, label='Shear Force')
ax2.fill_between(x_coords, shears_detailed, alpha=0.3, color='red')
ax2.axhline(y=0, color='k', linestyle='-', alpha=0.3)
ax2.set_ylabel('Shear Force (kN)', fontsize=12)
ax2.set_xlabel('Distance along beam (m)', fontsize=12)
ax2.set_title('Shear Force Diagram (SFD)', fontsize=14, fontweight='bold')
ax2.grid(True, alpha=0.3)
ax2.legend()
# Add support symbols
for pos in span_positions:
ax2.axvline(x=pos, color='red', linestyle='--', alpha=0.7)
ax2.plot(pos, 0, 'rs', markersize=8, label='Support' if pos == span_positions[0] else "")
plt.tight_layout()
# Close any other open figures to free memory
for i in plt.get_fignums():
if i != fig.number:
plt.close(i)
return fig
def plot_reinforcement_layout(self, design_results=None):
"""
Generate reinforcement layout diagram
"""
if design_results is None:
design_results = self.design_beam()
fig, ax = plt.subplots(1, 1, figsize=(14, 8))
# Calculate positions
total_length = sum(span['length'] for span in self.spans)
span_positions = [0]
current_pos = 0
for span in self.spans:
current_pos += span['length']
span_positions.append(current_pos)
# Draw beam outline
beam_height = 0.5 # Normalized height for drawing
ax.add_patch(plt.Rectangle((0, -beam_height/2), total_length, beam_height,
fill=False, edgecolor='black', linewidth=2))
# Add beam dimensions text
ax.text(total_length/2, beam_height/2 + 0.1,
f'{self.beam_width}mm × {self.beam_depth}mm',
ha='center', va='bottom', fontsize=10, fontweight='bold')
# Draw reinforcement for each span
colors = ['blue', 'green', 'orange', 'purple', 'brown']
for i, span_data in enumerate(design_results):
span_start = span_positions[i]
span_end = span_positions[i + 1]
span_center = (span_start + span_end) / 2
color = colors[i % len(colors)]
# Process reinforcement
for j, reinf in enumerate(span_data['reinforcement']):
if reinf['As_provided'] > 0:
location = reinf['location']
bars = reinf['bars']
if location == 'Left Support':
x_pos = span_start
y_pos = beam_height/3 # Top reinforcement
marker = '^'
label_pos = 'top'
elif location == 'Mid-span':
x_pos = span_center
y_pos = -beam_height/3 # Bottom reinforcement
marker = 'v'
label_pos = 'bottom'
else: # Right Support
x_pos = span_end
y_pos = beam_height/3 # Top reinforcement
marker = '^'
label_pos = 'top'
# Draw reinforcement symbol
ax.scatter(x_pos, y_pos, s=100, c=color, marker=marker,
edgecolor='black', linewidth=1, zorder=5)
# Get spacing information for this reinforcement
spacing_info = ""
if 'spacing' in reinf and reinf['spacing'] != "N/A":
spacing_info = f"\nSpacing: {reinf['spacing']}"
# Add reinforcement label with spacing
label_text = bars + spacing_info
if label_pos == 'top':
ax.text(x_pos, y_pos + 0.15, label_text, ha='center', va='bottom',
fontsize=9, fontweight='bold', rotation=0,
bbox=dict(boxstyle="round,pad=0.2", facecolor="lightblue", alpha=0.8))
else:
ax.text(x_pos, y_pos - 0.15, label_text, ha='center', va='top',
fontsize=9, fontweight='bold', rotation=0,
bbox=dict(boxstyle="round,pad=0.2", facecolor="lightblue", alpha=0.8))
# Draw dimension lines for bar spacing if multiple bars
if 'num_bars' in reinf and reinf['num_bars'] > 1 and 'spacing' in reinf and reinf['spacing'] != "N/A":
bar_count = reinf['num_bars']
# Extract numerical value from spacing string (e.g., "150mm" -> 150)
try:
spacing_mm = float(reinf['spacing'].replace('mm', ''))
spacing = spacing_mm / 1000 # Convert mm to m for plotting
except:
continue # Skip if spacing format is unexpected
# Calculate bar positions along the beam width (shown as small offset from main position)
if location == 'Mid-span': # Bottom bars
# Show individual bar positions
total_bar_width = (bar_count - 1) * spacing / 20 # Scale for visualization
start_offset = -total_bar_width / 2
for bar_idx in range(bar_count):
bar_x = x_pos + start_offset + (bar_idx * total_bar_width / (bar_count - 1) if bar_count > 1 else 0)
ax.scatter(bar_x, y_pos, s=30, c='darkblue', marker='o',
edgecolor='black', linewidth=0.5, zorder=6, alpha=0.7)
# Add spacing dimension line below
if bar_count > 1:
dim_y = y_pos - 0.25
ax.annotate('', xy=(x_pos + start_offset, dim_y),
xytext=(x_pos + start_offset + total_bar_width, dim_y),
arrowprops=dict(arrowstyle='<->', color='red', lw=1))
ax.text(x_pos, dim_y - 0.05, f'{reinf["spacing"]} c/c',
ha='center', va='top', fontsize=7, color='red', fontweight='bold')
# Draw supports
for i, pos in enumerate(span_positions):
# Support triangle
triangle_height = 0.2
triangle_width = 0.1
triangle = plt.Polygon([
[pos - triangle_width/2, -beam_height/2],
[pos + triangle_width/2, -beam_height/2],
[pos, -beam_height/2 - triangle_height]
], fill=True, facecolor='red', edgecolor='black')
ax.add_patch(triangle)
# Support label
ax.text(pos, -beam_height/2 - triangle_height - 0.1,
f'Support {i+1}', ha='center', va='top', fontsize=8)
# Add span labels and loads
for i, span in enumerate(self.spans):
span_start = span_positions[i]
span_end = span_positions[i + 1]
span_center = (span_start + span_end) / 2
# Create span label text
label_text = f'Span {i+1}\nL = {span["length"]}m\nw = {span["distributed_load"]}kN/m'
# Add point loads to label if any
if span.get('point_loads'):
point_load_text = '\nPoint Loads:'
for pos, load in span['point_loads']:
point_load_text += f'\n{load}kN @ {pos}m'
label_text += point_load_text
# Span label
ax.text(span_center, -beam_height/2 - 0.4, label_text,
ha='center', va='top', fontsize=9,
bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.7))
# Distributed load arrows
if span["distributed_load"] > 0:
num_arrows = 5
for j in range(num_arrows):
x_arrow = span_start + (span_end - span_start) * j / (num_arrows - 1)
ax.arrow(x_arrow, beam_height/2 + 0.3, 0, -0.2,
head_width=0.05, head_length=0.05, fc='red', ec='red')
# Point load arrows
if span.get('point_loads'):
for pos, load in span['point_loads']:
x_point = span_start + pos
# Larger arrow for point loads
ax.arrow(x_point, beam_height/2 + 0.5, 0, -0.4,
head_width=0.08, head_length=0.08, fc='blue', ec='blue', linewidth=2)
# Point load label
ax.text(x_point, beam_height/2 + 0.6, f'{load}kN',
ha='center', va='bottom', fontsize=8, fontweight='bold', color='blue')
# Formatting
ax.set_xlim(-0.5, total_length + 0.5)
ax.set_ylim(-1.2, 1.0)
ax.set_xlabel('Distance along beam (m)', fontsize=12)
ax.set_title('Reinforcement Layout', fontsize=14, fontweight='bold')
ax.grid(True, alpha=0.3)
ax.set_aspect('equal')
# Legend
legend_elements = [
plt.scatter([], [], s=100, c='blue', marker='^', edgecolor='black',
label='Top Reinforcement (Negative Moment)'),
plt.scatter([], [], s=100, c='blue', marker='v', edgecolor='black',
label='Bottom Reinforcement (Positive Moment)')
]
ax.legend(handles=legend_elements, loc='upper right')
plt.tight_layout()
# Close any other open figures to free memory
for i in plt.get_fignums():
if i != fig.number:
plt.close(i)
return fig
def plot_stirrup_layout(self, design_results=None):
"""
Generate shear stirrup layout diagram
"""
if design_results is None:
design_results = self.design_beam()
fig, ax = plt.subplots(1, 1, figsize=(14, 6))
# Calculate positions
total_length = sum(span['length'] for span in self.spans)
span_positions = [0]
current_pos = 0
for span in self.spans:
current_pos += span['length']
span_positions.append(current_pos)
# Draw beam outline (side view)
beam_height = 0.5
ax.add_patch(plt.Rectangle((0, 0), total_length, beam_height,
fill=False, edgecolor='black', linewidth=2))
# Draw detailed stirrup layout for each span
for i, span_data in enumerate(design_results):
span_start = span_positions[i]
span_end = span_positions[i + 1]
span_length = span_end - span_start
# Get stirrup information with locations
stirrup_regions = []
for stirrup in span_data['stirrups']:
if 'No stirrups' not in stirrup['stirrup_spacing']:
# Extract stirrup type and spacing
stirrup_parts = stirrup['stirrup_spacing'].split(' @ ')
if len(stirrup_parts) == 2:
stirrup_type = stirrup_parts[0] # e.g., "RB9" or "RB6"
spacing_str = stirrup_parts[1].replace(' mm c/c', '')
try:
spacing_mm = float(spacing_str)
spacing_m = spacing_mm / 1000
stirrup_regions.append({
'location': stirrup['location'],
'type': stirrup_type,
'spacing_mm': spacing_mm,
'spacing_m': spacing_m,
'shear': stirrup['shear']
})
except:
pass
if stirrup_regions:
# Create detailed stirrup pattern for the span
# Divide span into regions based on stirrup requirements
regions = {
'Left Support': {'start': span_start, 'end': span_start + span_length * 0.25},
'Mid-span': {'start': span_start + span_length * 0.25, 'end': span_start + span_length * 0.75},
'Right Support': {'start': span_start + span_length * 0.75, 'end': span_end}
}
stirrup_positions = []
stirrup_labels = []
for stirrup_region in stirrup_regions:
location = stirrup_region['location']
if location in regions:
region_start = regions[location]['start']
region_end = regions[location]['end']
region_length = region_end - region_start
spacing = stirrup_region['spacing_m']
# Calculate stirrup positions in this region
num_stirrups = max(2, int(region_length / spacing) + 1)
actual_spacing = region_length / (num_stirrups - 1) if num_stirrups > 1 else region_length
for j in range(num_stirrups):
x_pos = region_start + j * actual_spacing
if x_pos <= region_end:
stirrup_positions.append(x_pos)
stirrup_labels.append({
'x': x_pos,
'type': stirrup_region['type'],
'spacing': stirrup_region['spacing_mm'],
'location': location
})
# Draw all stirrups
colors = {'RB6': 'green', 'RB9': 'darkgreen'}
for pos in stirrup_positions:
# Draw stirrup as detailed U-shape
stirrup_width = 0.02
# Main vertical lines
ax.plot([pos, pos], [beam_height*0.05, beam_height*0.95], 'g-', linewidth=3, alpha=0.8)
# Horizontal top and bottom connections
ax.plot([pos-stirrup_width, pos+stirrup_width], [beam_height*0.05, beam_height*0.05], 'g-', linewidth=2, alpha=0.8)
ax.plot([pos-stirrup_width, pos+stirrup_width], [beam_height*0.95, beam_height*0.95], 'g-', linewidth=2, alpha=0.8)
# Side connections
ax.plot([pos-stirrup_width, pos-stirrup_width], [beam_height*0.05, beam_height*0.95], 'g-', linewidth=2, alpha=0.8)
ax.plot([pos+stirrup_width, pos+stirrup_width], [beam_height*0.05, beam_height*0.95], 'g-', linewidth=2, alpha=0.8)
# Add spacing dimensions between stirrups
if len(stirrup_positions) >= 2:
# Group consecutive stirrups and show spacing
prev_pos = stirrup_positions[0]
for k in range(1, min(4, len(stirrup_positions))): # Show first few spacings
curr_pos = stirrup_positions[k]
spacing_actual = (curr_pos - prev_pos) * 1000 # Convert to mm
# Dimension line above beam
dim_y = beam_height + 0.15 + (k-1) * 0.08
ax.annotate('', xy=(prev_pos, dim_y), xytext=(curr_pos, dim_y),
arrowprops=dict(arrowstyle='<->', color='red', lw=1.5))
# Spacing text
ax.text((prev_pos + curr_pos) / 2, dim_y + 0.02, f'{spacing_actual:.0f}mm',
ha='center', va='bottom', fontsize=7, color='red', fontweight='bold',
bbox=dict(boxstyle="round,pad=0.1", facecolor="white", alpha=0.9))
# Vertical dimension lines
ax.plot([prev_pos, prev_pos], [beam_height, dim_y - 0.01], 'r--', linewidth=1, alpha=0.5)
ax.plot([curr_pos, curr_pos], [beam_height, dim_y - 0.01], 'r--', linewidth=1, alpha=0.5)
prev_pos = curr_pos
# Add stirrup type and spacing summary
mid_span = (span_start + span_end) / 2
# Create summary text for stirrup types used
stirrup_summary = []
for region in stirrup_regions:
stirrup_summary.append(f"{region['type']} @ {region['spacing_mm']:.0f}mm ({region['location']})")
summary_text = "\n".join(stirrup_summary)
ax.text(mid_span, beam_height + 0.4, f'Span {i+1} Stirrups:\n{summary_text}',
ha='center', va='bottom', fontsize=8,
bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen", alpha=0.7))
# Draw supports
for i, pos in enumerate(span_positions):
# Support line
ax.plot([pos, pos], [-0.1, beam_height + 0.05], 'r--', linewidth=2, alpha=0.7)
# Support symbol (triangle)
triangle = plt.Polygon([
[pos - 0.05, -0.1],
[pos + 0.05, -0.1],
[pos, -0.2]
], fill=True, facecolor='red', edgecolor='black')
ax.add_patch(triangle)
# Support label
ax.text(pos, -0.25, f'Support {i+1}', ha='center', va='top', fontsize=8, fontweight='bold')
# Add dimension lines and labels
for i, span in enumerate(self.spans):
span_start = span_positions[i]
span_end = span_positions[i + 1]
span_center = (span_start + span_end) / 2
# Dimension line
ax.annotate('', xy=(span_start, -0.3), xytext=(span_end, -0.3),
arrowprops=dict(arrowstyle='<->', color='black', lw=1))
# Span length label
ax.text(span_center, -0.35, f'{span["length"]}m',
ha='center', va='top', fontsize=10)
# Formatting with more space for detailed annotations
ax.set_xlim(-0.3, total_length + 0.3)
ax.set_ylim(-0.6, beam_height + 0.8)
ax.set_xlabel('Distance along beam (m)', fontsize=12)
ax.set_ylabel('Beam Cross-Section', fontsize=12)
ax.set_title('Detailed Shear Stirrup Layout with Spacing Dimensions', fontsize=14, fontweight='bold')
ax.grid(True, alpha=0.3)
# Add comprehensive legend
legend_elements = [
plt.Line2D([0], [0], color='green', linewidth=3, alpha=0.8, label='Stirrups (U-shaped)'),
plt.Line2D([0], [0], color='red', linestyle='-', linewidth=1.5, label='Spacing Dimensions'),
plt.Line2D([0], [0], color='red', linestyle='--', linewidth=2, alpha=0.7, label='Supports'),
plt.Rectangle((0,0),1,1, facecolor='lightgreen', alpha=0.7, label='Stirrup Details')
]
ax.legend(handles=legend_elements, loc='upper right', fontsize=10)
# Add beam dimensions annotation
ax.text(total_length/2, -0.45, f'Beam: {self.beam_width}mm × {self.beam_depth}mm',
ha='center', va='center', fontsize=10, fontweight='bold',
bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.8))
plt.tight_layout()
# Close any other open figures to free memory
for i in plt.get_fignums():
if i != fig.number:
plt.close(i)
return fig