Repository: fib-international/structuralcodes (language: python)

Issue:
feat: Ec2 2023 section9 sls
First partial version of Section 9 or FprEN 1992-1-1:2022


Discussion / hints:

Fix patch:
diff --git a/structuralcodes/codes/ec2_2023/__init__.py b/structuralcodes/codes/ec2_2023/__init__.py
new file mode 100644
index 00000000..affc7d00
--- /dev/null
+++ b/structuralcodes/codes/ec2_2023/__init__.py
@@ -0,0 +1,72 @@
+"""EUROCODE 2 1992-1-1:2023."""
+import typing as t
+
+from ._section5_materials import (
+    A_phi_correction_exp,
+    alpha_c_th,
+    alpha_s_th,
+    Ecm,
+    eps_c1,
+    eps_cs_50y,
+    eps_cu1,
+    eps_ud,
+    Es,
+    eta_cc,
+    fcd,
+    fcm,
+    fctd,
+    fctk_5,
+    fctk_95,
+    fctm,
+    fpd,
+    fyd,
+    hn,
+    k_tc,
+    k_tt,
+    p_steel_stress_params,
+    phi_50y_t0,
+    phi_correction_factor,
+    r_steel_stress_strain_params,
+    sigma_c,
+    sigma_p,
+    sigma_s,
+    weight_c,
+    weight_s,
+)
+
+__all__ = [
+    'A_phi_correction_exp',
+    'alpha_c_th',
+    'alpha_s_th',
+    'Ecm',
+    'eps_c1',
+    'eps_cs_50y',
+    'eps_cu1',
+    'eps_ud',
+    'Es',
+    'eta_cc',
+    'fcd',
+    'fcm',
+    'fctd',
+    'fctk_5',
+    'fctk_95',
+    'fctm',
+    'fpd',
+    'fyd',
+    'hn',
+    'k_tc',
+    'k_tt',
+    'p_steel_stress_params',
+    'phi_50y_t0',
+    'phi_correction_factor',
+    'r_steel_stress_strain_params',
+    'sigma_c',
+    'sigma_p',
+    'sigma_s',
+    'weight_c',
+    'weight_s',
+]
+
+__title__: str = 'EUROCODE 2 1992-1-1'
+__year__: str = '2023'
+__materials__: t.Tuple[str] = ('concrete',)
diff --git a/structuralcodes/codes/ec2_2023/_section5_materials.py b/structuralcodes/codes/ec2_2023/_section5_materials.py
new file mode 100644
index 00000000..606681e7
--- /dev/null
+++ b/structuralcodes/codes/ec2_2023/_section5_materials.py
@@ -0,0 +1,1092 @@
+"""Functions from Section 5 of EN 1992-1-1:2023."""
+
+import math
+import typing as t
+import numpy as np
+import scipy.interpolate
+
+from structuralcodes.codes import mc2010
+
+
+def fcm(fck: float, delta_f: float = 8.0) -> float:
+    """Determines the mean strength of concrete from its characteristic value.
+
+    EN 1992-1-1:2023, Table 5.1
+
+    Args:
+        fck (float): is the characteristic compressive strength in MPa.
+
+    Keyword Args:
+        delta_s (float): the increment in MPa to compute the mean
+            compressive strength
+
+    Returns:
+        float: the mean compressive strength in MPa
+    """
+    return mc2010.fcm(fck, delta_f)
+
+
+def fctm(fck: float) -> float:
+    """Compute the mean concrete tensile strength from the characteristic
+    compressive strength.
+
+    EN 1992-1-1:2023, Table 5.1
+
+    Args:
+        fck (float): the characteristic compressive strength in MPa.
+
+    Returns:
+        float: the mean tensile strength in MPa.
+    """
+    if abs(fck) <= 50:
+        return 0.3 * math.pow(abs(fck), 2 / 3)
+    return 1.1 * math.pow(abs(fck), 1 / 3)
+
+
+def fctk_5(_fctm: float) -> float:
+    """Compute the 5% mean concrete tensile strength fractile.
+
+    EN 1992-1-1:2023, Table 5.1
+
+    Args:
+        _fctm (float): the mean concrete tensile strength in MPa
+
+    Returns:
+        float: the 5% mean concrete tensile strength fractile in MPa
+    """
+    return abs(_fctm) * 0.7
+
+
+def fctk_95(_fctm: float) -> float:
+    """Compute the 95% mean concrete tensile strength fractile.
+
+    EN 1992-1-1:2023, Table 5.1
+
+    Args:
+        _fctm (float): the mean concrete tensile strength in MPa
+
+    Returns:
+        float: the 5% mean concrete tensile strength fractile in MPa
+    """
+    return abs(_fctm) * 1.3
+
+
+def Ecm(_fcm: float, kE: float = 9500) -> float:
+    """Computes the secant modulus between sigma_c=0 and sigma_c=0.4*fcm.
+
+    EN 1992-1-1:2023, Eq. (5.1)
+
+    Args:
+        _fcm (float): the mean compressibe strength in MPa
+
+    Keyword Args:
+        kE (float): coefficient relating the aggregates used
+            in concrete. Default value is 9500, but it can vary
+            from 5000 to 13000.
+
+    Returns:
+        float: the secant concrete modulus in MPa
+
+    Raises:
+        ValueError: if _fcm is less than 0
+        ValueError: if kE is not between 5000 and 13000
+    """
+    if _fcm < 0:
+        raise ValueError(f'_fcm={_fcm} cannot be less than 0')
+    if kE < 5000:
+        raise ValueError(f'kE={kE} cannot be less than 5000')
+    if kE > 13000:
+        raise ValueError(f'kE={kE} cannot be larger than 13000')
+
+    return kE * math.pow(_fcm, 1 / 3)
+
+
+def hn(Ac: float, u: float) -> float:
+    """Computes the notional size of a given concrete cross-section.
+
+    EN 1992-1-1:2023, Table 5.2
+
+    Args:
+        Ac (float): the concrete cross-sectional area in (mm2)
+        u (float): the perimeter exposed to drying (mm)
+
+    Returns:
+        float: the notional size (mm)
+
+    Raises:
+        ValueError: if Ac is less than 0
+        ValueError: if u is less than 0
+    """
+    if Ac < 0:
+        raise ValueError(f'Ac={Ac} cannot be less than 0')
+    if u < 0:
+        raise ValueError(f'u={u} cannot be less than 0')
+
+    return 2 * Ac / u
+
+
+def A_phi_correction_exp(_hn: float, atm_conditions: str) -> float:
+    """Computes the correction exponent for the modification for the phi_50y_t0
+    with respect the fck value.
+
+    EN 1992-1-1:2023, Table 5.2
+
+    Args:
+        _hn (float): the notional size in mm
+        atm_conditions (str): 'dry' or 'humid'
+
+    Returns:
+        (float): the correction exponent value
+
+    Raises:
+        ValueError: if _hn is less than 100 or greater than 1000
+        ValueError: if atm_conditions is not 'dry' or 'humid'
+    """
+    if _hn < 100:
+        raise ValueError(f'_hn={_hn} cannot be less than 100')
+    if _hn > 1000:
+        raise ValueError(f'_hn={_hn} cannot be larger than 1000')
+
+    atm_conditions = atm_conditions.lower().strip()
+    x = (100, 200, 500, 1000)
+    if atm_conditions == 'dry':
+        y = (0.82, 0.79, 0.75, 0.72)
+    elif atm_conditions == 'humid':
+        y = (0.71, 0.68, 0.66, 0.64)
+    else:
+        raise ValueError(
+            f'atm_conditions={atm_conditions} can only take '
+            + '"dry" and "humid" as values'
+        )
+
+    interpol = scipy.interpolate.interp1d(x, y)
+    return interpol(_hn)
+
+
+def phi_correction_factor(fck: float, A_exponent: float) -> float:
+    """Computes the correction factor for the computation of the phi_50y_t0.
+
+    EN 1992-1-1:2023able 5.2
+
+    Args:
+        fck (float): characteristic strength of concrete in MPa
+        A_exponent (float): the A correction exponent value
+
+    Returns:
+        float: the correction factor value
+
+    Raises:
+        ValueError: if fck is not between 12 and 100 MPa
+        ValueError: if A_exponent is not between 0.64 and 0.82
+    """
+    if fck < 12:
+        raise ValueError(f'fck={fck} cannot be less than 12')
+    if fck > 100:
+        raise ValueError(f'fck={fck} cannot be larger than 100')
+    if A_exponent < 0.64:
+        raise ValueError(f'A_exponent={A_exponent} cannot be less than 0.64')
+    if A_exponent > 0.82:
+        raise ValueError(f'A_exponent={A_exponent} cannot be less than 0.82')
+
+    return math.pow(35 / fck, A_exponent)
+
+
+def phi_50y_t0(
+    t0: float, atm_conditions: str, _hn: float, concrete_dev_class: str
+) -> float:
+    """Computes the creep coefficient of plain concrete at 50 years of loading.
+    Interpolation is linear between values.
+
+    EN 1992-1-1:2023, Table 5.2
+
+    Args:
+        t0 (float): age at loading [days]
+        atm_conditions (str): 'dry' or 'humid'
+        _hn (float): the notional size in mm
+        concrete_dev_class (str): 'CS', 'CN' or 'CR'
+
+    Returns:
+        float: the creep coefficient
+
+    Raises:
+        ValueError: if t0 is less than 1
+        ValueError: if atm_conditions is not 'dry' or 'humid'
+        ValueError: if _hn is less than 100 or larger than 1000
+        ValueError: if concrete_dev_class is not 'CS', 'CN' or 'CR'
+        ValueError: if combination of t0 and _hn is out of scope
+    """
+    if t0 < 1:
+        raise ValueError(f't0={t0} cannot be less than 1')
+
+    atm_conditions = atm_conditions.lower().strip()
+    if atm_conditions not in ('dry', 'humid'):
+        raise ValueError(
+            f'atm_conditions={atm_conditions} must be "dry" or "humid"'
+        )
+
+    if _hn < 100:
+        raise ValueError(f'_hn={_hn} must be larger or equal than 100')
+    if _hn > 1000:
+        raise ValueError(f'_hn={_hn} must be less or equal than 1000')
+
+    concrete_dev_class = concrete_dev_class.upper().strip()
+    if concrete_dev_class not in ('CS', 'CN', 'CR'):
+        raise ValueError(
+            f'concrete_dev_class={concrete_dev_class} must be'
+            + '"CS", "CN" or "CR"'
+        )
+
+    if concrete_dev_class == 'CS':
+        _t = (3, 10, 32, 91, 365)
+    elif concrete_dev_class == 'CN':
+        _t = (1, 7, 28, 91, 365)
+    elif concrete_dev_class == 'CR':
+        _t = (1, 3, 23, 91, 365)
+
+    h_v = (100, 200, 500, 1000)
+
+    if atm_conditions == 'dry':
+        values = (
+            4.2,
+            3.8,
+            3.4,
+            3.1,
+            3.1,
+            2.8,
+            2.5,
+            2.3,
+            2.4,
+            2.2,
+            1.9,
+            1.8,
+            1.9,
+            1.7,
+            1.5,
+            1.4,
+            1.4,
+            1.3,
+            1.1,
+            1.0,
+        )
+    elif atm_conditions == 'humid':
+        values = (
+            3.0,
+            2.8,
+            2.6,
+            2.5,
+            2.2,
+            2.1,
+            2.0,
+            1.9,
+            1.7,
+            1.6,
+            1.6,
+            1.5,
+            1.4,
+            1.3,
+            1.2,
+            1.2,
+            1.0,
+            0.9,
+            0.9,
+            0.8,
+        )
+
+    grid = np.array(np.meshgrid(_t, h_v)).T.reshape(-1, 2)
+    p = (t0, _hn)
+
+    interp = scipy.interpolate.griddata(grid, values, p, method='linear')
+    _phi_50y_t0 = float(interp)
+
+    if math.isnan(_phi_50y_t0) or math.isnan(_phi_50y_t0):
+        raise ValueError('Combination of t0, _hn out of scope')
+
+    return _phi_50y_t0
+
+
+def eps_cs_50y(
+    fck_28: float, atm_conditions: str, _hn: float, concrete_dev_class: str
+) -> float:
+    """Computes the nominal total shrinkage in ‰ for concrete after a duration
+    of drying of 50 years.
+
+    EN 1992-1-1:2023, Table 5.3
+
+    Args:
+        fck_28 (float): characteristic strength at 28 days in MPa
+        atm_conditions (str): 'dry' or 'humid'
+        _hn (float): the notional size in mm
+        concrete_dev_class (str): 'CS', 'CN' or 'CR'
+
+    Returns:
+        float: the nominal shrinkage value in ‰
+
+    Raises:
+        ValueError: if fck_28 is less than 20 MPa or larger than 80 MPa
+        ValueError: if atm_conditions is not 'dry' or 'humid'
+        ValueError: if _hn is less than 100 or larger than 1000
+        ValueError: if concrete_dev_class is not 'CS', 'CN' or 'CR'
+        ValueError: if combination of fck_28 and _hn is out of scope
+    """
+    if fck_28 < 20:
+        raise ValueError(f'fck_28={fck_28} cannot be less than 20')
+    if fck_28 > 80:
+        raise ValueError(f'fck_28={fck_28} cannot be larger than 80')
+
+    atm_conditions = atm_conditions.lower().strip()
+    if atm_conditions not in ('dry', 'humid'):
+        raise ValueError(
+            f'atm_conditions={atm_conditions} must be "dry" or "humid"'
+        )
+
+    if _hn < 100:
+        raise ValueError(f'_hn={_hn} must be larger or equal than 100')
+    if _hn > 1000:
+        raise ValueError(f'_hn={_hn} must be less or equal than 1000')
+
+    concrete_dev_class = concrete_dev_class.upper().strip()
+    if concrete_dev_class == 'CS':
+        fck_v = (20, 35, 50)
+    elif concrete_dev_class == 'CN':
+        fck_v = (20, 35, 50, 80)
+    elif concrete_dev_class == 'CR':
+        fck_v = (35, 50, 80)
+    else:
+        raise ValueError(
+            f'concrete_dev_class={concrete_dev_class} '
+            + 'must be "CS", "CN" or "CR"'
+        )
+
+    h_v = (100, 200, 500, 1000)
+
+    data = {
+        'dry': {
+            'CS': (
+                0.57,
+                0.56,
+                0.48,
+                0.36,
+                0.53,
+                0.51,
+                0.45,
+                0.35,
+                0.49,
+                0.48,
+                0.43,
+                0.35,
+            ),
+            'CN': (
+                0.67,
+                0.65,
+                0.56,
+                0.41,
+                0.60,
+                0.59,
+                0.51,
+                0.39,
+                0.55,
+                0.54,
+                0.48,
+                0.37,
+                0.48,
+                0.48,
+                0.43,
+                0.36,
+            ),
+            'CR': (
+                0.76,
+                0.74,
+                0.65,
+                0.48,
+                0.67,
+                0.66,
+                0.58,
+                0.44,
+                0.55,
+                0.54,
+                0.49,
+                0.39,
+            ),
+        },
+        'humid': {
+            'CS': (
+                0.33,
+                0.32,
+                0.28,
+                0.21,
+                0.31,
+                0.31,
+                0.27,
+                0.22,
+                0.30,
+                0.29,
+                0.27,
+                0.23,
+            ),
+            'CN': (
+                0.38,
+                0.37,
+                0.32,
+                0.24,
+                0.34,
+                0.34,
+                0.30,
+                0.24,
+                0.31,
+                0.31,
+                0.28,
+                0.23,
+                0.30,
+                0.30,
+                0.28,
+                0.25,
+            ),
+            'CR': (
+                0.42,
+                0.41,
+                0.36,
+                0.28,
+                0.36,
+                0.35,
+                0.32,
+                0.26,
+                0.31,
+                0.30,
+                0.28,
+                0.25,
+            ),
+        },
+    }
+    values = data.get(atm_conditions).get(concrete_dev_class)
+
+    grid = np.array(np.meshgrid(fck_v, h_v)).T.reshape(-1, 2)
+    p = (fck_28, _hn)
+
+    interp = scipy.interpolate.griddata(grid, values, p, method='linear')
+    _eps_cs_50y = float(interp)
+
+    if math.isnan(_eps_cs_50y):
+        raise ValueError('Combination of fck_28, _hn out of scope')
+
+    return _eps_cs_50y
+
+
+def eta_cc(fck: float, fck_ref: float = 40) -> float:
+    """Computes the factor to measure the difference between the undistributed
+    compressibe strength of a cylinder and the effective compressive strength
+    in a structural member.
+
+    EN 1992-1-1:2023, Eq. (5.4)
+
+    Args:
+        fck (float): the characterisitic compressive strength in MPa
+        fck_ref (float, optional): the reference compressive strength MPa
+
+    Returns:
+        float: the value of the factor eta_cc
+
+    Raises:
+        ValueError: if fck is less than 12 MPa
+        ValueError: if fkc_ref is less or equal to 0
+    """
+    if fck < 12:
+        raise ValueError(f'fck={fck} must be larger or equal than 12 MPa')
+    if fck_ref <= 0:
+        raise ValueError(f'fck_ref={fck_ref} must be larger than 0')
+
+    return min(math.pow(fck_ref / fck, 1 / 3), 1)
+
+
+def k_tc(t_ref: float, t0: float, concrete_dev_class: str) -> float:
+    """Computes the factor for considering the effect of high sustained loads
+    and of time of loading on concrete compressive strength.
+
+    EN 1992-1-1:2023, Eq. (5.3)
+
+    Args:
+        t_ref (float): the reference time in days
+        t0 (float): age at loading in days
+        concrete_dev_class (str): 'CS', 'CN' or 'CR'
+
+    Returns:
+        float: the factor value
+
+    Raises:
+        ValueError: if t_ref is less than 0
+        ValueError: if t0 is less than 0
+        ValueError if concrete_dev_class is not 'CS', 'CN', or 'CR'
+    """
+    if t_ref < 0:
+        raise ValueError(f't_ref={t_ref} must be larger than 0')
+    if t0 < 0:
+        raise ValueError(f't0={t0} must be larger than 0')
+
+    concrete_dev_class = concrete_dev_class.upper().strip()
+
+    if concrete_dev_class not in ('CS', 'CN', 'CR'):
+        raise ValueError(
+            f'concrete_dev_class={concrete_dev_class}'
+            + 'should can only take "CS", "CN" or "CR" as values'
+        )
+
+    if concrete_dev_class in ('CR', 'CN') and t_ref <= 28 and t0 > 90:
+        return 1
+
+    if concrete_dev_class == 'CS' and t_ref <= 56 and t0 > 90:
+        return 1
+
+    return 0.85
+
+
+def fcd(fck: float, _eta_cc: float, _k_tc: float, gamma_C: float) -> float:
+    """Computes the value of the design compressive strength of concrete.
+
+    EN 1992-1-1:2023, Eq. (5.3)
+
+    Args:
+        fck (float): characteristic compressive strength in MPa
+        _eta_cc (float): factor for measuring the difference between
+            the undistributed compressive strength of a cylinder and
+            the effective compressive strength in the real structural
+            member
+        _k_tc (float): factor for taking into consideration high
+            sustained loads and of time of loading
+        gamma_C (float): partial factor of concrete
+
+    Returns:
+        float: the design compressive strength of concrete in MPa
+
+    Raises:
+        ValueError: if fck is less than 12 MPa
+        ValueError if _etc_cc is not between 0 and 1
+        ValueError: if gamma_C is less or equal to 0
+    """
+    if fck < 12:
+        raise ValueError(f'fck={fck} must be larger or equal than 12 MPa')
+    if _eta_cc < 0 or _eta_cc > 1:
+        raise ValueError(f'_eta_cc={_eta_cc} must be between 0 and 1')
+    if gamma_C <= 0:
+        raise ValueError(f'gamma_C={gamma_C} must be larger than 0')
+
+    return _eta_cc * _k_tc * fck / gamma_C
+
+
+def k_tt(t_ref: float, concrete_dev_class: str) -> float:
+    """Computes the factor for considering the effect of high sustained loads
+    and of time of loading on concrete tensile strength.
+
+    EN 1992-1-1:2023, Eq. (5.5)
+
+    Args:
+        t_ref (float): the reference time in days
+        concrete_dev_class (str): 'CS', 'CN' or 'CR'
+
+    Returns:
+        float: the factor value
+
+    Raises:
+        ValueError: if t_ref is less than 0
+        ValueError if concrete_dev_class is not 'CS', 'CN', or 'CR'
+    """
+    if t_ref < 0:
+        raise ValueError(f't_ref={t_ref} must be larger than 0')
+
+    concrete_dev_class = concrete_dev_class.upper().strip()
+
+    if concrete_dev_class not in ('CS', 'CN', 'CR'):
+        raise ValueError(
+            f'concrete_dev_class={concrete_dev_class}'
+            + 'should can only take "CS", "CN" or "CR" as values'
+        )
+
+    if concrete_dev_class in ('CR', 'CN') and t_ref <= 28:
+        return 0.8
+
+    if concrete_dev_class == 'CS' and t_ref <= 56:
+        return 0.8
+
+    return 0.7
+
+
+def fctd(_fctk_5: float, _k_tt: float, gamma_C: float) -> float:
+    """Computes the value of the design tensile strength of concrete.
+
+    EN 1992-1-1:2023, Eq. (5.5)
+
+    Args:
+        fctk_5 (float): the 5% mean concrete tensile strength fractile in MPa
+        _k_tt (float): the factor for considering the effect of high sustained
+            loads and of time of loading on concrete tensile strength
+        gamma_C (float): partial factor of concrete
+
+    Returns:
+        float: the design tensile strength of concrete in MPa
+
+    Raises:
+        ValueError: if fctk_5 is less than 0
+        ValueError: gamma_C is less than 1
+    """
+    if _fctk_5 < 0:
+        raise ValueError(f'fctk_5={_fctk_5} must be larger or equal to 0')
+    if gamma_C < 1:
+        raise ValueError(f'gamma_C={gamma_C} must be larger or equal to 1')
+
+    return _k_tt * _fctk_5 / gamma_C
+
+
+def eps_c1(_fcm: float) -> float:
+    """Computes the strain at maximum compressive strength of concrete (fcm)
+    for the Sargin constitutive law.
+
+    EN 1992-1-1:2023, Eq. (5.9)
+
+    Args:
+        _fcm (float): the mean strength of concrete in MPa
+
+    Returns:
+        float: the strain at maximum compressive strength of concrete
+
+    Raises:
+        ValueError: if _fcm is less than 12+8MPa
+    """
+    if _fcm < 20:
+        raise ValueError(f'_fcm={_fcm} must be larger or equal to 12+8MPa')
+
+    return min(0.7 * math.pow(_fcm, 1 / 3), 2.8) / 1000
+
+
+def eps_cu1(_fcm: float) -> float:
+    """Computes the strain at concrete failure of concrete.
+
+    EN 1992-1-1:2023, Eq. (5.10)
+
+    Args:
+        _fcm (float): the mean strength of concrete in MPa
+
+    Returns:
+        float: the maximum strength at failure of concrete
+
+    Raises:
+        ValueError: if _fcm is less than 12+8MPa
+    """
+    if _fcm < 20:
+        raise ValueError(f'_fcm={_fcm} must be larger or equal to 12+8MPa')
+
+    return min(2.8 + 14 * (1 - _fcm / 108) ** 4, 3.5) / 1000
+
+
+def sigma_c(
+    _Ecm: float, _fcm: float, _eps_c: float, _eps_c1: float, _eps_cu1: float
+) -> float:
+    """Computes the compressive stress of concrete given a strain eps_c under
+    short term uniaxial compression.
+
+    EN 1992-1-1:2023, Eq. (5.6)
+
+    Args:
+        _Ecm (float): the secant modulus between sigma_c=0 and
+            sigma_c=0.4*fcm in MPa
+        _fcm (float): the mean compressive strength of concrete
+            in MPa
+        _eps_c (float): the strain of concrete
+        _eps_c1 (float): the strain of concrete at stress _fcm
+        _eps_cu1 (float): the strain at failure of concrete
+
+    Returns:
+        float: the compressive stress of concrete in MPa
+
+    Raises:
+        ValueError: if _Ecm is less or equal to 0
+        ValueError: if _fcm is less than 12+8MPa
+        ValueError: if _eps_c is less than 0
+        ValueError: if _eps_c1 is less or equal to 0
+        ValueError: if _eps_cu1 is less or equal than 0
+        ValueError: if _eps_c is larger than _eps_cu1
+    """
+    if _Ecm <= 0:
+        raise ValueError(f'_Ecm={_Ecm} must be larger than 0')
+    if _fcm < 20:
+        raise ValueError(f'_fcm={_fcm} must be larger or equal to 12+8MPa')
+    if _eps_c < 0:
+        raise ValueError(f'_eps_c={_eps_c} must be larger or equal to 0')
+    if _eps_c1 <= 0:
+        raise ValueError(f'_eps_c1={_eps_c1} must be larger than 0')
+    if _eps_cu1 < 0:
+        raise ValueError(f'_eps_cu1={_eps_cu1} must be larger or equal to 0')
+    if _eps_c > _eps_cu1:
+        raise ValueError(
+            f'Current strain value _eps_c={_eps_c} is larger than'
+            + f'the strain at failure _eps_cu1={_eps_cu1}'
+        )
+
+    k = 1.05 * _Ecm * _eps_c1 / _fcm
+    eta = _eps_c / _eps_c1
+
+    n = k * eta - eta**2
+    d = 1 + (k - 2) * eta
+
+    return n / d * _fcm
+
+
+def weight_c(concrete_type: str) -> float:
+    """Returns the mean unit weight of concrete in kN/m3.
+
+    EN 1992-1-1:2023, 5.1.6-5
+
+    Args:
+        concrete_type (str):
+            'nc' for normal concrete
+            'npc' for normal plain concrete
+
+    Returns:
+        float: mean unit weight in kN/m3
+
+    Raises:
+        ValueError: if concrete_type is not 'nc' or 'npc'
+    """
+    concrete_type = concrete_type.lower().strip()
+
+    if concrete_type == 'nc':
+        return 25
+    if concrete_type == 'npc':
+        return 24
+
+    raise ValueError(
+        f'concrete_type={concrete_type} can only take'
+        + '"nc" or "npc" as values'
+    )
+
+
+def alpha_c_th() -> float:
+    """Returns the linear coefficient of thermal expansion in 1/Cº for
+    concrete.
+
+    EN 1992-1-1:2023, 5.1.6-6
+
+    Returns:
+        float: the linear coefficient of thermal expansion in 1/Cº
+            for concrete
+    """
+    return 10 * 10e-6
+
+
+def r_steel_stress_strain_params(
+    ductility_class: str,
+) -> t.Tuple[float, float]:
+    """Returns the properties that define the stress-strain diagram for
+    reinforced steel k and eps_uk.
+
+    EN 1992-1-1:2023, Table 5.5
+
+    Args:
+        ductility_class (str): 'A', 'B' or 'C'
+
+    Returns:
+        Tuple(float, float): with the characteristic value k
+            and the characteristic strain at maximum force eps_uk
+
+    Raises:
+        ValueError: if ductility class is not 'A', 'B' or 'C'
+    """
+    ductility_class = ductility_class.upper().strip()
+
+    if ductility_class == 'A':
+        return (1.05, 0.025)
+    if ductility_class == 'B':
+        return (1.08, 0.05)
+    if ductility_class == 'C':
+        return (1.25, 0.075)
+    raise ValueError(
+        f'ductility_class={ductility_class} can only '
+        + 'take as values "A", "B" or "C'
+    )
+
+
+def Es() -> float:
+    """Returns the value of the modulus of elasticity for weldable reinforcing
+    steel.
+
+    EN 1992-1-1:2023, 5.2.4-3
+
+    Returns:
+        float: modulus of elasticity in MPa
+    """
+    return 200000
+
+
+def alpha_s_th() -> float:
+    """Returns the linear coefficient of thermal expansion in 1/Cº for weldable
+    reinforced steel.
+
+    EN 1992-1-1:2023, 5.2.4-5
+
+    Returns:
+        float: the linear coefficient of thermal expansion in 1/Cº
+            for weldable reinforce steel
+    """
+    return 10 * 10e-6
+
+
+def weight_s() -> float:
+    """Returns the mean unit weight of reinforced steel for the purposes of
+    design in kN/m3.
+
+    EN 1992-1-1:2023.2.4-4
+
+    Returns:
+        float: the mean unit weight in kN/m3
+    """
+    return 78.5
+
+
+def fyd(fyk: float, gamma_S: float) -> float:
+    """Design value for the yielding stress for welding reinforcing steel.
+
+    EN 1992-1-1:2023, Eq (5.11)
+
+    Args:
+        fyk (float): characteristic yield stress for the steel in MPa
+        gamma_S (float): safety coefficient
+
+    Returns:
+        float: design yielding stress for steel in MPa
+
+    Raises:
+        ValueError: if fyk is less than 0
+        ValueError: if gamma_S is less or equal to 0
+    """
+    if fyk < 0:
+        raise ValueError(f'fyk={fyk} cannot be less than 0')
+    if gamma_S <= 0:
+        raise ValueError(f'gamma_S={gamma_S} must be larger than 0')
+
+    return fyk / gamma_S
+
+
+def eps_ud(eps_uk: float, gamma_S: float) -> float:
+    """Design value for the ultimate limit strain welding reinforcing steel.
+
+    EN 1992-1-1:2023, 5.2.4-2
+
+    Args:
+        eps_uk (float): characteristic ultimate limit
+            strain
+        gamma_S (float): safety coefficient
+
+    Returns:
+        float: design ultimate strain limit
+
+    Raises:
+        ValueError: if eps_uk is less than 0
+        ValueError: if gamma_S is less or equal to 0
+    """
+    if eps_uk < 0:
+        raise ValueError(f'eps_uk={eps_uk} must be equal or larger to 0')
+    if gamma_S < 0:
+        raise ValueError(f'gamma_S={gamma_S} must be larger than 0')
+
+    return eps_uk / gamma_S
+
+
+def sigma_s(
+    eps: float, fy: float, k: float, eps_u: float, _Es: float = 200000
+) -> float:
+    """Compute the stress for welded reinforcing steel in MPa for a given
+    strain.
+
+    EN 1992-1-1:2023, 5.2.4
+
+    Args:
+        eps (float): the strain value
+        fy (float): the yielding stress in MPa.
+            fyd: for the design strength
+            fyk: for the characteristic strength
+        k (float): curve parameter. Ratio between the ultimate
+            stress and the yielding stress.
+            k = 1 for horizontal post-elastic branch without
+            strain limit
+        eps_u (float): ultimate strain at failure
+            eps_ud: for the design ultimate strain
+            eps_uk: for the characteristic ultimate strain
+
+    Keyword Args:
+        _Es (float): the modulus of elasticity for reinforcing steel
+    Returns:
+        float: stre nominal stress in MPa
+
+    Raises:
+        ValueError: if eps is less than 0 or larger than eps_uk
+        ValueError: if _Es is less or equal to 0
+        ValueError: if fy is less or equal to 0
+        ValueError: if k is less than 1
+        ValueError: if eps_u is less or equal to 0 and k > 1
+    """
+    if eps < 0:
+        raise ValueError(f'eps={eps} must be larger or equal to 0')
+    if eps_u <= 0:
+        raise ValueError(f'eps_u={eps_u} must be larger than 0')
+    if eps > eps_u and k > 1:
+        raise ValueError(
+            f'eps={eps} must be equal o less than'
+            + 'eps_uk={eps_u} when k is larger than 1'
+        )
+    if _Es <= 0:
+        raise ValueError(f'_Es={_Es} must be larger than 0')
+    if fy <= 0:
+        raise ValueError(f'fyk={fy} must be larger than 0')
+    if k < 0:
+        raise ValueError(f'k={k} must be larger than 1')
+
+    eps_y = fy / _Es
+
+    # If in elastic area
+    if eps <= eps_y:
+        return eps * _Es
+
+    # If in plastic area
+    m = fy * (k - 1) / (eps_u - eps_y)
+    return fy + m * (eps - eps_y)
+
+
+def p_steel_stress_params(
+    prestress_class: str, element: str
+) -> t.Tuple[float, float]:
+    """Computes the stress-diagram parameters fp01k and fpk.
+
+    EN 1992-1-1:2023, 5.3.3
+
+    Args:
+        prestress_class (str): possible values: Y1560, 1670,
+            Y1770, Y1860, Y1770, Y1860, Y1960, Y2060,
+            Y1030, Y1050, Y1100 and Y1230
+        element (str):
+            'W' for Wires
+            'S' for Strands
+            'B' for Bars
+
+    Returns:
+        Tuple(float, float): with the value of fp01k and fpk in MPa
+
+    Raises:
+        ValueError: if combination of prestress_class and element
+            is not a possible value from the range
+    """
+    prestress_class = prestress_class.upper().strip()
+    element = element.upper().strip()
+    data = {
+        'W': {
+            'Y1570': (1380, 1570),
+            'Y1670': (1470, 1670),
+            'Y1770': (1550, 1770),
+            'Y1860': (1650, 1860),
+        },
+        'S': {
+            'Y1770': (1560, 1770),
+            'Y1860': (1640, 1860),
+            'Y1960': (1740, 1960),
+            'Y2060': (1830, 2060),
+        },
+        'B': {
+            'Y1030': (835, 1030),
+            'Y1050': (950, 1050),
+            'Y1100': (900, 1100),
+            'Y1230': (1080, 1230),
+        },
+    }
+
+    if element not in data:
+        raise ValueError(f'element={element} has not a valid value')
+
+    material = data[element]
+
+    if prestress_class not in material:
+        raise ValueError(
+            f'prestress_class={prestress_class} has not a valid value'
+        )
+
+    return material[prestress_class]
+
+
+def fpd(fp01k: float, gamma_P: float) -> float:
+    """Computes the design value for the prestressing steel stress.
+
+    EN 1992-1-1:2023, 5.3.3
+
+    Args:
+        fp01k (float): the 0.1% proof stress in MPa
+        gamma_P (float): the safety coefficient
+
+    Returns:
+        float: the design value for the design prestressing
+            steel stress in MPa
+
+    Raises:
+        ValueError: if fp01k is less than 0
+        ValueError: if gamma_P is less than 1
+    """
+    if fp01k < 0:
+        raise ValueError(f'fp01k={fp01k} must be larger or equal to 0')
+    if gamma_P < 1:
+        raise ValueError(f'gamma_P={gamma_P} must be larger or equal than 1')
+
+    return fp01k / gamma_P
+
+
+def sigma_p(
+    eps: float,
+    fpy: float,
+    fpu: float,
+    eps_u: float = 0.035,
+    Ep: float = 190000,
+) -> float:
+    """Computes the stress for prestressing steel as a function of the strain.
+
+    EN 1992-1-1:2023, 5.3.3
+
+    Args:
+        eps (float): strain value
+        fpy (float): yielding stress of the steel in MPa
+            fd for design stress values
+            fp01k for nominal stress values
+        fpu (float): the maximum stress at eps_u in MPa
+            fpd: for design stress values
+            fpk: for nominal stress values
+            fpu == fpy for horizontal post-elastic branch without
+            strain limit.
+
+    Keyword Args:
+        eps_u (float): ultimate strain.
+            eps_uk = 0.035 for nominal ultimate strain
+            eps_ud for design ultimate strain
+        Ep (float): modulus of elasticity of prestressing steel
+            in MPa
+
+    Raises:
+        ValueError: if eps is less than 0 or larger than eps_u
+        ValueError: if fpy is less or equal to 0
+        ValueError: if fpu is less than fpy
+        ValueError: if eps_u is lower or equal to 0
+        ValueError: if _Ep is less or equal to 0
+    """
+    if eps < 0:
+        raise ValueError(f'eps={eps} must be larger or equal to 0')
+    if eps_u <= 0:
+        raise ValueError(f'eps_u={eps_u} must be larger than 0')
+    if eps_u < eps and fpy != fpu:
+        raise ValueError(f'eps_u={eps_u} must be larger than eps={eps}')
+    if fpy <= 0:
+        raise ValueError(f'fpy={fpy} must be larger than 0')
+    if fpu < fpy:
+        raise ValueError(f'fpu={fpy} must be larger or equal to fpy={fpy}')
+    if Ep <= 0:
+        raise ValueError(f'Ep={Ep} must be larger than 0')
+
+    eps_y = fpy / Ep
+
+    # If elastic
+    if eps <= eps_y:
+        return eps * Ep
+
+    # If plastic
+    m = (fpu - fpy) / (eps_u - eps_y)
+    return fpy + m * (eps - eps_y)
diff --git a/structuralcodes/codes/ec2_2023/_section9_sls.py b/structuralcodes/codes/ec2_2023/_section9_sls.py
new file mode 100644
index 00000000..bd3f5ae9
--- /dev/null
+++ b/structuralcodes/codes/ec2_2023/_section9_sls.py
@@ -0,0 +1,247 @@
+"""Functions from Section 9 of FprEN 1992-1-1:2023."""
+
+
+def Ec_eff(fcm: float, phi: float, kE: float = 9500) -> float:
+    """Returns de Effective modulus of elasticity from fcm and phi.
+
+    FprEN 1992-1-1:2023, Eq. (9.1)
+
+    Args:
+        fcm (float): The mean compressive strength in MPa.
+        phi (float): The creep coefficient
+
+    Keyword Args:
+        kE (float): Constant to account for the type of aggregate.
+
+    Returns:
+        float: The effective modulus of elastiticy in MPa.
+    """
+    Ecm = kE * fcm ** (1 / 3)
+    return 1.05 * Ecm / (1 + phi)
+
+
+def As_min_y(
+    NEd: float, b: float, h: float, fct_eff: float, fyk: float
+) -> float:
+    """Returns the minimum reinforcement to avoid yielding of steel. Box or T
+    sections are to be divided into rectangles.
+
+    FprEN 1992-1-1:2023, Eq. (9.4)
+    Eq. (9.2) and (9.3) are particular cases of the general equation
+    Eq. (9.2) is valid for pure bending, hence NEd=0
+    Eq. (9.3) is valid for pure tension. The general expression has an upper
+    limit that equals the values of Eq. (9.3)
+
+    Args:
+        NEd (float): SLS axial force applied on the section or rectangle
+                     (compressions are negative) in kN
+        b (float): the width of the section or rectangle in meters
+        h (float): the height of the section or rectange in meters
+        fct_eff (float): effective tension strength of concrete (can normally
+                         be taken as the mean tensile strength) in MPa
+        fyk (float): characteristic yield strength of steel in MPa
+
+    Returns:
+        As_min_y[0] (float): The minimum tensile reinforcement to avoid
+                             yielding of steel on the most tensioned fibre of
+                             the rectangle (As_min_y1) in cm2
+        As_min_y[1] (float): The minimum tensile reinforcement to avoid
+                             yielding of steel on the most tensioned fibre of
+                             the rectangle (As_min_y2) in cm2
+    """
+    As_min_y1 = (
+        min(
+            max(
+                (0.3 * NEd / 1000 + 0.2 * kh(b, h) * fct_eff * b * h) / fyk, 0
+            ),
+            0.5 * kh(b, h) * fct_eff * b * h / fyk,
+        )
+        * 1e4
+    )
+    return As_min_y1, min(max(NEd / fyk * 10 - As_min_y1, 0), As_min_y1)
+
+
+def kh(b: float, h: float) -> float:
+    """Returns factor kh, which reduces the tensile strength of concrete to
+    account for imposed restrained deformations due to shrinkage.
+
+    FprEN 1992-1-1:2023, Eq. (9.5)
+
+    Args:
+        b (float): width of the rectangle in meters
+        h (float): height of the rectangle in meters
+
+    Returns:
+        Factor kh which applies to the tensile resistance of concrete
+    """
+    return min(max(0.8 - 0.6 * (min(b, h) - 0.3), 0.5), 0.8)
+
+
+def wk_cal2(
+    kw: float, k_1_r_: float, srm_cal_: float, epssm_epscm_: float
+) -> float:
+    """Returns de calculated characteristic crack width.
+
+    FprEN 1992-1-1:2023 Eq. (9.8)
+
+    Args:
+        kw: factor that converts the mean crack spacing to a characteristic
+        value
+        k_1_r_: factor accounting for the effect of curvature on cracl width -
+                can be determined using the function k_1_r
+        srm_cal_: mean crack spacing - can be determines using the function
+                  srm_cal
+        epssm_epscm_: mean diference of strain between steel anc concrete - can
+                     be determined using the function epssm_epscm
+    """
+    return kw * k_1_r_ * srm_cal_ * epssm_epscm_
+
+
+def k_1_r(h: float, x: float, ay: float) -> float:
+    """Returns k1/r factor to account for increase in crack width due to
+    curvature of the section in bending.
+
+    FprEN 1992-1-1:2023 Eq. (9.9)
+
+    Args:
+        h (float): height of the section in consistent units (e.g. meters)
+        x (float): distance from most compressed fibre to neutra axis in
+        consistent units (e.g. meters)
+        ay: cover to centre of tensioned reinforcement closest to most
+        tensioned face in consistent units
+        (e.g. meters)
+
+    Returs:
+        Factor k1/r (non-dimensional)
+    """
+    return (h - x) / (h - ay - x)
+
+
+def epssm_epscm(
+    sigma_s: float,
+    kt: float,
+    fct_eff: float,
+    rho_eff: float,
+    alphae: float,
+    Es: float,
+) -> float:
+    """Returns the mean strain difference between steel and concrete along
+    2 transfer lengths.
+
+    FprEN 1992-1-1:2023 Eq. (9.11)
+
+    Args:
+        sigmas (float): the stress in steel at the section of the crack
+        kt (float): an integration factor to account for the variation in
+            strain in steel and concrete it is to be taken as 0.6 for short
+            term loading or instantaneous loading and equal to 0.4 for long
+            term or repeated loading
+        fct_eff (float): the effective cracking stress, which can be taken
+            equal to the mean tensile strength of ocncrete, fctm
+        rho_eff (float): the effective reinforcement ratio in the tension zone
+        alphae (float): the equivalence factor equal to Es/Ecm
+        Es (float): is the modulus of elasticity of steel, normally taken
+            as 200 GPa
+    """
+    return max(
+        (sigma_s - kt * fct_eff / rho_eff * (1 + alphae * rho_eff)) / Es,
+        (1 - kt) * sigma_s / Es,
+    )
+
+
+def kfl(h: float, xg: float, hceff: float) -> float:
+    """Returns factor kfl which accounts for the distribution of stresses
+    before cracking.
+
+    FprEN 1992-1-1:2023 Eq. (9.17)
+
+    Args:
+        h (float): height of the cross section
+        xg (float): distance from the compressed fibre to the centroid of the
+            uncracked section
+        hceff (float): height of the effective tension area
+    """
+    return max(0.5 * (1 + (h - xg - hceff) / (h - xg)), 0.5)
+
+
+def srm_cal(
+    c: float,
+    kfl_: float,
+    kb: float,
+    phi: float,
+    rho_eff: float,
+    kw: float,
+    h,
+    x: float,
+) -> float:
+    """Returns the mean crack spacing.
+
+    FprEN 1992-1-1:2023 Eq. (9.15)
+
+    Args:
+        c (float): concrete cover of reinforcement to bar surface. Larger
+            value of lateral and vertical cover should be applied
+        kfl (float): factor accounting for distribution of stresses prior
+            to cracking
+        kb (float):  factor accounting for bond conditions
+        phi (float): bar diameter
+        rho_eff(float): effective reinforcement ratio in the tension zone
+        kw (float): factor converting the mean crack spacing into a
+            characteristic crack spacing, with a reocmmended value of 1.3
+            (NDP)
+        h (float): height of the cross section
+        x (float): depth of the neutral axis measured form the mots
+            compressed fibre
+    """
+    return min(1.5 * c + kfl_ * kb / 7.2 * phi / rho_eff, 1.3 / kw * (h - x))
+
+
+def wk_cal(
+    kw: float,
+    h: float,
+    xg: float,
+    hc_eff: float,
+    c: float,
+    kb: float,
+    phi: float,
+    rho_eff: float,
+    x: float,
+    sigma_s: float,
+    kt: float,
+    fct_eff: float,
+    alphae: float,
+    Es: float,
+):
+    """Returns the characteristic crack width, wk,cal, as well as auxiliary
+    variables, 1/r, srm,cal and epssm-epscm.
+
+    Fpr EN1991-1-1:2023 Eq. (9.8), complemented with Eq. (9.11), Eq. (9.15),
+        Eq. (9.17)
+
+    Args:
+        kw (float): factor that converts the mean crack spacing to a
+            characteristic value
+        h (float): height of cross section
+        xg (float): depth of centroid of section measured from compressed
+            fibre
+        hc_eff (float): height of the effective tensioned concrete area
+        c (float): concrete cover of reinforcement to bar surface. Larger
+            value of lateral and vertical cover should be applied
+        kb (float): factor account for bond conditions of bar
+        phi (float): diameter of tensioned bars (for different bar diameters,
+            equivalent diameter according to Eq. (9.19)
+        rho_eff (float): effective tension reinforcement ratio
+        x (float): depth of the neutral axis of the cracked section measured
+            from compressed fibre
+        sigma_s (float): tension in most tensioned bar according to fully
+            cracked analysis
+        kt (float): factor accounting for tension stiffening
+        fct_eff (float): effective tensile strength of concrete
+        alphae (float): modular ratio Es/Ecm
+        Es (float): modulus of elasticity of steel bars (normally Es=200 MPa)
+    """
+    k_1_r_ = k_1_r(h, x, c + phi / 2)
+    srm_cal_ = srm_cal(c, kfl(h, xg, hc_eff), kb, phi, rho_eff, kw, h, x)
+    epssm_epscm_ = epssm_epscm(sigma_s, kt, fct_eff, rho_eff, alphae, Es)
+    wk_cal_ = kw * k_1_r_ * srm_cal_ * epssm_epscm_
+    return wk_cal_, k_1_r_, srm_cal_, epssm_epscm_


Test patch:
diff --git a/tests/test_ec2_2023_section5_materials.py b/tests/test_ec2_2023_section5_materials.py
new file mode 100644
index 00000000..f65a472e
--- /dev/null
+++ b/tests/test_ec2_2023_section5_materials.py
@@ -0,0 +1,633 @@
+"""Tests for _section5_materials module"""
+import math
+
+import pytest
+
+from structuralcodes.codes.ec2_2023 import _section5_materials
+
+
+@pytest.mark.parametrize(
+    "fck, delta, expected",
+    [(12, 8, 20), (-16, 8, 24), (25, -10, 35), (-55, -15, 70)],
+)
+def test_fck(fck, delta, expected):
+    """Test the fck function"""
+    assert math.isclose(
+        _section5_materials.fcm(fck, delta_f=delta), expected, rel_tol=0.005
+    )
+
+
+@pytest.mark.parametrize(
+    "test_input1, expected",
+    [
+        (12, 1.572),
+        (16, 1.905),
+        (20, 2.21),
+        (25, 2.565),
+        (30, 2.896),
+        (35, 3.21),
+        (40, 3.509),
+        (45, 3.795),
+        (50, 4.072),
+        (55, 4.183),
+        (60, 4.306),
+        (70, 4.533),
+        (80, 4.74),
+        (90, 4.93),
+        (100, 5.106),
+    ],
+)
+def test_fctm(test_input1, expected):
+    """Test the fctm function."""
+    assert math.isclose(
+        _section5_materials.fctm(test_input1), expected, rel_tol=0.05
+    )
+
+
+@pytest.mark.parametrize(
+    "fctm, expected",
+    [
+        (1.6, 1.12),
+        (-2.2, 1.54),
+        (-4.1, 2.87),
+    ],
+)
+def test_fctk_5(fctm, expected):
+    """Test the fctk_5 function"""
+    assert math.isclose(
+        _section5_materials.fctk_5(fctm), expected, rel_tol=0.005
+    )
+
+
+@pytest.mark.parametrize(
+    "fctm, expected",
+    [
+        (1.6, 2.08),
+        (-2.2, 2.86),
+        (-4.1, 5.33),
+    ],
+)
+def test_fctm_95(fctm, expected):
+    """Test the fctk_95 function"""
+    assert math.isclose(
+        _section5_materials.fctk_95(fctm), expected, rel_tol=0.005
+    )
+
+
+@pytest.mark.parametrize(
+    "fcm, kE, expected",
+    [
+        (20, 9500, 25786.9673576516),
+        (24, 5000, 14422.4957030741),
+        (43, 13000, 45544.1747850274),
+        (53, 10000, 37562.8575422107),
+        (88, 6000, 26687.7610868318),
+    ],
+)
+def test_Ecm(fcm, kE, expected):
+    """Test the Ecm function"""
+    assert math.isclose(
+        _section5_materials.Ecm(fcm, kE=kE), expected, rel_tol=10e-5
+    )
+
+
+@pytest.mark.parametrize(
+    "fcm, kE",
+    [
+        (-20, 9500),
+        (24, -5000),
+        (43, 50000),
+        (53, 3000),
+    ],
+)
+def test_Ecm_raises_errors(fcm, kE):
+    """Test the Ecm function raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.Ecm(fcm, kE)
+
+
+@pytest.mark.parametrize(
+    "Ac, u, expected",
+    [
+        (400, 20, 40),
+        (500, 30, 33.3333333333333),
+        (200, 15, 26.6666666666667),
+        (100, 40, 5),
+    ],
+)
+def test_hn(Ac, u, expected):
+    """Test the hn function"""
+    assert math.isclose(_section5_materials.hn(Ac, u), expected, rel_tol=10e-5)
+
+
+@pytest.mark.parametrize("Ac, u", [(-40, 20), (40, -20), (-40, -20)])
+def test_hn_raises_errors(Ac, u):
+    """Test the hn function raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.hn(Ac, u)
+
+
+@pytest.mark.parametrize(
+    "hn, atm_conditions, expected",
+    [
+        (100, "dry", 0.82),
+        (150, "dry", 0.805),
+        (800, "dry", 0.732),
+        (200, "humid", 0.68),
+        (400, "humid", 0.66666667),
+        (800, "humid", 0.648),
+    ],
+)
+def test_A_phi_correction_exp(hn, atm_conditions, expected):
+    """Test the A_phi_correction_exp function"""
+    assert math.isclose(
+        _section5_materials.A_phi_correction_exp(hn, atm_conditions),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize(
+    "hn, atm_conditions", [(100, "1231"), (80, "dry"), (1100, "humid")]
+)
+def test_A_phi_correction_exp_raises_errors(hn, atm_conditions):
+    """Test A_phi_correction_exp raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.A_phi_correction_exp(hn, atm_conditions)
+
+
+@pytest.mark.parametrize(
+    "t0, atm_conditions, _hn, concrete_class, expected",
+    [
+        (10, "dry", 500, "CS", 2.5),
+        (28, "humid", 200, "CN", 1.6),
+        (91, "dry", 750, "CR", 1.45),
+        (60, "humid", 600, "CS", 1.41016),
+    ],
+)
+def test_phi_50y_t0(t0, atm_conditions, _hn, concrete_class, expected):
+    """Test phi_50y_t0 function"""
+    assert math.isclose(
+        _section5_materials.phi_50y_t0(
+            t0, atm_conditions, _hn, concrete_class
+        ),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize(
+    "t0, atm_conditions, _hn, concrete_class",
+    [
+        (-1, "dry", 500, "CS"),
+        (50, "ASDF", 500, "CS"),
+        (50, "dry", 50, "CS"),
+        (50, "dry", 1500, "CS"),
+        (50, "dry", 500, "ASD"),
+        (1, "dry", 100, "CS"),
+    ],
+)
+def test_phi_50y_t0_raises_errors(t0, atm_conditions, _hn, concrete_class):
+    """Test phi_50y_t0 raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.phi_50y_t0(t0, atm_conditions, _hn, concrete_class)
+
+
+@pytest.mark.parametrize(
+    "fck_28, atm_conditions, _hn, concrete_class, expected",
+    [
+        (35, "dry", 500, "CS", 0.45),
+        (50, "humid", 1000, "CN", 0.23),
+        (80, "dry", 200, "CR", 0.54),
+        (40, "humid", 300, "CS", 0.29),
+        (25, "dry", 800, "CN", 0.46333),
+    ],
+)
+def test_eps_cs_50y(fck_28, atm_conditions, _hn, concrete_class, expected):
+    """Test phi_50y_t0 function"""
+    assert math.isclose(
+        _section5_materials.eps_cs_50y(
+            fck_28, atm_conditions, _hn, concrete_class
+        ),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize(
+    "fck_28, atm_conditions, _hn, concrete_class",
+    [
+        (15, "dry", 500, "CS"),
+        (90, "dry", 500, "CS"),
+        (50, "ASDF", 500, "CS"),
+        (50, "dry", 50, "CS"),
+        (50, "dry", 1500, "CS"),
+        (50, "dry", 500, "ASD"),
+        (1, "dry", 100, "CS"),
+    ],
+)
+def test_eps_cs_50y_raises_errors(fck_28, atm_conditions, _hn, concrete_class):
+    """Test eps_cs_50y raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.eps_cs_50y(
+            fck_28, atm_conditions, _hn, concrete_class
+        )
+
+
+@pytest.mark.parametrize(
+    "fck, fck_ref, expected",
+    [(60, 40, 0.873580464736299), (40, 45, 1), (60, 50, 0.941036028881029)],
+)
+def test_eta_cc(fck, fck_ref, expected):
+    """Test eta_cc function"""
+    assert math.isclose(
+        _section5_materials.eta_cc(fck, fck_ref),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize("fck, fck_ref", [(-10, 40), (0, 20), (30, -10)])
+def test_eta_cc_raises_errors(fck, fck_ref):
+    """Test eta_cc raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.eta_cc(fck, fck_ref)
+
+
+@pytest.mark.parametrize(
+    "t_ref, t0, concrete_class, expected",
+    [
+        (20, 40, "CR", 0.85),
+        (27, 180, "CR", 1),
+        (57, 180, "CS", 0.85),
+        (55, 180, "CS", 1),
+    ],
+)
+def test_k_tc(t_ref, t0, concrete_class, expected):
+    """Test k_tc function"""
+    assert math.isclose(
+        _section5_materials.k_tc(t_ref, t0, concrete_class),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize(
+    "t_ref, t0, concrete_class",
+    [(-3, 20, "CS"), (10, -5, "CS"), (10, 10, "aadsf")],
+)
+def test_k_tc_raises_errors(t_ref, t0, concrete_class):
+    """Test k_tc taises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.k_tc(t_ref, t0, concrete_class)
+
+
+@pytest.mark.parametrize(
+    "fck, eta_cc, k_tc, gamma_C, expected",
+    [
+        (40, 0.9, 0.85, 1.35, 22.6666666666667),
+        (35, 1, 1, 1.5, 23.3333333333333),
+        (50, 0.85, 0.85, 1.4, 25.8035714285714),
+    ],
+)
+def test_fcd(fck, eta_cc, k_tc, gamma_C, expected):
+    """Test fcd function"""
+    assert math.isclose(
+        _section5_materials.fcd(fck, eta_cc, k_tc, gamma_C),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize(
+    "fck, eta_cc, k_tc, gamma_C",
+    [
+        (-10, 0.5, 0.85, 1.5),
+        (40, -1, 0.85, 1.5),
+        (40, 2, 1, 1.5),
+        (40, 0.9, 1.0, -2),
+    ],
+)
+def test_fcd_raises_errors(fck, eta_cc, k_tc, gamma_C):
+    """Test fcd function raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.fcd(fck, eta_cc, k_tc, gamma_C)
+
+
+@pytest.mark.parametrize(
+    "t_ref, concrete_class, expected",
+    [(25, "CR", 0.8), (30, "CR", 0.7), (48, "cs", 0.8), (70, "CS", 0.7)],
+)
+def test_k_tt(t_ref, concrete_class, expected):
+    """Test k_tt function"""
+    assert math.isclose(
+        _section5_materials.k_tt(t_ref, concrete_class),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize("t_ref, concrete_class", [(-10, "CR"), (20, "ADSF")])
+def test_k_tt_raises_errors(t_ref, concrete_class):
+    """Test k_tt raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.k_tt(t_ref, concrete_class)
+
+
+@pytest.mark.parametrize(
+    "fctk_5, k_tt, gamma_C", [(-20, 0.8, 1.5), (40, 0.7, -1)]
+)
+def test_fctd_raises_errors(fctk_5, k_tt, gamma_C):
+    """Test fctd raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.fctd(fctk_5, k_tt, gamma_C)
+
+
+@pytest.mark.parametrize(
+    "fctk_5, k_tt, gamma_C, expected",
+    [
+        (2, 0.7, 1.5, 0.933333333333333),
+        (3, 0.8, 1.25, 1.92),
+        (1.8, 0.7, 1.3, 0.969230769230769),
+    ],
+)
+def test_fctd(fctk_5, k_tt, gamma_C, expected):
+    """Test fctd"""
+    assert math.isclose(
+        _section5_materials.fctd(fctk_5, k_tt, gamma_C),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize(
+    "fcm, expected",
+    [(30, 0.0021750627541677), (50, 0.00257882204904827), (80, 0.0028)],
+)
+def test_eps_c1(fcm, expected):
+    """Test eps_c1"""
+    assert math.isclose(
+        _section5_materials.eps_c1(fcm),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize("fcm", [(-20)])
+def test_eps_c1_raises_errors(fcm):
+    """Test eps_c1 raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.eps_c1(fcm)
+
+
+@pytest.mark.parametrize(
+    "fcm, expected",
+    [(70, 0.00301456920899968), (50, 0.0035), (80, 0.00286325067128806)],
+)
+def test_eps_cu1(fcm, expected):
+    """Test eps_cu1"""
+    assert math.isclose(
+        _section5_materials.eps_cu1(fcm),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize("fcm", [(-20)])
+def test_eps_cu1_raises_errors(fcm):
+    """Test eps_cu1 raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.eps_cu1(fcm)
+
+
+@pytest.mark.parametrize(
+    "Ecm, fcm, eps_c, eps_c1, eps_cu1, expected",
+    [
+        (37562, 50, 0.0025, 0.00257882204904827, 0.0035, 49.9547867728839),
+        (25000, 80, 0.002, 0.0028, 0.00286325067128806, 51.3165266106443),
+    ],
+)
+def test_sigma_c(Ecm, fcm, eps_c, eps_c1, eps_cu1, expected):
+    """Test sigma_c function"""
+    assert math.isclose(
+        _section5_materials.sigma_c(Ecm, fcm, eps_c, eps_c1, eps_cu1),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize(
+    "Ecm, fcm, eps_c, eps_c1, eps_cu1",
+    [
+        (-37562, 50, 0.0025, 0.00257882204904827, 0.0035),
+        (37562, -50, 0.0025, 0.00257882204904827, 0.0035),
+        (37562, 50, -0.0025, 0.00257882204904827, 0.0035),
+        (37562, 50, 0.0025, 0, 0.0035),
+        (37562, 50, 0.0025, -0.00257882204904827, 0.0035),
+        (37562, 50, 0.0025, 0.00257882204904827, -0.0035),
+        (37562, 50, 0.02, 0.00257882204904827, 0.0035),
+    ],
+)
+def test_sigma_c_raises_errors(Ecm, fcm, eps_c, eps_c1, eps_cu1):
+    """Test sigma_c_raises_errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.sigma_c(Ecm, fcm, eps_c, eps_c1, eps_cu1)
+
+
+@pytest.mark.parametrize("concrete_type, expected", [("nc", 25), ("npc", 24)])
+def test_concrete_mean_unit_weight(concrete_type, expected):
+    """Test concrete_mean_weight function"""
+    assert math.isclose(
+        _section5_materials.weight_c(concrete_type),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize(
+    "concrete_type",
+    [
+        ("ADSF"),
+    ],
+)
+def test_weight_c_raises_errors(concrete_type):
+    """Test weight_c raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.weight_c(concrete_type)
+
+
+def test_alpha_c_th():
+    """Test alpha_c_th function"""
+    assert math.isclose(
+        _section5_materials.alpha_c_th(), 10 * 10e-6, rel_tol=10e-5
+    )
+
+
+@pytest.mark.parametrize(
+    "ductility_class, expected_k, expected_eps_uk",
+    [("A", 1.05, 0.025), ("B", 1.08, 0.05), ("C", 1.25, 0.075)],
+)
+def test_r_steel_stress_strain_params(
+    ductility_class, expected_k, expected_eps_uk
+):
+    """Test r_steel_stress_strain retuns proper values"""
+    params = _section5_materials.r_steel_stress_strain_params(ductility_class)
+    assert math.isclose(params[0], expected_k, rel_tol=10e-5)
+    assert math.isclose(params[1], expected_eps_uk, rel_tol=10e-5)
+
+
+def test_r_steel_stress_strain_params_raises_errors():
+    """Test r_steel_stress_strain_params raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.r_steel_stress_strain_params("asdf")
+
+
+def test_Es():
+    """Test Es function"""
+    assert math.isclose(_section5_materials.Es(), 200000, rel_tol=10e-5)
+
+
+def test_alpha_s_th():
+    """Test alpha_s_th function"""
+    assert math.isclose(
+        _section5_materials.alpha_s_th(), 10 * 10e-6, rel_tol=10e-5
+    )
+
+
+def test_weight_s():
+    """Test weight_s function"""
+    assert math.isclose(_section5_materials.weight_s(), 78.5, rel_tol=10e-5)
+
+
+@pytest.mark.parametrize(
+    "fyk, gamma_S, expected",
+    [(500, 1.15, 434.782609), (600, 1.15, 521.739130), (400, 1, 400)],
+)
+def test_fyd(fyk, gamma_S, expected):
+    """Test fyd function"""
+    assert math.isclose(
+        _section5_materials.fyd(fyk, gamma_S), expected, rel_tol=10e-5
+    )
+
+
+@pytest.mark.parametrize("fyk, gamma_S", [(-300, 1.15), (600, -3)])
+def test_fyd_raises_errors(fyk, gamma_S):
+    """Test fyd function raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.fyd(fyk, gamma_S)
+
+
+@pytest.mark.parametrize(
+    "eps_uk, gamma_S, expected",
+    [
+        (0.025, 1.15, 0.02174),
+        (0.05, 1, 0.05),
+    ],
+)
+def test_eps_ud(eps_uk, gamma_S, expected):
+    """Test eps_ud function"""
+    assert math.isclose(
+        _section5_materials.eps_ud(eps_uk, gamma_S), expected, rel_tol=10e-5
+    )
+
+
+@pytest.mark.parametrize("eps_uk, gamma_S", [(-0.025, 1.15), (0.025, -1)])
+def test_eps_ud_raises_errors(eps_uk, gamma_S):
+    """Test eps_ud raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.eps_ud(eps_uk, gamma_S)
+
+
+@pytest.mark.parametrize(
+    "eps, fy, k, eps_u, expected",
+    [
+        (0.015, 500, 1.05, 0.025, 513.888888888889),
+        (0.043, 521, 1, 0.0435, 521),
+        (0.001, 400, 1.25, 0.075, 200),
+        (0.043, 521, 1.08, 0.0435, 562.170402249664),
+    ],
+)
+def test_sigma_s(eps, fy, k, eps_u, expected):
+    """Test sigma_s function"""
+    assert math.isclose(
+        _section5_materials.sigma_s(eps, fy, k, eps_u), expected, rel_tol=10e-5
+    )
+
+
+@pytest.mark.parametrize(
+    "fp01k, gamma_S, expected",
+    [(1560, 1.15, 1356.521739), (1740, 1, 1740), (950, 1.15, 826.086956)],
+)
+def test_fpd(fp01k, gamma_S, expected):
+    """Test fpd function"""
+    assert math.isclose(
+        _section5_materials.fpd(fp01k, gamma_S), expected, rel_tol=10e-5
+    )
+
+
+@pytest.mark.parametrize(
+    "prestress_class, element, expected",
+    [
+        ("Y1770", "W", (1550, 1770)),
+        ("Y1960", "S", (1740, 1960)),
+        ("Y1030", "b", (835, 1030)),
+    ],
+)
+def test_p_steel_strain_params(prestress_class, element, expected):
+    """Test p_steel_strain_params"""
+    assert (
+        _section5_materials.p_steel_stress_params(prestress_class, element)
+        == expected
+    )
+
+
+@pytest.mark.parametrize(
+    "prestress_class, element", [("Y1770", "o"), ("ASDF", "S")]
+)
+def test_p_steel_strain_params_raises_errors(prestress_class, element):
+    """Test p_steel_strain_params raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.p_steel_stress_params(prestress_class, element)
+
+
+@pytest.mark.parametrize("fp01k, gamma_S", [(-1560, 1), (1560, 0), (1560, -1)])
+def test_fpd_raises_errors(fp01k, gamma_S):
+    """Test fpd raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.fpd(fp01k, gamma_S)
+
+
+@pytest.mark.parametrize(
+    "eps, fpy, fpu, eps_u, Ep, expected",
+    [
+        (0.03, 1380, 1570, 0.035, 200000, 1536.19217081851),
+        (0.004, 1740, 1960, 0.0304347826086957, 200000, 800),
+        (0.02, 1650, 1860, 0.035, 200000, 1742.24299065421),
+        (0.032, 900, 900, 0.0304347826086957, 200000, 900),
+    ],
+)
+def test_steel_p(eps, fpy, fpu, eps_u, Ep, expected):
+    """Test steel_p function"""
+    assert math.isclose(
+        _section5_materials.sigma_p(eps, fpy, fpu, eps_u, Ep),
+        expected,
+        rel_tol=10e-5,
+    )
+
+
+@pytest.mark.parametrize(
+    "eps, fpy, fpu, eps_u, Ep",
+    [
+        (-0.03, 1380, 1570, 0.035, 200000),
+        (0.035, 1380, 1570, 0.03, 200000),
+        (0.02, -1380, 1570, 0.035, 200000),
+        (0.02, 1380, 1200, 0.025, 200000),
+        (0.02, 1380, -1570, 0.025, 200000),
+        (0.02, 1380, 1570, -0.025, 200000),
+        (0.02, 1380, 1570, 0.025, -200000),
+    ],
+)
+def test_steel_p_raises_errors(eps, fpy, fpu, eps_u, Ep):
+    """Test steel_p raises errors"""
+    with pytest.raises(ValueError):
+        _section5_materials.sigma_p(eps, fpy, fpu, eps_u, Ep)
diff --git a/tests/test_ec2_2023_section9_sls.py b/tests/test_ec2_2023_section9_sls.py
new file mode 100644
index 00000000..d9a020f9
--- /dev/null
+++ b/tests/test_ec2_2023_section9_sls.py
@@ -0,0 +1,398 @@
+"""Tests for the _section9_sls module"""
+import math
+
+import pytest
+
+from structuralcodes.codes.ec2_2023 import _section9_sls
+
+
+@pytest.mark.parametrize(
+    'test_input1, test_input2, expected',
+    [(33, 1.5, 12188.63), (33, 2, 10157.19), (33, 2.5, 8706.16)],
+)
+def test_Ec_eff(test_input1, test_input2, expected):
+    """Test the Ec_eff function."""
+    assert math.isclose(
+        _section9_sls.Ec_eff(test_input1, test_input2), expected, rel_tol=0.05
+    )
+
+
+@pytest.mark.parametrize(
+    'test_input1, test_input2, test_input3, test_input4, test_input5,'
+    + 'expected1, expected2',
+    [
+        (-1000, 1, 0.15, 2.565, 500, 0, 0),
+        (-500, 1, 0.15, 2.565, 500, 0, 0),
+        (0, 1, 0.15, 2.565, 500, 1.231, 0),
+        (500, 1, 0.15, 2.565, 500, 3.078, 3.078),
+        (3000, 1, 0.15, 2.565, 500, 3.078, 3.078),
+        (-1000, 1, 0.5, 2.565, 400, 0, 0),
+        (-500, 1, 0.5, 2.565, 400, 0.611, 0),
+        (0, 1, 0.5, 2.565, 400, 4.361, 0),
+        (500, 1, 0.5, 2.565, 400, 8.111, 4.389),
+        (1000, 1, 0.5, 2.565, 400, 10.901, 10.901),
+        (-1000, 1, 0.5, 3.795, 500, 0, 0),
+        (-500, 1, 0.8, 3.795, 500, 3.072, 0),
+        (0, 1, 0.8, 3.795, 500, 6.072, 0),
+        (500, 1, 0.8, 3.795, 500, 9.072, 0.928),
+        (1000, 1, 0.8, 3.795, 500, 12.072, 7.928),
+    ],
+)
+def test_As_min_y(
+    test_input1,
+    test_input2,
+    test_input3,
+    test_input4,
+    test_input5,
+    expected1,
+    expected2,
+):
+    """Test the As_min_y function."""
+    (As_min_y1, As_min_y2) = _section9_sls.As_min_y(
+        test_input1, test_input2, test_input3, test_input4, test_input5
+    )
+
+    assert math.isclose(As_min_y1, expected1, rel_tol=0.05)
+    assert math.isclose(As_min_y2, expected2, rel_tol=0.05)
+
+
+@pytest.mark.parametrize(
+    'test_input1, test_input2, expected',
+    [
+        (0.15, 0.15, 0.8),
+        (0.15, 0.25, 0.8),
+        (0.25, 0.15, 0.8),
+        (0.25, 0.25, 0.8),
+        (0.3, 0.3, 0.8),
+        (0.4, 0.4, 0.74),
+        (0.5, 0.5, 0.68),
+        (0.6, 0.6, 0.62),
+        (0.7, 0.7, 0.56),
+        (0.8, 0.8, 0.5),
+        (0.9, 0.9, 0.5),
+        (1, 1, 0.5),
+        (1.1, 1.1, 0.5),
+    ],
+)
+def test_kh(test_input1, test_input2, expected):
+    """Test the kh function."""
+    assert math.isclose(
+        _section9_sls.kh(test_input1, test_input2), expected, rel_tol=0.05
+    )
+
+
+@pytest.mark.parametrize(
+    'test_input1, test_input2, test_input3, expected',
+    [
+        (200, 30, 45, 1.36),
+        (500, 120, 45, 1.134),
+        (1000, 200, 45, 1.06),
+        (200, 30, 50, 1.417),
+        (500, 120, 55, 1.169),
+        (1000, 200, 82, 1.114),
+        (300, 30, 58, 1.274),
+        (450, 30, 32, 1.082),
+        (650, 110, 45, 1.091),
+    ],
+)
+def test_k_1_r(test_input1, test_input2, test_input3, expected):
+    """Test the k1/r function."""
+    assert math.isclose(
+        _section9_sls.k_1_r(test_input1, test_input2, test_input3),
+        expected,
+        rel_tol=0.01,
+    )
+
+
+@pytest.mark.parametrize(
+    'test_input1, test_input2, test_input3, test_input4, test_input5,'
+    + ' test_input6, expected',
+    [
+        (100, 0.4, 4.072, 0.005, 5.365, 200000, 0.0003),
+        (100, 0.6, 4.072, 0.005, 5.365, 200000, 0.0002),
+        (150, 0.4, 2.565, 0.01, 6.354, 200000, 0.00045),
+        (150, 0.6, 2.565, 0.01, 6.354, 200000, 0.0003),
+        (200, 0.4, 2.896, 0.015, 6.091, 200000, 0.0006),
+        (200, 0.6, 2.896, 0.015, 6.091, 200000, 0.0004),
+        (250, 0.4, 3.21, 0.02, 5.869, 200000, 0.000891),
+        (250, 0.6, 3.21, 0.02, 5.869, 200000, 0.000712),
+        (300, 0.4, 3.795, 0.025, 5.512, 200000, 0.001155),
+        (300, 0.6, 3.795, 0.025, 5.512, 200000, 0.000982),
+    ],
+)
+def test_eps_sm_eps_cm(
+    test_input1,
+    test_input2,
+    test_input3,
+    test_input4,
+    test_input5,
+    test_input6,
+    expected,
+):
+    """Test the k1/r function."""
+    assert math.isclose(
+        _section9_sls.epssm_epscm(
+            test_input1,
+            test_input2,
+            test_input3,
+            test_input4,
+            test_input5,
+            test_input6,
+        ),
+        expected,
+        rel_tol=0.01,
+    )
+
+
+@pytest.mark.parametrize(
+    'test_input1, test_input2, test_input3, expected',
+    [
+        (1000, 500, 175, 0.825),
+        (1000, 250, 175, 0.883),
+        (500, 250, 250, 0.5),
+        (150, 75, 80, 0.5),
+        (150, 0.1, 80, 0.733),
+    ],
+)
+def test_kfl(test_input1, test_input2, test_input3, expected):
+    """Test the kfl function."""
+    assert math.isclose(
+        _section9_sls.kfl(test_input1, test_input2, test_input3),
+        expected,
+        rel_tol=0.01,
+    )
+
+
+@pytest.mark.parametrize(
+    'test_input1, test_input2, test_input3, test_input4, test_input5,'
+    + ' test_input6,test_input7, test_input8, expected',
+    [
+        (25, 0.5, 0.9, 16, 0.015, 1.3, 1000, 15, 104.167),
+        (40, 0.5, 0.9, 16, 0.015, 1.3, 1000, 15, 126.667),
+        (40, 0.7, 0.9, 25, 0.015, 1.2, 1000, 15, 205.833),
+        (40, 0.7, 0.9, 25, 0.025, 1.2, 1000, 15, 147.5),
+        (40, 1, 0.9, 25, 0.025, 1.2, 1000, 15, 185),
+        (40, 1, 0.9, 25, 0.025, 1.2, 150, 3, 159.25),
+        (40, 1, 0.9, 25, 0.025, 1.3, 150, 3, 147),
+        (80, 0.65, 1.2, 20, 0.025, 1.3, 250, 3, 206.667),
+        (80, 0.65, 1.2, 20, 0.01, 1.3, 250, 3, 247),
+    ],
+)
+def test_srm_cal(
+    test_input1,
+    test_input2,
+    test_input3,
+    test_input4,
+    test_input5,
+    test_input6,
+    test_input7,
+    test_input8,
+    expected,
+):
+    """Test the srm_cal function."""
+    assert math.isclose(
+        _section9_sls.srm_cal(
+            test_input1,
+            test_input2,
+            test_input3,
+            test_input4,
+            test_input5,
+            test_input6,
+            test_input7,
+            test_input8,
+        ),
+        expected,
+        rel_tol=0.01,
+    )
+
+
+@pytest.mark.parametrize(
+    'test_input1, test_input2, test_input3, test_input4, test_input5,'
+    + ' test_input6, test_input7, test_input8, test_input9, test_input10,'
+    + ' test_input11, test_input12, test_input13, test_input14,'
+    + ' expected1, expected2, expected3, expected4',
+    [
+        (
+            1.3,
+            1000,
+            500,
+            175,
+            45,
+            0.9,
+            16,
+            0.0153,
+            110,
+            253,
+            0.4,
+            2.56,
+            6.35,
+            200000,
+            0.218,
+            1.063,
+            175.343,
+            0.000898,
+        ),
+        (
+            1.3,
+            500,
+            150,
+            55,
+            80,
+            0.9,
+            25,
+            0.035,
+            0.225,
+            320,
+            0.6,
+            3.56,
+            7,
+            200000,
+            0.394,
+            1.227,
+            202.27,
+            0.00122,
+        ),
+        (
+            1.2,
+            500,
+            250,
+            55,
+            80,
+            0.9,
+            25,
+            0.035,
+            0.225,
+            320,
+            0.6,
+            3.56,
+            7,
+            200000,
+            0.358,
+            1.227,
+            199.464,
+            0.00122,
+        ),
+        (
+            1.2,
+            500,
+            250,
+            55,
+            80,
+            0.9,
+            12,
+            0.012,
+            0.225,
+            320,
+            0.6,
+            3.56,
+            7,
+            200000,
+            0.215,
+            1.208,
+            231.25,
+            0.00064,
+        ),
+        (
+            1.3,
+            300,
+            150,
+            150,
+            35,
+            0.9,
+            16,
+            0.0085382,
+            0.05,
+            250,
+            0.4,
+            2.89,
+            6.09,
+            200000,
+            0.193,
+            1.167,
+            169.621,
+            0.00075,
+        ),
+        (
+            1.3,
+            300,
+            150,
+            150,
+            35,
+            1.2,
+            16,
+            0.0085382,
+            0.05,
+            250,
+            0.4,
+            2.89,
+            6.09,
+            200000,
+            0.237,
+            1.167,
+            208.661,
+            0.00075,
+        ),
+        (
+            1.3,
+            300,
+            150,
+            150,
+            45,
+            1.2,
+            16,
+            0.0085382,
+            0.075,
+            250,
+            0.4,
+            2.89,
+            6.09,
+            200000,
+            0.265,
+            1.215,
+            223.661,
+            0.00075,
+        ),
+    ],
+)
+def test_wk_cal(
+    test_input1,
+    test_input2,
+    test_input3,
+    test_input4,
+    test_input5,
+    test_input6,
+    test_input7,
+    test_input8,
+    test_input9,
+    test_input10,
+    test_input11,
+    test_input12,
+    test_input13,
+    test_input14,
+    expected1,
+    expected2,
+    expected3,
+    expected4,
+):
+    """Test the wk_cal function."""
+    (wk_cal_, k_1_r_, srm_cal_, epssm_epscm_) = _section9_sls.wk_cal(
+        test_input1,
+        test_input2,
+        test_input3,
+        test_input4,
+        test_input5,
+        test_input6,
+        test_input7,
+        test_input8,
+        test_input9,
+        test_input10,
+        test_input11,
+        test_input12,
+        test_input13,
+        test_input14,
+    )
+
+    assert math.isclose(wk_cal_, expected1, rel_tol=0.01)
+    assert math.isclose(k_1_r_, expected2, rel_tol=0.01)
+    assert math.isclose(srm_cal_, expected3, rel_tol=0.01)
+    assert math.isclose(epssm_epscm_, expected4, rel_tol=0.01)
diff --git a/tests/test_get_set_design_code.py b/tests/test_get_set_design_code.py
index 1711be83..6d7a3bdb 100644
--- a/tests/test_get_set_design_code.py
+++ b/tests/test_get_set_design_code.py
@@ -7,13 +7,13 @@
 
 
 @pytest.mark.parametrize(
-    'design_code_to_set',
-    ['mc2010', 'MC2010', 'mC2010'],
+    "design_code_to_set",
+    ["mc2010", "MC2010", "mC2010"],
 )
 def test_set_design_code(design_code_to_set):
     """Test setting the design code."""
     # Arrange
-    expected_design_code_title = 'fib Model Code 2010'
+    expected_design_code_title = "fib Model Code 2010"
 
     # Act
     structuralcodes.set_design_code(design_code_to_set)
@@ -36,13 +36,13 @@ def test_get_design_codes():
 
 
 @pytest.mark.parametrize(
-    'na_to_set',
-    ['NO', 'no', 'No', 'nO'],
+    "na_to_set",
+    ["NO", "no", "No", "nO"],
 )
 def test_set_national_annex(na_to_set):
     """Test setting the national annex."""
     # Arrange
-    expected_na = 'no'
+    expected_na = "no"
 
     # Act
     structuralcodes.set_national_annex(na_to_set)
@@ -52,13 +52,13 @@ def test_set_national_annex(na_to_set):
 
 
 @pytest.mark.parametrize(
-    'design_code_to_user',
-    ['mc2010', 'MC2010', 'mC2010'],
+    "design_code_to_user",
+    ["mc2010", "MC2010", "mC2010"],
 )
 def test_use_design_code(design_code_to_user):
     """Test return a design code for use."""
     # Arrange
-    expected_design_code_title = 'fib Model Code 2010'
+    expected_design_code_title = "fib Model Code 2010"
 
     # Act
     code_to_use = structuralcodes.codes._use_design_code(design_code_to_user)
@@ -71,8 +71,8 @@ def test_use_design_code(design_code_to_user):
 def test_use_design_code_none():
     """Test return _CODE if called with None."""
     # Arrange
-    design_code_to_set = 'mc2010'
-    expected_design_code_title = 'fib Model Code 2010'
+    design_code_to_set = "mc2010"
+    expected_design_code_title = "fib Model Code 2010"
     structuralcodes.set_design_code(design_code_to_set)
 
     # Act