Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- app.py +234 -0
- beam_design_app.py +202 -0
- continuous_beam.py +1036 -0
- requirements.txt +4 -0
app.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from continuous_beam import ContinuousBeam
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
import numpy as np
|
| 6 |
+
import io
|
| 7 |
+
import base64
|
| 8 |
+
|
| 9 |
+
class GradioBeamApp:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.beam = ContinuousBeam()
|
| 12 |
+
self.spans_data = []
|
| 13 |
+
|
| 14 |
+
def parse_point_loads(self, point_loads_text):
|
| 15 |
+
"""Parse point loads from text input"""
|
| 16 |
+
if not point_loads_text or point_loads_text.strip() == "":
|
| 17 |
+
return []
|
| 18 |
+
|
| 19 |
+
point_loads = []
|
| 20 |
+
try:
|
| 21 |
+
# Expected format: "position1,load1; position2,load2; ..."
|
| 22 |
+
# Example: "2.0,50; 4.0,30"
|
| 23 |
+
load_pairs = point_loads_text.split(';')
|
| 24 |
+
for pair in load_pairs:
|
| 25 |
+
if pair.strip():
|
| 26 |
+
pos_str, load_str = pair.split(',')
|
| 27 |
+
position = float(pos_str.strip())
|
| 28 |
+
load = float(load_str.strip())
|
| 29 |
+
point_loads.append((position, load))
|
| 30 |
+
except:
|
| 31 |
+
raise ValueError("Invalid point load format. Use: position1,load1; position2,load2")
|
| 32 |
+
|
| 33 |
+
return point_loads
|
| 34 |
+
|
| 35 |
+
def add_span(self, length, distributed_load, point_loads_text, spans_display):
|
| 36 |
+
"""Add a span to the beam"""
|
| 37 |
+
try:
|
| 38 |
+
length = float(length)
|
| 39 |
+
distributed_load = float(distributed_load)
|
| 40 |
+
|
| 41 |
+
if length <= 0 or distributed_load < 0:
|
| 42 |
+
return spans_display, "Error: Please enter valid values (length > 0, distributed_load >= 0)"
|
| 43 |
+
|
| 44 |
+
# Parse point loads
|
| 45 |
+
point_loads = self.parse_point_loads(point_loads_text)
|
| 46 |
+
|
| 47 |
+
# Validate point load positions
|
| 48 |
+
for pos, load in point_loads:
|
| 49 |
+
if pos < 0 or pos > length:
|
| 50 |
+
return spans_display, f"Error: Point load at {pos}m is outside span length {length}m"
|
| 51 |
+
if load < 0:
|
| 52 |
+
return spans_display, f"Error: Point load {load}kN must be positive"
|
| 53 |
+
|
| 54 |
+
self.beam.add_span(length, distributed_load, point_loads)
|
| 55 |
+
|
| 56 |
+
# Create span description
|
| 57 |
+
span_info = f"Span {len(self.beam.spans)}: L={length}m, w={distributed_load}kN/m"
|
| 58 |
+
if point_loads:
|
| 59 |
+
point_load_str = ", ".join([f"P={load}kN@{pos}m" for pos, load in point_loads])
|
| 60 |
+
span_info += f", {point_load_str}"
|
| 61 |
+
|
| 62 |
+
self.spans_data.append(span_info)
|
| 63 |
+
|
| 64 |
+
# Update display
|
| 65 |
+
updated_display = "\n".join(self.spans_data)
|
| 66 |
+
return updated_display, f"Added: {span_info}"
|
| 67 |
+
|
| 68 |
+
except ValueError as e:
|
| 69 |
+
return spans_display, f"Error: {str(e)}"
|
| 70 |
+
|
| 71 |
+
def clear_spans(self):
|
| 72 |
+
"""Clear all spans"""
|
| 73 |
+
self.beam = ContinuousBeam()
|
| 74 |
+
self.spans_data = []
|
| 75 |
+
return "", "All spans cleared"
|
| 76 |
+
|
| 77 |
+
def design_beam(self, width, depth, fc, fy, cover, spans_display):
|
| 78 |
+
"""Design the beam and return results"""
|
| 79 |
+
if not self.beam.spans:
|
| 80 |
+
return "No spans added. Please add at least one span.", None, None, None, None, None
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
# Update beam properties
|
| 84 |
+
self.beam.beam_width = float(width)
|
| 85 |
+
self.beam.beam_depth = float(depth)
|
| 86 |
+
self.beam.fc = float(fc)
|
| 87 |
+
self.beam.fy = float(fy)
|
| 88 |
+
self.beam.cover = float(cover)
|
| 89 |
+
self.beam.d = self.beam.beam_depth - self.beam.cover
|
| 90 |
+
|
| 91 |
+
# Perform design
|
| 92 |
+
design_results = self.beam.design_beam()
|
| 93 |
+
|
| 94 |
+
# Generate report
|
| 95 |
+
report = self.beam.generate_report(design_results)
|
| 96 |
+
|
| 97 |
+
# Create summary table
|
| 98 |
+
summary_data = []
|
| 99 |
+
for span_data in design_results:
|
| 100 |
+
span_num = span_data['span']
|
| 101 |
+
for i, (reinf, stirrup) in enumerate(zip(span_data['reinforcement'], span_data['stirrups'])):
|
| 102 |
+
if reinf['moment'] != 0 or stirrup['shear'] != 0:
|
| 103 |
+
summary_data.append([
|
| 104 |
+
span_num if i == 0 else '',
|
| 105 |
+
reinf['location'],
|
| 106 |
+
f"{reinf['moment']:.2f}" if reinf['moment'] != 0 else '-',
|
| 107 |
+
reinf['bars'] if reinf['moment'] != 0 else '-',
|
| 108 |
+
f"{stirrup['shear']:.2f}" if stirrup['shear'] != 0 else '-',
|
| 109 |
+
stirrup['stirrup_spacing'] if stirrup['shear'] != 0 else '-'
|
| 110 |
+
])
|
| 111 |
+
|
| 112 |
+
# Create DataFrame for table display
|
| 113 |
+
df = pd.DataFrame(summary_data, columns=[
|
| 114 |
+
'Span', 'Location', 'Moment (kN-m)', 'Reinforcement', 'Shear (kN)', 'Stirrups'
|
| 115 |
+
])
|
| 116 |
+
|
| 117 |
+
# Generate plots
|
| 118 |
+
bmd_sfd_plot = self.beam.plot_bmd_sfd(design_results)
|
| 119 |
+
reinforcement_plot = self.beam.plot_reinforcement_layout(design_results)
|
| 120 |
+
stirrup_plot = self.beam.plot_stirrup_layout(design_results)
|
| 121 |
+
|
| 122 |
+
return report, df, bmd_sfd_plot, reinforcement_plot, stirrup_plot, "Design completed successfully!"
|
| 123 |
+
|
| 124 |
+
except Exception as e:
|
| 125 |
+
return f"Design calculation failed: {str(e)}", None, None, None, None, f"Error: {str(e)}"
|
| 126 |
+
|
| 127 |
+
def create_interface():
|
| 128 |
+
app = GradioBeamApp()
|
| 129 |
+
|
| 130 |
+
with gr.Blocks(title="Continuous Beam RC Design - ACI Code", theme=gr.themes.Default()) as interface:
|
| 131 |
+
|
| 132 |
+
gr.Markdown("# Continuous Beam RC Design - ACI Code")
|
| 133 |
+
gr.Markdown("*Using Finite Element Analysis for accurate structural behavior*")
|
| 134 |
+
|
| 135 |
+
with gr.Row():
|
| 136 |
+
with gr.Column(scale=1):
|
| 137 |
+
gr.Markdown("## Beam Properties")
|
| 138 |
+
|
| 139 |
+
with gr.Row():
|
| 140 |
+
width_input = gr.Number(label="Beam Width (mm)", value=300)
|
| 141 |
+
depth_input = gr.Number(label="Beam Depth (mm)", value=500)
|
| 142 |
+
|
| 143 |
+
with gr.Row():
|
| 144 |
+
fc_input = gr.Number(label="f'c (MPa)", value=28)
|
| 145 |
+
fy_input = gr.Number(label="fy (MPa)", value=420)
|
| 146 |
+
|
| 147 |
+
cover_input = gr.Number(label="Cover (mm)", value=40)
|
| 148 |
+
|
| 149 |
+
gr.Markdown("## Spans Configuration")
|
| 150 |
+
|
| 151 |
+
with gr.Row():
|
| 152 |
+
span_length_input = gr.Number(label="Span Length (m)", value=6.0)
|
| 153 |
+
distributed_load_input = gr.Number(label="Distributed Load (kN/m)", value=25.0)
|
| 154 |
+
|
| 155 |
+
point_loads_input = gr.Textbox(
|
| 156 |
+
label="Point Loads (position,load; position,load; ...)",
|
| 157 |
+
placeholder="Example: 2.0,50; 4.0,30 (means 50kN at 2m and 30kN at 4m)",
|
| 158 |
+
value=""
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
with gr.Row():
|
| 162 |
+
add_span_btn = gr.Button("Add Span", variant="primary")
|
| 163 |
+
clear_spans_btn = gr.Button("Clear All", variant="secondary")
|
| 164 |
+
|
| 165 |
+
spans_display = gr.Textbox(
|
| 166 |
+
label="Added Spans",
|
| 167 |
+
lines=5,
|
| 168 |
+
interactive=False,
|
| 169 |
+
placeholder="No spans added yet..."
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
design_btn = gr.Button("Design Beam", variant="primary", size="lg")
|
| 173 |
+
|
| 174 |
+
status_output = gr.Textbox(label="Status", interactive=False)
|
| 175 |
+
|
| 176 |
+
with gr.Column(scale=2):
|
| 177 |
+
gr.Markdown("## Design Results")
|
| 178 |
+
|
| 179 |
+
with gr.Tabs():
|
| 180 |
+
with gr.TabItem("Summary"):
|
| 181 |
+
summary_table = gr.Dataframe(
|
| 182 |
+
label="Design Summary",
|
| 183 |
+
headers=["Span", "Location", "Moment (kN-m)", "Reinforcement", "Shear (kN)", "Stirrups"],
|
| 184 |
+
interactive=False
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
with gr.TabItem("BMD & SFD"):
|
| 188 |
+
bmd_sfd_plot = gr.Plot(label="Bending Moment and Shear Force Diagrams")
|
| 189 |
+
|
| 190 |
+
with gr.TabItem("Reinforcement Layout"):
|
| 191 |
+
reinforcement_plot = gr.Plot(label="Reinforcement Bar Layout")
|
| 192 |
+
|
| 193 |
+
with gr.TabItem("Stirrup Layout"):
|
| 194 |
+
stirrup_plot = gr.Plot(label="Shear Stirrup Layout")
|
| 195 |
+
|
| 196 |
+
with gr.TabItem("Detailed Report"):
|
| 197 |
+
results_output = gr.Textbox(
|
| 198 |
+
label="Detailed Design Report",
|
| 199 |
+
lines=20,
|
| 200 |
+
interactive=False,
|
| 201 |
+
show_copy_button=True
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
# Event handlers
|
| 205 |
+
add_span_btn.click(
|
| 206 |
+
fn=lambda length, dist_load, point_loads, spans: app.add_span(length, dist_load, point_loads, spans),
|
| 207 |
+
inputs=[span_length_input, distributed_load_input, point_loads_input, spans_display],
|
| 208 |
+
outputs=[spans_display, status_output]
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
clear_spans_btn.click(
|
| 212 |
+
fn=lambda: app.clear_spans(),
|
| 213 |
+
outputs=[spans_display, status_output]
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
design_btn.click(
|
| 217 |
+
fn=lambda w, d, fc, fy, c, spans: app.design_beam(w, d, fc, fy, c, spans),
|
| 218 |
+
inputs=[width_input, depth_input, fc_input, fy_input, cover_input, spans_display],
|
| 219 |
+
outputs=[results_output, summary_table, bmd_sfd_plot, reinforcement_plot, stirrup_plot, status_output]
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
return interface
|
| 223 |
+
|
| 224 |
+
def main():
|
| 225 |
+
interface = create_interface()
|
| 226 |
+
interface.launch(
|
| 227 |
+
server_name="0.0.0.0", # Allow external access
|
| 228 |
+
server_port=8000, # Default Gradio port
|
| 229 |
+
share=False, # Set to True if you want a public link
|
| 230 |
+
debug=True
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
if __name__ == "__main__":
|
| 234 |
+
main()
|
beam_design_app.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tkinter as tk
|
| 2 |
+
from tkinter import ttk, messagebox, scrolledtext
|
| 3 |
+
from continuous_beam import ContinuousBeam
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
class BeamDesignApp:
|
| 9 |
+
def __init__(self, root):
|
| 10 |
+
self.root = root
|
| 11 |
+
self.root.title("Continuous Beam RC Design - ACI Code")
|
| 12 |
+
self.root.geometry("1200x800")
|
| 13 |
+
|
| 14 |
+
self.beam = ContinuousBeam()
|
| 15 |
+
self.setup_ui()
|
| 16 |
+
|
| 17 |
+
def setup_ui(self):
|
| 18 |
+
# Create main frame
|
| 19 |
+
main_frame = ttk.Frame(self.root, padding="10")
|
| 20 |
+
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
| 21 |
+
|
| 22 |
+
# Configure grid weights
|
| 23 |
+
self.root.columnconfigure(0, weight=1)
|
| 24 |
+
self.root.rowconfigure(0, weight=1)
|
| 25 |
+
main_frame.columnconfigure(1, weight=1)
|
| 26 |
+
main_frame.rowconfigure(3, weight=1)
|
| 27 |
+
|
| 28 |
+
# Title
|
| 29 |
+
title_label = ttk.Label(main_frame, text="Continuous Beam RC Design",
|
| 30 |
+
font=('Arial', 16, 'bold'))
|
| 31 |
+
title_label.grid(row=0, column=0, columnspan=2, pady=(0, 20))
|
| 32 |
+
|
| 33 |
+
# Input frame
|
| 34 |
+
input_frame = ttk.LabelFrame(main_frame, text="Beam Properties", padding="10")
|
| 35 |
+
input_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
|
| 36 |
+
|
| 37 |
+
# Beam properties
|
| 38 |
+
ttk.Label(input_frame, text="Beam Width (mm):").grid(row=0, column=0, sticky=tk.W)
|
| 39 |
+
self.width_var = tk.StringVar(value="300")
|
| 40 |
+
ttk.Entry(input_frame, textvariable=self.width_var, width=10).grid(row=0, column=1, sticky=tk.W, padx=(5, 20))
|
| 41 |
+
|
| 42 |
+
ttk.Label(input_frame, text="Beam Depth (mm):").grid(row=0, column=2, sticky=tk.W)
|
| 43 |
+
self.depth_var = tk.StringVar(value="500")
|
| 44 |
+
ttk.Entry(input_frame, textvariable=self.depth_var, width=10).grid(row=0, column=3, sticky=tk.W, padx=(5, 20))
|
| 45 |
+
|
| 46 |
+
ttk.Label(input_frame, text="f'c (MPa):").grid(row=1, column=0, sticky=tk.W)
|
| 47 |
+
self.fc_var = tk.StringVar(value="28")
|
| 48 |
+
ttk.Entry(input_frame, textvariable=self.fc_var, width=10).grid(row=1, column=1, sticky=tk.W, padx=(5, 20))
|
| 49 |
+
|
| 50 |
+
ttk.Label(input_frame, text="fy (MPa):").grid(row=1, column=2, sticky=tk.W)
|
| 51 |
+
self.fy_var = tk.StringVar(value="420")
|
| 52 |
+
ttk.Entry(input_frame, textvariable=self.fy_var, width=10).grid(row=1, column=3, sticky=tk.W, padx=(5, 20))
|
| 53 |
+
|
| 54 |
+
ttk.Label(input_frame, text="Cover (mm):").grid(row=2, column=0, sticky=tk.W)
|
| 55 |
+
self.cover_var = tk.StringVar(value="40")
|
| 56 |
+
ttk.Entry(input_frame, textvariable=self.cover_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=(5, 20))
|
| 57 |
+
|
| 58 |
+
# Spans frame
|
| 59 |
+
spans_frame = ttk.LabelFrame(main_frame, text="Spans Configuration", padding="10")
|
| 60 |
+
spans_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
|
| 61 |
+
|
| 62 |
+
# Span input
|
| 63 |
+
ttk.Label(spans_frame, text="Span Length (m):").grid(row=0, column=0, sticky=tk.W)
|
| 64 |
+
self.span_length_var = tk.StringVar(value="6.0")
|
| 65 |
+
ttk.Entry(spans_frame, textvariable=self.span_length_var, width=10).grid(row=0, column=1, sticky=tk.W, padx=(5, 20))
|
| 66 |
+
|
| 67 |
+
ttk.Label(spans_frame, text="Distributed Load (kN/m):").grid(row=0, column=2, sticky=tk.W)
|
| 68 |
+
self.load_var = tk.StringVar(value="25.0")
|
| 69 |
+
ttk.Entry(spans_frame, textvariable=self.load_var, width=10).grid(row=0, column=3, sticky=tk.W, padx=(5, 20))
|
| 70 |
+
|
| 71 |
+
# Buttons
|
| 72 |
+
button_frame = ttk.Frame(spans_frame)
|
| 73 |
+
button_frame.grid(row=1, column=0, columnspan=4, pady=10)
|
| 74 |
+
|
| 75 |
+
ttk.Button(button_frame, text="Add Span", command=self.add_span).pack(side=tk.LEFT, padx=(0, 10))
|
| 76 |
+
ttk.Button(button_frame, text="Clear All", command=self.clear_spans).pack(side=tk.LEFT, padx=(0, 10))
|
| 77 |
+
ttk.Button(button_frame, text="Design Beam", command=self.design_beam).pack(side=tk.LEFT, padx=(0, 10))
|
| 78 |
+
|
| 79 |
+
# Spans list
|
| 80 |
+
self.spans_listbox = tk.Listbox(spans_frame, height=4)
|
| 81 |
+
self.spans_listbox.grid(row=2, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(10, 0))
|
| 82 |
+
spans_frame.columnconfigure(0, weight=1)
|
| 83 |
+
spans_frame.columnconfigure(1, weight=1)
|
| 84 |
+
spans_frame.columnconfigure(2, weight=1)
|
| 85 |
+
spans_frame.columnconfigure(3, weight=1)
|
| 86 |
+
|
| 87 |
+
# Results frame
|
| 88 |
+
results_frame = ttk.LabelFrame(main_frame, text="Design Results", padding="10")
|
| 89 |
+
results_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
|
| 90 |
+
results_frame.columnconfigure(0, weight=1)
|
| 91 |
+
results_frame.rowconfigure(0, weight=1)
|
| 92 |
+
|
| 93 |
+
# Results text area
|
| 94 |
+
self.results_text = scrolledtext.ScrolledText(results_frame, width=80, height=20, font=('Courier', 9))
|
| 95 |
+
self.results_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
| 96 |
+
|
| 97 |
+
def add_span(self):
|
| 98 |
+
try:
|
| 99 |
+
length = float(self.span_length_var.get())
|
| 100 |
+
load = float(self.load_var.get())
|
| 101 |
+
|
| 102 |
+
if length <= 0 or load < 0:
|
| 103 |
+
messagebox.showerror("Error", "Please enter valid values (length > 0, load >= 0)")
|
| 104 |
+
return
|
| 105 |
+
|
| 106 |
+
self.beam.add_span(length, load)
|
| 107 |
+
|
| 108 |
+
# Update spans list
|
| 109 |
+
span_text = f"Span {len(self.beam.spans)}: L={length}m, w={load}kN/m"
|
| 110 |
+
self.spans_listbox.insert(tk.END, span_text)
|
| 111 |
+
|
| 112 |
+
# Clear input fields
|
| 113 |
+
self.span_length_var.set("")
|
| 114 |
+
self.load_var.set("")
|
| 115 |
+
|
| 116 |
+
except ValueError:
|
| 117 |
+
messagebox.showerror("Error", "Please enter valid numeric values")
|
| 118 |
+
|
| 119 |
+
def clear_spans(self):
|
| 120 |
+
self.beam = ContinuousBeam()
|
| 121 |
+
self.spans_listbox.delete(0, tk.END)
|
| 122 |
+
self.results_text.delete('1.0', tk.END)
|
| 123 |
+
|
| 124 |
+
def design_beam(self):
|
| 125 |
+
if not self.beam.spans:
|
| 126 |
+
messagebox.showwarning("Warning", "Please add at least one span")
|
| 127 |
+
return
|
| 128 |
+
|
| 129 |
+
try:
|
| 130 |
+
# Update beam properties
|
| 131 |
+
self.beam.beam_width = float(self.width_var.get())
|
| 132 |
+
self.beam.beam_depth = float(self.depth_var.get())
|
| 133 |
+
self.beam.fc = float(self.fc_var.get())
|
| 134 |
+
self.beam.fy = float(self.fy_var.get())
|
| 135 |
+
self.beam.cover = float(self.cover_var.get())
|
| 136 |
+
self.beam.d = self.beam.beam_depth - self.beam.cover
|
| 137 |
+
|
| 138 |
+
# Perform design
|
| 139 |
+
design_results = self.beam.design_beam()
|
| 140 |
+
|
| 141 |
+
# Generate and display report
|
| 142 |
+
report = self.beam.generate_report(design_results)
|
| 143 |
+
|
| 144 |
+
self.results_text.delete('1.0', tk.END)
|
| 145 |
+
self.results_text.insert('1.0', report)
|
| 146 |
+
|
| 147 |
+
# Show summary dialog
|
| 148 |
+
self.show_summary(design_results)
|
| 149 |
+
|
| 150 |
+
except Exception as e:
|
| 151 |
+
messagebox.showerror("Error", f"Design calculation failed: {str(e)}")
|
| 152 |
+
|
| 153 |
+
def show_summary(self, design_results):
|
| 154 |
+
"""Show design summary in a popup window"""
|
| 155 |
+
summary_window = tk.Toplevel(self.root)
|
| 156 |
+
summary_window.title("Design Summary")
|
| 157 |
+
summary_window.geometry("600x400")
|
| 158 |
+
|
| 159 |
+
# Create treeview for summary
|
| 160 |
+
tree = ttk.Treeview(summary_window, columns=('Span', 'Location', 'Moment', 'Reinforcement', 'Shear', 'Stirrups'))
|
| 161 |
+
tree.heading('#0', text='', anchor='w')
|
| 162 |
+
tree.heading('Span', text='Span')
|
| 163 |
+
tree.heading('Location', text='Location')
|
| 164 |
+
tree.heading('Moment', text='Moment (kN-m)')
|
| 165 |
+
tree.heading('Reinforcement', text='Reinforcement')
|
| 166 |
+
tree.heading('Shear', text='Shear (kN)')
|
| 167 |
+
tree.heading('Stirrups', text='Stirrups')
|
| 168 |
+
|
| 169 |
+
tree.column('#0', width=0, stretch=False)
|
| 170 |
+
tree.column('Span', width=60)
|
| 171 |
+
tree.column('Location', width=100)
|
| 172 |
+
tree.column('Moment', width=100)
|
| 173 |
+
tree.column('Reinforcement', width=120)
|
| 174 |
+
tree.column('Shear', width=80)
|
| 175 |
+
tree.column('Stirrups', width=120)
|
| 176 |
+
|
| 177 |
+
for span_data in design_results:
|
| 178 |
+
span_num = span_data['span']
|
| 179 |
+
for i, (reinf, stirrup) in enumerate(zip(span_data['reinforcement'], span_data['stirrups'])):
|
| 180 |
+
if reinf['moment'] != 0 or stirrup['shear'] != 0:
|
| 181 |
+
tree.insert('', 'end', values=(
|
| 182 |
+
span_num if i == 0 else '',
|
| 183 |
+
reinf['location'],
|
| 184 |
+
f"{reinf['moment']:.2f}" if reinf['moment'] != 0 else '-',
|
| 185 |
+
reinf['bars'] if reinf['moment'] != 0 else '-',
|
| 186 |
+
f"{stirrup['shear']:.2f}" if stirrup['shear'] != 0 else '-',
|
| 187 |
+
stirrup['stirrup_spacing'] if stirrup['shear'] != 0 else '-'
|
| 188 |
+
))
|
| 189 |
+
|
| 190 |
+
tree.pack(expand=True, fill='both', padx=10, pady=10)
|
| 191 |
+
|
| 192 |
+
# Close button
|
| 193 |
+
ttk.Button(summary_window, text="Close",
|
| 194 |
+
command=summary_window.destroy).pack(pady=10)
|
| 195 |
+
|
| 196 |
+
def main():
|
| 197 |
+
root = tk.Tk()
|
| 198 |
+
app = BeamDesignApp(root)
|
| 199 |
+
root.mainloop()
|
| 200 |
+
|
| 201 |
+
if __name__ == "__main__":
|
| 202 |
+
main()
|
continuous_beam.py
ADDED
|
@@ -0,0 +1,1036 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
from typing import List, Dict, Tuple
|
| 4 |
+
import math
|
| 5 |
+
|
| 6 |
+
class ContinuousBeam:
|
| 7 |
+
"""
|
| 8 |
+
Continuous beam analysis and RC design according to ACI code
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.spans = []
|
| 13 |
+
self.loads = []
|
| 14 |
+
self.supports = []
|
| 15 |
+
self.moments = []
|
| 16 |
+
self.shears = []
|
| 17 |
+
self.fc = 28 # Concrete compressive strength (MPa)
|
| 18 |
+
self.fy = 420 # Steel yield strength (MPa)
|
| 19 |
+
self.beam_width = 300 # mm
|
| 20 |
+
self.beam_depth = 500 # mm
|
| 21 |
+
self.cover = 40 # mm
|
| 22 |
+
self.d = self.beam_depth - self.cover # Effective depth
|
| 23 |
+
|
| 24 |
+
def add_span(self, length: float, distributed_load: float = 0, point_loads: List[Tuple[float, float]] = None):
|
| 25 |
+
"""
|
| 26 |
+
Add a span to the continuous beam
|
| 27 |
+
length: span length in meters
|
| 28 |
+
distributed_load: uniformly distributed load in kN/m
|
| 29 |
+
point_loads: list of (position, load) tuples in (m, kN)
|
| 30 |
+
"""
|
| 31 |
+
span = {
|
| 32 |
+
'length': length,
|
| 33 |
+
'distributed_load': distributed_load,
|
| 34 |
+
'point_loads': point_loads or []
|
| 35 |
+
}
|
| 36 |
+
self.spans.append(span)
|
| 37 |
+
|
| 38 |
+
def create_beam_element_stiffness(self, length, E, I):
|
| 39 |
+
"""
|
| 40 |
+
Create local stiffness matrix for a beam element
|
| 41 |
+
[k] = EI/L^3 * [[12, 6L, -12, 6L],
|
| 42 |
+
[6L, 4L^2, -6L, 2L^2],
|
| 43 |
+
[-12, -6L, 12, -6L],
|
| 44 |
+
[6L, 2L^2, -6L, 4L^2]]
|
| 45 |
+
DOFs: [v1, θ1, v2, θ2] where v=deflection, θ=rotation
|
| 46 |
+
"""
|
| 47 |
+
L = length
|
| 48 |
+
EI_L3 = E * I / (L**3)
|
| 49 |
+
|
| 50 |
+
k = EI_L3 * np.array([
|
| 51 |
+
[12, 6*L, -12, 6*L],
|
| 52 |
+
[6*L, 4*L**2, -6*L, 2*L**2],
|
| 53 |
+
[-12, -6*L, 12, -6*L],
|
| 54 |
+
[6*L, 2*L**2, -6*L, 4*L**2]
|
| 55 |
+
])
|
| 56 |
+
|
| 57 |
+
return k
|
| 58 |
+
|
| 59 |
+
def create_distributed_load_vector(self, length, w):
|
| 60 |
+
"""
|
| 61 |
+
Create equivalent nodal force vector for distributed load
|
| 62 |
+
For uniformly distributed load w:
|
| 63 |
+
[F] = wL/12 * [6, L, 6, -L]
|
| 64 |
+
"""
|
| 65 |
+
L = length
|
| 66 |
+
wL_12 = w * L / 12
|
| 67 |
+
|
| 68 |
+
f = wL_12 * np.array([6, L, 6, -L])
|
| 69 |
+
return f
|
| 70 |
+
|
| 71 |
+
def create_point_load_vector(self, length, point_loads):
|
| 72 |
+
"""
|
| 73 |
+
Create equivalent nodal force vector for point loads
|
| 74 |
+
"""
|
| 75 |
+
L = length
|
| 76 |
+
f = np.zeros(4)
|
| 77 |
+
|
| 78 |
+
for pos, load in point_loads:
|
| 79 |
+
a = pos # distance from left node
|
| 80 |
+
b = L - pos # distance from right node
|
| 81 |
+
|
| 82 |
+
# Shape functions at load position
|
| 83 |
+
xi = pos / L # normalized position
|
| 84 |
+
|
| 85 |
+
# Equivalent nodal forces using shape functions
|
| 86 |
+
N1 = 1 - 3*xi**2 + 2*xi**3
|
| 87 |
+
N2 = L * (xi - 2*xi**2 + xi**3)
|
| 88 |
+
N3 = 3*xi**2 - 2*xi**3
|
| 89 |
+
N4 = L * (-xi**2 + xi**3)
|
| 90 |
+
|
| 91 |
+
f += load * np.array([N1, N2, N3, N4])
|
| 92 |
+
|
| 93 |
+
return f
|
| 94 |
+
|
| 95 |
+
def finite_element_analysis(self):
|
| 96 |
+
"""
|
| 97 |
+
Perform finite element analysis of continuous beam
|
| 98 |
+
"""
|
| 99 |
+
if len(self.spans) == 0:
|
| 100 |
+
raise ValueError("No spans defined")
|
| 101 |
+
|
| 102 |
+
# Material properties (assumed values for analysis)
|
| 103 |
+
E = 30000 # MPa (typical for concrete)
|
| 104 |
+
# Calculate moment of inertia from beam dimensions
|
| 105 |
+
b = self.beam_width / 1000 # Convert mm to m
|
| 106 |
+
h = self.beam_depth / 1000 # Convert mm to m
|
| 107 |
+
I = b * h**3 / 12 # m^4
|
| 108 |
+
|
| 109 |
+
# Create mesh - each span divided into elements
|
| 110 |
+
elements_per_span = 10 # Number of elements per span
|
| 111 |
+
total_elements = len(self.spans) * elements_per_span
|
| 112 |
+
total_nodes = total_elements + 1
|
| 113 |
+
|
| 114 |
+
# Node coordinates
|
| 115 |
+
node_coords = []
|
| 116 |
+
current_x = 0
|
| 117 |
+
|
| 118 |
+
for span in self.spans:
|
| 119 |
+
span_length = span['length']
|
| 120 |
+
element_length = span_length / elements_per_span
|
| 121 |
+
|
| 122 |
+
for i in range(elements_per_span):
|
| 123 |
+
node_coords.append(current_x + i * element_length)
|
| 124 |
+
|
| 125 |
+
current_x += span_length
|
| 126 |
+
|
| 127 |
+
# Add final node
|
| 128 |
+
node_coords.append(current_x)
|
| 129 |
+
node_coords = np.array(node_coords)
|
| 130 |
+
|
| 131 |
+
# Global stiffness matrix (2 DOFs per node: deflection and rotation)
|
| 132 |
+
n_dofs = 2 * total_nodes
|
| 133 |
+
K_global = np.zeros((n_dofs, n_dofs))
|
| 134 |
+
F_global = np.zeros(n_dofs)
|
| 135 |
+
|
| 136 |
+
# Assembly process
|
| 137 |
+
for elem_idx in range(total_elements):
|
| 138 |
+
# Element properties
|
| 139 |
+
span_idx = elem_idx // elements_per_span
|
| 140 |
+
local_elem_idx = elem_idx % elements_per_span
|
| 141 |
+
|
| 142 |
+
span = self.spans[span_idx]
|
| 143 |
+
element_length = span['length'] / elements_per_span
|
| 144 |
+
|
| 145 |
+
# Local stiffness matrix
|
| 146 |
+
k_local = self.create_beam_element_stiffness(element_length, E, I)
|
| 147 |
+
|
| 148 |
+
# Global DOF indices for this element
|
| 149 |
+
node1 = elem_idx
|
| 150 |
+
node2 = elem_idx + 1
|
| 151 |
+
|
| 152 |
+
dofs = [2*node1, 2*node1+1, 2*node2, 2*node2+1] # [v1, θ1, v2, θ2]
|
| 153 |
+
|
| 154 |
+
# Assemble into global matrix
|
| 155 |
+
for i in range(4):
|
| 156 |
+
for j in range(4):
|
| 157 |
+
K_global[dofs[i], dofs[j]] += k_local[i, j]
|
| 158 |
+
|
| 159 |
+
# Create load vector for this element
|
| 160 |
+
# Distributed load
|
| 161 |
+
w = span['distributed_load'] * 1000 # Convert kN/m to N/m
|
| 162 |
+
f_dist = self.create_distributed_load_vector(element_length, w)
|
| 163 |
+
|
| 164 |
+
# Point loads (only if they fall within this element)
|
| 165 |
+
point_loads = span.get('point_loads', [])
|
| 166 |
+
f_point = np.zeros(4)
|
| 167 |
+
|
| 168 |
+
span_start = sum(self.spans[j]['length'] for j in range(span_idx))
|
| 169 |
+
elem_start = span_start + local_elem_idx * element_length
|
| 170 |
+
elem_end = elem_start + element_length
|
| 171 |
+
|
| 172 |
+
for pos, load in point_loads:
|
| 173 |
+
global_pos = span_start + pos
|
| 174 |
+
if elem_start <= global_pos <= elem_end:
|
| 175 |
+
local_pos = global_pos - elem_start
|
| 176 |
+
f_point += self.create_point_load_vector(element_length, [(local_pos, load * 1000)])
|
| 177 |
+
|
| 178 |
+
f_total = f_dist + f_point
|
| 179 |
+
|
| 180 |
+
# Assemble into global force vector
|
| 181 |
+
for i in range(4):
|
| 182 |
+
F_global[dofs[i]] += f_total[i]
|
| 183 |
+
|
| 184 |
+
# Apply boundary conditions (pin supports at all support locations)
|
| 185 |
+
# Support locations are at the ends of each span
|
| 186 |
+
support_nodes = [0] # First support
|
| 187 |
+
current_node = 0
|
| 188 |
+
for span in self.spans:
|
| 189 |
+
current_node += elements_per_span
|
| 190 |
+
support_nodes.append(current_node)
|
| 191 |
+
|
| 192 |
+
# Create arrays for free DOFs (removing constrained deflections)
|
| 193 |
+
constrained_dofs = [2 * node for node in support_nodes] # Vertical deflections at supports
|
| 194 |
+
free_dofs = [i for i in range(n_dofs) if i not in constrained_dofs]
|
| 195 |
+
|
| 196 |
+
# Extract free DOF matrices
|
| 197 |
+
K_free = K_global[np.ix_(free_dofs, free_dofs)]
|
| 198 |
+
F_free = F_global[free_dofs]
|
| 199 |
+
|
| 200 |
+
# Solve for displacements
|
| 201 |
+
try:
|
| 202 |
+
U_free = np.linalg.solve(K_free, F_free)
|
| 203 |
+
except np.linalg.LinAlgError:
|
| 204 |
+
# Fallback to least squares if matrix is singular
|
| 205 |
+
U_free = np.linalg.lstsq(K_free, F_free, rcond=None)[0]
|
| 206 |
+
|
| 207 |
+
# Reconstruct full displacement vector
|
| 208 |
+
U_global = np.zeros(n_dofs)
|
| 209 |
+
U_global[free_dofs] = U_free
|
| 210 |
+
|
| 211 |
+
# Store results for post-processing
|
| 212 |
+
self.node_coords = node_coords
|
| 213 |
+
self.displacements = U_global
|
| 214 |
+
self.elements_per_span = elements_per_span
|
| 215 |
+
self.element_properties = {'E': E, 'I': I}
|
| 216 |
+
self.K_global = K_global # Store for reaction calculation
|
| 217 |
+
|
| 218 |
+
return node_coords, U_global
|
| 219 |
+
|
| 220 |
+
def calculate_element_forces(self):
|
| 221 |
+
"""
|
| 222 |
+
Calculate internal forces (moment and shear) for each element
|
| 223 |
+
"""
|
| 224 |
+
if not hasattr(self, 'displacements'):
|
| 225 |
+
self.finite_element_analysis()
|
| 226 |
+
|
| 227 |
+
E = self.element_properties['E']
|
| 228 |
+
I = self.element_properties['I']
|
| 229 |
+
|
| 230 |
+
moments = []
|
| 231 |
+
shears = []
|
| 232 |
+
x_coords = []
|
| 233 |
+
|
| 234 |
+
# Calculate reactions first for proper shear calculation
|
| 235 |
+
reactions = self.calculate_reactions()
|
| 236 |
+
|
| 237 |
+
elem_idx = 0
|
| 238 |
+
for span_idx, span in enumerate(self.spans):
|
| 239 |
+
span_length = span['length']
|
| 240 |
+
element_length = span_length / self.elements_per_span
|
| 241 |
+
|
| 242 |
+
span_start_x = sum(self.spans[j]['length'] for j in range(span_idx))
|
| 243 |
+
|
| 244 |
+
for local_elem in range(self.elements_per_span):
|
| 245 |
+
# Element nodes
|
| 246 |
+
node1 = elem_idx
|
| 247 |
+
node2 = elem_idx + 1
|
| 248 |
+
|
| 249 |
+
# Element displacements
|
| 250 |
+
u1 = self.displacements[2*node1] # deflection at node 1
|
| 251 |
+
theta1 = self.displacements[2*node1+1] # rotation at node 1
|
| 252 |
+
u2 = self.displacements[2*node2] # deflection at node 2
|
| 253 |
+
theta2 = self.displacements[2*node2+1] # rotation at node 2
|
| 254 |
+
|
| 255 |
+
# Calculate forces at multiple points within element
|
| 256 |
+
n_points = 10
|
| 257 |
+
for i in range(n_points):
|
| 258 |
+
xi = i / (n_points - 1) # 0 to 1
|
| 259 |
+
x_local = xi * element_length
|
| 260 |
+
x_global = span_start_x + local_elem * element_length + x_local
|
| 261 |
+
|
| 262 |
+
# Shape function derivatives for moment calculation
|
| 263 |
+
# M = -EI * d²v/dx²
|
| 264 |
+
d2N1_dx2 = (-6 + 12*xi) / element_length**2
|
| 265 |
+
d2N2_dx2 = (-4 + 6*xi) / element_length
|
| 266 |
+
d2N3_dx2 = (6 - 12*xi) / element_length**2
|
| 267 |
+
d2N4_dx2 = (-2 + 6*xi) / element_length
|
| 268 |
+
|
| 269 |
+
curvature = (d2N1_dx2 * u1 + d2N2_dx2 * theta1 +
|
| 270 |
+
d2N3_dx2 * u2 + d2N4_dx2 * theta2)
|
| 271 |
+
moment = -E * I * curvature / 1000 # Convert to kN-m
|
| 272 |
+
|
| 273 |
+
# Calculate shear using equilibrium method (more reliable)
|
| 274 |
+
shear = self.calculate_shear_at_position(x_global, reactions)
|
| 275 |
+
|
| 276 |
+
x_coords.append(x_global)
|
| 277 |
+
moments.append(moment)
|
| 278 |
+
shears.append(shear)
|
| 279 |
+
|
| 280 |
+
elem_idx += 1
|
| 281 |
+
|
| 282 |
+
return np.array(x_coords), np.array(moments), np.array(shears)
|
| 283 |
+
|
| 284 |
+
def calculate_reactions(self):
|
| 285 |
+
"""
|
| 286 |
+
Calculate support reactions from finite element solution
|
| 287 |
+
"""
|
| 288 |
+
if not hasattr(self, 'K_global') or not hasattr(self, 'displacements'):
|
| 289 |
+
self.finite_element_analysis()
|
| 290 |
+
|
| 291 |
+
# Get support node indices
|
| 292 |
+
support_nodes = [0] # First support
|
| 293 |
+
current_node = 0
|
| 294 |
+
for span in self.spans:
|
| 295 |
+
current_node += self.elements_per_span
|
| 296 |
+
support_nodes.append(current_node)
|
| 297 |
+
|
| 298 |
+
# Calculate reactions using R = K*U - F for constrained DOFs
|
| 299 |
+
reactions = []
|
| 300 |
+
|
| 301 |
+
# Build complete force vector including applied loads
|
| 302 |
+
n_dofs = len(self.displacements)
|
| 303 |
+
F_complete = np.zeros(n_dofs)
|
| 304 |
+
|
| 305 |
+
# Assemble applied load vector (same as in FE analysis)
|
| 306 |
+
elem_idx = 0
|
| 307 |
+
for span_idx, span in enumerate(self.spans):
|
| 308 |
+
element_length = span['length'] / self.elements_per_span
|
| 309 |
+
|
| 310 |
+
for local_elem_idx in range(self.elements_per_span):
|
| 311 |
+
# Global DOF indices for this element
|
| 312 |
+
node1 = elem_idx
|
| 313 |
+
node2 = elem_idx + 1
|
| 314 |
+
dofs = [2*node1, 2*node1+1, 2*node2, 2*node2+1]
|
| 315 |
+
|
| 316 |
+
# Create load vector for this element
|
| 317 |
+
w = span['distributed_load'] * 1000 # Convert to N/m
|
| 318 |
+
f_dist = self.create_distributed_load_vector(element_length, w)
|
| 319 |
+
|
| 320 |
+
# Point loads (only if they fall within this element)
|
| 321 |
+
point_loads = span.get('point_loads', [])
|
| 322 |
+
f_point = np.zeros(4)
|
| 323 |
+
|
| 324 |
+
span_start = sum(self.spans[j]['length'] for j in range(span_idx))
|
| 325 |
+
elem_start = span_start + local_elem_idx * element_length
|
| 326 |
+
elem_end = elem_start + element_length
|
| 327 |
+
|
| 328 |
+
for pos, load in point_loads:
|
| 329 |
+
global_pos = span_start + pos
|
| 330 |
+
if elem_start <= global_pos <= elem_end:
|
| 331 |
+
local_pos = global_pos - elem_start
|
| 332 |
+
f_point += self.create_point_load_vector(element_length, [(local_pos, load * 1000)])
|
| 333 |
+
|
| 334 |
+
f_total = f_dist + f_point
|
| 335 |
+
|
| 336 |
+
# Assemble into global force vector
|
| 337 |
+
for i in range(4):
|
| 338 |
+
F_complete[dofs[i]] += f_total[i]
|
| 339 |
+
|
| 340 |
+
elem_idx += 1
|
| 341 |
+
|
| 342 |
+
# Calculate reactions at each support
|
| 343 |
+
for support_node in support_nodes:
|
| 344 |
+
# Vertical DOF for this support
|
| 345 |
+
dof = 2 * support_node
|
| 346 |
+
|
| 347 |
+
# Reaction = K*U - F at constrained DOF
|
| 348 |
+
# Since displacement is zero at support, reaction = -F_applied + K*U_other
|
| 349 |
+
reaction_force = 0
|
| 350 |
+
|
| 351 |
+
# Sum contributions from all DOFs
|
| 352 |
+
for j in range(n_dofs):
|
| 353 |
+
reaction_force += self.K_global[dof, j] * self.displacements[j]
|
| 354 |
+
|
| 355 |
+
# Subtract applied force (if any) at this DOF
|
| 356 |
+
reaction_force -= F_complete[dof]
|
| 357 |
+
|
| 358 |
+
# Convert to kN and store (positive = upward reaction)
|
| 359 |
+
# Note: FE convention may give negative values for upward reactions
|
| 360 |
+
reactions.append(-reaction_force / 1000)
|
| 361 |
+
|
| 362 |
+
# Store for debugging
|
| 363 |
+
self.reactions = reactions
|
| 364 |
+
return reactions
|
| 365 |
+
|
| 366 |
+
def calculate_shear_at_position(self, x_global, reactions):
|
| 367 |
+
"""
|
| 368 |
+
Calculate shear force at any position using equilibrium
|
| 369 |
+
"""
|
| 370 |
+
shear = 0
|
| 371 |
+
current_pos = 0
|
| 372 |
+
|
| 373 |
+
# Add reaction at first support
|
| 374 |
+
if len(reactions) > 0:
|
| 375 |
+
shear += reactions[0]
|
| 376 |
+
|
| 377 |
+
# Subtract loads to the left of current position
|
| 378 |
+
support_idx = 1
|
| 379 |
+
for span_idx, span in enumerate(self.spans):
|
| 380 |
+
span_start = current_pos
|
| 381 |
+
span_end = current_pos + span['length']
|
| 382 |
+
|
| 383 |
+
if x_global <= span_start:
|
| 384 |
+
break
|
| 385 |
+
|
| 386 |
+
# Check if we passed a support
|
| 387 |
+
if x_global > span_end and support_idx < len(reactions):
|
| 388 |
+
shear += reactions[support_idx]
|
| 389 |
+
support_idx += 1
|
| 390 |
+
|
| 391 |
+
# Calculate how much of this span is to the left of current position
|
| 392 |
+
span_length_to_left = min(x_global - span_start, span['length'])
|
| 393 |
+
|
| 394 |
+
if span_length_to_left > 0:
|
| 395 |
+
# Distributed load effect
|
| 396 |
+
w = span['distributed_load']
|
| 397 |
+
shear -= w * span_length_to_left
|
| 398 |
+
|
| 399 |
+
# Point load effects
|
| 400 |
+
point_loads = span.get('point_loads', [])
|
| 401 |
+
for pos, load in point_loads:
|
| 402 |
+
if pos <= span_length_to_left:
|
| 403 |
+
shear -= load
|
| 404 |
+
|
| 405 |
+
current_pos += span['length']
|
| 406 |
+
|
| 407 |
+
return shear
|
| 408 |
+
|
| 409 |
+
def analyze_moments(self):
|
| 410 |
+
"""
|
| 411 |
+
Analyze continuous beam using finite element method
|
| 412 |
+
"""
|
| 413 |
+
num_spans = len(self.spans)
|
| 414 |
+
if num_spans == 0:
|
| 415 |
+
raise ValueError("No spans defined")
|
| 416 |
+
|
| 417 |
+
# Perform finite element analysis
|
| 418 |
+
self.finite_element_analysis()
|
| 419 |
+
|
| 420 |
+
# Calculate detailed forces along the beam
|
| 421 |
+
x_coords, moments_detailed, shears_detailed = self.calculate_element_forces()
|
| 422 |
+
|
| 423 |
+
# Extract critical moments and shears for each span (for compatibility with existing code)
|
| 424 |
+
self.moments = []
|
| 425 |
+
self.shears = []
|
| 426 |
+
|
| 427 |
+
current_pos = 0
|
| 428 |
+
for i, span in enumerate(self.spans):
|
| 429 |
+
span_length = span['length']
|
| 430 |
+
span_start = current_pos
|
| 431 |
+
span_mid = current_pos + span_length / 2
|
| 432 |
+
span_end = current_pos + span_length
|
| 433 |
+
|
| 434 |
+
# Find indices closest to critical points
|
| 435 |
+
start_idx = np.argmin(np.abs(x_coords - span_start))
|
| 436 |
+
mid_idx = np.argmin(np.abs(x_coords - span_mid))
|
| 437 |
+
end_idx = np.argmin(np.abs(x_coords - span_end))
|
| 438 |
+
|
| 439 |
+
# Extract moments and shears at critical points
|
| 440 |
+
M_start = moments_detailed[start_idx]
|
| 441 |
+
M_mid = moments_detailed[mid_idx]
|
| 442 |
+
M_end = moments_detailed[end_idx]
|
| 443 |
+
|
| 444 |
+
V_start = shears_detailed[start_idx]
|
| 445 |
+
V_mid = shears_detailed[mid_idx]
|
| 446 |
+
V_end = shears_detailed[end_idx]
|
| 447 |
+
|
| 448 |
+
# Store for span (maintaining compatibility with existing design methods)
|
| 449 |
+
self.moments.append([M_start, M_mid, M_end])
|
| 450 |
+
self.shears.append([V_start, V_mid, V_end])
|
| 451 |
+
|
| 452 |
+
current_pos += span_length
|
| 453 |
+
|
| 454 |
+
# Store detailed results for plotting
|
| 455 |
+
self.detailed_x = x_coords
|
| 456 |
+
self.detailed_moments = moments_detailed
|
| 457 |
+
self.detailed_shears = shears_detailed
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
def calculate_required_reinforcement(self, moment: float, beam_type: str = "rectangular"):
|
| 461 |
+
"""
|
| 462 |
+
Calculate required area of reinforcement according to ACI code
|
| 463 |
+
moment: Design moment in kN-m
|
| 464 |
+
beam_type: Type of beam section
|
| 465 |
+
"""
|
| 466 |
+
if moment == 0:
|
| 467 |
+
return 0
|
| 468 |
+
|
| 469 |
+
# Convert moment to N-mm
|
| 470 |
+
Mu = abs(moment) * 1e6
|
| 471 |
+
|
| 472 |
+
# Material properties
|
| 473 |
+
fc = self.fc # MPa
|
| 474 |
+
fy = self.fy # MPa
|
| 475 |
+
b = self.beam_width # mm
|
| 476 |
+
d = self.d # mm
|
| 477 |
+
|
| 478 |
+
# Strength reduction factor
|
| 479 |
+
phi = 0.9
|
| 480 |
+
|
| 481 |
+
# Calculate required reinforcement
|
| 482 |
+
# Using simplified rectangular stress block
|
| 483 |
+
beta1 = 0.85 if fc <= 28 else max(0.65, 0.85 - 0.05 * (fc - 28) / 7)
|
| 484 |
+
|
| 485 |
+
# Calculate Rn
|
| 486 |
+
Rn = Mu / (phi * b * d**2)
|
| 487 |
+
|
| 488 |
+
# Calculate reinforcement ratio
|
| 489 |
+
rho = (0.85 * fc / fy) * (1 - math.sqrt(1 - 2 * Rn / (0.85 * fc)))
|
| 490 |
+
|
| 491 |
+
# Minimum reinforcement ratio
|
| 492 |
+
rho_min = max(1.4 / fy, 0.25 * math.sqrt(fc) / fy)
|
| 493 |
+
|
| 494 |
+
# Maximum reinforcement ratio (75% of balanced ratio)
|
| 495 |
+
rho_b = (0.85 * fc * beta1 * 600) / (fy * (600 + fy))
|
| 496 |
+
rho_max = 0.75 * rho_b
|
| 497 |
+
|
| 498 |
+
# Check limits
|
| 499 |
+
rho = max(rho, rho_min)
|
| 500 |
+
if rho > rho_max:
|
| 501 |
+
raise ValueError(f"Required reinforcement ratio {rho:.4f} exceeds maximum {rho_max:.4f}")
|
| 502 |
+
|
| 503 |
+
# Calculate required area
|
| 504 |
+
As_required = rho * b * d
|
| 505 |
+
|
| 506 |
+
return As_required
|
| 507 |
+
|
| 508 |
+
def calculate_shear_reinforcement(self, shear: float):
|
| 509 |
+
"""
|
| 510 |
+
Calculate shear reinforcement (stirrups) according to ACI code
|
| 511 |
+
shear: Design shear force in kN
|
| 512 |
+
"""
|
| 513 |
+
if shear == 0:
|
| 514 |
+
return {"stirrup_spacing": "No stirrups required", "Av": 0}
|
| 515 |
+
|
| 516 |
+
# Convert shear to N
|
| 517 |
+
Vu = abs(shear) * 1000
|
| 518 |
+
|
| 519 |
+
# Material properties
|
| 520 |
+
fc = self.fc # MPa
|
| 521 |
+
fy = self.fy # MPa (for stirrups)
|
| 522 |
+
b = self.beam_width # mm
|
| 523 |
+
d = self.d # mm
|
| 524 |
+
|
| 525 |
+
# Strength reduction factor for shear
|
| 526 |
+
phi_v = 0.75
|
| 527 |
+
|
| 528 |
+
# Concrete shear capacity
|
| 529 |
+
Vc = 0.17 * math.sqrt(fc) * b * d # N
|
| 530 |
+
|
| 531 |
+
# Check if shear reinforcement is required
|
| 532 |
+
if Vu <= phi_v * Vc / 2:
|
| 533 |
+
return {"stirrup_spacing": "No stirrups required", "Av": 0}
|
| 534 |
+
|
| 535 |
+
# Calculate required shear reinforcement
|
| 536 |
+
Vs = Vu / phi_v - Vc # Required steel shear capacity
|
| 537 |
+
|
| 538 |
+
# Maximum shear that can be carried by steel
|
| 539 |
+
Vs_max = 0.66 * math.sqrt(fc) * b * d
|
| 540 |
+
|
| 541 |
+
if Vs > Vs_max:
|
| 542 |
+
raise ValueError("Shear exceeds maximum capacity - increase beam size")
|
| 543 |
+
|
| 544 |
+
# Calculate required stirrup area
|
| 545 |
+
# Assuming #10 stirrups (2 legs, Area = 2 × 71 = 142 mm²)
|
| 546 |
+
Av = 142 # mm² (2-leg #10 stirrups)
|
| 547 |
+
|
| 548 |
+
# Calculate required spacing
|
| 549 |
+
s_required = Av * fy * d / Vs # mm
|
| 550 |
+
|
| 551 |
+
# Maximum spacing limits
|
| 552 |
+
s_max = min(d / 2, 600) # mm
|
| 553 |
+
|
| 554 |
+
# Minimum stirrup requirements
|
| 555 |
+
if Vu > phi_v * Vc:
|
| 556 |
+
Av_min = 0.35 * b * s_required / fy
|
| 557 |
+
s_max_min = min(d / 4, 300) # More restrictive for high shear
|
| 558 |
+
s_required = min(s_required, s_max_min)
|
| 559 |
+
|
| 560 |
+
s_required = min(s_required, s_max)
|
| 561 |
+
s_required = max(s_required, 50) # Minimum practical spacing
|
| 562 |
+
|
| 563 |
+
return {
|
| 564 |
+
"stirrup_spacing": f"{s_required:.0f} mm c/c",
|
| 565 |
+
"Av": Av,
|
| 566 |
+
"Vs": Vs / 1000, # Convert back to kN
|
| 567 |
+
"Vc": Vc / 1000 # Convert back to kN
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
def design_beam(self):
|
| 571 |
+
"""
|
| 572 |
+
Complete beam design including flexural and shear design
|
| 573 |
+
"""
|
| 574 |
+
if not self.moments:
|
| 575 |
+
self.analyze_moments()
|
| 576 |
+
|
| 577 |
+
design_results = []
|
| 578 |
+
|
| 579 |
+
for i, (moments, shears) in enumerate(zip(self.moments, self.shears)):
|
| 580 |
+
span_design = {
|
| 581 |
+
'span': i + 1,
|
| 582 |
+
'length': self.spans[i]['length'],
|
| 583 |
+
'moments': moments,
|
| 584 |
+
'shears': shears,
|
| 585 |
+
'reinforcement': [],
|
| 586 |
+
'stirrups': []
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
# Design for each critical section
|
| 590 |
+
moment_locations = ['Left Support', 'Mid-span', 'Right Support']
|
| 591 |
+
|
| 592 |
+
for j, (moment, shear) in enumerate(zip(moments, shears)):
|
| 593 |
+
# Flexural design
|
| 594 |
+
if moment != 0:
|
| 595 |
+
As_required = self.calculate_required_reinforcement(moment)
|
| 596 |
+
|
| 597 |
+
# Select reinforcement bars
|
| 598 |
+
bar_areas = {10: 79, 12: 113, 16: 201, 20: 314, 25: 491} # mm²
|
| 599 |
+
|
| 600 |
+
# Try different bar sizes
|
| 601 |
+
for bar_size, bar_area in bar_areas.items():
|
| 602 |
+
num_bars = math.ceil(As_required / bar_area)
|
| 603 |
+
if num_bars <= 8: # Practical limit
|
| 604 |
+
As_provided = num_bars * bar_area
|
| 605 |
+
break
|
| 606 |
+
else:
|
| 607 |
+
# Use largest bars if can't fit
|
| 608 |
+
bar_size = 25
|
| 609 |
+
bar_area = 491
|
| 610 |
+
num_bars = math.ceil(As_required / bar_area)
|
| 611 |
+
As_provided = num_bars * bar_area
|
| 612 |
+
|
| 613 |
+
reinforcement = {
|
| 614 |
+
'location': moment_locations[j],
|
| 615 |
+
'moment': moment,
|
| 616 |
+
'As_required': As_required,
|
| 617 |
+
'As_provided': As_provided,
|
| 618 |
+
'bars': f"{num_bars}T{bar_size}",
|
| 619 |
+
'ratio': As_provided / (self.beam_width * self.d) * 100
|
| 620 |
+
}
|
| 621 |
+
else:
|
| 622 |
+
reinforcement = {
|
| 623 |
+
'location': moment_locations[j],
|
| 624 |
+
'moment': 0,
|
| 625 |
+
'As_required': 0,
|
| 626 |
+
'As_provided': 0,
|
| 627 |
+
'bars': "No reinforcement",
|
| 628 |
+
'ratio': 0
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
span_design['reinforcement'].append(reinforcement)
|
| 632 |
+
|
| 633 |
+
# Shear design
|
| 634 |
+
stirrup_design = self.calculate_shear_reinforcement(shear)
|
| 635 |
+
stirrup_design['location'] = moment_locations[j]
|
| 636 |
+
stirrup_design['shear'] = shear
|
| 637 |
+
span_design['stirrups'].append(stirrup_design)
|
| 638 |
+
|
| 639 |
+
design_results.append(span_design)
|
| 640 |
+
|
| 641 |
+
return design_results
|
| 642 |
+
|
| 643 |
+
def generate_report(self, design_results):
|
| 644 |
+
"""
|
| 645 |
+
Generate design report
|
| 646 |
+
"""
|
| 647 |
+
report = []
|
| 648 |
+
report.append("="*60)
|
| 649 |
+
report.append("CONTINUOUS BEAM RC DESIGN REPORT")
|
| 650 |
+
report.append("According to ACI Code")
|
| 651 |
+
report.append("="*60)
|
| 652 |
+
report.append(f"Beam dimensions: {self.beam_width}mm × {self.beam_depth}mm")
|
| 653 |
+
report.append(f"Concrete strength (f'c): {self.fc} MPa")
|
| 654 |
+
report.append(f"Steel strength (fy): {self.fy} MPa")
|
| 655 |
+
report.append(f"Effective depth (d): {self.d} mm")
|
| 656 |
+
report.append("")
|
| 657 |
+
|
| 658 |
+
for span_data in design_results:
|
| 659 |
+
report.append(f"SPAN {span_data['span']} - Length: {span_data['length']} m")
|
| 660 |
+
report.append("-" * 40)
|
| 661 |
+
|
| 662 |
+
# Moments and reinforcement
|
| 663 |
+
report.append("FLEXURAL DESIGN:")
|
| 664 |
+
for reinf in span_data['reinforcement']:
|
| 665 |
+
if reinf['moment'] != 0:
|
| 666 |
+
report.append(f" {reinf['location']}:")
|
| 667 |
+
report.append(f" Moment: {reinf['moment']:.2f} kN-m")
|
| 668 |
+
report.append(f" As required: {reinf['As_required']:.0f} mm²")
|
| 669 |
+
report.append(f" As provided: {reinf['As_provided']:.0f} mm²")
|
| 670 |
+
report.append(f" Reinforcement: {reinf['bars']}")
|
| 671 |
+
report.append(f" Reinforcement ratio: {reinf['ratio']:.2f}%")
|
| 672 |
+
report.append("")
|
| 673 |
+
|
| 674 |
+
# Shear and stirrups
|
| 675 |
+
report.append("SHEAR DESIGN:")
|
| 676 |
+
for stirrup in span_data['stirrups']:
|
| 677 |
+
if stirrup['shear'] != 0:
|
| 678 |
+
report.append(f" {stirrup['location']}:")
|
| 679 |
+
report.append(f" Shear: {stirrup['shear']:.2f} kN")
|
| 680 |
+
if 'Vs' in stirrup:
|
| 681 |
+
report.append(f" Vc: {stirrup['Vc']:.2f} kN")
|
| 682 |
+
report.append(f" Vs: {stirrup['Vs']:.2f} kN")
|
| 683 |
+
report.append(f" Stirrup spacing: {stirrup['stirrup_spacing']}")
|
| 684 |
+
report.append("")
|
| 685 |
+
|
| 686 |
+
report.append("")
|
| 687 |
+
|
| 688 |
+
return "\n".join(report)
|
| 689 |
+
|
| 690 |
+
def plot_bmd_sfd(self, design_results=None):
|
| 691 |
+
"""
|
| 692 |
+
Generate BMD and SFD plots
|
| 693 |
+
"""
|
| 694 |
+
if design_results is None:
|
| 695 |
+
design_results = self.design_beam()
|
| 696 |
+
|
| 697 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
|
| 698 |
+
|
| 699 |
+
# Calculate total beam length and positions
|
| 700 |
+
total_length = 0
|
| 701 |
+
span_positions = [0]
|
| 702 |
+
|
| 703 |
+
for span in self.spans:
|
| 704 |
+
total_length += span['length']
|
| 705 |
+
span_positions.append(total_length)
|
| 706 |
+
|
| 707 |
+
# Use detailed FE results if available, otherwise fall back to approximate method
|
| 708 |
+
if hasattr(self, 'detailed_x') and hasattr(self, 'detailed_moments'):
|
| 709 |
+
# Use finite element results
|
| 710 |
+
x_coords = self.detailed_x
|
| 711 |
+
moments_detailed = self.detailed_moments
|
| 712 |
+
shears_detailed = self.detailed_shears
|
| 713 |
+
else:
|
| 714 |
+
# Fallback to approximate method for backwards compatibility
|
| 715 |
+
x_coords = []
|
| 716 |
+
moments_detailed = []
|
| 717 |
+
shears_detailed = []
|
| 718 |
+
|
| 719 |
+
for i, span_data in enumerate(design_results):
|
| 720 |
+
span_length = span_data['length']
|
| 721 |
+
start_pos = span_positions[i]
|
| 722 |
+
|
| 723 |
+
# Create x coordinates for this span
|
| 724 |
+
x_span = np.linspace(start_pos, start_pos + span_length, 100)
|
| 725 |
+
|
| 726 |
+
# Get moments and shears for this span
|
| 727 |
+
moments = span_data['moments'] # [left, mid, right]
|
| 728 |
+
shears = span_data['shears']
|
| 729 |
+
|
| 730 |
+
# Simple interpolation between critical points
|
| 731 |
+
moment_curve = np.interp(x_span,
|
| 732 |
+
[start_pos, start_pos + span_length/2, start_pos + span_length],
|
| 733 |
+
moments)
|
| 734 |
+
shear_curve = np.interp(x_span,
|
| 735 |
+
[start_pos, start_pos + span_length/2, start_pos + span_length],
|
| 736 |
+
shears)
|
| 737 |
+
|
| 738 |
+
x_coords.extend(x_span)
|
| 739 |
+
moments_detailed.extend(moment_curve)
|
| 740 |
+
shears_detailed.extend(shear_curve)
|
| 741 |
+
|
| 742 |
+
# Plot BMD
|
| 743 |
+
ax1.plot(x_coords, moments_detailed, 'b-', linewidth=2, label='Bending Moment')
|
| 744 |
+
ax1.fill_between(x_coords, moments_detailed, alpha=0.3, color='blue')
|
| 745 |
+
ax1.axhline(y=0, color='k', linestyle='-', alpha=0.3)
|
| 746 |
+
ax1.set_ylabel('Bending Moment (kN-m)', fontsize=12)
|
| 747 |
+
ax1.set_title('Bending Moment Diagram (BMD)', fontsize=14, fontweight='bold')
|
| 748 |
+
ax1.grid(True, alpha=0.3)
|
| 749 |
+
ax1.legend()
|
| 750 |
+
|
| 751 |
+
# Add support symbols
|
| 752 |
+
for pos in span_positions:
|
| 753 |
+
ax1.axvline(x=pos, color='red', linestyle='--', alpha=0.7)
|
| 754 |
+
ax1.plot(pos, 0, 'rs', markersize=8, label='Support' if pos == span_positions[0] else "")
|
| 755 |
+
|
| 756 |
+
# Plot SFD
|
| 757 |
+
ax2.plot(x_coords, shears_detailed, 'r-', linewidth=2, label='Shear Force')
|
| 758 |
+
ax2.fill_between(x_coords, shears_detailed, alpha=0.3, color='red')
|
| 759 |
+
ax2.axhline(y=0, color='k', linestyle='-', alpha=0.3)
|
| 760 |
+
ax2.set_ylabel('Shear Force (kN)', fontsize=12)
|
| 761 |
+
ax2.set_xlabel('Distance along beam (m)', fontsize=12)
|
| 762 |
+
ax2.set_title('Shear Force Diagram (SFD)', fontsize=14, fontweight='bold')
|
| 763 |
+
ax2.grid(True, alpha=0.3)
|
| 764 |
+
ax2.legend()
|
| 765 |
+
|
| 766 |
+
# Add support symbols
|
| 767 |
+
for pos in span_positions:
|
| 768 |
+
ax2.axvline(x=pos, color='red', linestyle='--', alpha=0.7)
|
| 769 |
+
ax2.plot(pos, 0, 'rs', markersize=8, label='Support' if pos == span_positions[0] else "")
|
| 770 |
+
|
| 771 |
+
plt.tight_layout()
|
| 772 |
+
return fig
|
| 773 |
+
|
| 774 |
+
def plot_reinforcement_layout(self, design_results=None):
|
| 775 |
+
"""
|
| 776 |
+
Generate reinforcement layout diagram
|
| 777 |
+
"""
|
| 778 |
+
if design_results is None:
|
| 779 |
+
design_results = self.design_beam()
|
| 780 |
+
|
| 781 |
+
fig, ax = plt.subplots(1, 1, figsize=(14, 8))
|
| 782 |
+
|
| 783 |
+
# Calculate positions
|
| 784 |
+
total_length = sum(span['length'] for span in self.spans)
|
| 785 |
+
span_positions = [0]
|
| 786 |
+
current_pos = 0
|
| 787 |
+
|
| 788 |
+
for span in self.spans:
|
| 789 |
+
current_pos += span['length']
|
| 790 |
+
span_positions.append(current_pos)
|
| 791 |
+
|
| 792 |
+
# Draw beam outline
|
| 793 |
+
beam_height = 0.5 # Normalized height for drawing
|
| 794 |
+
ax.add_patch(plt.Rectangle((0, -beam_height/2), total_length, beam_height,
|
| 795 |
+
fill=False, edgecolor='black', linewidth=2))
|
| 796 |
+
|
| 797 |
+
# Add beam dimensions text
|
| 798 |
+
ax.text(total_length/2, beam_height/2 + 0.1,
|
| 799 |
+
f'{self.beam_width}mm × {self.beam_depth}mm',
|
| 800 |
+
ha='center', va='bottom', fontsize=10, fontweight='bold')
|
| 801 |
+
|
| 802 |
+
# Draw reinforcement for each span
|
| 803 |
+
colors = ['blue', 'green', 'orange', 'purple', 'brown']
|
| 804 |
+
|
| 805 |
+
for i, span_data in enumerate(design_results):
|
| 806 |
+
span_start = span_positions[i]
|
| 807 |
+
span_end = span_positions[i + 1]
|
| 808 |
+
span_center = (span_start + span_end) / 2
|
| 809 |
+
color = colors[i % len(colors)]
|
| 810 |
+
|
| 811 |
+
# Process reinforcement
|
| 812 |
+
for j, reinf in enumerate(span_data['reinforcement']):
|
| 813 |
+
if reinf['As_provided'] > 0:
|
| 814 |
+
location = reinf['location']
|
| 815 |
+
bars = reinf['bars']
|
| 816 |
+
|
| 817 |
+
if location == 'Left Support':
|
| 818 |
+
x_pos = span_start
|
| 819 |
+
y_pos = beam_height/3 # Top reinforcement
|
| 820 |
+
marker = '^'
|
| 821 |
+
label_pos = 'top'
|
| 822 |
+
elif location == 'Mid-span':
|
| 823 |
+
x_pos = span_center
|
| 824 |
+
y_pos = -beam_height/3 # Bottom reinforcement
|
| 825 |
+
marker = 'v'
|
| 826 |
+
label_pos = 'bottom'
|
| 827 |
+
else: # Right Support
|
| 828 |
+
x_pos = span_end
|
| 829 |
+
y_pos = beam_height/3 # Top reinforcement
|
| 830 |
+
marker = '^'
|
| 831 |
+
label_pos = 'top'
|
| 832 |
+
|
| 833 |
+
# Draw reinforcement symbol
|
| 834 |
+
ax.scatter(x_pos, y_pos, s=100, c=color, marker=marker,
|
| 835 |
+
edgecolor='black', linewidth=1, zorder=5)
|
| 836 |
+
|
| 837 |
+
# Add reinforcement label
|
| 838 |
+
if label_pos == 'top':
|
| 839 |
+
ax.text(x_pos, y_pos + 0.15, bars, ha='center', va='bottom',
|
| 840 |
+
fontsize=9, fontweight='bold', rotation=0)
|
| 841 |
+
else:
|
| 842 |
+
ax.text(x_pos, y_pos - 0.15, bars, ha='center', va='top',
|
| 843 |
+
fontsize=9, fontweight='bold', rotation=0)
|
| 844 |
+
|
| 845 |
+
# Draw supports
|
| 846 |
+
for i, pos in enumerate(span_positions):
|
| 847 |
+
# Support triangle
|
| 848 |
+
triangle_height = 0.2
|
| 849 |
+
triangle_width = 0.1
|
| 850 |
+
|
| 851 |
+
triangle = plt.Polygon([
|
| 852 |
+
[pos - triangle_width/2, -beam_height/2],
|
| 853 |
+
[pos + triangle_width/2, -beam_height/2],
|
| 854 |
+
[pos, -beam_height/2 - triangle_height]
|
| 855 |
+
], fill=True, facecolor='red', edgecolor='black')
|
| 856 |
+
|
| 857 |
+
ax.add_patch(triangle)
|
| 858 |
+
|
| 859 |
+
# Support label
|
| 860 |
+
ax.text(pos, -beam_height/2 - triangle_height - 0.1,
|
| 861 |
+
f'Support {i+1}', ha='center', va='top', fontsize=8)
|
| 862 |
+
|
| 863 |
+
# Add span labels and loads
|
| 864 |
+
for i, span in enumerate(self.spans):
|
| 865 |
+
span_start = span_positions[i]
|
| 866 |
+
span_end = span_positions[i + 1]
|
| 867 |
+
span_center = (span_start + span_end) / 2
|
| 868 |
+
|
| 869 |
+
# Create span label text
|
| 870 |
+
label_text = f'Span {i+1}\nL = {span["length"]}m\nw = {span["distributed_load"]}kN/m'
|
| 871 |
+
|
| 872 |
+
# Add point loads to label if any
|
| 873 |
+
if span.get('point_loads'):
|
| 874 |
+
point_load_text = '\nPoint Loads:'
|
| 875 |
+
for pos, load in span['point_loads']:
|
| 876 |
+
point_load_text += f'\n{load}kN @ {pos}m'
|
| 877 |
+
label_text += point_load_text
|
| 878 |
+
|
| 879 |
+
# Span label
|
| 880 |
+
ax.text(span_center, -beam_height/2 - 0.4, label_text,
|
| 881 |
+
ha='center', va='top', fontsize=9,
|
| 882 |
+
bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.7))
|
| 883 |
+
|
| 884 |
+
# Distributed load arrows
|
| 885 |
+
if span["distributed_load"] > 0:
|
| 886 |
+
num_arrows = 5
|
| 887 |
+
for j in range(num_arrows):
|
| 888 |
+
x_arrow = span_start + (span_end - span_start) * j / (num_arrows - 1)
|
| 889 |
+
ax.arrow(x_arrow, beam_height/2 + 0.3, 0, -0.2,
|
| 890 |
+
head_width=0.05, head_length=0.05, fc='red', ec='red')
|
| 891 |
+
|
| 892 |
+
# Point load arrows
|
| 893 |
+
if span.get('point_loads'):
|
| 894 |
+
for pos, load in span['point_loads']:
|
| 895 |
+
x_point = span_start + pos
|
| 896 |
+
# Larger arrow for point loads
|
| 897 |
+
ax.arrow(x_point, beam_height/2 + 0.5, 0, -0.4,
|
| 898 |
+
head_width=0.08, head_length=0.08, fc='blue', ec='blue', linewidth=2)
|
| 899 |
+
# Point load label
|
| 900 |
+
ax.text(x_point, beam_height/2 + 0.6, f'{load}kN',
|
| 901 |
+
ha='center', va='bottom', fontsize=8, fontweight='bold', color='blue')
|
| 902 |
+
|
| 903 |
+
# Formatting
|
| 904 |
+
ax.set_xlim(-0.5, total_length + 0.5)
|
| 905 |
+
ax.set_ylim(-1.2, 1.0)
|
| 906 |
+
ax.set_xlabel('Distance along beam (m)', fontsize=12)
|
| 907 |
+
ax.set_title('Reinforcement Layout', fontsize=14, fontweight='bold')
|
| 908 |
+
ax.grid(True, alpha=0.3)
|
| 909 |
+
ax.set_aspect('equal')
|
| 910 |
+
|
| 911 |
+
# Legend
|
| 912 |
+
legend_elements = [
|
| 913 |
+
plt.scatter([], [], s=100, c='blue', marker='^', edgecolor='black',
|
| 914 |
+
label='Top Reinforcement (Negative Moment)'),
|
| 915 |
+
plt.scatter([], [], s=100, c='blue', marker='v', edgecolor='black',
|
| 916 |
+
label='Bottom Reinforcement (Positive Moment)')
|
| 917 |
+
]
|
| 918 |
+
ax.legend(handles=legend_elements, loc='upper right')
|
| 919 |
+
|
| 920 |
+
plt.tight_layout()
|
| 921 |
+
return fig
|
| 922 |
+
|
| 923 |
+
def plot_stirrup_layout(self, design_results=None):
|
| 924 |
+
"""
|
| 925 |
+
Generate shear stirrup layout diagram
|
| 926 |
+
"""
|
| 927 |
+
if design_results is None:
|
| 928 |
+
design_results = self.design_beam()
|
| 929 |
+
|
| 930 |
+
fig, ax = plt.subplots(1, 1, figsize=(14, 6))
|
| 931 |
+
|
| 932 |
+
# Calculate positions
|
| 933 |
+
total_length = sum(span['length'] for span in self.spans)
|
| 934 |
+
span_positions = [0]
|
| 935 |
+
current_pos = 0
|
| 936 |
+
|
| 937 |
+
for span in self.spans:
|
| 938 |
+
current_pos += span['length']
|
| 939 |
+
span_positions.append(current_pos)
|
| 940 |
+
|
| 941 |
+
# Draw beam outline (side view)
|
| 942 |
+
beam_height = 0.5
|
| 943 |
+
ax.add_patch(plt.Rectangle((0, 0), total_length, beam_height,
|
| 944 |
+
fill=False, edgecolor='black', linewidth=2))
|
| 945 |
+
|
| 946 |
+
# Draw stirrups for each span
|
| 947 |
+
for i, span_data in enumerate(design_results):
|
| 948 |
+
span_start = span_positions[i]
|
| 949 |
+
span_end = span_positions[i + 1]
|
| 950 |
+
span_length = span_end - span_start
|
| 951 |
+
|
| 952 |
+
# Get stirrup information
|
| 953 |
+
stirrup_info = []
|
| 954 |
+
for stirrup in span_data['stirrups']:
|
| 955 |
+
if 'No stirrups' not in stirrup['stirrup_spacing']:
|
| 956 |
+
spacing_str = stirrup['stirrup_spacing'].replace(' mm c/c', '')
|
| 957 |
+
try:
|
| 958 |
+
spacing = float(spacing_str) / 1000 # Convert mm to m
|
| 959 |
+
stirrup_info.append({
|
| 960 |
+
'location': stirrup['location'],
|
| 961 |
+
'spacing': spacing,
|
| 962 |
+
'shear': stirrup['shear']
|
| 963 |
+
})
|
| 964 |
+
except:
|
| 965 |
+
pass
|
| 966 |
+
|
| 967 |
+
if stirrup_info:
|
| 968 |
+
# Determine stirrup spacing pattern
|
| 969 |
+
# For simplicity, use average spacing
|
| 970 |
+
avg_spacing = np.mean([s['spacing'] for s in stirrup_info])
|
| 971 |
+
|
| 972 |
+
# Draw stirrups
|
| 973 |
+
num_stirrups = int(span_length / avg_spacing) + 1
|
| 974 |
+
for j in range(num_stirrups):
|
| 975 |
+
x_pos = span_start + j * avg_spacing
|
| 976 |
+
if x_pos <= span_end:
|
| 977 |
+
# Draw stirrup as vertical line
|
| 978 |
+
ax.plot([x_pos, x_pos], [0, beam_height], 'g-', linewidth=2, alpha=0.7)
|
| 979 |
+
|
| 980 |
+
# Add stirrup symbol (small rectangle)
|
| 981 |
+
stirrup_width = avg_spacing * 0.1
|
| 982 |
+
ax.add_patch(plt.Rectangle((x_pos - stirrup_width/2, beam_height*0.1),
|
| 983 |
+
stirrup_width, beam_height*0.8,
|
| 984 |
+
fill=False, edgecolor='green', linewidth=1))
|
| 985 |
+
|
| 986 |
+
# Add spacing annotation
|
| 987 |
+
mid_span = (span_start + span_end) / 2
|
| 988 |
+
ax.annotate(f'Stirrups @ {avg_spacing*1000:.0f}mm c/c',
|
| 989 |
+
xy=(mid_span, beam_height + 0.1),
|
| 990 |
+
ha='center', va='bottom', fontsize=9,
|
| 991 |
+
bbox=dict(boxstyle="round,pad=0.2", facecolor="lightgreen", alpha=0.7))
|
| 992 |
+
|
| 993 |
+
# Draw supports
|
| 994 |
+
for pos in span_positions:
|
| 995 |
+
# Support line
|
| 996 |
+
ax.plot([pos, pos], [-0.1, beam_height + 0.05], 'r--', linewidth=2, alpha=0.7)
|
| 997 |
+
|
| 998 |
+
# Support symbol
|
| 999 |
+
triangle = plt.Polygon([
|
| 1000 |
+
[pos - 0.05, -0.1],
|
| 1001 |
+
[pos + 0.05, -0.1],
|
| 1002 |
+
[pos, -0.2]
|
| 1003 |
+
], fill=True, facecolor='red', edgecolor='black')
|
| 1004 |
+
ax.add_patch(triangle)
|
| 1005 |
+
|
| 1006 |
+
# Add dimension lines and labels
|
| 1007 |
+
for i, span in enumerate(self.spans):
|
| 1008 |
+
span_start = span_positions[i]
|
| 1009 |
+
span_end = span_positions[i + 1]
|
| 1010 |
+
span_center = (span_start + span_end) / 2
|
| 1011 |
+
|
| 1012 |
+
# Dimension line
|
| 1013 |
+
ax.annotate('', xy=(span_start, -0.3), xytext=(span_end, -0.3),
|
| 1014 |
+
arrowprops=dict(arrowstyle='<->', color='black', lw=1))
|
| 1015 |
+
|
| 1016 |
+
# Span length label
|
| 1017 |
+
ax.text(span_center, -0.35, f'{span["length"]}m',
|
| 1018 |
+
ha='center', va='top', fontsize=10)
|
| 1019 |
+
|
| 1020 |
+
# Formatting
|
| 1021 |
+
ax.set_xlim(-0.2, total_length + 0.2)
|
| 1022 |
+
ax.set_ylim(-0.5, beam_height + 0.4)
|
| 1023 |
+
ax.set_xlabel('Distance along beam (m)', fontsize=12)
|
| 1024 |
+
ax.set_ylabel('Beam Height', fontsize=12)
|
| 1025 |
+
ax.set_title('Shear Stirrup Layout', fontsize=14, fontweight='bold')
|
| 1026 |
+
ax.grid(True, alpha=0.3)
|
| 1027 |
+
|
| 1028 |
+
# Add legend
|
| 1029 |
+
legend_elements = [
|
| 1030 |
+
plt.Line2D([0], [0], color='green', linewidth=2, alpha=0.7, label='Stirrups'),
|
| 1031 |
+
plt.Line2D([0], [0], color='red', linestyle='--', linewidth=2, alpha=0.7, label='Supports')
|
| 1032 |
+
]
|
| 1033 |
+
ax.legend(handles=legend_elements, loc='upper right')
|
| 1034 |
+
|
| 1035 |
+
plt.tight_layout()
|
| 1036 |
+
return fig
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy>=1.21.0
|
| 2 |
+
matplotlib>=3.5.0
|
| 3 |
+
gradio>=4.0.0
|
| 4 |
+
pandas>=1.3.0
|