Upload 16 files
Browse files- .gitattributes +1 -0
- .gitignore +57 -0
- Dump/example_4_1.tex +212 -0
- Dump/note.txt +0 -0
- Dump/solution_breakdown.md +107 -0
- Dump/solution_breakdown.pdf +3 -0
- LICENSE +21 -0
- MATLAB/app.m +651 -0
- README.md +175 -0
- STRUCTURE.md +22 -0
- TECHSTACK.md +15 -0
- app.py +542 -0
- beam_calc.db +0 -0
- calculator.py +234 -0
- db_manager.py +45 -0
- requirements.txt +3 -0
- test_calculator.py +163 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
Dump/solution_breakdown.pdf filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual Environment
|
| 24 |
+
.venv/
|
| 25 |
+
venv/
|
| 26 |
+
ENV/
|
| 27 |
+
env/
|
| 28 |
+
|
| 29 |
+
# IDE
|
| 30 |
+
.vscode/
|
| 31 |
+
.idea/
|
| 32 |
+
*.swp
|
| 33 |
+
*.swo
|
| 34 |
+
*~
|
| 35 |
+
|
| 36 |
+
# Database
|
| 37 |
+
*.db
|
| 38 |
+
*.sqlite3
|
| 39 |
+
|
| 40 |
+
# OS
|
| 41 |
+
.DS_Store
|
| 42 |
+
Thumbs.db
|
| 43 |
+
Desktop.ini
|
| 44 |
+
|
| 45 |
+
# Testing
|
| 46 |
+
.pytest_cache/
|
| 47 |
+
.coverage
|
| 48 |
+
htmlcov/
|
| 49 |
+
.tox/
|
| 50 |
+
.nox/
|
| 51 |
+
|
| 52 |
+
# Distribution
|
| 53 |
+
*.manifest
|
| 54 |
+
*.spec
|
| 55 |
+
|
| 56 |
+
# Logs
|
| 57 |
+
*.log
|
Dump/example_4_1.tex
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
\documentclass[12pt, letterpaper]{article}
|
| 2 |
+
\usepackage[utf8]{inputenc}
|
| 3 |
+
\usepackage{geometry}
|
| 4 |
+
\usepackage{amsmath}
|
| 5 |
+
\usepackage{amssymb}
|
| 6 |
+
\usepackage{graphicx}
|
| 7 |
+
\usepackage{tikz}
|
| 8 |
+
\usetikzlibrary{positioning,arrows.meta,calc}
|
| 9 |
+
\usepackage{siunitx}
|
| 10 |
+
\usepackage{fancyhdr}
|
| 11 |
+
\usepackage{parskip}
|
| 12 |
+
|
| 13 |
+
% Geometry settings
|
| 14 |
+
\geometry{margin=1in}
|
| 15 |
+
|
| 16 |
+
% Header and Footer
|
| 17 |
+
\pagestyle{fancy}
|
| 18 |
+
\fancyhf{}
|
| 19 |
+
\rhead{Example 4-1}
|
| 20 |
+
\lhead{Nominal Moment Strength Calculation}
|
| 21 |
+
\cfoot{\thepage}
|
| 22 |
+
|
| 23 |
+
\title{\textbf{Example 4-1: Nominal Moment Strength Calculation for a Singly Reinforced Concrete Beam}}
|
| 24 |
+
\author{}
|
| 25 |
+
\date{}
|
| 26 |
+
|
| 27 |
+
\begin{document}
|
| 28 |
+
|
| 29 |
+
\maketitle
|
| 30 |
+
|
| 31 |
+
\section*{Introduction}
|
| 32 |
+
This document provides a detailed, step-by-step breakdown of the example problem (referenced as Fig. 4-19a). The goal is to calculate the nominal moment strength $M_n$ for the beam and confirm that the area of tension steel exceeds the required minimum steel area as per Equation (4-11) from the relevant design code (specifically ACI 318). All calculations are performed without skipping any micro-steps, including unit conversions and intermediate arithmetic operations.
|
| 33 |
+
|
| 34 |
+
\textbf{Problem statement (paraphrased from the provided excerpt).} The task is to compute $M_n$ for the singly reinforced beam and verify that the provided tension reinforcement area exceeds the minimum required by code.
|
| 35 |
+
|
| 36 |
+
\textbf{Source excerpt (short quote).} ``Calculate $M_n$ and confirm that the area of tension steel exceeds the required minimum steel area.''
|
| 37 |
+
|
| 38 |
+
\begin{figure}[h]
|
| 39 |
+
\centering
|
| 40 |
+
\begin{tikzpicture}[
|
| 41 |
+
font=\small,
|
| 42 |
+
dim/.style={-Latex, thin},
|
| 43 |
+
outline/.style={draw, line width=0.8pt},
|
| 44 |
+
bar/.style={fill=black, draw=black},
|
| 45 |
+
note/.style={font=\scriptsize}
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
% Parameters (in cm for drawing convenience)
|
| 49 |
+
% Use a simple scale: 12 in -> 6 cm, 20 in -> 10 cm
|
| 50 |
+
\def\W{6}
|
| 51 |
+
\def\H{10}
|
| 52 |
+
\def\cover{1.25} % corresponds to 2.5 in in this scale
|
| 53 |
+
|
| 54 |
+
% Concrete section
|
| 55 |
+
\draw[outline] (0,0) rectangle (\W,\H);
|
| 56 |
+
|
| 57 |
+
% Bars (4 No. 8) near the bottom
|
| 58 |
+
\foreach \x in {1.2,2.4,3.6,4.8} {
|
| 59 |
+
\draw[bar] (\x,\cover) circle (0.12);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
% Dimension: height 20 in
|
| 63 |
+
\draw[dim] (-0.8,0) -- (-0.8,\H);
|
| 64 |
+
\draw[outline] (-0.9,0) -- (-0.7,0);
|
| 65 |
+
\draw[outline] (-0.9,\H) -- (-0.7,\H);
|
| 66 |
+
\node[rotate=90] at (-1.2,\H/2) {20 in.};
|
| 67 |
+
|
| 68 |
+
% Dimension: width 12 in
|
| 69 |
+
\draw[dim] (0,-0.8) -- (\W,-0.8);
|
| 70 |
+
\draw[outline] (0,-0.9) -- (0,-0.7);
|
| 71 |
+
\draw[outline] (\W,-0.9) -- (\W,-0.7);
|
| 72 |
+
\node at (\W/2,-1.2) {12 in.};
|
| 73 |
+
|
| 74 |
+
% Dimension: 2.5 in to steel layer (approx)
|
| 75 |
+
\draw[dim] (\W+0.8,0) -- (\W+0.8,\cover);
|
| 76 |
+
\draw[outline] (\W+0.7,0) -- (\W+0.9,0);
|
| 77 |
+
\draw[outline] (\W+0.7,\cover) -- (\W+0.9,\cover);
|
| 78 |
+
\node[rotate=90] at (\W+1.2,\cover/2) {2.5 in.};
|
| 79 |
+
|
| 80 |
+
% Label bars
|
| 81 |
+
\node[note] at (\W/2,\cover+0.6) {4 No. 8 bars};
|
| 82 |
+
|
| 83 |
+
\end{tikzpicture}
|
| 84 |
+
\caption{Beam cross-section used in Example 4-1 (redrawn from the provided image).}
|
| 85 |
+
\label{fig:beam-4-19a-redraw}
|
| 86 |
+
\end{figure}
|
| 87 |
+
|
| 88 |
+
The beam is a rectangular section made of concrete with compressive strength $f'_c = 4000$ psi, reinforced with four No. 8 bars in tension having yield strength $f_y = 60$ ksi. The beam dimensions are width $b = 12$ in. and total height $h = 20$ in. The effective depth $d$ is approximated as $h - 2.5$ in. to account for concrete cover, stirrup diameter, and half the longitudinal bar diameter.
|
| 89 |
+
|
| 90 |
+
\section*{Given Data}
|
| 91 |
+
\begin{itemize}
|
| 92 |
+
\item \textbf{Concrete compressive strength:} $f'_c = 4000$ psi
|
| 93 |
+
\item \textbf{Steel yield strength:} $f_y = 60$ ksi = $60,000$ psi
|
| 94 |
+
\item \textbf{Beam width:} $b = 12$ in.
|
| 95 |
+
\item \textbf{Beam total height:} $h = 20$ in.
|
| 96 |
+
\item \textbf{Effective depth (assumed):} $d = h - 2.5 = 20 - 2.5 = 17.5$ in.
|
| 97 |
+
\item \textbf{Tension reinforcement:} 4 No. 8 bars
|
| 98 |
+
\item \textbf{Diameter of No. 8 bar:} 1.0 in. (standard ASTM A615/A706 bar size)
|
| 99 |
+
\item \textbf{Area of one No. 8 bar (tabulated):} $A_{bar} = 0.79\ \text{in}^2$.
|
| 100 |
+
\item \textbf{Total tension steel area:}
|
| 101 |
+
\[
|
| 102 |
+
A_s = 4 \times 0.79 = 3.16 \text{ in}^2
|
| 103 |
+
\]
|
| 104 |
+
\item \textbf{Compression reinforcement:} Ignored (not designed for compression resistance).
|
| 105 |
+
\item \textbf{Rectangular stress block factor:} $\beta_1 = 0.85$ (for $f'_c = 4000$ psi).
|
| 106 |
+
\item \textbf{Minimum steel area rule:} $A_{s,min} = \max\left( \frac{3\sqrt{f'_c}}{f_y} bd, \frac{200}{f_y} bd \right)$
|
| 107 |
+
\end{itemize}
|
| 108 |
+
|
| 109 |
+
\textbf{Note on Effective Depth $d$:} The approximation of 2.5 in. accounts for:
|
| 110 |
+
\begin{itemize}
|
| 111 |
+
\item Clear concrete cover: 1.5 in.
|
| 112 |
+
\item Stirrup diameter: $\approx 0.5$ in.
|
| 113 |
+
\item Half the longitudinal bar diameter: 0.5 in.
|
| 114 |
+
\end{itemize}
|
| 115 |
+
Total: $1.5 + 0.5 + 0.5 = 2.5$ in.
|
| 116 |
+
|
| 117 |
+
\section*{Step 1: Confirm Tension Steel Area Exceeds Minimum Required}
|
| 118 |
+
|
| 119 |
+
\subsection*{Step 1.1: Calculate $\rho_{min}$ Using Equation (4-11)}
|
| 120 |
+
The minimum reinforcement ratio $\rho_{min}$ is the maximum of two values:
|
| 121 |
+
\begin{enumerate}
|
| 122 |
+
\item $\frac{3 \sqrt{f'_c}}{f_y}$
|
| 123 |
+
\item $\frac{200}{f_y}$ (in psi units)
|
| 124 |
+
\end{enumerate}
|
| 125 |
+
|
| 126 |
+
\subsubsection*{Micro-Calculation for First Term: $\frac{3 \sqrt{f'_c}}{f_y}$}
|
| 127 |
+
\begin{itemize}
|
| 128 |
+
\item Calculate $\sqrt{f'_c} = \sqrt{4000} \approx 63.2456$.
|
| 129 |
+
\item $3 \times 63.2456 = 189.7368$.
|
| 130 |
+
\item $\frac{189.7368}{60,000} = 0.00316228$.
|
| 131 |
+
\end{itemize}
|
| 132 |
+
|
| 133 |
+
\subsubsection*{Micro-Calculation for Second Term: $\frac{200}{f_y}$}
|
| 134 |
+
\[
|
| 135 |
+
\frac{200}{60,000} = 0.00333333
|
| 136 |
+
\]
|
| 137 |
+
|
| 138 |
+
\subsubsection*{Select $\rho_{min}$}
|
| 139 |
+
\[
|
| 140 |
+
\rho_{min} = \max(0.00316228, 0.00333333) = 0.00333333
|
| 141 |
+
\]
|
| 142 |
+
|
| 143 |
+
\subsection*{Step 1.2: Calculate Minimum Steel Area $A_{s,min}$}
|
| 144 |
+
\[
|
| 145 |
+
A_{s,min} = \rho_{min} \times b \times d
|
| 146 |
+
\]
|
| 147 |
+
\begin{itemize}
|
| 148 |
+
\item $b \times d = 12 \times 17.5 = 210 \text{ in}^2$
|
| 149 |
+
\item $A_{s,min} = 0.00333333 \times 210 = 0.6999993 \approx 0.70 \text{ in}^2$
|
| 150 |
+
\end{itemize}
|
| 151 |
+
|
| 152 |
+
\subsection*{Step 1.3: Compare Actual $A_s$ with $A_{s,min}$}
|
| 153 |
+
\begin{itemize}
|
| 154 |
+
\item Actual $A_s = 3.16 \text{ in}^2$.
|
| 155 |
+
\item Since $3.16 > 0.70$, the tension steel area exceeds the minimum required.
|
| 156 |
+
\item Reinforcement ratio $\rho = \frac{A_s}{b d} = \frac{3.16}{210} = 0.015048$, which is greater than $\rho_{min} = 0.003333$.
|
| 157 |
+
\end{itemize}
|
| 158 |
+
|
| 159 |
+
\section*{Step 2: Calculate Nominal Moment Strength $M_n$}
|
| 160 |
+
|
| 161 |
+
For a singly reinforced beam, utilizing the rectangular stress block assumption (Whitney block):
|
| 162 |
+
\[
|
| 163 |
+
M_n = A_s f_y \left( d - \frac{a}{2} \right)
|
| 164 |
+
\]
|
| 165 |
+
where $a$ is the depth of the equivalent rectangular stress block:
|
| 166 |
+
\[
|
| 167 |
+
a = \frac{A_s f_y}{0.85 f'_c b}
|
| 168 |
+
\]
|
| 169 |
+
|
| 170 |
+
\subsection*{Step 2.1: Calculate Depth of Stress Block $a$}
|
| 171 |
+
|
| 172 |
+
\subsubsection*{Micro-Calculation for Numerator: $A_s f_y$}
|
| 173 |
+
\[
|
| 174 |
+
A_s f_y = 3.16 \times 60,000 = 189,600 \text{ lb}
|
| 175 |
+
\]
|
| 176 |
+
|
| 177 |
+
\subsubsection*{Micro-Calculation for Denominator: $0.85 f'_c b$}
|
| 178 |
+
\[
|
| 179 |
+
0.85 \times 4000 \times 12 = 3,400 \times 12 = 40,800 \text{ lb/in}
|
| 180 |
+
\]
|
| 181 |
+
|
| 182 |
+
\subsubsection*{Calculate $a$}
|
| 183 |
+
\[\na = \frac{189,600}{40,800} \approx 4.6471 \text{ in.}\n\]
|
| 184 |
+
|
| 185 |
+
\subsection*{Step 2.2: Calculate Lever Arm $d - \frac{a}{2}$}
|
| 186 |
+
\begin{itemize}
|
| 187 |
+
\item $\frac{a}{2} = \frac{4.6471}{2} = 2.3236$ in.
|
| 188 |
+
\item $d - \frac{a}{2} = 17.5 - 2.3236 = 15.1764$ in.
|
| 189 |
+
\end{itemize}
|
| 190 |
+
|
| 191 |
+
\subsection*{Step 2.3: Calculate $M_n$ in in.-lb}
|
| 192 |
+
\[
|
| 193 |
+
M_n = 189,600 \times 15.1764 = 2,877,445.44 \text{ in.-lb}
|
| 194 |
+
\]
|
| 195 |
+
|
| 196 |
+
\subsection*{Step 2.4: Convert $M_n$ to ft-kip}
|
| 197 |
+
\begin{itemize}
|
| 198 |
+
\item Convert to ft-lb (divide by 12): $2,877,445.44 / 12 = 239,787.12 \text{ ft-lb}$.
|
| 199 |
+
\item Convert to ft-kip (divide by 1000): $239,787.12 / 1000 = 239.78712 \text{ ft-kip}$.
|
| 200 |
+
\item Rounded: \textbf{240 ft-kip}.
|
| 201 |
+
\end{itemize}
|
| 202 |
+
|
| 203 |
+
\section*{Step 3: Verify Assumptions and Additional Notes}
|
| 204 |
+
|
| 205 |
+
\begin{itemize}
|
| 206 |
+
\item \textbf{Strain compatibility (qualitative check):} The section is expected to be under-reinforced because the provided steel ratio ($\rho \approx 0.015$) is modest for the given section; thus tension steel yielding is the likely controlling behavior for this example.
|
| 207 |
+
\item \textbf{Compression Zone Bars:} Ignored as per problem statement.
|
| 208 |
+
\item \textbf{Accuracy of $d$:} The 2.5 in. assumption is sufficient. Using a No. 3 stirrup would result in $d \approx 17.625$ in., slightly increasing capacity, but 2.5 is conservative/standard for this example.
|
| 209 |
+
\item \textbf{Units:} All consistency checks passed.
|
| 210 |
+
\end{itemize}
|
| 211 |
+
|
| 212 |
+
\end{document}
|
Dump/note.txt
ADDED
|
File without changes
|
Dump/solution_breakdown.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Solution Breakdown: RecrBeam Calculator
|
| 2 |
+
|
| 3 |
+
This document details the **RecrBeam Calculator**, a software solution designed to solve the concrete beam design problem described in **Example 4-1**. It bridges the gap between the theoretical engineering calculations and the Python-based application mechanics.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 1. Theoretical Foundation (Example 4-1)
|
| 8 |
+
|
| 9 |
+
The core engineering problem is to calculate the **Nominal Moment Strength ($M_n$)** of a singly reinforced concrete beam.
|
| 10 |
+
|
| 11 |
+
### Problem Statement
|
| 12 |
+
Given a rectangular beam with the following properties:
|
| 13 |
+
* **Dimensions**: Width ($b$) = 12 in, Total Height ($h$) = 20 in.
|
| 14 |
+
* **Effective Depth**: $d \approx 17.5$ in (Derived from $h - 2.5$).
|
| 15 |
+
* **Materials**:
|
| 16 |
+
* Concrete Strength ($f'_c$) = 4000 psi.
|
| 17 |
+
* Steel Yield Strength ($f_y$) = 60000 psi.
|
| 18 |
+
* **Reinforcement**: 4 No. 8 bars.
|
| 19 |
+
* Area of one No. 8 bar = 0.79 in².
|
| 20 |
+
* Total Area ($A_s$) = $4 \times 0.79 = 3.16$ in².
|
| 21 |
+
|
| 22 |
+
**Goal**: Calculate $M_n$ and verify $A_s > A_{s,min}$.
|
| 23 |
+
|
| 24 |
+
### Manual Calculation Steps
|
| 25 |
+
|
| 26 |
+
#### Step 1: Verify Minimum Steel
|
| 27 |
+
The code requires $A_s$ to exceed $A_{s,min}$.
|
| 28 |
+
$$ \rho_{min} = \max\left( \frac{3\sqrt{f'_c}}{f_y}, \frac{200}{f_y} \right) $$
|
| 29 |
+
* $\frac{3\sqrt{4000}}{60000} \approx 0.00316$
|
| 30 |
+
* $\frac{200}{60000} \approx 0.00333$ (Governs)
|
| 31 |
+
|
| 32 |
+
$$ A_{s,min} = \rho_{min} \cdot b \cdot d = 0.00333 \cdot 12 \cdot 17.5 = 0.70 \text{ in}^2 $$
|
| 33 |
+
**Result**: $3.16 > 0.70$ (OK).
|
| 34 |
+
|
| 35 |
+
#### Step 2: Calculate Depth of Stress Block ($a$)
|
| 36 |
+
$$ a = \frac{A_s f_y}{0.85 f'_c b} $$
|
| 37 |
+
$$ a = \frac{3.16 \cdot 60000}{0.85 \cdot 4000 \cdot 12} = \frac{189,600}{40,800} \approx 4.647 \text{ in} $$
|
| 38 |
+
|
| 39 |
+
#### Step 3: Calculate Nominal Moment ($M_n$)
|
| 40 |
+
$$ M_n = A_s f_y \left( d - \frac{a}{2} \right) $$
|
| 41 |
+
* Lever Arm: $d - a/2 = 17.5 - 2.3235 = 15.1765$ in.
|
| 42 |
+
* $M_n = 189,600 \text{ lb} \cdot 15.1765 \text{ in} = 2,877,464 \text{ lb-in}$
|
| 43 |
+
* Convert to k-ft: $2,877,464 / 12 / 1000 \approx \textbf{239.79 k-ft}$
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## 2. Application Mechanics
|
| 48 |
+
|
| 49 |
+
The software implementation automates the above logic using Python.
|
| 50 |
+
|
| 51 |
+
### Core Logic: `calculator.py`
|
| 52 |
+
The `RectangularBeam` class mimics the manual steps.
|
| 53 |
+
|
| 54 |
+
```python
|
| 55 |
+
class RectangularBeam:
|
| 56 |
+
def calculate_mn(self):
|
| 57 |
+
# 1. Compute 'a' (Matches Step 2 above)
|
| 58 |
+
# a = (As * fy) / (0.85 * fc * b)
|
| 59 |
+
a = (self.As * self.fy) / (0.85 * self.fc * self.b)
|
| 60 |
+
|
| 61 |
+
# 2. Compute Nominal Moment Mn (Matches Step 3 above)
|
| 62 |
+
# Mn = As * fy * (d - a/2)
|
| 63 |
+
Mn_force = self.As * self.fy
|
| 64 |
+
arm = self.d - (a / 2)
|
| 65 |
+
Mn_kin = Mn_force * arm
|
| 66 |
+
|
| 67 |
+
# ... Conversions to k-ft
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
### Validation: `test_calculator.py`
|
| 71 |
+
The unit test acts as proof that the software aligns with the theory. It explicitly uses the Example 4-1 values as the "Golden Record".
|
| 72 |
+
|
| 73 |
+
```python
|
| 74 |
+
def test_example_4_19a(self):
|
| 75 |
+
# Inputs from Example 4-1
|
| 76 |
+
beam = RectangularBeam(
|
| 77 |
+
width=12.0, effective_depth=17.5,
|
| 78 |
+
f_c=4000.0, f_y=60000.0, rebar_area=3.16
|
| 79 |
+
)
|
| 80 |
+
results = beam.calculate_mn()
|
| 81 |
+
|
| 82 |
+
# Assert correctness within tolerance
|
| 83 |
+
self.assertAlmostEqual(results['a'], 4.647, delta=0.01)
|
| 84 |
+
self.assertAlmostEqual(results['Mn_kft'], 239.79, delta=0.5)
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### User Interface: `app.py`
|
| 88 |
+
The Streamlit app provides an interactive layer:
|
| 89 |
+
* **Inputs**: Sidebar allows modifying $b, h, f'_c, f_y$ and bar sizes.
|
| 90 |
+
* **Visualization**: Uses `matplotlib` to draw the cross-section (showing $b, h, d$ and rebar placement).
|
| 91 |
+
* **Math Rendering**: Uses `st.latex` to display the equations dynamically, showing students exactly how inputs flow into the formula.
|
| 92 |
+
```python
|
| 93 |
+
st.latex(fr"M_n = {As_total:.2f} \cdot {fy} \left({d:.2f} - \frac{{{results['a']:.3f}}}{{2}}\right)")
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### Data Resilience: `db_manager.py`
|
| 97 |
+
* **History**: Every calculation can be saved to a local SQLite database (`beam_calc.db`).
|
| 98 |
+
* **Persistence**: Enables review of past design iterations.
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## 3. Conclusion
|
| 103 |
+
|
| 104 |
+
The **RecrBeam Calculator** is a faithful digital twin of the manual engineering process defined in ACI 318.
|
| 105 |
+
* **Input**: Manual engineering parameters.
|
| 106 |
+
* **Process**: Standard Whitney Stress Block methodology (`calculator.py`).
|
| 107 |
+
* **Output**: Verified against text book examples (`test_calculator.py`).
|
Dump/solution_breakdown.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e4d7c4ed55a03ce2c7d443840f50f4de7e424740ecf5d16f911b3a03fb3d776f
|
| 3 |
+
size 150837
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 callmerem
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
MATLAB/app.m
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
classdef BeamAnalysisApp < matlab.apps.AppBase
|
| 2 |
+
% BeamAnalysisApp - Interactive Analysis of Nominal Moment Strength (Mn)
|
| 3 |
+
% Based on Example 4-1 and 4-1M from ACI 318 Provisions
|
| 4 |
+
%
|
| 5 |
+
% Variables from Example:
|
| 6 |
+
% fc' - Concrete compressive strength
|
| 7 |
+
% fy - Steel yield strength
|
| 8 |
+
% Es - Modulus of elasticity of steel
|
| 9 |
+
% b - Beam width
|
| 10 |
+
% h - Total beam depth
|
| 11 |
+
% d - Effective depth (to centroid of tension steel)
|
| 12 |
+
% As - Total area of tension steel
|
| 13 |
+
% beta1 - Stress block factor (0.85 for fc' <= 4000 psi)
|
| 14 |
+
% epsilon_cu - Ultimate concrete strain (0.003)
|
| 15 |
+
%
|
| 16 |
+
% Calculated:
|
| 17 |
+
% T - Tension force in steel
|
| 18 |
+
% a - Depth of equivalent stress block
|
| 19 |
+
% c - Neutral axis depth
|
| 20 |
+
% epsilon_y - Yield strain of steel
|
| 21 |
+
% epsilon_s - Strain in steel at ultimate
|
| 22 |
+
% Mn - Nominal moment strength
|
| 23 |
+
% As_min - Minimum steel area per ACI
|
| 24 |
+
|
| 25 |
+
properties (Access = public)
|
| 26 |
+
UIFigure matlab.ui.Figure
|
| 27 |
+
MainGrid matlab.ui.container.GridLayout
|
| 28 |
+
|
| 29 |
+
% Panels
|
| 30 |
+
InputPanel matlab.ui.container.Panel
|
| 31 |
+
DiagramPanel matlab.ui.container.Panel
|
| 32 |
+
ResultsPanel matlab.ui.container.Panel
|
| 33 |
+
EquationsPanel matlab.ui.container.Panel
|
| 34 |
+
|
| 35 |
+
% Inputs - Material
|
| 36 |
+
UnitSwitch matlab.ui.control.Switch
|
| 37 |
+
EditFc matlab.ui.control.NumericEditField
|
| 38 |
+
EditFy matlab.ui.control.NumericEditField
|
| 39 |
+
EditEs matlab.ui.control.NumericEditField
|
| 40 |
+
EditBeta1 matlab.ui.control.NumericEditField
|
| 41 |
+
EditEpsCu matlab.ui.control.NumericEditField
|
| 42 |
+
|
| 43 |
+
% Inputs - Geometry
|
| 44 |
+
EditB matlab.ui.control.NumericEditField
|
| 45 |
+
EditH matlab.ui.control.NumericEditField
|
| 46 |
+
EditD matlab.ui.control.NumericEditField
|
| 47 |
+
|
| 48 |
+
% Inputs - Reinforcement
|
| 49 |
+
EditBars matlab.ui.control.NumericEditField
|
| 50 |
+
EditBarArea matlab.ui.control.NumericEditField
|
| 51 |
+
|
| 52 |
+
% Visualization Axes
|
| 53 |
+
AxSection matlab.ui.control.UIAxes
|
| 54 |
+
AxStrain matlab.ui.control.UIAxes
|
| 55 |
+
AxStress matlab.ui.control.UIAxes
|
| 56 |
+
AxEquations matlab.ui.control.UIAxes
|
| 57 |
+
|
| 58 |
+
% Results
|
| 59 |
+
ResultsText matlab.ui.control.TextArea
|
| 60 |
+
end
|
| 61 |
+
|
| 62 |
+
properties (Access = private)
|
| 63 |
+
Units struct
|
| 64 |
+
% Color scheme
|
| 65 |
+
Colors struct
|
| 66 |
+
end
|
| 67 |
+
|
| 68 |
+
methods (Access = private)
|
| 69 |
+
|
| 70 |
+
function updateApp(app, ~)
|
| 71 |
+
% === 1. GET INPUTS ===
|
| 72 |
+
is_imperial = strcmp(app.UnitSwitch.Value, 'Imperial');
|
| 73 |
+
|
| 74 |
+
fc = app.EditFc.Value;
|
| 75 |
+
fy = app.EditFy.Value;
|
| 76 |
+
Es = app.EditEs.Value;
|
| 77 |
+
beta1 = app.EditBeta1.Value;
|
| 78 |
+
epsilon_cu = app.EditEpsCu.Value;
|
| 79 |
+
b = app.EditB.Value;
|
| 80 |
+
h = app.EditH.Value;
|
| 81 |
+
d = app.EditD.Value;
|
| 82 |
+
n_bars = app.EditBars.Value;
|
| 83 |
+
bar_A = app.EditBarArea.Value;
|
| 84 |
+
As = n_bars * bar_A;
|
| 85 |
+
|
| 86 |
+
% Set units
|
| 87 |
+
if is_imperial
|
| 88 |
+
app.Units = struct('len', 'in', 'area', 'in2', 'force', 'lb', ...
|
| 89 |
+
'stress', 'psi', 'moment', 'lb-in', 'moment_k', 'k-in', 'moment_alt', 'k-ft');
|
| 90 |
+
else
|
| 91 |
+
app.Units = struct('len', 'mm', 'area', 'mm2', 'force', 'N', ...
|
| 92 |
+
'stress', 'MPa', 'moment', 'N-mm', 'moment_k', 'N-mm', 'moment_alt', 'kN-m');
|
| 93 |
+
end
|
| 94 |
+
u = app.Units;
|
| 95 |
+
clr = app.Colors;
|
| 96 |
+
|
| 97 |
+
% === 2. CALCULATIONS ===
|
| 98 |
+
T = As * fy;
|
| 99 |
+
a = (As * fy) / (0.85 * fc * b);
|
| 100 |
+
c = a / beta1;
|
| 101 |
+
epsilon_y = fy / Es;
|
| 102 |
+
epsilon_s = epsilon_cu * (d - c) / c;
|
| 103 |
+
|
| 104 |
+
if epsilon_s >= epsilon_y
|
| 105 |
+
yield_check = true;
|
| 106 |
+
fs = fy;
|
| 107 |
+
else
|
| 108 |
+
yield_check = false;
|
| 109 |
+
fs = epsilon_s * Es;
|
| 110 |
+
end
|
| 111 |
+
|
| 112 |
+
Mn = As * fs * (d - a/2);
|
| 113 |
+
|
| 114 |
+
if is_imperial
|
| 115 |
+
Mn_disp = Mn / 12000;
|
| 116 |
+
T_disp = T / 1000;
|
| 117 |
+
Mn_k = Mn / 1000;
|
| 118 |
+
else
|
| 119 |
+
Mn_disp = Mn / 1e6;
|
| 120 |
+
T_disp = T / 1000;
|
| 121 |
+
Mn_k = Mn;
|
| 122 |
+
end
|
| 123 |
+
|
| 124 |
+
if is_imperial
|
| 125 |
+
term1 = (3 * sqrt(fc) / fy) * b * d;
|
| 126 |
+
term2 = (200 / fy) * b * d;
|
| 127 |
+
else
|
| 128 |
+
term1 = (0.25 * sqrt(fc) / fy) * b * d;
|
| 129 |
+
term2 = (1.4 / fy) * b * d;
|
| 130 |
+
end
|
| 131 |
+
As_min = max(term1, term2);
|
| 132 |
+
As_check = As >= As_min;
|
| 133 |
+
|
| 134 |
+
% === 3. CROSS SECTION DIAGRAM ===
|
| 135 |
+
ax = app.AxSection;
|
| 136 |
+
cla(ax); hold(ax, 'on');
|
| 137 |
+
|
| 138 |
+
% Concrete beam
|
| 139 |
+
rectangle(ax, 'Position', [0, 0, b, h], ...
|
| 140 |
+
'FaceColor', clr.concrete, 'EdgeColor', clr.outline, 'LineWidth', 1.5);
|
| 141 |
+
|
| 142 |
+
% Compression zone
|
| 143 |
+
patch(ax, [0, b, b, 0], [h, h, h-a, h-a], clr.compression, ...
|
| 144 |
+
'EdgeColor', 'none', 'FaceAlpha', 0.6);
|
| 145 |
+
|
| 146 |
+
% Neutral axis line
|
| 147 |
+
plot(ax, [0, b], [h-c, h-c], '--', 'Color', clr.neutral, 'LineWidth', 1.2);
|
| 148 |
+
|
| 149 |
+
% Steel bars
|
| 150 |
+
bar_r = sqrt(bar_A/pi) * 0.7;
|
| 151 |
+
steel_y = h - d;
|
| 152 |
+
if n_bars == 1
|
| 153 |
+
cx = b/2;
|
| 154 |
+
else
|
| 155 |
+
cx = linspace(b*0.12, b*0.88, n_bars);
|
| 156 |
+
end
|
| 157 |
+
for i = 1:n_bars
|
| 158 |
+
rectangle(ax, 'Position', [cx(i)-bar_r, steel_y-bar_r, 2*bar_r, 2*bar_r], ...
|
| 159 |
+
'FaceColor', clr.steel, 'EdgeColor', [0.1 0.1 0.1], 'Curvature', [1 1], 'LineWidth', 0.5);
|
| 160 |
+
end
|
| 161 |
+
|
| 162 |
+
% Dimension: h
|
| 163 |
+
xh = b + b*0.08;
|
| 164 |
+
plot(ax, [xh, xh], [0, h], 'k-', 'LineWidth', 0.8);
|
| 165 |
+
plot(ax, [b, xh+b*0.02], [0, 0], 'k-', 'LineWidth', 0.8);
|
| 166 |
+
plot(ax, [b, xh+b*0.02], [h, h], 'k-', 'LineWidth', 0.8);
|
| 167 |
+
text(ax, xh+b*0.04, h/2, sprintf('h=%.1f', h), 'FontSize', 9, 'FontName', 'Arial');
|
| 168 |
+
|
| 169 |
+
% Dimension: d
|
| 170 |
+
xd = b + b*0.22;
|
| 171 |
+
plot(ax, [xd, xd], [steel_y, h], 'Color', clr.dimension, 'LineWidth', 0.8);
|
| 172 |
+
text(ax, xd+b*0.04, (h+steel_y)/2, sprintf('d=%.1f', d), 'FontSize', 9, 'Color', clr.dimension, 'FontName', 'Arial');
|
| 173 |
+
|
| 174 |
+
% Dimension: a
|
| 175 |
+
xa = -b*0.08;
|
| 176 |
+
plot(ax, [xa, xa], [h-a, h], 'Color', clr.compression_line, 'LineWidth', 1.5);
|
| 177 |
+
text(ax, xa-b*0.15, h-a/2, sprintf('a=%.2f', a), 'FontSize', 9, 'Color', clr.compression_line, 'FontName', 'Arial');
|
| 178 |
+
|
| 179 |
+
% Dimension: b
|
| 180 |
+
text(ax, b/2, -h*0.06, sprintf('b=%.1f', b), 'HorizontalAlignment', 'center', 'FontSize', 9, 'FontName', 'Arial');
|
| 181 |
+
|
| 182 |
+
% c label (neutral axis depth)
|
| 183 |
+
text(ax, b+b*0.04, h-c, sprintf('c=%.2f', c), 'FontSize', 8, 'Color', clr.neutral, 'FontName', 'Arial');
|
| 184 |
+
|
| 185 |
+
axis(ax, 'equal');
|
| 186 |
+
ax.XLim = [-b*0.25, b*1.4];
|
| 187 |
+
ax.YLim = [-h*0.12, h*1.08];
|
| 188 |
+
ax.XTick = []; ax.YTick = [];
|
| 189 |
+
ax.XColor = 'none'; ax.YColor = 'none';
|
| 190 |
+
title(ax, 'Cross Section', 'FontWeight', 'bold', 'FontSize', 11, 'FontName', 'Arial');
|
| 191 |
+
|
| 192 |
+
% === 4. STRAIN DIAGRAM ===
|
| 193 |
+
ax = app.AxStrain;
|
| 194 |
+
cla(ax); hold(ax, 'on');
|
| 195 |
+
|
| 196 |
+
strain_w = 0.4;
|
| 197 |
+
|
| 198 |
+
% Beam outline (reference)
|
| 199 |
+
plot(ax, [0, 0], [0, h], 'Color', [0.7 0.7 0.7], 'LineWidth', 1);
|
| 200 |
+
|
| 201 |
+
% Strain profile fill
|
| 202 |
+
x_top = epsilon_cu * strain_w / 0.003;
|
| 203 |
+
x_bot = epsilon_s * strain_w / 0.003;
|
| 204 |
+
patch(ax, [0, x_top, x_bot, 0], [h, h, steel_y, steel_y], clr.strain, ...
|
| 205 |
+
'EdgeColor', clr.strain_edge, 'LineWidth', 1.2, 'FaceAlpha', 0.5);
|
| 206 |
+
|
| 207 |
+
% Neutral axis
|
| 208 |
+
plot(ax, [-0.05, strain_w*1.2], [h-c, h-c], '--', 'Color', clr.neutral, 'LineWidth', 1);
|
| 209 |
+
text(ax, -0.03, h-c, sprintf('c=%.2f', c), 'FontSize', 8, 'HorizontalAlignment', 'right', 'Color', clr.neutral, 'FontName', 'Arial');
|
| 210 |
+
|
| 211 |
+
% Labels
|
| 212 |
+
text(ax, x_top+0.02, h, sprintf('ecu=%.4f', epsilon_cu), 'FontSize', 9, 'Color', clr.strain_edge, 'FontName', 'Arial');
|
| 213 |
+
text(ax, x_bot+0.02, steel_y, sprintf('es=%.5f', epsilon_s), 'FontSize', 9, 'Color', clr.strain_edge, 'FontName', 'Arial');
|
| 214 |
+
|
| 215 |
+
ax.XLim = [-0.1, strain_w*1.5];
|
| 216 |
+
ax.YLim = [-h*0.12, h*1.08];
|
| 217 |
+
ax.XTick = []; ax.YTick = [];
|
| 218 |
+
ax.XColor = 'none'; ax.YColor = 'none';
|
| 219 |
+
title(ax, 'Strain Distribution', 'FontWeight', 'bold', 'FontSize', 11, 'FontName', 'Arial');
|
| 220 |
+
|
| 221 |
+
% === 5. STRESS/FORCE DIAGRAM ===
|
| 222 |
+
ax = app.AxStress;
|
| 223 |
+
cla(ax); hold(ax, 'on');
|
| 224 |
+
|
| 225 |
+
stress_w = 0.5;
|
| 226 |
+
|
| 227 |
+
% Compression block
|
| 228 |
+
patch(ax, [0, stress_w, stress_w, 0], [h, h, h-a, h-a], clr.compression, ...
|
| 229 |
+
'EdgeColor', clr.compression_line, 'LineWidth', 1.2, 'FaceAlpha', 0.7);
|
| 230 |
+
text(ax, stress_w/2, h-a/2, '0.85fc''', 'HorizontalAlignment', 'center', 'FontSize', 9, 'FontName', 'Arial');
|
| 231 |
+
|
| 232 |
+
% Compression force arrow
|
| 233 |
+
quiver(ax, stress_w+0.05, h-a/2, 0.2, 0, 0, 'Color', clr.compression_line, 'LineWidth', 2, 'MaxHeadSize', 0.8);
|
| 234 |
+
text(ax, stress_w+0.28, h-a/2, sprintf('C=%.0f k', T_disp), 'FontSize', 9, 'Color', clr.compression_line, 'FontName', 'Arial');
|
| 235 |
+
|
| 236 |
+
% Steel location
|
| 237 |
+
plot(ax, [0, stress_w*0.3], [steel_y, steel_y], 'Color', clr.steel, 'LineWidth', 2);
|
| 238 |
+
|
| 239 |
+
% Tension force arrow
|
| 240 |
+
quiver(ax, 0, steel_y, 0.25, 0, 0, 'Color', clr.tension, 'LineWidth', 2, 'MaxHeadSize', 0.8);
|
| 241 |
+
text(ax, 0.28, steel_y, sprintf('T=%.0f k', T_disp), 'FontSize', 9, 'Color', clr.tension, 'FontName', 'Arial');
|
| 242 |
+
|
| 243 |
+
% Neutral axis
|
| 244 |
+
plot(ax, [-0.05, stress_w+0.3], [h-c, h-c], '--', 'Color', clr.neutral, 'LineWidth', 1);
|
| 245 |
+
|
| 246 |
+
% Moment arm
|
| 247 |
+
xa_arm = stress_w + 0.45;
|
| 248 |
+
plot(ax, [xa_arm, xa_arm], [h-a/2, steel_y], 'Color', clr.moment_arm, 'LineWidth', 1.5);
|
| 249 |
+
plot(ax, [xa_arm-0.02, xa_arm+0.02], [h-a/2, h-a/2], 'Color', clr.moment_arm, 'LineWidth', 1.5);
|
| 250 |
+
plot(ax, [xa_arm-0.02, xa_arm+0.02], [steel_y, steel_y], 'Color', clr.moment_arm, 'LineWidth', 1.5);
|
| 251 |
+
text(ax, xa_arm+0.03, (h-a/2+steel_y)/2, 'd-a/2', 'FontSize', 9, 'Color', clr.moment_arm, 'FontName', 'Arial');
|
| 252 |
+
|
| 253 |
+
ax.XLim = [-0.1, stress_w+0.65];
|
| 254 |
+
ax.YLim = [-h*0.12, h*1.08];
|
| 255 |
+
ax.XTick = []; ax.YTick = [];
|
| 256 |
+
ax.XColor = 'none'; ax.YColor = 'none';
|
| 257 |
+
title(ax, 'Stress Block & Forces', 'FontWeight', 'bold', 'FontSize', 11, 'FontName', 'Arial');
|
| 258 |
+
|
| 259 |
+
% === 6. EQUATIONS PANEL ===
|
| 260 |
+
ax = app.AxEquations;
|
| 261 |
+
cla(ax); hold(ax, 'on');
|
| 262 |
+
axis(ax, 'off');
|
| 263 |
+
ax.XLim = [0 1]; ax.YLim = [0 1];
|
| 264 |
+
|
| 265 |
+
y = 0.92;
|
| 266 |
+
dy = 0.115;
|
| 267 |
+
fs = 10;
|
| 268 |
+
|
| 269 |
+
% Header
|
| 270 |
+
text(ax, 0.01, y, 'STEP-BY-STEP CALCULATIONS', 'FontWeight', 'bold', 'FontSize', 12, 'FontName', 'Arial');
|
| 271 |
+
y = y - dy*0.7;
|
| 272 |
+
|
| 273 |
+
% Step 1
|
| 274 |
+
eq1 = sprintf('$$A_s = n \\times A_{bar} = %.0f \\times %.3f = %.3f \\; \\mathrm{%s}$$', n_bars, bar_A, As, u.area);
|
| 275 |
+
text(ax, 0.01, y, eq1, 'Interpreter', 'latex', 'FontSize', fs);
|
| 276 |
+
y = y - dy*0.6;
|
| 277 |
+
|
| 278 |
+
eq2 = sprintf('$$T = A_s \\cdot f_y = %.3f \\times %.0f = %.0f \\; \\mathrm{%s} \\;\\; (%.1f \\; \\mathrm{kips})$$', As, fy, T, u.force, T_disp);
|
| 279 |
+
text(ax, 0.01, y, eq2, 'Interpreter', 'latex', 'FontSize', fs);
|
| 280 |
+
y = y - dy;
|
| 281 |
+
|
| 282 |
+
% Step 2
|
| 283 |
+
eq3 = sprintf('$$a = \\frac{A_s f_y}{0.85 f''_c b} = \\frac{%.0f}{0.85 \\times %.0f \\times %.1f} = %.4f \\; \\mathrm{%s}$$', T, fc, b, a, u.len);
|
| 284 |
+
text(ax, 0.01, y, eq3, 'Interpreter', 'latex', 'FontSize', fs);
|
| 285 |
+
y = y - dy*0.6;
|
| 286 |
+
|
| 287 |
+
eq4 = sprintf('$$c = \\frac{a}{\\beta_1} = \\frac{%.4f}{%.3f} = %.4f \\; \\mathrm{%s}$$', a, beta1, c, u.len);
|
| 288 |
+
text(ax, 0.01, y, eq4, 'Interpreter', 'latex', 'FontSize', fs);
|
| 289 |
+
y = y - dy;
|
| 290 |
+
|
| 291 |
+
% Step 3
|
| 292 |
+
eq5 = sprintf('$$\\varepsilon_y = \\frac{f_y}{E_s} = \\frac{%.0f}{%.0f} = %.6f$$', fy, Es, epsilon_y);
|
| 293 |
+
text(ax, 0.01, y, eq5, 'Interpreter', 'latex', 'FontSize', fs);
|
| 294 |
+
|
| 295 |
+
eq6 = sprintf('$$\\varepsilon_s = \\left(\\frac{d-c}{c}\\right)\\varepsilon_{cu} = \\left(\\frac{%.2f-%.2f}{%.2f}\\right)(%.4f) = %.6f$$', d, c, c, epsilon_cu, epsilon_s);
|
| 296 |
+
text(ax, 0.45, y, eq6, 'Interpreter', 'latex', 'FontSize', fs);
|
| 297 |
+
|
| 298 |
+
if yield_check
|
| 299 |
+
text(ax, 0.92, y, '[OK]', 'FontSize', 10, 'Color', clr.ok, 'FontWeight', 'bold', 'FontName', 'Arial');
|
| 300 |
+
else
|
| 301 |
+
text(ax, 0.92, y, '[NG]', 'FontSize', 10, 'Color', clr.ng, 'FontWeight', 'bold', 'FontName', 'Arial');
|
| 302 |
+
end
|
| 303 |
+
y = y - dy;
|
| 304 |
+
|
| 305 |
+
% Step 4
|
| 306 |
+
eq7 = sprintf('$$M_n = A_s f_y \\left(d - \\frac{a}{2}\\right) = %.0f \\left(%.2f - \\frac{%.4f}{2}\\right) = %.0f \\; \\mathrm{%s}$$', T, d, a, Mn_k, u.moment_k);
|
| 307 |
+
text(ax, 0.01, y, eq7, 'Interpreter', 'latex', 'FontSize', fs);
|
| 308 |
+
y = y - dy*0.6;
|
| 309 |
+
|
| 310 |
+
result_txt = sprintf('Mn = %.1f %s', Mn_disp, u.moment_alt);
|
| 311 |
+
text(ax, 0.01, y, result_txt, 'FontSize', 13, 'Color', clr.result, 'FontWeight', 'bold', 'FontName', 'Arial');
|
| 312 |
+
y = y - dy;
|
| 313 |
+
|
| 314 |
+
% Step 5
|
| 315 |
+
if is_imperial
|
| 316 |
+
eq9 = sprintf('$$A_{s,min} = \\max\\left(\\frac{3\\sqrt{f''_c}}{f_y}b_wd,\\;\\frac{200}{f_y}b_wd\\right) = %.4f \\; \\mathrm{%s}$$', As_min, u.area);
|
| 317 |
+
else
|
| 318 |
+
eq9 = sprintf('$$A_{s,min} = \\max\\left(\\frac{0.25\\sqrt{f''_c}}{f_y}b_wd,\\;\\frac{1.4}{f_y}b_wd\\right) = %.1f \\; \\mathrm{%s}$$', As_min, u.area);
|
| 319 |
+
end
|
| 320 |
+
text(ax, 0.01, y, eq9, 'Interpreter', 'latex', 'FontSize', fs);
|
| 321 |
+
|
| 322 |
+
if As_check
|
| 323 |
+
text(ax, 0.75, y, sprintf('As >= As,min [OK]'), 'FontSize', 10, 'Color', clr.ok, 'FontWeight', 'bold', 'FontName', 'Arial');
|
| 324 |
+
else
|
| 325 |
+
text(ax, 0.75, y, sprintf('As < As,min [NG]'), 'FontSize', 10, 'Color', clr.ng, 'FontWeight', 'bold', 'FontName', 'Arial');
|
| 326 |
+
end
|
| 327 |
+
|
| 328 |
+
% === 7. RESULTS TEXT ===
|
| 329 |
+
if yield_check
|
| 330 |
+
yield_str = 'Yes (Steel Yields)';
|
| 331 |
+
else
|
| 332 |
+
yield_str = 'No (Steel Elastic)';
|
| 333 |
+
end
|
| 334 |
+
|
| 335 |
+
if As_check
|
| 336 |
+
asmin_str = 'OK';
|
| 337 |
+
else
|
| 338 |
+
asmin_str = 'NOT OK';
|
| 339 |
+
end
|
| 340 |
+
|
| 341 |
+
results_str = sprintf([...
|
| 342 |
+
'RESULTS SUMMARY\n' ...
|
| 343 |
+
'================\n\n' ...
|
| 344 |
+
'Steel Area:\n' ...
|
| 345 |
+
' As = %.4f %s\n\n' ...
|
| 346 |
+
'Forces:\n' ...
|
| 347 |
+
' T = C = %.2f kips\n\n' ...
|
| 348 |
+
'Geometry:\n' ...
|
| 349 |
+
' a = %.4f %s\n' ...
|
| 350 |
+
' c = %.4f %s\n\n' ...
|
| 351 |
+
'Strain Check:\n' ...
|
| 352 |
+
' ey = %.6f\n' ...
|
| 353 |
+
' es = %.6f\n' ...
|
| 354 |
+
' Yield: %s\n\n' ...
|
| 355 |
+
'NOMINAL MOMENT:\n' ...
|
| 356 |
+
' Mn = %.1f %s\n\n' ...
|
| 357 |
+
'Min Steel Check:\n' ...
|
| 358 |
+
' As,min = %.4f %s\n' ...
|
| 359 |
+
' Status: %s'], ...
|
| 360 |
+
As, u.area, T_disp, a, u.len, c, u.len, ...
|
| 361 |
+
epsilon_y, epsilon_s, yield_str, ...
|
| 362 |
+
Mn_disp, u.moment_alt, As_min, u.area, asmin_str);
|
| 363 |
+
|
| 364 |
+
app.ResultsText.Value = results_str;
|
| 365 |
+
end
|
| 366 |
+
|
| 367 |
+
function switchUnits(app, ~)
|
| 368 |
+
if strcmp(app.UnitSwitch.Value, 'Imperial')
|
| 369 |
+
app.EditFc.Value = 4000;
|
| 370 |
+
app.EditFy.Value = 60000;
|
| 371 |
+
app.EditEs.Value = 29000000;
|
| 372 |
+
app.EditBeta1.Value = 0.85;
|
| 373 |
+
app.EditEpsCu.Value = 0.003;
|
| 374 |
+
app.EditB.Value = 12;
|
| 375 |
+
app.EditH.Value = 20;
|
| 376 |
+
app.EditD.Value = 17.5;
|
| 377 |
+
app.EditBars.Value = 4;
|
| 378 |
+
app.EditBarArea.Value = 0.79;
|
| 379 |
+
else
|
| 380 |
+
app.EditFc.Value = 20;
|
| 381 |
+
app.EditFy.Value = 420;
|
| 382 |
+
app.EditEs.Value = 200000;
|
| 383 |
+
app.EditBeta1.Value = 0.85;
|
| 384 |
+
app.EditEpsCu.Value = 0.003;
|
| 385 |
+
app.EditB.Value = 250;
|
| 386 |
+
app.EditH.Value = 565;
|
| 387 |
+
app.EditD.Value = 500;
|
| 388 |
+
app.EditBars.Value = 3;
|
| 389 |
+
app.EditBarArea.Value = 510;
|
| 390 |
+
end
|
| 391 |
+
updateApp(app);
|
| 392 |
+
end
|
| 393 |
+
end
|
| 394 |
+
|
| 395 |
+
methods (Access = public)
|
| 396 |
+
function app = BeamAnalysisApp()
|
| 397 |
+
createComponents(app);
|
| 398 |
+
initializeColors(app);
|
| 399 |
+
updateApp(app);
|
| 400 |
+
registerApp(app, app.UIFigure);
|
| 401 |
+
if nargout == 0
|
| 402 |
+
clear app
|
| 403 |
+
end
|
| 404 |
+
end
|
| 405 |
+
|
| 406 |
+
function delete(app)
|
| 407 |
+
delete(app.UIFigure);
|
| 408 |
+
end
|
| 409 |
+
end
|
| 410 |
+
|
| 411 |
+
methods (Access = private)
|
| 412 |
+
|
| 413 |
+
function initializeColors(app)
|
| 414 |
+
% Professional color scheme
|
| 415 |
+
app.Colors = struct(...
|
| 416 |
+
'concrete', [0.88 0.88 0.86], ...
|
| 417 |
+
'outline', [0.3 0.3 0.3], ...
|
| 418 |
+
'compression', [0.85 0.55 0.55], ...
|
| 419 |
+
'compression_line', [0.7 0.2 0.2], ...
|
| 420 |
+
'tension', [0.2 0.4 0.7], ...
|
| 421 |
+
'steel', [0.25 0.25 0.3], ...
|
| 422 |
+
'neutral', [0.4 0.4 0.4], ...
|
| 423 |
+
'dimension', [0.2 0.5 0.7], ...
|
| 424 |
+
'strain', [0.7 0.85 0.95], ...
|
| 425 |
+
'strain_edge', [0.2 0.4 0.6], ...
|
| 426 |
+
'moment_arm', [0.2 0.6 0.3], ...
|
| 427 |
+
'result', [0.1 0.3 0.6], ...
|
| 428 |
+
'ok', [0.1 0.5 0.2], ...
|
| 429 |
+
'ng', [0.7 0.15 0.15], ...
|
| 430 |
+
'bg', [0.97 0.97 0.97], ...
|
| 431 |
+
'panel_bg', [1 1 1]);
|
| 432 |
+
end
|
| 433 |
+
|
| 434 |
+
function createComponents(app)
|
| 435 |
+
% === MAIN FIGURE ===
|
| 436 |
+
app.UIFigure = uifigure('Visible', 'off');
|
| 437 |
+
app.UIFigure.Position = [80 60 1400 800];
|
| 438 |
+
app.UIFigure.Name = 'Beam Analysis - Nominal Moment Strength (ACI 318)';
|
| 439 |
+
app.UIFigure.Color = [0.94 0.94 0.94];
|
| 440 |
+
app.UIFigure.Resize = 'on';
|
| 441 |
+
|
| 442 |
+
% === MAIN GRID ===
|
| 443 |
+
app.MainGrid = uigridlayout(app.UIFigure);
|
| 444 |
+
app.MainGrid.ColumnWidth = {'0.18x', '0.82x'};
|
| 445 |
+
app.MainGrid.RowHeight = {'1x'};
|
| 446 |
+
app.MainGrid.Padding = [8 8 8 8];
|
| 447 |
+
app.MainGrid.ColumnSpacing = 8;
|
| 448 |
+
|
| 449 |
+
% === LEFT: INPUT PANEL ===
|
| 450 |
+
app.InputPanel = uipanel(app.MainGrid);
|
| 451 |
+
app.InputPanel.Layout.Row = 1;
|
| 452 |
+
app.InputPanel.Layout.Column = 1;
|
| 453 |
+
app.InputPanel.Title = 'INPUT PARAMETERS';
|
| 454 |
+
app.InputPanel.FontWeight = 'bold';
|
| 455 |
+
app.InputPanel.FontSize = 11;
|
| 456 |
+
app.InputPanel.BackgroundColor = [1 1 1];
|
| 457 |
+
|
| 458 |
+
inpGrid = uigridlayout(app.InputPanel);
|
| 459 |
+
inpGrid.ColumnWidth = {'1.3x', '1x'};
|
| 460 |
+
inpGrid.RowHeight = repmat({'fit'}, 1, 16);
|
| 461 |
+
inpGrid.RowSpacing = 4;
|
| 462 |
+
inpGrid.Padding = [8 8 8 8];
|
| 463 |
+
|
| 464 |
+
% Unit Switch
|
| 465 |
+
lbl = uilabel(inpGrid); lbl.Text = 'Unit System'; lbl.FontWeight = 'bold';
|
| 466 |
+
app.UnitSwitch = uiswitch(inpGrid, 'slider');
|
| 467 |
+
app.UnitSwitch.Items = {'Imperial', 'SI'};
|
| 468 |
+
app.UnitSwitch.ValueChangedFcn = createCallbackFcn(app, @switchUnits, true);
|
| 469 |
+
|
| 470 |
+
% Separator - Materials
|
| 471 |
+
lbl = uilabel(inpGrid); lbl.Text = 'MATERIALS'; lbl.FontWeight = 'bold'; lbl.FontColor = [0.3 0.3 0.5];
|
| 472 |
+
uilabel(inpGrid);
|
| 473 |
+
|
| 474 |
+
% fc
|
| 475 |
+
lbl = uilabel(inpGrid); lbl.Text = 'fc'' (Concrete)';
|
| 476 |
+
app.EditFc = uieditfield(inpGrid, 'numeric');
|
| 477 |
+
app.EditFc.ValueChangedFcn = createCallbackFcn(app, @updateApp, true);
|
| 478 |
+
|
| 479 |
+
% fy
|
| 480 |
+
lbl = uilabel(inpGrid); lbl.Text = 'fy (Steel Yield)';
|
| 481 |
+
app.EditFy = uieditfield(inpGrid, 'numeric');
|
| 482 |
+
app.EditFy.ValueChangedFcn = createCallbackFcn(app, @updateApp, true);
|
| 483 |
+
|
| 484 |
+
% Es
|
| 485 |
+
lbl = uilabel(inpGrid); lbl.Text = 'Es (Modulus)';
|
| 486 |
+
app.EditEs = uieditfield(inpGrid, 'numeric');
|
| 487 |
+
app.EditEs.ValueChangedFcn = createCallbackFcn(app, @updateApp, true);
|
| 488 |
+
|
| 489 |
+
% beta1
|
| 490 |
+
lbl = uilabel(inpGrid); lbl.Text = 'Beta1';
|
| 491 |
+
app.EditBeta1 = uieditfield(inpGrid, 'numeric');
|
| 492 |
+
app.EditBeta1.ValueChangedFcn = createCallbackFcn(app, @updateApp, true);
|
| 493 |
+
|
| 494 |
+
% epsilon_cu
|
| 495 |
+
lbl = uilabel(inpGrid); lbl.Text = 'ecu (Ult. Strain)';
|
| 496 |
+
app.EditEpsCu = uieditfield(inpGrid, 'numeric');
|
| 497 |
+
app.EditEpsCu.ValueDisplayFormat = '%.4f';
|
| 498 |
+
app.EditEpsCu.ValueChangedFcn = createCallbackFcn(app, @updateApp, true);
|
| 499 |
+
|
| 500 |
+
% Separator - Geometry
|
| 501 |
+
lbl = uilabel(inpGrid); lbl.Text = 'GEOMETRY'; lbl.FontWeight = 'bold'; lbl.FontColor = [0.3 0.3 0.5];
|
| 502 |
+
uilabel(inpGrid);
|
| 503 |
+
|
| 504 |
+
% b
|
| 505 |
+
lbl = uilabel(inpGrid); lbl.Text = 'b (Width)';
|
| 506 |
+
app.EditB = uieditfield(inpGrid, 'numeric');
|
| 507 |
+
app.EditB.ValueChangedFcn = createCallbackFcn(app, @updateApp, true);
|
| 508 |
+
|
| 509 |
+
% h
|
| 510 |
+
lbl = uilabel(inpGrid); lbl.Text = 'h (Total Depth)';
|
| 511 |
+
app.EditH = uieditfield(inpGrid, 'numeric');
|
| 512 |
+
app.EditH.ValueChangedFcn = createCallbackFcn(app, @updateApp, true);
|
| 513 |
+
|
| 514 |
+
% d
|
| 515 |
+
lbl = uilabel(inpGrid); lbl.Text = 'd (Eff. Depth)';
|
| 516 |
+
app.EditD = uieditfield(inpGrid, 'numeric');
|
| 517 |
+
app.EditD.ValueChangedFcn = createCallbackFcn(app, @updateApp, true);
|
| 518 |
+
|
| 519 |
+
% Separator - Reinforcement
|
| 520 |
+
lbl = uilabel(inpGrid); lbl.Text = 'REINFORCEMENT'; lbl.FontWeight = 'bold'; lbl.FontColor = [0.3 0.3 0.5];
|
| 521 |
+
uilabel(inpGrid);
|
| 522 |
+
|
| 523 |
+
% Bars
|
| 524 |
+
lbl = uilabel(inpGrid); lbl.Text = 'Number of Bars';
|
| 525 |
+
app.EditBars = uieditfield(inpGrid, 'numeric');
|
| 526 |
+
app.EditBars.ValueDisplayFormat = '%.0f';
|
| 527 |
+
app.EditBars.ValueChangedFcn = createCallbackFcn(app, @updateApp, true);
|
| 528 |
+
|
| 529 |
+
% Bar Area
|
| 530 |
+
lbl = uilabel(inpGrid); lbl.Text = 'Bar Area (each)';
|
| 531 |
+
app.EditBarArea = uieditfield(inpGrid, 'numeric');
|
| 532 |
+
app.EditBarArea.ValueChangedFcn = createCallbackFcn(app, @updateApp, true);
|
| 533 |
+
|
| 534 |
+
% === RIGHT PANEL ===
|
| 535 |
+
rightGrid = uigridlayout(app.MainGrid);
|
| 536 |
+
rightGrid.Layout.Row = 1;
|
| 537 |
+
rightGrid.Layout.Column = 2;
|
| 538 |
+
rightGrid.ColumnWidth = {'1x', '1x', '1x', '0.6x'};
|
| 539 |
+
rightGrid.RowHeight = {'1.2x', '1.5x'};
|
| 540 |
+
rightGrid.Padding = [0 0 0 0];
|
| 541 |
+
rightGrid.ColumnSpacing = 6;
|
| 542 |
+
rightGrid.RowSpacing = 6;
|
| 543 |
+
|
| 544 |
+
% --- Diagram Panels ---
|
| 545 |
+
% Section
|
| 546 |
+
sectionPanel = uipanel(rightGrid);
|
| 547 |
+
sectionPanel.Layout.Row = 1;
|
| 548 |
+
sectionPanel.Layout.Column = 1;
|
| 549 |
+
sectionPanel.BackgroundColor = [1 1 1];
|
| 550 |
+
sectionPanel.BorderType = 'line';
|
| 551 |
+
|
| 552 |
+
secGrid = uigridlayout(sectionPanel);
|
| 553 |
+
secGrid.ColumnWidth = {'1x'};
|
| 554 |
+
secGrid.RowHeight = {'1x'};
|
| 555 |
+
secGrid.Padding = [2 2 2 2];
|
| 556 |
+
|
| 557 |
+
app.AxSection = uiaxes(secGrid);
|
| 558 |
+
app.AxSection.Layout.Row = 1;
|
| 559 |
+
app.AxSection.Layout.Column = 1;
|
| 560 |
+
|
| 561 |
+
% Strain
|
| 562 |
+
strainPanel = uipanel(rightGrid);
|
| 563 |
+
strainPanel.Layout.Row = 1;
|
| 564 |
+
strainPanel.Layout.Column = 2;
|
| 565 |
+
strainPanel.BackgroundColor = [1 1 1];
|
| 566 |
+
strainPanel.BorderType = 'line';
|
| 567 |
+
|
| 568 |
+
strGrid = uigridlayout(strainPanel);
|
| 569 |
+
strGrid.ColumnWidth = {'1x'};
|
| 570 |
+
strGrid.RowHeight = {'1x'};
|
| 571 |
+
strGrid.Padding = [2 2 2 2];
|
| 572 |
+
|
| 573 |
+
app.AxStrain = uiaxes(strGrid);
|
| 574 |
+
app.AxStrain.Layout.Row = 1;
|
| 575 |
+
app.AxStrain.Layout.Column = 1;
|
| 576 |
+
|
| 577 |
+
% Stress
|
| 578 |
+
stressPanel = uipanel(rightGrid);
|
| 579 |
+
stressPanel.Layout.Row = 1;
|
| 580 |
+
stressPanel.Layout.Column = 3;
|
| 581 |
+
stressPanel.BackgroundColor = [1 1 1];
|
| 582 |
+
stressPanel.BorderType = 'line';
|
| 583 |
+
|
| 584 |
+
stressGrid = uigridlayout(stressPanel);
|
| 585 |
+
stressGrid.ColumnWidth = {'1x'};
|
| 586 |
+
stressGrid.RowHeight = {'1x'};
|
| 587 |
+
stressGrid.Padding = [2 2 2 2];
|
| 588 |
+
|
| 589 |
+
app.AxStress = uiaxes(stressGrid);
|
| 590 |
+
app.AxStress.Layout.Row = 1;
|
| 591 |
+
app.AxStress.Layout.Column = 1;
|
| 592 |
+
|
| 593 |
+
% Results
|
| 594 |
+
app.ResultsPanel = uipanel(rightGrid);
|
| 595 |
+
app.ResultsPanel.Layout.Row = 1;
|
| 596 |
+
app.ResultsPanel.Layout.Column = 4;
|
| 597 |
+
app.ResultsPanel.Title = 'Results';
|
| 598 |
+
app.ResultsPanel.FontWeight = 'bold';
|
| 599 |
+
app.ResultsPanel.BackgroundColor = [0.98 0.98 0.95];
|
| 600 |
+
|
| 601 |
+
resGrid = uigridlayout(app.ResultsPanel);
|
| 602 |
+
resGrid.ColumnWidth = {'1x'};
|
| 603 |
+
resGrid.RowHeight = {'1x'};
|
| 604 |
+
resGrid.Padding = [4 4 4 4];
|
| 605 |
+
|
| 606 |
+
app.ResultsText = uitextarea(resGrid);
|
| 607 |
+
app.ResultsText.Layout.Row = 1;
|
| 608 |
+
app.ResultsText.Layout.Column = 1;
|
| 609 |
+
app.ResultsText.FontName = 'Consolas';
|
| 610 |
+
app.ResultsText.FontSize = 9;
|
| 611 |
+
app.ResultsText.Editable = 'off';
|
| 612 |
+
app.ResultsText.BackgroundColor = [0.98 0.98 0.95];
|
| 613 |
+
|
| 614 |
+
% --- Equations Panel ---
|
| 615 |
+
app.EquationsPanel = uipanel(rightGrid);
|
| 616 |
+
app.EquationsPanel.Layout.Row = 2;
|
| 617 |
+
app.EquationsPanel.Layout.Column = [1 4];
|
| 618 |
+
app.EquationsPanel.Title = 'Calculation Procedure (ACI 318)';
|
| 619 |
+
app.EquationsPanel.FontWeight = 'bold';
|
| 620 |
+
app.EquationsPanel.BackgroundColor = [1 1 1];
|
| 621 |
+
|
| 622 |
+
eqGrid = uigridlayout(app.EquationsPanel);
|
| 623 |
+
eqGrid.ColumnWidth = {'1x'};
|
| 624 |
+
eqGrid.RowHeight = {'1x'};
|
| 625 |
+
eqGrid.Padding = [4 4 4 4];
|
| 626 |
+
|
| 627 |
+
app.AxEquations = uiaxes(eqGrid);
|
| 628 |
+
app.AxEquations.Layout.Row = 1;
|
| 629 |
+
app.AxEquations.Layout.Column = 1;
|
| 630 |
+
app.AxEquations.XTick = [];
|
| 631 |
+
app.AxEquations.YTick = [];
|
| 632 |
+
app.AxEquations.XColor = 'none';
|
| 633 |
+
app.AxEquations.YColor = 'none';
|
| 634 |
+
|
| 635 |
+
% === SET INITIAL VALUES ===
|
| 636 |
+
app.EditFc.Value = 4000;
|
| 637 |
+
app.EditFy.Value = 60000;
|
| 638 |
+
app.EditEs.Value = 29000000;
|
| 639 |
+
app.EditBeta1.Value = 0.85;
|
| 640 |
+
app.EditEpsCu.Value = 0.003;
|
| 641 |
+
app.EditB.Value = 12;
|
| 642 |
+
app.EditH.Value = 20;
|
| 643 |
+
app.EditD.Value = 17.5;
|
| 644 |
+
app.EditBars.Value = 4;
|
| 645 |
+
app.EditBarArea.Value = 0.79;
|
| 646 |
+
|
| 647 |
+
app.UIFigure.Visible = 'on';
|
| 648 |
+
end
|
| 649 |
+
end
|
| 650 |
+
end
|
| 651 |
+
|
README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# RecrBeam Calculator
|
| 2 |
+
|
| 3 |
+
[](https://www.python.org/)
|
| 4 |
+
[](https://opensource.org/licenses/MIT)
|
| 5 |
+
[](https://www.concrete.org/)
|
| 6 |
+
[](https://github.com/TomSchimansky/CustomTkinter)
|
| 7 |
+
|
| 8 |
+
A professional desktop application for calculating the **Nominal Moment Strength (Mn)** of rectangular reinforced concrete beams per ACI 318 provisions.
|
| 9 |
+
|
| 10 |
+
## Features
|
| 11 |
+
|
| 12 |
+
- Native Windows desktop application (no browser/localhost required)
|
| 13 |
+
- Professional dark theme UI
|
| 14 |
+
- Real-time calculations as you type
|
| 15 |
+
- **Unit system toggle**: Imperial (psi, in) / SI (MPa, mm)
|
| 16 |
+
- **Visual diagrams**: Cross Section, Strain Distribution, Stress Block & Forces
|
| 17 |
+
- **Step-by-step equations**: Detailed calculation breakdown with OK/NG indicators
|
| 18 |
+
- Minimum steel area check per ACI 318
|
| 19 |
+
|
| 20 |
+
## Installation
|
| 21 |
+
|
| 22 |
+
### Prerequisites
|
| 23 |
+
|
| 24 |
+
- Python 3.9 or higher
|
| 25 |
+
- pip (Python package manager)
|
| 26 |
+
|
| 27 |
+
### Setup
|
| 28 |
+
|
| 29 |
+
1. **Clone the repository**
|
| 30 |
+
```bash
|
| 31 |
+
git clone https://github.com/algorembrant/RecrBeam-Calculator.git
|
| 32 |
+
cd RecrBeam-Calculator
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
2. **Create a virtual environment** (recommended)
|
| 36 |
+
```bash
|
| 37 |
+
python -m venv .venv
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
3. **Activate the virtual environment**
|
| 41 |
+
|
| 42 |
+
Windows (PowerShell):
|
| 43 |
+
```powershell
|
| 44 |
+
.\.venv\Scripts\Activate.ps1
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Windows (Command Prompt):
|
| 48 |
+
```cmd
|
| 49 |
+
.venv\Scripts\activate.bat
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
macOS/Linux:
|
| 53 |
+
```bash
|
| 54 |
+
source .venv/bin/activate
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
4. **Install dependencies**
|
| 58 |
+
```bash
|
| 59 |
+
pip install -r requirements.txt
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## Usage
|
| 63 |
+
|
| 64 |
+
### Run the Application
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
python app.py
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
The application will launch as a native desktop window.
|
| 71 |
+
|
| 72 |
+
### Input Parameters
|
| 73 |
+
|
| 74 |
+
| Parameter | Description | Imperial | SI |
|
| 75 |
+
|-----------|-------------|----------|-----|
|
| 76 |
+
| fc' | Concrete compressive strength | psi | MPa |
|
| 77 |
+
| fy | Steel yield strength | psi | MPa |
|
| 78 |
+
| Es | Modulus of elasticity | psi | MPa |
|
| 79 |
+
| Beta1 | Stress block factor | - | - |
|
| 80 |
+
| ecu | Ultimate concrete strain | - | - |
|
| 81 |
+
| b | Beam width | in | mm |
|
| 82 |
+
| h | Total beam depth | in | mm |
|
| 83 |
+
| d | Effective depth | in | mm |
|
| 84 |
+
| n | Number of bars | - | - |
|
| 85 |
+
| Ab | Bar area (each) | in2 | mm2 |
|
| 86 |
+
|
| 87 |
+
### Default Values (Example 4-1)
|
| 88 |
+
|
| 89 |
+
**Imperial:**
|
| 90 |
+
- fc' = 4000 psi, fy = 60,000 psi
|
| 91 |
+
- b = 12 in, h = 20 in, d = 17.5 in
|
| 92 |
+
- 4 bars at 0.79 in2 each (No. 8 bars)
|
| 93 |
+
|
| 94 |
+
**SI:**
|
| 95 |
+
- fc' = 20 MPa, fy = 420 MPa
|
| 96 |
+
- b = 250 mm, h = 565 mm, d = 500 mm
|
| 97 |
+
- 3 bars at 510 mm2 each
|
| 98 |
+
|
| 99 |
+
## Project Structure
|
| 100 |
+
|
| 101 |
+
```
|
| 102 |
+
RecrBeam-Calculator/
|
| 103 |
+
├── app.py # Main desktop application (CustomTkinter)
|
| 104 |
+
├── calculator.py # Beam calculation engine
|
| 105 |
+
├── test_calculator.py # Unit tests
|
| 106 |
+
├── requirements.txt # Python dependencies
|
| 107 |
+
├── README.md # This file
|
| 108 |
+
├── LICENSE # MIT License
|
| 109 |
+
├── MATLAB/
|
| 110 |
+
│ └── app.m # Original MATLAB implementation
|
| 111 |
+
└── Dump/
|
| 112 |
+
└── example_4_1.tex # LaTeX documentation
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
## Theory
|
| 116 |
+
|
| 117 |
+
Based on ACI 318 provisions for nominal moment strength:
|
| 118 |
+
|
| 119 |
+
### Key Equations
|
| 120 |
+
|
| 121 |
+
**Stress block depth:**
|
| 122 |
+
```
|
| 123 |
+
a = (As * fy) / (0.85 * fc' * b)
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
**Neutral axis depth:**
|
| 127 |
+
```
|
| 128 |
+
c = a / Beta1
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
**Nominal moment strength:**
|
| 132 |
+
```
|
| 133 |
+
Mn = As * fy * (d - a/2)
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
**Minimum steel area (Imperial):**
|
| 137 |
+
```
|
| 138 |
+
As,min = max(3*sqrt(fc')/fy * b*d, 200/fy * b*d)
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
## Running Tests
|
| 142 |
+
|
| 143 |
+
```bash
|
| 144 |
+
python -m unittest test_calculator -v
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
## Dependencies
|
| 148 |
+
|
| 149 |
+
- [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter) - Modern UI framework
|
| 150 |
+
- [Matplotlib](https://matplotlib.org/) - Diagram plotting
|
| 151 |
+
- [NumPy](https://numpy.org/) - Numerical operations
|
| 152 |
+
|
| 153 |
+
## License
|
| 154 |
+
|
| 155 |
+
MIT License - see [LICENSE](LICENSE)
|
| 156 |
+
|
| 157 |
+
## References
|
| 158 |
+
|
| 159 |
+
- ACI 318-19: Building Code Requirements for Structural Concrete
|
| 160 |
+
- Example 4-1 and 4-1M from ACI 318 Design Handbook
|
| 161 |
+
|
| 162 |
+
## Citation
|
| 163 |
+
|
| 164 |
+
If you use this software in your research or project, please cite it as:
|
| 165 |
+
|
| 166 |
+
```bibtex
|
| 167 |
+
@software{recrbeam_calculator,
|
| 168 |
+
author = {algorembrant},
|
| 169 |
+
title = {RecrBeam Calculator: Nominal Moment Strength Calculator for Rectangular Beams},
|
| 170 |
+
year = {2026},
|
| 171 |
+
publisher = {GitHub},
|
| 172 |
+
url = {https://github.com/algorembrant/RecrBeam-Calculator},
|
| 173 |
+
note = {Based on ACI 318 provisions}
|
| 174 |
+
}
|
| 175 |
+
```
|
STRUCTURE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Project Structure
|
| 2 |
+
|
| 3 |
+
```text
|
| 4 |
+
RBC/
|
| 5 |
+
├── Dump/
|
| 6 |
+
│ ├── example_4_1.tex
|
| 7 |
+
│ ├── note.txt
|
| 8 |
+
│ ├── solution_breakdown.md
|
| 9 |
+
│ └── solution_breakdown.pdf
|
| 10 |
+
├── MATLAB/
|
| 11 |
+
│ └── app.m
|
| 12 |
+
├── .gitignore
|
| 13 |
+
├── app.py
|
| 14 |
+
├── beam_calc.db
|
| 15 |
+
├── calculator.py
|
| 16 |
+
├── db_manager.py
|
| 17 |
+
├── LICENSE
|
| 18 |
+
├── README.md
|
| 19 |
+
├── requirements.txt
|
| 20 |
+
├── TECHSTACK.md
|
| 21 |
+
└── test_calculator.py
|
| 22 |
+
```
|
TECHSTACK.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Techstack
|
| 2 |
+
|
| 3 |
+
Audit of **RBC** project files (excluding environment and cache):
|
| 4 |
+
|
| 5 |
+
| File Type | Count | Size (KB) |
|
| 6 |
+
| :--- | :--- | :--- |
|
| 7 |
+
| Python (.py) | 4 | 34.8 |
|
| 8 |
+
| (no extension) | 2 | 1.5 |
|
| 9 |
+
| Markdown (.md) | 2 | 8.7 |
|
| 10 |
+
| Plain Text (.txt) | 2 | 0.0 |
|
| 11 |
+
| Database (.db) | 1 | 12.0 |
|
| 12 |
+
| LaTeX (.tex) | 1 | 8.1 |
|
| 13 |
+
| Objective-C / MATLAB (.m) | 1 | 27.7 |
|
| 14 |
+
| PDF (.pdf) | 1 | 147.3 |
|
| 15 |
+
| **Total** | **14** | **240.1** |
|
app.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Beam Analysis - Nominal Moment Strength Calculator
|
| 3 |
+
Native Desktop Application using CustomTkinter
|
| 4 |
+
Based on ACI 318 Example 4-1 and 4-1M
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import customtkinter as ctk
|
| 8 |
+
import matplotlib.pyplot as plt
|
| 9 |
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
| 10 |
+
import matplotlib.patches as patches
|
| 11 |
+
import numpy as np
|
| 12 |
+
from calculator import RectangularBeam
|
| 13 |
+
|
| 14 |
+
# Set appearance
|
| 15 |
+
ctk.set_appearance_mode("dark")
|
| 16 |
+
ctk.set_default_color_theme("blue")
|
| 17 |
+
|
| 18 |
+
# Color Scheme
|
| 19 |
+
COLORS = {
|
| 20 |
+
"concrete": "#E0E0DC",
|
| 21 |
+
"outline": "#4D4D4D",
|
| 22 |
+
"compression": "#D98C8C",
|
| 23 |
+
"compression_line": "#B33333",
|
| 24 |
+
"tension": "#336699",
|
| 25 |
+
"steel": "#404050",
|
| 26 |
+
"neutral": "#666666",
|
| 27 |
+
"dimension": "#3380B3",
|
| 28 |
+
"strain": "#B3D9F2",
|
| 29 |
+
"strain_edge": "#336699",
|
| 30 |
+
"moment_arm": "#339933",
|
| 31 |
+
"result": "#1A4D99",
|
| 32 |
+
"ok": "#2ECC71",
|
| 33 |
+
"ng": "#E74C3C",
|
| 34 |
+
"bg_dark": "#1a1a2e",
|
| 35 |
+
"panel_bg": "#16213e",
|
| 36 |
+
"accent": "#0f3460",
|
| 37 |
+
"text": "#eaeaea",
|
| 38 |
+
"text_dim": "#a0a0a0",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class BeamAnalysisApp(ctk.CTk):
|
| 43 |
+
def __init__(self):
|
| 44 |
+
super().__init__()
|
| 45 |
+
|
| 46 |
+
self.title("Beam Analysis - Nominal Moment Strength (ACI 318)")
|
| 47 |
+
self.geometry("1400x850")
|
| 48 |
+
self.minsize(1200, 700)
|
| 49 |
+
|
| 50 |
+
# Configure grid
|
| 51 |
+
self.grid_columnconfigure(0, weight=0, minsize=280)
|
| 52 |
+
self.grid_columnconfigure(1, weight=1)
|
| 53 |
+
self.grid_rowconfigure(0, weight=1)
|
| 54 |
+
|
| 55 |
+
# Variables
|
| 56 |
+
self.unit_system = ctk.StringVar(value="Imperial")
|
| 57 |
+
self.input_vars = {}
|
| 58 |
+
|
| 59 |
+
# Create UI
|
| 60 |
+
self._create_input_panel()
|
| 61 |
+
self._create_main_panel()
|
| 62 |
+
|
| 63 |
+
# Initialize with default values
|
| 64 |
+
self._set_defaults()
|
| 65 |
+
self._update_calculations()
|
| 66 |
+
|
| 67 |
+
def _create_input_panel(self):
|
| 68 |
+
"""Create the left input panel."""
|
| 69 |
+
self.input_frame = ctk.CTkScrollableFrame(
|
| 70 |
+
self,
|
| 71 |
+
width=260,
|
| 72 |
+
corner_radius=0,
|
| 73 |
+
fg_color=COLORS["panel_bg"]
|
| 74 |
+
)
|
| 75 |
+
self.input_frame.grid(row=0, column=0, sticky="nsew", padx=0, pady=0)
|
| 76 |
+
|
| 77 |
+
# Title
|
| 78 |
+
title_label = ctk.CTkLabel(
|
| 79 |
+
self.input_frame,
|
| 80 |
+
text="INPUT PARAMETERS",
|
| 81 |
+
font=ctk.CTkFont(size=16, weight="bold"),
|
| 82 |
+
text_color=COLORS["text"]
|
| 83 |
+
)
|
| 84 |
+
title_label.pack(pady=(20, 15), padx=20)
|
| 85 |
+
|
| 86 |
+
# Unit System Toggle
|
| 87 |
+
unit_frame = ctk.CTkFrame(self.input_frame, fg_color="transparent")
|
| 88 |
+
unit_frame.pack(fill="x", padx=20, pady=(0, 15))
|
| 89 |
+
|
| 90 |
+
ctk.CTkLabel(
|
| 91 |
+
unit_frame,
|
| 92 |
+
text="Unit System",
|
| 93 |
+
font=ctk.CTkFont(size=12, weight="bold"),
|
| 94 |
+
text_color=COLORS["text"]
|
| 95 |
+
).pack(anchor="w")
|
| 96 |
+
|
| 97 |
+
self.unit_toggle = ctk.CTkSegmentedButton(
|
| 98 |
+
unit_frame,
|
| 99 |
+
values=["Imperial", "SI"],
|
| 100 |
+
variable=self.unit_system,
|
| 101 |
+
command=self._on_unit_change,
|
| 102 |
+
font=ctk.CTkFont(size=12)
|
| 103 |
+
)
|
| 104 |
+
self.unit_toggle.pack(fill="x", pady=(5, 0))
|
| 105 |
+
|
| 106 |
+
# Separator
|
| 107 |
+
self._add_separator()
|
| 108 |
+
|
| 109 |
+
# Materials Section
|
| 110 |
+
self._add_section_header("MATERIALS")
|
| 111 |
+
self._create_input_field("fc", "fc' (Concrete)", "psi")
|
| 112 |
+
self._create_input_field("fy", "fy (Steel Yield)", "psi")
|
| 113 |
+
self._create_input_field("Es", "Es (Modulus)", "psi")
|
| 114 |
+
self._create_input_field("beta1", "Beta1", "")
|
| 115 |
+
self._create_input_field("epsilon_cu", "ecu (Ult. Strain)", "")
|
| 116 |
+
|
| 117 |
+
self._add_separator()
|
| 118 |
+
|
| 119 |
+
# Geometry Section
|
| 120 |
+
self._add_section_header("GEOMETRY")
|
| 121 |
+
self._create_input_field("b", "b (Width)", "in")
|
| 122 |
+
self._create_input_field("h", "h (Total Depth)", "in")
|
| 123 |
+
self._create_input_field("d", "d (Eff. Depth)", "in")
|
| 124 |
+
|
| 125 |
+
self._add_separator()
|
| 126 |
+
|
| 127 |
+
# Reinforcement Section
|
| 128 |
+
self._add_section_header("REINFORCEMENT")
|
| 129 |
+
self._create_input_field("n_bars", "Number of Bars", "")
|
| 130 |
+
self._create_input_field("bar_area", "Bar Area (each)", "in2")
|
| 131 |
+
|
| 132 |
+
def _add_separator(self):
|
| 133 |
+
"""Add a subtle separator line."""
|
| 134 |
+
sep = ctk.CTkFrame(self.input_frame, height=1, fg_color=COLORS["accent"])
|
| 135 |
+
sep.pack(fill="x", padx=20, pady=15)
|
| 136 |
+
|
| 137 |
+
def _add_section_header(self, text):
|
| 138 |
+
"""Add a section header."""
|
| 139 |
+
label = ctk.CTkLabel(
|
| 140 |
+
self.input_frame,
|
| 141 |
+
text=text,
|
| 142 |
+
font=ctk.CTkFont(size=11, weight="bold"),
|
| 143 |
+
text_color=COLORS["dimension"]
|
| 144 |
+
)
|
| 145 |
+
label.pack(anchor="w", padx=20, pady=(5, 10))
|
| 146 |
+
|
| 147 |
+
def _create_input_field(self, key, label_text, unit):
|
| 148 |
+
"""Create an input field with label."""
|
| 149 |
+
frame = ctk.CTkFrame(self.input_frame, fg_color="transparent")
|
| 150 |
+
frame.pack(fill="x", padx=20, pady=3)
|
| 151 |
+
|
| 152 |
+
# Label
|
| 153 |
+
display_label = f"{label_text}" if not unit else f"{label_text} [{unit}]"
|
| 154 |
+
label = ctk.CTkLabel(
|
| 155 |
+
frame,
|
| 156 |
+
text=display_label,
|
| 157 |
+
font=ctk.CTkFont(size=11),
|
| 158 |
+
text_color=COLORS["text_dim"]
|
| 159 |
+
)
|
| 160 |
+
label.pack(anchor="w")
|
| 161 |
+
|
| 162 |
+
# Store label reference for unit updates
|
| 163 |
+
if not hasattr(self, '_unit_labels'):
|
| 164 |
+
self._unit_labels = {}
|
| 165 |
+
self._unit_labels[key] = (label, label_text, unit)
|
| 166 |
+
|
| 167 |
+
# Entry
|
| 168 |
+
var = ctk.StringVar()
|
| 169 |
+
self.input_vars[key] = var
|
| 170 |
+
|
| 171 |
+
entry = ctk.CTkEntry(
|
| 172 |
+
frame,
|
| 173 |
+
textvariable=var,
|
| 174 |
+
font=ctk.CTkFont(size=12),
|
| 175 |
+
height=32,
|
| 176 |
+
corner_radius=6
|
| 177 |
+
)
|
| 178 |
+
entry.pack(fill="x", pady=(2, 0))
|
| 179 |
+
entry.bind("<KeyRelease>", lambda e: self._update_calculations())
|
| 180 |
+
entry.bind("<FocusOut>", lambda e: self._update_calculations())
|
| 181 |
+
|
| 182 |
+
def _create_main_panel(self):
|
| 183 |
+
"""Create the main content area."""
|
| 184 |
+
self.main_frame = ctk.CTkFrame(self, fg_color=COLORS["bg_dark"], corner_radius=0)
|
| 185 |
+
self.main_frame.grid(row=0, column=1, sticky="nsew", padx=0, pady=0)
|
| 186 |
+
|
| 187 |
+
self.main_frame.grid_columnconfigure((0, 1, 2), weight=1)
|
| 188 |
+
self.main_frame.grid_columnconfigure(3, weight=0, minsize=200)
|
| 189 |
+
self.main_frame.grid_rowconfigure(0, weight=3)
|
| 190 |
+
self.main_frame.grid_rowconfigure(1, weight=2)
|
| 191 |
+
|
| 192 |
+
# Create diagram frames
|
| 193 |
+
self._create_diagram_frame("Cross Section", 0)
|
| 194 |
+
self._create_diagram_frame("Strain Distribution", 1)
|
| 195 |
+
self._create_diagram_frame("Stress Block & Forces", 2)
|
| 196 |
+
self._create_results_panel()
|
| 197 |
+
self._create_equations_panel()
|
| 198 |
+
|
| 199 |
+
def _create_diagram_frame(self, title, col):
|
| 200 |
+
"""Create a diagram frame with matplotlib canvas."""
|
| 201 |
+
frame = ctk.CTkFrame(self.main_frame, fg_color=COLORS["panel_bg"], corner_radius=8)
|
| 202 |
+
frame.grid(row=0, column=col, sticky="nsew", padx=5, pady=5)
|
| 203 |
+
|
| 204 |
+
# Title
|
| 205 |
+
label = ctk.CTkLabel(
|
| 206 |
+
frame,
|
| 207 |
+
text=title,
|
| 208 |
+
font=ctk.CTkFont(size=12, weight="bold"),
|
| 209 |
+
text_color=COLORS["text"]
|
| 210 |
+
)
|
| 211 |
+
label.pack(pady=(10, 5))
|
| 212 |
+
|
| 213 |
+
# Figure
|
| 214 |
+
fig, ax = plt.subplots(figsize=(4, 3.5), facecolor=COLORS["bg_dark"])
|
| 215 |
+
ax.set_facecolor(COLORS["bg_dark"])
|
| 216 |
+
|
| 217 |
+
canvas = FigureCanvasTkAgg(fig, frame)
|
| 218 |
+
canvas.get_tk_widget().pack(fill="both", expand=True, padx=5, pady=5)
|
| 219 |
+
|
| 220 |
+
# Store references
|
| 221 |
+
if col == 0:
|
| 222 |
+
self.fig_section, self.ax_section, self.canvas_section = fig, ax, canvas
|
| 223 |
+
elif col == 1:
|
| 224 |
+
self.fig_strain, self.ax_strain, self.canvas_strain = fig, ax, canvas
|
| 225 |
+
else:
|
| 226 |
+
self.fig_stress, self.ax_stress, self.canvas_stress = fig, ax, canvas
|
| 227 |
+
|
| 228 |
+
def _create_results_panel(self):
|
| 229 |
+
"""Create the results summary panel."""
|
| 230 |
+
frame = ctk.CTkFrame(self.main_frame, fg_color=COLORS["panel_bg"], corner_radius=8)
|
| 231 |
+
frame.grid(row=0, column=3, sticky="nsew", padx=5, pady=5)
|
| 232 |
+
|
| 233 |
+
# Title
|
| 234 |
+
ctk.CTkLabel(
|
| 235 |
+
frame,
|
| 236 |
+
text="Results",
|
| 237 |
+
font=ctk.CTkFont(size=12, weight="bold"),
|
| 238 |
+
text_color=COLORS["text"]
|
| 239 |
+
).pack(pady=(10, 5))
|
| 240 |
+
|
| 241 |
+
# Results text
|
| 242 |
+
self.results_text = ctk.CTkTextbox(
|
| 243 |
+
frame,
|
| 244 |
+
font=ctk.CTkFont(family="Consolas", size=10),
|
| 245 |
+
fg_color=COLORS["bg_dark"],
|
| 246 |
+
text_color=COLORS["text"],
|
| 247 |
+
corner_radius=6,
|
| 248 |
+
wrap="word"
|
| 249 |
+
)
|
| 250 |
+
self.results_text.pack(fill="both", expand=True, padx=8, pady=8)
|
| 251 |
+
|
| 252 |
+
def _create_equations_panel(self):
|
| 253 |
+
"""Create the equations panel."""
|
| 254 |
+
frame = ctk.CTkFrame(self.main_frame, fg_color=COLORS["panel_bg"], corner_radius=8)
|
| 255 |
+
frame.grid(row=1, column=0, columnspan=4, sticky="nsew", padx=5, pady=5)
|
| 256 |
+
|
| 257 |
+
# Title
|
| 258 |
+
ctk.CTkLabel(
|
| 259 |
+
frame,
|
| 260 |
+
text="Calculation Procedure (ACI 318)",
|
| 261 |
+
font=ctk.CTkFont(size=12, weight="bold"),
|
| 262 |
+
text_color=COLORS["text"]
|
| 263 |
+
).pack(pady=(10, 5), anchor="w", padx=15)
|
| 264 |
+
|
| 265 |
+
# Equations text
|
| 266 |
+
self.equations_text = ctk.CTkTextbox(
|
| 267 |
+
frame,
|
| 268 |
+
font=ctk.CTkFont(family="Consolas", size=11),
|
| 269 |
+
fg_color=COLORS["bg_dark"],
|
| 270 |
+
text_color=COLORS["text"],
|
| 271 |
+
corner_radius=6,
|
| 272 |
+
height=150
|
| 273 |
+
)
|
| 274 |
+
self.equations_text.pack(fill="both", expand=True, padx=10, pady=(5, 10))
|
| 275 |
+
|
| 276 |
+
def _set_defaults(self):
|
| 277 |
+
"""Set default values based on unit system."""
|
| 278 |
+
if self.unit_system.get() == "Imperial":
|
| 279 |
+
defaults = {
|
| 280 |
+
"fc": "4000", "fy": "60000", "Es": "29000000",
|
| 281 |
+
"beta1": "0.85", "epsilon_cu": "0.003",
|
| 282 |
+
"b": "12", "h": "20", "d": "17.5",
|
| 283 |
+
"n_bars": "4", "bar_area": "0.79"
|
| 284 |
+
}
|
| 285 |
+
units = {"fc": "psi", "fy": "psi", "Es": "psi", "b": "in", "h": "in", "d": "in", "bar_area": "in2"}
|
| 286 |
+
else:
|
| 287 |
+
defaults = {
|
| 288 |
+
"fc": "20", "fy": "420", "Es": "200000",
|
| 289 |
+
"beta1": "0.85", "epsilon_cu": "0.003",
|
| 290 |
+
"b": "250", "h": "565", "d": "500",
|
| 291 |
+
"n_bars": "3", "bar_area": "510"
|
| 292 |
+
}
|
| 293 |
+
units = {"fc": "MPa", "fy": "MPa", "Es": "MPa", "b": "mm", "h": "mm", "d": "mm", "bar_area": "mm2"}
|
| 294 |
+
|
| 295 |
+
for key, val in defaults.items():
|
| 296 |
+
self.input_vars[key].set(val)
|
| 297 |
+
|
| 298 |
+
# Update labels
|
| 299 |
+
for key, (label, text, _) in self._unit_labels.items():
|
| 300 |
+
unit = units.get(key, "")
|
| 301 |
+
display = f"{text}" if not unit else f"{text} [{unit}]"
|
| 302 |
+
label.configure(text=display)
|
| 303 |
+
|
| 304 |
+
def _on_unit_change(self, value):
|
| 305 |
+
"""Handle unit system change."""
|
| 306 |
+
self._set_defaults()
|
| 307 |
+
self._update_calculations()
|
| 308 |
+
|
| 309 |
+
def _get_input_value(self, key, default=0.0):
|
| 310 |
+
"""Get input value as float."""
|
| 311 |
+
try:
|
| 312 |
+
return float(self.input_vars[key].get())
|
| 313 |
+
except (ValueError, KeyError):
|
| 314 |
+
return default
|
| 315 |
+
|
| 316 |
+
def _update_calculations(self):
|
| 317 |
+
"""Update all calculations and displays."""
|
| 318 |
+
try:
|
| 319 |
+
# Get values
|
| 320 |
+
fc = self._get_input_value("fc", 4000)
|
| 321 |
+
fy = self._get_input_value("fy", 60000)
|
| 322 |
+
Es = self._get_input_value("Es", 29000000)
|
| 323 |
+
beta1 = self._get_input_value("beta1", 0.85)
|
| 324 |
+
epsilon_cu = self._get_input_value("epsilon_cu", 0.003)
|
| 325 |
+
b = self._get_input_value("b", 12)
|
| 326 |
+
h = self._get_input_value("h", 20)
|
| 327 |
+
d = self._get_input_value("d", 17.5)
|
| 328 |
+
n_bars = int(self._get_input_value("n_bars", 4))
|
| 329 |
+
bar_area = self._get_input_value("bar_area", 0.79)
|
| 330 |
+
|
| 331 |
+
if any(v <= 0 for v in [fc, fy, Es, b, h, d, n_bars, bar_area]):
|
| 332 |
+
return
|
| 333 |
+
|
| 334 |
+
# Create beam and calculate
|
| 335 |
+
beam = RectangularBeam(
|
| 336 |
+
b=b, h=h, d=d, fc=fc, fy=fy,
|
| 337 |
+
n_bars=n_bars, bar_area=bar_area,
|
| 338 |
+
Es=Es, beta1=beta1, epsilon_cu=epsilon_cu,
|
| 339 |
+
unit_system=self.unit_system.get().lower()
|
| 340 |
+
)
|
| 341 |
+
results = beam.calculate_mn()
|
| 342 |
+
units = beam.get_units()
|
| 343 |
+
|
| 344 |
+
# Update diagrams
|
| 345 |
+
self._draw_cross_section(b, h, d, results["a"], results["c"], n_bars, bar_area)
|
| 346 |
+
self._draw_strain_diagram(h, d, results["c"], epsilon_cu, results["epsilon_s"])
|
| 347 |
+
self._draw_stress_diagram(h, d, results["a"], results["c"], results["T_display"], units)
|
| 348 |
+
|
| 349 |
+
# Update results
|
| 350 |
+
self._update_results_text(results, units)
|
| 351 |
+
self._update_equations_text(results, units, n_bars, bar_area, fc, fy, Es, beta1, epsilon_cu, b, d)
|
| 352 |
+
|
| 353 |
+
except Exception as e:
|
| 354 |
+
pass # Silently handle errors during typing
|
| 355 |
+
|
| 356 |
+
def _draw_cross_section(self, b, h, d, a, c, n_bars, bar_area):
|
| 357 |
+
"""Draw the beam cross section."""
|
| 358 |
+
ax = self.ax_section
|
| 359 |
+
ax.clear()
|
| 360 |
+
ax.set_facecolor(COLORS["bg_dark"])
|
| 361 |
+
|
| 362 |
+
# Concrete beam
|
| 363 |
+
rect = patches.Rectangle((0, 0), b, h, facecolor=COLORS["concrete"],
|
| 364 |
+
edgecolor=COLORS["outline"], linewidth=1.5)
|
| 365 |
+
ax.add_patch(rect)
|
| 366 |
+
|
| 367 |
+
# Compression zone
|
| 368 |
+
comp = patches.Rectangle((0, h - a), b, a, facecolor=COLORS["compression"],
|
| 369 |
+
edgecolor="none", alpha=0.6)
|
| 370 |
+
ax.add_patch(comp)
|
| 371 |
+
|
| 372 |
+
# Neutral axis
|
| 373 |
+
ax.plot([0, b], [h - c, h - c], '--', color=COLORS["neutral"], linewidth=1.2)
|
| 374 |
+
|
| 375 |
+
# Steel bars
|
| 376 |
+
bar_r = np.sqrt(bar_area / np.pi) * 0.7
|
| 377 |
+
steel_y = h - d
|
| 378 |
+
cx = [b / 2] if n_bars == 1 else np.linspace(b * 0.12, b * 0.88, n_bars)
|
| 379 |
+
|
| 380 |
+
for x in cx:
|
| 381 |
+
circle = patches.Circle((x, steel_y), bar_r, facecolor=COLORS["steel"],
|
| 382 |
+
edgecolor="#1A1A1A", linewidth=0.5)
|
| 383 |
+
ax.add_patch(circle)
|
| 384 |
+
|
| 385 |
+
# Dimensions
|
| 386 |
+
ax.text(b / 2, -h * 0.06, f'b={b:.1f}', ha='center', fontsize=8, color=COLORS["text"])
|
| 387 |
+
ax.text(b + b * 0.1, h / 2, f'h={h:.1f}', fontsize=8, color=COLORS["text"])
|
| 388 |
+
ax.text(-b * 0.12, h - a / 2, f'a={a:.2f}', fontsize=8, color=COLORS["compression_line"])
|
| 389 |
+
ax.text(b + b * 0.04, h - c, f'c={c:.2f}', fontsize=7, color=COLORS["neutral"])
|
| 390 |
+
|
| 391 |
+
ax.set_xlim(-b * 0.2, b * 1.3)
|
| 392 |
+
ax.set_ylim(-h * 0.1, h * 1.05)
|
| 393 |
+
ax.set_aspect('equal')
|
| 394 |
+
ax.axis('off')
|
| 395 |
+
|
| 396 |
+
self.canvas_section.draw()
|
| 397 |
+
|
| 398 |
+
def _draw_strain_diagram(self, h, d, c, epsilon_cu, epsilon_s):
|
| 399 |
+
"""Draw the strain distribution."""
|
| 400 |
+
ax = self.ax_strain
|
| 401 |
+
ax.clear()
|
| 402 |
+
ax.set_facecolor(COLORS["bg_dark"])
|
| 403 |
+
|
| 404 |
+
steel_y = h - d
|
| 405 |
+
strain_w = 0.4
|
| 406 |
+
|
| 407 |
+
# Beam outline
|
| 408 |
+
ax.plot([0, 0], [0, h], color='#555', linewidth=1)
|
| 409 |
+
|
| 410 |
+
# Strain profile
|
| 411 |
+
x_top = epsilon_cu * strain_w / 0.003
|
| 412 |
+
x_bot = epsilon_s * strain_w / 0.003
|
| 413 |
+
|
| 414 |
+
ax.fill([0, x_top, x_bot, 0], [h, h, steel_y, steel_y],
|
| 415 |
+
facecolor=COLORS["strain"], edgecolor=COLORS["strain_edge"],
|
| 416 |
+
linewidth=1.2, alpha=0.5)
|
| 417 |
+
|
| 418 |
+
# Neutral axis
|
| 419 |
+
ax.plot([-0.05, strain_w * 1.2], [h - c, h - c], '--', color=COLORS["neutral"], linewidth=1)
|
| 420 |
+
ax.text(-0.03, h - c, f'c={c:.2f}', fontsize=7, ha='right', color=COLORS["neutral"])
|
| 421 |
+
|
| 422 |
+
# Labels
|
| 423 |
+
ax.text(x_top + 0.02, h, f'ecu={epsilon_cu:.4f}', fontsize=8, color=COLORS["text"])
|
| 424 |
+
ax.text(x_bot + 0.02, steel_y, f'es={epsilon_s:.5f}', fontsize=8, color=COLORS["text"])
|
| 425 |
+
|
| 426 |
+
ax.set_xlim(-0.1, strain_w * 1.5)
|
| 427 |
+
ax.set_ylim(-h * 0.1, h * 1.05)
|
| 428 |
+
ax.axis('off')
|
| 429 |
+
|
| 430 |
+
self.canvas_strain.draw()
|
| 431 |
+
|
| 432 |
+
def _draw_stress_diagram(self, h, d, a, c, T_display, units):
|
| 433 |
+
"""Draw the stress block and forces."""
|
| 434 |
+
ax = self.ax_stress
|
| 435 |
+
ax.clear()
|
| 436 |
+
ax.set_facecolor(COLORS["bg_dark"])
|
| 437 |
+
|
| 438 |
+
steel_y = h - d
|
| 439 |
+
stress_w = 0.5
|
| 440 |
+
|
| 441 |
+
# Compression block
|
| 442 |
+
comp = patches.Rectangle((0, h - a), stress_w, a, facecolor=COLORS["compression"],
|
| 443 |
+
edgecolor=COLORS["compression_line"], linewidth=1.2, alpha=0.7)
|
| 444 |
+
ax.add_patch(comp)
|
| 445 |
+
ax.text(stress_w / 2, h - a / 2, "0.85fc'", ha='center', fontsize=8, color=COLORS["text"])
|
| 446 |
+
|
| 447 |
+
# Force arrows
|
| 448 |
+
ax.annotate('', xy=(stress_w + 0.2, h - a / 2), xytext=(stress_w + 0.05, h - a / 2),
|
| 449 |
+
arrowprops=dict(arrowstyle='->', color=COLORS["compression_line"], lw=2))
|
| 450 |
+
ax.text(stress_w + 0.22, h - a / 2, f'C={T_display:.0f} {units["force_k"]}',
|
| 451 |
+
fontsize=8, color=COLORS["compression_line"])
|
| 452 |
+
|
| 453 |
+
# Steel and tension
|
| 454 |
+
ax.plot([0, stress_w * 0.3], [steel_y, steel_y], color=COLORS["steel"], linewidth=2)
|
| 455 |
+
ax.annotate('', xy=(0.2, steel_y), xytext=(0, steel_y),
|
| 456 |
+
arrowprops=dict(arrowstyle='->', color=COLORS["tension"], lw=2))
|
| 457 |
+
ax.text(0.22, steel_y, f'T={T_display:.0f} {units["force_k"]}',
|
| 458 |
+
fontsize=8, color=COLORS["tension"])
|
| 459 |
+
|
| 460 |
+
# Neutral axis
|
| 461 |
+
ax.plot([-0.05, stress_w + 0.25], [h - c, h - c], '--', color=COLORS["neutral"], linewidth=1)
|
| 462 |
+
|
| 463 |
+
# Moment arm
|
| 464 |
+
xa = stress_w + 0.35
|
| 465 |
+
ax.plot([xa, xa], [h - a / 2, steel_y], color=COLORS["moment_arm"], linewidth=1.5)
|
| 466 |
+
ax.text(xa + 0.02, (h - a / 2 + steel_y) / 2, 'd-a/2', fontsize=7, color=COLORS["moment_arm"])
|
| 467 |
+
|
| 468 |
+
ax.set_xlim(-0.1, stress_w + 0.55)
|
| 469 |
+
ax.set_ylim(-h * 0.1, h * 1.05)
|
| 470 |
+
ax.set_aspect('equal')
|
| 471 |
+
ax.axis('off')
|
| 472 |
+
|
| 473 |
+
self.canvas_stress.draw()
|
| 474 |
+
|
| 475 |
+
def _update_results_text(self, results, units):
|
| 476 |
+
"""Update the results text panel."""
|
| 477 |
+
yield_str = "Yes (Yields)" if results["yield_check"] else "No (Elastic)"
|
| 478 |
+
as_str = "OK" if results["as_check"] else "NOT OK"
|
| 479 |
+
|
| 480 |
+
text = f"""RESULTS SUMMARY
|
| 481 |
+
================
|
| 482 |
+
|
| 483 |
+
Steel Area:
|
| 484 |
+
As = {results['As']:.4f} {units['area']}
|
| 485 |
+
|
| 486 |
+
Forces:
|
| 487 |
+
T = C = {results['T_display']:.2f} {units['force_k']}
|
| 488 |
+
|
| 489 |
+
Geometry:
|
| 490 |
+
a = {results['a']:.4f} {units['length']}
|
| 491 |
+
c = {results['c']:.4f} {units['length']}
|
| 492 |
+
|
| 493 |
+
Strain Check:
|
| 494 |
+
ey = {results['epsilon_y']:.6f}
|
| 495 |
+
es = {results['epsilon_s']:.6f}
|
| 496 |
+
Yield: {yield_str}
|
| 497 |
+
|
| 498 |
+
NOMINAL MOMENT:
|
| 499 |
+
Mn = {results['Mn_display']:.1f} {units['moment_display']}
|
| 500 |
+
|
| 501 |
+
Min Steel Check:
|
| 502 |
+
As,min = {results['As_min']:.4f} {units['area']}
|
| 503 |
+
Status: {as_str}
|
| 504 |
+
"""
|
| 505 |
+
self.results_text.delete("1.0", "end")
|
| 506 |
+
self.results_text.insert("1.0", text)
|
| 507 |
+
|
| 508 |
+
def _update_equations_text(self, results, units, n_bars, bar_area, fc, fy, Es, beta1, epsilon_cu, b, d):
|
| 509 |
+
"""Update the equations panel."""
|
| 510 |
+
yield_ok = "[OK]" if results["yield_check"] else "[NG]"
|
| 511 |
+
as_ok = "[OK]" if results["as_check"] else "[NG]"
|
| 512 |
+
|
| 513 |
+
text = f"""STEP-BY-STEP CALCULATIONS
|
| 514 |
+
|
| 515 |
+
Step 1: Steel Area and Tension Force
|
| 516 |
+
As = n x A_bar = {n_bars} x {bar_area:.3f} = {results['As']:.3f} {units['area']}
|
| 517 |
+
T = As x fy = {results['As']:.3f} x {fy:.0f} = {results['T']:.0f} {units['force']} ({results['T_display']:.1f} {units['force_k']})
|
| 518 |
+
|
| 519 |
+
Step 2: Stress Block Depth
|
| 520 |
+
a = (As x fy) / (0.85 x fc' x b) = {results['T']:.0f} / (0.85 x {fc:.0f} x {b:.1f}) = {results['a']:.4f} {units['length']}
|
| 521 |
+
c = a / beta1 = {results['a']:.4f} / {beta1:.3f} = {results['c']:.4f} {units['length']}
|
| 522 |
+
|
| 523 |
+
Step 3: Strain Check {yield_ok}
|
| 524 |
+
ey = fy / Es = {fy:.0f} / {Es:.0f} = {results['epsilon_y']:.6f}
|
| 525 |
+
es = ((d - c) / c) x ecu = (({d:.2f} - {results['c']:.2f}) / {results['c']:.2f}) x {epsilon_cu:.4f} = {results['epsilon_s']:.6f}
|
| 526 |
+
|
| 527 |
+
Step 4: Nominal Moment
|
| 528 |
+
Mn = As x fy x (d - a/2) = {results['T']:.0f} x ({d:.2f} - {results['a']:.4f}/2) = {results['Mn_k']:.0f} {units['moment_k']}
|
| 529 |
+
|
| 530 |
+
>>> Mn = {results['Mn_display']:.1f} {units['moment_display']} <<<
|
| 531 |
+
|
| 532 |
+
Step 5: Minimum Steel Check {as_ok}
|
| 533 |
+
As,min = {results['As_min']:.4f} {units['area']}
|
| 534 |
+
As {'>' if results['as_check'] else '<'} As,min
|
| 535 |
+
"""
|
| 536 |
+
self.equations_text.delete("1.0", "end")
|
| 537 |
+
self.equations_text.insert("1.0", text)
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
if __name__ == "__main__":
|
| 541 |
+
app = BeamAnalysisApp()
|
| 542 |
+
app.mainloop()
|
beam_calc.db
ADDED
|
Binary file (12.3 kB). View file
|
|
|
calculator.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rectangular Beam Nominal Moment Strength Calculator
|
| 3 |
+
Based on ACI 318 Provisions (Example 4-1 and 4-1M)
|
| 4 |
+
|
| 5 |
+
Variables:
|
| 6 |
+
fc' - Concrete compressive strength
|
| 7 |
+
fy - Steel yield strength
|
| 8 |
+
Es - Modulus of elasticity of steel
|
| 9 |
+
b - Beam width
|
| 10 |
+
h - Total beam depth
|
| 11 |
+
d - Effective depth (to centroid of tension steel)
|
| 12 |
+
As - Total area of tension steel
|
| 13 |
+
beta1 - Stress block factor
|
| 14 |
+
epsilon_cu - Ultimate concrete strain (0.003)
|
| 15 |
+
|
| 16 |
+
Calculated:
|
| 17 |
+
T - Tension force in steel
|
| 18 |
+
a - Depth of equivalent stress block
|
| 19 |
+
c - Neutral axis depth
|
| 20 |
+
epsilon_y - Yield strain of steel
|
| 21 |
+
epsilon_s - Strain in steel at ultimate
|
| 22 |
+
Mn - Nominal moment strength
|
| 23 |
+
As_min - Minimum steel area per ACI
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import math
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class RectangularBeam:
|
| 30 |
+
def __init__(
|
| 31 |
+
self,
|
| 32 |
+
b: float,
|
| 33 |
+
h: float,
|
| 34 |
+
d: float,
|
| 35 |
+
fc: float,
|
| 36 |
+
fy: float,
|
| 37 |
+
n_bars: int,
|
| 38 |
+
bar_area: float,
|
| 39 |
+
Es: float = None,
|
| 40 |
+
beta1: float = None,
|
| 41 |
+
epsilon_cu: float = 0.003,
|
| 42 |
+
unit_system: str = "imperial"
|
| 43 |
+
):
|
| 44 |
+
"""
|
| 45 |
+
Initialize the Rectangular Beam.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
b: Beam width (in or mm)
|
| 49 |
+
h: Total beam depth (in or mm)
|
| 50 |
+
d: Effective depth (in or mm)
|
| 51 |
+
fc: Concrete compressive strength (psi or MPa)
|
| 52 |
+
fy: Steel yield strength (psi or MPa)
|
| 53 |
+
n_bars: Number of reinforcement bars
|
| 54 |
+
bar_area: Area of each bar (in2 or mm2)
|
| 55 |
+
Es: Modulus of elasticity of steel (psi or MPa). Default based on unit system.
|
| 56 |
+
beta1: Stress block factor. Default calculated from fc.
|
| 57 |
+
epsilon_cu: Ultimate concrete strain. Default 0.003.
|
| 58 |
+
unit_system: 'imperial' (psi, in) or 'si' (MPa, mm)
|
| 59 |
+
"""
|
| 60 |
+
self.b = b
|
| 61 |
+
self.h = h
|
| 62 |
+
self.d = d
|
| 63 |
+
self.fc = fc
|
| 64 |
+
self.fy = fy
|
| 65 |
+
self.n_bars = n_bars
|
| 66 |
+
self.bar_area = bar_area
|
| 67 |
+
self.As = n_bars * bar_area
|
| 68 |
+
self.epsilon_cu = epsilon_cu
|
| 69 |
+
self.unit_system = unit_system.lower()
|
| 70 |
+
|
| 71 |
+
# Set Es default based on unit system
|
| 72 |
+
if Es is None:
|
| 73 |
+
self.Es = 29000000 if self.unit_system == "imperial" else 200000
|
| 74 |
+
else:
|
| 75 |
+
self.Es = Es
|
| 76 |
+
|
| 77 |
+
# Set beta1 - calculate if not provided
|
| 78 |
+
if beta1 is None:
|
| 79 |
+
self.beta1 = self._calculate_beta1()
|
| 80 |
+
else:
|
| 81 |
+
self.beta1 = beta1
|
| 82 |
+
|
| 83 |
+
def _calculate_beta1(self) -> float:
|
| 84 |
+
"""Calculates beta1 based on fc per ACI 318."""
|
| 85 |
+
if self.unit_system == "imperial":
|
| 86 |
+
# fc in psi
|
| 87 |
+
if self.fc <= 4000:
|
| 88 |
+
return 0.85
|
| 89 |
+
elif self.fc >= 8000:
|
| 90 |
+
return 0.65
|
| 91 |
+
else:
|
| 92 |
+
return 0.85 - 0.05 * (self.fc - 4000) / 1000
|
| 93 |
+
else:
|
| 94 |
+
# fc in MPa
|
| 95 |
+
if self.fc <= 28:
|
| 96 |
+
return 0.85
|
| 97 |
+
elif self.fc >= 55:
|
| 98 |
+
return 0.65
|
| 99 |
+
else:
|
| 100 |
+
return 0.85 - 0.05 * (self.fc - 28) / 7
|
| 101 |
+
|
| 102 |
+
def calculate_as_min(self) -> float:
|
| 103 |
+
"""
|
| 104 |
+
Calculate minimum steel area per ACI 318.
|
| 105 |
+
|
| 106 |
+
Imperial: As_min = max(3*sqrt(fc)/fy * b*d, 200/fy * b*d)
|
| 107 |
+
SI: As_min = max(0.25*sqrt(fc)/fy * b*d, 1.4/fy * b*d)
|
| 108 |
+
"""
|
| 109 |
+
if self.unit_system == "imperial":
|
| 110 |
+
term1 = (3 * math.sqrt(self.fc) / self.fy) * self.b * self.d
|
| 111 |
+
term2 = (200 / self.fy) * self.b * self.d
|
| 112 |
+
else:
|
| 113 |
+
term1 = (0.25 * math.sqrt(self.fc) / self.fy) * self.b * self.d
|
| 114 |
+
term2 = (1.4 / self.fy) * self.b * self.d
|
| 115 |
+
return max(term1, term2)
|
| 116 |
+
|
| 117 |
+
def calculate_mn(self) -> dict:
|
| 118 |
+
"""
|
| 119 |
+
Calculates Nominal Moment Capacity (Mn) and related values.
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
dict with all calculated values including:
|
| 123 |
+
- T: Tension force
|
| 124 |
+
- a: Depth of stress block
|
| 125 |
+
- c: Neutral axis depth
|
| 126 |
+
- epsilon_y: Yield strain
|
| 127 |
+
- epsilon_s: Steel strain at ultimate
|
| 128 |
+
- yield_check: Whether steel yields
|
| 129 |
+
- fs: Steel stress
|
| 130 |
+
- Mn: Nominal moment
|
| 131 |
+
- Mn_display: Moment in display units (k-ft or kN-m)
|
| 132 |
+
- As_min: Minimum steel area
|
| 133 |
+
- as_check: Whether As >= As_min
|
| 134 |
+
- phi: Strength reduction factor
|
| 135 |
+
- Mu: Design moment capacity
|
| 136 |
+
"""
|
| 137 |
+
# Tension force
|
| 138 |
+
T = self.As * self.fy
|
| 139 |
+
|
| 140 |
+
# Stress block depth
|
| 141 |
+
a = (self.As * self.fy) / (0.85 * self.fc * self.b)
|
| 142 |
+
|
| 143 |
+
# Neutral axis depth
|
| 144 |
+
c = a / self.beta1
|
| 145 |
+
|
| 146 |
+
# Strains
|
| 147 |
+
epsilon_y = self.fy / self.Es
|
| 148 |
+
epsilon_s = self.epsilon_cu * (self.d - c) / c
|
| 149 |
+
|
| 150 |
+
# Check if steel yields
|
| 151 |
+
yield_check = epsilon_s >= epsilon_y
|
| 152 |
+
fs = self.fy if yield_check else epsilon_s * self.Es
|
| 153 |
+
|
| 154 |
+
# Nominal moment
|
| 155 |
+
Mn = self.As * fs * (self.d - a / 2)
|
| 156 |
+
|
| 157 |
+
# Convert to display units
|
| 158 |
+
if self.unit_system == "imperial":
|
| 159 |
+
Mn_display = Mn / 12000 # lb-in to k-ft
|
| 160 |
+
T_display = T / 1000 # lb to kips
|
| 161 |
+
Mn_k = Mn / 1000 # lb-in to k-in
|
| 162 |
+
else:
|
| 163 |
+
Mn_display = Mn / 1e6 # N-mm to kN-m
|
| 164 |
+
T_display = T / 1000 # N to kN
|
| 165 |
+
Mn_k = Mn # N-mm
|
| 166 |
+
|
| 167 |
+
# Minimum steel check
|
| 168 |
+
As_min = self.calculate_as_min()
|
| 169 |
+
as_check = self.As >= As_min
|
| 170 |
+
|
| 171 |
+
# Phi factor (ACI 318)
|
| 172 |
+
epsilon_t = epsilon_s # For tension-controlled check
|
| 173 |
+
if epsilon_t >= 0.005:
|
| 174 |
+
phi = 0.9
|
| 175 |
+
elif epsilon_t <= 0.002:
|
| 176 |
+
phi = 0.65
|
| 177 |
+
else:
|
| 178 |
+
phi = 0.65 + 0.25 * (epsilon_t - 0.002) / 0.003
|
| 179 |
+
|
| 180 |
+
Mu = phi * Mn
|
| 181 |
+
if self.unit_system == "imperial":
|
| 182 |
+
Mu_display = Mu / 12000
|
| 183 |
+
else:
|
| 184 |
+
Mu_display = Mu / 1e6
|
| 185 |
+
|
| 186 |
+
return {
|
| 187 |
+
"T": T,
|
| 188 |
+
"T_display": T_display,
|
| 189 |
+
"a": a,
|
| 190 |
+
"c": c,
|
| 191 |
+
"epsilon_y": epsilon_y,
|
| 192 |
+
"epsilon_s": epsilon_s,
|
| 193 |
+
"yield_check": yield_check,
|
| 194 |
+
"fs": fs,
|
| 195 |
+
"Mn": Mn,
|
| 196 |
+
"Mn_k": Mn_k,
|
| 197 |
+
"Mn_display": Mn_display,
|
| 198 |
+
"As": self.As,
|
| 199 |
+
"As_min": As_min,
|
| 200 |
+
"as_check": as_check,
|
| 201 |
+
"epsilon_t": epsilon_t,
|
| 202 |
+
"phi": phi,
|
| 203 |
+
"Mu": Mu,
|
| 204 |
+
"Mu_display": Mu_display,
|
| 205 |
+
# Legacy compatibility
|
| 206 |
+
"Mn_kft": Mn_display if self.unit_system == "imperial" else None,
|
| 207 |
+
"Mn_kin": Mn_k / 1000 if self.unit_system == "imperial" else None,
|
| 208 |
+
"Mu_kft": Mu_display if self.unit_system == "imperial" else None,
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
def get_units(self) -> dict:
|
| 212 |
+
"""Get unit labels based on current unit system."""
|
| 213 |
+
if self.unit_system == "imperial":
|
| 214 |
+
return {
|
| 215 |
+
"length": "in",
|
| 216 |
+
"area": "in^2",
|
| 217 |
+
"force": "lb",
|
| 218 |
+
"force_k": "kips",
|
| 219 |
+
"stress": "psi",
|
| 220 |
+
"moment": "lb-in",
|
| 221 |
+
"moment_k": "k-in",
|
| 222 |
+
"moment_display": "k-ft",
|
| 223 |
+
}
|
| 224 |
+
else:
|
| 225 |
+
return {
|
| 226 |
+
"length": "mm",
|
| 227 |
+
"area": "mm^2",
|
| 228 |
+
"force": "N",
|
| 229 |
+
"force_k": "kN",
|
| 230 |
+
"stress": "MPa",
|
| 231 |
+
"moment": "N-mm",
|
| 232 |
+
"moment_k": "N-mm",
|
| 233 |
+
"moment_display": "kN-m",
|
| 234 |
+
}
|
db_manager.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import json
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
DB_NAME = "beam_calc.db"
|
| 6 |
+
|
| 7 |
+
def init_db():
|
| 8 |
+
conn = sqlite3.connect(DB_NAME)
|
| 9 |
+
c = conn.cursor()
|
| 10 |
+
c.execute('''
|
| 11 |
+
CREATE TABLE IF NOT EXISTS calculations (
|
| 12 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 13 |
+
timestamp TEXT,
|
| 14 |
+
inputs TEXT,
|
| 15 |
+
results TEXT
|
| 16 |
+
)
|
| 17 |
+
''')
|
| 18 |
+
conn.commit()
|
| 19 |
+
conn.close()
|
| 20 |
+
|
| 21 |
+
def save_calculation(inputs: dict, results: dict):
|
| 22 |
+
conn = sqlite3.connect(DB_NAME)
|
| 23 |
+
c = conn.cursor()
|
| 24 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 25 |
+
c.execute('INSERT INTO calculations (timestamp, inputs, results) VALUES (?, ?, ?)',
|
| 26 |
+
(timestamp, json.dumps(inputs), json.dumps(results)))
|
| 27 |
+
conn.commit()
|
| 28 |
+
conn.close()
|
| 29 |
+
|
| 30 |
+
def get_history(limit=10):
|
| 31 |
+
conn = sqlite3.connect(DB_NAME)
|
| 32 |
+
c = conn.cursor()
|
| 33 |
+
c.execute('SELECT id, timestamp, inputs, results FROM calculations ORDER BY id DESC LIMIT ?', (limit,))
|
| 34 |
+
rows = c.fetchall()
|
| 35 |
+
conn.close()
|
| 36 |
+
|
| 37 |
+
history = []
|
| 38 |
+
for row in rows:
|
| 39 |
+
history.append({
|
| 40 |
+
'id': row[0],
|
| 41 |
+
'timestamp': row[1],
|
| 42 |
+
'inputs': json.loads(row[2]),
|
| 43 |
+
'results': json.loads(row[3])
|
| 44 |
+
})
|
| 45 |
+
return history
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
customtkinter
|
| 2 |
+
matplotlib
|
| 3 |
+
numpy
|
test_calculator.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unit tests for RectangularBeam calculator
|
| 3 |
+
Tests based on ACI 318 Example 4-1 (Imperial) and 4-1M (SI)
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import unittest
|
| 7 |
+
import math
|
| 8 |
+
from calculator import RectangularBeam
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TestRectangularBeamImperial(unittest.TestCase):
|
| 12 |
+
"""Tests for Imperial unit calculations (Example 4-1)."""
|
| 13 |
+
|
| 14 |
+
def setUp(self):
|
| 15 |
+
"""Set up beam with Example 4-1 values."""
|
| 16 |
+
self.beam = RectangularBeam(
|
| 17 |
+
b=12.0,
|
| 18 |
+
h=20.0,
|
| 19 |
+
d=17.5,
|
| 20 |
+
fc=4000.0,
|
| 21 |
+
fy=60000.0,
|
| 22 |
+
n_bars=4,
|
| 23 |
+
bar_area=0.79, # 4 x No. 8 bars
|
| 24 |
+
unit_system="imperial"
|
| 25 |
+
)
|
| 26 |
+
self.results = self.beam.calculate_mn()
|
| 27 |
+
|
| 28 |
+
def test_steel_area(self):
|
| 29 |
+
"""Test As = n_bars * bar_area."""
|
| 30 |
+
expected_As = 4 * 0.79 # 3.16 in2
|
| 31 |
+
self.assertAlmostEqual(self.results["As"], expected_As, places=2)
|
| 32 |
+
|
| 33 |
+
def test_stress_block_depth_a(self):
|
| 34 |
+
"""Test a = (As * fy) / (0.85 * fc * b)."""
|
| 35 |
+
# a = (3.16 * 60000) / (0.85 * 4000 * 12) = 4.647 in
|
| 36 |
+
self.assertAlmostEqual(self.results["a"], 4.647, delta=0.01)
|
| 37 |
+
|
| 38 |
+
def test_neutral_axis_depth_c(self):
|
| 39 |
+
"""Test c = a / beta1."""
|
| 40 |
+
# c = 4.647 / 0.85 = 5.467 in
|
| 41 |
+
expected_c = self.results["a"] / 0.85
|
| 42 |
+
self.assertAlmostEqual(self.results["c"], expected_c, delta=0.01)
|
| 43 |
+
|
| 44 |
+
def test_nominal_moment(self):
|
| 45 |
+
"""Test Mn calculation."""
|
| 46 |
+
# Mn = As * fy * (d - a/2) / 12000 k-ft
|
| 47 |
+
# Mn = 3.16 * 60000 * (17.5 - 4.647/2) / 12000 = 239.79 k-ft
|
| 48 |
+
self.assertAlmostEqual(self.results["Mn_display"], 239.79, delta=0.5)
|
| 49 |
+
|
| 50 |
+
def test_tension_controlled(self):
|
| 51 |
+
"""Test that beam is tension controlled (epsilon_t >= 0.005)."""
|
| 52 |
+
self.assertGreaterEqual(self.results["epsilon_t"], 0.005)
|
| 53 |
+
self.assertEqual(self.results["phi"], 0.9)
|
| 54 |
+
|
| 55 |
+
def test_yield_check(self):
|
| 56 |
+
"""Test that steel yields."""
|
| 57 |
+
self.assertTrue(self.results["yield_check"])
|
| 58 |
+
|
| 59 |
+
def test_minimum_steel_check(self):
|
| 60 |
+
"""Test minimum steel area calculation."""
|
| 61 |
+
# As_min for fc=4000, fy=60000, b=12, d=17.5
|
| 62 |
+
As_min = self.results["As_min"]
|
| 63 |
+
self.assertGreater(As_min, 0)
|
| 64 |
+
self.assertTrue(self.results["as_check"]) # As >= As_min
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class TestRectangularBeamSI(unittest.TestCase):
|
| 68 |
+
"""Tests for SI unit calculations (Example 4-1M)."""
|
| 69 |
+
|
| 70 |
+
def setUp(self):
|
| 71 |
+
"""Set up beam with Example 4-1M values."""
|
| 72 |
+
self.beam = RectangularBeam(
|
| 73 |
+
b=250.0,
|
| 74 |
+
h=565.0,
|
| 75 |
+
d=500.0,
|
| 76 |
+
fc=20.0,
|
| 77 |
+
fy=420.0,
|
| 78 |
+
n_bars=3,
|
| 79 |
+
bar_area=510.0, # 3 x 510 mm2 bars
|
| 80 |
+
unit_system="si"
|
| 81 |
+
)
|
| 82 |
+
self.results = self.beam.calculate_mn()
|
| 83 |
+
|
| 84 |
+
def test_steel_area(self):
|
| 85 |
+
"""Test As = n_bars * bar_area."""
|
| 86 |
+
expected_As = 3 * 510 # 1530 mm2
|
| 87 |
+
self.assertAlmostEqual(self.results["As"], expected_As, places=0)
|
| 88 |
+
|
| 89 |
+
def test_stress_block_depth_a(self):
|
| 90 |
+
"""Test a = (As * fy) / (0.85 * fc * b)."""
|
| 91 |
+
# a = (1530 * 420) / (0.85 * 20 * 250) = 151.76 mm
|
| 92 |
+
self.assertAlmostEqual(self.results["a"], 151.76, delta=1.0)
|
| 93 |
+
|
| 94 |
+
def test_yield_check(self):
|
| 95 |
+
"""Test that steel yields."""
|
| 96 |
+
self.assertTrue(self.results["yield_check"])
|
| 97 |
+
|
| 98 |
+
def test_minimum_steel_check(self):
|
| 99 |
+
"""Test minimum steel area calculation for SI."""
|
| 100 |
+
As_min = self.results["As_min"]
|
| 101 |
+
self.assertGreater(As_min, 0)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class TestBeta1Calculation(unittest.TestCase):
|
| 105 |
+
"""Tests for beta1 calculation based on fc."""
|
| 106 |
+
|
| 107 |
+
def test_beta1_low_fc_imperial(self):
|
| 108 |
+
"""Beta1 = 0.85 for fc <= 4000 psi."""
|
| 109 |
+
beam = RectangularBeam(
|
| 110 |
+
b=12, h=20, d=17.5, fc=4000, fy=60000,
|
| 111 |
+
n_bars=4, bar_area=0.79, unit_system="imperial"
|
| 112 |
+
)
|
| 113 |
+
self.assertEqual(beam.beta1, 0.85)
|
| 114 |
+
|
| 115 |
+
def test_beta1_high_fc_imperial(self):
|
| 116 |
+
"""Beta1 = 0.65 for fc >= 8000 psi."""
|
| 117 |
+
beam = RectangularBeam(
|
| 118 |
+
b=12, h=20, d=17.5, fc=8000, fy=60000,
|
| 119 |
+
n_bars=4, bar_area=0.79, unit_system="imperial"
|
| 120 |
+
)
|
| 121 |
+
self.assertEqual(beam.beta1, 0.65)
|
| 122 |
+
|
| 123 |
+
def test_beta1_mid_fc_imperial(self):
|
| 124 |
+
"""Beta1 interpolated for 4000 < fc < 8000 psi."""
|
| 125 |
+
beam = RectangularBeam(
|
| 126 |
+
b=12, h=20, d=17.5, fc=6000, fy=60000,
|
| 127 |
+
n_bars=4, bar_area=0.79, unit_system="imperial"
|
| 128 |
+
)
|
| 129 |
+
# beta1 = 0.85 - 0.05 * (6000 - 4000) / 1000 = 0.75
|
| 130 |
+
self.assertAlmostEqual(beam.beta1, 0.75, places=2)
|
| 131 |
+
|
| 132 |
+
def test_beta1_override(self):
|
| 133 |
+
"""Test that beta1 can be overridden."""
|
| 134 |
+
beam = RectangularBeam(
|
| 135 |
+
b=12, h=20, d=17.5, fc=4000, fy=60000,
|
| 136 |
+
n_bars=4, bar_area=0.79, beta1=0.70, unit_system="imperial"
|
| 137 |
+
)
|
| 138 |
+
self.assertEqual(beam.beta1, 0.70)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class TestEdgeCases(unittest.TestCase):
|
| 142 |
+
"""Tests for edge cases."""
|
| 143 |
+
|
| 144 |
+
def test_single_bar(self):
|
| 145 |
+
"""Test with single bar."""
|
| 146 |
+
beam = RectangularBeam(
|
| 147 |
+
b=12, h=20, d=17.5, fc=4000, fy=60000,
|
| 148 |
+
n_bars=1, bar_area=0.79, unit_system="imperial"
|
| 149 |
+
)
|
| 150 |
+
results = beam.calculate_mn()
|
| 151 |
+
self.assertEqual(results["As"], 0.79)
|
| 152 |
+
|
| 153 |
+
def test_custom_epsilon_cu(self):
|
| 154 |
+
"""Test with custom ultimate concrete strain."""
|
| 155 |
+
beam = RectangularBeam(
|
| 156 |
+
b=12, h=20, d=17.5, fc=4000, fy=60000,
|
| 157 |
+
n_bars=4, bar_area=0.79, epsilon_cu=0.0035, unit_system="imperial"
|
| 158 |
+
)
|
| 159 |
+
self.assertEqual(beam.epsilon_cu, 0.0035)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
if __name__ == '__main__':
|
| 163 |
+
unittest.main()
|