from dataclasses import dataclass
from IPython.display import Markdown, display
from typing import Optional, Dict
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle, FancyBboxPatch
from pint import Quantity
import numpy as np
import pandas as pd
from pandas import DataFrame
import math
import warnings
# from devtools import debug
from mento.rectangular import RectangularSection
from mento.material import (
Concrete_ACI_318_19,
Concrete_EN_1992_2004,
)
from mento.rebar import Rebar
from mento.units import MPa, mm, inch, kN, m, cm, kNm, dimensionless
from mento.results import Formatter, TablePrinter, DocumentBuilder, CUSTOM_COLORS
from mento.forces import Forces
from mento.settings import BeamSettings
from mento._version import __version__ as MENTO_VERSION
from mento.codes.EN_1992_2004_beam import (
_check_shear_EN_1992_2004,
_check_flexure_EN_1992_2004,
_design_shear_EN_1992_2004,
_design_flexure_EN_1992_2004,
)
from mento.codes.ACI_318_19_beam import (
_check_shear_ACI_318_19,
_design_shear_ACI_318_19,
_check_flexure_ACI_318_19,
_design_flexure_ACI_318_19,
)
[docs]
@dataclass
class RectangularBeam(RectangularSection):
"""
Represents a reinforced concrete rectangular beam section with methods for design, checking,
and visualization of longitudinal and transverse reinforcement according to various design codes.
Attributes:
settings (BeamSettings): Access to global design rules and settings.
Methods:
set_transverse_rebar(n_stirrups, d_b, s_l):
Sets the transverse (stirrup) rebar configuration for the beam.
set_longitudinal_rebar_bot(n1, d_b1, n2, d_b2, n3, d_b3, n4, d_b4):
Sets the bottom longitudinal rebar configuration.
set_longitudinal_rebar_top(n1, d_b1, n2, d_b2, n3, d_b3, n4, d_b4):
Sets the top longitudinal rebar configuration.
design_flexure(forces):
Designs the flexural reinforcement for the beam based on provided forces and design code.
check_flexure(forces):
Checks the flexural capacity for all provided forces and stores results.
design_shear(forces):
Designs the shear reinforcement for the beam based on provided forces and design code.
check_shear(forces):
Checks the shear capacity for all provided forces and stores results.
data:
Property. Displays basic beam data in Markdown format.
flexure_results:
Property. Displays summary of flexural design/check results in Markdown format.
shear_results:
Property. Displays summary of shear design/check results in Markdown format.
results:
Property. Displays all available results (properties, flexure, shear) in Markdown format.
flexure_results_detailed(force=None):
Displays detailed flexure results for a specific force or the limiting case.
flexure_results_detailed_doc(force=None):
Exports detailed flexure results to a Word document.
shear_results_detailed(force=None):
Displays detailed shear results for a specific force or the limiting case.
shear_results_detailed_doc(force=None):
Exports detailed shear results to a Word document.
plot():
Plots the beam section with its rebar.
Usage:
- Instantiate with required section and material properties.
- Use set_longitudinal_rebar_* and set_transverse_rebar to configure reinforcement.
- Call design_flexure and design_shear to perform design.
- Use flexure_results, shear_results, and results properties for summary output.
- Use flexure_results_detailed and shear_results_detailed for detailed output.
- Use plot() to visualize the rebar arrangement.
Note:
This class is intended for use in reinforced concrete beam design workflows,
supporting multiple design codes and detailed reporting.
"""
settings: Optional[BeamSettings] = None
def __post_init__(self) -> None:
super().__post_init__() # Call parent attributes
if not self.settings:
# create defaults based on concrete's unit system
self.settings = BeamSettings(unit_system=self.concrete.unit_system)
else:
# Fill missing fields with defaults from the given unit system
# (ensures your partial BeamSettings still gets the rest of defaults)
self.settings.unit_system = self.concrete.unit_system
self.settings.__post_init__() # recompute missing fields
self._initialize_attributes()
##########################################################
# INITIALIZE ATTRIBUTES
##########################################################
def _initialize_attributes(self) -> None:
"""Initialize all attributes of the beam."""
self.mode = "beam"
# Stirrups and shear attributes
self._stirrup_d_b = self.settings.stirrup_diameter_ini
self._stirrup_s_l: Quantity = 0 * cm
self._stirrup_s_w: Quantity = 0 * cm
self._stirrup_s_max_l: Quantity = 0 * cm
self._stirrup_s_max_w: Quantity = 0 * cm
self._stirrup_n: int = 0
self._A_v_min: Quantity = 0 * cm**2 / m
self._A_v: Quantity = 0 * cm**2 / m
self._A_s_req_bot: Quantity = 0 * cm**2
self._A_s_req_top: Quantity = 0 * cm**2
self._A_v_req: Quantity = 0 * cm**2 / m
self._A_s_tension: Quantity = 0 * cm**2
self._DCRv: float = 0
self._DCRb_top: float = 0
self._DCRb_bot: float = 0
self._alpha: float = math.radians(90)
self._V_s_req: Quantity = 0 * kN
# Design checks and effective heights
self._rho_l_bot: Quantity = 0 * dimensionless
self._rho_l_top: Quantity = 0 * dimensionless
self._bot_rebar_centroid = 0 * mm
self._top_rebar_centroid = 0 * mm
self._c_d_top: float = 0
self._c_d_bot: float = 0
self._shear_checked = False # Tracks if shear check or design has been done
self._flexure_checked = False # Tracks if shear check or design has been done
self._doubly_reinforced = False # Tracks if doubly reinforced section is used
# Initialize default concrete beam attributes
self._initialize_code_attributes()
# Longitudinal rebar attributes
self._initialize_longitudinal_rebar_attributes()
# Results attributes
self._materials_shear: Dict = {}
self._geometry_shear: Dict = {}
self._forces_shear: Dict = {}
self._all_shear_checks_passed: bool = False
self._data_min_max_shear: Dict = {}
self._shear_reinforcement: Dict = {}
self._shear_concrete: Dict = {}
self._shear_all_checks: bool = False
self._materials_flexure: Dict = {}
self._geometry_flexure: Dict = {}
self._forces_flexure: Dict = {}
self._all_flexure_checks_passed: bool = False
self._data_min_max_flexure: Dict = {}
self._flexure_capacity_bot: Dict = {}
self._flexure_capacity_top: Dict = {}
self._flexure_all_checks: bool = False
def _initialize_code_attributes(self) -> None:
if isinstance(self.concrete, Concrete_ACI_318_19):
self._initialize_ACI_318_attributes()
elif isinstance(self.concrete, Concrete_EN_1992_2004):
self._initialize_EN_1992_2004_attributes()
def _initialize_longitudinal_rebar_attributes(self) -> None:
"""Initialize all rebar-related attributes with default values."""
# Bottom rebar defaults
self._n2_b, self._d_b2_b = 0, 0 * mm
self._n3_b, self._d_b3_b = 0, 0 * mm
self._n4_b, self._d_b4_b = 0, 0 * mm
# Top rebar defaults
self._n2_t, self._d_b2_t = 0, 0 * mm
self._n3_t, self._d_b3_t = 0, 0 * mm
self._n4_t, self._d_b4_t = 0, 0 * mm
# Unit system default starter rebar for initial effective height
if self.concrete.unit_system == "metric":
self._n1_b, self._d_b1_b = 2, 8 * mm
self._n1_t, self._d_b1_t = 2, 8 * mm
else:
self._n1_b, self._d_b1_b = 2, 3 / 8 * inch
self._n1_t, self._d_b1_t = 2, 3 / 8 * inch
# Update dependent attributes
self._update_longitudinal_rebar_attributes()
def _create_rebar_designer(self) -> Rebar:
"""Factory for the longitudinal reinforcement optimizer."""
return Rebar(self)
def _apply_longitudinal_design_bot(self, design: dict) -> None:
"""Apply a discrete design to the bottom reinforcement."""
self.set_longitudinal_rebar_bot(
int(design.get("n_1", 0)),
design.get("d_b1"),
int(design.get("n_2", 0)),
design.get("d_b2"),
int(design.get("n_3", 0)),
design.get("d_b3"),
int(design.get("n_4", 0)),
design.get("d_b4"),
)
def _apply_longitudinal_design_top(self, design: dict) -> None:
"""Apply a discrete design to the top reinforcement."""
self.set_longitudinal_rebar_top(
int(design.get("n_1", 0)),
design.get("d_b1"),
int(design.get("n_2", 0)),
design.get("d_b2"),
int(design.get("n_3", 0)),
design.get("d_b3"),
int(design.get("n_4", 0)),
design.get("d_b4"),
)
def _clear_top_longitudinal(self) -> None:
"""Reset the top reinforcement to the default placeholder bars."""
if self.concrete.unit_system == "metric":
self.set_longitudinal_rebar_top(0, 0 * mm)
else:
self.set_longitudinal_rebar_top(0, 0 * inch)
def _initialize_ACI_318_attributes(self) -> None:
if isinstance(self.concrete, Concrete_ACI_318_19):
self._phi_V_n: Quantity = 0 * kN
self._phi_V_s: Quantity = 0 * kN
self._phi_V_c: Quantity = 0 * kN
self._phi_V_max: Quantity = 0 * kN
self._V_u: Quantity = 0 * kN
self._M_u: Quantity = 0 * kNm
self._M_u_bot: Quantity = 0 * kNm
self._M_u_top: Quantity = 0 * kNm
self._N_u: Quantity = 0 * kN
self._A_cv: Quantity = 0 * cm**2
self._k_c_min: Quantity = 0 * MPa
self._sigma_Nu: Quantity = 0 * MPa
self.V_c: Quantity = 0 * kN
self._rho_w: Quantity = 0 * dimensionless
self._lambda_s: float = 0
self.f_yt: Quantity = 0 * MPa
self._max_shear_ok: bool = False
self._A_s_min_bot: Quantity = 0 * cm**2
self._A_s_min_top: Quantity = 0 * cm**2
self._A_s_max_bot: Quantity = 0 * cm**2
self._A_s_max_top: Quantity = 0 * cm**2
self._phi_M_n_bot: Quantity = 0 * kNm
self._phi_M_n_top: Quantity = 0 * kNm
self._d_b_max_bot: Quantity = 0 * mm
self._d_b_max_top: Quantity = 0 * mm
self.flexure_design_results_bot: DataFrame = None
self.flexure_design_results_top: DataFrame = None
self._A_s_bool_bot: bool = False
self._A_s_bool_top: bool = False
def _initialize_EN_1992_2004_attributes(self) -> None:
if isinstance(self.concrete, Concrete_EN_1992_2004):
self._V_Ed_1: Quantity = 0 * kN
self._V_Ed_2: Quantity = 0 * kN
self._N_Ed: Quantity = 0 * kN
self._M_Ed: Quantity = 0 * kNm
self._sigma_cd: Quantity = 0 * MPa
self._V_Rd_c: Quantity = 0 * kN
self._V_Rd_s: Quantity = 0 * kN
self._V_Rd_max: Quantity = 0 * kN
self._V_Rd: Quantity = 0 * kN
self._k_value: float = 0
self._f_ywk = self.steel_bar.f_y
self._f_ywd: Quantity = 0 * MPa
self._f_cd: Quantity = 0 * MPa
self._f_cd_shear: Quantity = 0 * MPa
self._A_p = 0 * cm**2 # No prestressed for now
self._sigma_cp: Quantity = 0 * MPa
self._theta: float = 0
self._cot_theta: float = 0
self._z: Quantity = 0 * cm
self._M_Rd_bot: Quantity = 0 * kNm
self._M_Rd_top: Quantity = 0 * kNm
self._M_Ed_bot: Quantity = 0 * kNm
self._M_Ed_top: Quantity = 0 * kNm
##########################################################
# SET LONGITUDINAL AND TRANSVERSE REBAR AND UPDATE ATTRIBUTES
##########################################################
[docs]
def set_transverse_rebar(
self,
n_stirrups: int = 0,
d_b: Quantity = 0 * mm,
s_l: Quantity = 0 * cm,
) -> None:
"""Sets the transverse rebar in the beam section."""
self._stirrup_n = n_stirrups
self._stirrup_d_b = d_b
self._stirrup_s_l = s_l
n_legs = n_stirrups * 2
A_db = (d_b**2) * math.pi / 4 # Area of one stirrup leg
A_vs = n_legs * A_db # Total area of stirrups
self._A_v = A_vs / s_l # Stirrup area per unit length
# Update effective heights
self._update_effective_heights()
def _len_unit(self) -> Quantity:
# Base length unit for this section
return 1 * mm if getattr(self.concrete, "unit_system", "metric") == "metric" else 1 * inch
def _area_unit(self) -> Quantity:
L = self._len_unit()
return L**2
[docs]
def set_longitudinal_rebar_bot(
self,
n1: int,
d_b1: Quantity | None,
n2: int = 0,
d_b2: Quantity | None = None,
n3: int = 0,
d_b3: Quantity | None = None,
n4: int = 0,
d_b4: Quantity | None = None,
) -> None:
L = self._len_unit()
self._n1_b = n1
self._d_b1_b = d_b1 if d_b1 is not None else 0 * L
self._n2_b = n2
self._d_b2_b = d_b2 if d_b2 is not None else 0 * L
self._n3_b = n3
self._d_b3_b = d_b3 if d_b3 is not None else 0 * L
self._n4_b = n4
self._d_b4_b = d_b4 if d_b4 is not None else 0 * L
self._update_longitudinal_rebar_attributes()
[docs]
def set_longitudinal_rebar_top(
self,
n1: int,
d_b1: Quantity | None,
n2: int = 0,
d_b2: Quantity | None = None,
n3: int = 0,
d_b3: Quantity | None = None,
n4: int = 0,
d_b4: Quantity | None = None,
) -> None:
L = self._len_unit()
self._n1_t = n1
self._d_b1_t = d_b1 if d_b1 is not None else 0 * L
self._n2_t = n2
self._d_b2_t = d_b2 if d_b2 is not None else 0 * L
self._n3_t = n3
self._d_b3_t = d_b3 if d_b3 is not None else 0 * L
self._n4_t = n4
self._d_b4_t = d_b4 if d_b4 is not None else 0 * L
self._update_longitudinal_rebar_attributes()
def _calculate_longitudinal_rebar_area(self) -> None:
"""Calculate total rebar area (robust to None or zero diameters)."""
A0 = 0 * self._area_unit()
def area(n: int, d: Quantity | None) -> Quantity:
# Treat None or zero diameter as zero area; works with pint
if d is None:
return A0
mag = getattr(d, "magnitude", None)
if mag is not None and mag == 0:
return A0
return n * (d**2) * np.pi / 4
# Bottom
self._A_s_bot = (
area(self._n1_b, self._d_b1_b)
+ area(self._n2_b, self._d_b2_b)
+ area(self._n3_b, self._d_b3_b)
+ area(self._n4_b, self._d_b4_b)
)
# Top
self._A_s_top = (
area(self._n1_t, self._d_b1_t)
+ area(self._n2_t, self._d_b2_t)
+ area(self._n3_t, self._d_b3_t)
+ area(self._n4_t, self._d_b4_t)
)
def _calculate_min_clear_spacing(self) -> None:
"""
Calculates the maximum clear spacing between bars for the bottom rebar layers.
Returns:
Quantity: The maximum clear spacing between bars in either the first or second layer.
"""
def layer_clear_spacing(n_a: int, d_a: Quantity, n_b: int, d_b: Quantity) -> Quantity:
"""
Helper function to calculate clear spacing for a given layer.
Parameters:
n_a (int): Number of bars in the first group of the layer.
d_a (Quantity): Diameter of bars in the first group of the layer.
n_b (int): Number of bars in the second group of the layer.
d_b (Quantity): Diameter of bars in the second group of the layer.
Returns:
Quantity: Clear spacing for the given layer.
"""
effective_width = self.width - 2 * (self.c_c + self._stirrup_d_b)
total_bars = n_a + n_b
if total_bars <= 1:
return effective_width - max(d_a, d_b) # Clear space for one bar
total_bar_width = n_a * d_a + n_b * d_b
return (effective_width - total_bar_width) / (total_bars - 1)
# AVAIABLE CLEAR SPACING FOR BOTTOM BARS
# Calculate clear spacing for each layer
spacing_layer1_b = layer_clear_spacing(self._n1_b, self._d_b1_b, self._n2_b, self._d_b2_b)
spacing_layer2_b = layer_clear_spacing(self._n3_b, self._d_b3_b, self._n4_b, self._d_b4_b)
# Return the maximum clear spacing between the two layers
self._available_s_bot = min(spacing_layer1_b, spacing_layer2_b)
# AVAIABLE CLEAR SPACING FOR TOP BARS
# Calculate clear spacing for each layer
spacing_layer1_t = layer_clear_spacing(self._n1_t, self._d_b1_t, self._n2_t, self._d_b2_t)
spacing_layer2_t = layer_clear_spacing(self._n3_t, self._d_b3_t, self._n4_t, self._d_b4_t)
# Return the maximum clear spacing between the two layers
self._available_s_top = min(spacing_layer1_t, spacing_layer2_t)
def _calculate_long_rebar_centroid(self) -> None:
"""
Calculates the centroid (baricenter) of a group of rebars based on their diameters, quantities,
and layer spacing.
Returns:
float: The calculated centroid height of the rebar group.
"""
# BOTTOM BARS CENTROID
# Calculate the vertical positions of the bar layers
y1_b = self._d_b1_b / 2
y2_b = self._d_b2_b / 2
y3_b = max(self._d_b1_b, self._d_b2_b) + self.settings.layers_spacing + self._d_b3_b / 2
y4_b = max(self._d_b1_b, self._d_b2_b) + self.settings.layers_spacing + self._d_b4_b / 2
# Calculate the total area of each layer
area_1_b = self._n1_b * self._d_b1_b**2 * np.pi / 4.0
area_2_b = self._n2_b * self._d_b2_b**2 * np.pi / 4.0
area_3_b = self._n3_b * self._d_b3_b**2 * np.pi / 4.0
area_4_b = self._n4_b * self._d_b4_b**2 * np.pi / 4.0
# Calculate the centroid as a weighted average
total_area_b = area_1_b + area_2_b + area_3_b + area_4_b
if total_area_b == 0:
return 0 * mm # Avoid division by zero if no bars are present
self._bot_rebar_centroid = (
area_1_b * y1_b + area_2_b * y2_b + area_3_b * y3_b + area_4_b * y4_b
) / total_area_b
# TOP BARS CENTROID
# Calculate the vertical positions of the bar layers
y1_t = self._d_b1_t / 2
y2_t = self._d_b2_t / 2
y3_t = max(self._d_b1_t, self._d_b2_t) + self.settings.layers_spacing + self._d_b3_t / 2
y4_t = max(self._d_b1_t, self._d_b2_t) + self.settings.layers_spacing + self._d_b4_t / 2
# Calculate the total area of each layer
area_1_t = self._n1_t * self._d_b1_t**2 * np.pi / 4 # Area proportional to number of bars and their diameter
area_2_t = self._n2_t * self._d_b2_t**2 * np.pi / 4
area_3_t = self._n3_t * self._d_b3_t**2 * np.pi / 4
area_4_t = self._n4_t * self._d_b4_t**2 * np.pi / 4
# Calculate the centroid as a weighted average
total_area_t = area_1_t + area_2_t + area_3_t + area_4_t
if total_area_t == 0:
return 0 * mm # Avoid division by zero if no bars are present
self._top_rebar_centroid = (
area_1_t * y1_t + area_2_t * y2_t + area_3_t * y3_t + area_4_t * y4_t
) / total_area_t
def _update_longitudinal_rebar_attributes(self) -> None:
"""Recalculate attributes dependent on rebar configuration for both top and bottom reinforcing."""
self._calculate_longitudinal_rebar_area()
self._calculate_long_rebar_centroid()
self._calculate_min_clear_spacing()
self._update_effective_heights()
def _update_effective_heights(self) -> None:
"""Update effective heights and depths for moment and shear calculations."""
self._c_mec_bot = self.c_c + self._stirrup_d_b + self._bot_rebar_centroid
self._c_mec_top = self.c_c + self._stirrup_d_b + self._top_rebar_centroid
self._d_bot = self.height - self._c_mec_bot
self._d_top = self.height - self._c_mec_top
# Use bottom or top effective height
self._d_shear = min(self._d_bot, self._d_top)
##########################################################
# CHECK & DESIGN FLEXURE
##########################################################
[docs]
def design_flexure(self, forces: list[Forces]) -> DataFrame:
"""
Designs flexural reinforcement for the beam using the provided forces and design code.
Identifies the limiting cases for top and bottom reinforcement, designs for those cases,
and then checks flexural capacity for all forces.
Returns
-------
DataFrame
A DataFrame summarizing the flexural design results for all forces.
"""
# Initialize limiting cases
max_M_y_top = 0 * kN * m # For negative M_y (top reinforcement design)
max_M_y_bot = 0 * kN * m # For positive M_y (bottom reinforcement design)
# Identify the limiting cases
for force in forces:
# For top reinforcement, consider the minimum (most negative) moment
if force._M_y <= max_M_y_top:
max_M_y_top = force._M_y
self._limiting_case_bot = force
# For bottom reinforcement, consider the maximum positive moment
if force._M_y > max_M_y_bot:
max_M_y_bot = force._M_y
self._limiting_case_top = force
# Design flexural reinforcement for the limiting cases
if self.concrete.design_code == "ACI 318-19" or self.concrete.design_code == "CIRSOC 201-25":
_design_flexure_ACI_318_19(self, max_M_y_bot, max_M_y_top)
elif self.concrete.design_code == "EN 1992-2004":
_design_flexure_EN_1992_2004(self, max_M_y_bot, max_M_y_top)
# Check flexural capacity for all forces with the assigned reinforcement
all_results = self.check_flexure(forces)
return all_results
[docs]
def check_flexure(self, forces: list[Forces]) -> DataFrame:
# Initialize variables to track limiting cases
max_dcr_top: float = 0
max_dcr_bot: float = 0
limiting_case_top = None
limiting_case_bot = None
limiting_case_top_details = None
limiting_case_bot_details = None
# To compile results for all forces
self._flexure_results_list = [] # Store individual results for each force
self._flexure_results_detailed_list = {} # Store detailed results by force ID
for force in forces:
# Select the method based on design code
if self.concrete.design_code == "ACI 318-19" or self.concrete.design_code == "CIRSOC 201-25":
result = _check_flexure_ACI_318_19(self, force)
elif self.concrete.design_code == "EN 1992-2004":
result = _check_flexure_EN_1992_2004(self, force)
self._flexure_results_list.append(result)
# Store detailed results for this force
self._flexure_results_detailed_list[force.id] = {
"forces": self._forces_flexure.copy(),
"min_max": self._data_min_max_flexure.copy(),
"flexure_capacity_top": self._flexure_capacity_top.copy(),
"flexure_capacity_bot": self._flexure_capacity_bot.copy(),
"checks_pass": self._all_flexure_checks_passed,
}
# Extract the DCR values for top and bottom from the results
current_dcr_top = self._DCRb_top
current_dcr_bot = self._DCRb_bot
# Update top limiting case
if current_dcr_top >= max_dcr_top:
max_dcr_top = current_dcr_top
limiting_case_top = result
limiting_case_top_details = self._flexure_results_detailed_list[force.id]
# Update bottom limiting case
if current_dcr_bot >= max_dcr_bot:
max_dcr_bot = current_dcr_bot
limiting_case_bot = result
limiting_case_bot_details = self._flexure_results_detailed_list[force.id]
# Compile all results into a single DataFrame
all_data = pd.concat(self._flexure_results_list, ignore_index=True)
units_row = self._get_units_row_flexure()
all_results = pd.concat([units_row, all_data], ignore_index=True)
# Store limiting cases
self._limiting_case_flexure_top = limiting_case_top
self._limiting_case_flexure_bot = limiting_case_bot
self._limiting_case_flexure_top_details = limiting_case_top_details
self._limiting_case_flexure_bot_details = limiting_case_bot_details
# Store maximum DCRs for easy access
self._max_dcr_top = max_dcr_top
self._max_dcr_bot = max_dcr_bot
# Mark shear as checked
self._flexure_checked = True
return all_results
##########################################################
# CHECK & DESIGN SHEAR
##########################################################
# Factory method to select the shear design method
[docs]
def design_shear(self, forces: list[Forces]) -> DataFrame:
# Track the maximum A_v_req to identify the limiting case
max_A_v_req = 0 * cm**2 / m
# Step 1: Identify the worst-case force
for force in forces:
if self.concrete.design_code == "ACI 318-19" or self.concrete.design_code == "CIRSOC 201-25":
_design_shear_ACI_318_19(self, force)
elif self.concrete.design_code == "EN 1992-2004":
_design_shear_EN_1992_2004(self, force)
# Check if this result is the limiting case
current_A_v_req = self._A_v_req
if current_A_v_req >= max_A_v_req:
max_A_v_req = current_A_v_req
max_V_s_req = self._V_s_req
# Step 2: Perform rebar design for the worst-case force
section_rebar = Rebar(self)
self.shear_design_results = section_rebar.transverse_rebar(max_A_v_req, max_V_s_req, self._alpha)
self._best_rebar_design = section_rebar.transverse_rebar_design
self._stirrup_s_l = self._best_rebar_design["s_l"]
self._stirrup_s_w = self._best_rebar_design["s_w"]
self._stirrup_s_max_l = self._best_rebar_design["s_max_l"]
self._stirrup_s_max_w = self._best_rebar_design["s_max_w"]
self.set_transverse_rebar(
self._best_rebar_design["n_stir"],
self._best_rebar_design["d_b"],
self._best_rebar_design["s_l"],
)
# Update longitudinal rebar attributes
self._update_longitudinal_rebar_attributes()
# Step 3: Check shear adequacy for all forces using the designed rebar
all_results = self.check_shear(forces)
return all_results
# Factory method to select the shear check method
[docs]
def check_shear(self, forces: list[Forces]) -> DataFrame:
self._shear_results_list = [] # Store individual results for each force
self._shear_results_detailed_list = {} # Store detailed results by force ID
max_dcr = 0 # Track the maximum DCR to identify the limiting case
self._limiting_case_shear_details = None
for force in forces:
# Select the method based on design code
if self.concrete.design_code == "ACI 318-19" or self.concrete.design_code == "CIRSOC 201-25":
result = _check_shear_ACI_318_19(self, force)
elif self.concrete.design_code == "EN 1992-2004":
result = _check_shear_EN_1992_2004(self, force)
self._shear_results_list.append(result)
self._shear_results_detailed_list[force.id] = {
"forces": self._forces_shear.copy(),
"shear_reinforcement": self._shear_reinforcement.copy(),
"min_max": self._data_min_max_shear.copy(),
"checks_pass": self._all_shear_checks_passed,
"shear_concrete": self._shear_concrete.copy(),
}
# Check if this result is the limiting case
current_dcr = result["DCR"][0]
if current_dcr >= max_dcr:
max_dcr = current_dcr
self._limiting_case_shear = result
self._limiting_case_shear_details = self._shear_results_detailed_list[force.id]
# Compile all results into a single DataFrame
all_data = pd.concat(self._shear_results_list, ignore_index=True)
units_row = self._get_units_row_shear()
all_results = pd.concat([units_row, all_data], ignore_index=True)
# Identify the most limiting case by Demand-to-Capacity Ratio (DCR) or other criteria
self.limiting_case_shear = all_data.loc[all_data["DCR"].idxmax()] # Select row with highest DCR
self._shear_checked = True # Mark shear as checked
return all_results
def _get_units_row_shear(self) -> pd.DataFrame:
if isinstance(self.concrete, Concrete_EN_1992_2004):
# Orden exacto de columnas para EN 1992
return pd.DataFrame(
[
{
"Label": "",
"Comb.": "",
"Av,min": "cm²/m",
"Av,req": "cm²/m",
"Av": "cm²/m",
"NEd": "kN",
"VEd,1": "kN",
"VEd,2": "kN",
"VRd,c": "kN",
"VRd,s": "kN",
"VRd": "kN",
"VRd,max": "kN",
"VEd,1≤VRd,max": "",
"VEd,2≤VRd": "",
"DCR": "",
}
]
)
else:
return pd.DataFrame(
[
{
"Label": "",
"Comb.": "",
"Av,min": "cm²/m",
"Av,req": "cm²/m",
"Av": "cm²/m",
"Vu": "kN",
"Nu": "kN",
"ØVc": "kN",
"ØVs": "kN",
"ØVn": "kN",
"ØVmax": "kN",
"Vu≤ØVmax": "",
"Vu≤ØVn": "",
"DCR": "",
}
]
)
def _get_units_row_flexure(self) -> pd.DataFrame:
# TODO: Add imperial units row output
if self.concrete.design_code == "ACI 318-19" or self.concrete.design_code == "CIRSOC 201-25":
units_row = pd.DataFrame(
[
{
"Label": "",
"Comb.": "",
"Position": "",
"As,min": "cm²",
"As,req top": "cm²",
"As,req bot": "cm²",
"As": "cm²",
# "c/d": "", # Uncomment if you include this field later
"Mu": "kNm",
"ØMn": "kNm",
"Mu≤ØMn": "",
"DCR": "",
}
]
)
elif self.concrete.design_code == "EN 1992-2004":
units_row = pd.DataFrame(
[
{
"Label": "",
"Comb.": "",
"Position": "",
"As,min": "cm²",
"As,req top": "cm²",
"As,req bot": "cm²",
"As": "cm²",
# "c/d": "", # Uncomment if you include this field later
"MEd": "kNm",
"MRd": "kNm",
"MEd≤MRd": "",
"DCR": "",
}
]
)
else:
raise ValueError(f"Flexure design method not implemented for concrete type: {type(self.concrete).__name__}") # noqa: E501
return units_row
##########################################################
# CHECK & DESIGN ALL
##########################################################
[docs]
def design(self, forces: list[Forces]) -> None:
"""
Complete design: flexure + shear + flexure check.
"""
self.design_flexure(forces)
self.design_shear(forces)
self.check_flexure(forces)
[docs]
def check(self, forces: list[Forces]) -> None:
"""
Complete check: flexure + shear.
"""
self.check_flexure(forces)
self.check_shear(forces)
##########################################################
# RESULTS
##########################################################
# Beam results for Jupyter Notebook
@property
def data(self) -> None:
type = self.mode.capitalize()
markdown_content = (
f"{type} {self.label}, $b$={self.width.to('cm')}"
f", $h$={self.height.to('cm')}, $c_{{c}}$={self.c_c.to('cm')}, \
Concrete {self.concrete.name}, Rebar {self.steel_bar.name}."
)
self._md_data = markdown_content
# Display the combined content
display(Markdown(markdown_content)) # type: ignore
return None
@property
def flexure_results(self) -> None:
if not self._flexure_checked:
warnings.warn(
"Flexural design has not been performed yet. Call _check_flexure() or " "design_flexure() first.",
UserWarning,
)
self._md_flexure_results = "Flexural results are not available."
return None
# Check if limiting case details exist
top_details = self._limiting_case_flexure_top_details or {}
bot_details = self._limiting_case_flexure_bot_details or {}
# Use limiting case results
top_result_data = top_details.get("flexure_capacity_top")
bot_result_data = bot_details.get("flexure_capacity_bot")
checks_pass_top = top_details.get("checks_pass")
checks_pass_bot = bot_details.get("checks_pass")
warning_top = "⚠️ Some checks failed, see detailed results." if not checks_pass_top else ""
warning_bot = "⚠️ Some checks failed, see detailed results." if not checks_pass_bot else ""
# Pending for approval
# all_checks_top = top_details.get('flexure_check')
# all_checks_bot = bot_details.get('flexure_check')
# all_checks = all_checks_top and all_checks_bot
# Formatter instance for DCR formatting
markdown_content = ""
formatter = Formatter()
# Handle top result data
if top_result_data:
top_rebar_1 = top_result_data["Value"][0]
top_rebar_2 = top_result_data["Value"][1]
area_top = top_result_data["Value"][7]
Mu_top = self._limiting_case_flexure_top_details["forces"]["Value"][0]
Mn_top = top_result_data["Value"][9]
DCR_top = top_result_data["Value"][10]
rebar_top = f"{top_rebar_1}" + (f" ++ {top_rebar_2}" if top_rebar_2 != "-" else "")
formatted_DCR_top = formatter.DCR(DCR_top)
markdown_content += (
f"Top longitudinal rebar: {rebar_top}, $A_{{s,top}}$ = {area_top} cm², $M_u$ = {Mu_top} kNm, "
f"$\\phi M_n$ = {Mn_top} kNm → {formatted_DCR_top} {warning_top}\n\n"
)
else:
markdown_content += "No top moment to check.\n\n"
# Handle bottom result data
if bot_result_data:
bot_rebar_1 = bot_result_data["Value"][0]
bot_rebar_2 = bot_result_data["Value"][1]
area_bot = bot_result_data["Value"][7]
Mu_bot = self._limiting_case_flexure_bot_details["forces"]["Value"][1]
Mn_bot = bot_result_data["Value"][9]
DCR_bot = bot_result_data["Value"][10]
rebar_bot = f"{bot_rebar_1}" + (f" ++ {bot_rebar_2}" if bot_rebar_2 != "-" else "")
formatted_DCR_bot = formatter.DCR(DCR_bot)
markdown_content += (
f"Bottom longitudinal rebar: {rebar_bot}, $A_{{s,bot}}$ = {area_bot} cm², $M_u$ = {Mu_bot} kNm, "
f"$\\phi M_n$ = {Mn_bot} kNm → {formatted_DCR_bot} {warning_bot}"
)
else:
markdown_content += "No bottom moment to check."
# markdown_content += 'Beam flexure checks PASS ✔️' if all_checks else "Beam flexure checks FAIL ❌"
self._md_flexure_results = markdown_content
display(Markdown(markdown_content))
@property
def shear_results(self) -> None:
if not self._shear_checked:
warnings.warn(
"Shear design has not been performed yet. Call check_shear() or " "design_shear() first.",
UserWarning,
)
self._md_shear_results = "Shear results are not available."
return None
# Check if limiting case details exist
shear_details = self._limiting_case_shear_details or {}
# Use limiting case results
limiting_reinforcement = shear_details.get("shear_reinforcement")
limiting_forces = shear_details.get("forces")
limiting_shear_concrete = shear_details.get("shear_concrete")
checks_pass = shear_details.get("checks_pass")
markdown_content = ""
if shear_details:
# Create FUFormatter instance and format FU value
formatter = Formatter()
formatted_DCR = formatter.DCR(limiting_shear_concrete["Value"][-1])
if self._A_v == 0 * cm:
rebar_v = "not assigned"
else:
rebar_v = (
f"{int(limiting_reinforcement['Value'][0])}eØ{limiting_reinforcement['Value'][1]}/"
f"{limiting_reinforcement['Value'][2]} cm"
)
# Limitng cases checks
warning = "⚠️ Some checks failed, see detailed results." if not checks_pass else ""
if self.concrete.design_code == "ACI 318-19" or self.concrete.design_code == "CIRSOC 201-25":
markdown_content = (
f"Shear reinforcing {rebar_v}, $A_v$={limiting_reinforcement['Value'][6]} cm²/m"
f", $V_u$={limiting_forces['Value'][1]} kN, $\\phi V_n$={limiting_shear_concrete['Value'][7]} kN → {formatted_DCR} {warning}"
) # noqa: E501
else: # self.concrete.design_code == "EN 1992-2004"
markdown_content = (
f"Shear reinforcing {rebar_v}, $A_{{sw}}$={limiting_reinforcement['Value'][6]} cm²/m"
f", $V_{{Ed,2}}$={limiting_forces['Value'][1]} kN, $V_{{Rd}}$={limiting_shear_concrete['Value'][6]} kN → {formatted_DCR} {warning}"
) # noqa: E501
else:
markdown_content += "No shear to check."
self._md_shear_results = markdown_content
display(Markdown(markdown_content))
return None
# Beam results for Jupyter Notebook
@property
def results(self) -> None:
"""
Ensure that properties, flexure results, and shear results are available and display them.
Handles cases where flexure or shear results are not yet available.
"""
if not hasattr(self, "_md_properties"):
self.data # This will generate _md_properties
if self._flexure_checked:
self.flexure_results # This will generate _md_flexure_results
if self._shear_checked:
self.shear_results # This will generate _md_shear_results
return None
[docs]
def flexure_results_detailed(self, force: Optional[Forces] = None) -> None:
"""
Displays detailed flexure results.
Parameters
----------
forces : Forces, optional
The specific Forces object to display results for. If None, displays results for the limiting case.
Returns
-------
None
"""
if not self._flexure_checked:
warnings.warn(
"Flexural check has not been performed yet. Call _check_flexure or " "design_flexure first.",
UserWarning,
)
self._md_flexure_results = "Flexure results are not available."
return None
# Determine which results to display (limiting case by default)
if force:
if force.id not in self._flexure_results_detailed_list:
raise ValueError(f"No results found for Forces object with ID {force.id}.")
result_data = self._flexure_results_detailed_list[force.id]
top_result_data = result_data["flexure_capacity_top"]
bot_result_data = result_data["flexure_capacity_bot"]
forces_result = result_data["forces"]
min_max_result = result_data["min_max"]
else:
if self._limiting_case_flexure_top_details is None:
raise ValueError("Top limiting case details are not available.")
if self._limiting_case_flexure_bot_details is None:
raise ValueError("Bottom limiting case details are not available.")
# Use the worst-case top and bottom scenarios
top_result_data = self._limiting_case_flexure_top_details["flexure_capacity_top"]
bot_result_data = self._limiting_case_flexure_bot_details["flexure_capacity_bot"]
forces_result = {
"Design_forces": [
"Top max moment",
"Bottom max moment",
],
"Variable": ["Mu,top", "Mu,bot"],
"Value": [
round(self._limiting_case_flexure_top_details["forces"]["Value"][0], 2),
round(self._limiting_case_flexure_bot_details["forces"]["Value"][1], 2),
],
"Unit": ["kNm", "kNm"],
}
min_max_result = {
"Check": [
"Min/Max As rebar top",
"Minimum spacing top",
"Min/Max As rebar bottom",
"Minimum spacing bottom",
],
"Unit": ["cm²", "mm", "cm²", "mm"],
"Value": [
round(
self._limiting_case_flexure_top_details["min_max"]["Value"][0],
2,
), # Top limiting case As
round(
self._limiting_case_flexure_top_details["min_max"]["Value"][1],
2,
), # Top limiting case spacing
round(
self._limiting_case_flexure_bot_details["min_max"]["Value"][2],
2,
), # Bottom limiting case As
round(
self._limiting_case_flexure_bot_details["min_max"]["Value"][3],
2,
), # Bottom limiting case spacing
],
"Min.": [
round(self._limiting_case_flexure_top_details["min_max"]["Min."][0], 2), # Top limiting case As_min
self._limiting_case_flexure_top_details["min_max"]["Min."][1], # Top limiting case spacing min
round(
self._limiting_case_flexure_bot_details["min_max"]["Min."][2], 2
), # Bottom limiting case As_min
self._limiting_case_flexure_bot_details["min_max"]["Min."][3], # Bottom limiting case spacing min
],
"Max.": [
round(self._limiting_case_flexure_top_details["min_max"]["Max."][0], 2), # Top limiting case As_max
"", # No max constraint for spacing
round(
self._limiting_case_flexure_bot_details["min_max"]["Max."][2], 2
), # Bottom limiting case As_max
"", # No max constraint for spacing
],
"Ok?": [
self._limiting_case_flexure_top_details["min_max"]["Ok?"][0], # Top As check
self._limiting_case_flexure_top_details["min_max"]["Ok?"][1], # Top spacing check
self._limiting_case_flexure_bot_details["min_max"]["Ok?"][2], # Bottom As check
self._limiting_case_flexure_bot_details["min_max"]["Ok?"][3], # Bottom spacing check
],
}
# Create TablePrinter instances for detailed display
print("===== BEAM FLEXURE DETAILED RESULTS =====")
materials_printer = TablePrinter("MATERIALS")
materials_printer.print_table_data(self._materials_flexure, headers="keys")
geometry_printer = TablePrinter("GEOMETRY")
geometry_printer.print_table_data(self._geometry_flexure, headers="keys")
forces_printer = TablePrinter("FORCES")
forces_printer.print_table_data(forces_result, headers="keys")
min_max_printer = TablePrinter("MAX AND MIN LIMIT CHECKS")
min_max_printer.print_table_data(min_max_result, headers="keys")
capacity_printer = TablePrinter("FLEXURAL CAPACITY - TOP")
capacity_printer.print_table_data(top_result_data, headers="keys")
capacity_printer = TablePrinter("FLEXURAL CAPACITY - BOTTOM")
capacity_printer.print_table_data(bot_result_data, headers="keys")
[docs]
def flexure_results_detailed_doc(self, force: Optional[Forces] = None) -> None:
"""
Prints detailed flexure results in Word.
Parameters
----------
forces : Forces, optional
The specific Forces object to display results for. If None, displays results for the limiting case.
"""
if not self._flexure_checked:
warnings.warn(
"Flexural check has not been performed yet. Call _check_flexure or " "design_flexure first.",
UserWarning,
)
self._md_flexure_results = "Flexural results are not available."
return None
# Determine which results to display (limiting case by default)
if force:
if force.id not in self._flexure_results_detailed_list:
raise ValueError(f"No results found for Forces object with ID {force.id}.")
result_data = self._flexure_results_detailed_list[force.id]
top_result_data = result_data["flexure_capacity_top"]
bot_result_data = result_data["flexure_capacity_bot"]
forces_result = result_data["forces"]
min_max_result = result_data["min_max"]
else:
if self._limiting_case_flexure_top_details is None:
raise ValueError("Top limiting case details are not available.")
if self._limiting_case_flexure_bot_details is None:
raise ValueError("Bottom limiting case details are not available.")
# Use the worst-case top and bottom scenarios
top_result_data = self._limiting_case_flexure_top_details["flexure_capacity_top"]
bot_result_data = self._limiting_case_flexure_bot_details["flexure_capacity_bot"]
forces_result = {
"Design forces": [
"Top max moment",
"Bottom max moment",
],
"Variable": ["Mu,top", "Mu,bot"],
"Value": [
round(self._limiting_case_flexure_top_details["forces"]["Value"][0], 2),
round(self._limiting_case_flexure_bot_details["forces"]["Value"][1], 2),
],
"Unit": ["kNm", "kNm"],
}
min_max_result = {
"Check": [
"Min/Max As rebar top",
"Minimum spacing top",
"Min/Max As rebar bottom",
"Minimum spacing bottom",
],
"Unit": ["cm²", "mm", "cm²", "mm"],
"Value": [
round(
self._limiting_case_flexure_top_details["min_max"]["Value"][0],
2,
), # Top limiting case As
round(
self._limiting_case_flexure_top_details["min_max"]["Value"][1],
2,
), # Top limiting case spacing
round(
self._limiting_case_flexure_bot_details["min_max"]["Value"][2],
2,
), # Bottom limiting case As
round(
self._limiting_case_flexure_bot_details["min_max"]["Value"][3],
2,
), # Bottom limiting case spacing
],
"Min.": [
round(self._limiting_case_flexure_top_details["min_max"]["Min."][0], 2), # Top limiting case As_min
self._limiting_case_flexure_top_details["min_max"]["Min."][1], # Top limiting case spacing min
round(
self._limiting_case_flexure_bot_details["min_max"]["Min."][2], 2
), # Bottom limiting case As_min
self._limiting_case_flexure_bot_details["min_max"]["Min."][3], # Bottom limiting case spacing min
],
"Max.": [
round(self._limiting_case_flexure_top_details["min_max"]["Max."][0], 2), # Top limiting case As_max
"", # No max constraint for spacing
round(
self._limiting_case_flexure_bot_details["min_max"]["Max."][2], 2
), # Bottom limiting case As_max
"", # No max constraint for spacing
],
"Ok?": [
self._limiting_case_flexure_top_details["min_max"]["Ok?"][0], # Top As check
self._limiting_case_flexure_top_details["min_max"]["Ok?"][1], # Top spacing check
self._limiting_case_flexure_bot_details["min_max"]["Ok?"][2], # Bottom As check
self._limiting_case_flexure_bot_details["min_max"]["Ok?"][3], # Bottom spacing check
],
}
# Convert output Dicts into DataFrames
df_materials = pd.DataFrame(self._materials_flexure)
df_geometry = pd.DataFrame(self._geometry_flexure)
df_forces = pd.DataFrame(forces_result)
df_data_min_max = pd.DataFrame(min_max_result)
df_flexure_capacity_top = pd.DataFrame(top_result_data)
df_flexure_capacity_bottom = pd.DataFrame(bot_result_data)
# Create a document builder instance
doc_builder = DocumentBuilder(title="Concrete beam flexure check")
# Add first section and table
doc_builder.add_heading(f"Beam {self.label} flexure check", level=1)
doc_builder.add_text(f"Made with mento {MENTO_VERSION}. Design code: {self.concrete.design_code}")
doc_builder.add_heading("Materials", level=2)
doc_builder.add_table_data(df_materials)
doc_builder.add_table_data(df_geometry)
doc_builder.add_table_data(df_forces)
# Add third section for limit checks
doc_builder.add_heading("Limit Checks", level=2)
doc_builder.add_table_data(df_data_min_max)
# Add second section for flexural checks
doc_builder.add_heading("Flexural Capacity Top", level=2)
doc_builder.add_table_dcr(df_flexure_capacity_top)
doc_builder.add_heading("Flexural Capacity Bottom", level=2)
doc_builder.add_table_dcr(df_flexure_capacity_bottom)
# Save the Word doc
doc_builder.save(f"Beam {self.label} flexure check {self.concrete.design_code}.docx")
[docs]
def shear_results_detailed(self, force: Optional[Forces] = None) -> None:
"""
Displays detailed shear results.
Parameters
----------
forces : Forces, optional
The specific Forces object to display results for. If None, displays results for the limiting case.
Returns
-------
None
"""
if not self._shear_checked:
warnings.warn(
"Shear check has not been performed yet. Call check_shear or " "design_shear first.",
UserWarning,
)
self._md_shear_results = "Shear results are not available."
return None
# Determine which results to display (limiting case by default)
if force:
force_id = force.id
if force_id not in self._shear_results_detailed_list:
raise ValueError(f"No results found for Forces object with ID {force_id}.")
result_data = self._shear_results_detailed_list[force_id]
else:
# Default to limiting case
result_data = self._limiting_case_shear_details
# Create a TablePrinter instance and display tables
print("===== BEAM SHEAR DETAILED RESULTS =====")
materials_printer = TablePrinter("MATERIALS")
materials_printer.print_table_data(self._materials_shear, headers="keys")
geometry_printer = TablePrinter("GEOMETRY")
geometry_printer.print_table_data(self._geometry_shear, headers="keys")
forces_printer = TablePrinter("FORCES")
forces_printer.print_table_data(result_data["forces"], headers="keys")
steel_printer = TablePrinter("SHEAR STRENGTH")
steel_printer.print_table_data(result_data["shear_reinforcement"], headers="keys")
min_max_printer = TablePrinter("MAX AND MIN LIMIT CHECKS")
min_max_printer.print_table_data(result_data["min_max"], headers="keys")
concrete_printer = TablePrinter("CONCRETE STRENGTH")
concrete_printer.print_table_data(result_data["shear_concrete"], headers="keys")
[docs]
def shear_results_detailed_doc(self, force: Optional[Forces] = None) -> None:
"""
Prints detailed shear results in Word.
Parameters
----------
forces : Forces, optional
The specific Forces object to display results for. If None, displays results for the limiting case.
"""
if not self._shear_checked:
warnings.warn(
"Shear check has not been performed yet. Call check_shear or " "design_shear first.",
UserWarning,
)
self._md_shear_results = "Shear results are not available."
return None
# Determine which results to display (limiting case by default)
if force:
force_id = force.id
if force_id not in self._shear_results_detailed_list:
raise ValueError(f"No results found for Forces object with ID {force_id}.")
result_data = self._shear_results_detailed_list[force_id]
else:
# Default to limiting case
result_data = self._limiting_case_shear_details
# Convert output Dicts into DataFrames
df_materials = pd.DataFrame(self._materials_shear)
df_geometry = pd.DataFrame(self._geometry_shear)
df_forces = pd.DataFrame(result_data["forces"])
df_shear_reinforcement = pd.DataFrame(result_data["shear_reinforcement"])
df_data_min_max = pd.DataFrame(result_data["min_max"])
df_shear_concrete = pd.DataFrame(result_data["shear_concrete"])
# Create a document builder instance
doc_builder = DocumentBuilder(title="Concrete beam shear check")
# Add first section and table
doc_builder.add_heading(f"Beam {self.label} shear check", level=1)
doc_builder.add_text(f"Made with mento {MENTO_VERSION}. Design code: {self.concrete.design_code}")
doc_builder.add_heading("Materials", level=2)
doc_builder.add_table_data(df_materials)
doc_builder.add_table_data(df_geometry)
doc_builder.add_table_data(df_forces)
# Add second section and another table (can use different data)
doc_builder.add_heading("Limit checks", level=2)
doc_builder.add_table_min_max(df_data_min_max)
doc_builder.add_heading("Design checks", level=2)
doc_builder.add_table_data(df_shear_reinforcement)
doc_builder.add_table_dcr(df_shear_concrete)
# Save the Word doc
doc_builder.save(f"Beam {self.label} shear check {self.concrete.design_code}.docx")
def _format_longitudinal_rebar_string(self, n1: int, d_b1: Quantity, n2: int = 0, d_b2: Quantity = 0 * mm) -> str:
"""
Returns a formatted string representing the rebars and their diameters.
Rules:
- If n1 and n2 have the same diameter → combine (e.g., 2Ø16 + 1Ø16 → 3Ø16)
- If they differ → show both groups (e.g., 2Ø16+2Ø10)
- If no bars exist → "-"
"""
# Convert diameters safely
phi1 = int(d_b1.to("mm").magnitude) if (d_b1 is not None and d_b1.magnitude > 0) else 0
phi2 = int(d_b2.to("mm").magnitude) if (d_b2 is not None and d_b2.magnitude > 0) else 0
# No bars at all
if n1 == 0 and n2 == 0:
return "-"
# Only one group
if n2 == 0 or phi2 == 0:
return f"{n1}Ø{phi1}"
if n1 == 0 or phi1 == 0:
return f"{n2}Ø{phi2}"
# Same diameter → combine quantities
if phi1 == phi2:
return f"{n1 + n2}Ø{phi1}"
# Different diameters → write both
return f"{n1}Ø{phi1}+{n2}Ø{phi2}"
##########################################################
# PLOT BEAM SECTION WITH REBAR
##########################################################
def _plot_rebar_layer(
self,
width_cm: float,
height_cm: float,
c_c_cm: float,
stirrup_d_b_cm: float,
layers_spacing_cm: float,
n1: int,
d_b1: Quantity,
n2: int,
d_b2: Quantity,
max_db: Quantity,
is_bottom: bool = True,
is_second_layer: bool = False,
) -> None:
"""
Helper method to plot a single layer of rebars.
"""
# Calculate y-position based on layer and bottom/top
y_base = c_c_cm + stirrup_d_b_cm if is_bottom else height_cm - c_c_cm - stirrup_d_b_cm
if is_second_layer:
y_base += (
layers_spacing_cm + max_db.to("cm").magnitude
if is_bottom
else -layers_spacing_cm - max_db.to("cm").magnitude
)
# Plot side bars (position 1 or 3)
if n1 > 0:
diameter_cm = d_b1.to("cm").magnitude
radius_cm = diameter_cm / 2.0
# nominal vertical center before corner correction
y_center_nominal = y_base + radius_cm if is_bottom else y_base - radius_cm
# width available between inner faces of stirrup legs, for this bar diameter
clear_span = width_cm - 2 * (c_c_cm + stirrup_d_b_cm + radius_cm)
# corner offset depends on stirrup diameter (controls bend radius)
corner_offset = 0.43 * stirrup_d_b_cm # tune factor if needed
for i in range(n1):
# even if n1 == 1, just center it
if n1 == 1:
x_nominal = width_cm / 2.0
else:
x_nominal = c_c_cm + stirrup_d_b_cm + radius_cm + i * (clear_span / (n1 - 1))
# default: no shift
x_shift = 0.0
y_shift = 0.0
# leftmost bar
if i == 0:
x_shift = corner_offset # push inward (to the right)
y_shift = corner_offset if is_bottom else -corner_offset
# rightmost bar
elif i == n1 - 1:
x_shift = -corner_offset # push inward (to the left)
y_shift = corner_offset if is_bottom else -corner_offset
x_plot = x_nominal + x_shift
y_plot = y_center_nominal + y_shift
circle = Circle(
(x_plot, y_plot),
radius_cm,
color=CUSTOM_COLORS["dark_gray"],
fill=True,
)
self._ax.add_patch(circle)
# ---------------------------------
# Plot intermediate bars (group n2)
# ---------------------------------
if n2 > 0:
diameter_cm = d_b2.to("cm").magnitude
radius_cm = diameter_cm / 2.0
y_center_nominal = y_base + radius_cm if is_bottom else y_base - radius_cm
clear_span = width_cm - 2 * (c_c_cm + stirrup_d_b_cm + radius_cm)
for i in range(n2):
x_nominal = c_c_cm + stirrup_d_b_cm + radius_cm + (i + 1) * (clear_span / (n2 + 1))
# intermediate bars: no special offset
x_plot = x_nominal
y_plot = y_center_nominal
circle = Circle(
(x_plot, y_plot),
radius_cm,
color=CUSTOM_COLORS["dark_gray"],
fill=True,
)
self._ax.add_patch(circle)
def _format_rebar_layer_text(
self,
n1: int,
d_b1: Quantity,
n2: int,
d_b2: Quantity,
) -> str:
"""
Devuelve un string tipo '2Ø16+3Ø10' a partir de n1, d1, n2, d2.
Si un grupo tiene n=0, no se incluye.
Diámetros en mm.
En slab:
- Siempre combina en un único grupo: '5Ø12'.
En beam:
- Si n1 y n2 tienen el mismo diámetro, combina: '4Ø16'.
- Si son distintos, deja el formato '2Ø16+3Ø10'.
"""
mode = getattr(self, "mode", "beam")
# -------------------------------
# MODO SLAB: siempre combinar
# -------------------------------
if mode == "slab":
total_bars = n1 + n2
if total_bars == 0:
return ""
# Tomar el diámetro "no nulo"
if n1 > 0 and d_b1 is not None:
phi = d_b1.to("mm").magnitude
elif n2 > 0 and d_b2 is not None:
phi = d_b2.to("mm").magnitude
else:
return "" # por seguridad
return f"{total_bars}Ø{phi:.0f}"
# -------------------------------
# MODO BEAM
# -------------------------------
# Si n1 y n2 tienen el mismo diámetro y ambos > 0 → combinar
if n1 > 0 and n2 > 0 and d_b1 is not None and d_b2 is not None:
phi1 = d_b1.to("mm").magnitude
phi2 = d_b2.to("mm").magnitude
# Igualdad con una pequeña tolerancia
if abs(phi1 - phi2) < 1e-6:
total_bars = n1 + n2
return f"{total_bars}Ø{phi1:.0f}"
# Caso general: como lo tenías antes
parts: list[str] = []
if n1 > 0 and d_b1 is not None:
phi1 = d_b1.to("mm").magnitude
parts.append(f"{n1}Ø{phi1:.0f}")
if n2 > 0 and d_b2 is not None:
phi2 = d_b2.to("mm").magnitude
parts.append(f"{n2}Ø{phi2:.0f}")
return "+".join(parts) if parts else ""
def _annotate_rebar_layer_text(
self,
width_cm: float,
height_cm: float,
c_c_cm: float,
stirrup_d_b_cm: float,
layers_spacing_cm: float,
n1: int,
d_b1: Quantity,
n2: int,
d_b2: Quantity,
max_db: Quantity,
is_bottom: bool = True,
is_second_layer: bool = False,
) -> None:
"""
Escribe a la derecha de la sección la leyenda de armadura para un layer.
Ejemplo: '2Ø16+3Ø10'.
"""
text = self._format_rebar_layer_text(n1, d_b1, n2, d_b2)
if not text:
return # nada que mostrar
# misma lógica de y_base que en _plot_rebar_layer
y_base = c_c_cm + stirrup_d_b_cm if is_bottom else height_cm - c_c_cm - stirrup_d_b_cm
if is_second_layer:
shift = layers_spacing_cm + max_db.to("cm").magnitude
y_base = y_base + shift if is_bottom else y_base - shift
# posición vertical aproximada del centro del layer
rep_db_cm = max_db.to("cm").magnitude
y_center = y_base + rep_db_cm / 2.0 if is_bottom else y_base - rep_db_cm / 2.0
# posición horizontal del texto (a la derecha de la sección)
x_text = width_cm + 0.1 * width_cm
self._ax.text( # type: ignore
x_text,
y_center,
text,
ha="left",
va="center",
color=CUSTOM_COLORS["dark_gray"],
# fontsize=10,
)
def _annotate_stirrups_text(
self,
width_cm: float,
height_cm: float,
) -> None:
"""
Escribe la leyenda de estribos a la derecha, a media altura.
Ejemplo: '1eØ6/20' o '2eØ6/20'.
"""
if self._stirrup_n == 0:
return # nothing to show
phi_mm = self._stirrup_d_b.to("mm").magnitude
s_cm = self._stirrup_s_l.to("cm").magnitude
text = f"{self._stirrup_n:.0f}eØ{phi_mm:.0f}/{s_cm:.0f}"
x_text = width_cm + 0.1 * width_cm
y_text = height_cm / 2.0
self._ax.text( # type: ignore
x_text,
y_text,
text,
ha="left",
va="center",
color=CUSTOM_COLORS["dark_gray"],
# fontsize=10,
)
def _add_rounded_stirrup(
self,
x0: float,
y0: float,
width: float,
height: float,
db_cm: float,
facecolor: str,
) -> None:
"""
Add one closed stirrup with rounded corners and thickness db_cm.
All dimensions in cm.
(x0, y0) is bottom-left of the OUTER stirrup line.
"""
# corner radii (same logic you already have)
inner_radius = 4 * db_cm / 2
outer_radius = inner_radius + db_cm
outer = FancyBboxPatch(
(x0, y0),
width,
height,
boxstyle=f"Round, pad=0, rounding_size={outer_radius}",
edgecolor=CUSTOM_COLORS["dark_blue"],
facecolor="white",
linewidth=db_cm, # thickness of the steel
)
self._ax.add_patch(outer)
inner = FancyBboxPatch(
(x0 + db_cm, y0 + db_cm),
width - 2 * db_cm,
height - 2 * db_cm,
boxstyle=f"Round, pad=0, rounding_size={inner_radius}",
edgecolor=CUSTOM_COLORS["dark_blue"],
facecolor=facecolor,
linewidth=1,
)
self._ax.add_patch(inner)
def _plot_stirrups_in_section(
self,
c_c_cm: float,
section_width_cm: float,
section_height_cm: float,
stirrup_db_cm: float,
n_stirrups: int,
) -> None:
"""
Plot 1, 2 or 3 stirrups inside the beam section.
Rules:
- 1 stirrup: full width (current behavior).
- 2 stirrups: outer full width, inner with 1/2 width, centered.
- 3 stirrups: outer full width, plus 2 inner stirrups whose total
occupied width is about 1/4 of the section width (each ~1/8), placed
symmetrically left/right.
"""
# base geometry (outer stirrup like now)
stirrup_width = section_width_cm - 2 * c_c_cm
stirrup_height = section_height_cm - 2 * c_c_cm
# Always draw the outer stirrup
self._add_rounded_stirrup(
x0=c_c_cm,
y0=c_c_cm,
width=stirrup_width,
height=stirrup_height,
db_cm=stirrup_db_cm,
facecolor=CUSTOM_COLORS["light_gray"],
)
if n_stirrups <= 1:
return
# Case 2 stirrups: inner one with 1/2 width, centered
if n_stirrups == 2:
inner_width = 0.5 * stirrup_width
x_center = c_c_cm + stirrup_width / 2
x0_inner = x_center - inner_width / 2
self._add_rounded_stirrup(
x0=x0_inner,
y0=c_c_cm,
width=inner_width,
height=stirrup_height,
db_cm=stirrup_db_cm,
facecolor=CUSTOM_COLORS["light_gray"],
)
return
# Case 3+ stirrups: outer + 2 small inner ones.
# Clamp so it doesn't exceed outer stirrup
inner_width = min(0.25 * section_width_cm, 0.4 * stirrup_width)
# Place inner stirrups roughly at quarter points of the outer stirrup
# -> centers at 1/3 and 2/3 of stirrup span
x_left_center = c_c_cm + stirrup_width * (1 / 3) - stirrup_db_cm / 2
x_right_center = c_c_cm + stirrup_width * (2 / 3) + stirrup_db_cm / 2
x0_left = x_left_center - inner_width / 2 - stirrup_db_cm / 2
x0_right = x_right_center - inner_width / 2 + stirrup_db_cm / 2
self._add_rounded_stirrup(
x0=x0_left,
y0=c_c_cm,
width=inner_width,
height=stirrup_height,
db_cm=stirrup_db_cm,
facecolor=CUSTOM_COLORS["light_gray"],
)
self._add_rounded_stirrup(
x0=x0_right,
y0=c_c_cm,
width=inner_width,
height=stirrup_height,
db_cm=stirrup_db_cm,
facecolor=CUSTOM_COLORS["light_gray"],
)
[docs]
def plot(self, show: bool = False) -> plt.Figure: # type: ignore
"""
Plots the rectangular section with a dark gray border, light gray hatch, and dimensions.
Also plots the stirrup with rounded corners and thickness.
"""
# Convert dimensions to consistent units (cm)
width_cm: float = self.width.to("cm").magnitude
height_cm: float = self.height.to("cm").magnitude
c_c_cm: float = self.c_c.to("cm").magnitude
stirrup_d_b_cm: float = self._stirrup_d_b.to("cm").magnitude
layers_spacing_cm: float = self.settings.layers_spacing.to("cm").magnitude
# Create figure and axis
fig, self._ax = plt.subplots()
# Create a rectangle patch for the section
rect = Rectangle(
(0, 0),
width_cm,
height_cm,
linewidth=1.3,
edgecolor=CUSTOM_COLORS["dark_gray"],
facecolor=CUSTOM_COLORS["light_gray"],
)
self._ax.add_patch(rect)
if self.mode == "beam":
db = self._stirrup_d_b.to("cm").magnitude # bar diameter Ø
# Cap at 3 for drawing (you can show 1, 2, or 3)
n_stirrups = max(1, min(3, self._stirrup_n))
self._plot_stirrups_in_section(
c_c_cm=c_c_cm,
section_width_cm=width_cm,
section_height_cm=height_cm,
stirrup_db_cm=db,
n_stirrups=n_stirrups,
)
# Set plot limits with some padding
padding = max(width_cm, height_cm) * 0.2
self._ax.set_xlim(-padding, width_cm + padding)
self._ax.set_ylim(-padding, height_cm + padding)
# Text and dimension offsets
dim_offset = 2.5
text_offset = dim_offset + 2
# Add width dimension
self._ax.annotate(
"", # No text here, text is added separately
xy=(0, -dim_offset), # Start of arrow (left side)
xytext=(width_cm, -dim_offset), # End of arrow (right side)
arrowprops={
"arrowstyle": "<->",
"lw": 1,
"color": CUSTOM_COLORS["dark_blue"],
},
)
if self.concrete.unit_system == "imperial":
# Example: format to 2 decimal places, then use pint's compact (~P) format
width = "{:.0f~P}".format(self.width.to("inch"))
height = "{:.0f~P}".format(self.height.to("inch"))
else:
width = "{:.0f~P}".format(self.width.to("cm"))
height = "{:.0f~P}".format(self.height.to("cm"))
# Add width dimension text below the arrow
self._ax.text(
width_cm / 2, # Center of the arrow
-text_offset, # Slightly below the arrow
width,
ha="center",
va="top",
color=CUSTOM_COLORS["dark_gray"],
)
# Add height dimension
self._ax.annotate(
"", # No text here, text is added separately
xy=(-dim_offset, 0), # Start of arrow (bottom)
xytext=(-dim_offset, height_cm), # End of arrow (top)
arrowprops={
"arrowstyle": "<->",
"lw": 1,
"color": CUSTOM_COLORS["dark_blue"],
},
)
# Add height dimension text to the left of the arrow
self._ax.text(
-text_offset, # Slightly to the left of the arrow
height_cm / 2, # Center of the arrow
height,
ha="right",
va="center",
color=CUSTOM_COLORS["dark_gray"],
rotation=90, # Rotate text vertically
)
# Set aspect of the plot to be equal
self._ax.set_aspect("equal")
# Remove axes for better visualization
self._ax.axis("off")
# Calculate rebar positions
# Bottom rebars
self._plot_rebar_layer(
width_cm,
height_cm,
c_c_cm,
stirrup_d_b_cm,
layers_spacing_cm,
self._n1_b,
self._d_b1_b,
self._n2_b,
self._d_b2_b,
max_db=self._d_b1_b,
is_bottom=True,
)
self._plot_rebar_layer(
width_cm,
height_cm,
c_c_cm,
stirrup_d_b_cm,
layers_spacing_cm,
self._n3_b,
self._d_b3_b,
self._n4_b,
self._d_b4_b,
max_db=self._d_b1_b,
is_bottom=True,
is_second_layer=True,
)
# Top rebars
self._plot_rebar_layer(
width_cm,
height_cm,
c_c_cm,
stirrup_d_b_cm,
layers_spacing_cm,
self._n1_t,
self._d_b1_t,
self._n2_t,
self._d_b2_t,
max_db=self._d_b1_t,
is_bottom=False,
)
self._plot_rebar_layer(
width_cm,
height_cm,
c_c_cm,
stirrup_d_b_cm,
layers_spacing_cm,
self._n3_t,
self._d_b3_t,
self._n4_t,
self._d_b4_t,
max_db=self._d_b1_t,
is_bottom=False,
is_second_layer=True,
)
### REBAR TEXT ANNOTATIONS
# Bottom, 1st layer
self._annotate_rebar_layer_text(
width_cm,
height_cm,
c_c_cm,
stirrup_d_b_cm,
layers_spacing_cm,
self._n1_b,
self._d_b1_b,
self._n2_b,
self._d_b2_b,
max_db=self._d_b1_b,
is_bottom=True,
is_second_layer=False,
)
# Bottom, 2nd layer
self._annotate_rebar_layer_text(
width_cm,
height_cm,
c_c_cm,
stirrup_d_b_cm,
layers_spacing_cm,
self._n3_b,
self._d_b3_b,
self._n4_b,
self._d_b4_b,
max_db=self._d_b1_b,
is_bottom=True,
is_second_layer=True,
)
# Top, 1st layer
self._annotate_rebar_layer_text(
width_cm,
height_cm,
c_c_cm,
stirrup_d_b_cm,
layers_spacing_cm,
self._n1_t,
self._d_b1_t,
self._n2_t,
self._d_b2_t,
max_db=self._d_b1_t,
is_bottom=False,
is_second_layer=False,
)
# Top, 2nd layer
self._annotate_rebar_layer_text(
width_cm,
height_cm,
c_c_cm,
stirrup_d_b_cm,
layers_spacing_cm,
self._n3_t,
self._d_b3_t,
self._n4_t,
self._d_b4_t,
max_db=self._d_b1_t,
is_bottom=False,
is_second_layer=True,
)
# Stirrups text
self._annotate_stirrups_text(width_cm, height_cm)
# Store the section figure
self._fig = fig
if show:
plt.show()
# # Close the figure so notebooks don't auto-display it twice
plt.close(fig)
return fig