Source code for concreteproperties.stress_strain_profile

from __future__ import annotations

import warnings
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, 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 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 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, ]