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

"""This module provides base classes for representing beam-column joints
within the BNSM layer.

The module includes:

- ``JointBase``: abstract interface for joint objects, including access to
  design properties and common functionality.
- ``StairsJointBase``: mid-storey (stairs) joint implementation that creates
  a central joint node used to connect offset beam and column elements.
- ``FloorJointBase``: floor-level joint implementation that can optionally
  model joint flexibility (rigid, elastic, or inelastic).
"""
# Imports from installed packages
from abc import ABC, abstractmethod
import numpy as np
import openseespy.opensees as ops
from typing import Dict, List, Literal

# Imports from bdim base library
from ...bdim.baselib.joint import JointBase as JointDesign

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

# Imports from utils library
from ....utils.misc import PRECISION, round_list


[docs] class JointBase(ABC): """Abstract base class for beam-column joint implementations in the BNSM layer. Attributes ---------- design : JointDesign Instance of joint design information model. """ design: JointDesign @property def bx(self) -> float: """Joint width along global x-axis. Returns ------- float Joint width along global x-axis (based on columns' section widths). """ bx = 0.0 if self.design.top_column: bx = max(bx, self.design.top_column.bx) if self.design.bottom_column: bx = max(bx, self.design.bottom_column.bx) return bx @property def by(self) -> float: """Joint width along global y-axis. Returns ------- float Joint width along global y-axis (based on columns' section widths). """ by = 0.0 if self.design.top_column: by = max(by, self.design.top_column.by) if self.design.bottom_column: by = max(by, self.design.bottom_column.by) return by @property def h(self) -> float: """Joint height based on all beam section heights. Returns ------- float Joint height based on all beam section heights. Notes ----- The largest of the beam heights is selected. """ h = 0.0 if self.design.left_beam: h = max(h, self.design.left_beam.h) if self.design.right_beam: h = max(h, self.design.right_beam.h) if self.design.rear_beam: h = max(h, self.design.rear_beam.h) if self.design.front_beam: h = max(h, self.design.front_beam.h) return h @property def fcm(self) -> float: """Mean concrete compressive strength. Returns ------- float Mean value of concrete compressive strength (in base units). """ return self.design.fcm
[docs] @abstractmethod def add_to_ops(self) -> None: """Adds joint model objects to the OpenSees domain. """ pass
[docs] @abstractmethod def to_py(self) -> List[str]: """Gets the Python commands to define joint model objects in the OpenSees domain. Returns ------- List[str] List of Python commands for constructing the components of joint in OpenSees. """ pass
[docs] @abstractmethod def to_tcl(self) -> List[str]: """Gets the Tcl commands to define joint model objects in the OpenSees domain. Returns ------- List[str] List of Tcl commands for constructing the components of joint in OpenSees. """ pass
[docs] class StairsJointBase(JointBase): """Base class implementation used in the BNSM layer to represent beam-column joints at a mid-storey level (stairs joint). It provides methods to define a stairs joint in the OpenSees domain and to export equivalent Python and Tcl commands. The mid-storey joint modelling approach follows macro-model formulations, where joint behaviour is represented through rotational springs based on O'Reilly 2016. References ---------- O'Reilly, G. J. (2016). Performance-based seismic assessment and retrofit of existing RC frame buildings in Italy (Doctoral dissertation, IUSS Pavia). Attributes ---------- center_node : Node Central joint node for connecting nodes at joint offset distances. See Also -------- :class:`~JointBase` Base class joint definition extended by this class. """ center_node: Node def __init__(self, design: JointDesign, mass: float) -> None: """Initialize StairsJointBase object. Parameters ---------- design : JointDesign Reference design information of joint. mass : float Total mass assigned to joint. """ # Save reference design information of joint self.design = design # Set reference node properties ref_point = self.design.elastic_node ref_tag = ref_point.tag ref_coords = ref_point.coordinates # Initialize center node masses = [mass, mass, mass, 0.0, 0.0, 0.0] self.center_node = Node(ref_tag, ref_coords, masses)
[docs] def add_to_ops(self) -> None: """Adds stairs joint model objects to the OpenSees domain (i.e., nodes). """ # Central joint node self.center_node.add_to_ops()
[docs] def to_py(self) -> List[str]: """Gets the Python commands to define stairs joint model objects in the OpenSees domain (i.e., joint node). Returns ------- List[str] List of Python commands for constructing the components of stairs joint in OpenSees. """ grids = ', '.join([f"{i}" for i in self.design.elastic_node.grid_ids]) content = [f'# Joint grid ids (x, y, z): ({grids})'] content.append(self.center_node.to_py()) return content
[docs] def to_tcl(self) -> List[str]: """Gets the Tcl commands to define stairs joint model objects in the OpenSees domain (i.e., joint node). Returns ------- List[str] List of Tcl commands for constructing the components of stairs joint in OpenSees. """ grids = ', '.join([f"{i}" for i in self.design.elastic_node.grid_ids]) content = [f'# Joint grid ids (x, y, z): ({grids})'] content.append(self.center_node.to_tcl()) return content
[docs] class FloorJointBase(StairsJointBase): """Base class implementation used in the BNSM layer to represent beam-column joints at a floor level. It provides methods to define an floor-level joint in the OpenSees domain and to export equivalent Python and Tcl commands. Attributes ---------- floor_node : Node Floor node which is constrained by the floor diaphragm. flexibility_model : Literal['inelastic', 'elastic', 'rigid'] Joint flexibility model. axial_force : float Axial force acting on joint. See Also -------- :class:`~StairsJointBase` Mid-storey (stairs) joint definition that this class extends. """ floor_node: Node flexibility_model: Literal["inelastic", "elastic", "rigid"] axial_force: float @property def mz_tag(self) -> int: """Material tag for the joint hinge about global-z (Mz). Returns ------- int Material tag for the joint hinge about global-z (Mz). """ return int(300000 + self.center_node.tag) @property def my_tag(self) -> int: """Material tag for the joint hinge about global-y (My). Returns ------- int Material tag for the joint hinge about global-y (My). """ return int(400000 + self.center_node.tag) @property def sec_tag(self) -> int: """Tag of aggregated beam-column joint element section. Returns ------- int Tag of aggregated beam-column joint element section. """ return int(self.floor_node.tag) @property def ele_tag(self) -> int: """Tag of beam-column joint element. Returns ------- int Tag of beam-column joint element. """ return int(self.floor_node.tag) @property def fcm(self) -> float: """Mean concrete compressive strength. Returns ------- float Mean value of concrete compressive strength (in base units). """ return self.design.fcm @property def hstorey(self) -> float: """Internal storey height. Returns ------- float Internal storey height. Notes ----- The equations were derived using constant storey height. If possible the average one is used. """ if self.design.top_column is None: return self.design.bottom_column.H elif self.design.bottom_column is None: return self.design.top_column.H else: return self.design.top_column.H def __init__(self, design: JointDesign, mass: float, model: Literal["inelastic", "elastic", "rigid"], load_factors: Dict[Literal['G', 'Q'], float]) -> None: """Initialize FloorJointBase object. Parameters ---------- design : JointDesign Reference design information of joint. mass : float Total mass assigned to joint. model : Literal["inelastic", "elastic", "rigid"] Joint flexibility model. load_factors : Dict[Literal['G', 'Q'], float] Load factors used to compute axial load on joint. """ # Save joint flexibility model option self.flexibility_model = model # Initialize the nodes in stairs joint super().__init__(design, mass) # Set the joint node constrained by the floor diaphragm if model == 'rigid': # There is no need to create additional joint node self.floor_node = self.center_node else: # Initialize the new node to account for joint flexibility self.floor_node = Node( self.design.elastic_node.tag + 10000, self.design.elastic_node.coordinates ) # Axial force on the joint if self.design.bottom_column: self.axial_force = ( load_factors['G'] * self.design.bottom_column.hinge_Ng + load_factors['Q'] * self.design.bottom_column.hinge_Nq ) # forces = ( # load_factors['G'] * # self.design.bottom_column.forces['G/seismic'] + # load_factors['Q'] * # self.design.bottom_column.forces['Q/seismic'] # ) # self.axial_load = -forces.N9 else: raise ValueError( "Bottom column is missing, joint model won't work here.")
[docs] def add_to_ops(self) -> None: """Adds floor joint model objects to the OpenSees domain (i.e., joint nodes and flexibility element). """ # Add center joint node connected to beam-column elements super().add_to_ops() # Add joint flexibility element for no-rigid joint cases if self.flexibility_model != 'rigid': # Add floor joint node constrained by the floor diaphragm self.floor_node.add_to_ops() # Materials defining flexible rotation behaviour ops.uniaxialMaterial(*self._get_mat_inputs('X')) ops.uniaxialMaterial(*self._get_mat_inputs('Y')) # Create the new section with flexible rotation behaviour ops.section(*self._get_agg_sec_inputs()) # Create the joint flexibility element ops.element(*self._get_ele_inputs())
[docs] def to_py(self) -> List[str]: """Gets the Python commands to define floor joint model objects in the OpenSees domain (i.e., joint nodes and flexibility element). Returns ------- List[str] List of Python commands for constructing the components of floor joint in OpenSees. """ content = super().to_py() content.append(f"# Joint flexibility model: {self.flexibility_model}") # Add joint flexibility element for no-rigid joint cases if self.flexibility_model != 'rigid': # Add floor joint node constrained by the floor diaphragm note = ' # Constrained floor node' content.append(self.floor_node.to_py() + note) # Materials defining flexible rotation behaviour mz_mat_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_mat_inputs('X') ) my_mat_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_mat_inputs('Y') ) content.append(f"ops.uniaxialMaterial({mz_mat_inputs})") content.append(f"ops.uniaxialMaterial({my_mat_inputs})") # Create the new section with flexible rotation behaviour sec_inputs = ', '.join( repr(item) if isinstance(item, str) else str(item) for item in self._get_agg_sec_inputs() ) content.append(f"ops.section({sec_inputs})") # Create the joint flexibility 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 define floor joint model objects in the OpenSees domain (i.e., joint nodes and flexibility element). Returns ------- List[str] List of Tcl commands for constructing the components of floor joint in OpenSees. """ content = super().to_tcl() content.append(f"# Joint flexibility model: {self.flexibility_model}") # Add joint flexibility element for no-rigid joint cases if self.flexibility_model != 'rigid': # Add floor joint node constrained by the floor diaphragm note = ' # Constrained floor node' content.append(self.floor_node.to_tcl() + note) # Materials defining flexible rotation behaviour mz_mat_inputs = ' '.join( f'{item}' for item in self._get_mat_inputs('X') ) my_mat_inputs = ' '.join( f'{item}' for item in self._get_mat_inputs('Y') ) content.append(f"uniaxialMaterial {mz_mat_inputs}") content.append(f"uniaxialMaterial {my_mat_inputs}") # Create the new section with flexible rotation behaviour sec_inputs = ' '.join( f'{item}' for item in self._get_agg_sec_inputs() ) content.append(f"section {sec_inputs}") # Create the joint flexibility element ele_inputs = ' '.join( f'{item}' for item in self._get_ele_inputs() ) content.append(f"element {ele_inputs} ") return content
def _get_mat_inputs(self, axis: Literal['X', 'Y'] ) -> List[str | float | int]: """Gets the inputs for joint material around the specified global axis. Parameters ---------- axis : Literal['X', 'Y'] The axis about which the rotational behaviour is represented. Returns ------- mat_inputs : List[str | float | int] List of inputs for the material representing rotational behaviour around the specified global axis. """ # Materials defining flexible rotation behaviour if self.flexibility_model == 'elastic': # Elastic rotation return self._get_elastic_joint_params(axis) elif self.flexibility_model == 'inelastic': # Inelastic rotation return self._get_inelastic_joint_params(axis) def _get_elastic_joint_params(self, axis: Literal['X', 'Y'] ) -> List[str | float | int]: """Gets the parameters for elastic joint materials. Parameters ---------- axis : Literal['X', 'Y'] The axis about which the rotational behaviour is represented. Returns ------- mat_inputs : List[str | float | int] List of inputs for the material representing elastic rotational behaviour around the specified global axis. Notes ----- Even though original implementation by Gerard O'Reilly used the expression for interior joints, I believe this was done for the sake of simplicity. Herein, we use the corresponding equations. """ # Get the hysteretic material inputs params = self._get_inelastic_joint_params(axis) # Compute elastic stiffness values k_el = params[2] / params[3] # Material inputs mat_inputs = ['Elastic', params[1], round(float(k_el), PRECISION)] return mat_inputs def _get_inelastic_joint_params(self, axis: Literal['X', 'Y'] = 'X' ) -> List[str | float | int]: """Gets the material properties for inelastic joint behaviour, i.e., hysteretic material model parameters. Parameters ---------- axis : Literal['X', 'Y'] The axis about which the rotational behaviour is represented. Returns ------- mat_inputs : List[str | float | int] List of inputs for the material representing inelastic rotational behaviour around the specified global axis. Notes ----- The constants are slightly different than those in the references. The new calibrated values were directly provided by Gerard O'Reilly. References ---------- O'Reilly, G. J. (2016). Performance-based seismic assessment and retrofit of existing RC frame buildings in Italy (Doctoral dissertation, IUSS Pavia). O'Reilly, G. J., & Sullivan, T. J. (2019). Modeling techniques for the seismic assessment of the existing Italian RC frame structures. Journal of Earthquake Engineering, 23(8), 1262-1296. GitHub - gerardjoreilly/Numerical-Modelling-of-GLD-RC-Frames: Set of OpenSees procedures used to model RC frames designed prior to the 1970s in Italy, as outlined and calibrated in O'Reilly & Sullivan [2019]. https://github.com/gerardjoreilly/Numerical-Modelling-of-GLD-RC-Frames. Accessed 21 Oct 2024. TODO ---- Due to the lack of data, equation described for roof was not validated. However, we rarely expect nonlinearity for joints at the last floor. So, this has the least significance. """ # Flexural material around global X direction, beams are in global Y if axis == 'X': # Beam width bb = 0.0 if self.design.rear_beam: bb = max(bb, self.design.rear_beam.b) if self.design.front_beam: bb = max(bb, self.design.front_beam.b) # Beam height hb = 0.0 if self.design.rear_beam: hb = max(hb, self.design.rear_beam.h) if self.design.front_beam: hb = max(hb, self.design.front_beam.h) # Column width bc = self.bx # Column height hc = self.by # Joint type (location) if None in [self.design.front_beam, self.design.rear_beam]: jnt_type = 'exterior' else: jnt_type = 'interior' # Material tag mat_tag = self.mz_tag # Flexural material around global Y direction, beams are in global X if axis == 'Y': # Beam width bb = 0.0 if self.design.left_beam: bb = max(bb, self.design.left_beam.b) if self.design.right_beam: bb = max(bb, self.design.right_beam.b) # Beam height hb = 0.0 if self.design.left_beam: hb = max(hb, self.design.left_beam.h) if self.design.right_beam: hb = max(hb, self.design.right_beam.h) # Column width bc = self.by # Column height hc = self.bx # Joint type (location) if None in [self.design.left_beam, self.design.right_beam]: jnt_type = 'exterior' else: jnt_type = 'interior' # Material tag mat_tag = self.my_tag # Joint type (location) - Roof case if self.design.top_column is None: jnt_type = 'roof' # Lever arm, i.e., distance between comp. and tens. forces jd = 0.9 * (0.9 * hb) # Joint width definition Equation 2.48 (O'Reilly, 2016) # NZS 3101-1 (2006) 15.3.4 Equations A2(a) and A2(b) if bc >= bb: bj = min(bc, bb + 0.5 * hc) else: bj = min(bb, bc + 0.5 * hc) # Get Hysteretic material parameters if jnt_type == 'roof': # Shear strength coefficients for each LS: cracking, peak, ultimate kappa = 2 * [0.132, 0.132, 0.053] # Principle tensile stress values Equation 2.34 (O'Reilly, 2016) pt = np.array(kappa) * (self.fcm**0.5) # Stress values for material, comes from the model on GitHub. mj = ( 2 * (pt * bj * hc) * jd * ( (hb / (2 * hc)) + ( (hb / (2 * hc)) ** 2 + 1 + (self.axial_force / (pt * bj * hc)) ) ** 0.5 ) ) # Shear deformations for each limit state: cracking, peak, ultimate gamma = 2 * [0.0002, 0.0132, 0.0270] # strain values # Hysteretic parameters (pinchx, pinchy, damage1, damage2, beta) hyst_params = [0.6, 0.2, 0.0, 0.0, 0.3] elif jnt_type == 'exterior': # Shear strength coefficients for each LS: cracking, peak, ultimate kappa = 2 * [0.132, 0.132, 0.053] # Principle tensile stress values Equation 2.34 (O'Reilly, 2016) pt = np.array(kappa) * (self.fcm**0.5) # Stress values for material, Equation 2.33 (O'Reilly, 2016) mj = ( (pt * bj * hc) * (self.hstorey * jd) / (self.hstorey - jd) * ( (hb / (2 * hc)) + ( (hb / (2 * hc)) ** 2 + 1 + (self.axial_force / (pt * bj * hc)) ) ** 0.5 ) ) # Shear deformations for each limit state: cracking, peak, ultimate gamma = 2 * [0.0002, 0.0132, 0.0270] # strain values # Hysteretic parameters (pinchx, pinchy, damage1, damage2, beta) hyst_params = [0.6, 0.2, 0.0, 0.0, 0.3] else: # Shear strength coefficients for each LS: cracking, peak, ultimate kappa = 2 * [0.29, 0.42, 0.42] # Principle tensile stress values Equation 2.34 (O'Reilly, 2016) pt = np.array(kappa) * (self.fcm**0.5) # Stress values for material, Equation 2.55 (O'Reilly, 2016) mj = ( (pt * bj * hc) * (self.hstorey * jd) / (self.hstorey - jd) * (1 + (self.axial_force / (pt * bj * hc))) ** 0.5 ) # Shear deformations for each limit state: cracking, peak, ultimate gamma = 2 * [0.0002, 0.0090, 0.0200] # strain values # Hysteretic parameters (pinchx, pinchy, damage1, damage2, beta) hyst_params = [0.6, 0.2, 0.0, 0.01, 0.3] # Inputs for material representing rotational behaviour mat_params = [] for i in range(6): if i < 3: mat_params.extend([mj[i], gamma[i]]) else: mat_params.extend([-mj[i], -gamma[i]]) # Add hysteretic parameters mat_params.extend(hyst_params) # Rounding to precision mat_inputs = round_list(['Hysteretic', mat_tag] + mat_params) return mat_inputs def _get_agg_sec_inputs(self) -> List[str | int]: """Retrieves aggregation section inputs. Returns ------- sec_inputs : List[str | int] List of aggregation section inputs. """ sec_inputs = [ 'Aggregator', self.sec_tag, RIGID_MAT, 'P', RIGID_MAT, 'Vy', RIGID_MAT, 'Vz', self.my_tag, 'My', self.mz_tag, 'Mz', RIGID_MAT, 'T' ] # Rounding sec_inputs = round_list(sec_inputs) return sec_inputs def _get_ele_inputs(self) -> List[str | int | float]: """Retrieves joint element inputs. Returns ------- ele_inputs : List[str | int | float] List of joint element inputs. """ # Vector components in global coordinates defining local x-axis x1, x2, x3 = 0, 0, 1 # Vector components in global coordinates defining vector yp which # lies in the local x-y plane for the element yp1, yp2, yp3 = 0, 1, 0 # Flag for rayleigh damping rFlag = 0 ele_inputs = [ 'zeroLengthSection', self.ele_tag, self.center_node.tag, self.floor_node.tag, self.sec_tag, '-orient', x1, x2, x3, yp1, yp2, yp3, '-doRayleigh', rFlag ] return ele_inputs