algorembrant commited on
Commit
7c60059
·
verified ·
1 Parent(s): 6049cd3

Upload 16 files

Browse files
.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
+ [![Python](https://img.shields.io/badge/Python-3.9+-3776AB?style=flat&logo=python&logoColor=white)](https://www.python.org/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![ACI 318](https://img.shields.io/badge/Code-ACI%20318-blue.svg)](https://www.concrete.org/)
6
+ [![CustomTkinter](https://img.shields.io/badge/UI-CustomTkinter-green.svg)](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()