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..084e5f09 --- /dev/null +++ b/structuralcodes/materials/concrete/_concreteACI318.py @@ -0,0 +1,259 @@ +"""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. + + ACI 318 does not apply partial safety factors to material + strength. The gamma_c parameter defaults to 1.0. Strength + reduction factors (phi) are applied at the member capacity + level, not the material level. + """ + + _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: + Represents the Whitney equivalent rectangular stress + block approximation. + """ + 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..d69d8a7d --- /dev/null +++ b/structuralcodes/materials/reinforcement/_reinforcementACI318.py @@ -0,0 +1,146 @@ +"""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. + + Note: + ACI 318 does not apply partial safety factors to material + strength. The gamma_s parameter defaults to 1.0. Strength + reduction factors (phi) are applied at the member capacity + level, not the material level. + """ + + 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