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 |