Spaces:
Sleeping
Sleeping
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 |