Source code for simdesign.rcmrf.bnsm.baselib.column

"""This module provides a base class for representing columns
within the BNSM layer and for building and exporting their OpenSees
representations.
"""
# Imports from installed packages
from abc import ABC
import numpy as np
import openseespy.opensees as ops
from typing import Dict, Literal, List, Tuple, Optional

# Imports from bnsm library
from .node import Node
from .constants import RIGID_MAT

# Imports from bdim base library
from ...bdim.baselib.column import ColumnBase as ColumnDesign

# Imports from utils library
from ....utils.units import MPa
from ....utils.misc import PRECISION, round_list
from ....utils import rcsection as rc
from ....utils.tags import sub_tag

PINCHX = 1.0
"""Pinching factor for strain (or deformation) during reloading"""
PINCHY = 1.0
"""Pinching factor for stress (or force) during reloading"""
DAMAGE1 = 0.0  # no degradation (default)
"""Damage due to ductility: D1(mu-1)"""
DAMAGE2 = 0.0  # no degradation (default)
"""Damage due to energy: D2(Eii/Eult)"""
BETA = 0.0  # from Dolšek and Fajfar 2005
"""Power used to determine the degraded unloading stiffness based on
ductility, mu-beta (optional, default=0.0)"""
HYSTERETIC_PARAMETERS = [PINCHX, PINCHY, DAMAGE1, DAMAGE2, BETA]
"""Hysteretic parameters"""


[docs] class ColumnBase(ABC): """Abstract Base Class for column implementations in BNSM layer. This class defines the common interface and core behaviour required to: (1) define a column member in the OpenSees domain, and (2) export equivalent Python and Tcl commands. The column is modelled using a force-based beam-column formulation (``forceBeamColumn``) with a plastic-hinge integration scheme (e.g., ``HingeRadau``). Geometric nonlinearity is accounted for through a P-Delta transformation (``geomTransf('PDelta', ...)``). Nonlinear behaviour is concentrated within finite plastic-hinge regions at the element ends, while the interior region remains elastic. Flexural response about the local axes (My and Mz) is represented by phenomenological moment-curvature relationships implemented using uniaxial ``Hysteretic`` materials and assembled into an aggregated hinge section. When ``capacity_design`` is False, shear behaviour is additionally modelled through ``Pinching4`` materials (for Vy and Vz), wrapped with ``MinMax`` limiters to enforce strength and deformation bounds. These shear components are included in the same aggregated hinge section. The interior region is represented by an elastic section, with optional use of cracked-section (effective) stiffness properties derived from the yield moment and curvature of the plastic hinges. Attributes ---------- design : ~simdesign.rcmrf.bnsm.baselib.column.ColumnBase Instance of column design information model. capacity_design : bool Flag to check whether capacity shear design is followed or not. bondslip_factor : float Bondslip factor. axial_force: float Considered axial force on column. ele_node_i : Node Element node at the start of column. ele_node_j : Node Element node at the end of column. ele_load: float Uniformly distributed gravity load along the column. jnt_offsets : List[float] Rigid joint offset values (dx_i, dy_i, dz_i, dx_j, dy_j, dz_j), specified with respect to the global coordinate system. cracked : None | bool | float Cracked-section (effective) flexural properties of the elastic sections: True for ``EIeff = My / phi_y`` with the yield moment and curvature of the plastic hinges (see `yield_mom_curv`), or a float as the ratio of effective to gross flexural stiffness. If None or False, gross-section properties are used. cyclic_model : bool If True, the model parameters will be adjusted for cyclic analysis. _cy : float Neutral axis depth for shear hinge calculations in local-y. _cx : float Neutral axis depth for shear hinge calculations in local-x. _Iy_eff : float Effective column moment of inertia around local-y. _Ix_eff : float Effective column moment of inertia around local-x. yield_mom_curv : Literal['empirical', 'analytical'] Yield moment and curvature of the plastic hinges: ``'empirical'`` (Panagiotakos and Fardis, 2001) or ``'analytical'`` (section analysis). Notes ----- Section view: .. code-block:: text Y (2) |__X (1) -------------- ---- | y | | | | | | | z--+ | by | | | | | | -------------- ---- |---- bx ----| Vectors defining the local axes in Global Coordinate System: vx = np.array([0.0, 0.0, 1.0]) vy = np.array([0.0, 1.0, 0.0]) vz = np.array([-1.0, 0.0, 0.0]) vecxz = np.array([-1.0, 0.0, 0.0]) Compatibility check: np.allclose(vy, np.cross(vecxz, vx)) np.allclose(vz, np.cross(vx,vy)) """ design: ColumnDesign capacity_design: bool bondslip_factor: float axial_force: float ele_node_i: Node ele_node_j: Node ele_load: float jnt_offsets: List[float] cracked: Optional[bool | float] = None cyclic_model: bool yield_mom_curv: Literal['empirical', 'analytical'] _cy: float _cx: float _Iy_eff: float _Ix_eff: float @property def ele_tag(self) -> int: """ Returns ------- int Tag of beam-column element representing the column. """ return self.design.line.tag @property def vecxz(self) -> List[float]: """ Returns ------- List[float] X, Y, and Z components of vecxz for column elements along Z (3). """ return [-1, 0, 0] @property def Ecm_q(self) -> float: """ Returns ------- float Elastic young's modulus of concrete (in base units). Note ---- Based on quality adjusted concrete strength. """ # Use quality adjusted elastic youngs modulus fc_mpa = self.design.fc_q / MPa # convert to MPa return (22000 * (fc_mpa / 10)**0.3) * MPa @property def Gcm_q(self) -> float: """ Returns ------- float Elastic shear modulus of concrete (in base units). Note ---- Based on quality adjusted concrete strength. """ return self.Ecm_q / (2 * (1 + self.design.concrete.POISSONS_RATIO)) @property def Acc(self) -> float: """ Returns ------- float Confined concrete area. """ bx = self.design.bx - 2 * (self.design.cover_q + self.design.dbh_q / 2) by = self.design.by - 2 * (self.design.cover_q + self.design.dbh_q / 2) return float(bx * by) @property def rhol_q(self) -> float: """ Returns ------- float In situ (quality adjusted) longitudinal reinforcement area ratio. """ Abl_cor = (np.pi * self.design.dbl_cor_q**2) / 4 Abl_int = (np.pi * self.design.dbl_int_q**2) / 4 nbl_int = 2 * (self.design.nbly_int_q + self.design.nblx_int_q) nbl_cor = 4 return (nbl_cor * Abl_cor + nbl_int * Abl_int) / self.design.Ag @property def rhol_q_(self) -> float: """ Returns ------- float In situ (quality adjusted) longitudinal reinforcement area ratio. Computed using confined concrete dimensions. """ Abl_cor = (np.pi * self.design.dbl_cor_q**2) / 4 Abl_int = (np.pi * self.design.dbl_int_q**2) / 4 nbl_int = 2 * (self.design.nbly_int_q + self.design.nblx_int_q) nbl_cor = 4 return (nbl_cor * Abl_cor + nbl_int * Abl_int) / self.Acc @property def rhoh_x_q(self) -> float: """ Returns ------- float In situ (quality adjusted) transverse reinforcement area (in x) ratio. """ Abh_x = self.design.nbh_x_q * (np.pi * self.design.dbh_q**2) / 4 return Abh_x / (self.design.sbh_q * self.design.by) @property def rhoh_y_q(self) -> float: """ Returns ------- float In situ (quality adjusted) transverse reinforcement area (in y) ratio. """ Abh_y = self.design.nbh_y_q * (np.pi * self.design.dbh_q**2) / 4 return Abh_y / (self.design.sbh_q * self.design.bx) @property def rhoh_x_q_(self) -> float: """ Returns ------- float In situ (quality adjusted) transverse reinforcement area (in x) ratio. Computed using confined concrete dimensions. """ # Total transverse reinforcement area Av = self.design.nbh_x_q * (np.pi * self.design.dbh_q**2) / 4 # Transverse reinforcement spacing s = self.design.sbh_q # Core diameter dc = self.design.by - 2 * (self.design.cover_q + self.design.dbh_q / 2) return Av / (s * dc) @property def rhoh_y_q_(self) -> float: """ Returns ------- float In situ (quality adjusted) transverse reinforcement area (in y) ratio. Computed using confined concrete dimensions. """ # Total transverse reinforcement area Av = self.design.nbh_y_q * (np.pi * self.design.dbh_q**2) / 4 # Transverse reinforcement spacing s = self.design.sbh_q # Core diameter dc = self.design.bx - 2 * (self.design.cover_q + self.design.dbh_q / 2) return Av / (s * dc) @property def mz_mat_tag(self) -> int: """Uniaxial material tag for the flexural hinge about local-z (Mz).""" return sub_tag(self.design.line.tag, 91) @property def my_mat_tag(self) -> int: """Uniaxial material tag for the flexural hinge about local-y (My).""" return sub_tag(self.design.line.tag, 93) @property def vy_mat_tag_(self) -> int: """Pinching4 material tag for the shear hinge in local-y (Vy).""" return sub_tag(self.design.line.tag, 94) @property def vy_mat_tag(self) -> int: """MinMax material tag for the shear hinge in local-y (Vy).""" return sub_tag(self.design.line.tag, 95) @property def vz_mat_tag_(self) -> int: """Pinching4 material tag for the shear hinge in local-z (Vz).""" return sub_tag(self.design.line.tag, 96) @property def vz_mat_tag(self) -> int: """MinMax material tag for the shear hinge in local-z (Vz).""" return sub_tag(self.design.line.tag, 97) @property def elastic_sec_tag(self) -> int: """Section tag for the elastic (interior) section.""" return sub_tag(self.design.line.tag, 90) @property def inelastic_sec_tag(self) -> int: """Section tag for the inelastic aggregated section.""" return sub_tag(self.design.line.tag, 92) @property def interior_sec_tag(self) -> int: """Section tag for the interior section with shear springs.""" return sub_tag(self.design.line.tag, 93) @property def int_tag(self) -> int: """Beam integration tag used by OpenSees.""" return self.design.line.tag @property def geo_transf_tag(self) -> int: """Geometric transformation tag used by OpenSees.""" return self.design.line.tag @property def clear_length(self) -> float: """Length of the deformable element, without the rigid joint offsets.""" start = np.array(self.ele_node_i.coordinates) + self.jnt_offsets[:3] end = np.array(self.ele_node_j.coordinates) + self.jnt_offsets[3:] return float(np.linalg.norm(end - start)) def __init__( self, design: ColumnDesign, bondslip_factor: float, capacity_design: bool, load_factors: Dict[Literal['G', 'Q'], float], cyclic_model: bool = False, cracked: Optional[bool | float] = None, yield_mom_curv: Literal['empirical', 'analytical'] = 'empirical' ) -> None: """Initialize the Column object. Parameters ---------- design : ~simdesign.rcmrf.bnsm.baselib.column.ColumnBase Instance of column design information model. bondslip_factor : float Bondslip factor considered while defining plastic hinges. capacity_design : bool Flag to check whether capacity shear design is followed or not. load_factors : Dict[Literal['G', 'Q'], float] Load factors used to compute gravity loads/forces on the column. cyclic_model : bool, optional If True, the model parameters will be adjusted for cyclic analysis. By default False. cracked : None | bool | float, optional Cracked-section (effective) flexural properties of the elastic sections: True for My / phi_y of the plastic hinges (see `yield_mom_curv`), or a float as the ratio of effective to gross flexural stiffness. If None or False, gross-section properties are used. By default None. yield_mom_curv : Literal['empirical', 'analytical'], optional Yield moment and curvature of the plastic hinges: 'empirical' from the expressions of Panagiotakos and Fardis (2001), 'analytical' from a moment-curvature section analysis. By default 'empirical'. """ self.design = design self.bondslip_factor = bondslip_factor self.capacity_design = capacity_design self.cyclic_model = cyclic_model self.cracked = cracked self.yield_mom_curv = yield_mom_curv self.axial_force = -(load_factors['G'] * design.hinge_Ng + load_factors['Q'] * design.hinge_Nq) self.ele_load = float(design.self_wg * load_factors['G']) self.jnt_offsets = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # Set cracked section properties self._set_cracked_properties()
[docs] def add_to_ops(self) -> None: """Adds column components to the OpenSees domain (i.e, column element and nodes). Notes ----- Same hinge materials are used at both ends. """ # Define geometric transformation ops.geomTransf(*self._get_geo_transf_inputs()) # Create the section materials ops.uniaxialMaterial(*self._get_rot_hinge_mat_inputs('x')) ops.uniaxialMaterial(*self._get_rot_hinge_mat_inputs('y')) if not self.capacity_design: vy_mat, minmax_vy = self._get_shear_hinge_mat_inputs('y') vz_mat, minmax_vz = self._get_shear_hinge_mat_inputs('x') ops.uniaxialMaterial(*vy_mat) ops.uniaxialMaterial(*vz_mat) ops.uniaxialMaterial(*minmax_vy) ops.uniaxialMaterial(*minmax_vz) # Create element sections ops.section(*self._get_elastic_sec_inputs()) ops.section(*self._get_inelastic_sec_inputs()) if not self.capacity_design: # Interior section with shear springs ops.section(*self._get_interior_sec_inputs()) # Create beam integration ops.beamIntegration(*self._get_int_inputs()) # Create the element ops.element(*self._get_ele_inputs())
[docs] def to_py(self) -> List[str]: """Gets the Python commands to construct column components in OpenSees domain (i.e, column element and nodes). Returns ------- List[str] List of Python commands for constructing the components of column object in OpenSees. Notes ----- Same hinge materials are used at both ends. """ # Define geometric transformation content = ['# Create geometric transformation'] transf_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_geo_transf_inputs() ) content.append(f"ops.geomTransf({transf_inputs})") # Create the section materials content.append('# Create uniaxial materials') mz_mat_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_rot_hinge_mat_inputs('x') ) my_mat_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_rot_hinge_mat_inputs('y') ) content.append(f"ops.uniaxialMaterial({mz_mat_inputs})") content.append(f"ops.uniaxialMaterial({my_mat_inputs})") if not self.capacity_design: vy_mat, minmax_vy = self._get_shear_hinge_mat_inputs('y') vz_mat, minmax_vz = self._get_shear_hinge_mat_inputs('x') vy_mat_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in vy_mat ) vz_mat_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in vz_mat ) minmax_vy_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in minmax_vy ) minmax_vz_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in minmax_vz ) content.append(f"ops.uniaxialMaterial({vy_mat_inputs})") content.append(f"ops.uniaxialMaterial({minmax_vy_inputs})") content.append(f"ops.uniaxialMaterial({vz_mat_inputs})") content.append(f"ops.uniaxialMaterial({minmax_vz_inputs})") # Create sections content.append('# Create element sections') elastic_sec_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_elastic_sec_inputs() ) inelastic_sec_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_inelastic_sec_inputs() ) content.append(f"ops.section({elastic_sec_inputs})") content.append(f"ops.section({inelastic_sec_inputs})") if not self.capacity_design: # Interior section with shear springs interior_sec_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_interior_sec_inputs() ) content.append(f"ops.section({interior_sec_inputs})") # Create beam integration content.append('# Create integration scheme') int_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_int_inputs() ) content.append(f"ops.beamIntegration({int_inputs})") # Create column element content.append('# Create element') ele_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_ele_inputs() ) content.append(f"ops.element({ele_inputs})") return content
[docs] def to_tcl(self) -> List[str]: """Gets the Tcl commands to construct column components in OpenSees domain (i.e, column element and nodes). Returns ------- List[str] List of Tcl commands for constructing the components of column object in OpenSees. Notes ----- Same hinge materials are used at both ends. """ # Define geometric transformation content = ['# Create geometric transformation'] transf_inputs = ' '.join(f"{item}" for item in self._get_geo_transf_inputs()) content.append(f"geomTransf {transf_inputs}") # Create the section materials content.append('# Create uniaxial materials') mz_mat_inputs = ' '.join( f"{item}" for item in self._get_rot_hinge_mat_inputs('x')) my_mat_inputs = ' '.join( f"{item}" for item in self._get_rot_hinge_mat_inputs('y')) content.append(f"uniaxialMaterial {mz_mat_inputs}") content.append(f"uniaxialMaterial {my_mat_inputs}") if not self.capacity_design: vy_mat, minmax_vy = self._get_shear_hinge_mat_inputs('y') vz_mat, minmax_vz = self._get_shear_hinge_mat_inputs('x') vy_mat_inputs = ' '.join(f"{item}" for item in vy_mat) vz_mat_inputs = ' '.join(f"{item}" for item in vz_mat) minmax_vy_inputs = ' '.join(f"{item}" for item in minmax_vy) minmax_vz_inputs = ' '.join(f"{item}" for item in minmax_vz) content.append(f"uniaxialMaterial {vy_mat_inputs}") content.append(f"uniaxialMaterial {minmax_vy_inputs}") content.append(f"uniaxialMaterial {vz_mat_inputs}") content.append(f"uniaxialMaterial {minmax_vz_inputs}") # Create sections content.append('# Create element sections') elastic_sec_inputs = ' '.join( f"{item}" for item in self._get_elastic_sec_inputs()) inelastic_sec_inputs = ' '.join( f"{item}" for item in self._get_inelastic_sec_inputs()) content.append(f"section {elastic_sec_inputs}") content.append(f"section {inelastic_sec_inputs}") if not self.capacity_design: # Interior section with shear springs interior_sec_inputs = ' '.join( f"{item}" for item in self._get_interior_sec_inputs()) content.append(f"section {interior_sec_inputs}") # Create beam integration content.append('# Create integration scheme') int_inputs = ' '.join( f"{item}" for item in self._get_int_inputs()) content.append(f"beamIntegration {int_inputs}") # Create column element content.append('# Create element') ele_inputs = ' '.join( f"{item}" for item in self._get_ele_inputs()) content.append(f"element {ele_inputs}") return content
[docs] def add_grav_loads_to_ops(self) -> None: """Adds gravity load objects to the OpenSees domain (i.e, point loads at both ends). """ point_load = round(self.ele_load * self.design.H / 2, PRECISION) load_values = [0.0, 0.0, -point_load, 0.0, 0.0, 0.0] ops.load(self.ele_node_i.tag, *load_values) ops.load(self.ele_node_j.tag, *load_values)
[docs] def to_py_grav_loads(self) -> List[str]: """Gets the Python commands to construct column gravity load object in OpenSees domain (i.e, point loads at both ends). Returns ------- List[str] List of Python commands for adding column gravity loads to OpenSees. """ point_load = round(self.ele_load * self.design.H / 2, PRECISION) load_values = f"0.0, 0.0, {-point_load}, 0.0, 0.0, 0.0" return [ f"ops.load({self.ele_node_i.tag}, {load_values})", f"ops.load({self.ele_node_j.tag}, {load_values})" ]
[docs] def to_tcl_grav_loads(self) -> List[str]: """Gets the Tcl commands to construct column gravity load object in OpenSees domain (i.e, point loads at both ends). Returns ------- List[str] List of Tcl commands for adding column gravity loads to OpenSees. """ point_load = round(self.ele_load * self.design.H / 2, PRECISION) load_values = f"0.0 0.0 {-point_load} 0.0 0.0 0.0" return [ f"load {self.ele_node_i.tag} {load_values}", f"load {self.ele_node_j.tag} {load_values}" ]
def _get_geo_transf_inputs(self) -> List[str | float]: """Builds the OpenSees ``geomTransf`` command arguments. Returns ------- List[str | float | int] Inputs for ``ops.geomTransf``, including the transformation type, tag, ``vecxz`` definition, and optional joint offsets. """ inputs = ['PDelta', self.geo_transf_tag] \ + self.vecxz + ['-jntOffset'] + self.jnt_offsets return inputs def _get_ele_inputs(self) -> List[str | int]: """Retrieves column element inputs. Returns ------- List[str | int] List of column element inputs. """ ele_inputs = [ 'forceBeamColumn', self.design.line.tag, self.ele_node_i.tag, self.ele_node_j.tag, self.geo_transf_tag, self.int_tag ] return ele_inputs def _get_elastic_sec_inputs(self) -> List[str | float | int]: """Retrieves elastic column section inputs. Returns ------- List[str | float | int] List of elastic column section inputs. """ # Cracked section properties if self.cracked: Iz = self._Ix_eff Iy = self._Iy_eff else: Iz = self.design.Ix Iy = self.design.Iy # Set the inputs sec_inputs = round_list([ 'Elastic', self.elastic_sec_tag, self.Ecm_q, self.design.Ag, Iz, Iy, self.Gcm_q, self.design.J ]) return sec_inputs def _get_inelastic_sec_inputs(self) -> List[str | int]: """Retrieves inputs for inelastic aggregated column section. Returns ------- List[str | int] List of inputs for inelastic aggregated column section. """ sec_inputs = [ 'Aggregator', self.inelastic_sec_tag, RIGID_MAT, 'P', self.mz_mat_tag, 'Mz', self.vy_mat_tag, 'Vy', self.my_mat_tag, 'My', self.vz_mat_tag, 'Vz', RIGID_MAT, 'T' ] if self.capacity_design: sec_inputs[6], sec_inputs[10] = RIGID_MAT, RIGID_MAT return sec_inputs def _get_interior_sec_inputs(self) -> List[str | int]: """Retrieves inputs for the interior section with shear springs. Returns ------- List[str | int] List of inputs for the aggregated interior section, i.e., the elastic section aggregated with the shear force - shear strain materials (Vy and Vz). Notes ----- The shear materials define shear force vs. average shear strain of the member (Mergos and Kappos, 2012; Zimos et al., 2015). Including them in the interior section integrates the shear strains along the whole member length rather than only over the plastic hinge lengths. """ sec_inputs = [ 'Aggregator', self.interior_sec_tag, self.vy_mat_tag, 'Vy', self.vz_mat_tag, 'Vz', '-section', self.elastic_sec_tag ] return sec_inputs def _get_int_inputs(self) -> List[str | float | int]: """Retrieves column integration inputs. Returns ------- List[str | float | int] List of column integration inputs. References ---------- Scott, M. H., & Fenves, G. L. (2006). Plastic Hinge Integration Methods for Force-Based Beam-Column Elements. Journal of Structural Engineering, 132(2), 244-252. https://doi.org/10.1061/(asce)0733-9445(2006)132:2(244) """ # Plastic hinge length based on Priestley et al. 2007 Lp = self._get_lp() # Beam integration inputs # Interior section, including the shear springs if they are used if self.capacity_design: interior_sec_tag = self.elastic_sec_tag else: interior_sec_tag = self.interior_sec_tag int_inputs = [ 'HingeRadau', self.int_tag, self.inelastic_sec_tag, Lp, self.inelastic_sec_tag, Lp, interior_sec_tag ] return int_inputs def _get_shear_hinge_mat_inputs(self, axis: Literal['x', 'y'] ) -> Tuple[List[float], List[float]]: """Gets inputs the shear plastic hinge materials for given axis. The model is based on Zimos et al. (2015). Parameters ---------- axis : Literal['x', 'y'] The local axis considered for the calculations. Returns ------- shear_mat_inputs : List[float] Pinching4 material model inputs for the plastic hinge describing shear behaviour in `axis`. minmax_mat_inputs : List[float] Inputs for MinMax uniaxialMaterial describing the axial failure in `axis`. References ---------- Zimos, D. K., Mergos, P. E., & Kappos, A. J. (2015). Shear hysteresis model for reinforced concrete elements including the post-peak range. Proceedings of the COMPDYN. https://doi.org/10.7712/120115.3565.1184 Sezen, H. and Moehle, J.P. (2004). Shear Strength Model for Lightly Reinforced Concrete Columns. J Struct Eng 130:1692-1703. https://doi.org/10.1061/(asce)0733-9445(2004)130:11(1692) Priestley, M. N., Verma, R., & Xiao, Y. (1994). Seismic shear strength of reinforced concrete columns. Journal of structural engineering, 120(8), 2310-2329. https://doi.org/10.1061/(ASCE)0733-9445(1994)120:8(2310) Mergos, P. E., & Kappos, A. J. (2012). A gradual spread inelasticity model for R/C beam-columns, accounting for flexure, shear and anchorage slip. Engineering Structures, 44, 94-106. https://doi.org/10.1016/j.engstruct.2012.05.035 O'Reilly, G. J. (2016). Performance-based seismic assessment and retrofit of existing RC frame buildings in Italy (Doctoral dissertation, IUSS Pavia). """ if axis == 'y': # Section height h = self.design.by # along y # Section width b = self.design.bx # along x # Number of horizontal bars (stirrup legs) nbh = self.design.nbh_y_q # Transverse reinforcement area ratio rhoh = self.rhoh_y_q # Neutral axis depth (NOTE: the same value from M-Phi analysis) c = self._cy # An approximate conservative value # The integer tag for the MinMax uniaxial material defining failure minmax_mat_tag = self.vy_mat_tag # The integer tag for the material describing shear behaviour in # local -y (corresponds to y in ops) shear_mat_tag = self.vy_mat_tag_ elif axis == 'x': # Section height h = self.design.bx # along x # Section width b = self.design.by # along y # Number of horizontal bars (stirrup legs) nbh = self.design.nbh_x_q # Transverse reinforcement area ratio rhoh = self.rhoh_x_q # Neutral axis depth (NOTE: the same value from M-Phi analysis) c = self._cx # An approximate conservative value # The integer tag for the MinMax uniaxial material defining failure minmax_mat_tag = self.vz_mat_tag # The integer tag for the material describing shear behaviour in # local -x (corresponds to z in ops) shear_mat_tag = self.vz_mat_tag_ # Gross corss-section area Ag = self.design.Ag # Concrete compressive strength fc = self.design.fc_q # in base units # Concrete elastic modulus Ec = self.Ecm_q # Concrete shear modulus Gc = self.Gcm_q # Transverse reinforcement yield strength fsyh = self.design.fsyh_q # in base units # Steel shear modulus Es = self.design.steel.Es # Diameter of internal reinforcement dbl_int = self.design.dbl_int_q # Longitudinal reinforcement ratio rhol = self.rhol_q # Longitudinal reinforcement area Asl = rhol * Ag # Average long. reinf. bar diameter nbl_int = 2 * (self.design.nbly_int_q + self.design.nblx_int_q) nbl_cor = 4 dbl_ave = np.sqrt(4 * Asl / (nbl_int + nbl_cor) / np.pi) # Longitudinal reinforcement yield strength fsyl = self.design.fsyl_q # in base units # Concrete cover cover = self.design.cover_q # Stirrup spacing sbh = self.design.sbh_q # Horizontal (stirrup) reinforcement diameter dbh = self.design.dbh_q # Core height measured to centerline of transverse reinforcement hc = h - 2*(cover + dbh/2) # Core width measured to centerline of transverse reinforcement bc = b - 2*(cover + dbh/2) # Transverse reinforcement area Av = nbh * np.pi * (dbh**2) / 4 # Distance between top concrete fiber and bottom tension steel d = h - (cover + dbh + dbl_int/2) # Clear length of column, without the rigid joint offsets Ln = self.clear_length # Shear span, assuming equal to 50% of the free length of the element Ls = Ln / 2 # NOTE: Could be varied with intensity of loading, but ok. # Compressive axial load (+) - Positive in compression P = max(-self.axial_force, 0) # Normalized axial load ratio (dimensionless) nu = P / (self.design.Ag * fc) # Onset of cracking - Sezen and Moehle (2004), with the concrete # tensile strength of Collins and Mitchell (1997) V1, gamm_1 = rc.get_shear_cracking_sezen_moehle2004( fc, Gc, Ag, d, Ls, P) # Onset of maximum shear force - Priestley et al. (1994) Vc, Vp, Vs = rc.get_shear_strength_priestley1994( fc, Ag, h, c, Ls, Av, fsyh, hc, sbh, P) # Maximum shear strength Vmax = Vc + Vp + Vs V2 = Vmax # NOTE: V1 is limited to 80% of V2, a superficial adjustment. It keeps # the backbone monotonic: with V1 close to V2, gamm_2 could fall below # gamm_1, and the reduction V2 = 0.9 V2 below could make V2 < V1. if V1 > 0.8 * V2: # Rare, e.g., squat columns under high axial load V1 = 0.8 * V2 gamm_1 = V1 / (Gc * (0.8 * Ag)) # Adjust gamm_1 accordingly # Cracked shear stiffness, shear strains at the maximum shear force # and at the onset of shear failure - Mergos and Kappos (2012) _, gamm_2, gamm_3 = rc.get_shear_strains_mergos_kappos2012( V1, gamm_1, V2, Es, Ec, b, hc, rhoh, fsyh, fc, Ls, h, nu) V3 = Vmax # NOTE: Followings are superficial adjustments for numerical purposes if gamm_3 > gamm_2: # 2nd and 3rd points are not the same # OpenSees may complain due to zero shear stiffness V2 = 0.9 * V2 # To avoid 0 stiffness between 2nd & 3rd points elif gamm_3 == gamm_2: # 2nd and 3rd points are the same gamm_2 = gamm_2 - 0.1 * (gamm_2 - gamm_1) V2 = V2 - 0.1 * (V2 - V1) # Onset of axial failure - Zimos et al. (2015) Spp, gamm_tpp = rc.get_shear_post_peak_zimos2015( Vmax, P, b, d, Ag, hc * bc, rhol, rhoh, fc, fsyl, fsyh, sbh, dbl_ave, nu) # Residual shear force, at least 10% of the maximum - Eqn. 3 V4, gamm_tpp = rc.get_shear_residual_zimos2015(V3, Spp, gamm_tpp) # Residual shear strain gamm_4 = gamm_3 + gamm_tpp # Pinching4 material backbone parameters ePf1, ePf2, ePf3, ePf4 = V1, V2, V3, V4 ePd1, ePd2, ePd3, ePd4 = gamm_1, gamm_2, gamm_3, gamm_4 eNf1, eNf2, eNf3, eNf4 = -V1, -V2, -V3, -V4 eNd1, eNd2, eNd3, eNd4 = -gamm_1, -gamm_2, -gamm_3, -gamm_4 # Pinching4 material Unloading/Reloading parameters # Ratio of maximum deformation at which reloading begins rDispP, rDispN = 0.4, 0.4 # from Table 2.3. in O'Reilly 2016 # Ratio of envelope force at which reloading begins rForceP, rForceN = 0.2, 0.2 # from Table 2.3. in O'Reilly 2016 # Ratio of monotonic strength developed upon unloading uForceP, uForceN = 0.0, 0.0 # from Table 2.3. in O'Reilly 2016 # Coefficients for Unloading Stiffness degradation gK1, gK2, gK3, gK4, gKLim = 0.0, 0.0, 0.0, 0.0, 0.0 # No degredation # Coefficients for Reloading Stiffness degradation gD1, gD2, gD3, gD4, gDLim = 0.0, 0.0, 0.0, 0.0, 0.0 # No degredation # Coefficients for Strength degradation gF1, gF2, gF3, gF4, gFLim = 0.0, 0.0, 0.0, 0.0, 0.0 # No degredation # Cyclic energy dissipation factor (E_cyclic=gE*E_monotonic) gE = 0.0 # No degredation # Damage type dmgType = "energy" shear_mat_inputs = [ 'Pinching4', shear_mat_tag, ePf1, ePd1, ePf2, ePd2, ePf3, ePd3, ePf4, ePd4, eNf1, eNd1, eNf2, eNd2, eNf3, eNd3, eNf4, eNd4, rDispP, rForceP, uForceP, rDispN, rForceN, uForceN, gK1, gK2, gK3, gK4, gKLim, gD1, gD2, gD3, gD4, gDLim, gF1, gF2, gF3, gF4, gFLim, gE, dmgType ] # MinMax material inputs minmax_mat_inputs = [ 'MinMax', minmax_mat_tag, shear_mat_tag, '-min', -gamm_4, '-max', gamm_4 ] # Rounding to precision minmax_mat_inputs = round_list(minmax_mat_inputs) shear_mat_inputs = round_list(shear_mat_inputs) return shear_mat_inputs, minmax_mat_inputs def _get_rot_hinge_mat_inputs(self, axis: Literal['x', 'y'] ) -> List[float | str]: """Gets the plastic hinge material properties for given axis. Parameters ---------- axis : Literal['x', 'y'] The local axis considered for the calculations. Returns ------- rot_mat_inputs : List[float | str] Hysteretic material model inputs for the plastic hinge describing behaviour in flexure around `axis`. The curvatures are adjusted so that the member yields at the yield rotation (see ``_get_phiy_adjusted``), and the plastic rotations are spread over the plastic hinge length. """ if axis == 'x': # The integer tag for the material describing flexure behaviour # around local -x (corresponds to z in ops) flex_mat_tag = self.mz_mat_tag elif axis == 'y': # The integer tag for the material describing flexure behaviour # around local -y axis (corresponds to y in ops) flex_mat_tag = self.my_mat_tag # Backbone of the plastic hinges ( _, My, Mc, Mr, theta_y, theta_cap_pl, theta_pc, ) = self._get_rot_hinge_props(axis) # Rotation values theta_1 = theta_y theta_2 = theta_1 + theta_cap_pl theta_3 = theta_2 + theta_pc # Curvature values # Accounting for all the flexibility sources of the yield rotation Lp = self._get_lp() phi_1 = self._get_phiy_adjusted(My, theta_y, Lp, axis) phi_2 = (theta_2 - theta_1) / Lp + phi_1 phi_3 = (theta_3 - theta_1) / Lp + phi_1 # Material inputs rot_mat_inputs = [ 'Hysteretic', flex_mat_tag, My, phi_1, Mc, phi_2, Mr, phi_3, -My, -phi_1, -Mc, -phi_2, -Mr, -phi_3, *HYSTERETIC_PARAMETERS ] # Rounding to precision rot_mat_inputs = round_list(rot_mat_inputs) return rot_mat_inputs def _get_rot_hinge_props(self, axis: Literal['x', 'y']) -> List[float]: """Computes the backbone parameters of the rotational plastic hinge about the specified local axis. Parameters ---------- axis : Literal['x', 'y'] Local bending axis considered for the calculations. Returns ------- props : List[float] Ordered list of backbone parameters for the specified axis: 0. ``phi_y`` Yield curvature. 1. ``My`` Yield moment. 2. ``Mc`` Capping (maximum) moment. 3. ``Mr`` Residual moment. 4. ``theta_y`` Yield rotation. 5. ``theta_cap_pl`` Plastic rotation capacity up to capping. 6. ``theta_pc`` Post-capping rotation capacity. Notes ----- The yield rotation follows EN 1998-3 Eq. A.10b. Its shear distortion term is left out if the shear deformations are modelled with shear springs, i.e., if ``capacity_design`` is False. References ---------- Haselton, C. B., Liel, A. B., Lange, S. T., & Deierlein, G. G. (2008). Beam-column element model calibrated for predicting flexural response leading to global collapse of RC frame buildings. Pacific Earthquake Engineering Research Center, University of California, Berkeley, CA. Haselton, C. B., Liel, A. B., Taylor-Lange, S. C., & Deierlein, G. G. (2016). Calibration of model to simulate response of reinforced concrete beam-columns to collapse. ACI Structural Journal, 113(6). CEN (2005) Eurocode 8: Design of structures for earthquake resistance - Part 3: Assessment and retrofitting of existing buildings. Brussels, Belgium Panagiotakos, T. B., & Fardis, M. N. (2001). Deformations of reinforced concrete members at yielding and ultimate. Structural Journal, 98(2), 135-148. ACI CODE 318-25. (2025). Building Code for Structural Concrete-Code Requirements and Commentary. American Concrete Institute. """ if axis == 'x': # Section height and width h, b = self.design.by, self.design.bx # along y, along x # Transverse reinforcement ratio rhoh = self.rhoh_y_q # Stirrups are along -y axis elif axis == 'y': # Section height and width h, b = self.design.bx, self.design.by # along x, along y # Transverse reinforcement ratio rhoh = self.rhoh_x_q # Stirrups are along -x axis # Diameter of corner reinforcement dbl_cor = self.design.dbl_cor_q # Concrete compressive strength in base units fc = self.design.fc_q # Longitudinal steel yield strength in base units fsyl = self.design.fsyl_q # Clear length of column, without the rigid joint offsets Ln = self.clear_length # Stirrup spacing sbh = self.design.sbh_q # Longitudinal reinforcement ratio rhol = self.rhol_q # Compressive axial load (+) Nu = max(-self.axial_force, 0) # Compute yield moment and curvature My, phi_y, _ = self._get_my_phiy(axis) # Shear span, assuming equal to 50% of the free length of the element Ls = Ln / 2 # NOTE: Could be varied with intensity of loading, but ok. # Axial load ratio niu = Nu / (self.design.Ag * fc) # Yield rotation capacity - EN 1998-3:2004 - Equation A.10b # (tension shift factor from the shear cracking resistance - EN 1992-1) As_tens, dd = self._get_tension_rebar(axis) if self.capacity_design: include_shear = True else: include_shear = False theta_y = rc.get_yield_rot_eurocode8( phi_y, h, Ls, fc, fsyl, dbl_cor, self.bondslip_factor, My=My, b=b, As_tens=As_tens, dd=dd, Nu=Nu, include_shear=include_shear) # Maximum moment capacity and plastic rotation capacity by # Haselton et al. 2016 Mc, theta_cap_pl = rc.get_capping_mom_plastic_rot_haselton2016( My, fc, fsyl, rhol, rhoh, sbh, dbl_cor, niu, self.bondslip_factor, cyclic_model=self.cyclic_model) # Residual moment and post-capping rotation capacity by # Haselton et al. 2016 Mr, theta_pc = rc.get_residual_mom_post_capping_rot_haselton2016( Mc, rhoh, niu, cyclic_model=self.cyclic_model) # Save to list props = [ float(phi_y), float(My), float(Mc), float(Mr), float(theta_y), float(theta_cap_pl), float(theta_pc), ] return props def _get_phiy_adjusted(self, My: float, theta_y: float, Lp: float, axis: Literal['x', 'y']) -> float: """Computes the yield curvature of the plastic hinge sections based on yield rotation capacity. The hinges account for all the flexibility sources of the yield rotation ``theta_y`` (EN 1998-3 Eq. A.10b: tension shift, shear distortion and bond-slip). The elastic interior gives the flexural yield rotation ``My·Ls/(3·EI)``, and the hinge carries the rest of ``theta_y`` over ``Lp``, so that the member reaches ``theta_y`` when the hinge yields. Parameters ---------- My : float Yield moment. theta_y : float Yield rotation. Lp : float Plastic hinge length. axis : Literal['x', 'y'] The local axis considered for the calculations. Returns ------- float Yield curvature of the plastic hinge sections. """ sec_inputs = self._get_elastic_sec_inputs() ls = self.clear_length / 2 # Shear span, as in _get_backbone EIeff = self.Ecm_q * (sec_inputs[4] if axis == 'x' else sec_inputs[5]) dtheta = max(theta_y - My * ls / (3 * EIeff), 0.0) phi_y = My / EIeff + dtheta / Lp return phi_y def _get_lp(self) -> float: """Computes the plastic hinge length of the column. Returns ------- Lp : float Plastic hinge length. References ---------- Priestley, M. J. N., Calvi, G. M., & Kowalsky, M. J. (2007). *Displacement-based seismic design of structures*. IUSS Press. """ # Shear span, assuming equal to 50% of the free length of the element Ls = self.clear_length / 2 return rc.get_lp_priestley_2007( Ls, self.design.fsyl_q, self.design.dbl_int_q) def _get_my_phiy( self, axis: Literal['x', 'y'] ) -> Tuple[float, float, float]: """Computes the yield moment and curvature of the column section for bending around `axis`. Parameters ---------- axis : Literal['x', 'y'] The local axis considered for the calculations. Returns ------- My : float Yield moment. phi_y : float Yield curvature. cy : float Neutral axis depth at yield. See Also -------- :meth:`_get_my_phiy_empirical` : Empirical expressions, used if ``yield_mom_curv='empirical'``. :meth:`_get_my_phiy_analytical` : Section analysis, used if ``yield_mom_curv='analytical'``. """ if self.yield_mom_curv == 'analytical': return self._get_my_phiy_analytical(axis) return self._get_my_phiy_empirical(axis) def _get_tension_rebar( self, axis: Literal['x', 'y'] ) -> Tuple[float, float]: """Gets the longitudinal reinforcement on a single side of the column section for bending around `axis`. Parameters ---------- axis : Literal['x', 'y'] The local axis considered for the calculations. Returns ------- As_tens : float Area of the corner and internal reinforcement on a single side. dd : float Distance from the compression fiber to the centroid of the reinforcement on the tension side. """ if axis == 'x': h = self.design.by # Section height, along y nbl_int = self.design.nblx_int_q elif axis == 'y': h = self.design.bx # Section height, along x nbl_int = self.design.nbly_int_q # Number of corner reinforcement (on a single side) nbl_cor = 2 # Diameter of corner reinforcement dbl_cor = self.design.dbl_cor_q # Diameter of internal reinforcement dbl_int = self.design.dbl_int_q As_tens = (nbl_cor * ((0.25 * np.pi) * dbl_cor**2) + nbl_int * ((0.25 * np.pi) * dbl_int**2)) dd = (h - self.design.cover_q - self.design.dbh_q - 0.5 * dbl_cor) return As_tens, dd def _get_my_phiy_empirical( self, axis: Literal['x', 'y'] ) -> Tuple[float, float, float]: """Computes the yield moment and curvature of the column section for bending around `axis` with the empirical expressions of Panagiotakos and Fardis (2001). Parameters ---------- axis : Literal['x', 'y'] The local axis considered for the calculations. Returns ------- My : float Yield moment. phi_y : float Yield curvature. cy : float Neutral axis depth at yield. See Also -------- :func:`~simdesign.utils.rcsection.get_my_phiy_panagiotakos_fardis2001` References ---------- Panagiotakos, T. B., & Fardis, M. N. (2001). Deformations of reinforced concrete members at yielding and ultimate. Structural Journal, 98(2), 135-148. """ if axis == 'x': # Section height h = self.design.by # along y # Section width b = self.design.bx # along x # Number of internal web reinforcement (intermediate) nbl_v = self.design.nbly_int_q elif axis == 'y': # Section height h = self.design.bx # along x # Section width b = self.design.by # along y # Number of internal web reinforcement (intermediate) nbl_v = self.design.nblx_int_q # Diameter of internal reinforcement dbl_int = self.design.dbl_int_q # Tension and compression reinforcement (equal on both sides), and # distance from the compression fiber to the tension rebars As_tens, dd = self._get_tension_rebar(axis) As_comp = As_tens # Web reinforcement (intermediate) As_int = 2 * nbl_v * (0.25 * np.pi * dbl_int**2) # Compressive axial load (+) Nu = max(-self.axial_force, 0) return rc.get_my_phiy_panagiotakos_fardis2001( h=h, b=b, fc=self.design.fc_q, Ec=self.Ecm_q, Es=self.design.Es, fsyl=self.design.fsyl_q, As_tens=As_tens, As_comp=As_comp, dd=dd, As_int=As_int, Nu=Nu) def _get_my_phiy_analytical(self, axis: Literal['x', 'y'] ) -> Tuple[float, float, float]: """Computes the yield moment and curvature of the column section for bending around `axis` by the simplified section analysis. Parameters ---------- axis : Literal['x', 'y'] The local axis considered for the calculations. Returns ------- My : float Yield moment. phi_y : float Yield curvature. cy : float Neutral axis depth at yield. See Also -------- :func:`~simdesign.utils.rcsection.do_simplified_section_analysis` """ if axis == 'x': # Section height h = self.design.by # along y # Section width b = self.design.bx # along x # Number of internal web reinforcement (intermediate) nbl_v = np.array(self.design.nbly_int_q, dtype=int) # Number of internal reinforcement (on a single side) nbl_int = np.array(self.design.nblx_int_q, dtype=int) elif axis == 'y': # Section height h = self.design.bx # along x # Section width b = self.design.by # along y # Number of internal web reinforcement (intermediate) nbl_v = np.array(self.design.nblx_int_q, dtype=int) # Number of internal reinforcement (on a single side) nbl_int = np.array(self.design.nbly_int_q, dtype=int) # Number of corner reinforcement (on a single side) nbl_cor = 2 # Diameter of corner reinforcement dbl_cor = self.design.dbl_cor_q # Diameter of internal reinforcement dbl_int = self.design.dbl_int_q # Concrete compressive strength in base units fc = self.design.fc_q # Longitudinal steel yield strength in base units fsyl = self.design.fsyl_q # Concrete cover cover = self.design.cover_q # Horizontal (stirrup) reinforcement diameter dbh = self.design.dbh_q # Compressive axial load (+) Nu = max(-self.axial_force, 0) # Concrete modulus of elasticity Ec = self.Ecm_q # Steel modulus of elasticity Es = self.design.Es # Reinforcement layout (top to bottom) y_ct = cover + dbh + dbl_cor / 2 y_it = cover + dbh + dbl_int / 2 y_cb = h - (cover + dbh + dbl_cor / 2) y_ib = h - (cover + dbh + dbl_int / 2) dv = (y_cb - y_ct) / (nbl_v + 1) y_layers = [y_it, y_ct] + [y_ct + (i + 1) * dv for i in range(nbl_v) ] + [y_cb, y_ib] # Reinforcement area per layer (top to bottom) Ab_c = 0.25*np.pi*dbl_cor**2 Ab_i = 0.25*np.pi*dbl_int**2 As_c = nbl_cor * Ab_c As_i = nbl_int * Ab_i As_web = 2 * Ab_i # on both sides As_layers = [As_i, As_c] + [As_web for _ in range(nbl_v) ] + [As_c, As_i] # Cracking and yield moments _, _, _, My, phi_y, cy = rc.do_simplified_section_analysis( h=h, b=b, fc=fc, Ec=Ec, Pext=Nu, fyL=fsyl, Es=Es, As_layers=As_layers, y_layers=y_layers, ) return My, phi_y, cy def _set_cracked_properties(self) -> None: """Computes and stores the cracked-section (effective) flexural properties of the column about the local x and y axes. The effective second moment of area is always computed, regardless of ``cracked``, as ``Ieff = EIeff / Ec`` with ``EIeff = My / phi_y`` from the yield moment and curvature of the plastic hinges (see ``_get_my_phiy``). If ``cracked`` is a float, it is instead the ratio of effective to gross stiffness times the gross moment of inertia. The elastic sections use ``Ieff`` only if ``cracked`` is True or a float; if it is None or False, the gross section is used. The results are stored on the object for later use: - ``self._Ix_eff`` and ``self._cy`` (bending around local x), and - ``self._Iy_eff`` and ``self._cx`` (bending around local y), where ``_cy`` and ``_cx`` are the neutral axis depths at yield used by the shear hinges. See Also -------- :meth:`_get_my_phiy` : Yield point of the plastic hinges. References ---------- Panagiotakos, T. B., & Fardis, M. N. (2001). Deformations of reinforced concrete members at yielding and ultimate. Structural Journal, 98(2), 135-148. Priestley, M. J. N., Calvi, G. M., & Kowalsky, M. J. (2007). *Displacement-based seismic design of structures*. IUSS Press. """ for axis in ['x', 'y']: My, phiy, cy = self._get_my_phiy(axis) Ieff = (My / phiy) / self.Ecm_q if isinstance(self.cracked, float): # User-defined ratio of effective to gross flexural stiffness Ig = self.design.Ix if axis == 'x' else self.design.Iy Ieff = self.cracked * Ig # Save also the neutral axis depth for shear hinge calculations if axis == 'x': self._Ix_eff = Ieff self._cy = cy elif axis == 'y': self._Iy_eff = Ieff self._cx = cy