From 1bb0f0056f21b67b1bcab4dc465f16f3cce6482e Mon Sep 17 00:00:00 2001 From: gabe-kafka <225996232+gabe-kafka@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:52:53 -0400 Subject: [PATCH 1/2] feat: add ACI 318-19 concrete and reinforcement material properties Add the first American design code to the library. Implements material properties from ACI 318-19 Chapters 19, 20, and 22: Concrete (codes/aci318): - Ec: modulus of elasticity (Table 19.2.2.1) - fr: modulus of rupture (Eq. 19.2.3.1) - beta1: Whitney stress block depth factor (Table 22.2.2.4.3) - eps_cu: ultimate concrete strain (Section 22.2.2.1) - alpha1: stress block intensity (Section 22.2.2.4.1) - fct: splitting tensile strength (Section 19.2.4.3) - lambda_factor: lightweight modification factor (Table 19.2.4.2) Reinforcement (codes/aci318): - Es: modulus of elasticity (Section 20.2.2.2) - fy_design: design yield strength - epsyd: yield strain - reinforcement_grade_props: ASTM grade lookup (Table 20.2.2.4a) Material classes: - ConcreteACI318 with elastic, parabola-rectangle, and bilinear constitutive laws - ReinforcementACI318 with elastic and elastic-plastic laws ACI 318 uses LRFD (strength reduction factors on member capacity) rather than partial safety factors on material strength. Material classes default gamma_c=1.0 and gamma_s=1.0 accordingly. Ref: fib-international/structuralcodes#187 --- structuralcodes/codes/__init__.py | 4 +- structuralcodes/codes/aci318/__init__.py | 37 +++ .../aci318/_concrete_material_properties.py | 164 ++++++++++ .../_reinforcement_material_properties.py | 100 ++++++ .../materials/concrete/__init__.py | 3 + .../materials/concrete/_concreteACI318.py | 289 ++++++++++++++++++ .../materials/reinforcement/__init__.py | 3 + .../reinforcement/_reinforcementACI318.py | 164 ++++++++++ tests/test_aci318/__init__.py | 1 + ...est_aci318_concrete_material_properties.py | 149 +++++++++ ...ci318_reinforcement_material_properties.py | 88 ++++++ tests/test_aci318/test_concrete_aci318.py | 119 ++++++++ .../test_aci318/test_reinforcement_aci318.py | 121 ++++++++ 13 files changed, 1241 insertions(+), 1 deletion(-) create mode 100644 structuralcodes/codes/aci318/__init__.py create mode 100644 structuralcodes/codes/aci318/_concrete_material_properties.py create mode 100644 structuralcodes/codes/aci318/_reinforcement_material_properties.py create mode 100644 structuralcodes/materials/concrete/_concreteACI318.py create mode 100644 structuralcodes/materials/reinforcement/_reinforcementACI318.py create mode 100644 tests/test_aci318/__init__.py create mode 100644 tests/test_aci318/test_aci318_concrete_material_properties.py create mode 100644 tests/test_aci318/test_aci318_reinforcement_material_properties.py create mode 100644 tests/test_aci318/test_concrete_aci318.py create mode 100644 tests/test_aci318/test_reinforcement_aci318.py diff --git a/structuralcodes/codes/__init__.py b/structuralcodes/codes/__init__.py index 1877b243..b6e7cc6d 100644 --- a/structuralcodes/codes/__init__.py +++ b/structuralcodes/codes/__init__.py @@ -3,9 +3,10 @@ import types import typing as t -from . import ec2_2004, ec2_2023, mc2010, mc2020 +from . import aci318, ec2_2004, ec2_2023, mc2010, mc2020 __all__ = [ + 'aci318', 'mc2010', 'mc2020', 'ec2_2023', @@ -23,6 +24,7 @@ # Design code registry _DESIGN_CODES = { + 'aci318': aci318, 'mc2010': mc2010, 'mc2020': mc2020, 'ec2_2004': ec2_2004, diff --git a/structuralcodes/codes/aci318/__init__.py b/structuralcodes/codes/aci318/__init__.py new file mode 100644 index 00000000..0c45a3a0 --- /dev/null +++ b/structuralcodes/codes/aci318/__init__.py @@ -0,0 +1,37 @@ +"""ACI 318-19.""" + +import typing as t + +from ._concrete_material_properties import ( + Ec, + alpha1, + beta1, + eps_cu, + fct, + fr, + lambda_factor, +) +from ._reinforcement_material_properties import ( + Es, + epsyd, + fy_design, + reinforcement_grade_props, +) + +__all__ = [ + 'Ec', + 'Es', + 'alpha1', + 'beta1', + 'eps_cu', + 'epsyd', + 'fct', + 'fr', + 'fy_design', + 'lambda_factor', + 'reinforcement_grade_props', +] + +__title__: str = 'ACI 318-19' +__year__: str = '2019' +__materials__: t.Tuple[str] = ('concrete', 'reinforcement') diff --git a/structuralcodes/codes/aci318/_concrete_material_properties.py b/structuralcodes/codes/aci318/_concrete_material_properties.py new file mode 100644 index 00000000..c5bdff37 --- /dev/null +++ b/structuralcodes/codes/aci318/_concrete_material_properties.py @@ -0,0 +1,164 @@ +"""Concrete material properties according to ACI 318-19.""" + +from __future__ import annotations + +import math +import typing as t + +LAMBDA_FACTORS = { + 'normalweight': 1.0, + 'sand-lightweight': 0.85, + 'all-lightweight': 0.75, +} + + +def Ec(fc: float, wc: float = 2320.0) -> float: + """The modulus of elasticity of concrete. + + ACI 318-19, Table 19.2.2.1. + + Args: + fc (float): The specified compressive strength of concrete in + MPa. + + Keyword Args: + wc (float): The unit weight of concrete in kg/m3. + Default is 2320 kg/m3 (normalweight concrete). + + Returns: + float: The modulus of elasticity in MPa. + + Raises: + ValueError: If fc is not positive. + ValueError: If wc is outside the range 1440-2560 kg/m3. + """ + if fc <= 0: + raise ValueError(f'fc={fc} must be positive') + if wc < 1440 or wc > 2560: + raise ValueError(f'wc={wc} must be between 1440 and 2560 kg/m3') + return wc**1.5 * 0.043 * math.sqrt(fc) + + +def fr(fc: float, lambda_s: float = 1.0) -> float: + """The modulus of rupture of concrete. + + ACI 318-19, Eq. 19.2.3.1. + + Args: + fc (float): The specified compressive strength of concrete in + MPa. + + Keyword Args: + lambda_s (float): The modification factor for lightweight + concrete. Default is 1.0 (normalweight). + + Returns: + float: The modulus of rupture in MPa. + + Raises: + ValueError: If fc is not positive. + ValueError: If lambda_s is not in (0, 1]. + """ + if fc <= 0: + raise ValueError(f'fc={fc} must be positive') + if lambda_s <= 0 or lambda_s > 1.0: + raise ValueError(f'lambda_s={lambda_s} must be in the range (0, 1]') + return 0.62 * lambda_s * math.sqrt(fc) + + +def beta1(fc: float) -> float: + """The Whitney stress block depth factor. + + ACI 318-19, Table 22.2.2.4.3. + + Args: + fc (float): The specified compressive strength of concrete in + MPa. + + Returns: + float: The stress block depth factor (dimensionless). + + Raises: + ValueError: If fc is not positive. + """ + if fc <= 0: + raise ValueError(f'fc={fc} must be positive') + if fc <= 28: + return 0.85 + if fc >= 55: + return 0.65 + return 0.85 - 0.05 * (fc - 28) / 7 + + +def eps_cu() -> float: + """The maximum usable strain at the extreme concrete compression fiber. + + ACI 318-19, Section 22.2.2.1. + + Returns: + float: The ultimate concrete strain (dimensionless). + """ + return 0.003 + + +def lambda_factor( + concrete_type: t.Literal[ + 'normalweight', 'sand-lightweight', 'all-lightweight' + ], +) -> float: + """The modification factor for lightweight concrete. + + ACI 318-19, Table 19.2.4.2. + + Args: + concrete_type (str): The concrete type. One of + 'normalweight', 'sand-lightweight', or 'all-lightweight'. + + Returns: + float: The lightweight modification factor (dimensionless). + + Raises: + ValueError: If concrete_type is not recognized. + """ + result = LAMBDA_FACTORS.get(concrete_type.lower()) + if result is None: + raise ValueError( + f'Unknown concrete type: {concrete_type}. ' + f'Valid types: {list(LAMBDA_FACTORS.keys())}' + ) + return result + + +def fct(fc: float, lambda_s: float = 1.0) -> float: + """The approximate splitting tensile strength of concrete. + + ACI 318-19, Section 19.2.4.3. + + Args: + fc (float): The specified compressive strength of concrete in + MPa. + + Keyword Args: + lambda_s (float): The modification factor for lightweight + concrete. Default is 1.0 (normalweight). + + Returns: + float: The splitting tensile strength in MPa. + + Raises: + ValueError: If fc is not positive. + """ + if fc <= 0: + raise ValueError(f'fc={fc} must be positive') + return 0.56 * lambda_s * math.sqrt(fc) + + +def alpha1() -> float: + """The ratio of equivalent rectangular stress block intensity. + + ACI 318-19, Section 22.2.2.4.1. + + Returns: + float: The stress block intensity factor (dimensionless). + """ + return 0.85 diff --git a/structuralcodes/codes/aci318/_reinforcement_material_properties.py b/structuralcodes/codes/aci318/_reinforcement_material_properties.py new file mode 100644 index 00000000..03aa4969 --- /dev/null +++ b/structuralcodes/codes/aci318/_reinforcement_material_properties.py @@ -0,0 +1,100 @@ +"""Reinforcement material properties according to ACI 318-19.""" + +from __future__ import annotations + +import typing as t + +REINFORCEMENT_GRADES = { + '40': {'fy': 280.0, 'fu': 420.0}, + '60': {'fy': 420.0, 'fu': 550.0}, + '80': {'fy': 550.0, 'fu': 690.0}, + '100': {'fy': 690.0, 'fu': 860.0}, +} + + +def Es() -> float: + """The modulus of elasticity of reinforcement. + + ACI 318-19, Section 20.2.2.2. + + Returns: + float: The modulus of elasticity in MPa. + """ + return 200000.0 + + +def fy_design(fy: float, phi: float = 1.0) -> float: + """The design yield strength of reinforcement. + + ACI 318-19 applies strength reduction factors (phi) at the + member capacity level, not the material level. The default + phi=1.0 returns the unreduced yield strength, which is the + standard ACI convention for material properties. + + Args: + fy (float): The specified yield strength in MPa. + + Keyword Args: + phi (float): Optional strength reduction factor. + Default is 1.0 (no reduction). + + Returns: + float: The design yield strength in MPa. + + Raises: + ValueError: If fy is not positive. + ValueError: If phi is not in (0, 1]. + """ + if fy <= 0: + raise ValueError(f'fy={fy} must be positive') + if phi <= 0 or phi > 1.0: + raise ValueError(f'phi={phi} must be in the range (0, 1]') + return phi * fy + + +def epsyd(fy: float, _Es: float = 200000.0) -> float: + """The yield strain of reinforcement. + + Args: + fy (float): The specified yield strength in MPa. + + Keyword Args: + _Es (float): The modulus of elasticity in MPa. + Default is 200000 MPa. + + Returns: + float: The yield strain (dimensionless). + + Raises: + ValueError: If fy is not positive. + """ + if fy <= 0: + raise ValueError(f'fy={fy} must be positive') + return fy / _Es + + +def reinforcement_grade_props( + grade: t.Literal['40', '60', '80', '100'], +) -> t.Dict[str, float]: + """Return the minimum specified properties for a reinforcement grade. + + ACI 318-19, Table 20.2.2.4a (SI equivalents). + + Args: + grade (str): The ASTM reinforcement grade designation. + One of '40', '60', '80', or '100'. + + Returns: + Dict[str, float]: A dict with keys 'fy' (yield strength in + MPa) and 'fu' (ultimate strength in MPa). + + Raises: + ValueError: If the grade is not recognized. + """ + props = REINFORCEMENT_GRADES.get(str(grade)) + if props is None: + raise ValueError( + f'Unknown reinforcement grade: {grade}. ' + f'Valid grades: {list(REINFORCEMENT_GRADES.keys())}' + ) + return dict(props) diff --git a/structuralcodes/materials/concrete/__init__.py b/structuralcodes/materials/concrete/__init__.py index 715a79ac..9d403705 100644 --- a/structuralcodes/materials/concrete/__init__.py +++ b/structuralcodes/materials/concrete/__init__.py @@ -5,6 +5,7 @@ from structuralcodes.codes import _use_design_code from ._concrete import Concrete +from ._concreteACI318 import ConcreteACI318 from ._concreteEC2_2004 import ConcreteEC2_2004 from ._concreteEC2_2023 import ConcreteEC2_2023 from ._concreteMC2010 import ConcreteMC2010 @@ -12,12 +13,14 @@ __all__ = [ 'create_concrete', 'Concrete', + 'ConcreteACI318', 'ConcreteMC2010', 'ConcreteEC2_2023', 'ConcreteEC2_2004', ] CONCRETES: t.Dict[str, Concrete] = { + 'ACI 318-19': ConcreteACI318, 'fib Model Code 2010': ConcreteMC2010, 'EUROCODE 2 1992-1-1:2004': ConcreteEC2_2004, 'EUROCODE 2 1992-1-1:2023': ConcreteEC2_2023, diff --git a/structuralcodes/materials/concrete/_concreteACI318.py b/structuralcodes/materials/concrete/_concreteACI318.py new file mode 100644 index 00000000..9a8a60a3 --- /dev/null +++ b/structuralcodes/materials/concrete/_concreteACI318.py @@ -0,0 +1,289 @@ +"""The concrete class for ACI 318-19 Concrete Material.""" + +import typing as t + +from structuralcodes.codes import aci318 + +from ..constitutive_laws import ConstitutiveLaw, create_constitutive_law +from ._concrete import Concrete + + +class ConcreteACI318(Concrete): # noqa: N801 + """Concrete implementation for ACI 318-19. + + Note: + ACI 318 uses specified compressive strength (fc') rather than + characteristic strength (fck). The parameter is named fck for + compatibility with the base class, but represents fc' in ACI + notation. An fc property is provided as an alias. + + Usage philosophy (LRFD vs. partial factor method): + ACI 318 uses Load and Resistance Factor Design (LRFD), which + differs from the Eurocode / fib Model Code partial factor + method used elsewhere in this library. Two consequences for + users of this class: + + 1. Material strengths are not reduced at the material level. + gamma_c defaults to 1.0 and should be left at 1.0 for + standard ACI 318 design. Reducing fc' via gamma_c is not + ACI 318 compliant. + + 2. Strength reduction factors (phi, per ACI 318-19 Section + 21.2) must be applied by the user at the member capacity + level. This class does not apply phi for you. + + As with the Eurocode materials, the user is responsible for + choosing a constitutive law and parameters suited to the + limit state being analyzed: + + - Serviceability / cracked-stiffness estimates: use the + 'elastic' law with Ec, parameterized with mean (expected) + properties. + - Ultimate limit state (flexure, axial, biaxial interaction + domains): use 'parabolarectangle' (the default) or + 'bilinearcompression', which apply the ACI 318-19 Section + 22.2 stress block parameters (alpha1=0.85, eps_cu=0.003). + Compute nominal capacities Pn, Mn with this material, then + multiply by the appropriate phi factor at the member level + to obtain phi*Pn and phi*Mn. + """ + + _Ec: t.Optional[float] = None + _fr: t.Optional[float] = None + _fct: t.Optional[float] = None + _wc: float = 2320.0 + _lambda_s: float = 1.0 + + def __init__( + self, + fck: float, + name: t.Optional[str] = None, + density: float = 2320, + gamma_c: t.Optional[float] = None, + constitutive_law: t.Optional[ + t.Union[ + t.Literal[ + 'elastic', + 'parabolarectangle', + 'bilinearcompression', + ], + ConstitutiveLaw, + ] + ] = 'parabolarectangle', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, + Ec: t.Optional[float] = None, + fr: t.Optional[float] = None, + fct: t.Optional[float] = None, + wc: float = 2320.0, + lambda_s: float = 1.0, + **kwargs, + ) -> None: + """Initializes a new instance of Concrete for ACI 318-19. + + Arguments: + fck (float): Specified compressive strength (fc') in MPa. + + Keyword Arguments: + name (str): A descriptive name for concrete. + density (float): Density of material in kg/m3 + (default: 2320). + gamma_c (float, optional): Partial factor for concrete. + Default is 1.0 (ACI does not use material partial + factors). + constitutive_law (ConstitutiveLaw | str): A valid + ConstitutiveLaw object or string. Valid options: + 'elastic', 'parabolarectangle', + 'bilinearcompression'. + initial_strain (Optional[float]): Initial strain. + initial_stress (Optional[float]): Initial stress. + strain_compatibility (Optional[bool]): If True, the + material deforms with the geometry. + Ec (float, optional): The modulus of elasticity in MPa. + fr (float, optional): The modulus of rupture in MPa. + fct (float, optional): The splitting tensile strength + in MPa. + wc (float): Unit weight of concrete in kg/m3 + (default: 2320). + lambda_s (float): Lightweight modification factor + (default: 1.0 for normalweight). + + Raises: + ValueError: If the constitutive law is not valid for + concrete. + """ + del kwargs + if name is None: + name = f'C{round(fck):d}' + super().__init__( + fck=fck, + name=name, + density=density, + existing=False, + gamma_c=gamma_c, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + ) + self._Ec = abs(Ec) if Ec is not None else None + self._fr = abs(fr) if fr is not None else None + self._fct = abs(fct) if fct is not None else None + self._wc = wc + self._lambda_s = lambda_s + + self._constitutive_law = ( + constitutive_law + if isinstance(constitutive_law, ConstitutiveLaw) + else create_constitutive_law( + constitutive_law_name=constitutive_law, material=self + ) + ) + if 'concrete' not in self._constitutive_law.__materials__: + raise ValueError( + 'The provided constitutive law is not valid for concrete.' + ) + self._apply_initial_strain() + + @property + def fc(self) -> float: + """Returns the specified compressive strength (fc') in MPa. + + Returns: + float: The specified compressive strength in MPa. + + Note: + This is an alias for fck, using ACI notation. + """ + return self._fck + + @property + def gamma_c(self) -> float: + """The partial factor for concrete. + + Returns: + float: The partial factor (default 1.0 for ACI 318). + + Note: + ACI 318 does not use material partial factors. The + default value of 1.0 maintains compatibility with the + base class interface. + """ + return self._gamma_c or 1.0 + + @property + def Ec(self) -> float: + """Returns the modulus of elasticity in MPa. + + Returns: + float: The modulus of elasticity in MPa. + + Note: + Derived from fc and wc if not manually provided. + """ + if self._Ec is None: + return aci318.Ec(self._fck, self._wc) + return self._Ec + + @property + def fr(self) -> float: + """Returns the modulus of rupture in MPa. + + Returns: + float: The modulus of rupture in MPa. + + Note: + Derived from fc and lambda_s if not manually provided. + """ + if self._fr is None: + return aci318.fr(self._fck, self._lambda_s) + return self._fr + + @property + def beta1(self) -> float: + """Returns the Whitney stress block depth factor. + + Returns: + float: The stress block depth factor (dimensionless). + """ + return aci318.beta1(self._fck) + + @property + def eps_cu(self) -> float: + """Returns the ultimate concrete strain. + + Returns: + float: The ultimate strain (dimensionless). + """ + return aci318.eps_cu() + + @property + def alpha1(self) -> float: + """Returns the stress block intensity factor. + + Returns: + float: The stress block intensity (dimensionless). + """ + return aci318.alpha1() + + @property + def fct(self) -> float: + """Returns the splitting tensile strength in MPa. + + Returns: + float: The splitting tensile strength in MPa. + + Note: + Derived from fc and lambda_s if not manually provided. + """ + if self._fct is None: + return aci318.fct(self._fck, self._lambda_s) + return self._fct + + def fcd(self) -> float: + """The design compressive strength in MPa. + + Returns: + float: The design compressive strength in MPa. + + Note: + Returns alpha1 * fc / gamma_c. With ACI 318 defaults + (alpha1=0.85, gamma_c=1.0), this gives 0.85*fc'. + """ + return self.alpha1 * self._fck / self.gamma_c + + def __elastic__(self) -> dict: + """Returns kwargs for an elastic constitutive law.""" + return {'E': self.Ec} + + def __parabolarectangle__(self) -> dict: + """Returns kwargs for a parabola-rectangle constitutive law. + + Note: + Uses ACI 318 parameters: peak strain at 0.002, + ultimate strain at 0.003, parabolic exponent n=2. + """ + return { + 'fc': self.fcd(), + 'eps_0': 0.002, + 'eps_u': self.eps_cu, + 'n': 2, + } + + def __bilinearcompression__(self) -> dict: + """Returns kwargs for a bilinear compression constitutive law. + + Note: + Provides an elastic-perfectly-plastic alternative to + the parabola-rectangle default. This is not the ACI + 318 Whitney rectangular stress block. The library + integrates the parabola-rectangle curve directly, + which ACI 318-19 Section 22.2.2.3 permits as an + alternative to the Section 22.2.2.4 equivalent + rectangular stress block simplification. + """ + return { + 'fc': self.fcd(), + 'eps_c': 0.002, + 'eps_cu': self.eps_cu, + } diff --git a/structuralcodes/materials/reinforcement/__init__.py b/structuralcodes/materials/reinforcement/__init__.py index 291346f3..6e3d98b7 100644 --- a/structuralcodes/materials/reinforcement/__init__.py +++ b/structuralcodes/materials/reinforcement/__init__.py @@ -5,6 +5,7 @@ from structuralcodes.codes import _use_design_code from ._reinforcement import Reinforcement +from ._reinforcementACI318 import ReinforcementACI318 from ._reinforcementEC2_2004 import ReinforcementEC2_2004 from ._reinforcementEC2_2023 import ReinforcementEC2_2023 from ._reinforcementMC2010 import ReinforcementMC2010 @@ -12,12 +13,14 @@ __all__ = [ 'create_reinforcement', 'Reinforcement', + 'ReinforcementACI318', 'ReinforcementMC2010', 'ReinforcementEC2_2004', 'ReinforcementEC2_2023', ] REINFORCEMENTS: t.Dict[str, Reinforcement] = { + 'ACI 318-19': ReinforcementACI318, 'fib Model Code 2010': ReinforcementMC2010, 'EUROCODE 2 1992-1-1:2004': ReinforcementEC2_2004, 'EUROCODE 2 1992-1-1:2023': ReinforcementEC2_2023, diff --git a/structuralcodes/materials/reinforcement/_reinforcementACI318.py b/structuralcodes/materials/reinforcement/_reinforcementACI318.py new file mode 100644 index 00000000..ce5a7bfa --- /dev/null +++ b/structuralcodes/materials/reinforcement/_reinforcementACI318.py @@ -0,0 +1,164 @@ +"""The reinforcement class for ACI 318-19 Reinforcement Material.""" + +import typing as t + +from structuralcodes.codes import aci318 + +from ..constitutive_laws import ConstitutiveLaw, create_constitutive_law +from ._reinforcement import Reinforcement + + +class ReinforcementACI318(Reinforcement): # noqa: N801 + """Reinforcement implementation for ACI 318-19. + + Usage philosophy (LRFD vs. partial factor method): + ACI 318 uses Load and Resistance Factor Design (LRFD), which + differs from the Eurocode / fib Model Code partial factor + method used elsewhere in this library. Material strengths + are not reduced at the material level: gamma_s defaults to + 1.0 and should be left at 1.0 for standard ACI 318 design. + + Strength reduction factors (phi, per ACI 318-19 Section + 21.2) must be applied by the user at the member capacity + level. This class does not apply phi for you. For example, + when computing the nominal moment capacity Mn of a + reinforced section, use this reinforcement with its + 'elasticplastic' or 'elasticperfectlyplastic' constitutive + law and unreduced fy, then multiply the resulting Mn by the + appropriate phi factor (e.g. 0.90 for tension-controlled + flexure) to obtain phi*Mn. + + As with the Eurocode materials, the user chooses the + constitutive law appropriate to the limit state. For + ultimate limit state section analysis paired with + ConcreteACI318 using the 'parabolarectangle' law, pair this + reinforcement with 'elasticplastic' (the default) or + 'elasticperfectlyplastic'. + """ + + def __init__( + self, + fyk: float, + Es: float, + ftk: float, + epsuk: float, + gamma_s: t.Optional[float] = None, + name: t.Optional[str] = None, + density: float = 7850.0, + constitutive_law: t.Optional[ + t.Union[ + t.Literal[ + 'elastic', + 'elasticperfectlyplastic', + 'elasticplastic', + ], + ConstitutiveLaw, + ] + ] = 'elasticplastic', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, + ): + """Initializes a new instance of Reinforcement for ACI 318-19. + + Arguments: + fyk (float): Specified yield strength (fy) in MPa. + Es (float): The Young's modulus in MPa. + ftk (float): Specified ultimate strength (fu) in MPa. + epsuk (float): The strain at ultimate stress level. + + Keyword Arguments: + gamma_s (Optional(float)): The partial factor for + reinforcement. Default is 1.0 (ACI does not use + material partial factors). + name (str): A descriptive name for the reinforcement. + density (float): Density in kg/m3 (default: 7850). + constitutive_law (ConstitutiveLaw | str): A valid + ConstitutiveLaw or string. Valid options: 'elastic', + 'elasticplastic', 'elasticperfectlyplastic'. + initial_strain (Optional[float]): Initial strain. + initial_stress (Optional[float]): Initial stress. + strain_compatibility (Optional[bool]): If True, the + material deforms with the geometry. + + Raises: + ValueError: If the constitutive law is not valid for + reinforcement. + """ + if name is None: + name = f'Reinforcement{round(fyk):d}' + + super().__init__( + fyk=fyk, + Es=Es, + name=name, + density=density, + ftk=ftk, + epsuk=epsuk, + gamma_s=gamma_s, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + ) + self._constitutive_law = ( + constitutive_law + if isinstance(constitutive_law, ConstitutiveLaw) + else create_constitutive_law( + constitutive_law_name=constitutive_law, material=self + ) + ) + if 'steel' not in self._constitutive_law.__materials__: + raise ValueError( + 'The provided constitutive law is not valid for reinforcement.' + ) + self._apply_initial_strain() + + def fyd(self) -> float: + """The design yield strength. + + Note: + ACI 318 does not reduce material strength. Returns + fy / gamma_s, which with default gamma_s=1.0 gives + the unreduced yield strength. + """ + return aci318.fy_design(self.fyk, phi=1.0 / self.gamma_s) + + @property + def gamma_s(self) -> float: + """The partial factor for reinforcement. + + Note: + Default is 1.0 for ACI 318 (no material partial + factor). + """ + return self._gamma_s or 1.0 + + def ftd(self) -> float: + """The design ultimate strength.""" + return self.ftk / self.gamma_s + + def epsud(self) -> float: + """The design ultimate strain.""" + return self.epsuk + + def __elastic__(self) -> dict: + """Returns kwargs for an elastic constitutive law.""" + return {'E': self.Es} + + def __elasticperfectlyplastic__(self) -> dict: + """Returns kwargs for ElasticPlastic law with no hardening.""" + return { + 'E': self.Es, + 'fy': self.fyd(), + 'eps_su': self.epsud(), + } + + def __elasticplastic__(self) -> dict: + """Returns kwargs for ElasticPlastic law with hardening.""" + Eh = (self.ftd() - self.fyd()) / (self.epsud() - self.epsyd) + return { + 'E': self.Es, + 'fy': self.fyd(), + 'Eh': Eh, + 'eps_su': self.epsud(), + } diff --git a/tests/test_aci318/__init__.py b/tests/test_aci318/__init__.py new file mode 100644 index 00000000..7c8557a4 --- /dev/null +++ b/tests/test_aci318/__init__.py @@ -0,0 +1 @@ +"""Collection of tests for ACI 318-19.""" diff --git a/tests/test_aci318/test_aci318_concrete_material_properties.py b/tests/test_aci318/test_aci318_concrete_material_properties.py new file mode 100644 index 00000000..32714c5f --- /dev/null +++ b/tests/test_aci318/test_aci318_concrete_material_properties.py @@ -0,0 +1,149 @@ +"""Tests for concrete material properties of ACI 318-19.""" + +import math + +import pytest + +from structuralcodes.codes.aci318 import _concrete_material_properties + +WC_DEFAULT = 2320.0 + + +@pytest.mark.parametrize( + 'fc, expected', + [ + (21, WC_DEFAULT**1.5 * 0.043 * math.sqrt(21)), + (28, WC_DEFAULT**1.5 * 0.043 * math.sqrt(28)), + (35, WC_DEFAULT**1.5 * 0.043 * math.sqrt(35)), + (42, WC_DEFAULT**1.5 * 0.043 * math.sqrt(42)), + (55, WC_DEFAULT**1.5 * 0.043 * math.sqrt(55)), + ], +) +def test_Ec_normalweight(fc, expected): + """Test Ec for normalweight concrete (wc=2320 kg/m3).""" + assert math.isclose(_concrete_material_properties.Ec(fc), expected) + + +def test_Ec_custom_wc(): + """Test Ec with a custom unit weight.""" + fc = 28 + wc = 1800.0 + expected = wc**1.5 * 0.043 * math.sqrt(fc) + assert math.isclose(_concrete_material_properties.Ec(fc, wc=wc), expected) + + +def test_Ec_invalid_fc(): + """Test Ec raises for non-positive fc.""" + with pytest.raises(ValueError): + _concrete_material_properties.Ec(-1) + + +def test_Ec_invalid_wc(): + """Test Ec raises for wc outside valid range.""" + with pytest.raises(ValueError): + _concrete_material_properties.Ec(28, wc=1000) + + +@pytest.mark.parametrize( + 'fc, expected', + [ + (21, 0.62 * math.sqrt(21)), + (28, 0.62 * math.sqrt(28)), + (35, 0.62 * math.sqrt(35)), + (55, 0.62 * math.sqrt(55)), + ], +) +def test_fr(fc, expected): + """Test modulus of rupture.""" + assert math.isclose(_concrete_material_properties.fr(fc), expected) + + +def test_fr_lightweight(): + """Test modulus of rupture with lightweight factor.""" + fc = 28 + lambda_s = 0.75 + expected = 0.62 * lambda_s * math.sqrt(fc) + assert math.isclose( + _concrete_material_properties.fr(fc, lambda_s=lambda_s), + expected, + ) + + +def test_fr_invalid_fc(): + """Test fr raises for non-positive fc.""" + with pytest.raises(ValueError): + _concrete_material_properties.fr(-1) + + +def test_fr_invalid_lambda(): + """Test fr raises for invalid lambda_s.""" + with pytest.raises(ValueError): + _concrete_material_properties.fr(28, lambda_s=1.5) + + +@pytest.mark.parametrize( + 'fc, expected', + [ + (21, 0.85), + (28, 0.85), + (35, 0.80), + (41.5, 0.85 - 0.05 * (41.5 - 28) / 7), + (55, 0.65), + (69, 0.65), + ], +) +def test_beta1(fc, expected): + """Test Whitney stress block depth factor.""" + assert math.isclose( + _concrete_material_properties.beta1(fc), expected, rel_tol=1e-6 + ) + + +def test_beta1_invalid_fc(): + """Test beta1 raises for non-positive fc.""" + with pytest.raises(ValueError): + _concrete_material_properties.beta1(-1) + + +def test_eps_cu(): + """Test ultimate concrete strain.""" + assert _concrete_material_properties.eps_cu() == 0.003 + + +@pytest.mark.parametrize( + 'concrete_type, expected', + [ + ('normalweight', 1.0), + ('sand-lightweight', 0.85), + ('all-lightweight', 0.75), + ], +) +def test_lambda_factor(concrete_type, expected): + """Test lightweight modification factor.""" + assert ( + _concrete_material_properties.lambda_factor(concrete_type) == expected + ) + + +def test_lambda_factor_invalid(): + """Test lambda_factor raises for unknown type.""" + with pytest.raises(ValueError): + _concrete_material_properties.lambda_factor('unknown') + + +@pytest.mark.parametrize( + 'fc, expected', + [ + (21, 0.56 * math.sqrt(21)), + (28, 0.56 * math.sqrt(28)), + (35, 0.56 * math.sqrt(35)), + ], +) +def test_fct(fc, expected): + """Test splitting tensile strength.""" + assert math.isclose(_concrete_material_properties.fct(fc), expected) + + +def test_alpha1(): + """Test stress block intensity factor.""" + assert _concrete_material_properties.alpha1() == 0.85 diff --git a/tests/test_aci318/test_aci318_reinforcement_material_properties.py b/tests/test_aci318/test_aci318_reinforcement_material_properties.py new file mode 100644 index 00000000..bf23f1d3 --- /dev/null +++ b/tests/test_aci318/test_aci318_reinforcement_material_properties.py @@ -0,0 +1,88 @@ +"""Tests for reinforcement material properties of ACI 318-19.""" + +import math + +import pytest + +from structuralcodes.codes.aci318 import _reinforcement_material_properties + + +def test_Es(): + """Test modulus of elasticity of reinforcement.""" + assert _reinforcement_material_properties.Es() == 200000.0 + + +@pytest.mark.parametrize( + 'fy, expected', + [ + (280, 280), + (420, 420), + (550, 550), + (690, 690), + ], +) +def test_fy_design_default(fy, expected): + """Test design yield strength with default phi=1.0.""" + assert math.isclose( + _reinforcement_material_properties.fy_design(fy), expected + ) + + +def test_fy_design_with_phi(): + """Test design yield strength with explicit phi.""" + assert math.isclose( + _reinforcement_material_properties.fy_design(420, phi=0.9), 378 + ) + + +def test_fy_design_invalid_fy(): + """Test fy_design raises for non-positive fy.""" + with pytest.raises(ValueError): + _reinforcement_material_properties.fy_design(-1) + + +def test_fy_design_invalid_phi(): + """Test fy_design raises for phi outside (0, 1].""" + with pytest.raises(ValueError): + _reinforcement_material_properties.fy_design(420, phi=1.5) + + +@pytest.mark.parametrize( + 'fy, expected', + [ + (420, 420 / 200000), + (280, 280 / 200000), + (550, 550 / 200000), + ], +) +def test_epsyd(fy, expected): + """Test yield strain.""" + assert math.isclose(_reinforcement_material_properties.epsyd(fy), expected) + + +def test_epsyd_invalid_fy(): + """Test epsyd raises for non-positive fy.""" + with pytest.raises(ValueError): + _reinforcement_material_properties.epsyd(-1) + + +@pytest.mark.parametrize( + 'grade, exp_fy, exp_fu', + [ + ('40', 280, 420), + ('60', 420, 550), + ('80', 550, 690), + ('100', 690, 860), + ], +) +def test_reinforcement_grade_props(grade, exp_fy, exp_fu): + """Test reinforcement grade property lookup.""" + props = _reinforcement_material_properties.reinforcement_grade_props(grade) + assert math.isclose(props['fy'], exp_fy) + assert math.isclose(props['fu'], exp_fu) + + +def test_reinforcement_grade_props_invalid(): + """Test grade lookup raises for unknown grade.""" + with pytest.raises(ValueError): + _reinforcement_material_properties.reinforcement_grade_props('999') diff --git a/tests/test_aci318/test_concrete_aci318.py b/tests/test_aci318/test_concrete_aci318.py new file mode 100644 index 00000000..d64cc68a --- /dev/null +++ b/tests/test_aci318/test_concrete_aci318.py @@ -0,0 +1,119 @@ +"""Tests for the ConcreteACI318 material class.""" + +import math + +import pytest + +import structuralcodes +from structuralcodes.materials.concrete import ( + ConcreteACI318, + create_concrete, +) + + +@pytest.fixture(autouse=True) +def _reset_design_code(): + """Reset the global design code after each test.""" + yield + structuralcodes.set_design_code(None) + + +def test_create_via_factory(): + """Test creating concrete via the factory function.""" + c = create_concrete(fck=28, design_code='aci318') + assert isinstance(c, ConcreteACI318) + + +def test_default_name(): + """Test default name generation.""" + structuralcodes.set_design_code('aci318') + c = create_concrete(fck=28) + assert c.name == 'C28' + + +def test_fc_property(): + """Test the fc alias for fck.""" + c = ConcreteACI318(fck=28) + assert c.fc == 28 + assert c.fck == 28 + + +def test_gamma_c_default(): + """Test default gamma_c is 1.0 for ACI.""" + c = ConcreteACI318(fck=28) + assert c.gamma_c == 1.0 + + +def test_Ec_property(): + """Test Ec is derived from fc.""" + c = ConcreteACI318(fck=28) + expected = 2320**1.5 * 0.043 * math.sqrt(28) + assert math.isclose(c.Ec, expected, rel_tol=5e-3) + + +def test_Ec_specified(): + """Test Ec can be manually overridden.""" + c = ConcreteACI318(fck=28, Ec=30000) + assert c.Ec == 30000 + + +def test_fr_property(): + """Test fr is derived from fc.""" + c = ConcreteACI318(fck=28) + expected = 0.62 * math.sqrt(28) + assert math.isclose(c.fr, expected) + + +def test_fr_specified(): + """Test fr can be manually overridden.""" + c = ConcreteACI318(fck=28, fr=5.0) + assert c.fr == 5.0 + + +def test_beta1_property(): + """Test beta1 is derived from fc.""" + c = ConcreteACI318(fck=28) + assert c.beta1 == 0.85 + + +def test_eps_cu_property(): + """Test eps_cu returns 0.003.""" + c = ConcreteACI318(fck=28) + assert c.eps_cu == 0.003 + + +def test_alpha1_property(): + """Test alpha1 returns 0.85.""" + c = ConcreteACI318(fck=28) + assert c.alpha1 == 0.85 + + +def test_fcd(): + """Test fcd = alpha1 * fc / gamma_c.""" + c = ConcreteACI318(fck=28) + assert math.isclose(c.fcd(), 0.85 * 28) + + +def test_fct_property(): + """Test fct is derived from fc.""" + c = ConcreteACI318(fck=28) + expected = 0.56 * math.sqrt(28) + assert math.isclose(c.fct, expected) + + +def test_constitutive_law_elastic(): + """Test elastic constitutive law creation.""" + c = ConcreteACI318(fck=28, constitutive_law='elastic') + assert c.constitutive_law is not None + + +def test_constitutive_law_parabolarectangle(): + """Test parabola-rectangle constitutive law creation.""" + c = ConcreteACI318(fck=28, constitutive_law='parabolarectangle') + assert c.constitutive_law is not None + + +def test_constitutive_law_bilinear(): + """Test bilinear compression constitutive law creation.""" + c = ConcreteACI318(fck=28, constitutive_law='bilinearcompression') + assert c.constitutive_law is not None diff --git a/tests/test_aci318/test_reinforcement_aci318.py b/tests/test_aci318/test_reinforcement_aci318.py new file mode 100644 index 00000000..074d10d2 --- /dev/null +++ b/tests/test_aci318/test_reinforcement_aci318.py @@ -0,0 +1,121 @@ +"""Tests for the ReinforcementACI318 material class.""" + +import math + +import pytest + +import structuralcodes +from structuralcodes.materials.reinforcement import ( + ReinforcementACI318, + create_reinforcement, +) + + +@pytest.fixture(autouse=True) +def _reset_design_code(): + """Reset the global design code after each test.""" + yield + structuralcodes.set_design_code(None) + + +def test_create_via_factory(): + """Test creating reinforcement via the factory function.""" + r = create_reinforcement( + fyk=420, + Es=200000, + ftk=550, + epsuk=0.05, + design_code='aci318', + ) + assert isinstance(r, ReinforcementACI318) + + +def test_default_name(): + """Test default name generation.""" + r = ReinforcementACI318( + fyk=420, + Es=200000, + ftk=550, + epsuk=0.05, + ) + assert r.name == 'Reinforcement420' + + +def test_gamma_s_default(): + """Test default gamma_s is 1.0 for ACI.""" + r = ReinforcementACI318( + fyk=420, + Es=200000, + ftk=550, + epsuk=0.05, + ) + assert r.gamma_s == 1.0 + + +def test_fyd(): + """Test fyd returns unreduced fy (gamma_s=1.0).""" + r = ReinforcementACI318( + fyk=420, + Es=200000, + ftk=550, + epsuk=0.05, + ) + assert math.isclose(r.fyd(), 420) + + +def test_ftd(): + """Test ftd returns unreduced ftk (gamma_s=1.0).""" + r = ReinforcementACI318( + fyk=420, + Es=200000, + ftk=550, + epsuk=0.05, + ) + assert math.isclose(r.ftd(), 550) + + +def test_epsud(): + """Test epsud returns epsuk (no reduction for ACI).""" + r = ReinforcementACI318( + fyk=420, + Es=200000, + ftk=550, + epsuk=0.05, + ) + assert r.epsud() == 0.05 + + +def test_constitutive_law_elastic(): + """Test elastic constitutive law creation.""" + r = ReinforcementACI318( + fyk=420, + Es=200000, + ftk=550, + epsuk=0.05, + constitutive_law='elastic', + ) + assert r.constitutive_law is not None + + +def test_constitutive_law_elasticplastic(): + """Test elastic-plastic constitutive law creation.""" + r = ReinforcementACI318( + fyk=420, + Es=200000, + ftk=550, + epsuk=0.05, + constitutive_law='elasticplastic', + ) + assert r.constitutive_law is not None + + +def test_constitutive_law_perfectly_plastic(): + """Test elastic perfectly plastic constitutive law.""" + r = ReinforcementACI318( + fyk=420, + Es=200000, + ftk=550, + epsuk=0.05, + constitutive_law='elasticperfectlyplastic', + ) + assert r.constitutive_law is not None From abb8aa5f7d54385e0547569a743df6306cfbfae4 Mon Sep 17 00:00:00 2001 From: gabe-kafka <225996232+gabe-kafka@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:03:57 -0400 Subject: [PATCH 2/2] Address ACI 318-19 material review feedback --- structuralcodes/codes/__init__.py | 6 +- structuralcodes/codes/aci318/__init__.py | 37 -- structuralcodes/codes/aci318_19/__init__.py | 97 ++++ .../_concrete_material_properties.py | 129 +++-- .../_reinforcement_material_properties.py | 26 +- .../codes/aci318_19/_unit_conversions.py | 118 +++++ .../materials/concrete/__init__.py | 6 +- .../materials/concrete/_concreteACI318.py | 289 ------------ .../materials/concrete/_concreteACI318_19.py | 444 ++++++++++++++++++ .../materials/reinforcement/__init__.py | 6 +- .../reinforcement/_reinforcementACI318.py | 164 ------- .../reinforcement/_reinforcementACI318_19.py | 328 +++++++++++++ tests/test_aci318/test_concrete_aci318.py | 119 ----- .../__init__.py | 0 ...aci318_19_concrete_material_properties.py} | 77 +-- ...8_19_reinforcement_material_properties.py} | 17 +- .../test_aci318_19/test_concrete_aci318_19.py | 203 ++++++++ .../test_reinforcement_aci318_19.py} | 62 ++- 18 files changed, 1370 insertions(+), 758 deletions(-) delete mode 100644 structuralcodes/codes/aci318/__init__.py create mode 100644 structuralcodes/codes/aci318_19/__init__.py rename structuralcodes/codes/{aci318 => aci318_19}/_concrete_material_properties.py (57%) rename structuralcodes/codes/{aci318 => aci318_19}/_reinforcement_material_properties.py (78%) create mode 100644 structuralcodes/codes/aci318_19/_unit_conversions.py delete mode 100644 structuralcodes/materials/concrete/_concreteACI318.py create mode 100644 structuralcodes/materials/concrete/_concreteACI318_19.py delete mode 100644 structuralcodes/materials/reinforcement/_reinforcementACI318.py create mode 100644 structuralcodes/materials/reinforcement/_reinforcementACI318_19.py delete mode 100644 tests/test_aci318/test_concrete_aci318.py rename tests/{test_aci318 => test_aci318_19}/__init__.py (100%) rename tests/{test_aci318/test_aci318_concrete_material_properties.py => test_aci318_19/test_aci318_19_concrete_material_properties.py} (56%) rename tests/{test_aci318/test_aci318_reinforcement_material_properties.py => test_aci318_19/test_aci318_19_reinforcement_material_properties.py} (77%) create mode 100644 tests/test_aci318_19/test_concrete_aci318_19.py rename tests/{test_aci318/test_reinforcement_aci318.py => test_aci318_19/test_reinforcement_aci318_19.py} (58%) diff --git a/structuralcodes/codes/__init__.py b/structuralcodes/codes/__init__.py index b6e7cc6d..d7ade815 100644 --- a/structuralcodes/codes/__init__.py +++ b/structuralcodes/codes/__init__.py @@ -3,10 +3,10 @@ import types import typing as t -from . import aci318, ec2_2004, ec2_2023, mc2010, mc2020 +from . import aci318_19, ec2_2004, ec2_2023, mc2010, mc2020 __all__ = [ - 'aci318', + 'aci318_19', 'mc2010', 'mc2020', 'ec2_2023', @@ -24,7 +24,7 @@ # Design code registry _DESIGN_CODES = { - 'aci318': aci318, + 'aci318_19': aci318_19, 'mc2010': mc2010, 'mc2020': mc2020, 'ec2_2004': ec2_2004, diff --git a/structuralcodes/codes/aci318/__init__.py b/structuralcodes/codes/aci318/__init__.py deleted file mode 100644 index 0c45a3a0..00000000 --- a/structuralcodes/codes/aci318/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -"""ACI 318-19.""" - -import typing as t - -from ._concrete_material_properties import ( - Ec, - alpha1, - beta1, - eps_cu, - fct, - fr, - lambda_factor, -) -from ._reinforcement_material_properties import ( - Es, - epsyd, - fy_design, - reinforcement_grade_props, -) - -__all__ = [ - 'Ec', - 'Es', - 'alpha1', - 'beta1', - 'eps_cu', - 'epsyd', - 'fct', - 'fr', - 'fy_design', - 'lambda_factor', - 'reinforcement_grade_props', -] - -__title__: str = 'ACI 318-19' -__year__: str = '2019' -__materials__: t.Tuple[str] = ('concrete', 'reinforcement') diff --git a/structuralcodes/codes/aci318_19/__init__.py b/structuralcodes/codes/aci318_19/__init__.py new file mode 100644 index 00000000..d389f241 --- /dev/null +++ b/structuralcodes/codes/aci318_19/__init__.py @@ -0,0 +1,97 @@ +"""ACI 318-19.""" + +import typing as t + +from ._concrete_material_properties import ( + MINIMUM_STRESS_BLOCK_FC, + Ec, + alpha1, + beta1, + eps_c0, + eps_cu, + fr, + lambda_factor, +) +from ._reinforcement_material_properties import ( + Es, + epsyd, + fy_design, + reinforcement_grade_props, +) +from ._unit_conversions import ( + INCH_TO_MM, + KIP_TO_N, + KSI_TO_MPA, + LB_FORCE_TO_N, + LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER, + PSI_TO_MPA, + in2_to_mm2, + in3_to_mm3, + in4_to_mm4, + in_to_mm, + kg_per_m3_to_pcf, + kip_in_to_nmm, + kip_to_n, + ksi_to_mpa, + lb_in_to_nmm, + lb_to_n, + mm2_to_in2, + mm3_to_in3, + mm4_to_in4, + mm_to_in, + mpa_to_ksi, + mpa_to_psi, + n_to_kip, + n_to_lb, + nmm_to_kip_in, + nmm_to_lb_in, + pcf_to_kg_per_m3, + psi_to_mpa, +) + +__all__ = [ + 'Ec', + 'Es', + 'INCH_TO_MM', + 'KIP_TO_N', + 'KSI_TO_MPA', + 'LB_FORCE_TO_N', + 'LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER', + 'MINIMUM_STRESS_BLOCK_FC', + 'PSI_TO_MPA', + 'alpha1', + 'beta1', + 'eps_c0', + 'eps_cu', + 'epsyd', + 'fr', + 'fy_design', + 'in2_to_mm2', + 'in3_to_mm3', + 'in4_to_mm4', + 'in_to_mm', + 'kg_per_m3_to_pcf', + 'kip_in_to_nmm', + 'kip_to_n', + 'ksi_to_mpa', + 'lambda_factor', + 'lb_in_to_nmm', + 'lb_to_n', + 'mm2_to_in2', + 'mm3_to_in3', + 'mm4_to_in4', + 'mm_to_in', + 'mpa_to_ksi', + 'mpa_to_psi', + 'n_to_kip', + 'n_to_lb', + 'nmm_to_kip_in', + 'nmm_to_lb_in', + 'pcf_to_kg_per_m3', + 'psi_to_mpa', + 'reinforcement_grade_props', +] + +__title__: str = 'ACI 318-19' +__year__: str = '2019' +__materials__: t.Tuple[str] = ('concrete', 'reinforcement') diff --git a/structuralcodes/codes/aci318/_concrete_material_properties.py b/structuralcodes/codes/aci318_19/_concrete_material_properties.py similarity index 57% rename from structuralcodes/codes/aci318/_concrete_material_properties.py rename to structuralcodes/codes/aci318_19/_concrete_material_properties.py index c5bdff37..644c1afd 100644 --- a/structuralcodes/codes/aci318/_concrete_material_properties.py +++ b/structuralcodes/codes/aci318_19/_concrete_material_properties.py @@ -5,14 +5,14 @@ import math import typing as t -LAMBDA_FACTORS = { - 'normalweight': 1.0, - 'sand-lightweight': 0.85, - 'all-lightweight': 0.75, -} +from ._unit_conversions import LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER +LIGHTWEIGHT_LAMBDA_LIMIT = 100.0 * LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER +NORMALWEIGHT_LAMBDA_LIMIT = 135.0 * LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER +MINIMUM_STRESS_BLOCK_FC = 17.0 -def Ec(fc: float, wc: float = 2320.0) -> float: + +def Ec(fc: float, wc: t.Optional[float] = None) -> float: """The modulus of elasticity of concrete. ACI 318-19, Table 19.2.2.1. @@ -22,18 +22,26 @@ def Ec(fc: float, wc: float = 2320.0) -> float: MPa. Keyword Args: - wc (float): The unit weight of concrete in kg/m3. - Default is 2320 kg/m3 (normalweight concrete). + wc (float, optional): The equilibrium density of concrete in + kg/m3. If omitted, the normalweight expression + ``4700 * sqrt(fc)`` is used. Returns: float: The modulus of elasticity in MPa. Raises: ValueError: If fc is not positive. - ValueError: If wc is outside the range 1440-2560 kg/m3. + ValueError: If wc is given outside the range 1440-2560 kg/m3. + + Note: + wc is the ACI equilibrium density for Eq. 19.2.2.1, not the + base material density stored on a concrete object. Omitting wc + selects the ACI normalweight expression. """ if fc <= 0: raise ValueError(f'fc={fc} must be positive') + if wc is None: + return 4700.0 * math.sqrt(fc) if wc < 1440 or wc > 2560: raise ValueError(f'wc={wc} must be between 1440 and 2560 kg/m3') return wc**1.5 * 0.043 * math.sqrt(fc) @@ -66,28 +74,27 @@ def fr(fc: float, lambda_s: float = 1.0) -> float: return 0.62 * lambda_s * math.sqrt(fc) -def beta1(fc: float) -> float: - """The Whitney stress block depth factor. +def lambda_factor(wc: float) -> float: + """The modification factor for lightweight concrete. - ACI 318-19, Table 22.2.2.4.3. + ACI 318-19, Table 19.2.4.1(a). Args: - fc (float): The specified compressive strength of concrete in - MPa. + wc (float): The equilibrium density of concrete in kg/m3. Returns: - float: The stress block depth factor (dimensionless). + float: The lightweight modification factor (dimensionless). Raises: - ValueError: If fc is not positive. + ValueError: If wc is not positive. """ - if fc <= 0: - raise ValueError(f'fc={fc} must be positive') - if fc <= 28: - return 0.85 - if fc >= 55: - return 0.65 - return 0.85 - 0.05 * (fc - 28) / 7 + if wc <= 0: + raise ValueError(f'wc={wc} must be positive') + if wc <= LIGHTWEIGHT_LAMBDA_LIMIT: + return 0.75 + if wc >= NORMALWEIGHT_LAMBDA_LIMIT: + return 1.0 + return min(1.0, 0.0075 * wc / LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER) def eps_cu() -> float: @@ -101,64 +108,52 @@ def eps_cu() -> float: return 0.003 -def lambda_factor( - concrete_type: t.Literal[ - 'normalweight', 'sand-lightweight', 'all-lightweight' - ], -) -> float: - """The modification factor for lightweight concrete. +def eps_c0() -> float: + """The default strain at peak concrete compression. - ACI 318-19, Table 19.2.4.2. + Returns: + float: The peak concrete strain (dimensionless). - Args: - concrete_type (str): The concrete type. One of - 'normalweight', 'sand-lightweight', or 'all-lightweight'. + Note: + ACI 318-19 does not prescribe a complete concrete stress-strain + curve. The value 0.002 is the library default peak-strain + parameter for the ACI parabolic and bilinear constitutive laws. + """ + return 0.002 - Returns: - float: The lightweight modification factor (dimensionless). - Raises: - ValueError: If concrete_type is not recognized. +def alpha1() -> float: + """The ratio of equivalent rectangular stress block intensity. + + ACI 318-19, Section 22.2.2.4.1. + + Returns: + float: The stress block intensity factor (dimensionless). """ - result = LAMBDA_FACTORS.get(concrete_type.lower()) - if result is None: - raise ValueError( - f'Unknown concrete type: {concrete_type}. ' - f'Valid types: {list(LAMBDA_FACTORS.keys())}' - ) - return result + return 0.85 -def fct(fc: float, lambda_s: float = 1.0) -> float: - """The approximate splitting tensile strength of concrete. +def beta1(fc: float) -> float: + """The Whitney stress block depth factor. - ACI 318-19, Section 19.2.4.3. + ACI 318-19, Table 22.2.2.4.3. Args: fc (float): The specified compressive strength of concrete in MPa. - Keyword Args: - lambda_s (float): The modification factor for lightweight - concrete. Default is 1.0 (normalweight). - Returns: - float: The splitting tensile strength in MPa. + float: The stress block depth factor (dimensionless). Raises: - ValueError: If fc is not positive. + ValueError: If fc is below 17 MPa. """ - if fc <= 0: - raise ValueError(f'fc={fc} must be positive') - return 0.56 * lambda_s * math.sqrt(fc) - - -def alpha1() -> float: - """The ratio of equivalent rectangular stress block intensity. - - ACI 318-19, Section 22.2.2.4.1. - - Returns: - float: The stress block intensity factor (dimensionless). - """ - return 0.85 + if fc < MINIMUM_STRESS_BLOCK_FC: + raise ValueError( + f'fc={fc} must be at least {MINIMUM_STRESS_BLOCK_FC} MPa' + ) + if fc <= 28: + return 0.85 + if fc <= 55: + return 0.85 - 0.20 / 27 * (fc - 28) + return 0.65 diff --git a/structuralcodes/codes/aci318/_reinforcement_material_properties.py b/structuralcodes/codes/aci318_19/_reinforcement_material_properties.py similarity index 78% rename from structuralcodes/codes/aci318/_reinforcement_material_properties.py rename to structuralcodes/codes/aci318_19/_reinforcement_material_properties.py index 03aa4969..339e036b 100644 --- a/structuralcodes/codes/aci318/_reinforcement_material_properties.py +++ b/structuralcodes/codes/aci318_19/_reinforcement_material_properties.py @@ -4,6 +4,8 @@ import typing as t +# ACI 318-19, Table 20.2.1.3(a), SI equivalents for ASTM A615 +# reinforcement grades recognized by the code. REINFORCEMENT_GRADES = { '40': {'fy': 280.0, 'fu': 420.0}, '60': {'fy': 420.0, 'fu': 550.0}, @@ -23,33 +25,25 @@ def Es() -> float: return 200000.0 -def fy_design(fy: float, phi: float = 1.0) -> float: +def fy_design(fy: float) -> float: """The design yield strength of reinforcement. ACI 318-19 applies strength reduction factors (phi) at the - member capacity level, not the material level. The default - phi=1.0 returns the unreduced yield strength, which is the - standard ACI convention for material properties. + member capacity level, not the material level. This function + therefore returns the unreduced yield strength. Args: fy (float): The specified yield strength in MPa. - Keyword Args: - phi (float): Optional strength reduction factor. - Default is 1.0 (no reduction). - Returns: float: The design yield strength in MPa. Raises: ValueError: If fy is not positive. - ValueError: If phi is not in (0, 1]. """ if fy <= 0: raise ValueError(f'fy={fy} must be positive') - if phi <= 0 or phi > 1.0: - raise ValueError(f'phi={phi} must be in the range (0, 1]') - return phi * fy + return fy def epsyd(fy: float, _Es: float = 200000.0) -> float: @@ -78,7 +72,7 @@ def reinforcement_grade_props( ) -> t.Dict[str, float]: """Return the minimum specified properties for a reinforcement grade. - ACI 318-19, Table 20.2.2.4a (SI equivalents). + ACI 318-19, Table 20.2.1.3(a) (SI equivalents). Args: grade (str): The ASTM reinforcement grade designation. @@ -88,6 +82,12 @@ def reinforcement_grade_props( Dict[str, float]: A dict with keys 'fy' (yield strength in MPa) and 'fu' (ultimate strength in MPa). + Note: + ACI 318-19 Table 20.2.1.3(a) does not provide a single + grade-level ultimate strain. Applicable elongation requirements + depend on the reinforcement specification and bar size, so they + are not returned here. + Raises: ValueError: If the grade is not recognized. """ diff --git a/structuralcodes/codes/aci318_19/_unit_conversions.py b/structuralcodes/codes/aci318_19/_unit_conversions.py new file mode 100644 index 00000000..5f853e01 --- /dev/null +++ b/structuralcodes/codes/aci318_19/_unit_conversions.py @@ -0,0 +1,118 @@ +"""US customary and SI unit conversions for ACI 318-19 helpers.""" + +PSI_TO_MPA = 0.006894757293168361 +KSI_TO_MPA = 6.894757293168361 +INCH_TO_MM = 25.4 +LB_FORCE_TO_N = 4.4482216152605 +KIP_TO_N = 1000.0 * LB_FORCE_TO_N +LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER = 16.01846337396014 + + +def psi_to_mpa(psi: float) -> float: + """Convert stress from psi to MPa.""" + return psi * PSI_TO_MPA + + +def mpa_to_psi(mpa: float) -> float: + """Convert stress from MPa to psi.""" + return mpa / PSI_TO_MPA + + +def ksi_to_mpa(ksi: float) -> float: + """Convert stress from ksi to MPa.""" + return ksi * KSI_TO_MPA + + +def mpa_to_ksi(mpa: float) -> float: + """Convert stress from MPa to ksi.""" + return mpa / KSI_TO_MPA + + +def pcf_to_kg_per_m3(pcf: float) -> float: + """Convert density from lb/ft3 to kg/m3.""" + return pcf * LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER + + +def kg_per_m3_to_pcf(kg_per_m3: float) -> float: + """Convert density from kg/m3 to lb/ft3.""" + return kg_per_m3 / LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER + + +def in_to_mm(inches: float) -> float: + """Convert length from inches to mm.""" + return inches * INCH_TO_MM + + +def mm_to_in(mm: float) -> float: + """Convert length from mm to inches.""" + return mm / INCH_TO_MM + + +def in2_to_mm2(square_inches: float) -> float: + """Convert area from in2 to mm2.""" + return square_inches * INCH_TO_MM**2 + + +def mm2_to_in2(square_mm: float) -> float: + """Convert area from mm2 to in2.""" + return square_mm / INCH_TO_MM**2 + + +def in3_to_mm3(cubic_inches: float) -> float: + """Convert volume from in3 to mm3.""" + return cubic_inches * INCH_TO_MM**3 + + +def mm3_to_in3(cubic_mm: float) -> float: + """Convert volume from mm3 to in3.""" + return cubic_mm / INCH_TO_MM**3 + + +def in4_to_mm4(inches_fourth: float) -> float: + """Convert second moment of area from in4 to mm4.""" + return inches_fourth * INCH_TO_MM**4 + + +def mm4_to_in4(mm_fourth: float) -> float: + """Convert second moment of area from mm4 to in4.""" + return mm_fourth / INCH_TO_MM**4 + + +def lb_to_n(pounds: float) -> float: + """Convert force from lbf to N.""" + return pounds * LB_FORCE_TO_N + + +def n_to_lb(newtons: float) -> float: + """Convert force from N to lbf.""" + return newtons / LB_FORCE_TO_N + + +def kip_to_n(kips: float) -> float: + """Convert force from kip to N.""" + return kips * KIP_TO_N + + +def n_to_kip(newtons: float) -> float: + """Convert force from N to kip.""" + return newtons / KIP_TO_N + + +def lb_in_to_nmm(lb_in: float) -> float: + """Convert moment from lb-in to Nmm.""" + return lb_in * LB_FORCE_TO_N * INCH_TO_MM + + +def nmm_to_lb_in(nmm: float) -> float: + """Convert moment from Nmm to lb-in.""" + return nmm / (LB_FORCE_TO_N * INCH_TO_MM) + + +def kip_in_to_nmm(kip_in: float) -> float: + """Convert moment from kip-in to Nmm.""" + return kip_in * KIP_TO_N * INCH_TO_MM + + +def nmm_to_kip_in(nmm: float) -> float: + """Convert moment from Nmm to kip-in.""" + return nmm / (KIP_TO_N * INCH_TO_MM) diff --git a/structuralcodes/materials/concrete/__init__.py b/structuralcodes/materials/concrete/__init__.py index 9d403705..78f03ec7 100644 --- a/structuralcodes/materials/concrete/__init__.py +++ b/structuralcodes/materials/concrete/__init__.py @@ -5,7 +5,7 @@ from structuralcodes.codes import _use_design_code from ._concrete import Concrete -from ._concreteACI318 import ConcreteACI318 +from ._concreteACI318_19 import ConcreteACI318_19 from ._concreteEC2_2004 import ConcreteEC2_2004 from ._concreteEC2_2023 import ConcreteEC2_2023 from ._concreteMC2010 import ConcreteMC2010 @@ -13,14 +13,14 @@ __all__ = [ 'create_concrete', 'Concrete', - 'ConcreteACI318', + 'ConcreteACI318_19', 'ConcreteMC2010', 'ConcreteEC2_2023', 'ConcreteEC2_2004', ] CONCRETES: t.Dict[str, Concrete] = { - 'ACI 318-19': ConcreteACI318, + 'ACI 318-19': ConcreteACI318_19, 'fib Model Code 2010': ConcreteMC2010, 'EUROCODE 2 1992-1-1:2004': ConcreteEC2_2004, 'EUROCODE 2 1992-1-1:2023': ConcreteEC2_2023, diff --git a/structuralcodes/materials/concrete/_concreteACI318.py b/structuralcodes/materials/concrete/_concreteACI318.py deleted file mode 100644 index 9a8a60a3..00000000 --- a/structuralcodes/materials/concrete/_concreteACI318.py +++ /dev/null @@ -1,289 +0,0 @@ -"""The concrete class for ACI 318-19 Concrete Material.""" - -import typing as t - -from structuralcodes.codes import aci318 - -from ..constitutive_laws import ConstitutiveLaw, create_constitutive_law -from ._concrete import Concrete - - -class ConcreteACI318(Concrete): # noqa: N801 - """Concrete implementation for ACI 318-19. - - Note: - ACI 318 uses specified compressive strength (fc') rather than - characteristic strength (fck). The parameter is named fck for - compatibility with the base class, but represents fc' in ACI - notation. An fc property is provided as an alias. - - Usage philosophy (LRFD vs. partial factor method): - ACI 318 uses Load and Resistance Factor Design (LRFD), which - differs from the Eurocode / fib Model Code partial factor - method used elsewhere in this library. Two consequences for - users of this class: - - 1. Material strengths are not reduced at the material level. - gamma_c defaults to 1.0 and should be left at 1.0 for - standard ACI 318 design. Reducing fc' via gamma_c is not - ACI 318 compliant. - - 2. Strength reduction factors (phi, per ACI 318-19 Section - 21.2) must be applied by the user at the member capacity - level. This class does not apply phi for you. - - As with the Eurocode materials, the user is responsible for - choosing a constitutive law and parameters suited to the - limit state being analyzed: - - - Serviceability / cracked-stiffness estimates: use the - 'elastic' law with Ec, parameterized with mean (expected) - properties. - - Ultimate limit state (flexure, axial, biaxial interaction - domains): use 'parabolarectangle' (the default) or - 'bilinearcompression', which apply the ACI 318-19 Section - 22.2 stress block parameters (alpha1=0.85, eps_cu=0.003). - Compute nominal capacities Pn, Mn with this material, then - multiply by the appropriate phi factor at the member level - to obtain phi*Pn and phi*Mn. - """ - - _Ec: t.Optional[float] = None - _fr: t.Optional[float] = None - _fct: t.Optional[float] = None - _wc: float = 2320.0 - _lambda_s: float = 1.0 - - def __init__( - self, - fck: float, - name: t.Optional[str] = None, - density: float = 2320, - gamma_c: t.Optional[float] = None, - constitutive_law: t.Optional[ - t.Union[ - t.Literal[ - 'elastic', - 'parabolarectangle', - 'bilinearcompression', - ], - ConstitutiveLaw, - ] - ] = 'parabolarectangle', - initial_strain: t.Optional[float] = None, - initial_stress: t.Optional[float] = None, - strain_compatibility: t.Optional[bool] = None, - Ec: t.Optional[float] = None, - fr: t.Optional[float] = None, - fct: t.Optional[float] = None, - wc: float = 2320.0, - lambda_s: float = 1.0, - **kwargs, - ) -> None: - """Initializes a new instance of Concrete for ACI 318-19. - - Arguments: - fck (float): Specified compressive strength (fc') in MPa. - - Keyword Arguments: - name (str): A descriptive name for concrete. - density (float): Density of material in kg/m3 - (default: 2320). - gamma_c (float, optional): Partial factor for concrete. - Default is 1.0 (ACI does not use material partial - factors). - constitutive_law (ConstitutiveLaw | str): A valid - ConstitutiveLaw object or string. Valid options: - 'elastic', 'parabolarectangle', - 'bilinearcompression'. - initial_strain (Optional[float]): Initial strain. - initial_stress (Optional[float]): Initial stress. - strain_compatibility (Optional[bool]): If True, the - material deforms with the geometry. - Ec (float, optional): The modulus of elasticity in MPa. - fr (float, optional): The modulus of rupture in MPa. - fct (float, optional): The splitting tensile strength - in MPa. - wc (float): Unit weight of concrete in kg/m3 - (default: 2320). - lambda_s (float): Lightweight modification factor - (default: 1.0 for normalweight). - - Raises: - ValueError: If the constitutive law is not valid for - concrete. - """ - del kwargs - if name is None: - name = f'C{round(fck):d}' - super().__init__( - fck=fck, - name=name, - density=density, - existing=False, - gamma_c=gamma_c, - initial_strain=initial_strain, - initial_stress=initial_stress, - strain_compatibility=strain_compatibility, - ) - self._Ec = abs(Ec) if Ec is not None else None - self._fr = abs(fr) if fr is not None else None - self._fct = abs(fct) if fct is not None else None - self._wc = wc - self._lambda_s = lambda_s - - self._constitutive_law = ( - constitutive_law - if isinstance(constitutive_law, ConstitutiveLaw) - else create_constitutive_law( - constitutive_law_name=constitutive_law, material=self - ) - ) - if 'concrete' not in self._constitutive_law.__materials__: - raise ValueError( - 'The provided constitutive law is not valid for concrete.' - ) - self._apply_initial_strain() - - @property - def fc(self) -> float: - """Returns the specified compressive strength (fc') in MPa. - - Returns: - float: The specified compressive strength in MPa. - - Note: - This is an alias for fck, using ACI notation. - """ - return self._fck - - @property - def gamma_c(self) -> float: - """The partial factor for concrete. - - Returns: - float: The partial factor (default 1.0 for ACI 318). - - Note: - ACI 318 does not use material partial factors. The - default value of 1.0 maintains compatibility with the - base class interface. - """ - return self._gamma_c or 1.0 - - @property - def Ec(self) -> float: - """Returns the modulus of elasticity in MPa. - - Returns: - float: The modulus of elasticity in MPa. - - Note: - Derived from fc and wc if not manually provided. - """ - if self._Ec is None: - return aci318.Ec(self._fck, self._wc) - return self._Ec - - @property - def fr(self) -> float: - """Returns the modulus of rupture in MPa. - - Returns: - float: The modulus of rupture in MPa. - - Note: - Derived from fc and lambda_s if not manually provided. - """ - if self._fr is None: - return aci318.fr(self._fck, self._lambda_s) - return self._fr - - @property - def beta1(self) -> float: - """Returns the Whitney stress block depth factor. - - Returns: - float: The stress block depth factor (dimensionless). - """ - return aci318.beta1(self._fck) - - @property - def eps_cu(self) -> float: - """Returns the ultimate concrete strain. - - Returns: - float: The ultimate strain (dimensionless). - """ - return aci318.eps_cu() - - @property - def alpha1(self) -> float: - """Returns the stress block intensity factor. - - Returns: - float: The stress block intensity (dimensionless). - """ - return aci318.alpha1() - - @property - def fct(self) -> float: - """Returns the splitting tensile strength in MPa. - - Returns: - float: The splitting tensile strength in MPa. - - Note: - Derived from fc and lambda_s if not manually provided. - """ - if self._fct is None: - return aci318.fct(self._fck, self._lambda_s) - return self._fct - - def fcd(self) -> float: - """The design compressive strength in MPa. - - Returns: - float: The design compressive strength in MPa. - - Note: - Returns alpha1 * fc / gamma_c. With ACI 318 defaults - (alpha1=0.85, gamma_c=1.0), this gives 0.85*fc'. - """ - return self.alpha1 * self._fck / self.gamma_c - - def __elastic__(self) -> dict: - """Returns kwargs for an elastic constitutive law.""" - return {'E': self.Ec} - - def __parabolarectangle__(self) -> dict: - """Returns kwargs for a parabola-rectangle constitutive law. - - Note: - Uses ACI 318 parameters: peak strain at 0.002, - ultimate strain at 0.003, parabolic exponent n=2. - """ - return { - 'fc': self.fcd(), - 'eps_0': 0.002, - 'eps_u': self.eps_cu, - 'n': 2, - } - - def __bilinearcompression__(self) -> dict: - """Returns kwargs for a bilinear compression constitutive law. - - Note: - Provides an elastic-perfectly-plastic alternative to - the parabola-rectangle default. This is not the ACI - 318 Whitney rectangular stress block. The library - integrates the parabola-rectangle curve directly, - which ACI 318-19 Section 22.2.2.3 permits as an - alternative to the Section 22.2.2.4 equivalent - rectangular stress block simplification. - """ - return { - 'fc': self.fcd(), - 'eps_c': 0.002, - 'eps_cu': self.eps_cu, - } diff --git a/structuralcodes/materials/concrete/_concreteACI318_19.py b/structuralcodes/materials/concrete/_concreteACI318_19.py new file mode 100644 index 00000000..b69d1e23 --- /dev/null +++ b/structuralcodes/materials/concrete/_concreteACI318_19.py @@ -0,0 +1,444 @@ +"""The concrete class for ACI 318-19 Concrete Material.""" + +import typing as t + +from structuralcodes.codes import aci318_19 + +from ..constitutive_laws import ConstitutiveLaw, create_constitutive_law +from ._concrete import Concrete + + +class ConcreteACI318_19(Concrete): # noqa: N801 + """Concrete implementation for ACI 318-19. + + Note: + Use fc for ACI specified compressive strength. The fck parameter is + still accepted for compatibility with the base class and material + factory, and is treated as ACI fc' without fractile conversion. + + The base constructor uses SI units to match the rest of + structuralcodes: MPa for stress and kg/m3 for density. + The default density is 2400 kg/m3, approximately 150 lb/ft3 + expressed as mass density. It is the base material density, + not the ACI equilibrium density used in Eq. 19.2.2.1. Use + wc for that ACI density-dependent Ec and lambda calculation, + or use from_psi() when starting from US customary inputs. + + Usage philosophy (LRFD vs. partial factor method): + ACI 318 uses Load and Resistance Factor Design (LRFD), which + differs from the Eurocode / fib Model Code partial factor + method used elsewhere in this library. Two consequences for + users of this class: + + 1. Material strengths are not reduced at the material level. + gamma_c is fixed to 1.0 for standard ACI 318 design. + + 2. Strength reduction factors (phi, per ACI 318-19 Section + 21.2) must be applied by the user at the member capacity + level. This class does not apply phi for you. + + As with the Eurocode materials, the user is responsible for + choosing a constitutive law and parameters suited to the + limit state being analyzed: + + - Serviceability / cracked-stiffness estimates: use the + 'elastic' law with Ec, parameterized with mean (expected) + properties. + - Ultimate limit state (flexure, axial, biaxial interaction + domains): use 'parabolarectangle' (the default) or + 'bilinearcompression', which use the ACI 318-19 Section + 22.2 stress block parameters (alpha1=0.85, eps_cu=0.003). + Compute nominal capacities Pn, Mn with this material, then + multiply by the appropriate phi factor at the member level + to obtain phi*Pn and phi*Mn. + """ + + _Ec: t.Optional[float] = None + _fr: t.Optional[float] = None + _eps_c0: t.Optional[float] = None + _wc: t.Optional[float] = None + _lambda_s: t.Optional[float] = None + + def __init__( + self, + fc: t.Optional[float] = None, + name: t.Optional[str] = None, + density: float = 2400.0, + gamma_c: t.Optional[float] = None, + constitutive_law: t.Optional[ + t.Union[ + t.Literal[ + 'elastic', + 'parabolarectangle', + 'bilinearcompression', + ], + ConstitutiveLaw, + ] + ] = 'parabolarectangle', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, + Ec: t.Optional[float] = None, + fr: t.Optional[float] = None, + wc: t.Optional[float] = None, + lambda_s: t.Optional[float] = None, + eps_c0: t.Optional[float] = None, + fck: t.Optional[float] = None, + **kwargs, + ) -> None: + """Initializes a new instance of Concrete for ACI 318-19. + + Arguments: + fc (float, optional): Specified compressive strength (ACI fc') in + MPa. + + Keyword Arguments: + name (str): A descriptive name for concrete. + density (float): Material density in kg/m3 + (default: 2400). The default is approximately + 150 lb/ft3 expressed as mass density for the base + material model. It is separate from wc. + gamma_c (float, optional): Partial factor for concrete. + Must be None or 1.0 because ACI does not use material + partial factors. It is accepted only for compatibility + with the base concrete API; non-1.0 values raise + ValueError. + constitutive_law (ConstitutiveLaw | str): A valid + ConstitutiveLaw object or string. Valid options: + 'elastic', 'parabolarectangle', + 'bilinearcompression'. + initial_strain (Optional[float]): Initial strain. + initial_stress (Optional[float]): Initial stress. + strain_compatibility (Optional[bool]): If True, the + material deforms with the geometry. + Ec (float, optional): The modulus of elasticity in MPa. + fr (float, optional): The modulus of rupture in MPa. + wc (float, optional): Equilibrium density of concrete in + kg/m3 used for ACI density-dependent Ec and lambda. + If omitted, Ec uses the ACI normalweight expression + ``4700 * sqrt(fc)`` and lambda is 1.0. This is + separate from the base material density. + lambda_s (float, optional): Lightweight modification + factor. If omitted and wc is provided, it is derived + from ACI 318-19 Table 19.2.4.1(a). + eps_c0 (float, optional): Peak concrete strain used by + parabolic and bilinear constitutive laws. + fck (float, optional): Compatibility alias for ACI fc' in MPa, + accepted for the base concrete class and material factory. + + Raises: + ValueError: If the constitutive law is not valid for + concrete. + """ + del kwargs + if fck is None and fc is None: + raise ValueError('Either fc or fck must be provided.') + if fck is not None and fc is not None and fck != fc: + raise ValueError( + 'fc and fck cannot both be provided with different values.' + ) + if fck is None: + fck = fc + assert fck is not None + if name is None: + name = f'C{round(fck):d}' + super().__init__( + fck=fck, + name=name, + density=density, + existing=False, + gamma_c=gamma_c, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + ) + self._Ec = abs(Ec) if Ec is not None else None + self._fr = abs(fr) if fr is not None else None + self._wc = wc + self._lambda_s = lambda_s + self._eps_c0 = abs(eps_c0) if eps_c0 is not None else None + + self.__post_init__() + + # The constitutive law requires valid attributes, so it should be set + # after validation + self._constitutive_law = ( + constitutive_law + if isinstance(constitutive_law, ConstitutiveLaw) + else create_constitutive_law( + constitutive_law_name=constitutive_law, material=self + ) + ) + if 'concrete' not in self._constitutive_law.__materials__: + raise ValueError( + 'The provided constitutive law is not valid for concrete.' + ) + self._apply_initial_strain() + + def __post_init__(self): + """Validator for the attributes that are set in the constructor.""" + # fc + if self._fck < aci318_19.MINIMUM_STRESS_BLOCK_FC: + raise ValueError( + 'Specified compressive strength fc must be at least ' + f'{aci318_19.MINIMUM_STRESS_BLOCK_FC} MPa for ACI 318-19.' + ) + + # gamma_c + if self._gamma_c is not None and self._gamma_c != 1.0: + raise ValueError( + 'ACI 318-19 does not use material partial factors. ' + 'gamma_c must be 1.0.' + ) + + # wc + if self._wc is not None and (self._wc < 1440 or self._wc > 2560): + raise ValueError( + f'wc={self._wc} must be between 1440 and 2560 kg/m3' + ) + + # lambda_s + if self._lambda_s is not None and ( + self._lambda_s <= 0 or self._lambda_s > 1.0 + ): + raise ValueError( + f'lambda_s={self._lambda_s} must be in the range (0, 1]' + ) + + # eps_c0 + if self._eps_c0 is not None and ( + self._eps_c0 <= 0 or self._eps_c0 >= 0.1 + ): + raise ValueError( + 'eps_c0 should be a pure number without units. ' + f'Current: {self._eps_c0}.' + ) + + @property + def fc(self) -> float: + """Returns the specified compressive strength (fc') in MPa. + + Returns: + float: The specified compressive strength in MPa. + + Note: + The inherited fck property returns the same stored value for + base-class compatibility. + """ + return self._fck + + @property + def gamma_c(self) -> float: + """The partial factor for concrete. + + Returns: + float: The partial factor (default 1.0 for ACI 318). + + Note: + ACI 318 does not use material partial factors. The + value is fixed at 1.0; the constructor accepts gamma_c + only for base class compatibility and rejects non-1.0 + values. + """ + return self._gamma_c or 1.0 + + @property + def lambda_s(self) -> float: + """Returns the lightweight modification factor.""" + if self._lambda_s is not None: + return self._lambda_s + if self._wc is None: + return 1.0 + return aci318_19.lambda_factor(self._wc) + + @property + def Ec(self) -> float: + """Returns the modulus of elasticity in MPa. + + Returns: + float: The modulus of elasticity in MPa. + + Note: + Derived from fc and wc if not manually provided. If wc is + omitted, the ACI normalweight expression is used. + """ + if self._Ec is None: + return aci318_19.Ec(self._fck, self._wc) + return self._Ec + + @property + def fr(self) -> float: + """Returns the modulus of rupture in MPa. + + Returns: + float: The modulus of rupture in MPa. + + Note: + Derived from fc and lambda_s if not manually provided. + """ + if self._fr is None: + return aci318_19.fr(self._fck, self.lambda_s) + return self._fr + + @property + def eps_cu(self) -> float: + """Returns the ultimate concrete strain. + + Returns: + float: The ultimate strain (dimensionless). + """ + return aci318_19.eps_cu() + + @property + def eps_c0(self) -> float: + """Returns the peak concrete strain used by constitutive laws. + + Returns: + float: The peak strain (dimensionless). + + Note: + ACI 318-19 does not prescribe a complete concrete + stress-strain curve. The default value is the library's + peak-strain parameter for the selected constitutive laws. + """ + if self._eps_c0 is None: + return aci318_19.eps_c0() + return self._eps_c0 + + @property + def alpha1(self) -> float: + """Returns the stress block intensity factor. + + Returns: + float: The stress block intensity (dimensionless). + """ + return aci318_19.alpha1() + + @property + def beta1(self) -> float: + """Returns the Whitney stress block depth factor. + + Returns: + float: The stress block depth factor (dimensionless). + """ + return aci318_19.beta1(self._fck) + + def fcd(self) -> float: + """The design compressive strength in MPa. + + Returns: + float: The design compressive strength in MPa. + + Note: + Returns alpha1 * fc. ACI 318-19 strength reduction factors + are applied at member level, not material level. + """ + return self.alpha1 * self._fck + + def __elastic__(self) -> dict: + """Returns kwargs for an elastic constitutive law.""" + return {'E': self.Ec} + + def __parabolarectangle__(self) -> dict: + """Returns kwargs for a parabola-rectangle constitutive law. + + Note: + Uses library stress-strain parameters for ACI 318: + peak strain from eps_c0, ultimate strain at 0.003, + and parabolic exponent n=2. + """ + return { + 'fc': self.fcd(), + 'eps_0': self.eps_c0, + 'eps_u': self.eps_cu, + 'n': 2, + } + + def __bilinearcompression__(self) -> dict: + """Returns kwargs for a bilinear compression constitutive law. + + Note: + Provides an elastic-perfectly-plastic alternative to + the parabola-rectangle default. This is not the ACI + 318 Whitney rectangular stress block. The library + integrates the parabola-rectangle curve directly, + which ACI 318-19 Section 22.2.2.3 permits as an + alternative to the Section 22.2.2.4 equivalent + rectangular stress block simplification. + """ + return { + 'fc': self.fcd(), + 'eps_c': self.eps_c0, + 'eps_cu': self.eps_cu, + } + + @classmethod + def from_psi( + cls, + fc_psi: float, + name: t.Optional[str] = None, + density_pcf: float = 150.0, + gamma_c: t.Optional[float] = None, + constitutive_law: t.Optional[ + t.Union[ + t.Literal[ + 'elastic', + 'parabolarectangle', + 'bilinearcompression', + ], + ConstitutiveLaw, + ] + ] = 'parabolarectangle', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, + Ec_psi: t.Optional[float] = None, + fr_psi: t.Optional[float] = None, + wc_pcf: t.Optional[float] = None, + lambda_s: t.Optional[float] = None, + eps_c0: t.Optional[float] = None, + ) -> 'ConcreteACI318_19': + """Create ACI concrete from US customary inputs. + + Arguments: + fc_psi (float): Specified compressive strength (ACI fc') in psi. + + Keyword Arguments: + density_pcf (float): Material density in lb/ft3. Converted to + kg/m3 for the base material model. Default is 150 lb/ft3. + Ec_psi (float, optional): Manually specified concrete modulus + of elasticity in psi. Converted to MPa. + fr_psi (float, optional): Manually specified modulus of rupture + in psi. Converted to MPa. + wc_pcf (float, optional): Equilibrium density in lb/ft3 for + ACI density-dependent Ec and lambda calculations. Converted + to kg/m3. If omitted, normalweight Ec and lambda are used. + lambda_s (float, optional): Lightweight modification factor. + eps_c0 (float, optional): Peak concrete strain used by + parabolic and bilinear constitutive laws. + + Returns: + ConcreteACI318_19: A concrete material storing SI values + internally. + """ + if name is None: + name = f'C{round(fc_psi):d}psi' + return cls( + fc=aci318_19.psi_to_mpa(fc_psi), + name=name, + density=aci318_19.pcf_to_kg_per_m3(density_pcf), + gamma_c=gamma_c, + constitutive_law=constitutive_law, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + Ec=(aci318_19.psi_to_mpa(Ec_psi) if Ec_psi is not None else None), + fr=(aci318_19.psi_to_mpa(fr_psi) if fr_psi is not None else None), + wc=( + aci318_19.pcf_to_kg_per_m3(wc_pcf) + if wc_pcf is not None + else None + ), + lambda_s=lambda_s, + eps_c0=eps_c0, + ) diff --git a/structuralcodes/materials/reinforcement/__init__.py b/structuralcodes/materials/reinforcement/__init__.py index 6e3d98b7..d742f4cc 100644 --- a/structuralcodes/materials/reinforcement/__init__.py +++ b/structuralcodes/materials/reinforcement/__init__.py @@ -5,7 +5,7 @@ from structuralcodes.codes import _use_design_code from ._reinforcement import Reinforcement -from ._reinforcementACI318 import ReinforcementACI318 +from ._reinforcementACI318_19 import ReinforcementACI318_19 from ._reinforcementEC2_2004 import ReinforcementEC2_2004 from ._reinforcementEC2_2023 import ReinforcementEC2_2023 from ._reinforcementMC2010 import ReinforcementMC2010 @@ -13,14 +13,14 @@ __all__ = [ 'create_reinforcement', 'Reinforcement', - 'ReinforcementACI318', + 'ReinforcementACI318_19', 'ReinforcementMC2010', 'ReinforcementEC2_2004', 'ReinforcementEC2_2023', ] REINFORCEMENTS: t.Dict[str, Reinforcement] = { - 'ACI 318-19': ReinforcementACI318, + 'ACI 318-19': ReinforcementACI318_19, 'fib Model Code 2010': ReinforcementMC2010, 'EUROCODE 2 1992-1-1:2004': ReinforcementEC2_2004, 'EUROCODE 2 1992-1-1:2023': ReinforcementEC2_2023, diff --git a/structuralcodes/materials/reinforcement/_reinforcementACI318.py b/structuralcodes/materials/reinforcement/_reinforcementACI318.py deleted file mode 100644 index ce5a7bfa..00000000 --- a/structuralcodes/materials/reinforcement/_reinforcementACI318.py +++ /dev/null @@ -1,164 +0,0 @@ -"""The reinforcement class for ACI 318-19 Reinforcement Material.""" - -import typing as t - -from structuralcodes.codes import aci318 - -from ..constitutive_laws import ConstitutiveLaw, create_constitutive_law -from ._reinforcement import Reinforcement - - -class ReinforcementACI318(Reinforcement): # noqa: N801 - """Reinforcement implementation for ACI 318-19. - - Usage philosophy (LRFD vs. partial factor method): - ACI 318 uses Load and Resistance Factor Design (LRFD), which - differs from the Eurocode / fib Model Code partial factor - method used elsewhere in this library. Material strengths - are not reduced at the material level: gamma_s defaults to - 1.0 and should be left at 1.0 for standard ACI 318 design. - - Strength reduction factors (phi, per ACI 318-19 Section - 21.2) must be applied by the user at the member capacity - level. This class does not apply phi for you. For example, - when computing the nominal moment capacity Mn of a - reinforced section, use this reinforcement with its - 'elasticplastic' or 'elasticperfectlyplastic' constitutive - law and unreduced fy, then multiply the resulting Mn by the - appropriate phi factor (e.g. 0.90 for tension-controlled - flexure) to obtain phi*Mn. - - As with the Eurocode materials, the user chooses the - constitutive law appropriate to the limit state. For - ultimate limit state section analysis paired with - ConcreteACI318 using the 'parabolarectangle' law, pair this - reinforcement with 'elasticplastic' (the default) or - 'elasticperfectlyplastic'. - """ - - def __init__( - self, - fyk: float, - Es: float, - ftk: float, - epsuk: float, - gamma_s: t.Optional[float] = None, - name: t.Optional[str] = None, - density: float = 7850.0, - constitutive_law: t.Optional[ - t.Union[ - t.Literal[ - 'elastic', - 'elasticperfectlyplastic', - 'elasticplastic', - ], - ConstitutiveLaw, - ] - ] = 'elasticplastic', - initial_strain: t.Optional[float] = None, - initial_stress: t.Optional[float] = None, - strain_compatibility: t.Optional[bool] = None, - ): - """Initializes a new instance of Reinforcement for ACI 318-19. - - Arguments: - fyk (float): Specified yield strength (fy) in MPa. - Es (float): The Young's modulus in MPa. - ftk (float): Specified ultimate strength (fu) in MPa. - epsuk (float): The strain at ultimate stress level. - - Keyword Arguments: - gamma_s (Optional(float)): The partial factor for - reinforcement. Default is 1.0 (ACI does not use - material partial factors). - name (str): A descriptive name for the reinforcement. - density (float): Density in kg/m3 (default: 7850). - constitutive_law (ConstitutiveLaw | str): A valid - ConstitutiveLaw or string. Valid options: 'elastic', - 'elasticplastic', 'elasticperfectlyplastic'. - initial_strain (Optional[float]): Initial strain. - initial_stress (Optional[float]): Initial stress. - strain_compatibility (Optional[bool]): If True, the - material deforms with the geometry. - - Raises: - ValueError: If the constitutive law is not valid for - reinforcement. - """ - if name is None: - name = f'Reinforcement{round(fyk):d}' - - super().__init__( - fyk=fyk, - Es=Es, - name=name, - density=density, - ftk=ftk, - epsuk=epsuk, - gamma_s=gamma_s, - initial_strain=initial_strain, - initial_stress=initial_stress, - strain_compatibility=strain_compatibility, - ) - self._constitutive_law = ( - constitutive_law - if isinstance(constitutive_law, ConstitutiveLaw) - else create_constitutive_law( - constitutive_law_name=constitutive_law, material=self - ) - ) - if 'steel' not in self._constitutive_law.__materials__: - raise ValueError( - 'The provided constitutive law is not valid for reinforcement.' - ) - self._apply_initial_strain() - - def fyd(self) -> float: - """The design yield strength. - - Note: - ACI 318 does not reduce material strength. Returns - fy / gamma_s, which with default gamma_s=1.0 gives - the unreduced yield strength. - """ - return aci318.fy_design(self.fyk, phi=1.0 / self.gamma_s) - - @property - def gamma_s(self) -> float: - """The partial factor for reinforcement. - - Note: - Default is 1.0 for ACI 318 (no material partial - factor). - """ - return self._gamma_s or 1.0 - - def ftd(self) -> float: - """The design ultimate strength.""" - return self.ftk / self.gamma_s - - def epsud(self) -> float: - """The design ultimate strain.""" - return self.epsuk - - def __elastic__(self) -> dict: - """Returns kwargs for an elastic constitutive law.""" - return {'E': self.Es} - - def __elasticperfectlyplastic__(self) -> dict: - """Returns kwargs for ElasticPlastic law with no hardening.""" - return { - 'E': self.Es, - 'fy': self.fyd(), - 'eps_su': self.epsud(), - } - - def __elasticplastic__(self) -> dict: - """Returns kwargs for ElasticPlastic law with hardening.""" - Eh = (self.ftd() - self.fyd()) / (self.epsud() - self.epsyd) - return { - 'E': self.Es, - 'fy': self.fyd(), - 'Eh': Eh, - 'eps_su': self.epsud(), - } diff --git a/structuralcodes/materials/reinforcement/_reinforcementACI318_19.py b/structuralcodes/materials/reinforcement/_reinforcementACI318_19.py new file mode 100644 index 00000000..86d095d6 --- /dev/null +++ b/structuralcodes/materials/reinforcement/_reinforcementACI318_19.py @@ -0,0 +1,328 @@ +"""The reinforcement class for ACI 318-19 Reinforcement Material.""" + +import typing as t + +from structuralcodes.codes import aci318_19 + +from ..constitutive_laws import ConstitutiveLaw, create_constitutive_law +from ._reinforcement import Reinforcement + + +class ReinforcementACI318_19(Reinforcement): # noqa: N801 + """Reinforcement implementation for ACI 318-19. + + Usage philosophy (LRFD vs. partial factor method): + ACI 318 uses Load and Resistance Factor Design (LRFD), which + differs from the Eurocode / fib Model Code partial factor + method used elsewhere in this library. Material strengths + are not reduced at the material level: gamma_s is fixed to + 1.0 for standard ACI 318 design. The gamma_s constructor + argument is accepted only for compatibility with the base + reinforcement API and cannot be set to a value other than 1.0. + + Strength reduction factors (phi, per ACI 318-19 Section + 21.2) must be applied by the user at the member capacity + level. This class does not apply phi for you. For example, + when computing the nominal moment capacity Mn of a + reinforced section, use this reinforcement with its + 'elasticplastic' or 'elasticperfectlyplastic' constitutive + law and unreduced fy, then multiply the resulting Mn by the + appropriate phi factor (e.g. 0.90 for tension-controlled + flexure) to obtain phi*Mn. + + As with the Eurocode materials, the user chooses the + constitutive law appropriate to the limit state. For + ultimate limit state section analysis paired with + ConcreteACI318_19 using the 'parabolarectangle' law, pair this + reinforcement with 'elasticplastic' (the default) or + 'elasticperfectlyplastic'. + + The base constructor uses SI units to match the rest of + structuralcodes: MPa for stress and kg/m3 for density. Use + from_grade() or from_ksi() when starting from US customary + ACI inputs. + """ + + def __init__( + self, + fyk: float, + Es: float, + ftk: float, + epsuk: float, + gamma_s: t.Optional[float] = None, + name: t.Optional[str] = None, + density: float = 7850.0, + constitutive_law: t.Optional[ + t.Union[ + t.Literal[ + 'elastic', + 'elasticperfectlyplastic', + 'elasticplastic', + ], + ConstitutiveLaw, + ] + ] = 'elasticplastic', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, + ): + """Initializes a new instance of Reinforcement for ACI 318-19. + + Arguments: + fyk (float): Specified yield strength (fy) in MPa. + Es (float): The Young's modulus in MPa. + ftk (float): Specified ultimate strength (fu) in MPa. + epsuk (float): The strain at ultimate stress level. + + Keyword Arguments: + gamma_s (Optional(float)): The partial factor for + reinforcement. Must be None or 1.0 because ACI does + not use material partial factors. It is accepted only + for compatibility with the base reinforcement API; + non-1.0 values raise ValueError. + name (str): A descriptive name for the reinforcement. + density (float): Density in kg/m3 (default: 7850). + constitutive_law (ConstitutiveLaw | str): A valid + ConstitutiveLaw or string. Valid options: 'elastic', + 'elasticplastic', 'elasticperfectlyplastic'. + initial_strain (Optional[float]): Initial strain. + initial_stress (Optional[float]): Initial stress. + strain_compatibility (Optional[bool]): If True, the + material deforms with the geometry. + + Raises: + ValueError: If the constitutive law is not valid for + reinforcement. + """ + if name is None: + name = f'Reinforcement{round(fyk):d}' + + super().__init__( + fyk=fyk, + Es=Es, + name=name, + density=density, + ftk=ftk, + epsuk=epsuk, + gamma_s=gamma_s, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + ) + self.__post_init__() + + # The constitutive law requires valid attributes, so it should be set + # after validation + self._constitutive_law = ( + constitutive_law + if isinstance(constitutive_law, ConstitutiveLaw) + else create_constitutive_law( + constitutive_law_name=constitutive_law, material=self + ) + ) + if 'steel' not in self._constitutive_law.__materials__: + raise ValueError( + 'The provided constitutive law is not valid for reinforcement.' + ) + self._apply_initial_strain() + + def __post_init__(self): + """Validator for the attributes that are set in the constructor.""" + # gamma_s + if self._gamma_s is not None and self._gamma_s != 1.0: + raise ValueError( + 'ACI 318-19 does not use material partial factors. ' + 'gamma_s must be 1.0.' + ) + + # Es + if self._Es <= 0: + raise ValueError('Es must be positive.') + + # ftk + if self._ftk < self._fyk: + raise ValueError( + 'Specified ultimate strength cannot be lower than ' + 'specified yield strength.' + ) + + # epsuk + if self._epsuk <= self.epsyd: + raise ValueError( + 'Ultimate strain must be larger than yield strain.' + ) + + def fyd(self) -> float: + """The design yield strength. + + Note: + ACI 318 does not reduce material strength. Strength + reduction factors are applied at member level. + """ + return aci318_19.fy_design(self.fyk) + + @property + def gamma_s(self) -> float: + """The partial factor for reinforcement. + + Note: + ACI 318 does not use material partial factors. The + value is fixed at 1.0; the constructor accepts gamma_s + only for base class compatibility and rejects non-1.0 + values. + """ + return self._gamma_s or 1.0 + + def ftd(self) -> float: + """The design ultimate strength.""" + return self.ftk + + def epsud(self) -> float: + """The design ultimate strain.""" + return self.epsuk + + def __elastic__(self) -> dict: + """Returns kwargs for an elastic constitutive law.""" + return {'E': self.Es} + + def __elasticperfectlyplastic__(self) -> dict: + """Returns kwargs for ElasticPlastic law with no hardening.""" + return { + 'E': self.Es, + 'fy': self.fyd(), + 'eps_su': self.epsud(), + } + + def __elasticplastic__(self) -> dict: + """Returns kwargs for ElasticPlastic law with hardening.""" + Eh = (self.ftd() - self.fyd()) / (self.epsud() - self.epsyd) + return { + 'E': self.Es, + 'fy': self.fyd(), + 'Eh': Eh, + 'eps_su': self.epsud(), + } + + @classmethod + def from_grade( + cls, + grade: t.Union[str, int], + epsuk: float, + gamma_s: t.Optional[float] = None, + name: t.Optional[str] = None, + density_pcf: float = 490.0, + constitutive_law: t.Optional[ + t.Union[ + t.Literal[ + 'elastic', + 'elasticperfectlyplastic', + 'elasticplastic', + ], + ConstitutiveLaw, + ] + ] = 'elasticplastic', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, + ) -> 'ReinforcementACI318_19': + """Create ACI reinforcement from an ASTM grade designation. + + Arguments: + grade (str | int): ASTM reinforcement grade designation, + such as '40', '60', '80', or '100'. + epsuk (float): Strain at ultimate stress level. + + Keyword Arguments: + density_pcf (float): Material density in lb/ft3. Converted to + kg/m3 for the base material model. Default is 490 lb/ft3. + gamma_s (Optional(float)): Must be None or 1.0 because ACI + does not use material partial factors; non-1.0 values + raise ValueError. + name (str): A descriptive name for the reinforcement. + + Returns: + ReinforcementACI318_19: A reinforcement material storing SI + values internally. + + Note: + ACI 318-19 Table 20.2.1.3(a) does not provide a single + grade-level ultimate strain. The caller must provide epsuk + based on the applicable reinforcement specification and bar size. + """ + grade_label = str(grade) + props = aci318_19.reinforcement_grade_props(grade_label) + if name is None: + name = f'Grade {grade_label}' + return cls( + fyk=props['fy'], + Es=aci318_19.Es(), + ftk=props['fu'], + epsuk=epsuk, + gamma_s=gamma_s, + name=name, + density=aci318_19.pcf_to_kg_per_m3(density_pcf), + constitutive_law=constitutive_law, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + ) + + @classmethod + def from_ksi( + cls, + fy_ksi: float, + fu_ksi: float, + epsuk: float, + Es_ksi: float = 29000.0, + gamma_s: t.Optional[float] = None, + name: t.Optional[str] = None, + density_pcf: float = 490.0, + constitutive_law: t.Optional[ + t.Union[ + t.Literal[ + 'elastic', + 'elasticperfectlyplastic', + 'elasticplastic', + ], + ConstitutiveLaw, + ] + ] = 'elasticplastic', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, + ) -> 'ReinforcementACI318_19': + """Create ACI reinforcement from US customary stress inputs. + + Arguments: + fy_ksi (float): Specified yield strength in ksi. + fu_ksi (float): Specified ultimate strength in ksi. + epsuk (float): Strain at ultimate stress level. + + Keyword Arguments: + Es_ksi (float): Modulus of elasticity in ksi. + Default is 29000 ksi. + gamma_s (Optional(float)): Must be None or 1.0 because ACI + does not use material partial factors; non-1.0 values + raise ValueError. + density_pcf (float): Material density in lb/ft3. Converted to + kg/m3 for the base material model. Default is 490 lb/ft3. + + Returns: + ReinforcementACI318_19: A reinforcement material storing SI + values internally. + """ + if name is None: + name = f'Reinforcement{round(fy_ksi):d}ksi' + return cls( + fyk=aci318_19.ksi_to_mpa(fy_ksi), + Es=aci318_19.ksi_to_mpa(Es_ksi), + ftk=aci318_19.ksi_to_mpa(fu_ksi), + epsuk=epsuk, + gamma_s=gamma_s, + name=name, + density=aci318_19.pcf_to_kg_per_m3(density_pcf), + constitutive_law=constitutive_law, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + ) diff --git a/tests/test_aci318/test_concrete_aci318.py b/tests/test_aci318/test_concrete_aci318.py deleted file mode 100644 index d64cc68a..00000000 --- a/tests/test_aci318/test_concrete_aci318.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Tests for the ConcreteACI318 material class.""" - -import math - -import pytest - -import structuralcodes -from structuralcodes.materials.concrete import ( - ConcreteACI318, - create_concrete, -) - - -@pytest.fixture(autouse=True) -def _reset_design_code(): - """Reset the global design code after each test.""" - yield - structuralcodes.set_design_code(None) - - -def test_create_via_factory(): - """Test creating concrete via the factory function.""" - c = create_concrete(fck=28, design_code='aci318') - assert isinstance(c, ConcreteACI318) - - -def test_default_name(): - """Test default name generation.""" - structuralcodes.set_design_code('aci318') - c = create_concrete(fck=28) - assert c.name == 'C28' - - -def test_fc_property(): - """Test the fc alias for fck.""" - c = ConcreteACI318(fck=28) - assert c.fc == 28 - assert c.fck == 28 - - -def test_gamma_c_default(): - """Test default gamma_c is 1.0 for ACI.""" - c = ConcreteACI318(fck=28) - assert c.gamma_c == 1.0 - - -def test_Ec_property(): - """Test Ec is derived from fc.""" - c = ConcreteACI318(fck=28) - expected = 2320**1.5 * 0.043 * math.sqrt(28) - assert math.isclose(c.Ec, expected, rel_tol=5e-3) - - -def test_Ec_specified(): - """Test Ec can be manually overridden.""" - c = ConcreteACI318(fck=28, Ec=30000) - assert c.Ec == 30000 - - -def test_fr_property(): - """Test fr is derived from fc.""" - c = ConcreteACI318(fck=28) - expected = 0.62 * math.sqrt(28) - assert math.isclose(c.fr, expected) - - -def test_fr_specified(): - """Test fr can be manually overridden.""" - c = ConcreteACI318(fck=28, fr=5.0) - assert c.fr == 5.0 - - -def test_beta1_property(): - """Test beta1 is derived from fc.""" - c = ConcreteACI318(fck=28) - assert c.beta1 == 0.85 - - -def test_eps_cu_property(): - """Test eps_cu returns 0.003.""" - c = ConcreteACI318(fck=28) - assert c.eps_cu == 0.003 - - -def test_alpha1_property(): - """Test alpha1 returns 0.85.""" - c = ConcreteACI318(fck=28) - assert c.alpha1 == 0.85 - - -def test_fcd(): - """Test fcd = alpha1 * fc / gamma_c.""" - c = ConcreteACI318(fck=28) - assert math.isclose(c.fcd(), 0.85 * 28) - - -def test_fct_property(): - """Test fct is derived from fc.""" - c = ConcreteACI318(fck=28) - expected = 0.56 * math.sqrt(28) - assert math.isclose(c.fct, expected) - - -def test_constitutive_law_elastic(): - """Test elastic constitutive law creation.""" - c = ConcreteACI318(fck=28, constitutive_law='elastic') - assert c.constitutive_law is not None - - -def test_constitutive_law_parabolarectangle(): - """Test parabola-rectangle constitutive law creation.""" - c = ConcreteACI318(fck=28, constitutive_law='parabolarectangle') - assert c.constitutive_law is not None - - -def test_constitutive_law_bilinear(): - """Test bilinear compression constitutive law creation.""" - c = ConcreteACI318(fck=28, constitutive_law='bilinearcompression') - assert c.constitutive_law is not None diff --git a/tests/test_aci318/__init__.py b/tests/test_aci318_19/__init__.py similarity index 100% rename from tests/test_aci318/__init__.py rename to tests/test_aci318_19/__init__.py diff --git a/tests/test_aci318/test_aci318_concrete_material_properties.py b/tests/test_aci318_19/test_aci318_19_concrete_material_properties.py similarity index 56% rename from tests/test_aci318/test_aci318_concrete_material_properties.py rename to tests/test_aci318_19/test_aci318_19_concrete_material_properties.py index 32714c5f..7317f80c 100644 --- a/tests/test_aci318/test_aci318_concrete_material_properties.py +++ b/tests/test_aci318_19/test_aci318_19_concrete_material_properties.py @@ -4,23 +4,22 @@ import pytest -from structuralcodes.codes.aci318 import _concrete_material_properties - -WC_DEFAULT = 2320.0 +from structuralcodes.codes import aci318_19 +from structuralcodes.codes.aci318_19 import _concrete_material_properties @pytest.mark.parametrize( 'fc, expected', [ - (21, WC_DEFAULT**1.5 * 0.043 * math.sqrt(21)), - (28, WC_DEFAULT**1.5 * 0.043 * math.sqrt(28)), - (35, WC_DEFAULT**1.5 * 0.043 * math.sqrt(35)), - (42, WC_DEFAULT**1.5 * 0.043 * math.sqrt(42)), - (55, WC_DEFAULT**1.5 * 0.043 * math.sqrt(55)), + (21, 4700 * math.sqrt(21)), + (28, 4700 * math.sqrt(28)), + (35, 4700 * math.sqrt(35)), + (42, 4700 * math.sqrt(42)), + (55, 4700 * math.sqrt(55)), ], ) def test_Ec_normalweight(fc, expected): - """Test Ec for normalweight concrete (wc=2320 kg/m3).""" + """Test Ec for normalweight concrete.""" assert math.isclose(_concrete_material_properties.Ec(fc), expected) @@ -86,8 +85,8 @@ def test_fr_invalid_lambda(): [ (21, 0.85), (28, 0.85), - (35, 0.80), - (41.5, 0.85 - 0.05 * (41.5 - 28) / 7), + (35, 0.85 - 0.20 / 27 * (35 - 28)), + (41.5, 0.85 - 0.20 / 27 * (41.5 - 28)), (55, 0.65), (69, 0.65), ], @@ -110,40 +109,52 @@ def test_eps_cu(): assert _concrete_material_properties.eps_cu() == 0.003 +def test_eps_c0(): + """Test default peak concrete strain.""" + assert _concrete_material_properties.eps_c0() == 0.002 + + @pytest.mark.parametrize( - 'concrete_type, expected', + 'wc, expected', [ - ('normalweight', 1.0), - ('sand-lightweight', 0.85), - ('all-lightweight', 0.75), + (1600, 0.75), + (1800, 0.0075 * 1800 / 16.01846337396014), + (2163, 1.0), ], ) -def test_lambda_factor(concrete_type, expected): +def test_lambda_factor(wc, expected): """Test lightweight modification factor.""" - assert ( - _concrete_material_properties.lambda_factor(concrete_type) == expected + assert math.isclose( + _concrete_material_properties.lambda_factor(wc), expected ) def test_lambda_factor_invalid(): - """Test lambda_factor raises for unknown type.""" + """Test lambda_factor raises for invalid density.""" with pytest.raises(ValueError): - _concrete_material_properties.lambda_factor('unknown') - - -@pytest.mark.parametrize( - 'fc, expected', - [ - (21, 0.56 * math.sqrt(21)), - (28, 0.56 * math.sqrt(28)), - (35, 0.56 * math.sqrt(35)), - ], -) -def test_fct(fc, expected): - """Test splitting tensile strength.""" - assert math.isclose(_concrete_material_properties.fct(fc), expected) + _concrete_material_properties.lambda_factor(0) def test_alpha1(): """Test stress block intensity factor.""" assert _concrete_material_properties.alpha1() == 0.85 + + +def test_aci318_19_unit_conversions(): + """Test US customary conversion helpers exposed by aci318_19.""" + assert math.isclose(aci318_19.psi_to_mpa(5000), 34.473786465841806) + assert math.isclose(aci318_19.mpa_to_psi(34.473786465841806), 5000) + assert math.isclose(aci318_19.ksi_to_mpa(60), 413.6854375901017) + assert math.isclose(aci318_19.mpa_to_ksi(413.6854375901017), 60) + assert math.isclose(aci318_19.pcf_to_kg_per_m3(150), 2402.769506094021) + assert math.isclose(aci318_19.kg_per_m3_to_pcf(2402.769506094021), 150) + assert math.isclose(aci318_19.in_to_mm(12), 304.8) + assert math.isclose(aci318_19.mm_to_in(304.8), 12) + assert math.isclose(aci318_19.in2_to_mm2(1), 645.16) + assert math.isclose(aci318_19.mm2_to_in2(645.16), 1) + assert math.isclose(aci318_19.in4_to_mm4(1), 416231.4256) + assert math.isclose(aci318_19.mm4_to_in4(416231.4256), 1) + assert math.isclose(aci318_19.kip_to_n(1), 4448.2216152605) + assert math.isclose(aci318_19.n_to_kip(4448.2216152605), 1) + assert math.isclose(aci318_19.kip_in_to_nmm(1), 4448.2216152605 * 25.4) + assert math.isclose(aci318_19.nmm_to_kip_in(4448.2216152605 * 25.4), 1) diff --git a/tests/test_aci318/test_aci318_reinforcement_material_properties.py b/tests/test_aci318_19/test_aci318_19_reinforcement_material_properties.py similarity index 77% rename from tests/test_aci318/test_aci318_reinforcement_material_properties.py rename to tests/test_aci318_19/test_aci318_19_reinforcement_material_properties.py index bf23f1d3..183a76e9 100644 --- a/tests/test_aci318/test_aci318_reinforcement_material_properties.py +++ b/tests/test_aci318_19/test_aci318_19_reinforcement_material_properties.py @@ -4,7 +4,7 @@ import pytest -from structuralcodes.codes.aci318 import _reinforcement_material_properties +from structuralcodes.codes.aci318_19 import _reinforcement_material_properties def test_Es(): @@ -22,31 +22,18 @@ def test_Es(): ], ) def test_fy_design_default(fy, expected): - """Test design yield strength with default phi=1.0.""" + """Test design yield strength returns unreduced fy.""" assert math.isclose( _reinforcement_material_properties.fy_design(fy), expected ) -def test_fy_design_with_phi(): - """Test design yield strength with explicit phi.""" - assert math.isclose( - _reinforcement_material_properties.fy_design(420, phi=0.9), 378 - ) - - def test_fy_design_invalid_fy(): """Test fy_design raises for non-positive fy.""" with pytest.raises(ValueError): _reinforcement_material_properties.fy_design(-1) -def test_fy_design_invalid_phi(): - """Test fy_design raises for phi outside (0, 1].""" - with pytest.raises(ValueError): - _reinforcement_material_properties.fy_design(420, phi=1.5) - - @pytest.mark.parametrize( 'fy, expected', [ diff --git a/tests/test_aci318_19/test_concrete_aci318_19.py b/tests/test_aci318_19/test_concrete_aci318_19.py new file mode 100644 index 00000000..e8471b27 --- /dev/null +++ b/tests/test_aci318_19/test_concrete_aci318_19.py @@ -0,0 +1,203 @@ +"""Tests for the ConcreteACI318_19 material class.""" + +import math + +import pytest + +import structuralcodes +from structuralcodes.codes import aci318_19 +from structuralcodes.materials.concrete import ( + ConcreteACI318_19, + create_concrete, +) + + +@pytest.fixture(autouse=True) +def _reset_design_code(): + """Reset the global design code after each test.""" + yield + structuralcodes.set_design_code(None) + + +def test_create_via_factory(): + """Test creating concrete via the factory function.""" + c = create_concrete(fck=28, design_code='aci318_19') + assert isinstance(c, ConcreteACI318_19) + + +def test_default_name(): + """Test default name generation.""" + structuralcodes.set_design_code('aci318_19') + c = create_concrete(fck=28) + assert c.name == 'C28' + + +def test_fc_property(): + """Test the ACI fc property and inherited fck compatibility.""" + c = ConcreteACI318_19(fc=28) + assert c.fc == 28 + assert c.fck == 28 + + +def test_fc_constructor_alias(): + """Test direct ACI construction with fc.""" + c = ConcreteACI318_19(fc=28) + assert c.fc == 28 + assert c.fck == 28 + + +def test_fck_and_fc_constructor_alias_match(): + """Test fck and fc can both be provided when equal.""" + c = ConcreteACI318_19(fck=28, fc=28) + assert c.fc == 28 + assert c.fck == 28 + + +def test_fck_and_fc_constructor_alias_conflict(): + """Test fck and fc cannot conflict.""" + with pytest.raises(ValueError): + ConcreteACI318_19(fck=28, fc=35) + + +def test_missing_fc_and_fck(): + """Test one compressive strength input is required.""" + with pytest.raises(ValueError): + ConcreteACI318_19() + + +def test_from_psi(): + """Test creating concrete from US customary strength.""" + c = ConcreteACI318_19.from_psi(fc_psi=5000) + expected_fc = aci318_19.psi_to_mpa(5000) + assert c.name == 'C5000psi' + assert math.isclose(c.fc, expected_fc) + assert math.isclose(c.density, aci318_19.pcf_to_kg_per_m3(150)) + assert math.isclose(c.Ec, 4700 * math.sqrt(expected_fc)) + + +def test_from_psi_customary_overrides(): + """Test customary overrides are converted to SI internals.""" + c = ConcreteACI318_19.from_psi( + fc_psi=5000, + density_pcf=145, + wc_pcf=120, + Ec_psi=4000000, + fr_psi=500, + ) + assert math.isclose(c.fc, aci318_19.psi_to_mpa(5000)) + assert math.isclose(c.density, aci318_19.pcf_to_kg_per_m3(145)) + assert math.isclose(c.Ec, aci318_19.psi_to_mpa(4000000)) + assert math.isclose(c.fr, aci318_19.psi_to_mpa(500)) + assert math.isclose(c.lambda_s, 0.0075 * 120) + + +def test_gamma_c_default(): + """Test default gamma_c is 1.0 for ACI.""" + c = ConcreteACI318_19(fc=28) + assert c.gamma_c == 1.0 + + +def test_Ec_property(): + """Test Ec is derived from fc.""" + c = ConcreteACI318_19(fc=28) + expected = 4700 * math.sqrt(28) + assert math.isclose(c.Ec, expected, rel_tol=5e-3) + + +def test_Ec_property_with_wc(): + """Test Ec can use ACI density-dependent expression.""" + c = ConcreteACI318_19(fc=28, wc=1800) + expected = 1800**1.5 * 0.043 * math.sqrt(28) + assert math.isclose(c.Ec, expected) + + +def test_Ec_specified(): + """Test Ec can be manually overridden.""" + c = ConcreteACI318_19(fc=28, Ec=30000) + assert c.Ec == 30000 + + +def test_fr_property(): + """Test fr is derived from fc.""" + c = ConcreteACI318_19(fc=28) + expected = 0.62 * math.sqrt(28) + assert math.isclose(c.fr, expected) + + +def test_fr_specified(): + """Test fr can be manually overridden.""" + c = ConcreteACI318_19(fc=28, fr=5.0) + assert c.fr == 5.0 + + +def test_lambda_s_property_from_wc(): + """Test lambda_s is derived from wc when not manually provided.""" + c = ConcreteACI318_19(fc=28, wc=1800) + expected = 0.0075 * 1800 / 16.01846337396014 + assert math.isclose(c.lambda_s, expected) + + +def test_lambda_s_specified(): + """Test lambda_s can be manually overridden.""" + c = ConcreteACI318_19(fc=28, lambda_s=0.8) + assert c.lambda_s == 0.8 + + +def test_beta1_property(): + """Test beta1 is derived from fc.""" + c = ConcreteACI318_19(fc=28) + assert c.beta1 == 0.85 + + +def test_eps_cu_property(): + """Test eps_cu returns 0.003.""" + c = ConcreteACI318_19(fc=28) + assert c.eps_cu == 0.003 + + +def test_eps_c0_property(): + """Test eps_c0 returns 0.002.""" + c = ConcreteACI318_19(fc=28) + assert c.eps_c0 == 0.002 + + +def test_alpha1_property(): + """Test alpha1 returns 0.85.""" + c = ConcreteACI318_19(fc=28) + assert c.alpha1 == 0.85 + + +def test_fcd(): + """Test fcd = alpha1 * fc.""" + c = ConcreteACI318_19(fc=28) + assert math.isclose(c.fcd(), 0.85 * 28) + + +def test_invalid_gamma_c(): + """Test gamma_c values other than 1.0 are rejected.""" + with pytest.raises(ValueError): + ConcreteACI318_19(fc=28, gamma_c=1.5) + + +def test_invalid_fc(): + """Test ACI minimum concrete strength is enforced.""" + with pytest.raises(ValueError): + ConcreteACI318_19(fc=16) + + +def test_constitutive_law_elastic(): + """Test elastic constitutive law creation.""" + c = ConcreteACI318_19(fc=28, constitutive_law='elastic') + assert c.constitutive_law is not None + + +def test_constitutive_law_parabolarectangle(): + """Test parabola-rectangle constitutive law creation.""" + c = ConcreteACI318_19(fc=28, constitutive_law='parabolarectangle') + assert c.constitutive_law is not None + + +def test_constitutive_law_bilinear(): + """Test bilinear compression constitutive law creation.""" + c = ConcreteACI318_19(fc=28, constitutive_law='bilinearcompression') + assert c.constitutive_law is not None diff --git a/tests/test_aci318/test_reinforcement_aci318.py b/tests/test_aci318_19/test_reinforcement_aci318_19.py similarity index 58% rename from tests/test_aci318/test_reinforcement_aci318.py rename to tests/test_aci318_19/test_reinforcement_aci318_19.py index 074d10d2..8ecac4e0 100644 --- a/tests/test_aci318/test_reinforcement_aci318.py +++ b/tests/test_aci318_19/test_reinforcement_aci318_19.py @@ -1,12 +1,13 @@ -"""Tests for the ReinforcementACI318 material class.""" +"""Tests for the ReinforcementACI318_19 material class.""" import math import pytest import structuralcodes +from structuralcodes.codes import aci318_19 from structuralcodes.materials.reinforcement import ( - ReinforcementACI318, + ReinforcementACI318_19, create_reinforcement, ) @@ -25,14 +26,14 @@ def test_create_via_factory(): Es=200000, ftk=550, epsuk=0.05, - design_code='aci318', + design_code='aci318_19', ) - assert isinstance(r, ReinforcementACI318) + assert isinstance(r, ReinforcementACI318_19) def test_default_name(): """Test default name generation.""" - r = ReinforcementACI318( + r = ReinforcementACI318_19( fyk=420, Es=200000, ftk=550, @@ -43,7 +44,7 @@ def test_default_name(): def test_gamma_s_default(): """Test default gamma_s is 1.0 for ACI.""" - r = ReinforcementACI318( + r = ReinforcementACI318_19( fyk=420, Es=200000, ftk=550, @@ -52,9 +53,46 @@ def test_gamma_s_default(): assert r.gamma_s == 1.0 +def test_from_grade(): + """Test creating reinforcement from an ASTM grade designation.""" + r = ReinforcementACI318_19.from_grade('60', epsuk=0.05) + assert r.name == 'Grade 60' + assert math.isclose(r.fyk, 420) + assert math.isclose(r.Es, 200000) + assert math.isclose(r.ftk, 550) + assert math.isclose(r.epsuk, 0.05) + assert math.isclose(r.density, aci318_19.pcf_to_kg_per_m3(490)) + + +def test_from_ksi(): + """Test creating reinforcement from US customary stress inputs.""" + r = ReinforcementACI318_19.from_ksi( + fy_ksi=60, + fu_ksi=80, + epsuk=0.05, + ) + assert r.name == 'Reinforcement60ksi' + assert math.isclose(r.fyk, aci318_19.ksi_to_mpa(60)) + assert math.isclose(r.Es, aci318_19.ksi_to_mpa(29000)) + assert math.isclose(r.ftk, aci318_19.ksi_to_mpa(80)) + assert math.isclose(r.epsuk, 0.05) + + +def test_invalid_gamma_s(): + """Test gamma_s values other than 1.0 are rejected.""" + with pytest.raises(ValueError): + ReinforcementACI318_19( + fyk=420, + Es=200000, + ftk=550, + epsuk=0.05, + gamma_s=1.15, + ) + + def test_fyd(): """Test fyd returns unreduced fy (gamma_s=1.0).""" - r = ReinforcementACI318( + r = ReinforcementACI318_19( fyk=420, Es=200000, ftk=550, @@ -65,7 +103,7 @@ def test_fyd(): def test_ftd(): """Test ftd returns unreduced ftk (gamma_s=1.0).""" - r = ReinforcementACI318( + r = ReinforcementACI318_19( fyk=420, Es=200000, ftk=550, @@ -76,7 +114,7 @@ def test_ftd(): def test_epsud(): """Test epsud returns epsuk (no reduction for ACI).""" - r = ReinforcementACI318( + r = ReinforcementACI318_19( fyk=420, Es=200000, ftk=550, @@ -87,7 +125,7 @@ def test_epsud(): def test_constitutive_law_elastic(): """Test elastic constitutive law creation.""" - r = ReinforcementACI318( + r = ReinforcementACI318_19( fyk=420, Es=200000, ftk=550, @@ -99,7 +137,7 @@ def test_constitutive_law_elastic(): def test_constitutive_law_elasticplastic(): """Test elastic-plastic constitutive law creation.""" - r = ReinforcementACI318( + r = ReinforcementACI318_19( fyk=420, Es=200000, ftk=550, @@ -111,7 +149,7 @@ def test_constitutive_law_elasticplastic(): def test_constitutive_law_perfectly_plastic(): """Test elastic perfectly plastic constitutive law.""" - r = ReinforcementACI318( + r = ReinforcementACI318_19( fyk=420, Es=200000, ftk=550,