"""This module provides the beam class implementation for the ``DP03`` model in
the BNSM layer.
"""
# Imports from installed packages
import numpy as np
import openseespy.opensees as ops
from typing import List, Literal, Tuple
# Imports from bnsm base library
from ..baselib.beam import BeamBase
# Imports from utils library
from ....utils.units import MPa, mm
from ....utils.misc import round_list
from ....utils.rcsection import get_mander_confinement, build_beam_rebar_layout
[docs]
class Beam(BeamBase):
"""Beam implementation for the ``DP03`` model.
This class extends ``BeamBase`` by defining fiber sections at the plastic
hinge regions. The beam is modeled using a force-based beam-column element
with a plastic hinge integration scheme. Nonlinear behavior is distributed
over specified hinge lengths at the member ends, while the interior region
remains elastic.
The hinge sections are discretized into concrete and steel fibers, where
each fiber is associated with a uniaxial material model. The section
response is obtained by integrating stresses over the fibers, enabling
axial force-bending moment interaction to be captured. The interior region
is modeled as elastic using cracked section properties.
Attributes
----------
concrete_material : Literal['Concrete01', 'Concrete04'], optional
Uniaxial material type for concrete, by default 'Concrete04'.
interior_section : Literal['Elastic', 'Inelastic'], optional
Interior section used in the plastic hinge integration formulation,
by default 'Inelastic'.
See Also
--------
:class:`~BeamBase`
Base beam definition extended by this class.
"""
concrete_material: Literal['Concrete01', 'Concrete04'] = 'Concrete04'
interior_section: Literal['Elastic', 'Inelastic'] = 'Inelastic'
@property
def Ecm_q(self) -> float:
"""
Returns
-------
float
Elastic young's modulus of concrete (in base units).
References
----------
Priestley, M. J. N., Calvi, G. M., & Kowalsky, M. J. (2007).
*Displacement-based seismic design of structures*. IUSS Press.
"""
# Use quality adjusted elastic youngs modulus
fc_mpa = self.design.fc_q / MPa # convert to MPa
return (4700 * (fc_mpa**0.5)) * MPa # Mander et al. 1988
@property
def steel_mat_tag_(self) -> int:
"""Steel material tag."""
return int(str(self.design.line.tag) + '990')
@property
def steel_mat_tag(self) -> int:
"""MinMax material tag for the steel fibers. Used to simulate
bar buckling or rupture."""
return int(str(self.design.line.tag) + '992')
@property
def uconf_conc_mat_tag(self) -> int:
"""Material tag for the unconfined concrete fibers."""
return int(str(self.design.line.tag) + '993')
@property
def conf_conc_mat_tag_i(self) -> int:
"""Material tag for the confined concrete fibers at the *i-end*."""
return int(str(self.design.line.tag) + '994')
@property
def conf_conc_mat_tag_j(self) -> int:
"""Material tag for the confined concrete fibers at the *j-end*."""
return int(str(self.design.line.tag) + '995')
@property
def conf_conc_mat_tag_m(self) -> int:
"""Material tag for the confined concrete fibers at the *mid-section*.
"""
return int(str(self.design.line.tag) + '996')
@property
def inelastic_sec_m_tag(self) -> int:
"""Section tag for the inelastic middle (interior) section."""
return int(str(self.design.line.tag) + '993')
[docs]
def add_to_ops(self) -> None:
"""Adds beam components to the OpenSees domain
(i.e, elastic beam element and nodes).
"""
# Define geometric transformation
ops.geomTransf(*self._get_geo_transf_inputs())
# Create the section materials
ops.uniaxialMaterial(*self._get_steel_mat_inputs())
ops.uniaxialMaterial(*self._get_steel_minmax_mat_inputs())
ops.uniaxialMaterial(*self._get_unconfined_concrete_mat_inputs())
ops.uniaxialMaterial(*self._get_confined_concrete_mat_inputs('i'))
ops.uniaxialMaterial(*self._get_confined_concrete_mat_inputs('j'))
if self.interior_section == 'Inelastic': # use inelastic interior
ops.uniaxialMaterial(*self._get_confined_concrete_mat_inputs('m'))
# Create element sections
sectionI, fibersI, patchesI = self._get_inelastic_sec_inputs('i')
sectionJ, fibersJ, patchesJ = self._get_inelastic_sec_inputs('j')
# Section-I
ops.section(*sectionI)
for fiber in fibersI:
ops.fiber(*fiber)
for patch in patchesI:
ops.patch(*patch)
# Section-J
ops.section(*sectionJ)
for fiber in fibersJ:
ops.fiber(*fiber)
for patch in patchesJ:
ops.patch(*patch)
# Section-M / interior section
if self.interior_section == 'Inelastic': # use inelastic interior
sectionM, fibersM, patchesM = self._get_inelastic_sec_inputs('m')
ops.section(*sectionM)
for fiber in fibersM:
ops.fiber(*fiber)
for patch in patchesM:
ops.patch(*patch)
elif self.interior_section == 'Elastic': # use Elastic interior
ops.section(*self._get_elastic_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 beam components in OpenSees
domain (i.e, beam element and nodes).
Returns
-------
List[str]
List of Python commands for constructing the components of beam
object in OpenSees.
"""
# 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')
steel = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in self._get_steel_mat_inputs()
)
steel_minmax = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in self._get_steel_minmax_mat_inputs()
)
unconc = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in self._get_unconfined_concrete_mat_inputs()
)
conc_i = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in self._get_confined_concrete_mat_inputs('i')
)
conc_j = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in self._get_confined_concrete_mat_inputs('j')
)
content.append(f"ops.uniaxialMaterial({steel})")
content.append(f"ops.uniaxialMaterial({steel_minmax})")
content.append(f"ops.uniaxialMaterial({unconc})")
content.append(f"ops.uniaxialMaterial({conc_i})")
content.append(f"ops.uniaxialMaterial({conc_j})")
if self.interior_section == 'Inelastic': # use inelastic interior
conc_m = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in self._get_confined_concrete_mat_inputs('m')
)
content.append(f"ops.uniaxialMaterial({conc_m})")
# Create sections
content.append('# Create element sections')
sectionI, fibersI, patchesI = self._get_inelastic_sec_inputs('i')
sectionJ, fibersJ, patchesJ = self._get_inelastic_sec_inputs('j')
sectionI = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in sectionI
)
sectionJ = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in sectionJ
)
# Section-I
content.append(f"ops.section({sectionI})")
for fiber in fibersI:
fiber = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in fiber
)
content.append(f"ops.fiber({fiber})")
for patch in patchesI:
patch = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in patch
)
content.append(f"ops.patch({patch})")
# Section-J
content.append(f"ops.section({sectionJ})")
for fiber in fibersJ:
fiber = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in fiber
)
content.append(f"ops.fiber({fiber})")
for patch in patchesJ:
patch = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in patch
)
content.append(f"ops.patch({patch})")
# Section-M / interior section
if self.interior_section == 'Inelastic': # use inelastic interior
sectionM, fibersM, patchesM = self._get_inelastic_sec_inputs('m')
sectionM = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in sectionM
)
content.append(f"ops.section({sectionM})")
for fiber in fibersM:
fiber = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in fiber
)
content.append(f"ops.fiber({fiber})")
for patch in patchesM:
patch = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in patch
)
content.append(f"ops.patch({patch})")
elif self.interior_section == 'Elastic': # use elastic interior
elastic_sec_inputs = ', '.join(
repr(item) if isinstance(item, str) else str(item)
for item in self._get_elastic_sec_inputs()
)
content.append(f"ops.section({elastic_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 beam components in OpenSees
domain (i.e, beam element and nodes).
Returns
-------
List[str]
List of Tcl commands for constructing the components of beam
object in OpenSees.
"""
# 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')
steel = ' '.join(
f"{item}" for item in self._get_steel_mat_inputs()
)
steel_minmax = ' '.join(
f"{item}" for item in self._get_steel_minmax_mat_inputs()
)
unconc = ' '.join(
f"{item}" for item in self._get_unconfined_concrete_mat_inputs()
)
conc_i = ' '.join(
f"{item}" for item in self._get_confined_concrete_mat_inputs('i')
)
conc_j = ' '.join(
f"{item}" for item in self._get_confined_concrete_mat_inputs('j')
)
content.append(f"uniaxialMaterial {steel}")
content.append(f"uniaxialMaterial {steel_minmax}")
content.append(f"uniaxialMaterial {unconc}")
content.append(f"uniaxialMaterial {conc_i}")
content.append(f"uniaxialMaterial {conc_j}")
if self.interior_section == 'Inelastic': # use inelastic interior
conc_m = ' '.join(
f"{item}" for item in
self._get_confined_concrete_mat_inputs('m')
)
content.append(f"uniaxialMaterial {conc_m}")
# Create sections
content.append('# Create element sections')
sectionI, fibersI, patchesI = self._get_inelastic_sec_inputs('i')
sectionJ, fibersJ, patchesJ = self._get_inelastic_sec_inputs('j')
sectionI = ' '.join(f"{item}" for item in sectionI)
sectionJ = ' '.join(f"{item}" for item in sectionJ)
# Section-I
content.append(f"section {sectionI} " + "{")
for fiber in fibersI:
fiber = ' '.join(f"{item}" for item in fiber)
content.append(f" fiber {fiber}")
for patch in patchesI:
patch = ' '.join(f"{item}" for item in patch)
content.append(f" patch {patch}")
content.append("}")
# Section-J
content.append(f"section {sectionJ} " + "{")
for fiber in fibersJ:
fiber = ' '.join(f"{item}" for item in fiber)
content.append(f" fiber {fiber}")
for patch in patchesJ:
patch = ' '.join(f"{item}" for item in patch)
content.append(f" patch {patch}")
content.append("}")
# Section-M / interior section
if self.interior_section == 'Inelastic': # use inelastic interior
sectionM, fibersM, patchesM = self._get_inelastic_sec_inputs('m')
sectionM = ' '.join(f"{item}" for item in sectionM)
content.append(f"section {sectionM} " + "{")
for fiber in fibersM:
fiber = ' '.join(f"{item}" for item in fiber)
content.append(f" fiber {fiber}")
for patch in patchesM:
patch = ' '.join(f"{item}" for item in patch)
content.append(f" patch {patch}")
content.append("}")
elif self.interior_section == 'Elastic': # use elastic interior
elastic_sec_inputs = ' '.join(
f"{item}" for item in self._get_elastic_sec_inputs()
)
content.append(f"section {elastic_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
def _get_inelastic_sec_inputs(self, section: Literal['i', 'm', 'j']
) -> Tuple[list, list, list]:
"""Builds fiber-section inputs for an inelastic beam end section.
Constructs an OpenSees ``Fiber`` section for the specified beam section
(i-end, mid-section, j-end), including:
- the fiber section header (with torsional stiffness ``-GJ``),
- discrete steel fibers for each longitudinal bar, and
- rectangular concrete patches for confined core and unconfined cover.
The rebar coordinates are obtained from ``build_beam_rebar_layout`` and
then shifted so the section local origin is at the section centroid,
consistent with OpenSees fiber section conventions (yLoc, zLoc).
Parameters
----------
section : Literal['i', 'm', 'j']
Which section to use.
Returns
-------
Tuple[list, list, list]:
A tuple ``(section, steel_fibers, concrete_patches)`` where:
**section**: list[str | float | int]
Inputs for ``ops.section`` defining a fiber section, e.g.::
['Fiber', sec_tag, '-GJ', GJ]
**steel_fibers**: list[list[str | float | int]]
Each entry defines a steel fiber for ``ops.fiber`` as::
[yLoc, zLoc, area, steel_mat_tag]
**concrete_patches**: list[list[str | float | int]]
Patch definitions for ``ops.patch`` using ``'rect'``
geometry. Includes one confined core patch and four
unconfined cover patches (bottom/right/top/left).
Notes
-----
- The discretization counts are currently hard-coded.
"""
# Material and section tags
if section == 'i':
idx = 0
sec_tag = self.inelastic_sec_i_tag
conf_con = self.conf_conc_mat_tag_i
elif section == 'm':
idx = 1
sec_tag = self.inelastic_sec_m_tag
conf_con = self.conf_conc_mat_tag_m
elif section == 'j':
idx = 2
sec_tag = self.inelastic_sec_j_tag
conf_con = self.conf_conc_mat_tag_j
uconf_con = self.uconf_conc_mat_tag
steel = self.steel_mat_tag
# Section geometry [mm]
lx = self.design.b
ly = self.design.h
cv = self.design.cover_q
# Shear reinforcement [mm]
dbh = self.design.dbh_q[idx]
# Longitudinal reinforcement [mm]
nbl_b1 = int(self.design.nbl_b1_q[idx])
nbl_b2 = int(self.design.nbl_b2_q[idx])
dbl_b1 = self.design.dbl_b1_q[idx]
dbl_b2 = self.design.dbl_b2_q[idx]
nbl_t1 = int(self.design.nbl_t1_q[idx])
nbl_t2 = int(self.design.nbl_t2_q[idx])
dbl_t1 = self.design.dbl_t1_q[idx]
dbl_t2 = self.design.dbl_t2_q[idx]
# Set the reinforcement layout
layout = build_beam_rebar_layout(
dbl_b1, dbl_b2, dbl_t1, dbl_t2, nbl_b1, nbl_b2, nbl_t1, nbl_t2,
dbh, lx, ly, cv
)
# Steel fibers
steel_fibers = []
for x, y, d in zip(layout['rein_x'], layout['rein_y'],
layout['rein_d']):
area = 0.25 * np.pi * d**2
yLoc = y - ly/2 # Origin is the section center
zLoc = x - lx/2 # Origin is the section center
inputs = [float(yLoc), float(zLoc), float(area), steel]
steel_fibers.append(inputs)
# Concrete patches
cover_y = float(ly/2) # The distance from y-axis to the cover edge
cover_z = float(lx/2) # The distance from z-axis to the cover edge
core_y = float(ly/2 - cv) # The distance from y-axis to the core edge
core_z = float(lx/2 - cv) # The distance from z-axis to the core edge
nf_core_y = 12 # num. of fibers for concrete in y-direction - core
nf_core_z = 4 # num. of fibers for concrete in z-direction - core
nf_cover_y = 12 # num.r of fibers for concrete in y-direction - cover
nf_cover_z = 4 # num. of fibers for concrete in z-direction - cover
# Uncofined concrete
patch_bottom = ['rect', uconf_con, 1, nf_cover_z,
-cover_y, -cover_z, -core_y, cover_z]
patch_right = ['rect', uconf_con, nf_cover_y, 1,
-core_y, core_z, core_y, cover_z]
patch_top = ['rect', uconf_con, 1, nf_cover_z,
core_y, -cover_z, cover_y, cover_z]
patch_left = ['rect', uconf_con, nf_cover_y, 1,
-core_y, -cover_z, core_y, -core_z]
# Confined concrete
core_patch = ['rect', conf_con, nf_core_y, nf_core_z,
-core_y, -core_z, core_y, core_z]
concrete_patches = [
core_patch, patch_bottom, patch_right, patch_top, patch_left
]
# Fiber section
GJ = self.Gcm_q * self.design.J
section = ['Fiber', sec_tag, '-GJ', GJ]
section = round_list(section)
concrete_patches = round_list(concrete_patches)
steel_fibers = round_list(steel_fibers)
return section, steel_fibers, concrete_patches
def _get_int_inputs(self) -> List[str | float]:
"""Retrieves beam integration inputs.
Returns
-------
List[str | int | float]
List of beam integration inputs.
"""
int_inputs = super()._get_int_inputs()
if self.interior_section == 'Inelastic':
int_inputs[-1] = self.inelastic_sec_m_tag
return int_inputs
def _get_confined_concrete_mat_inputs(self, section: Literal['i', 'm', 'j']
) -> List[str | float | int]:
"""Builds OpenSees confined concrete material inputs for an end
section.
Parameters
----------
section : Literal['i', 'm', 'j']
Which section to use.
Returns
-------
List[str | float | int]
Arguments for ``ops.uniaxialMaterial`` defining a confined concrete
material ('Concrete01' or 'Concrete04') in OpenSees.
See Also
--------
``get_mander_confinement``: Computes confined peak stress and strains
using the Mander model.
``build_beam_rebar_layout``: Sets rebar coordinates and diameters
for the beam section.
Notes
-----
- Concrete confinement parameters are computed using the Mander model.
- For Concrete01, residual (crushing) stress is taken as
``fpcu = 0.2 * fpc``.
"""
# Index for the specified end section
if section == 'i':
idx = 0
tag = self.conf_conc_mat_tag_i
if section == 'm':
idx = 1
tag = self.conf_conc_mat_tag_m
elif section == 'j':
idx = 2
tag = self.conf_conc_mat_tag_j
# Materials [MPa]
fc = self.design.fc_q / MPa
fsyh = self.design.fsyh_q / MPa
# Section geometry [mm]
lx = self.design.b / mm
ly = self.design.h / mm
cv = self.design.cover_q / mm
# Shear reinforcement [mm]
dbh = self.design.dbh_q[idx] / mm
sbh = self.design.sbh_q[idx] / mm
legs_x = int(self.design.nbh_b_q[idx])
legs_y = int(self.design.nbh_h_q[idx])
# Longitudinal reinforcement [mm]
nbl_b1 = int(self.design.nbl_b1_q[idx])
nbl_b2 = int(self.design.nbl_b2_q[idx])
dbl_b1 = self.design.dbl_b1_q[idx] / mm
dbl_b2 = self.design.dbl_b2_q[idx] / mm
nbl_t1 = int(self.design.nbl_t1_q[idx])
nbl_t2 = int(self.design.nbl_t2_q[idx])
dbl_t1 = self.design.dbl_t1_q[idx] / mm
dbl_t2 = self.design.dbl_t2_q[idx] / mm
layout = build_beam_rebar_layout(
dbl_b1, dbl_b2, dbl_t1, dbl_t2, nbl_b1, nbl_b2, nbl_t1, nbl_t2,
dbh, lx, ly, cv
)
params = get_mander_confinement(
fc=fc, Lx=lx, Ly=ly, cover=cv, db_v=dbh, s=sbh,
legs_x=legs_x, legs_y=legs_y, fy_v=fsyh,
rein_x=layout['rein_x'], rein_y=layout['rein_y'],
rein_d=layout['rein_d']
) # strength parameters are in MPa
# Set concrete01 material parameters
if self.concrete_material == 'Concrete01':
fpc = -params['fcc'] * MPa # concrete compressive strength
epsc0 = -params['eps_cc'] # concrete strain at comp. strength
fpcu = 0.2*fpc # concrete crushing strength
epsU = -params['eps_cu'] # concrete strain at crushing strength
mat_inputs = [fpc, epsc0, fpcu, epsU]
elif self.concrete_material == 'Concrete04':
fpc = -params['fcc'] * MPa # concrete compressive strength
epsc = -params['eps_cc'] # concrete strain at compressive strength
epscu = -params['eps_cu'] # concrete strain at crushing strength
Ec = self.Ecm_q # elastic modulus
mat_inputs = [fpc, epsc, epscu, Ec]
# Rounding
mat_tags = [self.concrete_material, tag]
mat_inputs = round_list(mat_tags + mat_inputs)
return mat_inputs
def _get_unconfined_concrete_mat_inputs(self) -> List[str | float | int]:
"""Builds cover (unconfined) concrete material inputs for OpenSees.
Returns
-------
List[str | float | int]
Arguments for ``ops.uniaxialMaterial`` defining an unconfined
concrete material ('Concrete01' or 'Concrete04') in OpenSees.
Notes
-----
- Peak strain is taken as ``epsc0 = -0.002``.
- Crushing strain is taken as ``epsU = -0.006``.
- For Concrete01, residual (crushing) stress is taken as
``fpcu = 0.2 * fpc``.
"""
# Set concrete01 material parameters
if self.concrete_material == 'Concrete01':
fpc = -self.design.fc_q # concrete compressive strength
epsc0 = -0.002 # concrete strain at compressive strength
fpcu = 0.2*fpc # concrete crushing strength
epsU = -0.006 # concrete strain at crushing strength
mat_inputs = [fpc, epsc0, fpcu, epsU]
elif self.concrete_material == 'Concrete04':
fpc = -self.design.fc_q # concrete compressive strength
epsc = -0.002 # concrete strain at compressive strength
epscu = -0.006 # concrete strain at crushing strength
Ec = self.Ecm_q # elastic modulus
mat_inputs = [fpc, epsc, epscu, Ec]
# Rounding
mat_tags = [self.concrete_material, self.uconf_conc_mat_tag]
mat_inputs = round_list(mat_tags + mat_inputs)
return mat_inputs
def _get_steel_mat_inputs(self) -> List[str | float | int]:
"""Builds OpenSees reinforcing steel material inputs.
Returns
-------
List[str | float | int]
Arguments for ``ops.uniaxialMaterial`` defining a ``Steel01``
material in OpenSees
Notes
-----
- The strain-hardening ratio is assumed to be 0.005.
"""
b = 0.005 # strain-hardening ratio - assumed
Fy = self.design.fsyl_q # yield strength
E0 = self.design.Es # initial elastic tangent
mat_inputs = ['Steel01', self.steel_mat_tag_, Fy, E0, b]
mat_inputs = round_list(mat_inputs)
return mat_inputs
def _get_steel_minmax_mat_inputs(self) -> List[str | float | int]:
"""Builds OpenSees MinMax wrapper material inputs for reinforcing
steel.
Wraps the underlying steel material to cap strains in
tension/compression, enabling a simple representation of bar rupture or
buckling limits.
Returns
-------
List[str | float | int]
Arguments for ``ops.uniaxialMaterial`` defining a ``MinMax``
material in OpenSees.
Notes
-----
- The same absolute strain limit is used in tension and compression.
- Here ``epsU = 0.08`` is used per Priestley et al. 2007.
"""
epsU = 0.08 # bar buckling or bar rupture
mat_inputs = ['MinMax', self.steel_mat_tag, self.steel_mat_tag_,
'-min', -epsU, '-max', epsU]
mat_inputs = round_list(mat_inputs)
return mat_inputs