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

Issue:
fix: mc2010 beta_ds correct for time less than ts
Correct the calculated value of `beta_ds` when the requested time is less than the time at start of exposure. This is necessary for correct computation of shrinkage development when an array of time from zero is provided.
fix: mc2010 beta_ds correct for time less than ts
Correct the calculated value of `beta_ds` when the requested time is less than the time at start of exposure. This is necessary for correct computation of shrinkage development when an array of time from zero is provided.


Discussion / hints:

Fix patch:
diff --git a/docs/api/codes/ec2_2004/creep_shrinkage.md b/docs/api/codes/ec2_2004/creep_shrinkage.md
index 5503e057..93f6938b 100644
--- a/docs/api/codes/ec2_2004/creep_shrinkage.md
+++ b/docs/api/codes/ec2_2004/creep_shrinkage.md
@@ -2,12 +2,38 @@
 
 The following functions are related to calculation of creep and shrinkage.
 
+## General
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.t_T
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.h_0
+```
+
 ## Creep
 
 ```{eval-rst}
 .. autofunction:: structuralcodes.codes.ec2_2004.phi
 ```
 
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.phi_0
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.phi_RH
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.beta_fcm
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.beta_t0
+```
+
 ```{eval-rst}
 .. autofunction:: structuralcodes.codes.ec2_2004.beta_c
 ```
@@ -17,15 +43,23 @@ The following functions are related to calculation of creep and shrinkage.
 ```
 
 ```{eval-rst}
-.. autofunction:: structuralcodes.codes.ec2_2004.t0_adj
+.. autofunction:: structuralcodes.codes.ec2_2004.alpha_1
 ```
 
 ```{eval-rst}
-.. autofunction:: structuralcodes.codes.ec2_2004.beta_fcm
+.. autofunction:: structuralcodes.codes.ec2_2004.alpha_2
 ```
 
 ```{eval-rst}
-.. autofunction:: structuralcodes.codes.ec2_2004.phi_RH
+.. autofunction:: structuralcodes.codes.ec2_2004.alpha_3
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.t0_adj
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.alpha_cement
 ```
 
 ## Shrinkage
@@ -35,9 +69,41 @@ The following functions are related to calculation of creep and shrinkage.
 ```
 
 ```{eval-rst}
-.. autofunction:: structuralcodes.codes.ec2_2004.beta_RH
+.. autofunction:: structuralcodes.codes.ec2_2004.eps_cd
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.beta_ds
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.k_h
 ```
 
 ```{eval-rst}
 .. autofunction:: structuralcodes.codes.ec2_2004.eps_cd_0
-```
\ No newline at end of file
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.alpha_ds1
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.alpha_ds2
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.beta_RH
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.eps_ca
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.eps_ca_inf
+```
+
+```{eval-rst}
+.. autofunction:: structuralcodes.codes.ec2_2004.beta_as
+```
diff --git a/structuralcodes/codes/ec2_2004/__init__.py b/structuralcodes/codes/ec2_2004/__init__.py
index a6f8ae26..55fb8856 100644
--- a/structuralcodes/codes/ec2_2004/__init__.py
+++ b/structuralcodes/codes/ec2_2004/__init__.py
@@ -2,6 +2,33 @@
 
 import typing as t
 
+from ._concrete_creep_and_shrinkage import (
+    alpha_1,
+    alpha_2,
+    alpha_3,
+    alpha_cement,
+    alpha_ds1,
+    alpha_ds2,
+    beta_as,
+    beta_c,
+    beta_ds,
+    beta_fcm,
+    beta_H,
+    beta_RH,
+    beta_t0,
+    eps_ca,
+    eps_ca_inf,
+    eps_cd,
+    eps_cd_0,
+    eps_cs,
+    h_0,
+    k_h,
+    phi,
+    phi_0,
+    phi_RH,
+    t0_adj,
+    t_T,
+)
 from ._concrete_material_properties import (
     Ecm,
     eps_c1,
@@ -49,17 +76,6 @@
     wk,
     xi1,
 )
-from .annex_b_shrink_and_creep import (
-    beta_c,
-    beta_fcm,
-    beta_H,
-    beta_RH,
-    eps_cd_0,
-    eps_cs,
-    phi,
-    phi_RH,
-    t0_adj,
-)
 from .shear import (
     Asw_max,
     VEdmax_unreinf,
@@ -117,15 +133,31 @@
     'VRdc_prin_stress',
     'VRdmax',
     'VRds',
+    'alpha_1',
+    'alpha_2',
+    'alpha_3',
+    'alpha_cement',
+    'alpha_ds1',
+    'alpha_ds2',
+    'beta_as',
     'beta_c',
+    'beta_ds',
     'beta_fcm',
     'beta_H',
     'beta_RH',
+    'beta_t0',
+    'eps_ca',
+    'eps_ca_inf',
+    'eps_cd',
     'eps_cd_0',
     'eps_cs',
+    'h_0',
+    'k_h',
     'phi',
+    'phi_0',
     'phi_RH',
     't0_adj',
+    't_T',
 ]
 
 __title__: str = 'EUROCODE 2 1992-1-1:2004'
diff --git a/structuralcodes/codes/ec2_2004/_concrete_creep_and_shrinkage.py b/structuralcodes/codes/ec2_2004/_concrete_creep_and_shrinkage.py
new file mode 100644
index 00000000..a908f960
--- /dev/null
+++ b/structuralcodes/codes/ec2_2004/_concrete_creep_and_shrinkage.py
@@ -0,0 +1,529 @@
+"""Collection of functions from EUROCODE 1992-1-1:2004 related to creep and
+shrinkage.
+"""
+
+from __future__ import annotations  # To have clean hints of ArrayLike in docs
+
+import typing as t
+
+import numpy as np
+import numpy.typing as npt
+
+ALPHA_CEMENT_DICT = {'R': 1.0, 'N': 0.0, 'S': -1.0}  # As defined in Eq. (B.9)
+ALPHA_DS_DICT = {
+    'R': {'alpha_ds1': 6, 'alpha_ds2': 0.11},
+    'N': {'alpha_ds1': 4, 'alpha_ds2': 0.12},
+    'S': {'alpha_ds1': 3, 'alpha_ds2': 0.13},
+}  # As defined in Eq. (B.11)
+
+
+def eps_cs(eps_cd: npt.ArrayLike, eps_ca: npt.ArrayLike) -> npt.ArrayLike:
+    """Calculate the total shrinkage strain.
+
+    EN 1992-1-1:2004, Eq. (3.8).
+
+    Args:
+        eps_cd (npt.ArrayLike): The drying shrinkage defined in Eq. (3.9).
+        eps_ca (npt.ArrayLike): The autogenous shrinkage defined in Eq. (3.11).
+
+    Returns:
+        npt.ArrayLike: The total shrinkage strain.
+
+    Note:
+        In EC2 (2004), the shrinkage strain is calculated as a positive number.
+    """
+    eps_cd = eps_cd if np.isscalar(eps_cd) else np.atleast_1d(eps_cd)
+    eps_ca = eps_ca if np.isscalar(eps_ca) else np.atleast_1d(eps_ca)
+    return eps_cd + eps_ca
+
+
+def eps_cd(
+    beta_ds: npt.ArrayLike, k_h: float, eps_cd_0: float
+) -> npt.ArrayLike:
+    """Calculate the drying shrinkage.
+
+    EN 1992-1-1:2004, Eq. (3.9).
+
+    Args:
+        beta_ds (npt.ArrayLike): A coefficient taking into account the time of
+            drying defined in Eq. (3.10).
+        k_h (float): A coefficient depending on the effective thickness of the
+            section defined in Tab. 3.3.
+        eps_cd_0 (float): The nominal value of drying shrinkage defined in Eq.
+            (B.11).
+
+    Returns:
+        npt.ArrayLike: The drying shrinkage.
+
+    Note:
+        In EC2 (2004), the shrinkage strain is calculated as a positive number.
+    """
+    beta_ds = beta_ds if np.isscalar(beta_ds) else np.atleast_1d(beta_ds)
+    return beta_ds * k_h * eps_cd_0
+
+
+def beta_ds(t: npt.ArrayLike, t_s: float, h_0: float):
+    """Calculate the coefficient taking into account the time of drying.
+
+    EN 1992-1-1:2004, Eq. (3.10).
+
+    Args:
+        t (npt.ArrayLike): The age of the concrete in days.
+        t_s (float): The age of the concrete in days at the start of drying
+            (normally this is the point in time when curing measures end).
+        h_0 (float): The effective cross section thichkness in mm.
+
+    Returns:
+        npt.ArrayLike: The coefficient taking into account the time of drying.
+    """
+    t = t if np.isscalar(t) else np.atleast_1d(t)
+    t_drying = t - t_s
+    if np.isscalar(t_drying):
+        t_drying = max(t_drying, 0.0)
+    else:
+        t_drying[t_drying < 0.0] = 0.0
+    return t_drying / (t_drying + 0.04 * h_0 ** (3 / 2))
+
+
+def k_h(h_0: float) -> float:
+    """Calculate the coefficient depending on the notional size.
+
+    EN 1992-1-1:2004, Tab. 3.3.
+
+    Args:
+        h_0 (float): The notional size of the cross-section in mm.
+
+    Returns:
+        float: The coefficient depending on the notional size.
+    """
+    return np.interp(h_0, [100, 200, 300, 500], [1.0, 0.85, 0.75, 0.7])
+
+
+def eps_cd_0(
+    alpha_ds1: float,
+    alpha_ds2: float,
+    fcm: float,
+    beta_RH: float,
+    fcm_0: float = 10,
+) -> float:
+    """Calculate the nominal value of drying shrinkage.
+
+    EN 1992-1-1:2004, Eq. (B.11).
+
+    Args:
+        alpha_ds1 (float): A coefficient depending on the cement type, defined
+            in EC2 (2004), Sec. B.2.
+        alpha_ds2 (float): A coefficient depending on the cement type, defined
+            in EC2 (2004), Sec. B.2.
+        fcm (float): The mean compressive strength in MPa.
+        beta_RH (float): A factor describing the effect of relative humidity,
+            defined in Eq. (B.12).
+
+    Keyword Args:
+        fcm_0 (float): A reference strength in MPa, default 10 MPa.
+
+    Returns:
+        float: The nominal value of drying shrinkage.
+    """
+    return (
+        0.85
+        * ((220 + 110 * alpha_ds1) * np.exp(-alpha_ds2 * fcm / fcm_0))
+        * 1e-6
+        * beta_RH
+    )
+
+
+def alpha_ds1(cement_class: t.Literal['S', 'N', 'R']) -> float:
+    """Return a coefficient depending on the cement class.
+
+    EN 1992-1-1:2004, Sec. B.2.
+
+    Args:
+        cement_class (str): The cement class, either 'S', 'N' or 'R'.
+
+    Returns:
+        float: The exponent that depends on the cement type.
+
+    Raises:
+        ValueError: If an invalid cement class is provided.
+    """
+    _alpha_ds = _get_alpha_ds_dict(cement_class=cement_class)
+    return _alpha_ds['alpha_ds1']
+
+
+def alpha_ds2(cement_class: t.Literal['S', 'N', 'R']) -> float:
+    """Return a coefficient depending on the cement class.
+
+    EN 1992-1-1:2004, Sec. B.2.
+
+    Args:
+        cement_class (str): The cement class, either 'S', 'N' or 'R'.
+
+    Returns:
+        float: The exponent that depends on the cement type.
+
+    Raises:
+        ValueError: If an invalid cement class is provided.
+    """
+    _alpha_ds = _get_alpha_ds_dict(cement_class=cement_class)
+    return _alpha_ds['alpha_ds2']
+
+
+def _get_alpha_ds_dict(cement_class: str) -> t.Dict:
+    """Return a dictionary with values for aplha_ds1 and alpha_ds2."""
+    _alpha_ds = ALPHA_DS_DICT.get(cement_class.upper().strip())
+
+    if _alpha_ds is None:
+        raise ValueError(
+            (
+                f'"{cement_class}" is not a valid cement class. '
+                'Use either S, N or R.'
+            )
+        )
+    return _alpha_ds
+
+
+def beta_RH(RH: float, RH_0: float = 100) -> float:
+    """Calculate the factor describing the effect of relative humidity.
+
+    EN 1992-1-1:2004, Eq. (B.12).
+
+    Args:
+        RH (float): The relative humidity in percent.
+
+    Keyword Args:
+        RH_0 (float): The reference relative humidity, default: 100%.
+
+    Returns:
+        float: The factor taking into account the relative humidity.
+    """
+    return 1.55 * (1 - (RH / RH_0) ** 3)
+
+
+def eps_ca(beta_as: npt.ArrayLike, eps_ca_inf: float) -> npt.ArrayLike:
+    """Calculate the autogenous shrinkage.
+
+    EN 1992-1-1:2004, Eq. (3.11).
+
+    Args:
+        beta_as (npt.ArrayLike): A factor describing the autogenous shrinkage
+            development.
+        eps_ca_inf (float): The final autogenous shrinkage.
+
+    Returns:
+        npt.ArrayLike: The autogenous shrinkage.
+
+    Note:
+        In EC2 (2004), the shrinkage strain is calculated as a positive number.
+    """
+    beta_as = beta_as if np.isscalar(beta_as) else np.atleast_1d(beta_as)
+    return beta_as * eps_ca_inf
+
+
+def eps_ca_inf(fck: float) -> float:
+    """Calculate the final autogenous shrinkage.
+
+    EN 1992-1-1:2004, Eq. 3.12.
+
+    Args:
+        fck (float): The characteristic compressive strength in MPa.
+
+    Returns:
+        float: The final autogenous shrinkage.
+    """
+    return 2.5 * (fck - 10) * 1e-6
+
+
+def beta_as(t: npt.ArrayLike) -> npt.ArrayLike:
+    """Calculate the factor describing the development of autogenous
+    shrinkage.
+
+    EN 1992-1-1:2004, Eq. (3.13).
+
+    Args:
+        t (npt.ArrayLike): The age of the concrete in days.
+
+    Returns:
+        npt.ArrayLike: The factor describing the development of autogenous
+        shrinkage.
+    """
+    t = t if np.isscalar(t) else np.atleast_1d(t)
+    return 1 - np.exp(-0.2 * t**0.5)
+
+
+def phi(phi_0: float, beta_c: npt.ArrayLike) -> npt.ArrayLike:
+    """Calculate the creep number.
+
+    EN 1992-1-1:2004, Eq. (B.1).
+
+    Args:
+        phi_0 (float): The standardized creep number defined in Eq. B.2.
+        beta_c (npt.ArrayLike): A factor taking into account the creep
+            development as a function of time after loading defined in Eq.
+            (B.7).
+
+    Returns:
+        float: The creep number.
+    """
+    beta_c = beta_c if np.isscalar(beta_c) else np.atleast_1d(beta_c)
+    return phi_0 * beta_c
+
+
+def phi_0(phi_RH: float, beta_fcm: float, beta_t0: float) -> float:
+    """Calculate the standardized creep number.
+
+    EN 1992-1-1:2004, Eq. (B.2).
+
+    Args:
+        phi_RH (float): The effect of relative humidity defined in Eq. B.3.
+        beta_fcm (float): The effect of the concrete strength defined in Eq.
+            B.4.
+        beta_t0 (float): The effect of the age at loading defined in Eq. B.5.
+
+    Returns:
+        float: The standardized creep number.
+    """
+    return phi_RH * beta_fcm * beta_t0
+
+
+def phi_RH(
+    h_0: float, fcm: float, RH: float, alpha_1: float, alpha_2: float
+) -> float:
+    """Calculate the effect of relative humidity on the standardized creep
+    number.
+
+    EN 1992-1-1:2004, Eq. (B.3).
+
+    Args:
+        h_0 (float): The effective cross sectional thickness in mm, Equation
+            (B.6).
+        fcm (float): The mean concrete strength in MPa.
+        RH (float): The relative humidity in percent.
+        alpha_1 (float): A factor describing the effect of concrete strength
+            defined in Eq. (B.8c).
+        alpha_2 (float): A factor describing the effect of concrete strength
+            defined in Eq. (B.8c).
+
+    Returns:
+        float: The calculation parameter (B.3).
+    """
+    if fcm <= 35:
+        return 1 + (1 - RH / 100) / (0.1 * h_0 ** (1 / 3))
+    return (1 + (1 - RH / 100) / (0.1 * h_0 ** (1 / 3)) * alpha_1) * alpha_2
+
+
+def beta_fcm(fcm: float) -> float:
+    """Calculate the effect of the concrete strength on the standardized creep
+    number.
+
+    EN 1992-1-1:2004, Eq. (B.4).
+
+    Args:
+        fcm (float): The mean concrete strength in MPa.
+
+    Returns:
+        float: The effect of concrete strength.
+    """
+    return 16.8 / fcm**0.5
+
+
+def beta_t0(t0: float) -> float:
+    """Calculate the effect of age at loading on the notional creep
+    coefficient.
+
+    EN 1992-1-1:2004, Eq. (B.5).
+
+    Args:
+        t0 (float): The age at loading in days.
+
+    Returns:
+        float: The effect of age at loading.
+    """
+    return 1 / (0.1 + t0**0.20)
+
+
+def h_0(Ac: float, u: float) -> float:
+    """Calculate the effective thickness of the cross section.
+
+    EN 1992-1-1:2004, Eq. (B.6).
+
+    Args:
+        Ac (float): The cross section area.
+        u (float): The part of the circumference of the cross section subject
+            to drying.
+
+    Returns:
+        float: The effective thickness.
+
+    Note:
+        The unit of the return will be consistent with the input. E.g. if Ac is
+        mm ** 2 and u is mm, the return is mm.
+    """
+    return 2 * Ac / u
+
+
+def beta_c(t0: float, t: npt.ArrayLike, beta_H: float) -> float:
+    """Calculate the factor that describes the creep development as a function
+    of time after loading.
+
+    EN 1992-1-1:2004, Eq. (B.7).
+
+    Args:
+        t0 (float): The concrete age in days a the time of loading.
+        t (ArrayLike): The concrete age in days at the evaluated time.
+        beta_H (float): Parameter defined in (B.8).
+
+    Returns:
+        float: Parameter defined by Equation (B.7), beta_c.
+    """
+    t = t if np.isscalar(t) else np.atleast_1d(t)
+    t_load = t - t0
+    if np.isscalar(t_load):
+        t_load = max(t_load, 0.0)
+    else:
+        t_load[t_load < 0.0] = 0.0
+    return (t_load / (beta_H + t_load)) ** 0.3
+
+
+def beta_H(h_0: float, fcm: float, RH: float, alpha_3) -> float:
+    """Calculate the effect of relative humidity and the effective thickness
+    of the structural element.
+
+    EN 1992-1-1:2004, Eq. (B.8a and b).
+
+    Args:
+        h_0 (float): The effective cross sectional thickness in mm, Equation
+            (B.6).
+        fcm (float): The mean concrete strength in MPa.
+        RH (float): The relative humidity in percent.
+        alpha_3 (float): A factor describing the effect of concrete strength
+            defined in Eq. B.8c.
+
+    Returns:
+        float: The effect of humidity and the effective thickness of the
+        element.
+    """
+    if fcm <= 35:
+        # (B.8a)
+        return min(1.5 * (1 + (0.012 * RH) ** 18) * h_0 + 250, 1500)
+    # (B.8b)
+    return min(
+        1.5 * (1 + (0.012 * RH) ** 18) * h_0 + 250 * alpha_3,
+        1500 * alpha_3,
+    )
+
+
+def alpha_1(fcm: float) -> float:
+    """Return a factor taking into account the effect of concrete strength.
+
+    EN 1992-1-1:2004, Eq. (B.8c).
+
+    Args:
+        fcm (float): The mean concrete strength in MPa.
+    """
+    return (35 / fcm) ** 0.7
+
+
+def alpha_2(fcm: float) -> float:
+    """Return a factor taking into account the effect of concrete strength.
+
+    EN 1992-1-1:2004, Eq. (B.8c).
+
+    Args:
+        fcm (float): The mean concrete strength in MPa.
+    """
+    return (35 / fcm) ** 0.2
+
+
+def alpha_3(fcm: float) -> float:
+    """Return a factor taking into account the effect of concrete strength.
+
+    EN 1992-1-1:2004, Eq. (B.8c).
+
+    Args:
+        fcm (float): The mean concrete strength in MPa.
+    """
+    return (35 / fcm) ** 0.5
+
+
+def t0_adj(t0: float, alpha_cement: float) -> float:
+    """Calculate the adjusted age of the concrete.
+
+    EN 1992-1-1:2004, Eq. (B.9).
+
+    Args:
+        t0 (float): The concrete age in days at the time of loading.
+        alpha_cement (float): Exponent derived from the sement type.
+
+    Returns:
+        float: The adjusted age of the concrete.
+    """
+    return max(t0 * (9 / (2 + t0**1.2) + 1) ** alpha_cement, 0.5)
+
+
+def t_T(T: npt.ArrayLike, dt: npt.ArrayLike) -> float:
+    """Calculate the maturity of the concrete.
+
+    EN 1992-1-1:2004, Eq. (B.10).
+
+    Args:
+        T (npt.ArrayLike): The curing temperature history in degrees Celcius.
+        dt (npt.ArrayLike): The number of days with temperature T.
+
+    Returns:
+        float: The maturity of the concrete.
+
+    Note:
+        The two arrays T and dt should have the same length. Each item in dt
+        represents a time interval, and the corresponding item in T represents
+        the average temperature in that time interval.
+    """
+    # Prepare the input
+    T = T if np.isscalar(T) else np.atleast_1d(T)
+    dt = dt if np.isscalar(dt) else np.atleast_1d(dt)
+
+    # Check that both are scalar or both are arrays
+    if any(np.isscalar(this_check) for this_check in (T, dt)) and any(
+        not np.isscalar(this_check) for this_check in (T, dt)
+    ):
+        raise ValueError(
+            (
+                f'T ({type(T)}) and dt ({type(dt)}) should either both be '
+                'ArrayLike or scalars.'
+            )
+        )
+
+    # Check the shape of the input arrays
+    if not all((np.isscalar(T), np.isscalar(dt))) and T.shape != dt.shape:
+        raise ValueError(
+            f'T {T.shape} and dt {dt.shape} should have the same shape.'
+        )
+
+    # Return the sum of the temperature adjusted time increments
+    if not all((np.isscalar(T), np.isscalar(dt))):
+        return float(np.sum(np.exp(13.65 - 4000 / (273 + T)) * dt))
+    return np.exp(13.65 - 4000 / (273 + T)) * dt
+
+
+def alpha_cement(cement_class: t.Literal['S', 'N', 'R']) -> float:
+    """Return an exponent that depends on the cement type.
+
+    Args:
+        cement_class (str): The cement class, either 'S', 'N' or 'R'.
+
+    Returns:
+        float: The exponent that depends on the cement type.
+
+    Raises:
+        ValueError: If an invalid cement class is provided.
+    """
+    _alpha_cement = ALPHA_CEMENT_DICT.get(cement_class.upper())
+
+    if _alpha_cement is None:
+        raise ValueError(
+            (
+                f'"{cement_class}" is not a valid cement class. '
+                'Use either S, N or R.'
+            )
+        )
+    return _alpha_cement
diff --git a/structuralcodes/codes/ec2_2004/annex_b_shrink_and_creep.py b/structuralcodes/codes/ec2_2004/annex_b_shrink_and_creep.py
deleted file mode 100644
index 4b539a72..00000000
--- a/structuralcodes/codes/ec2_2004/annex_b_shrink_and_creep.py
+++ /dev/null
@@ -1,257 +0,0 @@
-"""Calculation routine for shrinkage and creep from EUROCODE 1992-1-1:2004
-Annex B.
-"""
-
-import math
-import typing as t
-
-import numpy as np
-
-ALPHA_CEMENT_DICT = {'R': 1, 'N': 0, 'S': -1}
-ALPHA_DS_DICT = {
-    'R': {'alpha_ds1': 6, 'alpha_ds2': 0.11},
-    'N': {'alpha_ds1': 4, 'alpha_ds2': 0.12},
-    'S': {'alpha_ds1': 3, 'alpha_ds2': 0.13},
-}
-
-
-def phi(
-    h_0: float,
-    f_cm: float,
-    RH: int = 50,
-    cement_class: t.Literal['R', 'N', 'S'] = 'R',
-    t0: int = 7,
-    t: int = 18263,
-) -> float:
-    """Calculates the creep number.
-
-    EN 1992-1-1:2004, Eq. (B.1).
-
-    Args:
-        h_0 (float): The product of 2 times the cross-sectional area divided by
-            the exposed circumference according to (B.6).
-        f_cm (float): The mean concrete strength,
-        RH (int): The relative humidity in percent, defaults to 50%.
-
-    Keyword Args:
-        cement_class (str): The cement class, defaults to 'R'. Possible values:
-            'R', 'N', 'S',
-        t0: The age of the concrete at the time (in days) of loading.
-        t: The age of the concrete at the time (in days) of evaluation (50
-            years default).
-
-    Returns:
-        float: The creep value for the load, phi(t, t0).
-
-    Raises:
-        ValueError: checks if the cement class equals R, N or S.
-    """
-    # The cement class will decide the value of alpha_cement, ds1 and ds2.
-    # The values are given in (B.9) and (B.12)
-
-    _cement_class = cement_class.upper().strip()
-    alpha_cement = ALPHA_CEMENT_DICT.get(_cement_class)
-    if alpha_cement is None:
-        raise ValueError(f'cement_class={cement_class}, expected R, N or S')
-
-    _beta_H = beta_H(h_0, f_cm, RH)
-    _phi_RH = phi_RH(h_0, f_cm, RH)
-    _beta_fcm = beta_fcm(f_cm)
-    _t0_adj = t0_adj(t0, alpha_cement)
-    beta_t0 = 1 / (0.1 + _t0_adj**0.20)  # (B.5)
-    _beta_c = beta_c(t0, t, _beta_H)
-
-    phi_0 = _phi_RH * _beta_fcm * beta_t0  # (B.2)
-    return phi_0 * _beta_c
-
-
-def beta_c(t0: float, t: float, beta_H: float) -> float:
-    """Calculates the factor beta_c.
-
-    EN 1992-1-1:2004, Eq. (B.7).
-
-    Args:
-        t0 (float): The concrete age in days a the time of loading.
-        t (float): The concrete age at the evaluated time.
-        beta_H: Parameter defined in (B.8).
-
-    Returns:
-        float: Parameter defined by Equation (B.7), beta_c.
-    """
-    return ((t - t0) / (beta_H + t - t0)) ** 0.3
-
-
-def t0_adj(t0: float, alpha_cement: float) -> float:
-    """Calculates the adjusted age of the concrete.
-
-    EN 1992-1-1:2004, Eq. (B.9).
-
-    Args:
-        t0 (float): The concrete age in days at the time of loading.
-        alpha_cement (float): Exponent derived from the sement type.
-
-    Returns:
-        float: The adjusted age of the concrete.
-    """
-    return max(t0 * (9 / (2 + t0**1.2) + 1) ** alpha_cement, 0.5)
-
-
-def beta_fcm(f_cm: float) -> float:
-    """Calculates beta_f_cm.
-
-    EN 1992-1-1:2004, Eq. (B.4).
-
-    Args:
-        f_cm (float): The mean concrete strength.
-
-    Returns:
-        float: The factor defined in Equation (B.4).
-    """
-    return 16.8 / f_cm**0.5
-
-
-def phi_RH(h_0: float, f_cm: float, RH: int) -> float:
-    """Calculates phi_RH.
-
-    EN 1992-1-1:2004, Eq. (B.3).
-
-    Args:
-        h_0 (float): The effective cross sectional thickness, Equation (B.6).
-        f_cm (float): The mean concrete strength.
-        RH (int): The relative humidity in percent.
-
-    Returns:
-        float: The calculation parameter (B.3).
-    """
-    # (B.8c) Alpha 1 to 2 is a constant where the only variable is f_cm
-    alpha_1, alpha_2 = (
-        (35 / f_cm) ** 0.7,
-        (35 / f_cm) ** 0.2,
-    )
-
-    if f_cm <= 35:
-        return 1 + (1 - RH / 100) / (0.1 * h_0 ** (1 / 3))
-    return (1 + (1 - RH / 100) / (0.1 * h_0 ** (1 / 3)) * alpha_1) * alpha_2
-
-
-def beta_H(h_0: float, f_cm: float, RH: int) -> float:
-    """Calculates beta_H.
-
-    EN 1992-1-1:2004, Eq. (B.8a and b).
-
-    Args:
-        h_0 (float): The effective cross sectional thickness, Equation (B.6).
-        f_cm (float): The mean concrete strength.
-        RH (int): The relative humidity in percent.
-
-    Returns:
-        float: The calculation parameter defined in (B.8).
-    """
-    # (B.8c) Alpha 3 is a constant where f_cm is the only  variable
-    alpha_3 = (35 / f_cm) ** 0.5
-
-    if f_cm <= 35:
-        # (B.8a) and (B.3a) applies
-        return min(1.5 * (1 + (0.012 * RH) ** 18) * h_0 + 250, 1500)
-    # (B.8b) and (B.3b) applies
-    return min(
-        1.5 * (1 + (0.012 * RH) ** 18) * h_0 + 250 * alpha_3,
-        1500 * alpha_3,
-    )
-
-
-def eps_cs(
-    h_0: float,
-    f_cm: float,
-    cement_class: t.Literal['R', 'N', 'S'] = 'R',
-    RH: int = 50,
-    t_S: int = 28,
-    t: int = 18263,
-) -> float:
-    """Calculates the shrinkage strain.
-
-    EN 1992-1-1:2004, Eq. (3.8).
-
-    Args:
-        h_0 (float): The effective cross sectional thickness, Equation (B.6).
-        f_cm (float): The mean concrete strength.
-        cement_class (str): The cement class, defaults to 'R'. Possible values:
-            'R', 'N', 'S'.
-
-    Keyword Args:
-        RH (int): The relative humidity in percent, defaults to 50.
-        t_S (int): the number of days when shrinkage begins, default: 28 days.
-        t (int): the concrete age at the time (in days) of evaluation, default:
-            50 years.
-
-    Returns:
-        float: The shrinkage. Given as absolute, not in percent or ppm.
-
-    Raises:
-        ValueError: Checks if the cement class equals R, N or S.
-    """
-    _cement_class = cement_class.upper().strip()
-    beta_ds = (t - t_S) / (t - t_S + 0.04 * h_0 ** (3 / 2))  # (3.10)
-    beta_as = 1 - math.exp(-0.2 * t**0.5)  # (3.13)
-
-    # k_h is defined in Table 3.3 under (3.9)
-    if h_0 >= 500:
-        k_h = 0.70
-    elif h_0 <= 100:
-        k_h = 1.0
-    else:
-        k_h = np.interp(h_0, [100, 200, 300, 500], [1.0, 0.85, 0.75, 0.7])
-
-    eps_ca_infinite = 2.5 * (f_cm - 18) * 1e-6  # (3.12)
-    eps_ca = beta_as * eps_ca_infinite  # (3.11)
-    eps_cd = beta_ds * k_h * eps_cd_0(_cement_class, f_cm, RH)  # (3.9)
-    return eps_cd + eps_ca  # (3.8)
-
-
-def beta_RH(RH: int, RH_0: int = 100) -> float:
-    """Calculates beta_RH.
-
-    EN 1992-1-1:2004, Eq. (B.12).
-
-    Args:
-        RH (int): The relative humidity in percent.
-
-    Keyword Args:
-        RH_0 (int): The reference relative humidity, default: 100%.
-
-    Returns:
-        float: Calculation parameter from Equation (B.12).
-    """
-    return 1.55 * (1 - (RH / RH_0) ** 3)
-
-
-def eps_cd_0(cement_class: str, f_cm: float, RH: int) -> float:
-    """Calculates eps_cd_0.
-
-    EN 1992-1-1:2004, Eq. (B.11).
-
-    Args:
-        cement_class (str): The cement class, defaults to 'R'. Possible values:
-            'R', 'N', 'S'.
-        f_cm (float): The mean concrete strength.
-        RH (int): The relative humidity in percent.
-
-    Returns:
-        float: The nominal value for shrinkage.
-
-    Raises:
-        ValueError: Checks if the cement class equals R, N or S.
-    """
-    _cement_class = cement_class.upper().strip()
-    alpha = ALPHA_DS_DICT.get(_cement_class)
-    if alpha is None:
-        raise ValueError(f'cement_class={cement_class}, expected R, N or S')
-    alpha_ds1 = alpha['alpha_ds1']
-    alpha_ds2 = alpha['alpha_ds2']
-
-    return (
-        0.85
-        * ((220 + 110 * alpha_ds1) * math.exp(-alpha_ds2 * f_cm / 10))
-        * 1e-6
-        * beta_RH(RH)
-    )  # (B.11)


Test patch:
diff --git a/tests/test_ec2_2004/test_ec2_2004_annex_B_shrink_and_creep.py b/tests/test_ec2_2004/test_ec2_2004_annex_B_shrink_and_creep.py
deleted file mode 100644
index 946e0b68..00000000
--- a/tests/test_ec2_2004/test_ec2_2004_annex_B_shrink_and_creep.py
+++ /dev/null
@@ -1,72 +0,0 @@
-"""Tests for EUROCODE 2-1-2:2004 Annex B and chapter 3."""
-
-import math
-
-import pytest
-
-from structuralcodes.codes.ec2_2004 import annex_b_shrink_and_creep
-
-
-@pytest.mark.parametrize(
-    'test_h_0, test_f_cm, test_RH, test_c_class, test_t0, test_t, expected',
-    [
-        (138.5, 43, 50, 'R', 7, 18263, 2.567),
-        (136.5, 38, 55, 'N', 7, 18263, 3.083),
-        (136.5, 28, 55, 'N', 7, 18263, 3.748),
-    ],
-)
-def test_creep_returns_expected_values(
-    test_h_0, test_f_cm, test_RH, test_c_class, test_t0, test_t, expected
-):
-    """Test that phi returns expected values."""
-    creep = annex_b_shrink_and_creep.phi(
-        test_h_0, test_f_cm, test_RH, test_c_class, test_t0, test_t
-    )
-    assert math.isclose(creep, expected, abs_tol=1e-3)
-
-
-@pytest.mark.parametrize(
-    'test_h_0, test_f_cm, test_c_class, test_RH, test_t_S, test_t, expected',
-    [
-        (138.5, 43, 'R ', 50, 28, 18263, 0.0006560211272528516),
-        (138.5, 43, 'r', 50, 28, 18263, 0.0006560211272528516),
-        (600.0, 43, 'N', 50, 28, 18263, 0.0003704813729755042),
-        (600.0, 43, 'n', 50, 28, 18263, 0.0003704813729755042),
-        (500.0, 43, 'N', 50, 28, 18263, 0.00037280025633482355),
-        (136.8, 38, 'N ', 55, 28, 18263, 0.0004825589169449677),
-        (100.0, 38, 'N', 55, 28, 18263, 0.000508432488714812),
-        (90.0, 38, 'N', 55, 28, 18263, 0.0005085792190408255),
-        (136.8, 28, 'N', 55, 28, 18263, 0.0005127088169780931),
-    ],
-)
-def test_shrinkage_returns_expected_values(
-    test_h_0, test_f_cm, test_c_class, test_RH, test_t_S, test_t, expected
-):
-    """Test that eps_cs returns expected values."""
-    shrinkage = annex_b_shrink_and_creep.eps_cs(
-        test_h_0, test_f_cm, test_c_class, test_RH, test_t_S, test_t
-    )
-    assert math.isclose(shrinkage, expected)
-
-
-invalid_cement_classes = pytest.mark.parametrize(
-    'cement_class',
-    [
-        'not a valid class',
-        'rns',
-    ],
-)
-
-
-@invalid_cement_classes
-def test_creep_wrong_cement_class(cement_class):
-    """Test that ValueError is raised if a wrong cement class is provided."""
-    with pytest.raises(ValueError):
-        annex_b_shrink_and_creep.phi(300, 38, 50, cement_class)
-
-
-@invalid_cement_classes
-def test_shrinkage_wrong_cement_class(cement_class):
-    """Test that ValueError is raised if a wrong cement class is provided."""
-    with pytest.raises(ValueError):
-        annex_b_shrink_and_creep.eps_cd_0(cement_class, 38, 50)
diff --git a/tests/test_ec2_2004/test_ec2_2004_shrinkage_and_creep.py b/tests/test_ec2_2004/test_ec2_2004_shrinkage_and_creep.py
new file mode 100644
index 00000000..2e4bb15c
--- /dev/null
+++ b/tests/test_ec2_2004/test_ec2_2004_shrinkage_and_creep.py
@@ -0,0 +1,289 @@
+"""Tests for EUROCODE 2-1-2:2004 creep and shrinkage."""
+
+import numpy as np
+import pytest
+
+from structuralcodes.codes.ec2_2004 import _concrete_creep_and_shrinkage
+
+
+@pytest.mark.parametrize(
+    'test_h_0, test_f_cm, test_RH, test_c_class, test_t0, test_t, expected',
+    [
+        (138.5, 43, 50, 'R', 7, 18263, 2.567),
+        (136.5, 38, 55, 'N', 7, 18263, 3.083),
+        (136.5, 28, 55, 'N', 7, 18263, 3.748),
+    ],
+)
+def test_creep_returns_expected_values(
+    test_h_0, test_f_cm, test_RH, test_c_class, test_t0, test_t, expected
+):
+    """Test that phi returns expected values."""
+    creep = _concrete_creep_and_shrinkage.phi(
+        _concrete_creep_and_shrinkage.phi_0(
+            _concrete_creep_and_shrinkage.phi_RH(
+                test_h_0,
+                test_f_cm,
+                test_RH,
+                _concrete_creep_and_shrinkage.alpha_1(test_f_cm),
+                _concrete_creep_and_shrinkage.alpha_2(test_f_cm),
+            ),
+            _concrete_creep_and_shrinkage.beta_fcm(test_f_cm),
+            _concrete_creep_and_shrinkage.beta_t0(
+                _concrete_creep_and_shrinkage.t0_adj(
+                    test_t0,
+                    _concrete_creep_and_shrinkage.alpha_cement(test_c_class),
+                )
+            ),
+        ),
+        _concrete_creep_and_shrinkage.beta_c(
+            _concrete_creep_and_shrinkage.t0_adj(
+                test_t0,
+                _concrete_creep_and_shrinkage.alpha_cement(test_c_class),
+            ),
+            test_t,
+            _concrete_creep_and_shrinkage.beta_H(
+                test_h_0,
+                test_f_cm,
+                test_RH,
+                _concrete_creep_and_shrinkage.alpha_3(test_f_cm),
+            ),
+        ),
+    )
+    assert np.isclose(creep, expected, atol=1e-3)
+
+
+def test_creep_array_input():
+    """Test calculating creep number with array input."""
+    # Arrange
+    h_0 = 352
+    f_cm = 51
+    RH = 67
+    cement_class = 's'
+    t0 = 8
+    t_final = 23 * 365
+    num_time = 13
+    t = np.linspace(0, t_final, num_time)
+
+    # Act
+    phi_RH = _concrete_creep_and_shrinkage.phi_RH(
+        h_0,
+        f_cm,
+        RH,
+        _concrete_creep_and_shrinkage.alpha_1(f_cm),
+        _concrete_creep_and_shrinkage.alpha_2(f_cm),
+    )
+    beta_fcm = _concrete_creep_and_shrinkage.beta_fcm(f_cm)
+    beta_t0 = _concrete_creep_and_shrinkage.beta_t0(
+        _concrete_creep_and_shrinkage.t0_adj(
+            t0,
+            _concrete_creep_and_shrinkage.alpha_cement(cement_class),
+        )
+    )
+    phi_0 = _concrete_creep_and_shrinkage.phi_0(phi_RH, beta_fcm, beta_t0)
+    beta_c_array = _concrete_creep_and_shrinkage.beta_c(
+        _concrete_creep_and_shrinkage.t0_adj(
+            t0, _concrete_creep_and_shrinkage.alpha_cement(cement_class)
+        ),
+        t,
+        _concrete_creep_and_shrinkage.beta_H(
+            h_0,
+            f_cm,
+            RH,
+            _concrete_creep_and_shrinkage.alpha_3(f_cm),
+        ),
+    )
+    beta_c_float = _concrete_creep_and_shrinkage.beta_c(
+        _concrete_creep_and_shrinkage.t0_adj(
+            t0, _concrete_creep_and_shrinkage.alpha_cement(cement_class)
+        ),
+        t[-1],
+        _concrete_creep_and_shrinkage.beta_H(
+            h_0,
+            f_cm,
+            RH,
+            _concrete_creep_and_shrinkage.alpha_3(f_cm),
+        ),
+    )
+
+    creep = _concrete_creep_and_shrinkage.phi(phi_0, beta_c_array)
+    creep_final = _concrete_creep_and_shrinkage.phi(phi_0, beta_c_float)
+
+    # Assert
+    assert len(creep) == num_time
+    assert np.isclose(creep[-1], creep_final)
+
+
+@pytest.mark.parametrize(
+    'test_h_0, test_f_cm, test_c_class, test_RH, test_t_S, test_t, expected',
+    [
+        (138.5, 43, 'R ', 50, 28, 18263, 0.0006560211272528516),
+        (138.5, 43, 'r', 50, 28, 18263, 0.0006560211272528516),
+        (600.0, 43, 'N', 50, 28, 18263, 0.0003704813729755042),
+        (600.0, 43, 'n', 50, 28, 18263, 0.0003704813729755042),
+        (500.0, 43, 'N', 50, 28, 18263, 0.00037280025633482355),
+        (136.8, 38, 'N ', 55, 28, 18263, 0.0004825589169449677),
+        (100.0, 38, 'N', 55, 28, 18263, 0.000508432488714812),
+        (90.0, 38, 'N', 55, 28, 18263, 0.0005085792190408255),
+        (136.8, 28, 'N', 55, 28, 18263, 0.0005127088169780931),
+    ],
+)
+def test_shrinkage_returns_expected_values(
+    test_h_0, test_f_cm, test_c_class, test_RH, test_t_S, test_t, expected
+):
+    """Test that eps_cs returns expected values."""
+    eps_ca = _concrete_creep_and_shrinkage.eps_ca(
+        _concrete_creep_and_shrinkage.beta_as(test_t),
+        _concrete_creep_and_shrinkage.eps_ca_inf(test_f_cm - 8),
+    )
+    eps_cd = _concrete_creep_and_shrinkage.eps_cd(
+        _concrete_creep_and_shrinkage.beta_ds(test_t, test_t_S, test_h_0),
+        _concrete_creep_and_shrinkage.k_h(test_h_0),
+        _concrete_creep_and_shrinkage.eps_cd_0(
+            _concrete_creep_and_shrinkage.alpha_ds1(test_c_class),
+            _concrete_creep_and_shrinkage.alpha_ds2(test_c_class),
+            test_f_cm,
+            _concrete_creep_and_shrinkage.beta_RH(test_RH),
+        ),
+    )
+    shrinkage = _concrete_creep_and_shrinkage.eps_cs(
+        eps_ca=eps_ca, eps_cd=eps_cd
+    )
+    assert np.isclose(shrinkage, expected, atol=1e-6)
+
+
+invalid_cement_classes = pytest.mark.parametrize(
+    'cement_class',
+    [
+        'not a valid class',
+        'rns',
+    ],
+)
+
+
+@invalid_cement_classes
+def test_creep_wrong_cement_class(cement_class):
+    """Test that ValueError is raised if a wrong cement class is provided."""
+    with pytest.raises(ValueError):
+        _concrete_creep_and_shrinkage.alpha_cement(cement_class)
+
+
+@invalid_cement_classes
+def test_shrinkage_wrong_cement_class(cement_class):
+    """Test that ValueError is raised if a wrong cement class is provided."""
+    with pytest.raises(ValueError):
+        _concrete_creep_and_shrinkage.alpha_ds1(cement_class)
+    with pytest.raises(ValueError):
+        _concrete_creep_and_shrinkage.alpha_ds2(cement_class)
+
+
+def test_shrinkage_array_input():
+    """Test calculating the shrinkage strain with an array as input."""
+    # Arrange
+    h_0 = 400
+    f_cm = 53
+    cement_class = 'n'
+    RH = 70
+    t_s = 14
+    t_final = 35 * 365
+    num_time = 10
+    t = np.linspace(0, t_final, num_time)
+
+    # Act
+    eps_ca_inf = _concrete_creep_and_shrinkage.eps_ca_inf(f_cm - 8)
+    beta_as_array = _concrete_creep_and_shrinkage.beta_as(t)
+    beta_as_float = _concrete_creep_and_shrinkage.beta_as(t_final)
+    eps_ca_array = _concrete_creep_and_shrinkage.eps_ca(
+        beta_as_array, eps_ca_inf
+    )
+    eps_ca_float = _concrete_creep_and_shrinkage.eps_ca(
+        beta_as_float, eps_ca_inf
+    )
+    beta_ds_array = _concrete_creep_and_shrinkage.beta_ds(t, t_s, h_0)
+    beta_ds_float = _concrete_creep_and_shrinkage.beta_ds(t_final, t_s, h_0)
+    alpha_ds1, alpha_ds2 = (
+        _concrete_creep_and_shrinkage.alpha_ds1(cement_class),
+        _concrete_creep_and_shrinkage.alpha_ds2(cement_class),
+    )
+    k_h = _concrete_creep_and_shrinkage.k_h(h_0)
+    beta_RH = _concrete_creep_and_shrinkage.beta_RH(RH)
+    eps_cd_0 = _concrete_creep_and_shrinkage.eps_cd_0(
+        alpha_ds1,
+        alpha_ds2,
+        f_cm,
+        beta_RH,
+    )
+    beta_ds_array = _concrete_creep_and_shrinkage.beta_ds(t, t_s, h_0)
+    beta_ds_float = _concrete_creep_and_shrinkage.beta_ds(t_final, t_s, h_0)
+    eps_cd_array = _concrete_creep_and_shrinkage.eps_cd(
+        beta_ds_array, k_h, eps_cd_0
+    )
+    eps_cd_float = _concrete_creep_and_shrinkage.eps_cd(
+        beta_ds_float, k_h, eps_cd_0
+    )
+    shrinkage = _concrete_creep_and_shrinkage.eps_cs(
+        eps_ca=eps_ca_array, eps_cd=eps_cd_array
+    )
+    shrinkage_final = _concrete_creep_and_shrinkage.eps_cs(
+        eps_ca=eps_ca_float, eps_cd=eps_cd_float
+    )
+
+    # Assert
+    assert len(shrinkage) == num_time
+    assert np.isclose(shrinkage[-1], shrinkage_final)
+
+
+def test_maturity():
+    """Test calculating the maturity of concrete."""
+    # Arrange
+    T = [19, 20, 20, 21, 22, 23]
+    dt = [1, 2, 1, 1, 1, 2]
+
+    # Act
+    t_T = _concrete_creep_and_shrinkage.t_T(T=T, dt=dt)
+
+    # Assert
+    assert np.isclose(t_T, 8.37986781)
+
+
+@pytest.mark.parametrize(
+    'T, dt',
+    (
+        ([19, 20, 20, 21, 22, 23], [1, 2, 1, 1, 1]),
+        ([19, 20, 20, 21, 22], [1, 2, 1, 1, 1, 2]),
+    ),
+)
+def test_maturity_invalid_arrays(T, dt):
+    """Test if maturity calculation raises an exception."""
+    # Act and assert
+    with pytest.raises(ValueError):
+        _concrete_creep_and_shrinkage.t_T(T=T, dt=dt)
+
+
+def test_calculate_h0():
+    """Test calculating the effective section thickness."""
+    # Arrange
+    width = 250
+    height = 350
+    Ac = width * height
+    u = 2 * (width + height)
+    expected_h0 = 145.8333333
+
+    # Act
+    h_0 = _concrete_creep_and_shrinkage.h_0(Ac=Ac, u=u)
+
+    # Assert
+    assert np.isclose(h_0, expected_h0)
+
+
+@pytest.mark.parametrize(
+    'T, dt',
+    (
+        ([1, 2, 3], 1),  # Array and scalar
+        (1, [1, 2, 3]),  # Scalar and array
+        ([1, 2], [1, 2, 3]),  # Arrays with different lengts
+    ),
+)
+def test_calculate_maturity_invalid_input(T, dt):
+    """Test calculating maturity with invalid input."""
+    with pytest.raises(ValueError):
+        _concrete_creep_and_shrinkage.t_T(T, dt)