Source code for mento.rebar

from __future__ import annotations
from typing import Any, Dict, TYPE_CHECKING, Tuple
import math
import pandas as pd
import numpy as np

from mento.units import psi, mm, cm, inch, MPa
from mento.material import Concrete_ACI_318_19

if TYPE_CHECKING:
    from mento.beam import RectangularBeam
    from pint import Quantity
    from pandas import DataFrame


[docs] class Rebar: def __init__(self, beam: RectangularBeam): """ Initializes the Rebar object with the associated beam and settings. """ self.mode = getattr(beam, "mode", "beam") # "beam" or "slab" self.beam = beam self._long_combos_df: DataFrame = pd.DataFrame() self._trans_combos_df: DataFrame = pd.DataFrame() # Precompute spacing limits as floats in mm self._clear_limit_mm = self.beam.settings.clear_spacing.to("mm").magnitude self._vibrator_mm = self.beam.settings.vibrator_size.to("mm").magnitude self._clear_spacing = self.beam.settings.clear_spacing.to("mm") # Unit system default rebar if self.beam.concrete.unit_system == "metric": self.rebar_diameters = [ 6 * mm, 8 * mm, 10 * mm, 12 * mm, 16 * mm, 20 * mm, 25 * mm, 32 * mm, ] self.rebar_areas = {d: (math.pi * d**2) / 4 for d in self.rebar_diameters} else: self.rebar_diameters = [ 3 * inch / 8, 4 * inch / 8, 5 * inch / 8, 6 * inch / 8, 7 * inch / 8, 8 * inch / 8, 1.128 * inch, 1.27 * inch, 1.41 * inch, 1.693 * inch, ] rebar_areas_list = [(d**2 * np.pi / 4) for d in self.rebar_diameters] self.rebar_areas = dict(zip(self.rebar_diameters, rebar_areas_list)) @property def longitudinal_rebar_design(self) -> DataFrame: return self._long_combos_df.iloc[0] @property def transverse_rebar_design(self) -> DataFrame: return self._trans_combos_df.iloc[0] ########################################################## # TRANSVERSE REBAR DESIGN ##########################################################
[docs] def calculate_max_spacing_ACI_318_19(self, V_s_req: Quantity, A_cv: Quantity) -> Tuple[Quantity, Quantity]: """ Calculate the maximum allowable spacing across the length and width of the beam based on design requirements. Parameters: ----------- V_s_req : float Required shear force for the rebar. A_cv : float Effective shear area of the concrete section. Returns: -------- tuple (s_max_l, s_max_w): The maximum spacing across the length and width of the beam. """ f_c = self.beam.concrete.f_c if isinstance(self.beam.concrete, Concrete_ACI_318_19): lambda_factor = self.beam.concrete.lambda_factor # Determine maximum spacing based on V_s_req condition # Maximum spacing for lower shear demand if self.beam.concrete.unit_system == "metric": if V_s_req <= 0.083 * lambda_factor * math.sqrt(f_c / MPa) * MPa * A_cv: s_max_l = min(self.beam._d_shear / 2, 60 * cm) s_max_w = min(self.beam._d_shear, 60 * cm) else: # Maximum spacing for higher shear demand s_max_l = min(self.beam._d_shear / 4, 30 * cm) s_max_w = min(self.beam._d_shear / 2, 30 * cm) else: if V_s_req <= 4 * lambda_factor * math.sqrt(f_c / psi) * psi * A_cv: s_max_l = min(self.beam._d_shear / 2, 24 * inch) s_max_w = min(self.beam._d_shear, 24 * inch) else: s_max_l = min(self.beam._d_shear / 4, 12 * inch) s_max_w = min(self.beam._d_shear / 2, 12 * inch) return s_max_l, s_max_w
[docs] def calculate_max_spacing_EN_1992_2004(self, alpha: float) -> Tuple[Quantity, Quantity]: """ Calculate the maximum allowable spacing across the length and width of the beam based on design requirements for EN 1992-2004. Parameters: ----------- alpha: stirrups angle Returns: -------- tuple (s_max_l, s_max_w): The maximum spacing along the length and width of the beam. """ # Maximum of 40 cm for stirrup spacing s_max_l = min(0.75 * self.beam._d_shear * (1 + 1 / math.tan(alpha)), 40 * cm) s_max_w = min(0.75 * self.beam._d_shear, 60 * cm) return s_max_l, s_max_w
[docs] def transverse_rebar_ACI_318_19(self, V_s_req: Quantity) -> Any: if self.beam.concrete.unit_system == "metric": valid_diameters = self.rebar_diameters[2:5] # Minimum 10 mm else: valid_diameters = self.rebar_diameters[0:3] A_cv = self.beam.width * self.beam._d_shear s_max_l, s_max_w = self.calculate_max_spacing_ACI_318_19(V_s_req, A_cv) return valid_diameters, s_max_l, s_max_w
[docs] def transverse_rebar_CIRSOC_201_25(self, V_s_req: Quantity) -> Any: valid_diameters = self.rebar_diameters[0:4] # Minimum 6 mm, maximum 12 mm A_cv = self.beam.width * self.beam._d_shear s_max_l, s_max_w = self.calculate_max_spacing_ACI_318_19(V_s_req, A_cv) return valid_diameters, s_max_l, s_max_w
[docs] def transverse_rebar_EN_1992_2004(self, alpha: float) -> Any: valid_diameters = self.rebar_diameters[0:4] # Minimum 6 mm, maximum 12 mm s_max_l, s_max_w = self.calculate_max_spacing_EN_1992_2004(alpha) return valid_diameters, s_max_l, s_max_w
[docs] def transverse_rebar(self, A_v_req: Quantity, V_s_req: Quantity, alpha: float) -> DataFrame: """ Computes the required transverse reinforcement based on ACI 318-19. Args: A_v_req: Required area for transverse reinforcement. Returns: A dictionary containing the transverse rebar design. """ # Prepare the list for valid combinations valid_combinations = [] # Get code specific limitations if self.beam.concrete.design_code == "ACI 318-19": valid_diameters, s_max_l, s_max_w = self.transverse_rebar_ACI_318_19(V_s_req) elif self.beam.concrete.design_code == "CIRSOC 201-25": valid_diameters, s_max_l, s_max_w = self.transverse_rebar_CIRSOC_201_25(V_s_req) elif self.beam.concrete.design_code == "EN 1992-2004": valid_diameters, s_max_l, s_max_w = self.transverse_rebar_EN_1992_2004(alpha) # Iterate through available diameters for d_b in valid_diameters: # Start with 2 legs = 1 stirrup n_legs = 2 # Start with maximum allowed spacing s_max_l if self.beam.concrete.unit_system == "metric": s_l: Quantity = math.floor(s_max_l.to("cm").magnitude) * cm else: s_l = math.floor(s_max_l.to("inch").magnitude) * inch while True: # Calculate spacing based on current legs n_stirrups = math.ceil(n_legs / 2) # Number of stirrups based on number of legs n_legs_actual = n_stirrups * 2 # Ensure legs are even # Consider 1 leg less for spacing laong width s_w = (self.beam.width - 2 * self.beam.c_c - self.beam._stirrup_d_b) / (n_legs_actual - 1) # noqa: F841 A_db = self.rebar_areas[d_b] # Area of a stirrup bar A_vs = n_legs_actual * A_db # Area of vertical stirrups A_v: Quantity = A_vs / s_l # Area of vertical stirrups per unit length # Store the valid combination if spacing is also valid if self.beam.concrete.unit_system == "metric": # Check if the calculated A_v meets or exceeds the required A_v if A_v >= A_v_req: valid_combinations.append( { "n_stir": int(n_stirrups), "d_b": d_b, "s_l": s_l.to("cm"), # spacing along length "s_w": s_w.to("cm"), # spacing along width "A_v": A_v.to("cm**2/m"), "s_max_l": s_max_l.to("cm"), "s_max_w": s_max_w.to("cm"), } ) # Stop checking larger diameters break # If A_v is insufficient, reduce s_l by 1 cm s_l -= 1 * cm # If s_l becomes less than 5 cm, increase the number of legs by 2 and reset s_l to s_max_l if s_l < 5 * cm: # If spacing is less than 5 cm, increase 1 stirrup n_legs += 2 s_l = math.floor(s_max_l.to("cm").magnitude) * cm # Reset s_l to the max allowed spacing else: # Check if the calculated A_v meets or exceeds the required A_v if A_v >= A_v_req: valid_combinations.append( { "n_stir": int(n_stirrups), "d_b": d_b, "s_l": s_l.to("inch"), # spacing along length "s_w": s_w.to("inch"), # spacing along width "A_v": A_v.to("inch**2/ft"), "s_max_l": s_max_l.to("inch"), "s_max_w": s_max_w.to("inch"), } ) # Stop checking larger diameters break # If A_v is insufficient, reduce s_l by 1 inch s_l -= 1 * inch # If s_l becomes less than 2 inch, increase the number of legs by 2 and reset s_l to s_max_l if s_l < 2 * inch: # If spacing is less than 2 inch, increase 1 stirrup n_legs += 2 s_l = math.floor(s_max_l.to("inch").magnitude) * inch # Reset s_l to the max allowed spacing # Create a DataFrame with all valid combinations df_combinations = pd.DataFrame(valid_combinations) # Sort combinations by the total rebar area required (ascending) # Sort by 'A_v' first, then by 'n_stir' to prioritize fewer bars df_combinations.sort_values(by=["n_stir", "A_v"], inplace=True) df_combinations.reset_index(drop=True, inplace=True) self._trans_combos_df = df_combinations return df_combinations
########################################################## # LONGITUDINAL REBAR DESIGN ##########################################################
[docs] def longitudinal_rebar_ACI_318_19( self, A_s_req: Quantity, A_s_max: Quantity | None = None, mech_cover: Quantity | None = None, ) -> DataFrame: """ Computes the required longitudinal reinforcement based on ACI 318-19. Args: A_s_req: Required longitudinal rebar area. A_s_max: Optional maximum allowable longitudinal rebar area. If not provided, the limit defaults to ``10 * A_s_req``. Returns: A DataFrame containing the best combinations of rebar details. If no combination satisfies ``A_s_req``, returns the combination with the maximum possible area. """ self.A_s_req = A_s_req self.original_mech_cover = mech_cover effective_width = self.beam.width - 2 * (self.beam.c_c + self.beam._stirrup_d_b) # --- Early exit: no steel required ------------------------------------- if A_s_req.to("cm**2").magnitude == 0: df = pd.DataFrame( [ { "n_1": 0, "d_b1": 0 * mm, "n_2": 0, "d_b2": None, "n_3": 0, "d_b3": None, "n_4": 0, "d_b4": None, "total_as": 0 * cm**2, "total_bars": 0, "clear_spacing": self.beam.settings.clear_spacing.to("mm"), } ] ) self._long_combos_df = df return df # Variables to track the combinations valid_combinations = [] best_fallback_combination = None # To store the best fallback design max_fallback_area = 0 * cm**2 # To track the maximum area in fallback cases # Create a list of rebar diameters that are equal to or greater than the minimum diameter if self.beam.concrete.unit_system == "metric": self.min_long_rebar = 10 * mm else: self.min_long_rebar = 3 * inch / 8 # Filter valid rebar diameters based on the minimum longitudinal diameter per design code and beam settings valid_rebar_diameters = [ d for d in self.rebar_diameters if d >= self.min_long_rebar and d >= self.beam.settings.minimum_longitudinal_diameter and d <= self.beam.settings.max_longitudinal_diameter ] for d_b1 in valid_rebar_diameters: # Without taking Ø6 as a possible solution for d_b2 in [d for d in valid_rebar_diameters if d <= d_b1]: # Quick upper-bound check for this diameter pair area1 = self.rebar_areas[d_b1] # cm² area2 = self.rebar_areas[d_b2] max_bars = self.beam.settings.max_bars_per_layer # Max for layer 1 (n1 fixed, n2 up to max_bars) A_layer1_max = 2 * area1 + max_bars * area2 # n1=2 fixed if self.mode == "slab": # Slab: mirror layer 2 => max total As = 2 * layer1 A_total_max = 2 * A_layer1_max else: # Beam: rough upper bound (could be improved) A_total_max = 2 * A_layer1_max # Max limit for As (same logic as later) max_limit = 10 * A_s_req if A_s_max is not None: max_limit = min(max_limit, A_s_max) # If even the maximum possible As with these diameters # is less than required, skip all n2/n3/n4 loops. if A_total_max < min(A_s_req, max_limit): continue for d_b3 in [d for d in valid_rebar_diameters if d <= d_b2]: for d_b4 in [d for d in valid_rebar_diameters if d <= d_b3]: # Condition 5: |d_b1 - d_b2| and |d_b3 - d_b4| must not exceed max_diameter_diff # Apply diameter difference condition across all combinations # Ensure that no two bars exceed `max_diameter_diff` diameters = [d_b1, d_b2, d_b3, d_b4] diameters = [d for d in diameters if d is not None] # Filter out None values for unused bars # Apply the diameter difference check across all bars if not self._check_diameter_differences(diameters): continue # Skip this combination if any diameter pair exceeds max_diameter_diff` n1 = 0 if self.A_s_req == 0 * cm**2 else 2 # This is a fixed value for every beam # Iterate over possible numbers of bars in each group for n2 in range(0, self.beam.settings.max_bars_per_layer + 1): # n2 can be 0 or more if n1 + n2 > self.beam.settings.max_bars_per_layer: continue # Skip if the total bars in layer 1 exceed the limit if not self._check_spacing(n1, n2, d_b1, d_b2, effective_width): continue # Calculate area for layer 1 A_s_layer_1 = n1 * self.rebar_areas[d_b1] + ( n2 * self.rebar_areas[d_b2] if n2 > 0 else 0 * cm**2 ) max_limit = 10 * A_s_req if A_s_max is not None: max_limit = min(max_limit, A_s_max) max_limit = max(max_limit, n1 * self.rebar_areas[self.min_long_rebar]) if A_s_layer_1 > max_limit: break # further n2 will only increase area # Check if total area from layer 1 is enough for required A_s # And also less than the maximum limit if A_s_layer_1 >= A_s_req and A_s_layer_1 <= max_limit: total_as = A_s_layer_1 # Only consider layer 1 total_bars = n1 + n2 # Total bars only in layer 1 valid_combinations.append( { "n_1": n1, "d_b1": d_b1, "n_2": n2, "d_b2": d_b2 if n2 > 0 else None, # Display as None if n2 is 0 "n_3": 0, # No bars in layer 2 "d_b3": None, "n_4": 0, # No bars in layer 2 "d_b4": None, "total_as": total_as.to("cm**2"), "total_bars": total_bars, "clear_spacing": self._clear_spacing.to("mm"), } ) else: # Track the combination with the maximum possible area (fallback) if A_s_layer_1 > max_fallback_area and A_s_layer_1 <= max_limit: max_fallback_area = A_s_layer_1 best_fallback_combination = { "n_1": n1, "d_b1": d_b1, "n_2": n2, "d_b2": d_b2 if n2 > 0 else None, "n_3": 0, "d_b3": None, "n_4": 0, "d_b4": None, "total_as": A_s_layer_1.to("cm**2"), "total_bars": n1 + n2, "clear_spacing": self._clear_spacing.to("mm"), } # ============================================================= # --- Layer 2 combinations (beam vs slab logic) --- # ============================================================= if self.mode == "slab": # --------------------------------------------------------- # In slab mode: # - Only two cases are considered: # (1) one single layer # (2) second layer identical to the first (mirror) # - n3 = n1, n4 = n2 when a second layer exists # --------------------------------------------------------- for has_second_layer in [False, True]: if not has_second_layer: n3, n4 = 0, 0 else: n3, n4 = n1, n2 # --- Compute total reinforcement in layer 2 ------------ A_s_layer_2 = n3 * self.rebar_areas[d_b3] + ( n4 * self.rebar_areas[d_b4] if n4 > 0 else 0 * cm**2 ) # --- Compute total reinforcement and evaluate ----------- total_as = A_s_layer_1 + A_s_layer_2 if total_as >= A_s_req and total_as <= max_limit: total_bars = n1 + n2 + n3 + n4 valid_combinations.append( { "n_1": n1, "d_b1": d_b1, "n_2": n2, "d_b2": d_b2 if n2 > 0 else None, "n_3": n3, "d_b3": d_b3 if n3 > 0 else None, "n_4": n4, "d_b4": d_b4 if n4 > 0 else None, "total_as": total_as.to("cm**2"), "total_bars": total_bars, "clear_spacing": self._clear_spacing.to("mm"), } ) else: # Track fallback combination with maximum As if total_as > max_fallback_area and total_as <= max_limit: max_fallback_area = total_as best_fallback_combination = { "n_1": n1, "d_b1": d_b1, "n_2": n2, "d_b2": d_b2 if n2 > 0 else None, "n_3": n3, "d_b3": d_b3 if n3 > 0 else None, "n_4": n4, "d_b4": d_b4 if n4 > 0 else None, "total_as": total_as.to("cm**2"), "total_bars": n1 + n2 + n3 + n4, "clear_spacing": self._clear_spacing.to("mm"), } else: # --------------------------------------------------------- # Normal beam logic (original) # --------------------------------------------------------- for n3 in [0, 2]: for n4 in range(0, self.beam.settings.max_bars_per_layer + 1): # Ensure layer 2 bars are not more than layer 1 bars if n3 + n4 > n1 + n2: continue if n3 == 0 and n4 > 0: continue A_s_layer_2 = n3 * self.rebar_areas[d_b3] + ( n4 * self.rebar_areas[d_b4] if n4 > 0 else 0 * cm**2 ) total_as = A_s_layer_1 + A_s_layer_2 if total_as >= A_s_req and total_as <= max_limit: total_bars = n1 + n2 + n3 + n4 valid_combinations.append( { "n_1": n1, "d_b1": d_b1, "n_2": n2, "d_b2": d_b2 if n2 > 0 else None, "n_3": n3, "d_b3": d_b3 if n3 > 0 else None, "n_4": n4, "d_b4": d_b4 if n4 > 0 else None, "total_as": total_as.to("cm**2"), "total_bars": total_bars, "clear_spacing": self._clear_spacing.to("mm"), } ) else: if total_as > max_fallback_area and total_as <= max_limit: max_fallback_area = total_as best_fallback_combination = { "n_1": n1, "d_b1": d_b1, "n_2": n2, "d_b2": d_b2 if n2 > 0 else None, "n_3": n3, "d_b3": d_b3 if n3 > 0 else None, "n_4": n4, "d_b4": d_b4 if n4 > 0 else None, "total_as": total_as.to("cm**2"), "total_bars": n1 + n2 + n3 + n4, "clear_spacing": self._clear_spacing.to("mm"), } # Convert valid combinations to DataFrame df = pd.DataFrame(valid_combinations) # Drop duplicate rows based on the specified columns df = df.drop_duplicates(subset=["n_1", "d_b1", "n_2", "d_b2", "n_3", "d_b3", "n_4", "d_b4"]) # If no valid combinations satisfy A_s_req, use the best fallback combination if df.empty and best_fallback_combination is not None: df = pd.DataFrame([best_fallback_combination]) # Only calculate penalties if we have valid combinations if not df.empty: modified_df = self._calculate_penalties_long_rebar(df) # Sort by 'Functional' to sort by the best options modified_df.sort_values(by=["functional"], inplace=True) modified_df.reset_index(drop=True, inplace=True) self._long_combos_df = modified_df return modified_df.head(10) else: # Return empty DataFrame with expected structure if no combinations found self._long_combos_df = df return df
[docs] def longitudinal_rebar_EN_1992_2004(self, A_s_req: Quantity) -> None: self.longitudinal_rebar_ACI_318_19(A_s_req) # TODO WE HAVE TO CHANGE THIS
def _check_spacing( self, n1: int, n2: int, d_b1: Quantity, d_b2: Quantity, effective_width: Quantity, ) -> bool: """ Checks the clear spacing between rebars in a layer. Parameters: n1 (int): Number of bars in the first group of the layer. n2 (int): Number of bars in the second group of the layer. d_b1 (Quantity): Diameter of bars in the first group of the layer. d_b2 (Quantity): Diameter of bars in the second group of the layer. effective_width (Quantity): The effective width available for bar placement. Returns: bool: True if the clear spacing satisfies the design limits, False otherwise. """ # Convert everything once to mm (floats) eff_mm = effective_width.to("mm").magnitude d1_mm = d_b1.to("mm").magnitude d2_mm = d_b2.to("mm").magnitude total_bars = n1 + n2 total_bar_width_mm = n1 * d1_mm + n2 * d2_mm clear_mm = (eff_mm - total_bar_width_mm) / (total_bars - 1) # Store as Quantity for reporting self._clear_spacing = clear_mm * mm # Determine the maximum clear spacing limit # Max required clear spacing in mm (precomputed) max_clear_spacing_mm = max( self._clear_limit_mm, self._vibrator_mm, max(d1_mm, d2_mm), ) # Check if the clear spacing is within limits return clear_mm >= max_clear_spacing_mm def _check_diameter_differences(self, diameters: list) -> bool: """ Checks that all diameter differences between bars in the list do not exceed max_diameter_diff. """ for i, d1 in enumerate(diameters): for d2 in diameters[i + 1 :]: if abs(d1 - d2) > self.beam.settings.max_diameter_diff: return False return True def _calculate_penalties_long_rebar( self, df: pd.DataFrame, alpha: float = 3.5, beta: float = 0.30, gamma: float = 0.25, delta: float = 1, epsilon: float = 0, # No penalty for beams, just slabs ) -> pd.DataFrame: """ Calculate penalties for rebar configurations and add them as columns to the DataFrame. Args: df (pd.DataFrame): The input DataFrame containing rebar configurations. alpha (float): Weight for the number of bars penalty. beta (float): Weight for the diameter difference penalty. gamma (float): Weight for the layer penalty. Returns: pd.DataFrame: The modified DataFrame with penalty columns and the final functional. """ # Adjust penalty weights depending on element type if getattr(self, "mode", "beam") == "slab": # Slabs prefer many small bars → penalize large diameters and spacing more alpha *= 0.8 # reduce area weight (slabs have smaller demand differences) beta *= 0.1 # stronger sensitivity to number of bars gamma *= 0.5 # less penalty for multi-diameter use delta *= 0.5 # allow two layers if needed epsilon = 0.75 # strong penalty on large bar sizes # Calculate minimum bars and minimum area of steel min_bars = df["total_bars"].min() min_as = df["total_as"].min() # Diameter difference penalty function def diameter_difference_penalty(row: pd.Series) -> float: """ Calculate the penalty for variation in bar diameters. Args: row (pd.Series): A row of the DataFrame. Returns: float: The standard deviation of the diameters (penalty). """ diameters = [] if row["n_1"] > 0: diameters.extend([row["d_b1"].magnitude] * row["n_1"]) if row["n_2"] > 0: diameters.extend([row["d_b2"].magnitude] * row["n_2"]) if row["n_3"] > 0: diameters.extend([row["d_b3"].magnitude] * row["n_3"]) if row["n_4"] > 0: diameters.extend([row["d_b4"].magnitude] * row["n_4"]) return np.std(diameters) if diameters else 0.0 # Layer penalty function def layer_penalty(row: pd.Series) -> int: """ Calculate the penalty for using additional layers of reinforcement. Args: row (pd.Series): A row of the DataFrame. Returns: int: 1 if additional layers are used, 0 otherwise. """ # Check if any bars are in n_3, n_4, or future layers if row["n_3"] > 0 or row["n_4"] > 0: return 1 # Penalize if additional layers are used return 0 # No penalty if only the first layer is used # Calculate penalties and add them as columns df["area_penalty"] = df.apply(lambda row: (alpha * row["total_as"] / min_as), axis=1) # Prefer moderate bar counts, where very high or very low will be penalized df["bars_penalty"] = beta * ((df["total_bars"] - min_bars) / min_bars) ** 2 df["diameter_penalty"] = gamma * df.apply(diameter_difference_penalty, axis=1) df["layer_penalty"] = delta * df.apply(layer_penalty, axis=1) # Diameter size penalty, for very large or very small min_d = df[["d_b1", "d_b2", "d_b3", "d_b4"]].stack().dropna().apply(lambda x: x.magnitude).min() df["diameter_size_penalty"] = epsilon * ( df.apply( lambda r: ( max( [ d.magnitude for d, n in zip( [r["d_b1"], r["d_b2"], r["d_b3"], r["d_b4"]], [r["n_1"], r["n_2"], r["n_3"], r["n_4"]], ) if n > 0 and d is not None ] ) / min_d - 1 ), axis=1, ) ) # Slab penalty for large spacing if getattr(self, "mode", "beam") == "slab": max_spacing_allowed = 300 # mm df["spacing_penalty"] = df["clear_spacing"].apply( lambda s: 0 if s.magnitude <= max_spacing_allowed else (s.magnitude - max_spacing_allowed) / max_spacing_allowed ) else: df["spacing_penalty"] = 0 # Calculate the final functional df["functional"] = df.apply( lambda row: ( row["area_penalty"] + row["bars_penalty"] + row["diameter_penalty"] + row["layer_penalty"] + row["diameter_size_penalty"] + row["spacing_penalty"] ), axis=1, ) return df
[docs] def longitudinal_rebar(self, A_s_req: Quantity, A_s_max: Quantity | None = None) -> Dict[str, Any]: """ Selects the appropriate longitudinal rebar method based on the design code. Args: A_s_req: Required longitudinal rebar area. A_s_max: Optional maximum allowable longitudinal rebar area. """ if self.beam.concrete.design_code == "ACI 318-19" or self.beam.concrete.design_code == "CIRSOC 201-25": return self.longitudinal_rebar_ACI_318_19(A_s_req, A_s_max) elif self.beam.concrete.design_code == "EN 1992-2004": return self.longitudinal_rebar_EN_1992_2004(A_s_req)