diff --git a/structuralcodes/codes/ec2_2023/__init__.py b/structuralcodes/codes/ec2_2023/__init__.py index c8662148..4bfbf609 100644 --- a/structuralcodes/codes/ec2_2023/__init__.py +++ b/structuralcodes/codes/ec2_2023/__init__.py @@ -50,8 +50,39 @@ wk_cal, wk_cal2, ) +from ._section_8_1_bending import ( + MEd_min, + NRd0, + biaxial_resistant_ratio, + confinement_sigma_c2d_circular_square, + confinement_sigma_c2d_compression_zones, + confinement_sigma_c2d_multiple, + confinement_sigma_c2d_rectangular, + delta_fcd_confined, + epsc2_c, + epscu_c, + fcd_c, + kconf_b_bending, + kconf_b_circular, + kconf_b_multiple, + kconf_b_square_single, + kconf_s_bending, + kconf_s_multiple, + kconf_s_square_single, + sigma_cd, +) __all__ = [ + 'epsc2_c', + 'epscu_c', + 'fcd_c', + 'kconf_b_bending', + 'kconf_b_circular', + 'kconf_s_multiple', + 'kconf_s_bending', + 'kconf_b_multiple', + 'kconf_b_square_single', + 'kconf_s_square_single', 'A_phi_correction_exp', 'alpha_c_th', 'alpha_s_th', @@ -97,6 +128,15 @@ 'srm_cal', 'wk_cal', 'wk_cal2', + 'MEd_min', + 'NRd0', + 'biaxial_resistant_ratio', + 'sigma_cd', + 'delta_fcd_confined', + 'confinement_sigma_c2d_circular_square', + 'confinement_sigma_c2d_compression_zones', + 'confinement_sigma_c2d_multiple', + 'confinement_sigma_c2d_rectangular', ] __title__: str = 'EUROCODE 2 1992-1-1:2023' diff --git a/structuralcodes/codes/ec2_2023/_section_8_1_bending.py b/structuralcodes/codes/ec2_2023/_section_8_1_bending.py new file mode 100644 index 00000000..44a32766 --- /dev/null +++ b/structuralcodes/codes/ec2_2023/_section_8_1_bending.py @@ -0,0 +1,601 @@ +from typing import Literal + +import numpy as np +from numpy.typing import ArrayLike + + +def MEd_min(h: float, NEd: float) -> float: + """Minimum eccentricity for effects of imperfections. + + EN1992-1-1:2023 Eq.(8.1). + + Computes the minimum moment for a determined h-height section for taking + into consideration geometric imperfections unless second order effects are + used. + + Args: + h (float): Height of the element in mm. + Ned (float): Axial force in kN. + + Returns: + float: minimum design moment in kNm. + """ + ed_min = max(h / 30, 20) / 1000 + return NEd * ed_min + + +def e_min(h: float) -> float: + """Compute the minimum eccentricity for geometric imperfections. + + EN1992-1-1:2023 Eq.(8.1). + + Args: + h (float): Height of the element in mm. + + Returns: + float: Minimum eccentricity in m. + """ + return max(h / 30, 20) / 1000 + + +def NRd0(Ac: float, fcd: float, As: float, fyd: float) -> float: + """Design value of axial resistance in compression. + + EN1992-1-1:2023 Eq. (8.3). + + Computes the design value of axial resistance under compression without + accompanying moments. + + Args: + Ac (float): Concrete area in mm2. + fcd (float): Compressive design resistance of concrete in MPa. If + confined concrete, then replace by fcd,c (8.15). + As (float): Reinforcement area in mm2. + fyd (float): Yield tensile resistance of steel in MPa. + + Returns: + float: Axial resistance in compression in kN. + + Raises: + ValueError: If any of Ac, fcd, As or Ayd is less than 0. + """ + if Ac < 0: + raise ValueError('Concrete area cannot be negative') + if fcd < 0: + raise ValueError('Concrete resistance cannot be negative') + if As < 0: + raise ValueError('Steel area cannot be negative') + if fyd < 0: + raise ValueError('Steel resistance cannot be negative') + + return (Ac * fcd + As * fyd) / 1000 + + +def biaxial_resistant_ratio( + MEdz_MRdz: float, + MEdy_MRdy: float, + Ned_NRd: float, + section_type: Literal['rectangular', 'circular', 'elliptical'], +) -> float: + """Computes the resistant ratio in biaxial bending. + + EN1992-1-1:2023 Eq. (8.2). + + In the absence of an accurate cross-section design for biaxial beding this + criterion may be used. + + Args: + MEdz_MRdz (float): Ratio between the design bending moment and the + resistance in the Z-axis. + MEdy_MRdy (float): Ratio between the design bending moment and the + resistance in the Y-axis. + Ned_NRd (float): Ratio between the design axial force and the axial + compressive resistance. + section_type (str): The section geometry type. + + Returns: + float: The resistance ratio (non-dimensional). + """ + if section_type in ('elliptical', 'circular'): + an = 2.0 + else: + an = np.interp( + Ned_NRd, + xp=[0.1, 0.7, 2.0], + fp=[1.0, 1.5, 2.0], + ) + + return abs(MEdz_MRdz) ** an + abs(MEdy_MRdy) ** an + + +def sigma_cd(fcd: float, eps_c: float) -> float: + """Computes the stress distribution in the compression zones. + + EN1992-1-1:2023 Eq. (8.4). + + Computes the scress distribution in the compression zones (compressive + shown as positive). + + Args: + fcd (float): Compressive design resistance of concrete (MPa). + eps_c (float): Strain value of concrete (non dimensional). + + Returns: + float: Concrete stress in MPa. + + Raises: + ValueError: If strain greater than eps_c_u=0.0035. + """ + if eps_c <= 0: + return 0.0 + if eps_c <= 0.002: + return fcd * (1 - (1 - eps_c / 0.002) ** 2) + if eps_c <= 0.0035: + return fcd + + raise ValueError('Strain cannot be greater than eps_c_u=0.0035') + + +def delta_fcd_confined(sigma_c2d: float, f_cd: float, ddg: float) -> float: + """Calculate the compressive strength increase (delta_f_cd) due to a + transverse compressive stress. + + EN1992-1-1:2023 Eq. (8.9 and 8.10). + + Args: + sigma_c2d (float): the absolute alue of the minum principal + transverse compressive stress in MPa. + f_cd (float): Compressive design strength in MPa. + ddg (float): Maximum aggregate size in mm. + + Returns: + float: Compressive strength increase in MPa. + """ + if sigma_c2d < 0: + raise ValueError( + f'sigma_c2d must be non-negative. Got {sigma_c2d} instead.' + ) + if f_cd < 0: + raise ValueError(f'f_cd must be non-negative. Got {f_cd} instead.') + if ddg < 0: + raise ValueError(f'ddg must be non-negative. Got {ddg} instead.') + + if sigma_c2d <= 0.6 * f_cd: + delta_f_cd = 4 * sigma_c2d + else: + delta_f_cd = 3.5 * sigma_c2d ** (3 / 4) * f_cd ** (1 / 4) + + if ddg < 32: + delta_f_cd *= ddg / 32 + + return delta_f_cd + + +def confinement_sigma_c2d_circular_square( + A_s_conf: float, f_yd: float, b_cs: float, s: float +) -> float: + """Calculate the confinement stress (sigma_c2d) for circular and square + members with single confinement reinforcement. + + EN1992-1-1:2023 Eq (8.11). + + Args: + A_s_conf (float): Cross-sectional area of one leg of confinement + reinforcement in mm2. + f_yd (float): Yield design strength of reinforcement in MPa. + b_cs (float): Width of the confinement core in mm. + s (float): Spacing of confinement reinforcement in mm. + + Returns: + float: Confinement stress in MPa. + """ + if A_s_conf < 0: + raise ValueError( + f'A_s_conf must be non-negative. Got {A_s_conf} instead.' + ) + if f_yd < 0: + raise ValueError(f'f_yd must be non-negative. Got {f_yd} instead.') + if b_cs < 0: + raise ValueError(f'b_cs must be non-negative. Got {b_cs} instead.') + if s < 0: + raise ValueError(f's must be non-negative. Got {s} instead.') + + return 2 * A_s_conf * f_yd / (b_cs * s) + + +def confinement_sigma_c2d_rectangular( + A_s_conf: float, f_yd: float, b_csx: float, b_csy: float, s: float +) -> float: + """Calculate the confinement stress (sigma_c2d) for rectangular members + with single confinement reinforcement. + + EN1992-1-1:2023 Eq. (8.12). + + Args: + A_s_conf (float): Cross-sectional area of one leg of confinement + reinforcement. + f_yd (float): Yield strength of reinforcement. + b_csx (float): Width of the confinement core in x direction. + b_csy (float): Width of the confinement core in y direction. + s (float): Spacing of confinement reinforcement. + + Returns: + float: Confinement stress. + """ + if A_s_conf < 0: + raise ValueError( + f'A_s_conf must be non-negative. Got {A_s_conf} instead.' + ) + if f_yd < 0: + raise ValueError(f'f_yd must be non-negative. Got {f_yd} instead.') + if b_csx < 0: + raise ValueError(f'b_csx must be non-negative. Got {b_csx} instead.') + if b_csy < 0: + raise ValueError(f'b_csy must be non-negative. Got {b_csy} instead.') + if s < 0: + raise ValueError(f's must be non-negative. Got {s} instead.') + + return 2 * A_s_conf * f_yd / (max(b_csx, b_csy) * s) + + +def confinement_sigma_c2d_multiple( + A_s_confx: ArrayLike, + A_s_confy: ArrayLike, + f_yd: float, + b_csx: float, + b_csy: float, + s: float, +) -> float: + """Calculate the confinement stress (sigma_c2d) for members with multiple + confinement reinforcement. + + EN1992-1-1:2023 Eq. (8.13). + + Args: + A_s_confx (ArrayLike): Cross-sectional areas of confinement + reinforcement in x direction in mm2. + A_s_confy (ArrayLike): Cross-sectional areas of confinement + reinforcement in y direction in mm2. + f_yd (float): Yield strength of reinforcement in MPa. + b_csx (float): Width of the confinement core in x direction in mm. + b_csy (float): Width of the confinement core in y direction in mm. + s (float): Spacing of confinement reinforcement in mm. + + Returns: + float: Confinement stress in mm. + """ + A_s_confx = np.atleast_1d(A_s_confx) + A_s_confy = np.atleast_1d(A_s_confy) + + if np.any(A_s_confx < 0): + raise ValueError(f'A_s_confx must be non-negative. Got {A_s_confx}') + if np.any(A_s_confy < 0): + raise ValueError(f'A_s_confy must be non-negative. Got {A_s_confy}') + + if f_yd < 0: + raise ValueError(f'f_yd must be non-negative. Got {f_yd} instead.') + if b_csx < 0: + raise ValueError(f'b_csx must be non-negative. Got {b_csx} instead.') + if b_csy < 0: + raise ValueError(f'b_csy must be non-negative. Got {b_csy} instead.') + if s < 0: + raise ValueError(f's must be non-negative. Got {s} instead.') + + return min((sum(A_s_confx) / b_csy), (sum(A_s_confy) / b_csx)) * f_yd / s + + +def confinement_sigma_c2d_compression_zones( + A_s_confx: ArrayLike, + A_s_confy: ArrayLike, + f_yd: float, + b_csy: float, + x_cs: float, + s: float, +) -> float: + """Calculate the confinement stress (sigma_c2d) for compression zones. + + EN1992-1-1:2023 Eq. (8.14). + + Args: + A_s_confx (ArrayLike): Cross-sectional area of confinement + reinforcement in x direction in mm2. + A_s_confy (ArrayLike): Cross-sectional area of confinement + reinforcement in y direction in mm2. + f_yd (float): Yield strength of reinforcement in MPa. + b_csy (float): Width of the confinement core in mm. + x_cs (float): Width of the confinement core in mm. + s (float): Spacing of confinement reinforcement in mm. + + Returns: + float: Confinement stress in mm2. + """ + A_s_confx = np.atleast_1d(A_s_confx) + A_s_confy = np.atleast_1d(A_s_confy) + + if np.any(A_s_confx < 0): + raise ValueError(f'A_s_confx must be non-negative. Got {A_s_confx}') + if np.any(A_s_confy < 0): + raise ValueError(f'A_s_confy must be non-negative. Got {A_s_confy}') + + if f_yd < 0: + raise ValueError(f'f_yd must be non-negative. Got {f_yd} instead.') + if x_cs < 0: + raise ValueError(f'x_cs must be non-negative. Got {x_cs} instead.') + if b_csy < 0: + raise ValueError(f'b_csy must be non-negative. Got {b_csy} instead.') + if s < 0: + raise ValueError(f's must be non-negative. Got {s} instead.') + + return min((sum(A_s_confx) / b_csy), (sum(A_s_confy) / x_cs)) * f_yd / s + + +def fcd_c( + fcd: float, kconf_b: float, kconf_s: float, delta_fcd: float +) -> float: + """Calculate the average concrete strength increase in the confined areas. + + EN1992-1-1:2023 Eq. (8.15). + + Args: + fcd (float): The concrete design strength in MPa. + kconf_b (float): Effectiveness factor for the shape of the compression + zone and confinement reinforcement (non-dimensional). + kconf_s (float): Effectiveness factor for the spacing of the + confinement reinforcement (non-dimensional). + delta_fcd (float): The increase in concrete design strength due to + confinement in MPa. + + Returns: + float: The average concrete strength increase in MPa. + """ + if fcd < 0: + raise ValueError(f'fcd must be non-negative. Got {fcd} instead.') + if kconf_b < 0: + raise ValueError( + f'kconf_b must be non-negative. Got {kconf_b} instead.' + ) + if kconf_s < 0: + raise ValueError( + f'kconf_s must be non-negative. Got {kconf_s} instead.' + ) + if delta_fcd < 0: + raise ValueError( + f'delta_fcd must be non-negative. Got {delta_fcd} instead.' + ) + + return fcd + kconf_b * kconf_s * delta_fcd + + +def kconf_b_square_single(bcs: float, b: float) -> float: + """Calculate the kconf_b effectiveness factor for square members in + compression with single confinement reinforcement. + + EN1992-1-1:2023 Table (8.1). + + Args: + bcs (float): Width of the confined section in mm. + b (float): Width of the unconfined section in mm. + + Returns: + float: The effectiveness factor kconf_b (non-dimensional). + """ + if bcs < 0: + raise ValueError(f'bcs must be non-negative. Got {bcs} instead.') + if b < 0: + raise ValueError(f'b must be non-negative. Got {b} instead.') + + return (1 / 3) * (bcs / b) ** 2 + + +def kconf_s_square_single(s: float, bcs: float) -> float: + """Calculate the kconf_s effectiveness factor for square members in + compression with single confinement reinforcement. + + EN1992-1-1:2023 Table (8.1). + + Args: + s (float): Spacing of the confinement reinforcement in mm. + bcs (float): Width of the confined section in mm. + + Returns: + float: The effectiveness factor kconf_s (non-dimensional). + """ + if s < 0: + raise ValueError(f's must be non-negative. Got {s} instead.') + if bcs < 0: + raise ValueError(f'bcs must be non-negative. Got {bcs} instead.') + + return (max(1 - s / (2 * bcs), 0)) ** 2 + + +def kconf_b_circular(bcs: float, b: float) -> float: + """Calculate the kconf_b effectiveness factor for circular members in + compression with circular confinement reinforcement. + + EN1992-1-1:2023 Table (8.1). + + Args: + bcs (float): Diameter of the confined section in mm. + b (float): Diameter of the unconfined section in mm. + + Returns: + float: The effectiveness factor kconf_b (non-dimensional). + """ + if bcs < 0: + raise ValueError(f'bcs must be non-negative. Got {bcs} instead.') + if b < 0: + raise ValueError(f'b must be non-negative. Got {b} instead.') + + return (bcs / b) ** 2 + + +def kconf_b_multiple( + bcsx: float, bcsy: float, b_i: ArrayLike, bx: float, by: float +) -> float: + """Calculate the kconf_b effectiveness factor for square and rectangular + members in compression with multiple confinement reinforcement. + + EN1992-1-1:2023 Table (8.1). + + Args: + bcsx (float): Width of the confined section in x-direction in mm. + bcsy (float): Width of the confined section in y-direction in mm. + b_i (ArrayLike): Distances between bends of straight segments in mm. + bx (float): Width of the unconfined section in x-direction in mm. + by (float): Width of the unconfined section in y-direction in mm. + + Returns: + float: The effectiveness factor kconf_b. + """ + b_i = np.atleast_1d(b_i) + if bcsx < 0: + raise ValueError(f'bcsx must be non-negative. Got {bcsx} instead.') + if bcsy < 0: + raise ValueError(f'bcsy must be non-negative. Got {bcsy} instead.') + if np.any(b_i < 0): + raise ValueError(f'b_i must be non-negative. Got {b_i} instead.') + if bx < 0: + raise ValueError(f'bx must be non-negative. Got {bx} instead.') + if by < 0: + raise ValueError(f'by must be non-negative. Got {by} instead.') + + sq_sum = np.sum(b_i**2) + + return (bcsx * bcsy - (1 / 6) * sq_sum) / (bx * by) + + +def kconf_s_multiple(s: float, bcsx: float, bcsy: float) -> float: + """Calculate the kconf_s effectiveness factor for square and rectangular + members in compression with multiple confinement reinforcement. + + EN1992-1-1:2023 Table (8.1). + + Args: + s (float): Spacing of the confinement reinforcement in mm. + bcsx (float): Width of the confined section in x-direction in mm. + bcsy (float): Width of the confined section in y-direction in mm. + + Returns: + float: The effectiveness factor kconf_s. + """ + if s < 0: + raise ValueError(f's must be non-negative. Got {s} instead.') + if bcsx < 0: + raise ValueError(f'bcsx must be non-negative. Got {bcsx} instead.') + if bcsy < 0: + raise ValueError(f'bcsy must be non-negative. Got {bcsy} instead.') + + return max((1 - s / (2 * bcsx)), 0) * max((1 - s / (2 * bcsy)), 0) + + +def kconf_b_bending(Ac_conf: float, Acc: float, b_i: ArrayLike) -> float: + """Calculate the kconf_b effectiveness factor for compression zones due to + bending and axial force. + + EN1992-1-1:2023 Table (8.1). + + Args: + Ac_conf (float): Confined area within the centrelines of the + confinement reinforcement and the neutral axis in mm2. + Acc (float): Compressive area mm2. + b_i (ArrayLike): Distances between bends of straight segments in mm. + + Returns: + float: The effectiveness factor kconf_b. + """ + if Ac_conf < 0: + raise ValueError( + f'Ac_conf must be non-negative. Got {Ac_conf} instead.' + ) + if Acc < 0: + raise ValueError(f'Acc must be non-negative. Got {Acc} instead.') + b_i = np.atleast_1d(b_i) + if np.any(b_i < 0): + raise ValueError(f'b_i must be non-negative. Got {b_i} instead.') + + sq_sum = np.sum(b_i**2) + + return (Ac_conf - (1 / 6) * sq_sum) / Acc + + +def kconf_s_bending(s: float, xcs: float, bcsx: float, bcsy: float) -> float: + """Calculate the kconf_s effectiveness factor for compression zones + under bending and axial force. + + Based on EN1992-1-1:2023 Table (8.1). + + Args: + s (float): Spacing of the confinement reinforcement. + xcs (float): Shortest perpendicular distance from the confined + reinforcement to the neutral axis. + bcsx (float): Width of the total confined section. + bcsy (float): Width of the confined section + delimited by the neutral axis and confinement reinforcement. + + Returns: + float: The effectiveness factor kconf_s (dimensionless). + """ + if s < 0: + raise ValueError(f's must be non-negative. Got {s} instead.') + if xcs < 0: + raise ValueError(f'xcs must be non-negative. Got {xcs} instead.') + if bcsx < 0: + raise ValueError(f'bcsx must be non-negative. Got {bcsx} instead.') + if bcsy < 0: + raise ValueError(f'bcsy must be non-negative. Got {bcsy} instead.') + + xcs = min(xcs, bcsx / 2) + return max((1 - s / (4 * xcs)), 0) * max((1 - s / (2 * bcsy)), 0) + + +def epsc2_c(epsc2: float, delta_fcd: float, fcd: float) -> float: + """Calculate the confined concrete strain limit at maximum stress. + + EN1992-1-1:2023 Eq. (8.16). + + Args: + epsc2 (float): The strain limit at maximum stress for unconfined + concrete. + delta_fcd (float): The increase in concrete design strength due to + confinement in MPa. + fcd (float): The concrete design strength in MPa. + + Returns: + float: The confined concrete strain limit at maximum stress. + """ + if epsc2 < 0: + raise ValueError(f'epsc2 must be non-negative. Got {epsc2} instead.') + if delta_fcd < 0: + raise ValueError( + f'delta_fcd must be non-negative. Got {delta_fcd} instead.' + ) + if fcd < 0: + raise ValueError(f'fcd must be non-negative. Got {fcd} instead.') + + return epsc2 * (1 + 5 * delta_fcd / fcd) + + +def epscu_c(epscu: float, sigma_c2d: float, fcd: float) -> float: + """Calculate the confined concrete ultimate strain limit. + + EN1992-1-1:2023 Eq. (8.17). + + Args: + epscu (float): The ultimate strain limit for unconfined concrete. + sigma_c2d (float): The stress at maximum strain for confined concrete + in MPa. + fcd (float): The concrete design strength in MPa. + + Returns: + float: The confined concrete ultimate strain limit. + """ + if epscu < 0: + raise ValueError(f'epscu must be non-negative. Got {epscu} instead.') + if sigma_c2d < 0: + raise ValueError( + f'sigma_c2d must be non-negative. Got {sigma_c2d} instead.' + ) + if fcd < 0: + raise ValueError(f'fcd must be non-negative. Got {fcd} instead.') + + return epscu + 0.2 * sigma_c2d / fcd diff --git a/tests/test_ec2_2023/test_ec2_2023_section_8_1_bending.py b/tests/test_ec2_2023/test_ec2_2023_section_8_1_bending.py new file mode 100644 index 00000000..d6b7a838 --- /dev/null +++ b/tests/test_ec2_2023/test_ec2_2023_section_8_1_bending.py @@ -0,0 +1,659 @@ +"""EC2-2023 Section 8.1 Bending Test.""" + +from typing import List + +import pytest + +from structuralcodes.codes.ec2_2023 import _section_8_1_bending + + +@pytest.mark.parametrize( + 'h, NEd, expected', + [ + (3000, 500, 50.0), + (600, 1000, 20.0), + (450, 250, 5.0), + (1500, 800, 40.0), + (2000, 300, 20.0), + ], +) +def test_MEd_min(h, NEd, expected): + """Test minimum beding due to imperfections.""" + assert _section_8_1_bending.MEd_min(h, NEd) == pytest.approx( + expected, rel=1e-2 + ) + + +@pytest.mark.parametrize( + 'h, expected', + [ + (600, 0.02), + (900, 0.03), + (3000, 0.1), + (30, 0.02), + (1500, 0.05), + (20, 0.02), + ], +) +def test_e_min(h, expected): + """Test minimum eccentricity for geometric imperfections.""" + assert _section_8_1_bending.e_min(h) == pytest.approx(expected, rel=1e-5) + + +@pytest.mark.parametrize( + 'Ac, fcd, As, fyd, expected', + [ + (10000, 30, 1000, 500, 800), + (20000, 25, 1500, 400, 1100), + (15000, 20, 2000, 450, 1200), + (25000, 40, 2500, 600, 2500), + (5000, 50, 500, 250, 375), + ], +) +def test_NRd0(Ac, fcd, As, fyd, expected): + """Test resistance compressive strength.""" + assert _section_8_1_bending.NRd0(Ac, fcd, As, fyd) == pytest.approx( + expected, rel=1e-2 + ) + + +@pytest.mark.parametrize( + 'Ac, fcd, As, fyd', + [ + (-10000, 30, 1000, 500), # Negative concrete area + (10000, -30, 1000, 500), # Negative concrete resistance + (10000, 30, -1000, 500), # Negative steel area + (10000, 30, 1000, -500), # Negative steel resistance + ], +) +def test_NRd0_errors(Ac, fcd, As, fyd): + """Test resistance compressive strength with negative values.""" + with pytest.raises(ValueError): + _section_8_1_bending.NRd0(Ac, fcd, As, fyd) + + +@pytest.mark.parametrize( + 'MEdz_MRdz, MEdy_MRdy, Ned_NRd, section_type, expected', + [ + # Cases for elliptical and circular sections + (0.5, 0.5, 0.5, 'circular', 0.5**2 + 0.5**2), # an = 2.0 + (0.3, 0.4, 0.6, 'elliptical', 0.3**2 + 0.4**2), # an = 2.0 + # Cases for rectangular sections with different Ned_NRd + ( + 0.2, + 0.3, + 0.1, + 'rectangular', + 0.2**1.0 + 0.3**1.0, + ), # an = 1.0 (Ned_NRd <= 0.1) + ( + 0.3, + 0.4, + 1.0, + 'rectangular', + 0.3706, + ), # an = 2.0 (Ned_NRd >= 1.0) + ( + 0.1, + 0.2, + 0.5, + 'rectangular', + 0.1633, + ), # an interpolated between 1.0 and 2.0 + ], +) +def test_biaxial_resistant_ratio( + MEdz_MRdz, MEdy_MRdy, Ned_NRd, section_type, expected +): + """Computes the biaxial resistant ratio.""" + result = _section_8_1_bending.biaxial_resistant_ratio( + MEdz_MRdz, MEdy_MRdy, Ned_NRd, section_type + ) + assert result == pytest.approx(expected, rel=1e-2) + + +@pytest.mark.parametrize( + 'fcd, eps_c, expected', + [ + (30.0, 0.0, 0.0), # eps_c <= 0 + (30.0, 0.001, 22.5), # eps_c <= 0.002 + (30.0, 0.002, 30.0), # eps_c == 0.002 + (30.0, 0.003, 30.0), # 0.002 < eps_c <= 0.0035 + (30.0, 0.0035, 30.0), # eps_c == 0.0035 + ], +) +def test_sigma_cd(fcd, eps_c, expected): + """Tests the concrete stress calculation.""" + assert _section_8_1_bending.sigma_cd(fcd, eps_c) == pytest.approx( + expected, rel=1e-2 + ) + + +@pytest.mark.parametrize( + 'fcd, eps_c', + [ + (30.0, 0.004), # eps_c > 0.0035 + ], +) +def test_sigma_cd_errors(fcd, eps_c): + """Tests the compressive stress concrete calculation raises Errors.""" + with pytest.raises(ValueError): + _section_8_1_bending.sigma_cd(fcd, eps_c) + + +@pytest.mark.parametrize( + 'sigma_c2d, f_cd, ddg, expected', + [ + (10.0, 30.0, 32.0, 40.0), # Test case with sigma_c2d <= 0.6 * f_cd + (20.0, 30.0, 32.0, 77.467), # Test case with sigma_c2d > 0.6 * f_cd + (10.0, 30.0, 16.0, 20.0), # Test case with ddg < 32 + ], +) +def test_delta_fcd_confined( + sigma_c2d: float, f_cd: float, ddg: float, expected: float +): + """Test delta_fcd_confined.""" + result = _section_8_1_bending.delta_fcd_confined(sigma_c2d, f_cd, ddg) + assert pytest.approx(result, 0.01) == expected + + +@pytest.mark.parametrize( + 'sigma_c2d, f_cd, ddg', + [ + (-1.0, 30.0, 32.0), # Negative sigma_c2d + (10.0, -30.0, 32.0), # Negative f_cd + (10.0, 30.0, -32.0), # Negative ddg + ], +) +def test_delta_fcd_confined_raises_value_error(sigma_c2d, f_cd, ddg): + """Test delta_fcd_confined raises ValueError for negative inputs.""" + with pytest.raises(ValueError): + _section_8_1_bending.delta_fcd_confined(sigma_c2d, f_cd, ddg) + + +@pytest.mark.parametrize( + 'A_s_conf, f_yd, b_cs, s, expected', + [ + (500.0, 500.0, 200.0, 150.0, 16.67), + ], +) +def test_confinement_sigma_c2d_circular_square( + A_s_conf: float, f_yd: float, b_cs: float, s: float, expected: float +): + """Test confinement_sigma_c2d_circular_square.""" + result = _section_8_1_bending.confinement_sigma_c2d_circular_square( + A_s_conf, f_yd, b_cs, s + ) + assert pytest.approx(result, 0.01) == expected + + +@pytest.mark.parametrize( + 'A_s_conf, f_yd, b_cs, s', + [ + (-1.0, 500.0, 200.0, 150.0), + (500.0, -500.0, 200.0, 150.0), + (500.0, 500.0, -200.0, 150.0), + (500.0, 500.0, 200.0, -150.0), + ], +) +def test_confinement_sigma_c2d_circular_square_raises_value_error( + A_s_conf, f_yd, b_cs, s +): + """Test ValueError is raised for negative inputs.""" + with pytest.raises(ValueError): + _section_8_1_bending.confinement_sigma_c2d_circular_square( + A_s_conf, f_yd, b_cs, s + ) + + +@pytest.mark.parametrize( + 'A_s_conf, f_yd, b_csx, b_csy, s, expected', + [ + (500.0, 500.0, 250.0, 300.0, 150.0, 11.11), + ], +) +def test_confinement_sigma_c2d_rectangular( + A_s_conf: float, + f_yd: float, + b_csx: float, + b_csy: float, + s: float, + expected: float, +): + """Test test_confinement_sigma_c2d_rectangular.""" + result = _section_8_1_bending.confinement_sigma_c2d_rectangular( + A_s_conf, f_yd, b_csx, b_csy, s + ) + assert pytest.approx(result, 0.01) == expected + + +@pytest.mark.parametrize( + 'A_s_conf, f_yd, b_csx, b_csy, s', + [ + (-1.0, 500.0, 250.0, 300.0, 150.0), + (500.0, -500.0, 250.0, 300.0, 150.0), + (500.0, 500.0, -250.0, 300.0, 150.0), + (500.0, 500.0, 250.0, -300.0, 150.0), + (500.0, 500.0, 250.0, 300.0, -150.0), + ], +) +def test_confinement_sigma_c2d_rectangular_raises_value_error( + A_s_conf, f_yd, b_csx, b_csy, s +): + """Test ValueError is raised for negative inputs.""" + with pytest.raises(ValueError): + _section_8_1_bending.confinement_sigma_c2d_rectangular( + A_s_conf, f_yd, b_csx, b_csy, s + ) + + +@pytest.mark.parametrize( + 'A_s_confx, A_s_confy, f_yd, b_csx, b_csy, s, expected', + [ + ([100.0, 100.0], [100.0, 100.0], 500.0, 250.0, 300.0, 150.0, 2.22), + ], +) +def test_confinement_sigma_c2d_multiple( + A_s_confx: List[float], + A_s_confy: List[float], + f_yd: float, + b_csx: float, + b_csy: float, + s: float, + expected: float, +): + """Test test_confinement_sigma_c2d_multiple.""" + result = _section_8_1_bending.confinement_sigma_c2d_multiple( + A_s_confx, A_s_confy, f_yd, b_csx, b_csy, s + ) + assert pytest.approx(result, 0.01) == expected + + +@pytest.mark.parametrize( + 'A_s_confx, A_s_confy, f_yd, b_csx, b_csy, s', + [ + ([-100.0, 100.0], [100.0, 100.0], 500.0, 250.0, 300.0, 150.0), + ([100.0, 100.0], [-100.0, 100.0], 500.0, 250.0, 300.0, 150.0), + ([100.0, 100.0], [100.0, 100.0], -500.0, 250.0, 300.0, 150.0), + ([100.0, 100.0], [100.0, 100.0], 500.0, -250.0, 300.0, 150.0), + ([100.0, 100.0], [100.0, 100.0], 500.0, 250.0, -300.0, 150.0), + ([100.0, 100.0], [100.0, 100.0], 500.0, 250.0, 300.0, -150.0), + ], +) +def test_confinement_sigma_c2d_multiple_raises_value_error( + A_s_confx, A_s_confy, f_yd, b_csx, b_csy, s +): + """Test ValueError is raised for negative inputs.""" + with pytest.raises(ValueError): + _section_8_1_bending.confinement_sigma_c2d_multiple( + A_s_confx, A_s_confy, f_yd, b_csx, b_csy, s + ) + + +@pytest.mark.parametrize( + 'A_s_confx, A_s_confy, f_yd, b_csy, b_csx, s, expected', + [ + ([100.0, 100.0], [100.0, 100.0], 500.0, 250.0, 300.0, 150.0, 2.22), + ], +) +def test_confinement_sigma_c2d_compression_zones( + A_s_confx: float, + A_s_confy: float, + f_yd: float, + b_csy: float, + b_csx: float, + s: float, + expected: float, +): + """Test confinement_sigma_c2d_compression_zones.""" + result = _section_8_1_bending.confinement_sigma_c2d_compression_zones( + A_s_confx, A_s_confy, f_yd, b_csy, b_csx, s + ) + assert pytest.approx(result, 0.01) == expected + + +@pytest.mark.parametrize( + 'A_s_confx, A_s_confy, f_yd, b_csy, x_cs, s', + [ + ([-100.0, 100.0], [100.0, 100.0], 500.0, 250.0, 300.0, 150.0), + ([100.0, 100.0], [-100.0, 100.0], 500.0, 250.0, 300.0, 150.0), + ([100.0, 100.0], [100.0, 100.0], -500.0, 250.0, 300.0, 150.0), + ([100.0, 100.0], [100.0, 100.0], 500.0, -250.0, 300.0, 150.0), + ([100.0, 100.0], [100.0, 100.0], 500.0, 250.0, -300.0, 150.0), + ([100.0, 100.0], [100.0, 100.0], 500.0, 250.0, 300.0, -150.0), + ], +) +def test_confinement_sigma_c2d_compression_zones_raises_value_error( + A_s_confx, A_s_confy, f_yd, b_csy, x_cs, s +): + """Test ValueError is raised for negative inputs.""" + with pytest.raises(ValueError): + _section_8_1_bending.confinement_sigma_c2d_compression_zones( + A_s_confx, A_s_confy, f_yd, b_csy, x_cs, s + ) + + +@pytest.mark.parametrize( + 'fcd, kconf_b, kconf_s, delta_fcd, expected', + [ + (30, 0.5, 0.6, 5, 30 + 0.5 * 0.6 * 5), + (40, 0.7, 0.8, 10, 40 + 0.7 * 0.8 * 10), + (25, 1, 1, 15, 25 + 1 * 1 * 15), + ], +) +def test_fcd_c(fcd, kconf_b, kconf_s, delta_fcd, expected): + """Test fcd_c.""" + assert _section_8_1_bending.fcd_c( + fcd, kconf_b, kconf_s, delta_fcd + ) == pytest.approx(expected, rel=1e-5) + + +@pytest.mark.parametrize( + 'fcd, kconf_b, kconf_s, delta_fcd', + [ + (-10, 0.5, 0.6, 5), + (30, -0.5, 0.6, 5), + (30, 0.5, -0.6, 5), + (30, 0.5, 0.6, -5), + ], +) +def test_fcd_c_raises_value_error(fcd, kconf_b, kconf_s, delta_fcd): + """Test fcd_c_raises_value_error.""" + with pytest.raises(ValueError): + _section_8_1_bending.fcd_c(fcd, kconf_b, kconf_s, delta_fcd) + + +@pytest.mark.parametrize( + 'bcs, b, expected', + [ + (100, 200, (1 / 3) * (100 / 200) ** 2), + (150, 300, (1 / 3) * (150 / 300) ** 2), + (200, 400, (1 / 3) * (200 / 400) ** 2), + ], +) +def test_kconf_b_square_single(bcs, b, expected): + """Test kconf_b_square_single.""" + assert _section_8_1_bending.kconf_b_square_single(bcs, b) == pytest.approx( + expected, rel=1e-5 + ) + + +@pytest.mark.parametrize( + 'bcs, b', + [ + (-100, 200), + (100, -200), + ], +) +def test_kconf_b_square_single_raises_value_error(bcs, b): + """Test kconf_b_square_single_raises_value_error.""" + with pytest.raises(ValueError): + _section_8_1_bending.kconf_b_square_single(bcs, b) + + +@pytest.mark.parametrize( + 's, bcs, expected', + [ + (50, 200, max(1 - 50 / (2 * 200), 0) ** 2), + (30, 150, max(1 - 30 / (2 * 150), 0) ** 2), + (20, 100, max(1 - 20 / (2 * 100), 0) ** 2), + ], +) +def test_kconf_s_square_single(s, bcs, expected): + """Test kconf_s_square_single.""" + assert _section_8_1_bending.kconf_s_square_single(s, bcs) == pytest.approx( + expected, rel=1e-5 + ) + + +@pytest.mark.parametrize( + 's, bcs', + [ + (-50, 200), + (50, -200), + ], +) +def test_kconf_s_square_single_raises_value_error(s, bcs): + """Test kconf_s_square_single_raises_value_error.""" + with pytest.raises(ValueError): + _section_8_1_bending.kconf_s_square_single(s, bcs) + + +@pytest.mark.parametrize( + 'bcs, b, expected', + [ + (100, 200, (100 / 200) ** 2), + (150, 300, (150 / 300) ** 2), + (200, 400, (200 / 400) ** 2), + ], +) +def test_kconf_b_circular(bcs, b, expected): + """Test kconf_b_circular.""" + assert _section_8_1_bending.kconf_b_circular(bcs, b) == pytest.approx( + expected, rel=1e-5 + ) + + +@pytest.mark.parametrize( + 'bcs, b', + [ + (-100, 200), + (100, -200), + ], +) +def test_kconf_b_circular_raises_value_error(bcs, b): + """Test kconf_b_circular_raises_value_error.""" + with pytest.raises(ValueError): + _section_8_1_bending.kconf_b_circular(bcs, b) + + +@pytest.mark.parametrize( + 'bcsx, bcsy, b_i, bx, by, expected', + [ + ( + 100, + 200, + [50, 50], + 400, + 400, + (100 * 200 - (1 / 6) * (50**2 + 50**2)) / (400 * 400), + ), + ( + 150, + 250, + [75, 75], + 500, + 500, + (150 * 250 - (1 / 6) * (75**2 + 75**2)) / (500 * 500), + ), + ], +) +def test_kconf_b_multiple(bcsx, bcsy, b_i, bx, by, expected): + """Test kconf_b_multiple.""" + assert _section_8_1_bending.kconf_b_multiple( + bcsx, bcsy, b_i, bx, by + ) == pytest.approx(expected, rel=1e-5) + + +@pytest.mark.parametrize( + 'bcsx, bcsy, b_i, bx, by', + [ + (-100, 200, [50, 50], 400, 400), + (100, -200, [50, 50], 400, 400), + (100, 200, [-50, 50], 400, 400), + (100, 200, [50, 50], -400, 400), + (100, 200, [50, 50], 400, -400), + ], +) +def test_kconf_b_multiple_raises_value_error(bcsx, bcsy, b_i, bx, by): + """Test kconf_b_multiple_raises_value_error.""" + with pytest.raises(ValueError): + _section_8_1_bending.kconf_b_multiple(bcsx, bcsy, b_i, bx, by) + + +@pytest.mark.parametrize( + 's, bcsx, bcsy, expected', + [ + ( + 50, + 200, + 200, + max((1 - 50 / (2 * 200)), 0) * max((1 - 50 / (2 * 200)), 0), + ), + ( + 30, + 150, + 150, + max((1 - 30 / (2 * 150)), 0) * max((1 - 30 / (2 * 150)), 0), + ), + ], +) +def test_kconf_s_multiple(s, bcsx, bcsy, expected): + """Test kconf_s_multiple.""" + assert _section_8_1_bending.kconf_s_multiple( + s, bcsx, bcsy + ) == pytest.approx(expected, rel=1e-5) + + +@pytest.mark.parametrize( + 's, bcsx, bcsy', + [ + (-50, 200, 200), + (50, -200, 200), + (50, 200, -200), + ], +) +def test_kconf_s_multiple_raises_value_error(s, bcsx, bcsy): + """Test kconf_s_multiple_raises_value_error.""" + with pytest.raises(ValueError): + _section_8_1_bending.kconf_s_multiple(s, bcsx, bcsy) + + +@pytest.mark.parametrize( + 'Ac_conf, Acc, b_i, expected', + [ + (50000, 10000, [50, 50], (50000 - (1 / 6) * (50**2 + 50**2)) / 10000), + ( + 100000, + 20000, + [75, 75], + (100000 - (1 / 6) * (75**2 + 75**2)) / 20000, + ), + ], +) +def test_kconf_b_bending(Ac_conf, Acc, b_i, expected): + """Test kconf_b_bending.""" + assert _section_8_1_bending.kconf_b_bending( + Ac_conf, Acc, b_i + ) == pytest.approx(expected, rel=1e-5) + + +@pytest.mark.parametrize( + 'Ac_conf, Acc, b_i', + [ + (-50000, 10000, [50, 50]), + (50000, -10000, [50, 50]), + (50000, 10000, [-50, 50]), + ], +) +def test_kconf_b_bending_raises_value_error(Ac_conf, Acc, b_i): + """Test kconf_b_bending_raises_value_error.""" + with pytest.raises(ValueError): + _section_8_1_bending.kconf_b_bending(Ac_conf, Acc, b_i) + + +@pytest.mark.parametrize( + 's, xcs, bcsx, bcsy, expected', + [ + ( + 50, + 100, + 200, + 200, + max((1 - 50 / (4 * 100)), 0) * max((1 - 50 / (2 * 200)), 0), + ), + ( + 30, + 75, + 150, + 150, + max((1 - 30 / (4 * 75)), 0) * max((1 - 30 / (2 * 150)), 0), + ), + ], +) +def test_kconf_s_bending(s, xcs, bcsx, bcsy, expected): + """Test kconf_s_bending.""" + assert _section_8_1_bending.kconf_s_bending( + s, xcs, bcsx, bcsy + ) == pytest.approx(expected, rel=1e-5) + + +@pytest.mark.parametrize( + 's, xcs, bcsx, bcsy', + [ + (-50, 100, 200, 200), + (50, -100, 200, 200), + (50, 100, -200, 200), + (50, 100, 200, -200), + ], +) +def test_kconf_s_bending_raises_value_error(s, xcs, bcsx, bcsy): + """Test kconf_s_bending_raises_value_error.""" + with pytest.raises(ValueError): + _section_8_1_bending.kconf_s_bending(s, xcs, bcsx, bcsy) + + +@pytest.mark.parametrize( + 'epsc2, delta_fcd, fcd, expected', + [ + (0.002, 5, 30, 0.002 * (1 + 5 * 5 / 30)), + (0.003, 10, 40, 0.003 * (1 + 5 * 10 / 40)), + ], +) +def test_epsc2_c(epsc2, delta_fcd, fcd, expected): + """Test epsc2_c.""" + assert _section_8_1_bending.epsc2_c( + epsc2, delta_fcd, fcd + ) == pytest.approx(expected, rel=1e-5) + + +@pytest.mark.parametrize( + 'epsc2, delta_fcd, fcd', + [ + (-0.002, 5, 30), + (0.002, -5, 30), + (0.002, 5, -30), + ], +) +def test_epsc2_c_raises_value_error(epsc2, delta_fcd, fcd): + """Test epsc2_c_raises_value_error.""" + with pytest.raises(ValueError): + _section_8_1_bending.epsc2_c(epsc2, delta_fcd, fcd) + + +@pytest.mark.parametrize( + 'epscu, sigma_c2d, fcd, expected', + [ + (0.003, 5, 30, 0.003 + 0.2 * 5 / 30), + (0.004, 10, 40, 0.004 + 0.2 * 10 / 40), + ], +) +def test_epscu_c(epscu, sigma_c2d, fcd, expected): + """Test epscu_c.""" + assert _section_8_1_bending.epscu_c( + epscu, sigma_c2d, fcd + ) == pytest.approx(expected, rel=1e-5) + + +@pytest.mark.parametrize( + 'epscu, sigma_c2d, fcd', + [ + (-0.003, 5, 30), + (0.003, -5, 30), + (0.003, 5, -30), + ], +) +def test_epscu_c_raises_value_error(epscu, sigma_c2d, fcd): + """Test epscu_c_raises_value_error.""" + with pytest.raises(ValueError): + _section_8_1_bending.epscu_c(epscu, sigma_c2d, fcd)