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