Source code for concreteproperties.stress_strain_profile

from __future__ import annotations

import warnings
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional, Union

import matplotlib.pyplot as plt
import numpy as np
from rich.console import Console
from rich.table import Table
from scipy.interpolate import interp1d
from scipy.optimize import brentq

from concreteproperties.post import plotting_context

if TYPE_CHECKING:
    import matplotlib


[docs]@dataclass class StressStrainProfile: """Abstract base class for a material stress-strain profile. Implements a piecewise linear stress-strain profile. Positive stresses & strains are compression. :param strains: List of strains (must be increasing or equal) :param stresses: List of stresses """ strains: List[float] stresses: List[float] def __post_init__( self, ): # validate input - same length lists if len(self.strains) != len(self.stresses): raise ValueError("Length of strains must equal length of stresses") # validate input - length > 1 if len(self.strains) < 2: raise ValueError("Length of strains and stresses must be greater than 1") # validate input - increasing values prev_strain = self.strains[0] for idx in range(len(self.strains)): if idx != 0: if self.strains[idx] < prev_strain: msg = "strains must contain increasing values." raise ValueError(msg) prev_strain = self.strains[idx]
[docs] def get_stress( self, strain: float, ) -> float: """Returns a stress given a strain. :param strain: Strain at which to return a stress. :return: Stress """ # create interpolation function stress_function = interp1d( x=self.strains, y=self.stresses, kind="linear", fill_value="extrapolate", # type: ignore ) return stress_function(strain)
[docs] def get_elastic_modulus( self, ) -> float: """Returns the elastic modulus of the stress-strain profile. :return: Elastic modulus """ small_strain = 1e-6 # get stress at zero strain stress_0 = self.get_stress(strain=0) # get stress at small positive strain & compute elastic modulus stress_positive = self.get_stress(strain=small_strain) em_positive = stress_positive / small_strain # get stress at small negative strain & compute elastic modulus stress_negative = self.get_stress(strain=-small_strain) em_negative = stress_negative / -small_strain # check elastic moduli are equal, if not print warning if not np.isclose(em_positive, em_negative): warnings.warn( "Initial compressive and tensile elastic moduli are not equal" ) if np.isclose(em_positive, 0): raise ValueError("Elastic modulus is zero.") return em_positive
[docs] def get_compressive_strength( self, ) -> float: """Returns the most positive stress. :return: Compressive strength """ return max(self.stresses)
[docs] def get_tensile_strength( self, ) -> float: """Returns the most negative stress. :return: Tensile strength """ return min(self.stresses)
[docs] def get_yield_strength( self, ) -> float: """Returns the yield strength of the stress-strain profile. :return: Yield strength """ raise NotImplementedError
[docs] def get_ultimate_compressive_strain( self, ) -> float: """Returns the largest compressive strain. :return: Ultimate strain """ return max(self.strains)
[docs] def get_ultimate_tensile_strain( self, ) -> float: """Returns the largest tensile strain. :return: Ultimate strain """ return min(self.strains)
[docs] def get_unique_strains( self, ) -> List[float]: """Returns an ordered list of unique strains. :return: Ordered list of unique strains """ unique_strains = list(set(self.strains)) unique_strains.sort() return unique_strains
[docs] def print_properties( self, fmt: str = "8.6e", ): """Prints the stress-strain profile properties to the terminal. :param fmt: Number format """ table = Table(title=f"Stress-Strain Profile - {type(self).__name__}") table.add_column("Property", justify="left", style="cyan", no_wrap=True) table.add_column("Value", justify="right", style="green") table.add_row( "Elastic Modulus", "{:>{fmt}}".format(self.get_elastic_modulus(), fmt=fmt) ) table.add_row( "Compressive Strength", "{:>{fmt}}".format(self.get_compressive_strength(), fmt=fmt), ) table.add_row( "Tensile Strength", "{:>{fmt}}".format(-self.get_tensile_strength(), fmt=fmt), ) table.add_row( "Ultimate Compressive Strain", "{:>{fmt}}".format(self.get_ultimate_compressive_strain(), fmt=fmt), ) table.add_row( "Ultimate Tensile Strain", "{:>{fmt}}".format(self.get_ultimate_tensile_strain(), fmt=fmt), ) console = Console() console.print(table)
[docs] def plot_stress_strain( self, title: str = "Stress-Strain Profile", fmt: str = "o-", **kwargs, ) -> matplotlib.axes.Axes: # type: ignore """Plots the stress-strain profile. :param title: Plot title :param fmt: Plot format string :param kwargs: Passed to :func:`~concreteproperties.post.plotting_context` :return: Matplotlib axes object """ # create plot and setup the plot with plotting_context(title=title, **kwargs) as ( fig, ax, ): ax.plot(self.strains, self.stresses, fmt) # type: ignore plt.xlabel("Strain") plt.ylabel("Stress") plt.grid(True) return ax
[docs]@dataclass class ConcreteServiceProfile(StressStrainProfile): """Abstract class for a concrete service stress-strain profile. :param strains: List of strains (must be increasing or equal) :param stresses: List of stresses :param ultimate_strain: Concrete strain at failure """ strains: List[float] stresses: List[float] elastic_modulus: float = field(init=False) ultimate_strain: float
[docs] def print_properties( self, fmt: str = "8.6e", ): """Prints the stress-strain profile properties to the terminal. :param fmt: Number format """ table = Table(title=f"Stress-Strain Profile - {type(self).__name__}") table.add_column("Property", justify="left", style="cyan", no_wrap=True) table.add_column("Value", justify="right", style="green") table.add_row( "Elastic Modulus", "{:>{fmt}}".format(self.get_elastic_modulus(), fmt=fmt) ) table.add_row( "Ultimate Compressive Strain", "{:>{fmt}}".format(self.get_ultimate_compressive_strain(), fmt=fmt), ) console = Console() console.print(table)
[docs] def get_elastic_modulus( self, ) -> float: """Returns the elastic modulus of the stress-strain profile. :return: Elastic modulus """ try: return self.elastic_modulus except AttributeError: return super().get_elastic_modulus()
[docs] def get_compressive_strength( self, ) -> Union[float, None]: """Returns the most positive stress. :return: Compressive strength """ return None
[docs] def get_tensile_strength( self, ) -> Union[float, None]: """Returns the most negative stress. :return: Tensile strength """ return None
[docs] def get_ultimate_compressive_strain( self, ) -> float: """Returns the largest strain. :return: Ultimate strain """ return self.ultimate_strain
[docs]@dataclass class ConcreteLinear(ConcreteServiceProfile): """Class for a symmetric linear stress-strain profile. :param elastic_modulus: Elastic modulus of the stress-strain profile :param ultimate_strain: Concrete strain at failure """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) elastic_modulus: float ultimate_strain: float = field(default=1) def __post_init__( self, ): self.strains = [-0.001, 0, 0.001] self.stresses = [-0.001 * self.elastic_modulus, 0, 0.001 * self.elastic_modulus]
[docs]@dataclass class ConcreteLinearNoTension(ConcreteServiceProfile): """Class for a linear stress-strain profile with no tensile strength. :param elastic_modulus: Elastic modulus of the stress-strain profile :param ultimate_strain: Concrete strain at failure :param compressive_strength: Compressive strength of the concrete """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) elastic_modulus: float ultimate_strain: float = field(default=1) compressive_strength: Union[float, None] = field(default=None) def __post_init__( self, ): self.strains = [-0.001, 0, 0.001] self.stresses = [0, 0, 0.001 * self.elastic_modulus] if self.compressive_strength is not None: self.strains[-1] = self.compressive_strength / self.elastic_modulus self.stresses[-1] = self.compressive_strength self.strains.append(self.ultimate_strain) self.stresses.append(self.compressive_strength)
[docs]@dataclass class EurocodeNonLinear(ConcreteServiceProfile): r"""Class for a non-linear stress-strain relationship to EC2. Tension is modelled with a symmetric ``elastic_modulus`` until failure at ``tensile_strength``, after which the tensile stress reduces according to the ``tension_softening_stiffness``. :param elastic_modulus: Concrete elastic modulus (:math:`E_{cm}`) :param ultimate_strain: Concrete strain at failure (:math:`\epsilon_{cu1}`) :param compressive_strength: Concrete compressive strength (:math:`f_{cm}`) :param compressive_strain: Strain at which the concrete stress equals the compressive strength (:math:`\epsilon_{c1}`) :param tensile_strength: Concrete tensile strength :param tension_softening_stiffness: Slope of the linear tension softening branch :param n_points_1: Number of points to discretise the curve prior to the peak stress :param n_points_2: Number of points to discretise the curve after the peak stress """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) elastic_modulus: float ultimate_strain: float compressive_strength: float compressive_strain: float tensile_strength: float tension_softening_stiffness: float n_points_1: int = field(default=10) n_points_2: int = field(default=3) def __post_init__( self, ): self.strains = [] self.stresses = [] # tensile portion of curve strain_tension_strength = -self.tensile_strength / self.elastic_modulus strain_zero_tension = ( strain_tension_strength - self.tensile_strength / self.tension_softening_stiffness ) self.strains.append(1.1 * strain_zero_tension) self.stresses.append(0) self.strains.append(strain_zero_tension) self.stresses.append(0) self.strains.append(strain_tension_strength) self.stresses.append(-self.tensile_strength) self.strains.append(0) self.stresses.append(0) # constants k = ( 1.05 * self.elastic_modulus * self.compressive_strain / self.compressive_strength ) # initialise concrete stress and strain conc_strain = 0 conc_stress = 0 # prior to peak stress for idx in range(self.n_points_1): conc_strain = self.compressive_strain / self.n_points_1 * (idx + 1) eta = conc_strain / self.compressive_strain conc_stress = ( self.compressive_strength * (k * eta - eta * eta) / (1 + eta * (k - 2)) ) self.strains.append(conc_strain) self.stresses.append(conc_stress) # after peak stress for idx in range(self.n_points_2): remaining_strain = self.ultimate_strain - self.compressive_strain conc_strain = ( self.compressive_strain + remaining_strain / self.n_points_2 * (idx + 1) ) eta = conc_strain / self.compressive_strain conc_stress = ( self.compressive_strength * (k * eta - eta * eta) / (1 + eta * (k - 2)) ) self.strains.append(conc_strain) self.stresses.append(conc_stress) # close off final stress self.strains.append(1.01 * conc_strain) self.stresses.append(conc_stress)
[docs]@dataclass class ModifiedMander(ConcreteServiceProfile): r"""Class for a non-linear stress-strain relationship based on the Mander stress-strain model for confined & unconfined concrete for a rectangular cross section. Intended for use with moment-curvature analyses with rectangular or circular cross sections. Refer to references [1]_ [2]_ [3]_ for further information on the Mander stress-strain models for confined and unconfined concrete. This stress strain relationship has been specifically modified for use as per the modified implementation documented within the NZSEE C5 assessment guidelines. However input parameters can also be customised to suit other implementations if desired. .. tip:: Optional input variables are only required for defining a confined concrete stress-strain relationship. Note if any variables are missed when attempting to define a confined concrete stress-strain relationship (using ``conc_confined=True``), then the material will default to being defined as an unconfined concrete stress-strain relationship with a warning given. .. admonition:: Modifications to Mander confined concrete model:- The original formulation of the expression for confined concrete presented by Mander et al. [1]_ can predict high levels of confined concrete strain dependant on the assumed value for the ultimate steel strain for the transverse reinforcement. The modified expression given the NZSEE C5 assesment guidelines [3]_ provides a correction and is directly implemented in the :class:`ModifiedMander` material class. These corrections to avoid overestimating the confined concrete limiting strain consist of three allowances:- - Modifying the maximum steel strain by a factor of 0.6:- - :math:`\varepsilon_{s,max}= 0.6\varepsilon_{su} \leq 0.06` - Note this 0.6 modifier can be altered via the ``n_steel_strain`` parameter. - Note the steel material used for reinforcement is also required to be defined with this same limiting fracture strain for a moment-curvature analysis. - Modifying the volumetric ratio of confinement reinforcement by a factor of 0.75. i.e.:- - For rectangular sections - :math:`\displaystyle{\rho_{st}=\frac{0.75}{s}\left[\frac{A_{v,d}} {b_{core}}+\frac{A_{v,b}}{d_{core}}\right]}` - For circular sections - :math:`\displaystyle{\rho_{st}=\frac{0.75}{s}\frac{4A_v}{d_s}}` - Note this 0.75 modifier can be altered via the ``n_confinement`` parameter. - For confined concrete utilising a maximum concrete compressive strain of:- - :math:`\displaystyle{\varepsilon_{c,max}=0.004+\frac{0.6\rho_{st}f_{yh} \varepsilon_{su}}{f'_{cc}}\leq0.05}` - Note that the 0.6 factor applied to the ultimate tensile failure strain can be modified as noted above. .. plot:: ./_static/doc_plots/mander_unconfined_plot.py mander_unconfined_plot :include-source: False :caption: ModifiedMander Parameters for Unconfined Concrete .. plot:: ./_static/doc_plots/mander_confined_plot.py mander_confined_plot :include-source: False :caption: ModifiedMander Parameters for Confined Concrete .. [1] Theoretical Stress-Strain Model For Confined Concrete - Mander, Priestley, Park (1988) .. [2] Observed Stress-Strain Behavior of Confined Concrete - Mander, Priestley, Park (1988) .. [3] NZSEE C5 Assessment Guidelines - Part C5 - Concrete Buildings - Technical Proposal to Revise the Engineering Assessment Guidelines (2018) :param elastic_modulus: Concrete elastic modulus (:math:`E_c`) :param compressive_strength: Concrete compressive strength (:math:`f'_c`) :param tensile_strength: Concrete tensile strength (:math:`f_t`) :param sect_type: The type of concrete cross section for which to create a confined concrete stress-strain relationship for:- - **rect** = Rectangular section with closed stirrup/tie transverse reinforcement - **circ_hoop** = Circular section with closed hoop transverse reinforcement - **circ_spiral** = Circular section with spiral transverse reinforcement :param conc_confined: True to return a confined concrete stress-strain relationship based on provided reinforcing parameters, False to return an unconfined concrete stress-strain relationship :param conc_tension: True to include tension in the concrete within the stress-strain relationship (up to the tensile strength of the concrete is reached), False to not consider any tension behaviour in the concrete :param conc_spalling: True to consider the spalling effect for unconfined concrete, False to not consider the spalling branch and truncate the unconfined concrete curve at min(:math:`2 \varepsilon_{co},\varepsilon_{c,max}`) :param eps_co: Strain at which the maximum concrete stress is obtained for an unconfined concrete material (:math:`\varepsilon_{co}`) :param eps_c_max_unconfined: Maximum strain that is able to be supported within unconfined concrete (:math:`\varepsilon_{c,max}`) :param eps_sp: Spalling strain, the strain at which the stress returns to zero for unconfined concrete (:math:`\varepsilon_{sp}`) :param d: Depth of a rectangular concrete cross section, or diameter of circular concrete cross section (:math:`d`) :param b: Breadth of a rectangular concrete cross section (:math:`b`) :param long_reinf_area: Total area of the longitudinal reinforcement in the concrete cross section (:math:`A_{st}`) :param w_dash: List of clear spacing between longitudinal reinforcement around the full perimeter of a rectangular concrete cross section (:math:`w'`) :param cvr: Concrete cover (to confining reinforcement) :param trans_spacing: Spacing of transverse confining reinforcement (:math:`s`) :param trans_d_b: Diameter of the transverse confining reinforcement (:math:`d_b`) :param trans_num_d: Number of legs/cross links parallel to the depth of a rectangular concrete cross section :param trans_num_b: Number of legs/cross links parallel to the breadth of a rectangular concrete cross section :param trans_f_y: Yield strength of the transverse confining reinforcement (:math:`f_{yh}`) :param eps_su: Strain at the ultimate tensile strength of the reinforcement (:math:`\varepsilon_{su}`) :param n_points: Number of points to discretise the compression part of the stress-strain curve between :math:`\varepsilon_{c}=0` & :math:`\varepsilon_{c} =2\varepsilon_{co}` for an unconfined concrete, or between :math:`\varepsilon_{c}=0` & :math:`\varepsilon_{c}=\varepsilon_{cu}` for a confined concrete :param n_steel_strain: Modifier for maximum steel reinforcement strain. Steel reinforcement material within the concrete cross section should also be defined with the same limit for the fracture strain :param n_confinement: Modifier for volumetric ratio of confinement reinforcement :raises ValueError: If specified section type is not rect, circ_hoop or circ_spiral """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) elastic_modulus: float ultimate_strain: float = field(init=False, default=0) compressive_strength: float tensile_strength: float sect_type: Optional[str] = None conc_confined: bool = False conc_tension: bool = False conc_spalling: bool = False eps_co: float = 0.002 eps_c_max_unconfined: float = 0.004 eps_sp: float = 0.006 d: Optional[float] = None b: Optional[float] = None long_reinf_area: Optional[float] = None w_dash: Optional[List[float]] = None cvr: Optional[float] = None trans_spacing: Optional[float] = None trans_d_b: Optional[float] = None trans_num_d: Optional[int] = None trans_num_b: Optional[int] = None trans_f_y: Optional[float] = None eps_su: Optional[float] = None n_points: int = field(default=50) n_steel_strain: float = 0.6 n_confinement: float = 0.75 def __post_init__( self, ): self.strains = [] self.stresses = [] # check section type is valid if self.conc_confined and str(self.sect_type).lower() not in [ "rect", "circ_hoop", "circ_spiral", ]: raise ValueError( f"The specified section type '{str(self.sect_type).lower()}' should be " f"'rect', 'circ_hoop' or 'circ_spiral'." ) if self.conc_confined and self.sect_type in ["circ_hoop", "circ_spiral"]: self.b = 0 self.w_dash = [0] self.trans_num_b = 0 self.trans_num_d = 0 # if confined concrete required, check that all inputs have been provided, # otherwise reset to unconfined stress-strain relationship and provide warning input_not_provided = [ i for i in self.__dataclass_fields__.keys() if self.__getattribute__(i) is None ] if self.conc_confined and input_not_provided: self.conc_confined = False warnings.warn( f"Reverting analysis to utilise an unconfined concrete Mander " f"stress-strain model, as the following input variables required for a " f"confined concrete Mander stress-strain model have not been " f"provided:-\n{input_not_provided}" ) # calculate confined/unconfined compressive strength if self.conc_confined: # calculate clear distance between transverse reinforcement s_dash = self.trans_spacing - self.trans_d_b if self.sect_type.lower() in ["rect"]: # calculate core dimensions (between centrelines of confining transverse # reinforcement) d_core = self.d - 2 * self.cvr - self.trans_d_b b_core = self.b - 2 * self.cvr - self.trans_d_b # calculate core area A_c = d_core * b_core # calculate area of transverse reinforcement in each direction within a depth s A_vd = self.trans_num_d * self.trans_d_b**2 * np.pi / 4 A_vb = self.trans_num_b * self.trans_d_b**2 * np.pi / 4 # calculate volumetric ratio of confinement reinforcement rho_st = ( self.n_confinement / self.trans_spacing * (A_vd / b_core + A_vb / d_core) ) # calculate ratio of reinforcement area to core area rho_cc = self.long_reinf_area / A_c # calculate plan area of ineffectually confined core concrete at the level of # the transverse reinforcement A_i = 0 for w in self.w_dash: A_i = A_i + pow(w, 2) # calculate confinement effectiveness coefficient k_e = ( (1 - A_i / (6 * A_c)) * (1 - s_dash / (2 * b_core)) * (1 - s_dash / (2 * d_core)) / (1 - rho_cc) ) # calculate tranverse reinforcement ratios and confining pressures # across defined depth rho_d = A_vd / (self.trans_spacing * b_core) f_ld = k_e * rho_d * self.trans_f_y # calculate tranverse reinforcement ratios and confining pressures # across defined width rho_b = A_vb / (self.trans_spacing * d_core) f_lb = k_e * rho_b * self.trans_f_y # calculate confined concrete strength f_cc = self.compressive_strength * ( -1.254 + 2.254 * (1 + 7.94 * min(f_lb, f_ld) / self.compressive_strength) ** 0.5 - 2 * min(f_lb, f_ld) / self.compressive_strength ) else: # calculate core diameter d_s = self.d - 2 * self.cvr - self.trans_d_b # calculate core area A_c = d_s**2 * np.pi / 4 # calculate volumetric ratio of confinement reinforcement rho_st = ( self.n_confinement / self.trans_spacing * (4 * self.trans_d_b**2 * np.pi / 4 / d_s) ) # calculate ratio of reinforcement area to core area rho_cc = self.long_reinf_area / A_c # calculate confinement effectiveness coefficient exp = 2 if self.sect_type in ["circ_hoop"] else 1 k_e = (1 - s_dash / (2 * d_s)) ** exp / (1 - rho_cc) # calculate tranverse confining pressures # rho_b = A_vb / (self.trans_spacing * d_core) f_l = k_e * rho_st * self.trans_f_y # calculate confined concrete strength f_cc = self.compressive_strength * ( -1.254 + 2.254 * (1 + 7.94 * f_l / self.compressive_strength) ** 0.5 - 2 * f_l / self.compressive_strength ) else: # calculate unconfined concrete strength f_cc = self.compressive_strength # calculate strain associated with max confined/unconfined concrete strength eps_cc = self.eps_co * (1 + 5 * (f_cc / self.compressive_strength - 1)) # calculate maximum confined/unconfined compressive strain if self.conc_confined: eps_c_max = min( 0.004 + self.n_steel_strain * rho_st * self.trans_f_y * self.eps_su / f_cc, 0.05, ) else: eps_c_max = self.eps_c_max_unconfined # calculate secant modulus E_sec = f_cc / eps_cc if self.conc_confined: self.strains = np.linspace(0, eps_c_max, self.n_points) # add eps_cc point corresponding to max stress point at end self.strains = np.append(self.strains, eps_cc) else: self.strains = np.linspace(0, min(2 * eps_cc, eps_c_max), self.n_points) # add eps_cc point corresponding to max stress point at end self.strains = np.append(self.strains, eps_cc) # sort strains numerically self.strains.sort() # calculate stresses from strains & convert to List r = self.elastic_modulus / (self.elastic_modulus - E_sec) x = self.strains / eps_cc self.strains = self.strains.tolist() self.stresses = (f_cc * x * r / (r - 1 + x**r)).tolist() # add spalling branch if specified for unconfined curve if not self.conc_confined and self.conc_spalling: self.strains.append(self.eps_sp) self.stresses.append(0) # calculate max tension strain based on modulus of rupture/concrete tension # strength eps_t = self.tensile_strength / self.elastic_modulus if self.conc_tension: # add tension stress/strain limit self.strains.insert(0, -eps_t) self.stresses.insert(0, -self.tensile_strength) self.strains.insert(0, self.strains[0]) self.stresses.insert(0, 0) self.strains.insert(0, 2 * self.strains[0]) self.stresses.insert(0, 0) else: # add flat horizontal tension stress/strain branch self.strains.insert(0, -eps_t) self.stresses.insert(0, 0) # initiate ultimate compressive strain as maximum strain self.ultimate_strain = max(self.strains) # add small horizontal compressive strain to improve interpolation self.strains.append(self.strains[-1] + 1e-12) self.stresses.append(self.stresses[-1])
[docs]@dataclass class ConcreteUltimateProfile(StressStrainProfile): """Abstract class for a concrete ultimate stress-strain profile. :param strains: List of strains (must be increasing or equal) :param stresses: List of stresses :param compressive_strength: Concrete compressive strength """ strains: List[float] stresses: List[float] compressive_strength: float
[docs] def get_compressive_strength( self, ) -> float: """Returns the most positive stress. :return: Compressive strength """ return self.compressive_strength
[docs] def get_ultimate_compressive_strain( self, ) -> float: """Returns the ultimate strain, or largest compressive strain. :return: Ultimate strain """ try: return self.ultimate_strain # type: ignore except AttributeError: return super().get_ultimate_compressive_strain()
[docs] def print_properties( self, fmt: str = "8.6e", ): """Prints the stress-strain profile properties to the terminal. :param fmt: Number format """ table = Table(title=f"Stress-Strain Profile - {type(self).__name__}") table.add_column("Property", justify="left", style="cyan", no_wrap=True) table.add_column("Value", justify="right", style="green") table.add_row( "Compressive Strength", "{:>{fmt}}".format(self.get_compressive_strength(), fmt=fmt), ) table.add_row( "Ultimate Compressive Strain", "{:>{fmt}}".format(self.get_ultimate_compressive_strain(), fmt=fmt), ) console = Console() console.print(table)
[docs]@dataclass class RectangularStressBlock(ConcreteUltimateProfile): """Class for a rectangular stress block. :param compressive_strength: Concrete compressive strength :param alpha: Factor that modifies the concrete compressive strength :param gamma: Factor that modifies the depth of the stress block :param ultimate_strain: Concrete strain at failure """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) compressive_strength: float alpha: float gamma: float ultimate_strain: float def __post_init__( self, ): self.strains = [ 0, self.ultimate_strain * (1 - self.gamma), self.ultimate_strain * (1 - self.gamma), self.ultimate_strain, ] self.stresses = [ 0, 0, self.alpha * self.compressive_strength, self.alpha * self.compressive_strength, ]
[docs] def get_stress( self, strain: float, ) -> float: """Returns a stress given a strain. Overrides parent method with small tolerance to aid ultimate stress generation at nodes. :param strain: Strain at which to return a stress. :return: Stress """ if strain >= self.strains[1] - 1e-8: return self.stresses[2] else: return 0
[docs]@dataclass class BilinearStressStrain(ConcreteUltimateProfile): """Class for a bilinear stress-strain relationship. :param compressive_strength: Concrete compressive strength :param compressive_strain: Strain at which the concrete stress equals the compressive strength :param ultimate_strain: Concrete strain at failure """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) compressive_strength: float compressive_strain: float ultimate_strain: float def __post_init__( self, ): self.strains = [ -self.compressive_strain, 0, self.compressive_strain, self.ultimate_strain, ] self.stresses = [ 0, 0, self.compressive_strength, self.compressive_strength, ]
[docs]@dataclass class EurocodeParabolicUltimate(ConcreteUltimateProfile): """Class for an ultimate parabolic stress-strain relationship to EC2. :param compressive_strength: Concrete compressive strength :param compressive_strain: Strain at which the concrete stress equals the compressive strength :param ultimate_strain: Concrete strain at failure :param n: Parabolic curve exponent :param n_points: Number of points to discretise the parabolic segment of the curve """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) compressive_strength: float compressive_strain: float ultimate_strain: float n: float n_points: int = field(default=10) def __post_init__( self, ): self.strains = [] self.stresses = [] # tensile portion of curve self.strains.append(-self.compressive_strain) self.stresses.append(0) self.strains.append(0) self.stresses.append(0) # parabolic portion of curve for idx in range(self.n_points): conc_strain = self.compressive_strain / self.n_points * (idx + 1) conc_stress = self.compressive_strength * ( 1 - np.power(1 - (conc_strain / self.compressive_strain), self.n) ) self.strains.append(conc_strain) self.stresses.append(conc_stress) # compressive plateau self.strains.append(self.ultimate_strain) self.stresses.append(self.compressive_strength)
[docs]@dataclass class SteelProfile(StressStrainProfile): """Abstract class for a steel stress-strain profile. :param strains: List of strains (must be increasing or equal) :param stresses: List of stresses :param yield_strength: Steel yield strength :param elastic_modulus: Steel elastic modulus :param fracture_strain: Steel fracture strain """ strains: List[float] stresses: List[float] yield_strength: float elastic_modulus: float fracture_strain: float
[docs] def get_elastic_modulus( self, ) -> float: """Returns the elastic modulus of the stress-strain profile. :return: Elastic modulus """ return self.elastic_modulus
[docs] def get_yield_strength( self, ) -> float: """Returns the yield strength of the stress-strain profile. :return: Yield strength """ return self.yield_strength
[docs] def print_properties( self, fmt: str = "8.6e", ): """Prints the stress-strain profile properties to the terminal. :param fmt: Number format """ table = Table(title=f"Stress-Strain Profile - {type(self).__name__}") table.add_column("Property", justify="left", style="cyan", no_wrap=True) table.add_column("Value", justify="right", style="green") table.add_row( "Elastic Modulus", "{:>{fmt}}".format(self.get_elastic_modulus(), fmt=fmt) ) table.add_row( "Yield Strength", "{:>{fmt}}".format(self.yield_strength, fmt=fmt) ) table.add_row( "Tensile Strength", "{:>{fmt}}".format(-self.get_tensile_strength(), fmt=fmt), ) table.add_row( "Fracture Strain", "{:>{fmt}}".format(self.get_ultimate_tensile_strain(), fmt=fmt), ) console = Console() console.print(table)
[docs]@dataclass class SteelElasticPlastic(SteelProfile): """Class for a perfectly elastic-plastic steel stress-strain profile. :param yield_strength: Steel yield strength :param elastic_modulus: Steel elastic modulus :param fracture_strain: Steel fracture strain """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) yield_strength: float elastic_modulus: float fracture_strain: float def __post_init__( self, ): yield_strain = self.yield_strength / self.elastic_modulus self.strains = [ -self.fracture_strain, -yield_strain, 0, yield_strain, self.fracture_strain, ] self.stresses = [ -self.yield_strength, -self.yield_strength, 0, self.yield_strength, self.yield_strength, ]
[docs]@dataclass class SteelHardening(SteelProfile): """Class for a steel stress-strain profile with strain hardening. :param yield_strength: Steel yield strength :param elastic_modulus: Steel elastic modulus :param fracture_strain: Steel fracture strain :param ultimate_strength: Steel ultimate strength """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) yield_strength: float elastic_modulus: float fracture_strain: float ultimate_strength: float def __post_init__( self, ): yield_strain = self.yield_strength / self.elastic_modulus self.strains = [ -self.fracture_strain, -yield_strain, 0, yield_strain, self.fracture_strain, ] self.stresses = [ -self.ultimate_strength, -self.yield_strength, 0, self.yield_strength, self.ultimate_strength, ]
[docs]@dataclass class StrandProfile(StressStrainProfile): """Abstract class for a steel strand stress-strain profile. Implements a piecewise linear stress-strain profile. Positive stresses & strains are compression. :param strains: List of strains (must be increasing or equal) :param stresses: List of stresses :param yield_strength: Strand yield strength """ strains: List[float] stresses: List[float] yield_strength: float def __post_init__(self) -> None: return super().__post_init__()
[docs] def get_strain( self, stress: float, ) -> float: """Returns a strain given a stress. :param stress: Stress at which to return a strain. :return: Strain """ # create interpolation function strain_function = interp1d( x=self.stresses, y=self.strains, kind="linear", fill_value="extrapolate", # type: ignore ) return strain_function(stress)
[docs] def get_yield_strength(self) -> float: """Returns the yield strength of the stress-strain profile. :return: Yield strength """ return self.yield_strength
[docs] def print_properties( self, fmt: str = "8.6e", ) -> None: """Prints the stress-strain profile properties to the terminal. :param fmt: Number format """ table = Table(title=f"Stress-Strain Profile - {type(self).__name__}") table.add_column("Property", justify="left", style="cyan", no_wrap=True) table.add_column("Value", justify="right", style="green") table.add_row( "Elastic Modulus", "{:>{fmt}}".format(self.get_elastic_modulus(), fmt=fmt) ) table.add_row( "Yield Strength", "{:>{fmt}}".format(self.yield_strength, fmt=fmt) ) table.add_row( "Breaking Strength", "{:>{fmt}}".format(-self.get_tensile_strength(), fmt=fmt), ) table.add_row( "Fracture Strain", "{:>{fmt}}".format(self.get_ultimate_tensile_strain(), fmt=fmt), ) console = Console() console.print(table)
[docs]@dataclass class StrandHardening(StrandProfile): """Class for a strand stress-strain profile with strain hardening. :param yield_strength: Strand yield strength :param elastic_modulus: Strand elastic modulus :param fracture_strain: Strand fracture strain :param breaking_strength: Strand breaking strength """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) yield_strength: float elastic_modulus: float fracture_strain: float breaking_strength: float def __post_init__(self) -> None: yield_strain = self.yield_strength / self.elastic_modulus self.strains = [ -self.fracture_strain, -yield_strain, 0, yield_strain, self.fracture_strain, ] self.stresses = [ -self.breaking_strength, -self.yield_strength, 0, self.yield_strength, self.breaking_strength, ] return super().__post_init__()
[docs] def get_elastic_modulus( self, ) -> float: """Returns the elastic modulus of the stress-strain profile. :return: Elastic modulus """ return self.elastic_modulus
[docs]@dataclass class StrandPCI1992(StrandProfile): """Class for a strand stress-strain profile by R. Devalapura and M. Tadros from the March-April issue of the PCI Journal. :param yield_strength: Strand yield strength :param elastic_modulus: Strand elastic modulus :param fracture_strain: Strand fracture strain :param breaking_strength: Strand breaking strength :param bilinear_yield_ratio: Ratio between the stress at the intersection of a bilinear profile, and the yield strength :param strain_cps: Strain control points, generates the following strain segments: ``[0, strain_cps[0], strain_cps[1], fracture_strain]``. Length must be equal to 2. :param n_points: Number of points to discretise within each strain segment. Length must be equal to 3. """ strains: List[float] = field(init=False) stresses: List[float] = field(init=False) yield_strength: float elastic_modulus: float fracture_strain: float breaking_strength: float bilinear_yield_ratio: float = 1.04 strain_cps: List[float] = field(default_factory=lambda: [0.005, 0.015]) n_points: List[int] = field(default_factory=lambda: [5, 14, 5]) def __post_init__(self) -> None: # validate control points if len(self.strain_cps) != 2: raise ValueError("Length of strain_cps must be equal to 2.") if len(self.n_points) != 3: raise ValueError("Length of n_points must be equal to 3.") # determine constants f_so = self.bilinear_yield_ratio * self.yield_strength const_c = self.elastic_modulus / f_so const_a = ( self.elastic_modulus * (self.breaking_strength - f_so) / (self.fracture_strain * self.elastic_modulus - f_so) ) const_b = self.elastic_modulus - const_a # function that determines the stress def stress_eq(a, b, c, d, eps_ps, f_pu): if eps_ps != 0: sign = eps_ps / abs(eps_ps) # get sign of strain else: sign = 1 eps_ps = abs(eps_ps) # ensure strain is positive denom = pow(1 + pow(c * eps_ps, d), 1 / d) # calculate denominator stress = min(eps_ps * (a + b / denom), f_pu) # calculate stress return sign * stress # determine constant D that yields the yield strength at a strain of 0.01 def find_d(const_d): return ( stress_eq( a=const_a, b=const_b, c=const_c, d=const_d, eps_ps=0.01, f_pu=self.breaking_strength, ) - self.yield_strength ) const_d = brentq(f=find_d, a=1, b=20) # generate stresses and strains seg1 = np.linspace( start=0, stop=self.strain_cps[0], num=self.n_points[0], endpoint=False ) seg2 = np.linspace( start=self.strain_cps[0], stop=self.strain_cps[1], num=self.n_points[1], endpoint=False, ) seg3 = np.linspace( start=self.strain_cps[1], stop=self.fracture_strain, num=self.n_points[2] ) strain_list = seg1.tolist() + seg2.tolist() + seg3.tolist() # generate compressive region of profile strains_c = [] stresses_c = [] for strain in strain_list: strains_c.append(strain) stresses_c.append( stress_eq( a=const_a, b=const_b, c=const_c, d=const_d, eps_ps=strain, f_pu=self.breaking_strength, ) ) # generate tensile region of profile strains_t = [eps * -1.0 for eps in strains_c[:0:-1]] stresses_t = [sig * -1.0 for sig in stresses_c[:0:-1]] # combine lists self.strains = strains_t + strains_c self.stresses = stresses_t + stresses_c return super().__post_init__()