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

Issue:
Concrete refactoring dt
First attempt to refactor concrete:

- one abstract class and then subclasses for each design code
- new implementation on how attributes are used: by default they assume a None value. If overwritten by user, will use the new values
- added optional description/name for materials and concrete. Default behaviors for names are defined
- created a factory function that creates the proper type of concrete based on the code wanted (the active global one or another specified by user)
- simple test.py file shows the use of the new interface

TO BE DONE STILL:
- split the class in more files (for now it is all in a single concrete.py file): one file for abstract class and factory function + one file for each concrete implementation for the different codes
- some more value checking in the setters of properties in concrete
- improve docstrings (sphinx format is ok?)
- add concrete for Mc2020, Eurocode 1992:2004 and ACI once the framework is approved




Discussion / hints:

Fix patch:
diff --git a/structuralcodes/__init__.py b/structuralcodes/__init__.py
index 440bb17c..e886b7c0 100644
--- a/structuralcodes/__init__.py
+++ b/structuralcodes/__init__.py
@@ -1,7 +1,9 @@
 """A Python package that contains models from structural design codes"""
-from .code import set_design_code, get_design_codes, set_national_annex
+from .codes import set_design_code, get_design_codes, set_national_annex
 
-from .code import mc2010
+from . import materials
+from . import core
+from . import codes
 
 __version__ = ''
 
@@ -9,5 +11,7 @@
     'set_design_code',
     'get_design_codes',
     'set_national_annex',
-    'mc2010',
+    'codes',
+    'core',
+    'materials',
 ]
diff --git a/structuralcodes/code/__init__.py b/structuralcodes/codes/__init__.py
similarity index 94%
rename from structuralcodes/code/__init__.py
rename to structuralcodes/codes/__init__.py
index 47fcddd1..497e5086 100644
--- a/structuralcodes/code/__init__.py
+++ b/structuralcodes/codes/__init__.py
@@ -4,6 +4,13 @@
 
 from . import mc2010
 
+__all__ = [
+    'mc2010',
+    'set_design_code',
+    'get_design_codes',
+    'set_national_annex',
+]
+
 # Global code object used by material classes
 _CODE: t.Optional[types.ModuleType] = None
 
diff --git a/structuralcodes/code/mc2010/__init__.py b/structuralcodes/codes/mc2010/__init__.py
similarity index 100%
rename from structuralcodes/code/mc2010/__init__.py
rename to structuralcodes/codes/mc2010/__init__.py
diff --git a/structuralcodes/code/mc2010/_concrete_material_properties.py b/structuralcodes/codes/mc2010/_concrete_material_properties.py
similarity index 82%
rename from structuralcodes/code/mc2010/_concrete_material_properties.py
rename to structuralcodes/codes/mc2010/_concrete_material_properties.py
index 65c022c2..6be8b025 100644
--- a/structuralcodes/code/mc2010/_concrete_material_properties.py
+++ b/structuralcodes/codes/mc2010/_concrete_material_properties.py
@@ -11,7 +11,7 @@ def fcm(fck: float, delta_f: float = 8.0) -> float:
     Args:
         fck (float): The characteristic compressive strength in MPa.
 
-    Kwargs:
+    Keyword Args:
         delta_f (float): The difference between the mean and the
         characteristic strength.
 
@@ -38,34 +38,34 @@ def fctm(fck: float) -> float:
     return 2.12 * math.log(1 + 0.1 * fcm(fck))
 
 
-def fctkmin(fck: float) -> float:
+def fctkmin(_fctm: float) -> float:
     """Compute the lower bound value of the characteristic tensile strength
-    from the characteristic compressive strength.
+    from the mean tensile strength.
 
     fib Model Code 2010, Eq. (5.1-4)
 
     Args:
-        fck (float): The characteristic compressive strength in MPa.
+        _fctm (float): The mean tensile strength in MPa.
 
     Returns:
         float: Lower bound of the characteristic tensile strength in MPa.
     """
-    return 0.7 * fctm(fck)
+    return 0.7 * _fctm
 
 
-def fctkmax(fck: float) -> float:
+def fctkmax(_fctm: float) -> float:
     """Compute the upper bound value of the characteristic tensile strength
-    from the characteristic compressive strength.
+    from the mean tensile strength.
 
     fib Model Code 2010, Eq. (5.1-5)
 
     Args:
-        fck (float): The characteristic compressive strength in MPa.
+        _fctm (float): The mean tensile strength in MPa.
 
     Returns:
         float: Upper bound of the characteristic tensile strength in MPa.
     """
-    return 1.3 * fctm(fck)
+    return 1.3 * _fctm
 
 
 def Gf(fck: float) -> float:
diff --git a/structuralcodes/core/__init__.py b/structuralcodes/core/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/structuralcodes/core/base.py b/structuralcodes/core/base.py
new file mode 100644
index 00000000..d694c8ce
--- /dev/null
+++ b/structuralcodes/core/base.py
@@ -0,0 +1,50 @@
+"""Abstract base classes"""
+import abc
+import typing as t
+import warnings
+
+
+class Material(abc.ABC):
+    """Abstract base class for materials."""
+
+    def __init__(self, density: float, name: t.Optional[str] = None) -> None:
+        """Initializes an instance of a new material
+
+        Args:
+            density (float): density of the material in kg/m3
+
+        Keyword Args:
+            name (Optional[str]): descriptive name of the material
+        """
+        self._density = abs(density)
+        self._name = name if name is not None else "Material"
+
+    def update_attributes(self, updated_attributes: t.Dict) -> None:
+        """Function for updating the attributes specified in the input
+        dictionary
+
+        Args:
+            updated_attributes (dict): the dictionary of parameters to be
+                updated (not found parameters are skipped with a warning)
+        """
+        for key, value in updated_attributes.items():
+            if not hasattr(self, '_' + key):
+                str_list_keys = ', '.join(updated_attributes.keys())
+                str_warn = (
+                    f"WARNING: attribute '{key}' not found."
+                    " Ignoring the entry.\n"
+                    f"Used keys in the call: {str_list_keys}"
+                )
+                warnings.warn(str_warn)
+                continue
+            setattr(self, '_' + key, value)
+
+    @property
+    def name(self):
+        """Returns the name of the material"""
+        return self._name
+
+    @property
+    def density(self):
+        """Returns the density of the material in kg/m3"""
+        return self._density
diff --git a/structuralcodes/materials/__init__.py b/structuralcodes/materials/__init__.py
new file mode 100644
index 00000000..74f80344
--- /dev/null
+++ b/structuralcodes/materials/__init__.py
@@ -0,0 +1,7 @@
+"""Main entry point for materials."""
+from . import concrete
+
+
+__all__ = [
+    'concrete',
+]
diff --git a/structuralcodes/materials/concrete/__init__.py b/structuralcodes/materials/concrete/__init__.py
new file mode 100644
index 00000000..c54faff1
--- /dev/null
+++ b/structuralcodes/materials/concrete/__init__.py
@@ -0,0 +1,61 @@
+"""Concrete material"""
+import typing as t
+from structuralcodes.codes import _use_design_code
+from ._concrete import Concrete
+from ._concreteMC2010 import ConcreteMC2010
+
+__all__ = [
+    'create_concrete',
+    'Concrete',
+    'ConcreteMC2010',
+]
+
+
+def create_concrete(
+    fck: float,
+    name: t.Optional[str] = None,
+    density: float = 2400.0,
+    existing: bool = False,
+    design_code: t.Optional[str] = None,
+) -> t.Optional[Concrete]:
+    """
+    A factory function to create the correct type of concrete based on the
+    desired design code.
+
+    Args:
+        fck (float): Characteristic strength of concrete in MPa.
+            (if existing it is intended as the mean strength)
+
+    Keyword Args:
+        density (float): Density of Concrete in kg/m3 (default: 2400)
+        existing (bool): Boolean indicating if the concrete is of an
+            existing structure (default: False)
+        design_code (str): Optional string (default: None) indicating the
+            desired standard. If None (default) the globally used design
+            standard will be adopted. Otherwise the design standard specified
+            will be used for the instance of the material.
+            Currently available codes: 'mc2010'
+
+    Raises:
+        ValueError: if the design code is not valid or does not cover
+            concrete as a material.
+    """
+    # Get the code from the global variable
+    _code = _use_design_code(design_code)
+
+    # Check if the code is a proper concrete code
+    code = None
+    if _code is not None:
+        if 'concrete' in _code.__materials__:
+            code = _code
+    if code is None:
+        raise ValueError(
+            'The design code is not set, either use '
+            'structuralcodes.code.set_designcode, or provide a valid '
+            'string in the function.'
+        )
+
+    # Create the proper concrete object
+    if code.__title__ == 'fib Model Code 2010':
+        return ConcreteMC2010(fck, name, density, existing)
+    return None
diff --git a/structuralcodes/materials/concrete/_concrete.py b/structuralcodes/materials/concrete/_concrete.py
new file mode 100644
index 00000000..19ac2048
--- /dev/null
+++ b/structuralcodes/materials/concrete/_concrete.py
@@ -0,0 +1,45 @@
+"""Core implementation of the concrete material"""
+import abc
+import typing as t
+from structuralcodes.core.base import Material
+
+
+class Concrete(Material):
+    """The abstract concrete material."""
+
+    _fck: float
+    _existing: bool
+
+    def __init__(
+        self,
+        fck: float,
+        name: t.Optional[str] = None,
+        density: float = 2400,
+        existing: t.Optional[bool] = False,
+    ) -> None:
+        """Initializes an abstract concrete material"""
+        name = name if name is not None else "Concrete"
+        super().__init__(density=density, name=name)
+
+        self._fck = abs(fck)
+        if existing:
+            raise NotImplementedError(
+                'Existing concrete feature not implemented yet'
+            )
+        self._existing = existing
+
+    @property
+    def fck(self) -> float:
+        """Returns fck in MPa"""
+        return self._fck
+
+    @fck.setter
+    def fck(self, fck: float) -> None:
+        """Setter for fck (in MPa)"""
+        self._fck = abs(fck)
+        self._reset_attributes()
+
+    @abc.abstractmethod
+    def _reset_attributes(self):
+        """Each concrete should define its own _reset_attributes method
+        This is because fck setting, reset the object arguments"""
diff --git a/structuralcodes/materials/concrete/_concreteMC2010.py b/structuralcodes/materials/concrete/_concreteMC2010.py
new file mode 100644
index 00000000..17c14de2
--- /dev/null
+++ b/structuralcodes/materials/concrete/_concreteMC2010.py
@@ -0,0 +1,168 @@
+"""The concrete class for Model Code 2020 Concrete Material"""
+import typing as t
+import warnings
+
+from structuralcodes.codes import mc2010
+from ._concrete import Concrete
+
+
+class ConcreteMC2010(Concrete):
+    """Concrete implementation for MC 2010"""
+
+    _fcm: t.Optional[float] = None
+    _fctm: t.Optional[float] = None
+    _fctkmin: t.Optional[float] = None
+    _fctkmax: t.Optional[float] = None
+    _Gf: t.Optional[float] = None
+
+    def __init__(
+        self,
+        fck: float,
+        name: t.Optional[str] = None,
+        density: float = 2400.0,
+        existing: bool = False,
+    ):
+        """Initializes a new instance of Concrete for MC 2010
+
+        Args:
+            fck (float): Characteristic strength in MPa if concrete is not
+                existing.
+
+        Keyword Args:
+            name (str): A descriptive name for concrete
+            density (float): Density of material in kg/m3 (default: 2400)
+            existing (bool): The material is of an existing structure
+                (default: False)
+        """
+
+        if name is None:
+            name = f'C{round(fck):d}'
+        super().__init__(
+            fck=fck, name=name, density=density, existing=existing
+        )
+
+    def _reset_attributes(self):
+        self._fcm = None
+        self._fctm = None
+        self._fctkmin = None
+        self._fctkmax = None
+        self._Gf = None
+
+    @property
+    def fcm(self) -> float:
+        """Returns fcm in MPa.
+
+        Returns:
+            float: The mean compressive strength in MPa.
+        """
+        if self._fcm is not None:
+            return self._fcm
+        return mc2010.fcm(self._fck)
+
+    @fcm.setter
+    def fcm(self, value: float):
+        """Sets a user defined value for fcm
+
+        Args:
+            value (float): the value of fcm in MPa
+
+        Raises:
+            ValueError: if value is lower than fck
+        """
+        if abs(value) <= self._fck:
+            raise ValueError(
+                (
+                    'Mean compressive strength cannot be lower than',
+                    'characteristic strength.\n',
+                    'Current characteristing strength: ',
+                    f'fck = {self._fck}.',
+                    f'Current value: value = {value}',
+                )
+            )
+        self._fcm = abs(value)
+
+    @property
+    def fctm(self) -> float:
+        """Returns fctm in MPa
+
+        Returns:
+            float: The mean tensile strength in MPa
+        """
+        if self._fctm is not None:
+            return self._fctm
+        return mc2010.fctm(self._fck)
+
+    @fctm.setter
+    def fctm(self, value: float):
+        """Sets a user defined value for fctm
+
+        Args:
+            value (float): the value of fctm in MPa
+        """
+        if value > 0.5 * self._fck:
+            warnings.warn(
+                'A suspect value of fctm has been input. Please check.'
+            )
+        self._fctm = abs(value)
+
+    @property
+    def fctkmin(self) -> float:
+        """Returns fctkmin in MPa
+
+        Returns:
+            float: The lower bound tensile strength in MPa
+        """
+        if self._fctkmin is not None:
+            return self._fctkmin
+
+        return mc2010.fctkmin(self.fctm)
+
+    @fctkmin.setter
+    def fctkmin(self, value: float):
+        """Sets a user defined value for fctkmin
+
+        Args:
+            value (float): the value of fctkmin in MPa
+        """
+        self._fctkmin = abs(value)
+
+    @property
+    def fctkmax(self) -> float:
+        """Returns fctkmax in MPa
+
+        Returns:
+            float: The upper bound tensile strength in MPa
+        """
+        if self._fctkmax is not None:
+            return self._fctkmax
+
+        return mc2010.fctkmax(self.fctm)
+
+    @fctkmax.setter
+    def fctkmax(self, value: float):
+        """Sets a user defined value for fctkmax
+
+        Args:
+            value (float): the value of fctkmax in MPa
+        """
+        self._fctkmax = abs(value)
+
+    @property
+    def Gf(self) -> float:
+        """Fracture energy of concrete
+
+        Returns:
+            float: The fracture energy in N/m
+        """
+        if self._Gf is not None:
+            return self._Gf
+        return mc2010.Gf(self._fck)
+
+    @Gf.setter
+    def Gf(self, value: float):
+        """Sets a user defined value for fracture energy Gf
+
+        Args:
+            value (float): the value of Gf in N/m
+        """
+        self._Gf = abs(value)


Test patch:
diff --git a/tests/test_concrete_mc2010.py b/tests/test_concrete_mc2010.py
new file mode 100644
index 00000000..2c697508
--- /dev/null
+++ b/tests/test_concrete_mc2010.py
@@ -0,0 +1,335 @@
+"""Tests for the concrete mc2010"""
+import math
+
+import pytest
+
+from structuralcodes.materials.concrete import create_concrete, ConcreteMC2010
+
+# Series of tests using the factory function
+
+
+@pytest.mark.parametrize(
+    'design_code_to_set',
+    ['mc2010', 'MC2010', 'mC2010'],
+)
+@pytest.mark.parametrize(
+    'fck, expected_name',
+    [(20, 'C20'), (25, 'C25'), (30, 'C30'), (35, 'C35'), (40, 'C40')]
+)
+def test_create_concrete(design_code_to_set, fck, expected_name):
+    """Test creating a concrete with MC2010."""
+    # Arrange
+    expected_density = 2400
+
+    # Act
+    c = create_concrete(fck=fck, design_code=design_code_to_set)
+
+    # Assert
+    assert isinstance(c, ConcreteMC2010)
+    assert c.name == expected_name
+    assert c.density == expected_density
+
+
+def test_create_concrete_wrong_code():
+    """Test if a ValueError exception raises when passing the wrong code."""
+    with pytest.raises(ValueError):
+        create_concrete(fck=25, design_code='EN1995')
+
+
+@pytest.mark.parametrize(
+    'fck, fcm',
+    [(12, 20), (35, 43), (55, 63), (90, 98), (120, 128)],
+)
+def test_update_attributes(fck, fcm):
+    """Test update_attributes function"""
+    c = create_concrete(fck=fck, design_code='mc2010')
+    c.update_attributes({'fcm': fcm})
+    # Test a warning is raised when a not valid key is inputted
+    with pytest.warns(UserWarning):
+        c.update_attributes({'not_valid_key': fcm})
+
+    assert c.fcm is not None
+    assert c.fcm == fcm
+
+
+def test_not_implemented_existing_error():
+    """Test existing not implemented function"""
+    with pytest.raises(NotImplementedError):
+        create_concrete(fck=25, design_code='mc2010', existing=True)
+
+
+# Series of tests using ConcreteMC2010 class
+
+
+fck_parametrized = pytest.mark.parametrize(
+    'fck',
+    [20, 25, 30, 35, 40]
+)
+
+
+@fck_parametrized
+def test_fck_getter(fck):
+    """
+    Test fck getter
+    """
+    c = ConcreteMC2010(fck)
+
+    assert c.fck == fck
+
+
+@fck_parametrized
+def test_fck_setter(fck):
+    """
+    Test fck setter
+    """
+    c = ConcreteMC2010(fck)
+    c.fck = fck + 5
+
+    assert c.fck == fck + 5
+
+
+def test_properties_initialized_to_none():
+    """
+    Test if a ConcreteMC2010 when created has the attributes set to None
+    """
+    c = ConcreteMC2010(fck=25)
+
+    assert c._fcm is None
+    assert c._fctm is None
+    assert c._fctkmin is None
+    assert c._fctkmax is None
+    assert c._Gf is None
+
+
+def test_reset_properties():
+    """
+    Test _reset_attributes function
+    """
+    c = ConcreteMC2010(fck=25)
+
+    c._reset_attributes()
+
+    assert c._fcm is None
+    assert c._fctm is None
+    assert c._fctkmin is None
+    assert c._fctkmax is None
+    assert c._Gf is None
+
+
+fcm_parametrized = pytest.mark.parametrize(
+    'test_input, expected',
+    [(12, 20), (35, 43), (55, 63), (90, 98), (120, 128)],    
+)
+
+
+@fcm_parametrized
+def test_fcm_getter(test_input, expected):
+    """Test the fcm getter."""
+    c = ConcreteMC2010(fck=test_input)
+    assert math.isclose(
+        c.fcm, expected
+    )
+
+
+@fcm_parametrized
+def test_fcm_setter(test_input, expected):
+    """Test the fcm setter."""
+    c = ConcreteMC2010(fck=test_input)
+    c.fcm = expected
+
+    assert math.isclose(
+        c.fcm, expected
+    )
+
+
+@pytest.mark.parametrize(
+    'test_input',
+    [12, 35, 55, 90, 120],
+)
+def test_fcm_setter_exception(test_input):
+    """Test the fcm setter with a wrong value."""
+    c = ConcreteMC2010(fck=test_input)
+    with pytest.raises(ValueError):
+        c.fcm = test_input - 1
+
+
+fctm_parmetrized = pytest.mark.parametrize(
+    'test_input, expected',
+    [
+        (12, 1.6),
+        (16, 1.9),
+        (20, 2.2),
+        (25, 2.6),
+        (30, 2.9),
+        (35, 3.2),
+        (40, 3.5),
+        (45, 3.8),
+        (50, 4.1),
+        (55, 4.2),
+        (60, 4.4),
+        (70, 4.6),
+        (80, 4.8),
+        (90, 5.0),
+        (100, 5.2),
+        (110, 5.4),
+        (120, 5.6),
+    ],
+)
+
+
+@fctm_parmetrized
+def test_fctm_getter(test_input, expected):
+    """Test the fctm getter function."""
+    c = ConcreteMC2010(fck=test_input)
+    assert math.isclose(
+        c.fctm, expected, rel_tol=0.02
+    )
+
+
+@fctm_parmetrized
+def test_fctm_setter(test_input, expected):
+    """Test the fctm setter function."""
+    c = ConcreteMC2010(fck=test_input)
+    c.fctm = expected
+
+    assert math.isclose(
+        c.fctm, expected
+    )
+
+
+@pytest.mark.parametrize(
+    'test_input',
+    [10, 15, 20, 25, 30, 35]
+)
+def test_fctm_setter_warning(test_input):
+    """
+    Test the fctm setter function.
+    Check that a warning is raised when
+    trying to set a value higher than 0.5 times fck"""
+    c = ConcreteMC2010(fck=test_input)
+
+    with pytest.warns(UserWarning):
+        c.fctm = test_input * 0.5 + 1
+
+
+fctkmin_parametrized = pytest.mark.parametrize(
+    'test_input, expected',
+    [
+        (12, 1.1),
+        (16, 1.3),
+        (20, 1.5),
+        (25, 1.8),
+        (30, 2.0),
+        (35, 2.2),
+        (40, 2.5),
+        (45, 2.7),
+        (50, 2.9),
+        (55, 3.0),
+        (60, 3.1),
+        (70, 3.2),
+        (80, 3.4),
+        (90, 3.5),
+        (100, 3.7),
+        (110, 3.8),
+        (120, 3.9),
+    ],
+)
+
+
+@fctkmin_parametrized
+def test_fctkmin_getter(test_input, expected):
+    """Test the fctkmin getter function."""
+    c = ConcreteMC2010(fck=test_input)
+    assert math.isclose(
+        c.fctkmin, expected, rel_tol=0.031
+    )
+
+
+@fctkmin_parametrized
+def test_fctkmin_setter(test_input, expected):
+    """Test the fctkmin setter function."""
+    c = ConcreteMC2010(fck=test_input)
+    c.fctkmin = expected
+
+    assert math.isclose(
+        c.fctkmin, expected
+    )
+
+
+fctkmax_parmetrized = pytest.mark.parametrize(
+    'test_input, expected',
+    [
+        (12, 2.0),
+        (16, 2.5),
+        (20, 2.9),
+        (25, 3.3),
+        (30, 3.8),
+        (35, 4.2),
+        (40, 4.6),
+        (45, 4.8),
+        (50, 5.3),
+        (55, 5.5),
+        (60, 5.7),
+        (70, 6.0),
+        (80, 6.3),
+        (90, 6.6),
+        (100, 6.8),
+        (110, 7.0),
+        (120, 7.2),
+    ],
+)
+
+
+@fctkmax_parmetrized
+def test_fctkmax_getter(test_input, expected):
+    """Test the fctkmax getter function."""
+    c = ConcreteMC2010(fck=test_input)
+    assert math.isclose(
+        c.fctkmax, expected, rel_tol=0.028
+    )
+
+
+@fctkmax_parmetrized
+def test_fctkmax_setter(test_input, expected):
+    """Test the fctkmax setter function."""
+    c = ConcreteMC2010(fck=test_input)
+    c.fctkmax = expected
+
+    assert math.isclose(
+        c.fctkmax, expected
+    )
+
+
+gf_parametrized = pytest.mark.parametrize(
+    'test_input, expected',
+    [
+        (12, 125.172),
+        (35, 143.664),
+        (55, 153.888),
+        (90, 166.626),
+        (120, 174.832),
+    ],
+)
+
+
+@gf_parametrized
+def test_Gf_getter(test_input, expected):
+    """Test the Gf getter function."""
+    c = ConcreteMC2010(fck=test_input)
+    assert math.isclose(
+        c.Gf, expected, rel_tol=1e-5
+    )
+
+
+@gf_parametrized
+def test_Gf_setter(test_input, expected):
+    """Test the Gf setter function."""
+    c = ConcreteMC2010(fck=test_input)
+    c.Gf = expected
+
+    assert math.isclose(
+        c.Gf, expected
+    )
+
+
+
diff --git a/tests/test_get_set_design_code.py b/tests/test_get_set_design_code.py
index 65686dcf..1711be83 100644
--- a/tests/test_get_set_design_code.py
+++ b/tests/test_get_set_design_code.py
@@ -19,14 +19,14 @@ def test_set_design_code(design_code_to_set):
     structuralcodes.set_design_code(design_code_to_set)
 
     # Assert
-    assert isinstance(structuralcodes.code._CODE, types.ModuleType)
-    assert structuralcodes.code._CODE.__title__ == expected_design_code_title
+    assert isinstance(structuralcodes.codes._CODE, types.ModuleType)
+    assert structuralcodes.codes._CODE.__title__ == expected_design_code_title
 
 
 def test_get_design_codes():
     """Test get a list of implemented design codes."""
     # Arrange
-    expected_list_of_codes = list(structuralcodes.code._DESIGN_CODES.keys())
+    expected_list_of_codes = list(structuralcodes.codes._DESIGN_CODES.keys())
 
     # Act
     available_codes = structuralcodes.get_design_codes()
@@ -48,7 +48,7 @@ def test_set_national_annex(na_to_set):
     structuralcodes.set_national_annex(na_to_set)
 
     # Assert
-    assert structuralcodes.code._NATIONAL_ANNEX == expected_na
+    assert structuralcodes.codes._NATIONAL_ANNEX == expected_na
 
 
 @pytest.mark.parametrize(
@@ -61,7 +61,7 @@ def test_use_design_code(design_code_to_user):
     expected_design_code_title = 'fib Model Code 2010'
 
     # Act
-    code_to_use = structuralcodes.code._use_design_code(design_code_to_user)
+    code_to_use = structuralcodes.codes._use_design_code(design_code_to_user)
 
     # Assert
     assert isinstance(code_to_use, types.ModuleType)
@@ -76,7 +76,7 @@ def test_use_design_code_none():
     structuralcodes.set_design_code(design_code_to_set)
 
     # Act
-    code_to_use = structuralcodes.code._use_design_code()
+    code_to_use = structuralcodes.codes._use_design_code()
 
     # Assert
     assert isinstance(code_to_use, types.ModuleType)
diff --git a/tests/test_mc2010_material_properties.py b/tests/test_mc2010_material_properties.py
index 13e339f7..e43bd26e 100644
--- a/tests/test_mc2010_material_properties.py
+++ b/tests/test_mc2010_material_properties.py
@@ -3,7 +3,7 @@
 
 import pytest
 
-from structuralcodes.code.mc2010 import _concrete_material_properties
+from structuralcodes.codes.mc2010 import _concrete_material_properties
 
 
 @pytest.mark.parametrize(
@@ -71,7 +71,9 @@ def test_fctm(test_input, expected):
 def test_fctkmin(test_input, expected):
     """Test the fctkmin function."""
     assert math.isclose(
-        _concrete_material_properties.fctkmin(test_input),
+        _concrete_material_properties.fctkmin(
+            _concrete_material_properties.fctm(test_input)
+        ),
         expected,
         rel_tol=0.031,
     )
@@ -102,7 +104,9 @@ def test_fctkmin(test_input, expected):
 def test_fctkmax(test_input, expected):
     """Test the fctkmax function."""
     assert math.isclose(
-        _concrete_material_properties.fctkmax(test_input),
+        _concrete_material_properties.fctkmax(
+            _concrete_material_properties.fctm(test_input)
+        ),
         expected,
         rel_tol=0.028,
     )
@@ -115,7 +119,7 @@ def test_fctkmax(test_input, expected):
         (35, 143.664),
         (55, 153.888),
         (90, 166.626),
-        (120, 174.832)
+        (120, 174.832),
     ],
 )
 def test_Gf(test_input, expected):