Repository: StemVibrations/STEM (language: python)

Issue:
Fix read the docs



Discussion / hints:

Fix patch:
diff --git a/requirements.txt b/requirements.txt
index 184da363..5ad7278e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 numpy==1.24.2
-gmsh_utils @ git+https://github.com/StemVibrations/gmsh_utils@main
+matplotlib==3.7.1
+gmsh_utils @ git+https://github.com/StemVibrations/gmsh_utils@vakantie_branch
diff --git a/requirements_dev.txt b/requirements_dev.txt
index d6af7035..fb858f40 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,5 +1,6 @@
 numpy==1.24.2
-gmsh_utils @ git+https://github.com/StemVibrations/gmsh_utils@main
+matplotlib==3.7.1
+gmsh_utils @ git+https://github.com/StemVibrations/gmsh_utils@vakantie_branch
 pytest==7.2.2
 pytest-cov==4.0.0
 tox==4.4.11
diff --git a/run_stem/demo_create_gmsh_mesh.py b/run_stem/demo_create_gmsh_mesh.py
index 54934ca9..9fb7043c 100644
--- a/run_stem/demo_create_gmsh_mesh.py
+++ b/run_stem/demo_create_gmsh_mesh.py
@@ -24,7 +24,6 @@
 mesh_output_dir = "./"
 
 
-
 gmsh_io = GmshIO()
 
 gmsh_io.generate_gmsh_mesh(input_points, extrusion_length, element_size, dims, name_label, mesh_name, mesh_output_dir,
diff --git a/run_stem/demo_run_kratos_model.py b/run_stem/demo_run_kratos_model.py
new file mode 100644
index 00000000..916c69c3
--- /dev/null
+++ b/run_stem/demo_run_kratos_model.py
@@ -0,0 +1,30 @@
+import os
+import sys
+
+# IMPORTANT !!!! SPECIFY LOCAL PATH TO KRATOS...
+pth_kratos = r"C:\Please\Define\Installation\Directory\Of\Kratos"
+materialfname = "MaterialParameters.json"
+projectfname = "ProjectParameters.json"
+meshfname = "test_simple_dynamic.mdpa"
+
+# --------------------------------------------------------------------------------------------------------------------
+sys.path.append(os.path.join(pth_kratos, "KratosGeoMechanics"))
+sys.path.append(os.path.join(pth_kratos, r"KratosGeoMechanics\libs"))
+
+import KratosMultiphysics.GeoMechanicsApplication
+from KratosMultiphysics.GeoMechanicsApplication.geomechanics_analysis import (
+    GeoMechanicsAnalysis,
+)
+
+project_folder = "dir_test"
+# -------------------------------------------------------------------------------------
+# run Kratos!!
+
+os.chdir(project_folder)
+
+with open(projectfname, "r") as parameter_file:
+    parameters = KratosMultiphysics.Parameters(parameter_file.read())
+
+model = KratosMultiphysics.Model()
+simulation = GeoMechanicsAnalysis(model, parameters)
+simulation.Run()
diff --git a/setup.cfg b/setup.cfg
index 75bd09b7..09b24a5e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -17,7 +17,8 @@ packages =
 include_package_data = True
 install_requires =
     numpy>=1.24
-    gmsh_utils @ git+https://github.com/StemVibrations/gmsh_utils@main
+    matplotlib>=3.7.1
+    gmsh_utils @ git+https://github.com/StemVibrations/gmsh_utils@vakantie_branch
 python_requires = >=3.8
 
 [options.extras_require]
diff --git a/stem/IO/io_utils.py b/stem/IO/io_utils.py
new file mode 100644
index 00000000..3a9b05c4
--- /dev/null
+++ b/stem/IO/io_utils.py
@@ -0,0 +1,118 @@
+from typing import Sequence, Dict, Any, List, Union, Optional, Tuple
+from pathlib import Path
+import json
+from copy import deepcopy
+
+from stem.table import Table
+from stem.boundary import BoundaryParametersABC
+from stem.load import LoadParametersABC
+
+class IOUtils:
+
+    @staticmethod
+    def write_json_file(output_folder: str, file_name: str, dictionary: Dict[Any, Any]):
+        """
+        Write a dictionary to a json file.
+
+        Args:
+            - output_folder (str): output folder path
+            - file_name (str): name of the json file
+            - dictionary (Dict[Any, Any]): dictionary to be written to the json file
+
+        """
+        # write json file
+        output_folder_pth = Path(output_folder)
+        output_folder_pth.mkdir(exist_ok=True, parents=True)
+
+        output_path_file = output_folder_pth.joinpath(file_name)
+        json.dump(dictionary, open(output_path_file, "w"), indent=4)
+
+    @staticmethod
+    def create_value_and_table(part_name: str, parameters: Union["LoadParametersABC", "BoundaryParametersABC"]) \
+            -> Tuple[List[float], List[int]]:
+        """
+        Assemble values and tables for the boundary condition from the `value` attribute of the boundary parameters.
+        If the displacement or rotation is time-dependent, a `table` is required. If the displacement or rotation is
+        constant, a `value` is required. Each direction (x,y,z), requires either a `table` or a `value`. When a `table`
+        is provided, the `value` is set to 0. If a `value` is provided, the `table` is set to 0.
+
+        Args:
+            - part_name (str): name of the model part on which the boundary is applied.
+            - parameters (Union[:class:`stem.load.LoadParametersABC`, \
+                                :class:`stem.boundary.BoundaryParametersABC`]): boundary parameters object.
+
+        Raises:
+            - ValueError: if table ids are not initialised.
+            - ValueError: when element in `parameters.value` is not of type int, float or Table.
+            - ValueError: when provided parameters class doesn't implement values (e.g. AbsorbingBoundary).
+
+        Returns:
+            - _value (List[float]): list of values for the boundary condition or load
+            - _table (List[int]): list of table ids for the boundary condition \
+                or load
+        """
+
+        _value: List[float] = []
+        _table: List[int] = []
+
+        if hasattr(parameters, "value"):
+
+            # check the values per direction
+            for vv in parameters.value:
+                # if a table is provided, the value is set to 0
+                if isinstance(vv, Table):
+                    if vv.id is None:
+                        raise ValueError(f"Table id is not initialised for values in {parameters.__class__.__name__}"
+                                         f" in model part: {part_name}.")
+                    _table.append(vv.id)
+                    _value.append(0.0)
+                # if a value is provided, the table is set to 0
+                elif isinstance(vv, (int, float)):
+                    _table.append(0)
+                    _value.append(float(vv))
+                else:
+                    raise ValueError(f"'value' attribute in {parameters.__class__.__name__} in model part: "
+                                     f"{part_name} is neither a Table object, nor a float nor an integer.")
+
+        else:
+            raise ValueError(f"Attribute `value` does not exist in class: {parameters.__class__.__name__}.")
+
+        return _value, _table
+
+    @staticmethod
+    def create_vector_constraint_table_process_dict(global_domain: str, part_name: str,
+                                                    parameters: Union[LoadParametersABC, BoundaryParametersABC],
+                                                    variable_name: str) -> Dict[str, Any]:
+        """
+        Creates a dictionary containing the vector constraint table process parameters. The vector constraint table
+        process is used for loads and boundary conditions. This process applies either a constant value or a
+        time-dependent table to the load or boundary condition.
+
+        Args:
+            - global_domain (str): name of the global domain
+            - part_name (str): name of the model part on which the load is applied
+            - parameters (Union[:class:`stem.load.LoadParametersABC`, \
+                                :class:`stem.boundary.BoundaryParametersABC`]): boundary parameters object.
+            - variable_name (str): name of the variable to which the boundary condition or load is applied
+
+        Returns:
+            - Dict[str, Any]: dictionary containing the load parameters
+        """
+
+        # initialize load dictionary
+        load_dict: Dict[str, Any] = {
+            "python_module": "apply_vector_constraint_table_process",
+            "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
+            "process_name": "ApplyVectorConstraintTableProcess",
+            "Parameters": deepcopy(parameters.__dict__),
+        }
+
+        load_dict["Parameters"]["model_part_name"] = f"{global_domain}.{part_name}"
+        load_dict["Parameters"]["variable_name"] = variable_name
+
+        # get tables and values
+        _value, _table = IOUtils.create_value_and_table(part_name, parameters)
+        load_dict["Parameters"]["table"] = _table
+        load_dict["Parameters"]["value"] = _value
+
+        return load_dict
\ No newline at end of file
diff --git a/stem/IO/kratos_additional_processes_io.py b/stem/IO/kratos_additional_processes_io.py
index db4fdb66..436b706e 100644
--- a/stem/IO/kratos_additional_processes_io.py
+++ b/stem/IO/kratos_additional_processes_io.py
@@ -1,12 +1,11 @@
 from typing import Any, Dict, Union
-from copy import deepcopy
 
 from stem.additional_processes import *
 
 
 class KratosAdditionalProcessesIO:
     """
-    Class containing methods to write boundary conditions to Kratos
+    Class containing methods for additional Kratos processes
 
     Attributes:
         - domain (str): name of the Kratos domain
@@ -42,11 +41,12 @@ def __create_excavation_dict(
             "python_module": "apply_excavation_process",
             "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
             "process_name":  "ApplyExcavationProcess",
-            "Parameters": parameters.__dict__,
+            "Parameters": {},
         }
 
         boundary_dict["Parameters"]["model_part_name"] = f"{self.domain}.{part_name}"
         boundary_dict["Parameters"]["variable_name"] = "EXCAVATION"
+        boundary_dict["Parameters"]["deactivate_soil_part"] = parameters.deactivate_body_model_part
 
         return boundary_dict
 
@@ -58,7 +58,7 @@ def create_additional_processes_dict(
 
         Args:
             - part_name (str): part name where the boundary condition is applied
-            - parameters (:class:`stem.additional_processes.AdditionalProcessesParametersABC`): additional process
+            - parameters (:class:`stem.additional_processes.AdditionalProcessesParametersABC`): additional process \
                 parameters object
 
         Returns:
diff --git a/stem/IO/kratos_boundaries_io.py b/stem/IO/kratos_boundaries_io.py
index 7cb5ea16..6d0f153b 100644
--- a/stem/IO/kratos_boundaries_io.py
+++ b/stem/IO/kratos_boundaries_io.py
@@ -1,7 +1,8 @@
-from typing import Any, Dict, Union
 from copy import deepcopy
+from typing import Any, Dict, Union, List
 
 from stem.boundary import *
+from stem.IO.io_utils import IOUtils
 
 
 class KratosBoundariesIO:
@@ -23,61 +24,6 @@ def __init__(self, domain: str):
         """
         self.domain = domain
 
-    def __create_displacement_constraint_dict(
-        self, part_name: str, parameters: DisplacementConstraint
-    ) -> Dict[str, Any]:
-        """
-        Creates a dictionary containing the displacement constraint parameters
-
-        Args:
-            - part_name (str): part name where the boundary condition is applied
-            - parameters (:class:`stem.boundary.DisplacementConstraint`): displacement constraint parameters object
-        Returns:
-            - Dict[str, Any]: dictionary containing the boundary parameters
-        """
-
-        # initialize boundary dictionary
-        boundary_dict: Dict[str, Any] = {
-            "python_module": "apply_vector_constraint_table_process",
-            "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
-            "process_name": "ApplyVectorConstraintTableProcess",
-            "Parameters": deepcopy(parameters.__dict__),
-        }
-
-        boundary_dict["Parameters"]["model_part_name"] = f"{self.domain}.{part_name}"
-        boundary_dict["Parameters"]["variable_name"] = "DISPLACEMENT"
-        boundary_dict["Parameters"]["table"] = [0, 0, 0]
-
-        return boundary_dict
-
-    def __create_rotation_constraint_dict(
-        self, part_name: str, parameters: RotationConstraint
-    ) -> Dict[str, Any]:
-        """
-        Creates a dictionary containing the rotation constraint parameters
-
-        Args:
-            - part_name (str): part name where the boundary condition is applied
-            - parameters (:class:`stem.boundary.RotationConstraint`): rotation constraint parameters object
-
-        Returns:
-            - Dict[str, Any]: dictionary containing the boundary parameters
-        """
-
-        # initialize boundary dictionary
-        boundary_dict: Dict[str, Any] = {
-            "python_module": "apply_vector_constraint_table_process",
-            "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
-            "process_name": "ApplyVectorConstraintTableProcess",
-            "Parameters": deepcopy(parameters.__dict__),
-        }
-
-        boundary_dict["Parameters"]["model_part_name"] = f"{self.domain}.{part_name}"
-        boundary_dict["Parameters"]["variable_name"] = "ROTATION"
-        boundary_dict["Parameters"]["table"] = [0, 0, 0]
-
-        return boundary_dict
-
     def __create_absorbing_boundary_dict(
         self, part_name: str, parameters: AbsorbingBoundary
     ) -> Dict[str, Any]:
@@ -97,7 +43,7 @@ def __create_absorbing_boundary_dict(
             "python_module": "set_absorbing_boundary_parameters_process",
             "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
             "process_name": "SetAbsorbingBoundaryParametersProcess",
-            "Parameters": parameters.__dict__,
+            "Parameters": deepcopy(parameters.__dict__),
         }
 
         boundary_dict["Parameters"]["model_part_name"] = f"{self.domain}.{part_name}"
@@ -121,9 +67,11 @@ def create_boundary_condition_dict(
         # add boundary parameters to dictionary based on boundary type.
 
         if isinstance(parameters, DisplacementConstraint):
-            return self.__create_displacement_constraint_dict(part_name, parameters)
+            return IOUtils.create_vector_constraint_table_process_dict(self.domain, part_name, parameters,
+                                                                       "DISPLACEMENT")
         elif isinstance(parameters, RotationConstraint):
-            return self.__create_rotation_constraint_dict(part_name, parameters)
+            return IOUtils.create_vector_constraint_table_process_dict(self.domain, part_name, parameters,
+                                                                       "ROTATION")
         elif isinstance(parameters, AbsorbingBoundary):
             return self.__create_absorbing_boundary_dict(part_name, parameters)
         else:
diff --git a/stem/IO/kratos_io.py b/stem/IO/kratos_io.py
index b7c94453..00bba490 100644
--- a/stem/IO/kratos_io.py
+++ b/stem/IO/kratos_io.py
@@ -1,12 +1,37 @@
+from functools import reduce
+from pathlib import Path
+from typing import List, Dict, Any, Optional, Sequence
+
+import numpy as np
 
 from stem.IO.kratos_boundaries_io import KratosBoundariesIO
 from stem.IO.kratos_loads_io import KratosLoadsIO
 from stem.IO.kratos_material_io import KratosMaterialIO
 from stem.IO.kratos_output_io import KratosOutputsIO
+from stem.IO.kratos_solver_io import KratosSolverIO
+from stem.structural_material import *
+from stem.boundary import BoundaryParametersABC, AbsorbingBoundary, DisplacementConstraint, RotationConstraint
+from stem.load import LoadParametersABC, LineLoad, MovingLoad, SurfaceLoad, PointLoad
+from stem.mesh import Element, Node
+from stem.model import Model
+from stem.model_part import ModelPart, BodyModelPart
+from stem.table import Table
+from stem.output import Output
+from stem.utils import Utils
+from stem.IO.io_utils import IOUtils
 
-
+# define domain name
 DOMAIN = "PorousDomain"
 
+# indentation between entries in the mdpa file. Defaults to 2.
+INDENTATION = 2
+# format for integers
+FORMAT_INTEGER: str = "{:d}"
+# format for floats (long)
+FORMAT_FLOAT_LONG: str = " {:.10f}"
+# format for floats (short)
+FORMAT_FLOAT_SHORT: str = " {:.4f}"
+
 
 class KratosIO:
     """
@@ -14,8 +39,13 @@ class KratosIO:
 
     Attributes:
         - ndim (int): The number of dimensions of the problem.
-        - loads_io (:class:`stem.IO.kratos_loads_io.KratosLoadsIO`): The loads IO object.
         - material_io (:class:`stem.IO.kratos_material_io.KratosMaterialIO`): The material IO object.
+        - loads_io (:class:`stem.IO.kratos_loads_io.KratosLoadsIO`): The loads IO object.
+        - boundaries_io (:class:`stem.IO.kratos_boundaries_io.KratosBoundariesIO`): The boundaries IO object.
+        - outputs_io (:class:`stem.IO.kratos_output_io.KratosOutputsIO`): The outputs IO object.
+        - model_io (:class:`stem.IO.kratos_model_io.KratosModelIO`): The model IO object.
+        - solver_io (:class:`stem.IO.kratos_solver_io.KratosSolverIO`): The solver IO object.
+
     """
 
     def __init__(self, ndim: int):
@@ -23,7 +53,7 @@ def __init__(self, ndim: int):
         Constructor of KratosIO class
 
         Args:
-            - ndim: The number of dimensions of the problem.
+            - ndim (int): The number of dimensions of the problem.
         """
 
         self.ndim = ndim
@@ -32,49 +62,930 @@ def __init__(self, ndim: int):
         self.loads_io = KratosLoadsIO(DOMAIN)
         self.boundaries_io = KratosBoundariesIO(DOMAIN)
         self.outputs_io = KratosOutputsIO(DOMAIN)
+        self.solver_io = KratosSolverIO(self.ndim, DOMAIN)
+
+    @staticmethod
+    def __is_body_model_part(model_part: ModelPart):
+        """
+        Check whether the model part is a body model part.
+
+        Args:
+            - model_part (:class:`stem.model_part.ModelPart`): the model part
+
+        Returns:
+            - bool: whether the model part is body
+        """
+        return isinstance(model_part, BodyModelPart)
+
+    @staticmethod
+    def __check_if_process_writes_conditions(process_model_part: ModelPart) -> bool:
+        """
+        Check whether process needs to write condition elements. For example PointLoad,
+        Excavation and DisplacementConstraint do not need condition elements.
+
+        Args:
+            - process_model_part (:class:`stem.model_part.ModelPart`): the process model part to write to mdpa.
+
+        Returns:
+            - bool: whether the process model part writes condition elements
+        """
+        return isinstance(process_model_part.parameters, (LineLoad, MovingLoad, SurfaceLoad, AbsorbingBoundary))
+
+    def __initialise_process_model_part_ids(self, model: Model):
+        """
+        Resets the process model part ids.
+
+        Args:
+            - model (:class:`stem.model.Model`): the model object containing the process model parts.
+
+        """
+
+        # reset all condition ids
+        new_id = 0
+        for pmp in model.process_model_parts:
+            # if the process writes condition add an id
+            if self.__check_if_process_writes_conditions(pmp):
+                new_id += 1
+                pmp.id = new_id
+
+    @staticmethod
+    def __initialise_body_model_part_ids(model: Model):
+        """
+        Resets the body model part ids.
+
+        Args:
+            - model (:class:`stem.model.Model`): the model object containing the body model parts.
+
+        """
+        # reset all body model part ids
+        for ix, bmp in enumerate(model.body_model_parts):
+            bmp.id = ix + 1
+
+    @staticmethod
+    def __get_unique_tables_process_model_part(process_model_part: ModelPart) -> List[Table]:
+        """
+        Retrieve all the memory-unique tables in the model part.
+
+        Args:
+            - process_model_part (:class:`stem.model_part.ModelPart`): the process model part containing \
+                the tables.
+
+        Returns:
+            - tables (List[:class:`stem.table.Table`]): list of the tables in the process model part.
+        """
+
+        tables: List[Table] = []
+        if isinstance(process_model_part.parameters, (PointLoad, LineLoad, SurfaceLoad, DisplacementConstraint,
+                                                      RotationConstraint)):
+            for vv in process_model_part.parameters.value:
+                if isinstance(vv, Table):
+                    tables.append(vv)
+
+        return Utils.get_unique_objects(tables)
+
+    def __get_unique_tables(self, model: Model) -> List[Table]:
+        """
+        Retrieve all the memory-unique tables in the model.
+
+        Args:
+            - model (:class:`stem.model.Model`): the model object containing the info on the loads.
+
+        Returns:
+            - tables (List[:class:`stem.table.Table`]): list of the unique table objects in the models
+        """
+
+        tables: List[Table] = []
+        for pmp in model.process_model_parts:
+            tables.extend(self.__get_unique_tables_process_model_part(pmp))
+
+        return Utils.get_unique_objects(tables)
+
+    def __initialise_table_ids(self, model: Model):
+        """
+        Initialise or reset the id of the tables contained in the load parameters objects.
+
+        Args:
+            - model (:class:`stem.model.Model`): the model object containing the body model parts.
+
+        """
+
+        unique_tables = self.__get_unique_tables(model)
+
+        for ix, table in enumerate(unique_tables):
+            table.id = ix + 1
+
+    def initialise_model_ids(self, model: Model):
+        """
+        Initialise the ids of the model parts and tables.
+
+        Args:
+            model (:class:`stem.model.Model`): the model object containing the model parts and tables.
+
+        """
+
+        self.__initialise_table_ids(model)
+        self.__initialise_process_model_part_ids(model)
+        self.__initialise_body_model_part_ids(model)
+
+    @staticmethod
+    def __write_sub_model_part_block(buffer: List[str], block_name: str,
+                                     block_entities: Optional[List[Optional[int]]] = None) -> List[str]:
+        """
+        Helping function to write the sub-model part blocks for the model parts.
+
+        Args:
+            - buffer (List[str]): buffer containing the sub-model part info to be updated with the current block.
+            - block_name (str): block name, it can be one of Tables, Nodes, Elements or Conditions.
+            - block_entities (Optional[List[Optional[int]]]): ids to be written to the block. If None, an empty block is
+            written.
+
+        Returns:
+            - buffer (List[str]): updated buffer with info of the current block.
+        """
+
+        # define indentation
+        space = " " * INDENTATION
+
+        # append header
+        buffer.append(f"{space}Begin SubModelPart{block_name}")
+        # write block entities
+        if block_entities is not None:
+            fmt = f"{space}{FORMAT_INTEGER}"
+            buffer += [fmt.format(entity) for entity in block_entities]
+
+        # append footer
+        buffer.append(f"{space}End SubModelPart{block_name}")
+        return buffer
+
+    def write_submodelpart_body_model_part(self, body_model_part: BodyModelPart) -> List[str]:
+        """
+        Writes the submodelpart block for a body model part (physical parts with materials).
+
+        Args:
+            - body_model_part (:class:`stem.model_part.BodyModelPart`): the body model part to write to mdpa.
+
+        Raises:
+            - ValueError: if model part is not a body model part
+            - ValueError: if mesh not initialised first
+
+        Returns:
+            - block_text (List[str]): list of strings for the submodelpart. Each element is a line in the mdpa file.
+        """
+        # validate part is body model part
+        if not self.__is_body_model_part(body_model_part):
+            raise ValueError(
+                f"Model part {body_model_part.name} is not a body model part!"
+            )
+
+        # check if mesh is initialised
+        if body_model_part.mesh is None:
+            raise ValueError(
+                f"Model part {body_model_part.name} has not been meshed."
+                f"Before creating the mdpa file, the model part needs to be meshed."
+                f"Please run Model.generate_mesh()"
+            )
+
+        # initialise block
+        block_text = ["", f"Begin SubModelPart {body_model_part.name}"]
+
+        # define type entities
+        entities: List[Optional[int]]
+
+        # write tables
+        block_text = self.__write_sub_model_part_block(
+            block_text, block_name="Tables", block_entities=None
+        )
+
+        # write nodes
+        entities = [node.id for node in body_model_part.mesh.nodes]
+        block_text = self.__write_sub_model_part_block(
+            block_text, block_name="Nodes", block_entities=entities
+        )
+
+        # write elements
+        entities = [el.id for el in body_model_part.mesh.elements]
+        block_text = self.__write_sub_model_part_block(
+            block_text, block_name="Elements", block_entities=entities
+        )
+        block_text += [f"End SubModelPart", ""]
+        return block_text
+
+    def write_submodelpart_process_model_part(self, process_model_part: ModelPart) -> List[str]:
+        """
+        Writes the submodelpart block for a process model part (loads, boundary conditions or
+        additional processes such as excavations).
+
+        Args:
+            - process_model_part (:class:`stem.model_part.ModelPart`): the process model part to write to mdpa.
+
+        Raises:
+            - ValueError: if model part is not a process model part
+            - ValueError: if mesh not initialised first
+
+        Returns:
+            - block_text (List[str]): list of strings for the submodelpart. Each element is a line in the mdpa file.
+        """
+
+        # validate part is process model part
+        if self.__is_body_model_part(process_model_part):
+            raise ValueError(
+                f"Model part {process_model_part.name} is not a process model part!"
+            )
+
+        # check if mesh is initialised
+        if process_model_part.mesh is None:
+            raise ValueError(
+                f"Model part {process_model_part.name} has not been meshed."
+                f"Before creating the mdpa file, the model part needs to be meshed."
+                f"Please run Model.generate_mesh()"
+            )
+
+        # initialise block
+        block_text = ["", f"Begin SubModelPart {process_model_part.name}"]
+
+        # write tables
+        entities = [table.id for table in self.__get_unique_tables_process_model_part(process_model_part)]
+        block_text = self.__write_sub_model_part_block(
+            block_text, block_name="Tables", block_entities=entities
+        )
+
+        # write nodes
+        entities = [node.id for node in process_model_part.mesh.nodes]
+        block_text = self.__write_sub_model_part_block(
+            block_text, block_name="Nodes", block_entities=entities
+        )
+
+        # write conditions if the process contains condition elements
+        if ((process_model_part.mesh.elements is not None) and
+                self.__check_if_process_writes_conditions(process_model_part)):
+            # write conditions
+            entities = [el.id for el in process_model_part.mesh.elements]
+            block_text = self.__write_sub_model_part_block(
+                block_text, block_name="Conditions", block_entities=entities
+            )
+
+        block_text += [f"End SubModelPart", ""]
+        return block_text
+
+    @staticmethod
+    def __write_element_line(mat_id: int, element: Element) -> str:
+        """
+        Writes an element to the mdpa format for Kratos
+
+        Args:
+            - mat_id (int): integer representing the material id connected to the element.
+            - element (:class:`stem.mesh.Element`): element object to write to Kratos.
+
+        Returns:
+            - line (str): string representing an element (or condition) in Kratos.
+        """
+        # simplify space syntax
+        space = " " * INDENTATION
+        _node_ids = element.node_ids
+        # assemble format for element/condition string
+        # `  element_id  property_id  node_1 node_2 node_3 ... node_N`
+        # where N=number of nodes of the element/condition
+        _fmt = f"{space}{FORMAT_INTEGER}{space}{FORMAT_INTEGER}{space}" + " ".join(
+            [FORMAT_INTEGER] * len(_node_ids)
+        )
+        line = _fmt.format(element.id, mat_id, *_node_ids)
+        return line
+
+    @staticmethod
+    def __write_node_line(node: Node) -> str:
+        """
+        Writes a node to the mdpa format for Kratos
+
+        Args:
+            - node (:class:`stem.mesh.Node`): node object to write to Kratos.
+
+        Returns:
+            - line: string representing a node in Kratos.
+        """
+        # simplify space syntax
+        space = " " * INDENTATION
+        node_coords = node.coordinates
+        # assemble format for nodal string
+        #   node_id  coordinate_1 coordinate_2 coordinate_3
+        _fmt = f"{space}{FORMAT_INTEGER}{space}" + " ".join(
+            [FORMAT_FLOAT_LONG] * len(node_coords)
+        )
+        line = _fmt.format(node.id, *node_coords)
+        return line
+
+    @staticmethod
+    def __write_table_line(time: float, value: float) -> str:
+        """
+        Write the line for a Kratos table.
+
+        Args:
+            - time (Union[int, float]): time at the j-th line of the table
+            - value (float): value at the j-th line of the table
+
+        Returns:
+            - str: string corresponding to the j-th line in a table for Kratos.
+        """
+        # simplify space syntax
+        space = " " * INDENTATION
+        # assemble format for table string at line j
+        #   time value
+        _fmt = f"{space}{FORMAT_FLOAT_SHORT}{space}{FORMAT_FLOAT_SHORT}"
+        return _fmt.format(time, value)
+
+    def __write_table_block(self, table: Table) -> List[str]:
+        """
+        Writes a table to the mdpa format for Kratos.
+
+        Args:
+            - table (:class:`stem.table.Table`): table object to write to Kratos.
+
+        Raises:
+            - ValueError: if table id is not initialised.
+
+        Returns:
+            - block_text (List[str]): list of strings for the table. Each element is a line in the mdpa file.
+        """
+
+        # check initialisation of id
+        if table.id is None:
+            raise ValueError("Table id not initialised!")
+
+        # initialise block
+        block_text = ["", f"Begin Table {table.id} TIME VALUE"]
+        block_text.extend(
+            [
+                self.__write_table_line(table.times[ix], table.values[ix])
+                for ix in range(len(table.values))
+             ]
+        )
+        block_text += [f"End Table", ""]
+        return block_text
+
+    @staticmethod
+    def __map_gmsh_element_to_kratos(model: Model, model_part: ModelPart) -> Optional[str]:
+        """
+        Returns the corresponding element type based on the analysis type, the model part (condition or body)
+        and type of element (e.g. rod vs beam or line load vs moving load).
+
+        Args:
+            - model (:class:`stem.model.Model`): the model object containing the info on the solver settings and
+                problem.
+            - model_part (:class:`stem.model_part.ModelPart`): the model part
+
+        Raises:
+            - ValueError: if model dimension is not 2 or 3
+            - ValueError: if mesh not initialised first
+            - ValueError: if element types are not unique in the model part.
+            - ValueError: if the analysis type is not specified.
+
+        Returns:
+            - Optional[str]: the Kratos element type
+        """
+
+        # get number of dimensions of the model
+        if model.ndim != 2 and model.ndim != 3:
+            raise ValueError(
+                f"Model dimension {model.ndim} is not supported. Only 2D and 3D are supported."
+            )
+        else:
+            n_dimensions = model.ndim
+
+        # Check if mesh is initialised
+        if model_part.mesh is None:
+            raise ValueError(
+                f"Model part {model_part.name} has not been meshed."
+                f"Before creating the mdpa file, the model part needs to be meshed."
+                f"Please run Model.generate_mesh()"
+            )
+
+        # check unique_elements
+        element_part_type = np.unique(
+            [element.element_type for element in model_part.mesh.elements]
+        )
+
+        if len(element_part_type) > 1:
+            raise ValueError(
+                f"Model part {model_part.name} has more than 1 element type assigned."
+                f"\n{element_part_type}. Error."
+            )
+
+        # get number of nodes per element
+        n_nodes_element = len(model_part.mesh.elements[0].node_ids)
+
+
+        # check analysis type
+        if model.project_parameters is not None:
+            analysis_type = model.project_parameters.settings.analysis_type
+            # get element name from model part (body or condition)
+            element_name = model_part.get_element_name(n_dimensions, n_nodes_element, analysis_type)
+        else:
+            raise ValueError(
+                f"Analysis type not specified in the model. Please initialise the model with the analysis type."
+            )
 
-    def write_mesh_to_mdpa(self, filename):
+        return element_name
+
+    @staticmethod
+    def __check_if_mesh_is_present_in_model_part(model_part: ModelPart):
+        """
+        Check if the mesh is present in the model part.
+
+        Args:
+            - model_part (:class:`stem.model_part.ModelPart`): the model part
+
+        Raises:
+            - ValueError: if mesh not initialised first
+
+        """
+        # check if mesh is initialised
+        if model_part.mesh is None:
+            raise ValueError(
+                f"Model part {model_part.name} has not been meshed."
+                f"Before creating the mdpa file, the model part needs to be meshed."
+                f"Please run Model.generate_mesh()"
+            )
+
+    def write_elements_body_model_part(self, body_model_part: BodyModelPart, mat_id: int, kratos_element_type: str) \
+            -> List[str]:
+        """
+        Writes the elements of the body model part to the mdpa file
+
+        Args:
+            - body_model_part (:class:`stem.model_part.BodyModelPart`): the body model part to write to mdpa.
+            - mat_id (int): the material id connected to the element block
+            - kratos_element_type (str): the kratos element type
+
+        Raises:
+            - ValueError: if model part is not a body model part
+
+        Returns:
+            - block_text (List[str]): list of strings for the elements of the body model part. \
+                Each element is a line in the mdpa file.
+        """
+        # validate part is body model part
+        if not self.__is_body_model_part(body_model_part):
+            raise ValueError(
+                f"Model part {body_model_part.name} is not a body model part!"
+            )
+
+        # validate if mesh is present
+        self.__check_if_mesh_is_present_in_model_part(body_model_part)
+
+        # initialise block
+        block_text = ["", f"Begin Elements {kratos_element_type}"]
+        if body_model_part.mesh is not None:
+            block_text.extend(
+                [
+                    self.__write_element_line(mat_id, el)
+                    for el in body_model_part.mesh.elements
+                ]
+            )
+        block_text += [f"End Elements", ""]
+        return block_text
+
+    def write_conditions_process_model_part(self, process_model_part: ModelPart, mat_id: int,
+                                            kratos_element_type: str) -> List[str]:
+        """
+        Writes the conditions of the process model part to the mdpa file.
+
+        Args:
+            - process_model_part (:class:`stem.model_part.ModelPart`): the process model part to write to mdpa.
+            - mat_id (int): the material id connected to the conditions block
+            - kratos_element_type (str): the kratos element type for the condition.
+
+        Raises:
+            - ValueError: if model part is not a process model part
+            - ValueError: if mesh not initialised first
+
+        Returns:
+            - block_text (List[str]): list of strings for the elements of the body model part. \
+                Each element is a line in the mdpa file.
+        """
+        # validate part is body model part
+        if self.__is_body_model_part(process_model_part):
+            raise ValueError(
+                f"Model part {process_model_part.name} is not a process model part!"
+            )
+
+        # validate if mesh is present
+        self.__check_if_mesh_is_present_in_model_part(process_model_part)
+
+        # no elements to write to conditions or process doesn't write condition elements
+
+        if ((process_model_part.mesh is not None) and (process_model_part.mesh.elements is not None)
+                and self.__check_if_process_writes_conditions(process_model_part)):
+
+            block_text = ["", f"Begin Conditions {kratos_element_type}"]
+            block_text.extend(
+                [self.__write_element_line(mat_id, el) for el in process_model_part.mesh.elements]
+            )
+            block_text += [f"End Conditions", ""]
+        else:
+            block_text = []
+
+        return block_text
+
+    def __write_all_nodes(self, model: Model) -> List[str]:
+        """
+        Writes nodes to mdpa format.
+
+        Args:
+            - model (:class:`stem.model.Model`): the model object containing the info on the nodes.
+
+        Returns:
+            - block_text (List[str]): list of strings for the mdpa file. Each element is a line in the mdpa file.
+        """
+        nodes_dict = model.get_all_nodes()
+        # sort by key
+        nodes_dict = dict(sorted(nodes_dict.items()))
+        block_text = ["", "Begin Nodes"]
+        block_text.extend(
+            [self.__write_node_line(node) for node in nodes_dict.values()]
+        )
+        block_text += ["End Nodes", ""]
+        return block_text
+
+    def __write_all_tables(self, model: Model) -> List[str]:
         """
-        Saves mesh data to mdpa file
+        Writes tables to mdpa format.
 
         Args:
-            - filename (str): filename of mdpa file
+            - model (:class:`stem.model.Model`): the model object containing the info on the tables.
 
         Returns:
+            - block_text (List[str]): list of strings for the mdpa file. Each element is a line in the mdpa file.
         """
-        pass
 
-    def __write_problem_data(self):
-        pass
+        unique_tables = self.__get_unique_tables(model)
+        block_text = []
+        for table in unique_tables:
+            block_text.extend(self.__write_table_block(table))
+        return block_text
 
-    def __write_solver_settings(self):
-        pass
+    @staticmethod
+    def __write_property_ids(model: Model) -> List[str]:
+        """
+        Writes the block initialising the material ids (properties).
+
+        Args:
+            - model (:class:`stem.model.Model`): the model object containing the info of the materials.
+
+        Returns:
+            - block_text (List[str]): list of strings for the mdpa file. Each element is a line in the mdpa file.
+        """
+        # get the unique ids in material and conditions
+        ids_to_write = list(set([mp.id for mp in model.get_all_model_parts() if mp.id is not None]))
+
+        # get the unique ids and write properties
+        block_text = []
+        for _id in np.sort(ids_to_write):
+            block_text.extend(["", f"Begin Properties {_id}", "End Properties", ""])
+        return block_text
+
+    def __write_elements_model(self, model: Model) -> List[str]:
+        """
+        Returns the mdpa block related to elements.
+
+        Args:
+            - model (:class:`stem.model.Model`): the model object containing the info of the elements.
+
+        Raises:
+            - ValueError: if id of body model part is not initialised.
+
+        Returns:
+            - block_text (List[str]): list of strings for the mdpa file. Each element is a line in the mdpa file.
+        """
+        block_text = []
+        # write elements per body model part
+        for bmp in model.body_model_parts:
+            if bmp.id is None:
+                raise ValueError(
+                    f"Body model part {bmp.name} has no id."
+                    "First, material parameters needs to be written to json."
+                )
+
+            # get the element type
+            element_type = self.__map_gmsh_element_to_kratos(model, bmp)
+            # write text block with elements
+            if element_type is not None:
+                block_text.extend(
+                    self.write_elements_body_model_part(
+                        mat_id=bmp.id, kratos_element_type=element_type, body_model_part=bmp
+                    )
+                )
+        return block_text
+
+    def __write_conditions_model(self, model: Model) -> List[str]:
+        """
+        Returns the mdpa block related to conditions.
+
+        Args:
+            - model (:class:`stem.model.Model`): the model object containing the info of the conditions.
 
-    def __write_output_processes(self):
-        pass
+        Raises:
+            - ValueError: if id of process model part is not initialised.
 
-    def __write_input_processes(self):
-        pass
+        Returns:
+            - block_text (List[str]): list of strings for the mdpa file. Each element is a line in the mdpa file.
+        """
+        block_text = []
+        # write per conditions per process model part
+        for pmp in model.process_model_parts:
+            # get the condition element type
+            condition_type = self.__map_gmsh_element_to_kratos(model, pmp)
+            if not self.__check_if_process_writes_conditions(pmp):
+                continue
+            if pmp.id is None:
+                raise ValueError(
+                    f"Process model part id of part {pmp.name} not initialised."
+                )
 
-    def __write_constraints(self):
-        pass
+            # write text block with conditions
+            if condition_type is not None:
+                block_text.extend(
+                    self.write_conditions_process_model_part(
+                        mat_id=pmp.id,
+                        kratos_element_type=condition_type,
+                        process_model_part=pmp,
+                    )
+                )
+        return block_text
+
+    def __write_submodel_parts(self, model: Model) -> List[str]:
+        """
+        Returns the mdpa block related to the submodel parts of process and body model parts.
 
-    def __write_loads(self):
-        pass
+        Args:
+            - model (:class:`stem.model.Model`): the model object containing the info of the conditions.
 
-    def write_project_parameters_json(self, filename: str):
+        Returns:
+            - block_text (List[str]): list of strings for the mdpa file. Each element is a line in the mdpa file.
+        """
+        block_text = []
+
+        for bmp in model.body_model_parts:
+            block_text.extend(self.write_submodelpart_body_model_part(bmp))
+
+        for pmp in model.process_model_parts:
+            block_text.extend(self.write_submodelpart_process_model_part(pmp))
+
+        return block_text
+
+    def write_mdpa_text(self, model: Model) -> List[str]:
+        """
+        Returns the  mesh data to mdpa format as list of strings representing each a line in the mdpa file.
+
+        Args:
+            - model (:class:`stem.model.Model`): The model object containing all the required info of the model parts.
+
+        Returns:
+            - block_text (List[str]): list of strings for the mdpa file. Each element is a line in the mdpa file.
+        """
+
+        # initialise process model part ids
+        self.__initialise_process_model_part_ids(model)
+        self.__initialise_body_model_part_ids(model)
+        self.__initialise_table_ids(model)
+        block_text = []
+        # retrieve the materials in the model and write mdpa text blocks
+        block_text.extend(self.__write_property_ids(model))
+
+        # write the table block
+        block_text.extend(self.__write_all_tables(model))
+
+        # retrieve the unique nodes of all the model parts
+        block_text.extend(self.__write_all_nodes(model))
+
+        # write elements per body model part
+        block_text.extend(self.__write_elements_model(model))
+
+        # write conditions per process model part
+        block_text.extend(self.__write_conditions_model(model))
+
+        # write submodel parts
+        block_text.extend(self.__write_submodel_parts(model))
+
+        return block_text
+
+    def write_mesh_to_mdpa(self, model: Model, mesh_file_name: str, output_folder="./") -> List[str]:
+        """
+        Saves mesh data to mdpa file.
+
+        Args:
+            - model (:class:`stem.model.Model`): The model object containing all the required info on the \
+                materials.
+            - mesh_file_name (str): The name of the mesh file to store the mdpa file.
+            - output_folder (str): folder to store the project parameters file. Defaults to the working directory.
+
+        Returns:
+            - block_text (List[str]): list of strings for the mdpa file. Each element is a line in the mdpa file.
+        """
+        block_text = self.write_mdpa_text(model)
+
+        # append EOL to each line
+        output_formatted_txt = [f"{line}\n" for line in block_text]
+
+        output_folder_pth = Path(output_folder)
+        output_folder_pth.mkdir(exist_ok=True, parents=True)
+
+        output_path = output_folder_pth.joinpath(mesh_file_name)
+        # if no suffix or wrong suffix change to mdpa
+        output_path = output_path.with_suffix(".mdpa")
+
+        with open(output_path, "w") as _buf:
+            _buf.writelines(output_formatted_txt)
+
+        return output_formatted_txt
+
+    def write_material_parameters_json(self, model: Model, materials_file_name: str = "MaterialParameters.json",
+                                       output_folder: str = "./") -> Dict[str, Any]:
+        """
+        Writes the material parameters to json format for Kratos.
+
+        Args:
+            - model (:class:`stem.model.Model`): The model object containing all the required info on the \
+                materials.
+            - materials_file_name (str): name of the material parameters file. Defaults to `MaterialParameters.json`.
+            - output_folder (str): folder to store the material parameters file. Defaults to the working directory.
+
+        Raises:
+            - ValueError: if material is not assigned to the body model part
+            - ValueError: if material id is not initialised
+
+        Returns:
+            - materials_dict Dict([str, Any]): dictionary containing the material parameters' dictionary.
+        """
+
+        materials_dict: Dict[str, Any] = {"properties": []}
+
+        # initialise the model ids
+        self.initialise_model_ids(model)
+
+        # iterate over the body model parts and create materials
+        for bmp in model.body_model_parts:
+
+            if bmp.material is None:
+                raise ValueError(f"Body model part {bmp.name} has no material assigned.")
+
+            if bmp.id is None:
+                raise ValueError(f"Body model part {bmp.name} has no id initialised.")
+
+            materials_dict["properties"].append(
+                self.material_io.create_material_dict(
+                    part_name=bmp.name,
+                    material=bmp.material,
+                    material_id=bmp.id,
+                )
+            )
+
+        # write the material parameters file to json
+        IOUtils.write_json_file(output_folder, materials_file_name, materials_dict)
+
+        return materials_dict
+
+    def __create_solver_settings_dictionary(self, model: Model, mesh_file_name: str, materials_file_name: str) \
+            -> Dict[str, Any]:
+        """
+        Creates a dictionary containing the solver settings.
+
+        Args:
+            - model (:class:`stem.model.Model`): The model object containing the solver data and model parts.
+            - mesh_file_name (str): The name of the mesh file.
+            - materials_file_name (str): The name of the materials parameters json file.
+
+        Raises:
+            - ValueError: if solver_settings in model are not initialised.
+
+        Returns:
+            - Dict[str, Any]: dictionary containing the part of the project parameters
+                dictionary related to problem data and solver settings.
+        """
+
+        if model.project_parameters is None:
+            raise ValueError("Solver settings are not initialised in model.")
+
+        return self.solver_io.create_settings_dictionary(
+            model.project_parameters,
+            Path(mesh_file_name).stem,
+            materials_file_name,
+            model.get_all_model_parts(),
+        )
+
+    def __create_output_process_dictionary(self, outputs: Optional[List[Output]] = None) -> Dict[str, Any]:
+        """
+        Creates a dictionary containing the output settings.
+
+        Args:
+            - outputs (Optional[List[:class:`stem.output.Output`]]): The list of output processes objects to write \
+                in outputs.
+
+        Returns:
+            - Dict[str, Any]: dictionary containing the part of the project parameters dictionary related to outputs
+        """
+        if outputs is None or len(outputs) == 0:
+            return {"output_processes": {}, "processes": {}}
+        else:
+            return self.outputs_io.create_output_process_dictionary(outputs=outputs)
+
+    def __create_loads_and_boundary_conditions_dictionary(self, model: Model) -> Dict[str, Any]:
+        """
+        Creates a dictionary containing the loads and boundary conditions.
+
+        Args:
+            - model (:class:`stem.model.Model`): The model object containing the process model parts.
+
+        Returns:
+            - Dict[str, Any]: dictionary containing the part of the project parameters dictionary related \
+                to loads and boundary conditions
+        """
+        processes_dict: Dict[str, Any] = {
+            "processes": {"constraints_process_list": [], "loads_process_list": []}
+        }
+
+        # loop on the process model parts
+        for mp in model.process_model_parts:
+
+            # add load
+            if isinstance(mp.parameters, LoadParametersABC):
+                _parameters = self.loads_io.create_load_dict(mp.name, mp.parameters)
+                processes_dict["processes"]["loads_process_list"].append(_parameters)
+
+            # add boundary condition
+            elif isinstance(mp.parameters, BoundaryParametersABC):
+                _parameters = self.boundaries_io.create_boundary_condition_dict(
+                    mp.name, mp.parameters
+                )
+
+                if mp.parameters.is_constraint:
+                    _key = "constraints_process_list"
+                else:
+                    _key = "loads_process_list"
+                processes_dict["processes"][_key].append(_parameters)
+
+        return processes_dict
+
+    def write_project_parameters_json(self, model: Model, outputs: List[Output], mesh_file_name: str,
+                                      materials_file_name: str, project_file_name: str = "ProjectParameters.json",
+                                      output_folder: str = "./") -> Dict[str, Any]:
         """
         Writes project parameters to json file
 
         Args:
-            - filename (str): filename of json file
+            - model (:class:`stem.model.Model`): The model object containing all the required info, i.e. \
+                body and process model parts, boundary conditions, solver settings and problem data.
+            - outputs (List[:class:`stem.output.Output`]): The list of output processes objects to write in outputs.
+            - mesh_file_name (str): The name of the mesh file.
+            - materials_file_name (str): The name of the materials file.
+            - project_file_name (str): name of the project parameters file. Defaults to `ProjectParameters.json`.
+            - output_folder (str): folder to store the project parameters file. Defaults to the working directory.
 
         Returns:
+            - project_parameters_dict (Dict[str, Any]): the dictionary containing the project parameters.
+        """
+        # initialise material, tables and process model part ids
+        self.initialise_model_ids(model)
+
+        # get the solver dictionary
+        solver_dict = self.__create_solver_settings_dictionary(
+            model, mesh_file_name, materials_file_name
+        )
+        # get the output dictionary
+        outputs_dict = self.__create_output_process_dictionary(outputs=outputs)
+        # get the boundary condition dictionary
+        loads_and_bc_dict = self.__create_loads_and_boundary_conditions_dictionary(model=model)
+        # TODO get the additional_processes dictionary
+
+        # merge dictionaries into one
+        project_parameters_dict: Dict[str, Any] = reduce(
+            Utils.merge, (solver_dict, outputs_dict, loads_and_bc_dict)
+        )
+        # write json file
+        IOUtils.write_json_file(output_folder, project_file_name, project_parameters_dict)
+
+        return project_parameters_dict
+
+    def write_input_files_for_kratos(self, model: Model, outputs: List[Output], mesh_file_name: str,
+                                     materials_file_name: str = "MaterialParameters.json",
+                                     project_file_name: str = "ProjectParameters.json", output_folder: str = "./"):
         """
-        self.__write_problem_data()
-        self.__write_solver_settings()
-        self.__write_output_processes()
-        self.__write_input_processes()
-        self.__write_constraints()
-        self.__write_loads()
-        # todo write Projectparameters.json
+        Writes all required input files for a Kratos simulation, i.e: project parameters json; material parameters json
+        and the mdpa mesh file
+
+        Args:
+            - model (:class:`stem.model.Model`): The model object containing all the required info.
+            - outputs (List[:class:`stem.output.Output`]): The list of output processes objects to write in outputs.
+            - mesh_file_name (str): The name of the mesh file.
+            - materials_file_name (str): The name of the materials file.
+            - project_file_name (str): name of the project parameters file. Defaults to `ProjectParameters.json`.
+            - output_folder (str): folder to store the project parameters file. Defaults to the working directory.
+        """
+
+        # write materials
+        self.write_material_parameters_json(model, materials_file_name, output_folder)
+
+        # write project parameters
+        self.write_project_parameters_json(model, outputs, mesh_file_name, materials_file_name, project_file_name,
+                                           output_folder)
+
+        # write mdpa files
+        self.write_mesh_to_mdpa(model, mesh_file_name, output_folder)
\ No newline at end of file
diff --git a/stem/IO/kratos_loads_io.py b/stem/IO/kratos_loads_io.py
index a56610c2..8e38cd56 100644
--- a/stem/IO/kratos_loads_io.py
+++ b/stem/IO/kratos_loads_io.py
@@ -1,7 +1,7 @@
 from copy import deepcopy
 from typing import Dict, List, Any, Union
-
 from stem.load import *
+from stem.IO.io_utils import IOUtils
 
 
 class KratosLoadsIO:
@@ -21,33 +21,7 @@ def __init__(self, domain: str):
         """
         self.domain = domain
 
-    def __create_point_load_dict(self, part_name:str, parameters: PointLoad) -> Dict[str, Any]:
-        """
-        Creates a dictionary containing the point load parameters
-
-        Args:
-            - part_name (str): name of the model part on which the load is applied
-            - parameters (:class:`stem.load.PointLoad`): point load parameters object
-
-        Returns:
-            - Dict[str, Any]: dictionary containing the load parameters
-        """
-
-        # initialize load dictionary
-        load_dict: Dict[str, Any] = {
-            "python_module": "apply_vector_constraint_table_process",
-            "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
-            "process_name": "ApplyVectorConstraintTableProcess",
-            "Parameters": deepcopy(parameters.__dict__),
-        }
-
-        load_dict["Parameters"]["model_part_name"] = f"{self.domain}.{part_name}"
-        load_dict["Parameters"]["variable_name"] = "POINT_LOAD"
-        load_dict["Parameters"]["table"] = [0, 0, 0]
-
-        return load_dict
-
-    def __create_moving_load_dict(self, part_name:str, parameters: MovingLoad) -> Dict[str, Any]:
+    def __create_moving_load_dict(self, part_name: str, parameters: MovingLoad) -> Dict[str, Any]:
         """
         Creates a dictionary containing the moving load parameters
 
@@ -72,90 +46,16 @@ def __create_moving_load_dict(self, part_name:str, parameters: MovingLoad) -> Di
 
         return load_dict
 
-    def __create_line_load_dict(self, part_name:str, parameters: LineLoad) -> Dict[str, Any]:
-        """
-        Creates a dictionary containing the line load parameters
-
-        Args:
-            - part_name (str): name of the model part on which the load is applied
-            - parameters (:class:`stem.load.LineLoad`): line load parameters object
-
-        Returns:
-            - Dict[str, Any]: dictionary containing the load parameters
-        """
-
-        # initialize load dictionary
-        load_dict: Dict[str, Any] = {
-            "python_module": "apply_vector_constraint_table_process",
-            "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
-            "process_name": "ApplyVectorConstraintTableProcess",
-            "Parameters": deepcopy(parameters.__dict__),
-        }
-
-        load_dict["Parameters"]["model_part_name"] = f"{self.domain}.{part_name}"
-        load_dict["Parameters"]["variable_name"] = "LINE_LOAD"
-        load_dict["Parameters"]["table"] = [0, 0, 0]
-
-        return load_dict
-
-    def __create_surface_load_dict(self, part_name:str, parameters: LoadParametersABC) -> Dict[str, Any]:
-        """
-        Creates a dictionary containing the surface load parameters
-
-        Args:
-            - part_name (str): name of the model part on which the load is applied
-            - parameters (:class:`stem.load.SurfaceLoad`): surface load parameters object
-
-        Returns:
-            - Dict[str, Any]: dictionary containing the load parameters
-        """
-
-        # initialize load dictionary
-        load_dict: Dict[str, Any] = {
-            "python_module": "apply_vector_constraint_table_process",
-            "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
-            "process_name": "ApplyVectorConstraintTableProcess",
-            "Parameters": deepcopy(parameters.__dict__),
-        }
-
-        load_dict["Parameters"]["model_part_name"] = f"{self.domain}.{part_name}"
-        load_dict["Parameters"]["variable_name"] = "SURFACE_LOAD"
-        load_dict["Parameters"]["table"] = [0, 0, 0]
-
-        return load_dict
-
-    def __create_gravity_load_dict(self, part_name:str, parameters: GravityLoad) -> Dict[str, Any]:
+    def create_load_dict(self, part_name: str, parameters: LoadParametersABC) -> Union[Dict[str, Any], None]:
         """
-        Creates a dictionary containing the surface load parameters
+        Creates a dictionary containing the load parameters
 
         Args:
             - part_name (str): name of the model part on which the load is applied
-            - parameters (:class:`stem.load.GravityLoad`): gravity load parameters object
-
-        Returns:
-            - Dict[str, Any]: dictionary containing the load parameters
-        """
-
-        # initialize load dictionary
-        load_dict: Dict[str, Any] = {
-            "python_module": "apply_vector_constraint_table_process",
-            "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
-            "process_name":  "ApplyVectorConstraintTableProcess",
-            "Parameters": deepcopy(parameters.__dict__),
-        }
-
-        load_dict["Parameters"]["model_part_name"] = f"{self.domain}.{part_name}"
-        load_dict["Parameters"]["variable_name"] = "VOLUME_ACCELERATION"
-        load_dict["Parameters"]["table"] = [0, 0, 0]
-
-        return load_dict
+            - parameters (:class:`stem.load.LoadParametersABC`): load parameters object
 
-    def create_load_dict(self, part_name:str, parameters: LoadParametersABC) -> Union[Dict[str, Any], None]:
-        """
-        Creates a dictionary containing the load parameters
-
-        Args:
-            - model_part (:class:`stem.model_part.ModelPart`): model part (load) object
+        Raises:
+            - NotImplementedError: if the load type is not implemented
 
         Returns:
             - Dict[str, Any]: dictionary containing the load parameters
@@ -163,14 +63,16 @@ def create_load_dict(self, part_name:str, parameters: LoadParametersABC) -> Unio
 
         # add load parameters to dictionary based on load type.
         if isinstance(parameters, PointLoad):
-            return self.__create_point_load_dict(part_name, parameters)
+            return IOUtils.create_vector_constraint_table_process_dict(self.domain, part_name, parameters, "POINT_LOAD")
         elif isinstance(parameters, MovingLoad):
             return self.__create_moving_load_dict(part_name, parameters)
         elif isinstance(parameters, LineLoad):
-            return self.__create_line_load_dict(part_name, parameters)
+            return IOUtils.create_vector_constraint_table_process_dict(self.domain, part_name, parameters, "LINE_LOAD")
         elif isinstance(parameters, SurfaceLoad):
-            return self.__create_surface_load_dict(part_name, parameters)
+            return IOUtils.create_vector_constraint_table_process_dict(self.domain, part_name, parameters,
+                                                                       "SURFACE_LOAD")
         elif isinstance(parameters, GravityLoad):
-            return self.__create_gravity_load_dict(part_name, parameters)
+            return IOUtils.create_vector_constraint_table_process_dict(self.domain, part_name, parameters,
+                                                                       "VOLUME_ACCELERATION")
         else:
-            raise NotImplementedError
+            raise NotImplementedError(f"Load type {type(parameters)} not implemented")
diff --git a/stem/IO/kratos_material_io.py b/stem/IO/kratos_material_io.py
index 58213cb0..893d0494 100644
--- a/stem/IO/kratos_material_io.py
+++ b/stem/IO/kratos_material_io.py
@@ -1,4 +1,3 @@
-import json
 from typing import Dict, Union, Any, List
 from copy import deepcopy
 
@@ -31,7 +30,8 @@ def __create_umat_material_dict(material: SoilConstitutiveLawABC) -> Dict[str, A
         keys are moved to the UDSM_NAME and IS_FORTRAN_UDSM keys, as this can be recognized by Kratos.
 
         Args:
-            - material (:class:`stem.soil_material.SoilConstitutiveLawABC`): soil constitutive law object containing the material parameters for UMAT
+            - material (:class:`stem.soil_material.SoilConstitutiveLawABC`): soil constitutive law object containing \
+                the material parameters for UMAT
 
         Returns:
             - Dict[str, Any]: dictionary containing the material parameters
@@ -51,7 +51,8 @@ def __create_udsm_material_dict(material: SoilConstitutiveLawABC) -> Dict[str, A
         the UMAT_PARAMETERS key, as this can be recognized by Kratos.
 
         Args:
-            - material (:class:`stem.soil_material.SoilConstitutiveLawABC`): soil constitutive law object containing the material parameters for UDSM
+            - material (:class:`stem.soil_material.SoilConstitutiveLawABC`): soil constitutive law object containing \
+                the material parameters for UDSM
 
         Returns:
             - Dict[str, Any]: dictionary containing the material parameters
@@ -70,8 +71,8 @@ def __create_elastic_spring_damper_dict(material: StructuralParametersABC) -> Di
         and NODAL_ROTATIONAL_DAMPING_RATIO keys, as this can be recognized by Kratos.
 
         Args:
-            - material (:class:`stem.structural_material.StructuralParametersABC`): material object containing the material parameters for an elastic
-                spring damper material
+            - material (:class:`stem.structural_material.StructuralParametersABC`): material object containing the \
+                material parameters for an elastics pring damper material
 
         Returns:
             - Dict[str, Any]: dictionary containing the material parameters
@@ -91,8 +92,8 @@ def __create_nodal_concentrated_dict(material: StructuralParametersABC) -> Dict[
         NODAL_DAMPING_COEFFICIENT key is moved to the NODAL_DAMPING_RATIO key, as this can be recognized by Kratos.
 
         Args:
-            - material (:class:`stem.structural_material.StructuralParametersABC`): material object containing the material parameters for the nodal
-                concentrated material.
+            - material (:class:`stem.structural_material.StructuralParametersABC`): material object containing the \
+                material parameters for the nodal concentrated material.
 
         Returns:
             - Dict[str, Any]: dictionary containing the material parameters
@@ -303,7 +304,8 @@ def __create_euler_beam_dict(self, material_parameters: StructuralParametersABC)
         Creates a dictionary containing the euler beam parameters
 
         Args:
-            - material (:class:`stem.structural_material.StructuralParametersABC`): Material object containing the material parameters
+            - material (:class:`stem.structural_material.StructuralParametersABC`): Material object containing the \
+            material parameters
 
         Returns:
             - Dict[str, Any]: Dictionary containing the euler beam parameters
@@ -364,10 +366,13 @@ def create_material_dict(
 
         Args:
             - part_name (str): name of the body model part for the material
-            - material (Union[:class:`stem.soil_material.SoilMaterial`,
+            - material (Union[:class:`stem.soil_material.SoilMaterial`, \
                               :class:`stem.soil_material.StructuralMaterial`]): material object
             - material_id (int): material id
 
+        Raises:
+            - ValueError: if material is not of either SoilMaterial or StructuralMaterial type
+
         Returns:
             - Dict[str, Any]: dictionary containing the material parameters
         """
diff --git a/stem/IO/kratos_output_io.py b/stem/IO/kratos_output_io.py
index b3bd525d..1d2db75c 100644
--- a/stem/IO/kratos_output_io.py
+++ b/stem/IO/kratos_output_io.py
@@ -1,5 +1,3 @@
-import json
-from copy import deepcopy
 from pathlib import Path
 from typing import List, Dict, Tuple, Union, Any
 
@@ -30,13 +28,8 @@ def __init__(self, domain: str):
         """
         self.domain = domain
 
-    def __create_gid_output_dict(
-        self,
-        part_name: str,
-        output_dir: Path,
-        output_name: str,
-        output_parameters: GiDOutputParameters,
-    ) -> Dict[str, Any]:
+    def __create_gid_output_dict(self, part_name: str, output_dir: Path, output_name: str,
+                                 output_parameters: GiDOutputParameters) -> Dict[str, Any]:
         """
         Creates a dictionary containing the output parameters to produce outputs in GiD
         format. To visualize the outputs, the software GiD is required.
@@ -46,7 +39,7 @@ def __create_gid_output_dict(
             - output_dir (Path): output path for the GiD output
             - output_name (str): Name for the output file.
             - output_parameters (:class:`stem.output.GiDOutputParameters`): class containing GiD output
-                  parameters
+                  parameters.
 
         Returns:
             - Dict[str, Any]: dictionary containing the output parameters in Kratos format
@@ -55,8 +48,8 @@ def __create_gid_output_dict(
         if output_name is None or output_name == "":
             output_name = f"{part_name}"
 
-        _output_path = output_dir.joinpath(output_name)
-        __output_path_gid = str(_output_path).replace("\\", "/")
+        output_path = output_dir.joinpath(output_name)
+        output_path_gid = Path(output_path).as_posix()
 
         if output_parameters.file_format == "binary":
             gid_post_mode = "GiD_PostBinary"
@@ -72,9 +65,13 @@ def __create_gid_output_dict(
                 f"Accepted inputs are `binary`, `ascii` or `hdf5`."
             )
 
+        model_part_name = f"{self.domain}"
+        if part_name is not None:
+            model_part_name += f".{part_name}"
+
         parameters_dict = {
-            "model_part_name": f"{self.domain}.{part_name}",
-            "output_name": __output_path_gid,
+            "model_part_name": model_part_name,
+            "output_name": output_path_gid,
             "postprocess_parameters": {
                 "result_file_configuration": {
                     "gidpost_flags": {
@@ -110,13 +107,8 @@ def __create_gid_output_dict(
 
         return output_dict
 
-    def __create_vtk_output_dict(
-        self,
-        part_name: str,
-        output_dir: Path,
-        output_name: str,
-        output_parameters: VtkOutputParameters,
-    ) -> Dict[str, Any]:
+    def __create_vtk_output_dict(self, part_name: str, output_dir: Path, output_name: str,
+                                 output_parameters: VtkOutputParameters) -> Dict[str, Any]:
         """
         Creates a dictionary containing the output parameters to produce outputs in vtk
         format. The format can be visualized e.g., using Paraview.
@@ -126,19 +118,20 @@ def __create_vtk_output_dict(
             - output_dir (Path): output path for the VTK output
             - output_name (str): Name for the output file. This parameter is ignored by
                 VTK output process.
-            - output_parameters (:class:`stem.output.VtkOutputParameters`): class containing VTK output
-                  parameters
+            - output_parameters (:class:`stem.output.VtkOutputParameters`): class containing VTK output parameters
 
         Returns:
             - Dict[str, Any]: dictionary containing the output parameters in Kratos format
         """
 
-        _output_path = output_dir
-        __output_path_vtk = str(_output_path).replace("\\", "/")
+        output_path_vtk = Path(output_dir).as_posix()
+        model_part_name = f"{self.domain}"
+        if part_name is not None:
+            model_part_name += f".{part_name}"
 
         parameters_dict = {
-            "model_part_name": f"{self.domain}.{part_name}",
-            "output_path": __output_path_vtk,
+            "model_part_name": model_part_name,
+            "output_path": output_path_vtk,
             "file_format": output_parameters.file_format,
             "output_precision": output_parameters.output_precision,
             "output_control_type": output_parameters.output_control_type,
@@ -161,13 +154,8 @@ def __create_vtk_output_dict(
 
         return output_dict
 
-    def __create_json_output_dict(
-        self,
-        part_name,
-        output_dir: Path,
-        output_name: str,
-        output_parameters: JsonOutputParameters,
-    ) -> Dict[str, Any]:
+    def __create_json_output_dict(self, part_name, output_dir: Path, output_name: str,
+                                  output_parameters: JsonOutputParameters) -> Dict[str, Any]:
         """
         Creates a dictionary containing the output parameters to produce outputs in
         JSON format.
@@ -178,7 +166,7 @@ def __create_json_output_dict(
             - output_name (str): Name for the output file. If not needed, the name is
                   taken as the part_name.
             - output_parameters (:class:`stem.output.JsonOutputParameters`): class containing JSON output
-                  parameters
+                  parameters.
 
         Returns:
             - Dict[str, Any]: dictionary containing the output parameters in Kratos format
@@ -190,11 +178,15 @@ def __create_json_output_dict(
         # create the target folder for json or simulation will not run.
         output_dir.mkdir(parents=True, exist_ok=True)
 
-        _output_path = output_dir.joinpath(output_name)
-        if _output_path.suffix == "":
-            _output_path = _output_path.with_suffix(".json")
+        output_path = output_dir.joinpath(output_name)
+        if output_path.suffix == "":
+            output_path = output_path.with_suffix(".json")
+
+        output_path_json = Path(output_path).as_posix()
 
-        __output_path_json = str(_output_path).replace("\\", "/")
+        model_part_name = f"{self.domain}"
+        if part_name is not None:
+            model_part_name += f".{part_name}"
 
         # initialize output dictionary
         output_dict: Dict[str, Any] = {
@@ -202,12 +194,13 @@ def __create_json_output_dict(
             "kratos_module": "KratosMultiphysics",
             "process_name": "JsonOutputProcess",
             "Parameters": {
-                "model_part_name": f"{self.domain}.{part_name}",
-                "output_file_name": __output_path_json,
+                "model_part_name": model_part_name,
+                "output_file_name": output_path_json,
                 "output_variables": [op.name for op in output_parameters.nodal_results],
                 "gauss_points_output_variables": [
                     op.name for op in output_parameters.gauss_point_results
                 ],
+                "time_frequency": output_parameters.time_frequency,
             },
         }
         return output_dict
diff --git a/stem/IO/kratos_solver_io.py b/stem/IO/kratos_solver_io.py
index 0640622b..5d29b12d 100644
--- a/stem/IO/kratos_solver_io.py
+++ b/stem/IO/kratos_solver_io.py
@@ -236,7 +236,7 @@ def __create_solver_settings_dictionary(self, solver_settings: SolverSettings, m
         return solver_settings_dict
 
     def create_settings_dictionary(self, problem_data: Problem, mesh_file_name: str, materials_file_name: str,
-                                   model_parts: List[ModelPart]):
+                                   model_parts: List[ModelPart]) -> Dict[str, Any]:
         """
         Creates a dictionary containing the solver settings
 
diff --git a/stem/additional_processes.py b/stem/additional_processes.py
index c43d7432..f3f3878a 100644
--- a/stem/additional_processes.py
+++ b/stem/additional_processes.py
@@ -1,6 +1,5 @@
-from typing import List, Dict, Any, Union, Optional
-from dataclasses import dataclass, field
-from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from abc import ABC
 
 
 @dataclass
@@ -20,7 +19,7 @@ class Excavation(AdditionalProcessesParametersABC):
         - :class:`AdditionalProcessesParametersABC`
 
     Attributes:
-        - deactivate_soil_part (bool): Deactivate or not the body model part
+        - deactivate_body_model_part (bool): Deactivate or not the body model part
     """
 
-    deactivate_soil_part: bool
+    deactivate_body_model_part: bool
diff --git a/stem/boundary.py b/stem/boundary.py
index 1de9e535..a1a57b62 100644
--- a/stem/boundary.py
+++ b/stem/boundary.py
@@ -1,7 +1,11 @@
 from typing import List, Dict, Any, Union, Optional
-from dataclasses import dataclass, field
+from dataclasses import dataclass
 from abc import ABC, abstractmethod
 
+from stem.solver import AnalysisType
+from stem.utils import Utils
+from stem.table import Table
+
 
 @dataclass
 class BoundaryParametersABC(ABC):
@@ -9,19 +13,34 @@ class BoundaryParametersABC(ABC):
     Abstract base class for boundary parameters
     """
 
-    pass
-
     @property
     @abstractmethod
     def is_constraint(self) -> bool:
         """
         Property which indicates if boundary condition is a constraint.
-        
-        Returns:
-            - bool
+
+        Raises:
+            - Exception: abstract method is called
+
         """
         raise Exception("abstract method 'is_constraint' of boundary parameters class is called")
 
+    @staticmethod
+    @abstractmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Abstract static method to get the element name for a boundary condition.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model
+            - n_nodes_element (int): The number of nodes per element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type.
+
+        Raises:
+            - Exception: abstract method is called
+        """
+        raise Exception("abstract method 'get_element_name' of boundary parameters class is called")
+
 
 @dataclass
 class DisplacementConstraint(BoundaryParametersABC):
@@ -34,12 +53,15 @@ class DisplacementConstraint(BoundaryParametersABC):
     Attributes:
         - active (List[bool]): Activate/deactivate constraint for each direction.
         - is_fixed (List[bool]): Specify if constraint is fixed for each direction.
-        - value (List[float]): Displacement constraint for direction [m].
+        - value (List[Union[float, :class:`stem.table.Table`]]): Displacement value for direction [m]. \
+            It should be a list of either float or table for each displacement. If a float is specified, the \
+            displacement is time-independent, otherwise the table specifies the amplitude of the amplitude of the \
+            displacement [m] over time [s] for each direction.
     """
 
     active: List[bool]
     is_fixed: List[bool]
-    value: List[float]
+    value: List[Union[float, Table]]
 
     @property
     def is_constraint(self) -> bool:
@@ -51,6 +73,41 @@ def is_constraint(self) -> bool:
         """
         return True
 
+    @staticmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Static method to get the element name for a displacement constraint. Displacement constraint does not have a
+        name.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model (2 or 3)
+            - n_nodes_element (int): The number of nodes per element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type.
+
+        Raises:
+            - ValueError: Displacement constraint can only be applied in mechanical or mechanical groundwater flow
+
+        Returns:
+            - None: Displacement constraint does not have a name
+
+        """
+
+        available_node_dim_combinations = {
+            2: [None],
+            3: [None],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, None, available_node_dim_combinations,
+                                             "Displacement constraint")
+
+        if analysis_type == AnalysisType.MECHANICAL_GROUNDWATER_FLOW or analysis_type == AnalysisType.MECHANICAL:
+            # displacement constraint does not have an element name
+            element_name = None
+        else:
+            raise ValueError("Displacement constraint can only be applied in mechanical or mechanical groundwater "
+                             "flow analysis")
+
+        return element_name
+
 
 @dataclass
 class RotationConstraint(BoundaryParametersABC):
@@ -63,12 +120,16 @@ class RotationConstraint(BoundaryParametersABC):
     Attributes:
         - active (List[bool]): Activate/deactivate constraint for each direction.
         - is_fixed (List[bool]): Specify if constraint is fixed around each axis.
-        - value (List[float]): Rotation constraint around x, y and axis.
+        - value (List[float]): Rotation constraint
+        - value (List[Union[float, :class:`stem.table.Table`]]): Rotation value around x, y and z axis [Rad]. \
+            It should be a list of either float or table for each direction. If a float is specified, the rotation is \
+            time-independent, otherwise the table specifies the amplitude of the rotation [Rad] over \
+            time [s] around each axis.
     """
 
     active: List[bool]
     is_fixed: List[bool]
-    value: List[float]
+    value: List[Union[float, Table]]
 
     @property
     def is_constraint(self) -> bool:
@@ -80,6 +141,40 @@ def is_constraint(self) -> bool:
         """
         return True
 
+    @staticmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Static method to get the element name for a rotation constraint. Rotation constraint does not have a
+        name.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model
+            - n_nodes_element (int): The number of nodes per element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type.
+
+        Raises:
+            - ValueError: Rotation constraint can only be applied in mechanical or mechanical groundwater flow
+
+        Returns:
+            - None: Rotation constraint does not have a name
+
+        """
+        available_node_dim_combinations = {
+            2: [None],
+            3: [None],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, None, available_node_dim_combinations,
+                                             "Rotation constraint")
+
+        if analysis_type == AnalysisType.MECHANICAL_GROUNDWATER_FLOW or analysis_type == AnalysisType.MECHANICAL:
+            # rotation constraint does not have an element name
+            element_name = None
+        else:
+            raise ValueError("Rotation constraint can only be applied in mechanical or mechanical groundwater "
+                             "flow analysis")
+
+        return element_name
+
 
 @dataclass
 class AbsorbingBoundary(BoundaryParametersABC):
@@ -108,3 +203,37 @@ def is_constraint(self) -> bool:
             - bool
         """
         return False
+
+    @staticmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Static method to get the element name for an absorbing boundary.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model (2 or 3)
+            - n_nodes_element (int): The number of nodes per element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type.
+
+        Raises:
+            - ValueError: Absorbing boundary conditions can only be applied in mechanical or mechanical groundwater \
+                flow analysis
+
+        Returns:
+            - Optional[str]: The element name
+
+        """
+
+        available_node_dim_combinations = {
+            2: [2, 3],
+            3: [3, 4],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, n_nodes_element, available_node_dim_combinations,
+                                             "Absorbing boundary")
+
+        if analysis_type == AnalysisType.MECHANICAL_GROUNDWATER_FLOW or analysis_type == AnalysisType.MECHANICAL:
+            element_name = f"UPwLysmerAbsorbingCondition{n_dim_model}D{n_nodes_element}N"
+        else:
+            raise ValueError("Absorbing boundary conditions can only be applied in mechanical or mechanical "
+                            "groundwater flow analysis")
+
+        return element_name
diff --git a/stem/default_materials.py b/stem/default_materials.py
index dd190dae..6764e842 100644
--- a/stem/default_materials.py
+++ b/stem/default_materials.py
@@ -27,6 +27,9 @@ def set_default_steel_rail_material(ndim: int, rail_type: RailTypes) -> Structur
         - ndim (int): number of dimensions of the problem (either 2 or 3)
         - rail_type (:class:`RailTypes`): instance of the enumeration to describe the rail type
 
+    Raises:
+        - ValueError: if the rail type is not implemented
+
     Returns:
         - :class:`stem.structural_material.StructuralMaterial`
     """
@@ -48,7 +51,7 @@ def set_default_steel_rail_material(ndim: int, rail_type: RailTypes) -> Structur
             parameters["I22"] = 5.123e-06
             parameters["TORSIONAL_INERTIA"] = 3.550e-05
     else:
-        raise ValueError
+        raise ValueError(f"Rail type {rail_type} is not implemented")
 
     name = f"default_elastic_{rail_type.name}_{ndim}D"
     beam_object = EulerBeam(
diff --git a/stem/geometry.py b/stem/geometry.py
index bf083cb4..0ee562f0 100644
--- a/stem/geometry.py
+++ b/stem/geometry.py
@@ -1,7 +1,6 @@
 from typing import Dict, List, Any, Optional, Sequence
 from abc import ABC, abstractmethod
 
-
 class GeometricalObjectABC(ABC):
     """
     An abstract base class for all geometrical objects.
@@ -29,39 +28,65 @@ class Point(GeometricalObjectABC):
 
     Attributes:
         - __id (int): A unique identifier for the point.
-        - coordinates (List[float]): An iterable of floats representing the x, y and z coordinates of the point.
+        - coordinates (Sequence[float]): A sequence of floats representing the x, y and z coordinates of the point.
     """
     def __init__(self, id: int):
         """
         Constructor for the point class.
 
         Args:
-            id (int): The id of the point.
+            - id (int): The id of the point.
         """
         self.__id: int = id
-        self.coordinates: List[float] = []
+        self.coordinates: Sequence[float] = []
 
-    @property
-    def id(self) -> int:
+    def __getattribute__(self, item: str) -> Any:
         """
-        Getter for the id of the point.
+        Overrides the getattribute method of the object class.
+
+        Args:
+            - item (str): The name of the attribute.
+
+        Raises:
+            - AttributeError: Cannot call create method on an initialised point instance.
 
         Returns:
-            - int: The id of the point.
+            - Any: The attribute.
 
         """
-        return self.__id
+        # make sure that the create method cannot be called on an initialised point instance
+        if item == "create":
+            raise AttributeError("Cannot call create method on an initialised point instance.")
+        else:
+            return super().__getattribute__(item)
 
-    @id.setter
-    def id(self, value: int):
+    @classmethod
+    def create(cls, coordinates: Sequence[float], id: int) -> "Point":
         """
-        Setter for the id of the point.
+        Creates a point object from a list of coordinates and a point id.
 
         Args:
-            - value (int): The id of the point.
+            - coordinates (Sequence[float]): An iterable of floats representing the x, y and z coordinates of the point.
+            - id (int): The id of the point.
+
+        Returns:
+            - :class:`Point`: A point object.
 
         """
-        self.__id = value
+        point = cls(id)
+        point.coordinates = coordinates
+        return point
+
+    @property
+    def id(self) -> int:
+        """
+        Getter for the id of the point.
+
+        Returns:
+            - int: The id of the point.
+
+        """
+        return self.__id
 
 
 class Line(GeometricalObjectABC):
@@ -73,7 +98,7 @@ class Line(GeometricalObjectABC):
 
     Attributes:
         - id (int): A unique identifier for the line.
-        - point_ids (List[int]): An Iterable of two integers representing the ids of the points that make up the\
+        - point_ids (Sequence[int]): A sequence of two integers representing the ids of the points that make up the\
             line.
     """
 
@@ -85,28 +110,55 @@ def __init__(self, id: int):
             id (int): The id of the line.
         """
         self.__id: int = id
-        self.point_ids: List[int] = []
+        self.point_ids: Sequence[int] = []
 
-    @property
-    def id(self) -> int:
+    def __getattribute__(self, item: str) -> Any:
         """
-        Getter for the id of the line.
+        Overrides the getattribute method of the object class.
+
+        Args:
+            - item (str): The name of the attribute.
+
+        Raises:
+            - AttributeError: Cannot call create method on an initialised line instance.
 
         Returns:
-            - int: The id of the line.
+            - Any: The attribute.
+
         """
-        return self.__id
+        # make sure that the create method cannot be called on an initialised line instance
+        if item == "create":
+            raise AttributeError("Cannot call create method on an initialised line instance.")
+        else:
+            return super().__getattribute__(item)
 
-    @id.setter
-    def id(self, value: int):
+    @classmethod
+    def create(cls, point_ids: Sequence[int], id: int) -> "Line":
         """
-        Setter for the id of the line.
+        Creates a line object from a list of point ids and a line id.
 
         Args:
-            - value (int): The id of the line.
+            - point_ids (Sequence[int]): A sequence of two integers representing the ids of the points that make up the\
+                line.
+            - id (int): The id of the line.
+
+        Returns:
+            - :class:`Line`: A line object.
 
         """
-        self.__id = value
+        line = cls(id)
+        line.point_ids = point_ids
+        return line
+
+    @property
+    def id(self) -> int:
+        """
+        Getter for the id of the line.
+
+        Returns:
+            - int: The id of the line.
+        """
+        return self.__id
 
 
 class Surface(GeometricalObjectABC):
@@ -118,12 +170,32 @@ class Surface(GeometricalObjectABC):
 
     Attributes:
         - __id (int): A unique identifier for the surface.
-        - line_ids (List[int]): An Iterable of three or more integers representing the ids of the lines that make\
+        - line_ids (Sequence[int]): A sequence of three or more integers representing the ids of the lines that make\
             up the surface.
     """
     def __init__(self, id: int):
         self.__id: int = id
-        self.line_ids: List[int] = []
+        self.line_ids: Sequence[int] = []
+
+    def __getattribute__(self, item: str) -> Any:
+        """
+        Overrides the getattribute method of the object class.
+
+        Args:
+            - item (str): The name of the attribute.
+
+        Raises:
+            - AttributeError: Cannot call create method on an initialised surface instance.
+
+        Returns:
+            - Any: The attribute.
+
+        """
+        # make sure that the create method cannot be called on an initialised surface instance
+        if item == "create":
+            raise AttributeError("Cannot call create method on an initialised surface instance.")
+        else:
+            return super().__getattribute__(item)
 
     @property
     def id(self) -> int:
@@ -135,16 +207,23 @@ def id(self) -> int:
         """
         return self.__id
 
-    @id.setter
-    def id(self, value: int):
+    @classmethod
+    def create(cls, line_ids: Sequence[int], id: int) -> "Surface":
         """
-        Setter for the id of the surface.
+        Creates a surface object from a list of line ids and a surface id.
 
         Args:
-            - value (int): The id of the surface.
+            - line_ids (Sequence[int]): A sequence of three or more integers representing the ids of the lines that\
+              make up the surface.
+            - id (int): The id of the surface.
+
+        Returns:
+            - :class:`Surface`: A surface object.
 
         """
-        self.__id = value
+        surface = cls(id)
+        surface.line_ids = line_ids
+        return surface
 
 
 class Volume(GeometricalObjectABC):
@@ -156,12 +235,32 @@ class Volume(GeometricalObjectABC):
 
     Attributes:
         - __id (int): A unique identifier for the volume.
-        - surface_ids (List[int]): An Iterable of four or more integers representing the ids of the surfaces that\
+        - surface_ids (Sequence[int]): A sequence of four or more integers representing the ids of the surfaces that\
             make up the volume.
     """
     def __init__(self, id: int):
         self.__id: int = id
-        self.surface_ids: List[int] = []
+        self.surface_ids: Sequence[int] = []
+
+    def __getattribute__(self, item: str) -> Any:
+        """
+        Overrides the getattribute method of the object class.
+
+        Args:
+            - item (str): The name of the attribute.
+
+        Raises:
+            - AttributeError: Cannot call create method on an initialised volume instance.
+
+        Returns:
+            - Any: The attribute.
+
+        """
+        # make sure that the create method cannot be called on an initialised volume instance
+        if item == "create":
+            raise AttributeError("Cannot call create method on an initialised volume instance.")
+        else:
+            return super().__getattribute__(item)
 
     @property
     def id(self) -> int:
@@ -173,16 +272,23 @@ def id(self) -> int:
         """
         return self.__id
 
-    @id.setter
-    def id(self, value: int):
+    @classmethod
+    def create(cls, surface_ids: Sequence[int], id: int) -> "Volume":
         """
-        Setter for the id of the volume.
+        Creates a volume object from a list of surface ids and a volume id.
 
         Args:
-            - value (int): The id of the volume.
+            - surface_ids (Sequence[int]): A sequence of four or more integers representing the ids of the surfaces that\
+                make up the volume.
+            - id (int): The id of the volume.
+
+        Returns:
+            - :class:`Volume`: A volume object.
 
         """
-        self.__id = value
+        volume = cls(id)
+        volume.surface_ids = surface_ids
+        return volume
 
 
 class Geometry:
@@ -190,37 +296,36 @@ class Geometry:
     A class to represent a collection of geometric objects in a zero-, one-, two- or three-dimensional space.
 
     Attributes:
-        - points (Optional[List[:class:`Point`]]): An Iterable of Point objects representing the points in the geometry.
-        - lines (Optional[List[:class:`Line`]]): An Iterable of Line objects representing the lines in the geometry.
-        - surfaces (Optional[List[:class:`Surface`]]): An Iterable of Surface objects representing the surfaces in the geometry.
-        - volumes (Optional[List[:class:`Volume`]]): An Iterable of Volume objects representing the volumes in the geometry.
+        - points (Dict[int, :class:`Point`]): An dictionary of Point objects representing the points in the geometry.
+        - lines (Dict[int, :class:`Line`]): A dictionary of Line objects representing the lines in the geometry.
+        - surfaces (Dict[int, :class:`Surface`]): A dictionary of Surface objects representing the surfaces in the \
+          geometry.
+        - volumes (Dict[int, :class:`Volume`]): A dictionary of Volume objects representing the volumes in the geometry.
     """
-    def __init__(self, points: Optional[List[Point]] = None, lines: Optional[List[Line]] = None,
-                 surfaces: Optional[List[Surface]] = None, volumes: Optional[List[Volume]] = None):
-        self.points: Optional[List[Point]] = points
-        self.lines: Optional[List[Line]] = lines
-        self.surfaces: Optional[List[Surface]] = surfaces
-        self.volumes: Optional[List[Volume]] = volumes
+    def __init__(self, points: Dict[int, Point] = {}, lines: Dict[int, Line] = {},
+                 surfaces: Dict[int, Surface] = {}, volumes: Dict[int, Volume] = {}):
+        self.points: Dict[int, Point] = points
+        self.lines: Dict[int, Line] = lines
+        self.surfaces: Dict[int, Surface] = surfaces
+        self.volumes: Dict[int, Volume] = volumes
 
-    @staticmethod
-    def __get_unique_entities_by_ids(entities: Sequence[GeometricalObjectABC]):
+    def __getattribute__(self, item: str):
         """
-        Returns a list of unique entities by their ids.
+        Overrides the getattribute method of the object class.
 
         Args:
-            - entities (Sequence[:class:`GeometricalObjectABC`]): An Sequence of geometrical entities.
+            - item (str): The name of the attribute.
 
         Returns:
-            - unique_entities (List[:class:`GeometricalObjectABC`): A list of unique geometrical entities entities.
+            - Any: The attribute.
 
         """
-        unique_entity_ids = []
-        unique_entities = []
-        for entity in entities:
-            if entity.id not in unique_entity_ids:
-                unique_entity_ids.append(entity.id)
-                unique_entities.append(entity)
-        return unique_entities
+        # Make sure that the create_geometry_from_geo_data method  and the create_geometry_from_gmsh_group cannot be
+        # called on an initialised geometry instance
+        if item == "create_geometry_from_geo_data" or item == "create_geometry_from_gmsh_group":
+            raise AttributeError(f"Cannot call class method: {item} from an initialised geometry instance.")
+        else:
+            return super().__getattribute__(item)
 
     @staticmethod
     def __set_point(geo_data: Dict[str, Any], point_id: int):
@@ -232,16 +337,14 @@ def __set_point(geo_data: Dict[str, Any], point_id: int):
             - point_id (int): The id of the line to create.
 
         Returns:
-            - point (:class:`Point`): The point object.
+            - :class:`Point`: The point object.
         """
 
         # create point
-        point = Point(point_id)
-        point.coordinates = geo_data["points"][point.id]
-        return point
+        return Point.create(geo_data["points"][point_id], point_id)
 
     @staticmethod
-    def __set_line(geo_data: Dict[str,Any], line_id: int):
+    def __set_line(geo_data: Dict[str, Any], line_id: int):
         """
         Creates a line from the geometry data.
 
@@ -250,17 +353,16 @@ def __set_line(geo_data: Dict[str,Any], line_id: int):
             - line_id (int): The id of the line to create.
 
         Returns:
-            - line (:class:`Line`): The line object.
+            - Tuple[:class:`Line`, Sequence[:class:`Point`]]: The line object and the points that make up the line.
         """
 
-        # Initialise point list
-        points = []
-
         # create line and lower dimensional objects
-        line = Line(abs(line_id))
-        line.point_ids = geo_data["lines"][line.id]
-        for point_id in line.point_ids:
-            points.append(Geometry.__set_point(geo_data, point_id))
+        line_id = abs(line_id)
+        line = Line.create(geo_data["lines"][line_id], line_id)
+
+        # Using list comprehension to get the points
+        points = [Geometry.__set_point(geo_data, point_id) for point_id in line.point_ids]
+
         return line, points
 
     @staticmethod
@@ -273,7 +375,8 @@ def __create_surface(geo_data: Dict[str, Any], surface_id: int):
             - surface_id (int): The id of the surface to create.
 
         Returns:
-            - surface (:class:`Surface`): The surface object.
+            - Tuple[:class:`Surface`, Sequence[:class:`Line`], Sequence[:class:`Point`]]: The surface object, \
+                the lines that make up the surface and the points that make up the lines.
         """
 
         # Initialise point and line lists
@@ -281,8 +384,8 @@ def __create_surface(geo_data: Dict[str, Any], surface_id: int):
         lines = []
 
         # create surface and lower dimensional objects
-        surface = Surface(abs(surface_id))
-        surface.line_ids = geo_data["surfaces"][surface.id]
+        surface_id = abs(surface_id)
+        surface = Surface.create(geo_data["surfaces"][surface_id], surface_id)
         for line_id in surface.line_ids:
             line, line_points = Geometry.__set_line(geo_data, line_id)
 
@@ -291,6 +394,43 @@ def __create_surface(geo_data: Dict[str, Any], surface_id: int):
 
         return surface, lines, points
 
+    @classmethod
+    def create_geometry_from_geo_data(cls, geo_data: Dict[str, Any]):
+        """
+        Creates the geometry from gmsh geo_data
+
+        Args:
+            - geo_data (Dict[str, Any]): A dictionary containing the geometry data as provided by gmsh_utils.
+
+        Returns:
+            - :class:`Geometry`: The geometry object.
+        """
+
+        # initialise geometry dictionaries
+        points = {}
+        lines = {}
+        surfaces = {}
+        volumes = {}
+
+        # add volumes to geometry
+        for key, value in geo_data["volumes"].items():
+            volumes[key] = Volume.create(value, key)
+
+        # add surfaces to geometry
+        for key, value in geo_data["surfaces"].items():
+            surfaces[key] = Surface.create(value, key)
+
+        # add lines to geometry
+        for key, value in geo_data["lines"].items():
+            lines[key] = Line.create(value,key)
+
+        # add points to geometry
+        for key, value in geo_data["points"].items():
+            points[key] = Point.create(value,key)
+
+        # create the geometry class
+        return cls(points, lines, surfaces, volumes)
+
     @classmethod
     def create_geometry_from_gmsh_group(cls, geo_data: Dict[str, Any], group_name: str):
         """
@@ -301,14 +441,14 @@ def create_geometry_from_gmsh_group(cls, geo_data: Dict[str, Any], group_name: s
             - group_name (str): The name of the group to create the geometry from.
 
         Returns:
-            - geometry (:class:`Geometry`): A Geometry object containing the geometric objects in the group.
+            - :class:`Geometry`: A Geometry object containing the geometric objects in the group.
         """
 
-        # initialize point, line, surface and volume lists
-        points = []
-        lines = []
-        surfaces = []
-        volumes = []
+        # initialize point, line, surface and volume dictionaries
+        points = {}
+        lines = {}
+        surfaces = {}
+        volumes = {}
 
         group_data = geo_data["physical_groups"][group_name]
         ndim_group = group_data["ndim"]
@@ -316,42 +456,41 @@ def create_geometry_from_gmsh_group(cls, geo_data: Dict[str, Any], group_name: s
         if ndim_group == 0:
             # create points
             for id in group_data["geometry_ids"]:
-                points.append(Geometry.__set_point(geo_data, id))
+                points[id] = Geometry.__set_point(geo_data, id)
         elif ndim_group == 1:
             # create lines and lower dimensional objects
             for id in group_data["geometry_ids"]:
                 line, line_points = Geometry.__set_line(geo_data, id)
 
-                lines.append(line)
-                points.extend(line_points)
+                lines[id] = line
+                for point in line_points:
+                    points[point.id] = point
 
         elif ndim_group == 2:
             # create surfaces and lower dimensional objects
             for id in group_data["geometry_ids"]:
 
                 surface, surface_lines, surface_points = Geometry.__create_surface(geo_data, id)
-                surfaces.append(surface)
-                lines.extend(surface_lines)
-                points.extend(surface_points)
+                surfaces[id] = surface
+                for line in surface_lines:
+                    lines[line.id] = line
+                for point in surface_points:
+                    points[point.id] = point
 
         elif ndim_group == 3:
             # Create volumes and lower dimensional objects
             for id in group_data["geometry_ids"]:
-                volume = Volume(id)
-                volume.surface_ids = geo_data["volumes"][volume.id]
+                volume = Volume.create(geo_data["volumes"][id], id)
 
                 # create surfaces and lower dimensional objects which are part of the current volume
                 for surface_id in volume.surface_ids:
                     surface, surface_lines, surface_points = Geometry.__create_surface(geo_data, surface_id)
-                    surfaces.append(surface)
-                    lines.extend(surface_lines)
-                    points.extend(surface_points)
-                volumes.append(volume)
-
-        # remove duplicates from points, lines, surfaces, volumes
-        unique_volumes = Geometry.__get_unique_entities_by_ids(volumes)
-        unique_surfaces = Geometry.__get_unique_entities_by_ids(surfaces)
-        unique_lines = Geometry.__get_unique_entities_by_ids(lines)
-        unique_points = Geometry.__get_unique_entities_by_ids(points)
-
-        return cls(unique_points, unique_lines, unique_surfaces, unique_volumes)
+                    surfaces[abs(surface_id)] = surface
+                    for line in surface_lines:
+                        lines[line.id] = line
+                    for point in surface_points:
+                        points[point.id] = point
+
+                volumes[id] = volume
+
+        return cls(points, lines, surfaces, volumes)
diff --git a/stem/load.py b/stem/load.py
index a013a600..8297f217 100644
--- a/stem/load.py
+++ b/stem/load.py
@@ -1,14 +1,33 @@
 from typing import List, Dict, Any, Union, Optional
-from dataclasses import dataclass, field
-from abc import ABC
+from dataclasses import dataclass
+from abc import ABC, abstractmethod
 
+from stem.solver import AnalysisType
+from stem.table import Table
+from stem.utils import Utils
 
 @dataclass
 class LoadParametersABC(ABC):
     """
     Abstract base class for load parameters
     """
-    pass
+
+    @staticmethod
+    @abstractmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Abstract static method to get the element name for a load.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model
+            - n_nodes_element (int): The number of nodes per condition-element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type.
+
+        Raises:
+            - Exception: abstract method is called
+
+        """
+        raise Exception("abstract method 'get_element_name' of load parameters class is called")
 
 
 @dataclass
@@ -21,11 +40,46 @@ class PointLoad(LoadParametersABC):
 
     Attributes:
         - active (List[bool]): Activate/deactivate load for each direction.
-        - value (List[float]): Entity of the load in the 3 directions [N].
+        - value (List[Union[float, :class:`stem.table.Table`]]): Entity of the load in the 3 directions [N]. \
+            It should be a list of either float or table for each load. If a float is specified, the \
+            load is time-independent, otherwise the table specifies the amplitude of the \
+            load [N] over time [s] for each direction.
     """
 
-    active: List[bool] = field(default_factory=lambda: [True, True, True])
-    value: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
+    active: List[bool]
+    value: List[Union[float, Table]]
+
+    @staticmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Static method to get the element name for a point load. Point load does not have a name.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model (2 or 3)
+            - n_nodes_element (int): The number of nodes per condition-element (1)
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type
+
+        Raises:
+            - ValueError: If the analysis type is not mechanical or mechanical groundwater flow
+
+        Returns:
+            - None: Point load does not have a name
+
+
+        """
+
+        available_node_dim_combinations = {
+            2: [1],
+            3: [1],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, n_nodes_element, available_node_dim_combinations,
+                                             "Point load")
+
+        if analysis_type != AnalysisType.MECHANICAL_GROUNDWATER_FLOW and analysis_type != AnalysisType.MECHANICAL:
+            raise ValueError("Point load can only be applied in mechanical or mechanical groundwater flow analysis")
+
+        # Point load does not have a name
+        return None
 
 
 @dataclass
@@ -35,10 +89,50 @@ class LineLoad(LoadParametersABC):
 
     Attributes:
         - active (List[bool]): Activate/deactivate load for each direction.
-        - value (List[float]): Entity of the load in the 3 directions [N].
+        - value (List[Union[float, :class:`stem.table.Table`]]): Entity of the load in the 3 directions [N/m]. \
+            It should be a list of either float or table for each load. If a float is specified, the \
+            load is time-independent, otherwise the table specifies the amplitude of the \
+            load [N/m] over time [s] for each direction.
     """
-    active: List[bool] = field(default_factory=lambda: [True, True, True])
-    value: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
+    active: List[bool]
+    value: List[Union[float, Table]]
+
+    @staticmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Static method to get the element name for a line load.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model (2 or 3)
+            - n_nodes_element (int): The number of nodes per condition-element (2, 3)
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type
+
+        Raises:
+            - ValueError: If the analysis type is not mechanical or mechanical groundwater flow
+
+        Returns:
+            - Optional[str]: The element name for a line load
+
+        """
+
+        available_node_dim_combinations = {
+            2: [2, 3],
+            3: [2, 3],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, n_nodes_element, available_node_dim_combinations,
+                                             "Line load")
+
+        if analysis_type == AnalysisType.MECHANICAL_GROUNDWATER_FLOW or analysis_type == AnalysisType.MECHANICAL:
+            if n_dim_model == 2 and n_nodes_element > 2:
+                # 2d quadratic line load is set on outer nodes, but displacement is calculated on all nodes for
+                # stability reasons
+                element_name = f"LineLoadDiffOrderCondition{n_dim_model}D{n_nodes_element}N"
+            else:
+                element_name = f"LineLoadCondition{n_dim_model}D{n_nodes_element}N"
+        else:
+            raise ValueError("Line load can only be applied in mechanical or mechanical groundwater flow analysis")
+
+        return element_name
 
 
 @dataclass
@@ -48,10 +142,46 @@ class SurfaceLoad(LoadParametersABC):
 
     Attributes:
         - active (List[bool]): Activate/deactivate load for each direction.
-        - value (List[float]): Entity of the load in the 3 directions [N].
+        - value (List[Union[float, :class:`stem.table.Table`]]): Entity of the load in the 3 directions [Pa]. \
+            It should be a list of either float or table for each load. If a float is specified, the \
+            load is time-independent, otherwise the table specifies the amplitude of the \
+            load [Pa] over time [s] for each direction.
     """
-    active: List[bool] = field(default_factory=lambda: [True, True, True])
-    value: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
+    active: List[bool]
+    value: Union[List[float], List[Table]]
+
+    @staticmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Static method to get the element name for a surface load.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model (3)
+            - n_nodes_element (int): The number of nodes per condition-element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type
+
+        Raises:
+            - ValueError: If the analysis type is not mechanical or mechanical groundwater flow
+
+        Returns:
+            - Optional[str]: The element name for a surface load
+        """
+
+        available_node_dim_combinations = {
+            3: [3, 4, 6, 8],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, n_nodes_element, available_node_dim_combinations,
+                                             "Surface load")
+
+        if analysis_type == AnalysisType.MECHANICAL_GROUNDWATER_FLOW or analysis_type == AnalysisType.MECHANICAL:
+            if n_nodes_element == 3 or n_nodes_element == 4:
+                element_name = f"UPwFaceLoadCondition{n_dim_model}D{n_nodes_element}N"
+            else:
+                element_name = f"SurfaceLoadDiffOrderCondition{n_dim_model}D{n_nodes_element}N"
+        else:
+            raise ValueError("Surface load can only be applied in mechanical or mechanical groundwater flow analysis")
+
+        return element_name
 
 
 @dataclass
@@ -72,12 +202,43 @@ class MovingLoad(LoadParametersABC):
         - offset (float): Offset of the moving load [m].
     """
 
-    load: Union[List[float], List[str]] = field(default_factory=lambda: [0.0, 0.0, 0.0])
-    direction: List[float] = field(default_factory=lambda: [1, 1, 1])
-    velocity: Union[float, str] = 0.0
-    origin: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
+    load: Union[List[float], List[str]]
+    direction: List[float]
+    velocity: Union[float, str]
+    origin: List[float]
     offset: float = 0.0
 
+    @staticmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Static method to get the element name for a moving load.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model (2 or 3)
+            - n_nodes_element (int): The number of nodes per condition-element (2, 3)
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type
+
+        Raises:
+            - ValueError: If the analysis type is not mechanical or mechanical groundwater flow
+
+        Returns:
+            - Optional[str]: The element name for a moving load
+        """
+
+        available_node_dim_combinations = {
+            2: [2, 3],
+            3: [2, 3],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, n_nodes_element, available_node_dim_combinations,
+                                             "Moving load")
+
+        if analysis_type == AnalysisType.MECHANICAL_GROUNDWATER_FLOW or analysis_type == AnalysisType.MECHANICAL:
+            element_name = f"MovingLoadCondition{n_dim_model}D{n_nodes_element}N"
+        else:
+            raise ValueError("Moving load can only be applied in mechanical or mechanical groundwater flow analysis")
+
+        return element_name
+
 
 @dataclass
 class GravityLoad(LoadParametersABC):
@@ -89,8 +250,31 @@ class GravityLoad(LoadParametersABC):
 
     Attributes:
         - active (List[bool]): Activate/deactivate load for each direction. Input True only in the vertical direction.
-        - value (List[float]): Entity of the gravity acceleration in the 3 directions [m/s^2]. Should be -9.81 only in
+        - value (List[float]): Entity of the gravity acceleration in the 3 directions [m/s^2]. Should be -9.81 only in \
             the vertical direction
     """
-    active: List[bool] = field(default_factory=lambda: [False, False, False])
-    value: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
+    active: List[bool]
+    value: List[float]
+
+    @staticmethod
+    def get_element_name(n_dim_model, n_nodes_element, analysis_type) -> Optional[str]:
+        """
+        Static method to get the element name for a gravity load.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model
+            - n_nodes_element (int): The number of nodes per element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type
+
+        Raises:
+            - ValueError: If the analysis type is not mechanical or mechanical groundwater flow
+
+        Returns:
+            - None: Gravity load does not have a name
+        """
+
+        if analysis_type != AnalysisType.MECHANICAL_GROUNDWATER_FLOW and analysis_type != AnalysisType.MECHANICAL:
+            raise ValueError("Point load can only be applied in mechanical or mechanical groundwater flow analysis")
+
+        # Gravity load does not have a name
+        return None
diff --git a/stem/mesh.py b/stem/mesh.py
index 6ea3323b..2ab01ab0 100644
--- a/stem/mesh.py
+++ b/stem/mesh.py
@@ -1,8 +1,73 @@
-from typing import Dict, List, Tuple, Union, Any
-import numpy as np
-import numpy.typing as npt
+from typing import Dict, List, Sequence, Any
+from enum import Enum
 
-from stem.IO.kratos_io import KratosIO
+
+class ElementShape(Enum):
+    """
+    Enum class for the element shape. TRIANGLE for triangular elements and tetrahedral elements, QUADRILATERAL for
+    quadrilateral elements and hexahedral elements.
+
+    """
+    TRIANGLE = "triangle"
+    QUADRILATERAL = "quadrilateral"
+
+
+class MeshSettings:
+    """
+    A class to represent the mesh settings.
+
+    Attributes:
+        - element_size (float): The element size (default -1, which means that gmsh determines the size).
+        - element_shape (:class:`ElementShape`): The element shape. TRIANGLE for triangular elements and \
+            tetrahedral elements,  QUADRILATERAL for quadrilateral elements and hexahedral elements. (default TRIANGLE)
+        - __element_order (int): The element order. 1 for linear elements, 2 for quadratic elements. (default 1)
+    """
+
+    def __init__(self, element_size: float = -1, element_order: int = 1,
+                 element_shape: ElementShape = ElementShape.TRIANGLE):
+        """
+        Initialize the mesh settings.
+
+        Args:
+            - element_size (float): The element size (default -1, which means that gmsh determines the size).
+            - element_order (int): The element order. 1 for linear elements, 2 for quadratic elements. (default 1)
+            - element_shape (:class:`ElementShape`): The element shape. TRIANGLE for triangular elements and \
+            tetrahedral elements,  QUADRILATERAL for quadrilateral elements and hexahedral elements. (default TRIANGLE)
+        """
+        self.element_size: float = element_size
+        self.element_shape: ElementShape = element_shape
+
+        if element_order not in [1, 2]:
+            raise ValueError("The element order must be 1 or 2. Higher order elements are not supported.")
+
+        self.__element_order: int = element_order
+
+    @property
+    def element_order(self) -> int:
+        """
+        Get the element order.
+
+        Returns:
+            - int: element order
+        """
+        return self.__element_order
+
+    @element_order.setter
+    def element_order(self, element_order: int):
+        """
+        Set the element order. The element order must be 1 or 2.
+
+        Args:
+            - element_order (int): element order
+
+        Raises:
+            - ValueError: If the element order is not 1 or 2.
+        """
+
+        if element_order not in [1, 2]:
+            raise ValueError("The element order must be 1 or 2. Higher order elements are not supported.")
+
+        self.__element_order = element_order
 
 
 class Node:
@@ -11,12 +76,21 @@ class Node:
 
     Attributes:
         - id (int): node id
-        - coordinates (np.array): node coordinates
+        - coordinates (Sequence[float]): node coordinates
 
     """
-    def __init__(self, id, coordinates):
-        self.id = id
-        self.coordinates = coordinates
+
+    def __init__(self, id: int, coordinates: Sequence[float]):
+        """
+        Initialize the node.
+
+        Args:
+            id (int): Node id
+            coordinates (Sequence[float]): Node coordinates
+        """
+        self.id: int = id
+        self.coordinates: Sequence[float] = coordinates
+
 
 class Element:
     """
@@ -24,30 +98,23 @@ class Element:
 
     Attributes:
         - id (int): element id
-        - element_type (str): element type
-        - node_ids (Union[List[int], npt.NDArray[np.int64]]): node ids
-
-    """
-    def __init__(self, id: int, element_type: str, node_ids: Union[List[int], npt.NDArray[np.int64]]):
-        self.id: int = id
-        self.element_type: str = element_type
-        self.node_ids: Union[List[int], npt.NDArray[np.int64]] = node_ids
-
+        - element_type (str): Gmsh element type
+        - node_ids (Sequence[int]): node ids
 
-class Condition:
     """
-    Class containing information about a condition
 
-    Attributes:
-        - id (int): condition id
-        - element_type (str): element type
-        - node_ids (Union[List[int], npt.NDArray[np.int64]]): node ids
+    def __init__(self, id: int, element_type: str, node_ids: Sequence[int]):
+        """
+        Initialize the element.
 
-    """
-    def __init__(self, id: int, element_type: str, node_ids: Union[List[int], npt.NDArray[np.int64]]):
+        Args:
+            id (int): Element id
+            element_type (str): Gmsh-element type
+            node_ids (Sequence[int]): Node connectivities
+        """
         self.id: int = id
         self.element_type: str = element_type
-        self.node_ids: Union[List[int], npt.NDArray[np.int64]] = node_ids
+        self.node_ids: Sequence[int] = node_ids
 
 
 class Mesh:
@@ -59,68 +126,78 @@ class Mesh:
 
     Attributes:
         - ndim (int): number of dimensions of the mesh
-        - nodes (np.array or None): node id followed by node coordinates in an array
-        - elements (np.array or None): element id followed by connectivities in an array
-        - conditions (np.array or None): condition id followed by connectivities in an array
+        - nodes (List[Node]): node id followed by node coordinates in a list
+        - elements (List[Element]): element id followed by connectivities in a list
 
     """
     def __init__(self, ndim: int):
-
-        self.ndim: int = ndim
-        self.nodes = None
-        self.elements = None
-        self.conditions = None
-
-
-    @classmethod
-    def read_mesh_from_gmsh(cls, mesh_file_name: str) -> None:
-        #todo implement this method to read mesh from gmsh file and create a mesh object with the data read from the
-        # file.
-        pass
-
-    def prepare_data_for_kratos(self, mesh_data: Dict[str, Any]) \
-            -> Tuple[npt.NDArray[np.float64], npt.NDArray[np.int64]]:
         """
-        Prepares mesh data for Kratos
+        Initialize the mesh.
 
         Args:
-            - mesh_data (Dict[str, Any]): dictionary of mesh data
-
-
-        Returns:
-            - nodes (npt.NDArray[np.float64]): node id followed by node coordinates in an array
-            - elements (npt.NDArray[np.int64]): element id followed by connectivities in an array
+            ndim (int): number of dimensions of the mesh
         """
 
-        # create array of nodes where each row is represented by [id, x,y,z]
-        nodes = np.concatenate((mesh_data["nodes"]["ids"][:, None], mesh_data["nodes"]["coordinates"]), axis=1)
+        self.ndim: int = ndim
+        self.nodes: List[Node] = []
+        self.elements: List[Element] = []
 
-        all_elements_list = []
-        # create array of elements where each row is represented by [id, node connectivities]
-        for v in mesh_data["elements"].values():
-            all_elements_list.append(np.concatenate((v["element_ids"][:, None], v["element_nodes"]), axis=1))
+    def __getattribute__(self, item: str) -> Any:
+        """
+        Overrides the getattribute method of the object class.
 
-        all_elements = np.array(all_elements_list).astype(int)
+        Args:
+            - item (str): The name of the attribute.
 
-        return nodes, all_elements
+        Returns:
+            - Any: The attribute.
 
+        """
+        # Make sure that the create_mesh_from_gmsh_group method cannot be
+        # called on an initialised mesh instance
+        if item == "create_mesh_from_gmsh_group":
+            raise AttributeError(f"Cannot call class method: {item} from an initialised mesh instance.")
+        else:
+            return super().__getattribute__(item)
 
-    def write_mesh_to_kratos_structure(self, mesh_data: Dict[str, Any], filename: str) -> None:
+    @classmethod
+    def create_mesh_from_gmsh_group(cls, mesh_data: Dict[str, Any], group_name: str) -> "Mesh":
         """
-        Writes mesh data to the structure which can be read by Kratos
+        Creates a mesh object from gmsh group
 
         Args:
             - mesh_data (Dict[str, Any]): dictionary of mesh data
-            - filename (str): filename of the kratos mesh file
+            - group_name (str): name of the group
+
+        Raises:
+            - ValueError: If the group name is not found in the mesh data
 
         Returns:
+            - :class:`Mesh`: mesh object
         """
 
-        nodes, elements = self.prepare_data_for_kratos(mesh_data)
+        if group_name not in mesh_data["physical_groups"]:
+            raise ValueError(f"Group {group_name} not found in mesh data")
+
+        # create mesh object
+        group_data = mesh_data["physical_groups"][group_name]
+
+        group_element_ids = group_data["element_ids"]
+        group_node_ids = group_data["node_ids"]
+        group_element_type = group_data["element_type"]
 
-        kratos_io = KratosIO(self.ndim)
-        kratos_io.write_mesh_to_mdpa(filename)
+        element_type_data = mesh_data["elements"][group_element_type]
 
+        # create element per element id
+        elements = [Element(element_id, group_element_type, element_type_data[element_id])
+                    for element_id in group_element_ids]
 
+        # create node per node id
+        nodes = [Node(node_id, mesh_data["nodes"][node_id]) for node_id in group_node_ids]
 
+        # add nodes and elements to mesh object
+        mesh = cls(group_data["ndim"])
+        mesh.nodes = nodes
+        mesh.elements = elements
 
+        return mesh
diff --git a/stem/model.py b/stem/model.py
index 4aec9b72..47283cec 100644
--- a/stem/model.py
+++ b/stem/model.py
@@ -1,6 +1,23 @@
-from typing import List
+from typing import List, Sequence, Dict, Any, Optional, Union
+
+import numpy.typing as npty
+import numpy as np
+
+from gmsh_utils import gmsh_IO
 
 from stem.model_part import ModelPart, BodyModelPart
+from stem.soil_material import *
+from stem.structural_material import *
+from stem.boundary import *
+from stem.geometry import Geometry
+from stem.mesh import Mesh, MeshSettings, Node
+from stem.load import *
+from stem.solver import Problem, StressInitialisationType
+from stem.utils import Utils
+from stem.plot_utils import PlotUtils
+
+
+NUMBER_TYPES = (int, float, np.int64, np.float64)
 
 
 class Model:
@@ -8,17 +25,502 @@ class Model:
     A class to represent the main model.
 
     Attributes:
-        - project_parameters (dict): A dictionary containing the project parameters.
-        - solver (Solver): The solver used to solve the problem.
-        - body_model_parts (list): A list containing the body model parts.
-        - process_model_parts (list): A list containing the process model parts.
+        - ndim (int): Number of dimensions of the model
+        - project_parameters (:class:`stem.solver.Problem): Object containing the problem data and solver settings.
+        - geometry (Optional[:class:`stem.geometry.Geometry`]) The geometry of the whole model.
+        - body_model_parts (List[:class:`stem.model_part.BodyModelPart`]): A list containing the body model parts.
+        - process_model_parts (List[:class:`stem.model_part.ModelPart`]): A list containing the process model parts.
+        - extrusion_length (Optional[Sequence[float]]): The extrusion length in x, y and z direction
 
     """
-    def __init__(self):
+    def __init__(self, ndim: int):
+        """
+        Constructor of the Model class.
 
-        self.project_parameters = None
-        self.solver = None
+        Args:
+            - ndim (int): Number of dimensions of the model
+        """
+        self.ndim: int = ndim
+        self.project_parameters: Optional[Problem] = None
+        self.geometry: Optional[Geometry] = None
+        self.mesh_settings: MeshSettings = MeshSettings()
+        self.gmsh_io = gmsh_IO.GmshIO()
         self.body_model_parts: List[BodyModelPart] = []
         self.process_model_parts: List[ModelPart] = []
 
+        self.extrusion_length: Optional[Sequence[float]] = None
+
+    def __del__(self):
+        """
+        Destructor of the Model class. Finalizes the gmsh_io instance.
+
+        """
+        self.gmsh_io.finalize_gmsh()
+
+    def __get_geometry_from_geo_data(self, geo_data: Dict[str, Any]):
+        """
+        Get the geometry from the geo_data as generated by gmsh_io.
+
+        Args:
+            - geo_data (Dict[str, Any]): dictionary containing the geometry data as generated by the gmsh_io
+
+        """
+
+        self.geometry = Geometry.create_geometry_from_geo_data(geo_data)
+
+    def add_all_layers_from_geo_file(self, geo_file_name: str, body_names: Sequence[str]):
+        """
+        Add all physical groups from a geo file to the model. The physical groups with the names in body_names are
+        added as body model parts, the other physical groups are added as process model parts.
+
+        Args:
+            - geo_file_name (str): name of the geo file
+            - body_names (Sequence[str]): names of the physical groups which should be added as body model parts
+
+        """
+
+        # read the geo file and generate the geo_data dictionary
+        self.gmsh_io.read_gmsh_geo(geo_file_name)
+
+        # Reset the gmsh instance with the geo data, as read from the geo file
+        self.gmsh_io.generate_geo_from_geo_data()
+
+        # Create geometry and model part for each physical group in the gmsh geo_data
+        model_part: Union[ModelPart, BodyModelPart]
+        for group_name in self.gmsh_io.geo_data["physical_groups"].keys():
+
+            # create model part, if the group name is in the body names, create a body model part, otherwise a process
+            # model part
+            if group_name in body_names:
+                model_part = BodyModelPart(group_name)
+            else:
+                model_part = ModelPart(group_name)
+
+            # set the name and geometry of the model part
+            model_part.get_geometry_from_geo_data(self.gmsh_io.geo_data, group_name)
+
+            # add model part to either body model parts or process model part
+            if isinstance(model_part, BodyModelPart):
+                self.body_model_parts.append(model_part)
+            else:
+                self.process_model_parts.append(model_part)
+
+    def add_soil_layer_by_coordinates(self, coordinates: Sequence[Sequence[float]],
+                       material_parameters: Union[SoilMaterial, StructuralMaterial], name: str,
+                       ):
+        """
+        Adds a soil layer to the model by giving a sequence of 2D coordinates. In 3D the 2D geometry is extruded in
+        the direction of the extrusion_length
+
+        Args:
+            - coordinates (Sequence[Sequence[float]]): The plane coordinates of the soil layer.
+            - material_parameters (Union[:class:`stem.soil_material.SoilMaterial`, \
+                :class:`stem.structural_material.StructuralMaterial`]): The material parameters of the soil layer.
+            - name (str): The name of the soil layer.
+
+        Raises:
+            - ValueError: if extrusion_length is not specified in 3D.
+        """
+
+        # sort coordinates in anti-clockwise order, such that elements in mesh are also in anti-clockwise order
+        if Utils.are_2d_coordinates_clockwise(coordinates):
+            coordinates = coordinates[::-1]
+
+        gmsh_input = {name: {"coordinates": coordinates, "ndim": self.ndim}}
+        # check if extrusion length is specified in 3D
+        if self.ndim == 3:
+            if self.extrusion_length is None:
+                raise ValueError("Extrusion length must be specified for 3D models")
+
+            gmsh_input[name]["extrusion_length"] = self.extrusion_length
+
+        # todo check if this function in gmsh io can be improved
+        self.gmsh_io.generate_geometry(gmsh_input, "")
+
+        # create body model part
+        body_model_part = BodyModelPart(name)
+        body_model_part.material = material_parameters
+
+        # set the geometry of the body model part
+        body_model_part.get_geometry_from_geo_data(self.gmsh_io.geo_data, name)
+
+        self.body_model_parts.append(body_model_part)
+
+    def add_load_by_coordinates(self, coordinates: Sequence[Sequence[float]], load_parameters: LoadParametersABC,
+                                name: str):
+        """
+        Adds a load to the model by giving a sequence of 3D coordinates. For a 2D model, the third coordinate is
+        ignored.
+
+        Args:
+            - coordinates (Sequence[Sequence[float]]): The coordinates of the load.
+            - load_parameters (:class:`stem.load.LoadParametersABC`): The parameters of the load.
+            - name (str): The name of the load part.
+
+        Raises:
+            - ValueError: if load_parameters is not of one of the classes PointLoad, MovingLoad, LineLoad
+                          or SurfaceLoad.
+        """
+
+        # todo add validation that load is applied on a body model part
+
+        # validation of inputs
+        self.validate_coordinates(coordinates)
+        if isinstance(load_parameters, MovingLoad):
+            self.__validate_moving_load_parameters(coordinates, load_parameters)
+
+        # create input for gmsh
+        if isinstance(load_parameters, PointLoad):
+            gmsh_input = {name: {"coordinates": coordinates, "ndim": 0}}
+        elif isinstance(load_parameters, LineLoad) or isinstance(load_parameters, MovingLoad):
+            gmsh_input = {name: {"coordinates": coordinates, "ndim": 1}}
+        elif isinstance(load_parameters, SurfaceLoad):
+            gmsh_input = {name: {"coordinates": coordinates, "ndim": 2}}
+        else:
+            raise ValueError(f'Invalid load_parameters ({load_parameters.__class__.__name__}) object'
+                             f' provided for the load {name}. Expected one of PointLoad, MovingLoad,'
+                             f' LineLoad or SurfaceLoad.')
+
+        self.gmsh_io.generate_geometry(gmsh_input, "")
+
+        # create model part
+        model_part = ModelPart(name)
+        model_part.parameters = load_parameters
+
+        # set the geometry of the model part
+        model_part.get_geometry_from_geo_data(self.gmsh_io.geo_data, name)
+
+        self.process_model_parts.append(model_part)
+
+    @staticmethod
+    def validate_coordinates(coordinates: Union[Sequence[Sequence[float]], npty.NDArray[np.float64]]):
+        """
+        Validates the coordinates in input.
+
+        Args:
+            - coordinates (Sequence[Sequence[float]]): The coordinates of the load.
+
+        Raises:
+            - ValueError: if coordinates is not a sequence real numbers.
+            - ValueError: if coordinates is not convertible to a 2D array (i.e. a sequence of sequences)
+            - ValueError: if the number of elements (number of coordinates) is not 3.
+        """
+
+        # if is not an array, make it array!
+        if not isinstance(coordinates, np.ndarray):
+            coordinates = np.array(coordinates, dtype=np.float64)
+
+        if len(coordinates.shape) != 2:
+            raise ValueError(f"Coordinates are not a sequence of a sequence or a 2D array.")
+
+        if coordinates.shape[1] != 3:
+            raise ValueError(f"Coordinates should be 3D but {coordinates.shape[1]} coordinates were given.")
+
+        # check if coordinates are real numbers
+        for coordinate in coordinates:
+            for i in coordinate:
+                if not isinstance(i, NUMBER_TYPES) or np.isnan(i) or np.isinf(i):
+                    raise ValueError(f"Coordinates should be a sequence of sequence of real numbers, "
+                                     f"but {i} was given.")
+
+    @staticmethod
+    def __validate_moving_load_parameters(coordinates: Sequence[Sequence[float]], load_parameters: MovingLoad) -> None:
+        """
+        Validates the coordinates in input for the moving load and the trajectory (collinearity of the
+        points and if the origin is between the point).
+
+        Args:
+            - coordinates (Sequence[Sequence[float]]): The start-end coordinate of the moving load.
+            - parameters (:class:`stem.load.LoadParametersABC`): The parameters of the load.
+
+        Raises:
+            - ValueError: if moving load origin is not on trajectory
+
+        Returns:
+            - None
+        """
+
+        # iterate over each line constituting the trajectory
+        for ix in range(len(coordinates)-1):
+
+            # check origin is collinear to the edges of the line
+            collinear_check = Utils.is_collinear(
+                point=load_parameters.origin, start_point=coordinates[ix],end_point=coordinates[ix+1]
+            )
+            # check origin is between the edges of the line (edges included)
+            is_between_check = Utils.is_point_between_points(
+                point=load_parameters.origin, start_point=coordinates[ix], end_point=coordinates[ix+1]
+            )
+            # check if point complies
+            is_on_line = collinear_check and is_between_check
+            # exit at the first success of the test (point in the line)
+            if is_on_line:
+                return
+
+        # none of the lines contain the origin, then raise an error
+        raise ValueError(f"Origin is not in the trajectory of the moving load.")
+
+    def add_boundary_condition_by_geometry_ids(self, ndim_boundary: int, geometry_ids: Sequence[int],
+                                               boundary_parameters: BoundaryParametersABC, name: str):
+        """
+        Add a boundary condition to the model by giving the geometry ids of the boundary condition.
+
+        Args:
+            - ndim_boundary (int): dimension of the boundary condition
+            - geometry_ids (Sequence[int]): geometry ids of the boundary condition
+            - boundary_condition (:class:`stem.boundary_condition.BoundaryCondition`): boundary condition object
+            - name (str): name of the boundary condition
+
+        """
+
+        # add physical group to gmsh
+        self.gmsh_io.add_physical_group(name, ndim_boundary, geometry_ids)
+
+        # create model part
+        model_part = ModelPart(name)
+
+        # retrieve geometry from gmsh and add to model part
+        model_part.get_geometry_from_geo_data(self.gmsh_io.geo_data, name)
+
+        # add boundary parameters to model part
+        model_part.parameters = boundary_parameters
+
+        self.process_model_parts.append(model_part)
+
+    def synchronise_geometry(self):
+        """
+        Synchronise the geometry of all model parts and synchronise the geometry of the whole model. This function
+        recalculates all ids and connectivities of all geometrical entities.
+
+        """
+
+        # synchronize gmsh and extract geo data
+        self.gmsh_io.synchronize_gmsh()
+        self.gmsh_io.extract_geo_data()
+
+        # collect all model parts
+        all_model_parts: List[Union[BodyModelPart, ModelPart]] = []
+        all_model_parts.extend(self.body_model_parts)
+        all_model_parts.extend(self.process_model_parts)
+
+        # Get the geometry from the geo_data for each model part
+        for model_part in all_model_parts:
+            model_part.get_geometry_from_geo_data(self.gmsh_io.geo_data, model_part.name)
+
+        # get the complete geometry
+        self.__get_geometry_from_geo_data(self.gmsh_io.geo_data)
+
+    def set_mesh_size(self, element_size: float):
+        """
+        Set the element size to dimension [m].
+
+        Args:
+            - element_size (float): the desired element size [m].
+        """
+        self.mesh_settings.element_size = element_size
+
+    def generate_mesh(self):
+        """
+        Generate the mesh for the whole model.
+
+        """
+
+        # generate mesh
+        self.gmsh_io.generate_mesh(self.ndim, element_size=self.mesh_settings.element_size,
+                                   order=self.mesh_settings.element_order)
+
+        # collect all model parts
+        all_model_parts: List[Union[BodyModelPart, ModelPart]] = []
+        all_model_parts.extend(self.body_model_parts)
+        all_model_parts.extend(self.process_model_parts)
+
+        # add the mesh to each model part
+        for model_part in all_model_parts:
+            model_part.mesh = Mesh.create_mesh_from_gmsh_group(self.gmsh_io.mesh_data, model_part.name)
+
+    def __validate_model_part_names(self):
+        """
+        Checks if all model parts have a unique name.
+
+        Raises:
+            - ValueError: If not all model parts have a name.
+            - ValueError: If not all model part names are unique .
+        """
+
+        # collect all model parts
+        all_model_parts: List[Union[BodyModelPart, ModelPart]] = []
+        all_model_parts.extend(self.body_model_parts)
+        all_model_parts.extend(self.process_model_parts)
+
+        unique_names = []
+        for model_part in all_model_parts:
+            # Check if all model parts have a name
+            if model_part.name is None:
+                raise ValueError("All model parts must have a name")
+            else:
+                if model_part.name in unique_names:
+                    raise ValueError("All model parts must have a unique name")
+                unique_names.append(model_part.name)
+
+    def __add_gravity_model_part(self, gravity_load: GravityLoad, ndim: int, geometry_ids: Sequence[int]):
+        """
+        Add a gravity model part to the complete model.
+
+        Args:
+            - gravity_load (GravityLoad): The gravity load object.
+            - ndim (int): The number of dimensions of the on which the gravity load should be applied.
+            - geometry_ids (Sequence[int]): The geometry on which the gravity load should be applied.
+
+        """
+
+        # set new model part name
+        model_part_name = f"gravity_load_{ndim}d"
+
+        # create new gravity physical group and model part
+        self.gmsh_io.add_physical_group(model_part_name, ndim, geometry_ids)
+        model_part = ModelPart(model_part_name)
+
+        model_part.parameters = gravity_load
+
+        # add gravity load to process model parts
+        self.process_model_parts.append(model_part)
+
+    def __add_gravity_load(self, gravity_acceleration: float = -9.81, vertical_axis: int = 1):
+        """
+        Add a gravity load to the complete model.
+
+        Args:
+            - gravity_acceleration  (float): The gravity acceleration [m/s^2]. (default -9.81)
+            - vertical_axis (int): The vertical axis of the model. x=>0, y=>1, z=>2. (default y, 1)
+
+        """
+
+        # set gravity load at vertical axis
+        gravity_load_values: List[float] = [0, 0, 0]
+        gravity_load_values[vertical_axis] = gravity_acceleration
+        gravity_load = GravityLoad(value=gravity_load_values, active=[True, True, True])
+
+        # get all body model part names
+        body_model_part_names = [body_model_part.name for body_model_part in self.body_model_parts]
+
+        # get geometry ids and ndim for each body model part
+        model_parts_geometry_ids = np.array([self.gmsh_io.geo_data["physical_groups"][name]["geometry_ids"] for name in
+                                    body_model_part_names])
+
+        model_parts_ndim = np.array([self.gmsh_io.geo_data["physical_groups"][name]["ndim"]
+                                     for name in body_model_part_names]).ravel()
+
+        # add gravity load as physical group per dimension
+        body_geometries_1d = model_parts_geometry_ids[model_parts_ndim == 1].ravel()
+        if len(body_geometries_1d) > 0:
+            self.__add_gravity_model_part(gravity_load, 1, body_geometries_1d)
+
+        body_geometries_2d = model_parts_geometry_ids[model_parts_ndim == 2].ravel()
+        if len(body_geometries_2d) > 0:
+            self.__add_gravity_model_part(gravity_load, 2, body_geometries_2d)
+
+        body_geometries_3d = model_parts_geometry_ids[model_parts_ndim == 3].ravel()
+        if len(body_geometries_3d) > 0:
+            self.__add_gravity_model_part(gravity_load, 3, body_geometries_3d)
+
+        self.synchronise_geometry()
+
+    def get_all_model_parts(self) -> List[Union[BodyModelPart, ModelPart]]:
+        """
+        Returns both body and process model parts in the model.
+
+        Returns:
+            - all_model_parts (List[:class:`stem.model_part.ModelPart`]): list of all the model parts.
+        """
+        all_model_parts = []
+        all_model_parts.extend(self.process_model_parts)
+        all_model_parts.extend(self.body_model_parts)
+        return all_model_parts
+
+    def get_all_nodes(self):
+        """
+        Retrieve all the unique nodes in the model mesh.
+
+        Raises:
+            - ValueError: If the geometry has not been meshed yet.
+
+        Returns:
+            - node_dict (Dict[int, :class:`stem.mesh.Node`]): dictionary containing nodes id and nodes objects.
+        """
+
+        node_dict: Dict[int, Node] = {}
+        for mp in self.get_all_model_parts():
+            if mp.mesh is None:
+                raise ValueError('Geometry has not been meshed yet! Please first run the Model.generate_mesh method.')
+            for node in mp.mesh.nodes:
+                if not node.id in node_dict.keys():
+                    node_dict[node.id] = node
+
+        return node_dict
+
+    def validate(self):
+        """
+        Validate the model. \
+            - Checks if all model parts have a unique name.
+
+        """
+
+        self.__validate_model_part_names()
+
+    def show_geometry(self, show_volume_ids: bool = False, show_surface_ids: bool = False, show_line_ids: bool = False,
+                      show_point_ids: bool = False):
+        """
+        Show the 2D or 3D geometry in a plot.
+
+        Args:
+            - show_volume_ids (bool): Show the volume ids in the plot. (default False)
+            - show_surface_ids (bool): Show the surface ids in the plot. (default False)
+            - show_line_ids (bool): Show the line ids in the plot. (default False)
+            - show_point_ids (bool): Show the point ids in the plot. (default False)
+
+        Raises:
+            - ValueError: If the geometry is not set.
+
+        """
+        if self.geometry is None:
+            raise ValueError("Geometry must be set before showing the geometry")
+
+        fig = PlotUtils.create_geometry_figure(self.ndim, self.geometry, show_volume_ids, show_surface_ids, show_line_ids,
+                                               show_point_ids)
+        fig.show()
+
+    def __setup_stress_initialisation(self):
+        """
+        Set up the stress initialisation. For K0 procedure and gravity loading, a gravity load is added to the model.
+
+        Raises:
+            - ValueError: If the project parameters are not set.
+
+        """
+
+        if self.project_parameters is None:
+            raise ValueError("Project parameters must be set before setting up the stress initialisation")
+
+        # add gravity load if K0 procedure or gravity loading is used
+        if (self.project_parameters.settings.stress_initialisation_type ==
+            StressInitialisationType.K0_PROCEDURE) or \
+                (self.project_parameters.settings.stress_initialisation_type ==
+                 StressInitialisationType.GRAVITY_LOADING):
+
+            self.__add_gravity_load()
+
+    def post_setup(self):
+        """
+        Post setup of the model.
+            - Synchronise the geometry.
+            - Generate the mesh.
+            - Validate the model.
+            - Set up the stress initialisation.
+
+        """
+
+        self.synchronise_geometry()
+        self.generate_mesh()
+        self.validate()
 
+        self.__setup_stress_initialisation()
diff --git a/stem/model_part.py b/stem/model_part.py
index 8ba1d540..ec45b656 100644
--- a/stem/model_part.py
+++ b/stem/model_part.py
@@ -1,9 +1,15 @@
 from typing import Optional, Union, Dict, Any
 
+from stem.additional_processes import AdditionalProcessesParametersABC
+from stem.boundary import BoundaryParametersABC
+from stem.load import LoadParametersABC
 from stem.soil_material import SoilMaterial
 from stem.structural_material import StructuralMaterial
 
-from stem.geometry import Geometry, Volume, Surface, Line, Point
+from stem.geometry import Geometry
+from stem.mesh import Mesh
+from stem.solver import AnalysisType
+
 
 class ModelPart:
     """
@@ -11,21 +17,40 @@ class ModelPart:
     like excavation.
 
     Attributes:
-        - name (str): name of the model part
-        - nodes (np.array or None): node id followed by node coordinates in an array
-        - elements (np.array or None): element id followed by connectivities in an array
-        - conditions (np.array or None): condition id followed by connectivities in an array
+        - __name (str): name of the model part
         - geometry (Optional[:class:`stem.geometry.Geometry`]): geometry of the model part
-        - parameters (dict): dictionary containing the model part parameters
+        - parameters (Optional[Union[:class:`stem.load.LoadParametersABC`, \
+            :class:`stem.boundary.BoundaryParametersABC`, \
+            :class:`stem.additional_processes.AdditionalProcessesParametersABC`]]): process parameters containing the \
+            model part parameters.
+        - mesh (Optional[:class:`stem.mesh.Mesh`]): mesh of the model part
+        - id (Optional[int]): the id of the model part
     """
-    def __init__(self):
-        self.name = None
-        self.nodes = None
-        self.elements = None
-        self.conditions = None
+    def __init__(self, name: str):
+        """
+        Initialize the model part
 
+        Args:
+            - name (str): name of the model part
+        """
+        self.__name: str = name
         self.geometry: Optional[Geometry] = None
-        self.parameters = {}
+        self.parameters: Optional[
+            Union[LoadParametersABC, BoundaryParametersABC, AdditionalProcessesParametersABC]
+        ] = None
+        self.mesh: Optional[Mesh] = None
+        self.id: Optional[int] = None
+
+    @property
+    def name(self) -> str:
+        """
+        Get the name of the model part
+
+        Returns:
+            - str: name of the model part
+
+        """
+        return self.__name
 
     def get_geometry_from_geo_data(self, geo_data: Dict[str, Any], name: str):
         """
@@ -38,25 +63,68 @@ def get_geometry_from_geo_data(self, geo_data: Dict[str, Any], name: str):
 
         self.geometry = Geometry.create_geometry_from_gmsh_group(geo_data, name)
 
+    def get_element_name(self, n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Get the element name of the model part. Only loads and boundary conditions currently may have an element name.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model (2 or 3)
+            - n_nodes_element (int): The number of nodes per element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type of the model
+
+        Returns:
+            - Optional[str]: element name of the model part
+
+        """
+
+        if isinstance(self.parameters, (LoadParametersABC, BoundaryParametersABC)):
+            return self.parameters.get_element_name(n_dim_model, n_nodes_element, analysis_type)
+        else:
+            return None
+
 
 class BodyModelPart(ModelPart):
     """
     This class contains model parts which are part of the body, e.g. a soil layer or track components.
 
-        Inheritance:
+    Inheritance:
         - :class:`ModelPart`
 
     Attributes:
-        - name (str): name of the model part
-        - nodes (np.array or None): node id followed by node coordinates in an array
-        - elements (np.array or None): element id followed by connectivities in an array
-        - conditions (np.array or None): condition id followed by connectivities in an array
-        - parameters (dict): dictionary containing the model part parameters
+        - __name (str): name of the model part
+        - geometry (Optional[:class:`stem.geometry.Geometry`]): geometry of the model part
+        - mesh (Optional[:class:`stem.mesh.Mesh`]): mesh of the model part
+        - parameters (Dict[str, Any]): dictionary containing the model part parameters
         - material (Union[:class:`stem.soil_material.SoilMaterial`, \
             :class:`stem.structural_material.StructuralMaterial`]): material of the model part
     """
 
-    def __init__(self):
-        super().__init__()
+    def __init__(self, name: str):
+        """
+        Initialize the body model part
+
+        Args:
+            - name (str): name of the body model part
+        """
+        super().__init__(name)
 
         self.material: Optional[Union[SoilMaterial, StructuralMaterial]] = None
+
+    def get_element_name(self, n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Get the element name of the elements within the model part
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model (2 or 3)
+            - n_nodes_element (int): The number of nodes per element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type of the model
+
+        Returns:
+            - Optional[str]: element name of the model part
+
+        """
+
+        if self.material is not None:
+            return self.material.get_element_name(n_dim_model, n_nodes_element, analysis_type)
+        else:
+            return None
diff --git a/stem/output.py b/stem/output.py
index 179aac64..1598d52a 100644
--- a/stem/output.py
+++ b/stem/output.py
@@ -115,14 +115,15 @@ class OutputParametersABC(ABC):
     Abstract class for the definition of user output parameters (GiD, VTK, json).
     """
 
-    pass
-
     @abstractmethod
     def validate(self):
         """
         Abstract method for validating user inputs
+
+        Raises:
+            - Exception: Abstract method for validate output parameters is called
         """
-        pass
+        raise Exception("Abstract method for validate output parameters is called")
 
 
 @dataclass
@@ -243,38 +244,53 @@ class Output:
     Class containing output information for postprocessing
 
     Attributes:
-        - part_name (str): name of the model part
-        - output_dir (str): Optional input. output directory for the relative or \
-              absolute path to the output file. The path will be created if it does \
-              not exist yet. If not specified, the files it corresponds to the working \
-              directory. \n
-              example1='test1' results in the test1 output folder relative to current folder as '.test1'\
-              example2='path1/path2/test2' saves the outputs in 'current_folder/path1/path2/test2' \
-              example3='C:/Documents/yourproject/test3' saves the outputs in 'C:/Documents/yourproject/test3'.
-        - output_name (str): Optional input. Name for the output file. This parameter is \
-              used by GiD and JSON outputs while is ignored in VTK. If the name is not \
-              given, the part_name is used instead.
-        - output_parameters (:class:`OutputParametersABC`): class containing the output parameters
+        - output_parameters (:class:`OutputParametersABC`): class containing output parameters
+        - part_name (Optional[str]): name of the model part
+        - output_dir (str): path to the output files
+        - output_name (Optional[str]): name for the output file
     """
 
     def __init__(
         self,
-        part_name: str,
         output_parameters: OutputParametersABC,
-        output_dir: str = "",
-        output_name: str = "",
+        part_name: Optional[str] = None,
+        output_dir: str = "./",
+        output_name: Optional[str] = None
     ):
         """
         Constructor of the output process class
 
         Args:
-            - part_name (str): name of the model part
-            - output_name (str): name for the output file
-            - output_dir (str): path to the output files
-            - output_parameters (:class:`OutputParametersABC`): class containing output parameters
+            - output_parameters (:class:`OutputParametersABC`): class containing the output parameters
+            - part_name (Optional[str]): name of the submodelpart to be given in output. If None, all the model is
+                provided in  output.
+            - output_dir (Optional[str]): output directory for the relative or absolute path to the output file. The \
+                path will be created if it does not exist yet. \n
+
+                example1='test1' results in the test1 output folder relative to current folder as './test1'\
+                example2='path1/path2/test2' saves the outputs in './path1/path2/test2' \
+                example3='C:/Documents/yourproject/test3' saves the outputs in 'C:/Documents/yourproject/test3'.
+
+                if output_dir is None, then the current directory is assumed.
+
+                [NOTE]: for VTK file type, the content of the target directory will be deleted. Therefore a subfolder is
+                always appended to the specified output directory to avoid erasing important memory content.
+                The appended folder is defined based on the submodelpart name specified.
+
+            - output_name (Optional[str]): Name for the output file. This parameter is \
+                  used by GiD and JSON outputs while is ignored in VTK. If the name is not \
+                  given, the part_name is used instead.
         """
 
-        self.output_name: str = output_name
-        self.part_name: str = part_name
-        self.output_dir: Path = Path(output_dir)
-        self.output_parameters: OutputParametersABC = output_parameters
+        new_output_dir = Path(output_dir)
+
+        if isinstance(output_parameters, VtkOutputParameters):
+            if part_name is None:
+                new_output_dir = new_output_dir.joinpath("output_vtk_full_model")
+            else:
+                new_output_dir = new_output_dir.joinpath("output_vtk_" + part_name)
+
+        self.output_parameters = output_parameters
+        self.part_name = part_name
+        self.output_dir = new_output_dir
+        self.output_name = output_name
diff --git a/stem/plot_utils.py b/stem/plot_utils.py
new file mode 100644
index 00000000..b9706c70
--- /dev/null
+++ b/stem/plot_utils.py
@@ -0,0 +1,294 @@
+import numpy as np
+import matplotlib.pyplot as plt
+from mpl_toolkits.mplot3d import Axes3D
+# Axes3D import has side effects, it enables using projection='3d' in add_subplot
+from mpl_toolkits.mplot3d.art3d import Poly3DCollection, PolyCollection
+
+# import required typing classes
+from typing import TYPE_CHECKING, List
+if TYPE_CHECKING:
+    from stem.geometry import Geometry, Volume, Surface
+
+
+class PlotUtils:
+
+    @staticmethod
+    def __add_2d_surface_to_plot(geometry: 'Geometry',  surface: 'Surface', show_surface_ids,
+                                 show_line_ids, show_point_ids, ax):
+        """
+        Adds a 2D surface to a plot
+
+        Args:
+            - geometry (stem.geometry.Geometry): geometry object
+            - surface (stem.geometry.Surface): surface object
+            - show_surface_ids (bool): flag to show surface ids
+            - show_line_ids (bool): flag to show line ids
+            - show_point_ids (bool): flag to show point ids
+            - ax (matplotlib.axes.Axes): axes object to which the surface is added
+
+        Returns:
+            - NDArray[float]: surface centroid
+
+        """
+        # initialize list of surface point ids
+        surface_point_ids: List[int] = []
+
+        # calculate centroids of lines to show line ids
+        line_centroids = []
+        for line_k in surface.line_ids:
+
+            # get current line
+            line = geometry.lines[abs(line_k)]
+
+            # copy line point ids as line_connectivities can be reversed
+            line_connectivities = np.copy(line.point_ids)
+
+            # reverse line connectivity if line is defined in opposite direction
+            if line_k < 0:
+                line_connectivities = line_connectivities[::-1]
+
+            # calculate line centroid
+            line_centroids.append(np.mean([geometry.points[line_connectivities[0]].coordinates,
+                                           geometry.points[line_connectivities[1]].coordinates], axis=0))
+
+            surface_point_ids.extend(line_connectivities)
+
+        # get unique points within surface
+        unique_points = []
+        for point_id in surface_point_ids:
+
+            if point_id not in unique_points:
+                unique_points.append(point_id)
+
+        # get coordinates of surface points
+        surface_point_coordinates = np.array([geometry.points[point_id].coordinates
+                                              for point_id in unique_points])
+
+        # set vertices in format as required by Poly3DCollection
+        vertices = [list(zip(surface_point_coordinates[:, 0],
+                             surface_point_coordinates[:, 1]))]
+
+        # create Poly3DCollection
+        poly = PolyCollection(vertices, facecolors='blue', linewidths=1, edgecolors='black', alpha=0.35)
+
+        # calculate surface centroid and add to list of all surface centroids which are required to calculate
+        # the volume centroid
+        surface_centroid = np.mean(surface_point_coordinates, axis=0)
+
+        # show surface ids
+        if show_surface_ids:
+            ax.text(surface_centroid[0], surface_centroid[1], f"s_{abs(surface.id)}",
+                    color='black', fontsize=14, fontweight='bold')
+
+        # show line ids
+        if show_line_ids:
+            for line_centroid, line_k in zip(line_centroids, surface.line_ids):
+                ax.text(line_centroid[0], line_centroid[1], f"l_{abs(line_k)}",
+                        color='black', fontsize=14, fontweight='bold')
+
+        # show point ids
+        if show_point_ids:
+            for point_id, point in geometry.points.items():
+                ax.text(point.coordinates[0], point.coordinates[1], f"p_{point_id}",
+                        color='black', fontsize=14, fontweight='bold')
+
+        # add PolyCollection to figure
+        ax.add_collection(poly)
+
+        return surface_centroid
+
+
+    @staticmethod
+    def __add_3d_surface_to_plot(geometry: 'Geometry', surface: 'Surface', show_surface_ids, show_line_ids,
+                                 show_point_ids, ax):
+        """
+        Adds a 3D surface to a plot.
+
+        Args:
+            - geometry (stem.geometry.Geometry): geometry object
+            - surface (stem.geometry.Surface): surface object
+            - show_surface_ids (bool): flag to show surface ids
+            - show_line_ids (bool): flag to show line ids
+            - show_point_ids (bool): flag to show point ids
+            - ax (mpl_toolkits.mplot3d.axes3d.Axes3D): axes object to which the surface is added
+
+        Returns:
+            - NDArray[float]: surface centroid
+
+        """
+        # initialize list of surface point ids
+        surface_point_ids: List[int] = []
+
+        # calculate centroids of lines to show line ids
+        line_centroids = []
+        for line_k in surface.line_ids:
+
+            # get current line
+            line = geometry.lines[abs(line_k)]
+
+            # copy line point ids as line_connectivities can be reversed
+            line_connectivities = np.copy(line.point_ids)
+
+            # reverse line connectivity if line is defined in opposite direction
+            if line_k < 0:
+                line_connectivities = line_connectivities[::-1]
+
+            # calculate line centroid
+            line_centroids.append(np.mean([geometry.points[line_connectivities[0]].coordinates,
+                                           geometry.points[line_connectivities[1]].coordinates], axis=0))
+
+            surface_point_ids.extend(line_connectivities)
+
+        # get unique points within surface
+        unique_points = []
+        for point_id in surface_point_ids:
+
+            if point_id not in unique_points:
+                unique_points.append(point_id)
+
+        # get coordinates of surface points
+        surface_point_coordinates = np.array([geometry.points[point_id].coordinates
+                                              for point_id in unique_points])
+
+        # set vertices in format as required by Poly3DCollection
+        vertices = [list(zip(surface_point_coordinates[:, 0],
+                             surface_point_coordinates[:, 1],
+                             surface_point_coordinates[:, 2]))]
+
+        # create Poly3DCollection
+        poly = Poly3DCollection(vertices, facecolors='blue', linewidths=1, edgecolors='black', alpha=0.35)
+
+        # calculate surface centroid and add to list of all surface centroids which are required to calculate
+        # the volume centroid
+        surface_centroid = np.mean(surface_point_coordinates, axis=0)
+
+        # show surface ids
+        if show_surface_ids:
+            ax.text(surface_centroid[0], surface_centroid[1], surface_centroid[2], f"s_{abs(surface.id)}",
+                    color='black', fontsize=14, fontweight='bold')
+
+        # show line ids
+        if show_line_ids:
+            for line_centroid, line_k in zip(line_centroids, surface.line_ids):
+                ax.text(line_centroid[0], line_centroid[1], line_centroid[2], f"l_{abs(line_k)}",
+                        color='black', fontsize=14, fontweight='bold')
+
+        # show point ids
+        if show_point_ids:
+            for point_id, point in geometry.points.items():
+                ax.text(point.coordinates[0], point.coordinates[1], point.coordinates[2], f"p_{point_id}",
+                        color='black', fontsize=14, fontweight='bold')
+
+        # add Poly3DCollection to figure
+        ax.add_collection3d(poly)
+
+        return surface_centroid
+
+
+    @staticmethod
+    def __add_3d_volume_to_plot(geometry: 'Geometry', volume: 'Volume', show_volume_ids, show_surface_ids,
+                                show_line_ids, show_point_ids, ax):
+        """
+        Adds a 3D volume to a matplotlib figure.
+
+        Args:
+            - geometry (:class:`stem.geometry.Geometry`): Geometry object
+            - volume (:class:`stem.geometry.Volume`): Volume object
+            - show_volume_ids (bool): Show volume ids
+            - show_surface_ids (bool): Show surface ids
+            - show_line_ids (bool): Show line ids
+            - show_point_ids (bool): Show point ids
+            - ax (mpl_toolkits.mplot3d.axes3d.Axes3D): Axes object to which the volume is added
+
+        """
+        # initialize list of surface centroids which are required to plot the surface ids
+        all_surface_centroids = []
+
+        # loop over all surfaces within the volume
+        for surface_k in volume.surface_ids:
+            # get current surface
+            surface = geometry.surfaces[abs(surface_k)]
+
+            surface_centroid = PlotUtils.__add_3d_surface_to_plot(geometry, surface, show_surface_ids, show_line_ids,
+                                                             show_point_ids, ax)
+            all_surface_centroids.append(surface_centroid)
+
+        # show volume ids
+        if show_volume_ids:
+            volume_centroid = np.mean(all_surface_centroids, axis=0)
+            ax.text(volume_centroid[0], volume_centroid[1], volume_centroid[2], f"v_{volume.id}",
+                    color='black', fontsize=14, fontweight='bold')
+
+    @staticmethod
+    def create_geometry_figure(ndim: int, geometry: 'Geometry', show_volume_ids: bool = False, show_surface_ids: bool = False,
+                               show_line_ids: bool = False, show_point_ids: bool = False) -> plt.Figure:
+        """
+        Creates the geometry of the model in a matplotlib plot.
+
+        Args:
+            - ndim (int): Number of dimensions of the geometry. Either 2 or 3.
+            - geometry (:class:`stem.geometry.Geometry`): Geometry object.
+            - show_volume_ids (bool): If True, the volume ids are shown in the plot.
+            - show_surface_ids (bool): If True, the surface ids are shown in the plot.
+            - show_line_ids (bool): If True, the line ids are shown in the plot.
+            - show_point_ids (bool): If True, the point ids are shown in the plot.
+
+        Returns:
+            - plt.Figure: Figure object
+
+        """
+        # Initialize figure in 3D
+        fig = plt.figure()
+
+        if ndim == 2:
+            ax = fig.add_subplot(111)
+            for surface in geometry.surfaces.values():
+                PlotUtils.__add_2d_surface_to_plot(geometry, surface, show_surface_ids, show_line_ids,
+                                                   show_point_ids, ax)
+
+        elif ndim == 3:
+            ax = fig.add_subplot(111, projection='3d')
+            # loop over all volumes
+            for volume_data in geometry.volumes.values():
+                PlotUtils.__add_3d_volume_to_plot(geometry, volume_data, show_volume_ids, show_surface_ids,
+                                                  show_line_ids, show_point_ids, ax)
+        else:
+            raise ValueError("Number of dimensions should be 2 or 3")
+
+        # set limits of plot
+        # extend limits with buffer, which is 10% of the difference between min and max
+        buffer = 0.1
+        all_coordinates = np.array([point.coordinates for point in geometry.points.values()])
+
+        # calculate and set x and y limits
+        min_x, max_x = np.min(all_coordinates[:, 0]), np.max(all_coordinates[:, 0])
+        dx = max_x - min_x
+        min_y, max_y = np.min(all_coordinates[:, 1]), np.max(all_coordinates[:, 1])
+        dy = max_y - min_y
+
+        xlim = [min_x - buffer * dx, max_x + buffer * dx]
+        ylim = [min_y - buffer * dy, max_y + buffer * dy]
+
+        ax.set_xlim(xlim)
+        ax.set_ylim(ylim)
+
+        # set x and y labels
+        ax.set_xlabel("x coordinates [m]")
+        ax.set_ylabel("y coordinates [m]")
+
+        if ndim == 3:
+            # calculate and set z limits
+            min_z, max_z = np.min(all_coordinates[:, 2]), np.max(all_coordinates[:, 2])
+            dz = max_z - min_z
+
+            zlim = [min_z - buffer * dz, max_z + buffer * dz]
+
+            ax.set_zlim(zlim)
+
+            # set z label
+            ax.set_zlabel("z coordinates [m]")
+
+        # set equal aspect ratio to equal axes
+        ax.set_aspect('equal')
+
+        return fig
diff --git a/stem/soil_material.py b/stem/soil_material.py
index 9a4c999c..7c865b83 100644
--- a/stem/soil_material.py
+++ b/stem/soil_material.py
@@ -2,6 +2,9 @@
 from dataclasses import dataclass, field
 from abc import ABC
 
+from stem.solver import AnalysisType
+from stem.utils import Utils
+
 
 @dataclass
 class SoilFormulationParametersABC(ABC):
@@ -103,6 +106,9 @@ class TwoPhaseSoil(SoilFormulationParametersABC):
     def __post_init__(self):
         """
         Check if the permeability parameters are defined for the correct number of dimensions
+
+        Raises:
+            - ValueError: If the permeability parameters are not defined for the correct number of dimensions
         """
         if self.ndim == 2:
             # set permeability_yz and permeability_zx to None as they are not used in 2D
@@ -246,3 +252,41 @@ class SoilMaterial:
     constitutive_law: SoilConstitutiveLawABC
     retention_parameters: RetentionLawABC
     fluid_properties: FluidProperties = field(default_factory=FluidProperties)
+
+    @staticmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> str:
+        """
+        Function to get the element name based on the number of dimensions, the number of nodes and the analysis type.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model.
+            - n_nodes_element (int): The number of nodes per element.
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type.
+
+        Raises:
+            - ValueError: If the analysis type is not implemented yet for nodal concentrated elements.
+
+        Returns:
+            - element_name (str): The name of the element.
+
+        """
+
+        available_node_dim_combinations = {
+            2: [3, 4, 6, 8],
+            3: [4, 8, 10, 20],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, n_nodes_element, available_node_dim_combinations,
+                                             "Soil")
+
+        if analysis_type == AnalysisType.MECHANICAL_GROUNDWATER_FLOW or analysis_type == AnalysisType.MECHANICAL:
+
+            # for higher order elements, pore pressure is calculated on a lower order than displacements
+            if (n_dim_model == 2 and n_nodes_element > 4) or (n_dim_model == 3 and n_nodes_element > 8):
+                element_name = f"SmallStrainUPwDiffOrderElement{n_dim_model}D{n_nodes_element}N"
+            else:
+                element_name = f"UPwSmallStrainElement{n_dim_model}D{n_nodes_element}N"
+
+        else:
+            raise ValueError(f"Analysis type {analysis_type} is not implemented yet for soil material.")
+
+        return element_name
diff --git a/stem/solver.py b/stem/solver.py
index 075091a3..0d9f116d 100644
--- a/stem/solver.py
+++ b/stem/solver.py
@@ -380,6 +380,13 @@ class LinearSolverSettingsABC(ABC):
     @property
     @abc.abstractmethod
     def solver_type(self):
+        """
+        Abstract property for returning the solver type
+
+        Raises:
+            - Exception: abstract class of linear solver settings is called
+
+        """
         raise Exception("abstract class of linear solver settings is called")
 
 
@@ -395,12 +402,12 @@ class Amgcl(LinearSolverSettingsABC):
         - scaling (bool): if true, the system matrix will be scaled before solving the linear system of equations.\
             Default value is False.
         - tolerance (float): tolerance for the linear solver convergence criteria. Default value is 1e-6.
-        - max_iterations (int): maximum number of iterations for the linear solver. Default value is 1000.
+        - max_iteration (int): maximum number of iterations for the linear solver. Default value is 1000.
 
     """
     scaling: bool = False
     tolerance: float = 1e-6
-    max_iterations: int = 1000
+    max_iteration: int = 1000
 
     @property
     def solver_type(self):
diff --git a/stem/structural_material.py b/stem/structural_material.py
index f920d89b..30a2a0f8 100644
--- a/stem/structural_material.py
+++ b/stem/structural_material.py
@@ -1,6 +1,9 @@
 from typing import List, Any, Optional, Union
-from dataclasses import dataclass, field
-from abc import ABC
+from dataclasses import dataclass
+from abc import ABC, abstractmethod
+
+from stem.solver import AnalysisType
+from stem.utils import Utils
 
 @dataclass
 class StructuralParametersABC(ABC):
@@ -9,6 +12,22 @@ class StructuralParametersABC(ABC):
     """
     pass
 
+    @staticmethod
+    @abstractmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Abstract static method to get the element name for a structural material.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model
+            - n_nodes_element (int): The number of nodes per element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type.
+
+        Raises:
+            - Exception: abstract method is called
+        """
+        raise Exception("abstract method 'get_element_name' of structural parameters class is called")
+
 
 @dataclass
 class EulerBeam(StructuralParametersABC):
@@ -43,7 +62,9 @@ def __post_init__(self):
         """
         Check if the second moment of area about the y-axis and the torsional inertia are defined for 3D
 
-        Returns:
+        Raises:
+            - ValueError: If the second moment of area around the y-axis (I22) or the torsional inertia\
+                    (TORSIONAL_INERTIA) is not defined for 3D.
         """
         if self.ndim == 3:
             if self.I22 is None:
@@ -51,6 +72,38 @@ def __post_init__(self):
             if self.TORSIONAL_INERTIA is None:
                 raise ValueError("The torsional inertia (TORSIONAL_INERTIA) is not defined.")
 
+    @staticmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Static method to get the element name for an Euler beam element.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model
+            - n_nodes_element (int): The number of nodes per element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type.
+
+        Raises:
+            - ValueError: If the analysis type is not implemented yet for Euler beam elements.
+
+        Returns:
+            - Optional[str]: The element name
+
+        """
+
+        available_node_dim_combinations = {
+            2: [2],
+            3: [2],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, n_nodes_element, available_node_dim_combinations,
+                                             "Euler beam")
+
+        if analysis_type == AnalysisType.MECHANICAL_GROUNDWATER_FLOW or analysis_type == AnalysisType.MECHANICAL:
+            element_name = f"GeoCrBeamElement{n_dim_model}D{n_nodes_element}N"
+        else:
+            raise ValueError(f"Analysis type {analysis_type} is not implemented for euler beams.")
+
+        return element_name
+
 
 @dataclass
 class ElasticSpringDamper(StructuralParametersABC):
@@ -72,6 +125,37 @@ class ElasticSpringDamper(StructuralParametersABC):
     NODAL_DAMPING_COEFFICIENT: List[float]
     NODAL_ROTATIONAL_DAMPING_COEFFICIENT: List[float]
 
+    @staticmethod
+    def get_element_name(n_dim_model, n_nodes_element, analysis_type) -> Optional[str]:
+        """
+        Static method to get the element name for an elastic spring damper element.
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model
+            - n_nodes_element (int): The number of nodes per element
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type.
+
+        Raises:
+            - ValueError: If the analysis type is not implemented yet for elastic spring damper elements.
+
+        Returns:
+            - Optional[str]: The element name
+        """
+
+        available_node_dim_combinations = {
+            2: [2],
+            3: [2],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, n_nodes_element, available_node_dim_combinations,
+                                             "Elastic spring damper")
+
+        if analysis_type == AnalysisType.MECHANICAL_GROUNDWATER_FLOW or analysis_type == AnalysisType.MECHANICAL:
+            element_name = f"SpringDamperElement{n_dim_model}D"
+        else:
+            raise ValueError(f"Analysis type {analysis_type} is not implemented for elastic spring dampers.")
+
+        return element_name
+
 
 @dataclass
 class NodalConcentrated(StructuralParametersABC):
@@ -90,6 +174,38 @@ class NodalConcentrated(StructuralParametersABC):
     NODAL_MASS: float
     NODAL_DAMPING_COEFFICIENT: List[float]
 
+    @staticmethod
+    def get_element_name(n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Get the element name for the nodal concentrated element
+
+        Args:
+            - n_dim_model (int): The number of dimensions of the model (2 or 3)
+            - n_nodes_element (int): The number of nodes of the element (1)
+            - analysis_type (AnalysisType): The analysis type of the model
+
+        Raises:
+            - ValueError: If the analysis type is not implemented yet for nodal concentrated elements.
+
+        Returns:
+            - Optional[str]: The element name
+        """
+
+        available_node_dim_combinations = {
+            2: [1],
+            3: [1],
+        }
+        Utils.check_ndim_nnodes_combinations(n_dim_model, n_nodes_element, available_node_dim_combinations,
+                                             "Nodal concentrated")
+
+        if analysis_type == AnalysisType.MECHANICAL_GROUNDWATER_FLOW or analysis_type == AnalysisType.MECHANICAL:
+            element_name = f"NodalConcentratedElement{n_dim_model}D1N"
+        else:
+            raise ValueError(f"Analysis type {analysis_type} is not implemented for nodal concentrated elements.")
+
+        return element_name
+
+
 @dataclass
 class StructuralMaterial:
     """
@@ -101,3 +217,20 @@ class StructuralMaterial:
     """
     name: str
     material_parameters: StructuralParametersABC
+
+    def get_element_name(self, n_dim_model: int, n_nodes_element: int, analysis_type: AnalysisType) -> Optional[str]:
+        """
+        Get the element name for the structural material
+
+        Args:
+            - n_dim_model (int): The dimension of the model.
+            - n_nodes_element (int): The number of nodes per element.
+            - analysis_type (:class:`stem.solver.AnalysisType`): The analysis type.
+
+        Returns:
+            - Optional[str]: The element name.
+
+        """
+
+        return self.material_parameters.get_element_name(n_dim_model, n_nodes_element, analysis_type)
+
diff --git a/stem/table.py b/stem/table.py
new file mode 100644
index 00000000..bf659616
--- /dev/null
+++ b/stem/table.py
@@ -0,0 +1,60 @@
+from dataclasses import dataclass
+from typing import Optional, Union, Sequence
+import numpy.typing as npty
+import numpy as np
+
+
+@dataclass
+class Table:
+
+    """
+    Class to write time-dependent functions for imposed load and constraints.
+    If analysis runs outside the specified time-steps, the function is linearly extrapolated.
+    If load/constraint is required to remain constant, please specify the same load/constraint value for the last
+    two point of the sequence/array.
+
+    Attributes:
+        - values (Union[Sequence[float], npty.NDArray[np.float64]]): values of the load/constraint.
+        - times (Union[Sequence[Union[int,float]], npty.NDArray[Union[np.float64, np.int64]]]): time [s] \
+            corresponding to the values specified.
+        - __id (Optional[int]): unique identifier for the table.
+    """
+
+    values: Union[Sequence[Union[int, float]], npty.NDArray[Union[np.float64, np.int_]]]
+    times: Union[Sequence[float], npty.NDArray[np.float64]]
+    __id: Optional[int] = None
+
+    @property
+    def id(self) -> Optional[int]:
+        """
+        Getter for the id of the table.
+
+        Returns:
+            - Optional[int]: The id of the table.
+
+        """
+        return self.__id
+
+    @id.setter
+    def id(self, value: int):
+        """
+        Setter for the id of the table.
+
+        Args:
+            - value (int): The id of the table.
+
+        """
+        self.__id = value
+
+    def __post_init__(self):
+        """
+        Post-initialisation method to validate table attributes.
+
+        Raises:
+            - ValueError: if time and values have different number of elements.
+        """
+
+        if len(self.times) != len(self.values):
+            raise ValueError(f"Dimension mismatch between times and values in table:\n"
+                             f" - times: {len(self.times)}\n"
+                             f" - values: {len(self.values)}\n")
diff --git a/stem/utils.py b/stem/utils.py
new file mode 100644
index 00000000..44fefd39
--- /dev/null
+++ b/stem/utils.py
@@ -0,0 +1,222 @@
+from typing import Sequence, Dict, Any, List, Union, Optional, Generator
+
+import numpy as np
+
+
+class Utils:
+    """
+    Class containing utility methods.
+
+    """
+    @staticmethod
+    def check_ndim_nnodes_combinations(n_dim: int, n_nodes_element: Optional[int],
+                                       available_combinations: Dict[int, List[Any]],
+                                       class_name: str):
+        """
+        Check if the combination of number of global dimensions and number of nodes per element is supported.
+
+        Args:
+            - n_dim (int): number of dimensions
+            - n_nodes_element (int): number of nodes per element or condition-element
+            - available_combinations (Dict[int, List[int]]): dictionary containing the supported combinations of number\
+               of dimensions and number of nodes per element or condition-element
+            - class_name (str): name of the class to be checked
+
+        Raises:
+            - ValueError: when the number of dimensions is not supported.
+            - ValueError: when the combination of number of dimensions and number of nodes per element is not supported.
+
+        """
+        # check if the number of dimensions is supported
+        if n_dim not in available_combinations.keys():
+            raise ValueError(f"Number of dimensions {n_dim} is not supported for {class_name} elements. Supported "
+                             f"dimensions are {list(available_combinations.keys())}.")
+
+        # check if the number of nodes per element is supported
+        if n_nodes_element not in available_combinations[n_dim]:
+            raise ValueError(
+                f"In {n_dim} dimensions, only {available_combinations[n_dim]} noded {class_name} elements are "
+                f"supported. {n_nodes_element} nodes were provided."
+            )
+
+    @staticmethod
+    def are_2d_coordinates_clockwise(coordinates: Sequence[Sequence[float]]) -> bool:
+        """
+        Checks if the 2D coordinates are given in clockwise order. If the signed area is positive, the coordinates
+        are given in clockwise order.
+
+        Args:
+            - coordinates (Sequence[Sequence[float]]): coordinates of the points of a surface
+
+        Returns:
+            - bool: True if the coordinates are given in clockwise order, False otherwise.
+        """
+
+        # calculate signed area of polygon
+        signed_area = 0.0
+        for i in range(len(coordinates) - 1):
+            signed_area += (coordinates[i + 1][0] - coordinates[i][0]) * (coordinates[i + 1][1] + coordinates[i][1])
+
+        signed_area += (coordinates[0][0] - coordinates[-1][0]) * (coordinates[0][1] + coordinates[-1][1])
+
+        # if signed area is positive, the coordinates are given in clockwise order
+        return signed_area > 0.0
+
+    @staticmethod
+    def check_dimensions(points:Sequence[Sequence[float]]) -> None:
+        """
+
+        Check if points have the same dimensions (2D or 3D).
+
+        Args:
+            - points: (Sequence[Sequence[float]]): points to be tested
+
+        Raises:
+            - ValueError: when the points have different dimensions.
+            - ValueError: when the dimension is not either 2 or 3D.
+
+        Returns:
+            - None
+        """
+
+        lengths = [len(point) for point in points]
+        if len(np.unique(lengths)) != 1:
+            raise ValueError("Mismatch in dimension of given points!")
+
+        if any([ll not in [2, 3] for ll in lengths]):
+            raise ValueError("Dimension of the points should be 2D or 3D.")
+
+    @staticmethod
+    def is_collinear(point: Sequence[float], start_point: Sequence[float], end_point: Sequence[float],
+                     a_tol: float = 1e-06) -> bool:
+        """
+        Check if point is aligned with the other two on a line. Points must have the same dimension (2D or 3D)
+
+        Args:
+            - point (Sequence[float]): point coordinates to be tested
+            - start_point (Sequence[float]): coordinates of first point of a line
+            - end_point (Sequence[float]): coordinates of second point of a line
+            - a_tol (float): absolute tolerance to check collinearity (default 1e-6)
+
+        Raises:
+            - ValueError: when there is a dimension mismatch in the point dimensions.
+
+        Returns:
+            - bool: whether the point is aligned or not
+        """
+
+        # check dimensions of points for validation
+        Utils.check_dimensions([point, start_point, end_point])
+
+        vec_1 = np.asarray(point) - np.asarray(start_point)
+        vec_2 = np.asarray(end_point) - np.asarray(start_point)
+
+        # cross product of the two vector
+        cross_product = np.cross(vec_1, vec_2)
+
+        # It should be smaller than tolerance for points to be aligned
+        is_collinear: bool = np.sum(np.abs(cross_product)) < a_tol
+        return is_collinear
+
+    @staticmethod
+    def is_point_between_points(point:Sequence[float], start_point:Sequence[float], end_point:Sequence[float]) -> bool:
+        """
+        Check if point is between the other two. Points must have the same dimension (2D or 3D).
+
+        Args:
+            - point (Sequence[float]): point coordinates to be tested
+            - start_point (Sequence[float]): first extreme coordinates of the line
+            - end_point (Sequence[float]): second extreme coordinates of the line
+
+        Raises:
+            - ValueError: when there is a dimension mismatch in the point dimensions.
+
+        Returns:
+            - bool: whether the point is between the other two or not
+        """
+
+        # check dimensions of points for validation
+        Utils.check_dimensions([point, start_point, end_point])
+
+        # Calculate vectors between the points
+        vec_1 = np.asarray(point) - np.asarray(start_point)
+        vec_2 = np.asarray(end_point) - np.asarray(start_point)
+
+        # Calculate the scalar projections of vector1 onto vector2
+        scalar_projection = sum(v1 * v2 for v1, v2 in zip(vec_1, vec_2)) / sum(v ** 2 for v in vec_2)
+
+        # Check if the scalar projection is between 0 and 1 (inclusive)
+        is_between: bool = 0 <= scalar_projection <= 1
+        return is_between
+
+    @staticmethod
+    def is_non_str_sequence(seq: object) -> bool:
+        """
+        check whether object is a sequence but also not a string
+
+        Returns:
+            - bool: whether the sequence but also not a string
+        """
+        return isinstance(seq, Sequence) and not isinstance(seq, str)
+
+    @staticmethod
+    def chain_sequence(sequences: Sequence[Sequence[Any]]) -> Generator[Sequence[Any], Sequence[Any], None]:
+        """
+        Chains sequences together
+
+        Args:
+           - sequences (Sequence[Sequence[Any]]): sequences to chain
+
+        Returns:
+            - Generator[Sequence[Any], Sequence[Any], None]: generator for chaining sequences
+
+        """
+        for seq in sequences:
+            yield from seq
+
+    @staticmethod
+    def merge(a: Dict[Any, Any], b: Dict[Any, Any], path: Union[List[str], Any] = None) -> Dict[Any, Any]:
+        """
+        merges dictionary b into dictionary a. if existing keywords conflict it assumes
+        they are concatenated in a list
+
+        Args:
+            - a (Dict[Any,Any]): first dictionary
+            - b (Dict[Any,Any]): second dictionary
+            - path (List[str]): object to help navigate the deeper layers of the dictionary. \
+                Initially this has to be None
+
+        Returns:
+            - a (Dict[Any,Any]): updated dictionary with the additional dictionary `b`
+        """
+        if path is None:
+            path = []
+        for key in b:
+            if key in a:
+                if isinstance(a[key], dict) and isinstance(b[key], dict):
+                    Utils.merge(a[key], b[key], path + [str(key)])
+                elif a[key] == b[key]:
+                    pass  # same leaf value
+                elif any([not Utils.is_non_str_sequence(val) for val in (a[key], b[key])]):
+                    # if none of them is a sequence and are found at the same key, then something went wrong.
+                    # this should not be merged silently.
+                    raise ValueError(f"Conflict of merging keys at {'->'.join(path + [str(key)])}. Two non sequence "
+                                     f"values have been found.")
+                else:
+                    a[key] = list(Utils.chain_sequence([a[key], b[key]]))
+            else:
+                a[key] = b[key]
+        return a
+
+    @staticmethod
+    def get_unique_objects(input_sequence: Sequence[Any]) -> List[Any]:
+        """
+        Get the unique objects, i.e., the objects that share the same memory location.
+
+        Args:
+            - input_sequence (Sequence[Any]): full list of possible duplicate objects
+
+        Returns:
+            - List[Any]: list of unique objects
+        """
+        return list({id(obj): obj for obj in input_sequence}.values())


Test patch:
diff --git a/tests/test_data/expected_MaterialParameters.json b/tests/test_data/expected_MaterialParameters.json
new file mode 100644
index 00000000..b6dfd107
--- /dev/null
+++ b/tests/test_data/expected_MaterialParameters.json
@@ -0,0 +1,34 @@
+{
+    "properties": [
+        {
+            "model_part_name": "PorousDomain.soil1",
+            "properties_id": 1,
+            "Material": {
+                "constitutive_law": {
+                    "name": "GeoLinearElasticPlaneStrain2DLaw"
+                },
+                "Variables": {
+                    "YOUNG_MODULUS": 100000000.0,
+                    "POISSON_RATIO": 0.3,
+                    "DENSITY_SOLID": 2650,
+                    "POROSITY": 0.3,
+                    "BULK_MODULUS_SOLID": 50000000000.0,
+                    "IGNORE_UNDRAINED": true,
+                    "PERMEABILITY_XX": 1e-30,
+                    "PERMEABILITY_YY": 1e-30,
+                    "PERMEABILITY_ZZ": 1e-30,
+                    "PERMEABILITY_XY": 0,
+                    "PERMEABILITY_YZ": 0,
+                    "PERMEABILITY_ZX": 0,
+                    "RETENTION_LAW": "SaturatedBelowPhreaticLevelLaw",
+                    "SATURATED_SATURATION": 1.0,
+                    "RESIDUAL_SATURATION": 1e-10,
+                    "DYNAMIC_VISCOSITY": 0.0013,
+                    "BULK_MODULUS_FLUID": 2000000000.0,
+                    "DENSITY_WATER": 1000
+                }
+            },
+            "Tables": {}
+        }
+    ]
+}
\ No newline at end of file
diff --git a/tests/test_data/expected_ProjectParameters.json b/tests/test_data/expected_ProjectParameters.json
new file mode 100644
index 00000000..2b5d6404
--- /dev/null
+++ b/tests/test_data/expected_ProjectParameters.json
@@ -0,0 +1,244 @@
+{
+    "problem_data": {
+        "problem_name": "test",
+        "start_time": 0.0,
+        "end_time": 1.0,
+        "echo_level": 1,
+        "parallel_type": "OpenMP",
+        "number_of_threads": 2
+    },
+    "solver_settings": {
+        "solver_type": "U_Pw",
+        "model_part_name": "PorousDomain",
+        "domain_size": 2,
+        "start_time": 0.0,
+        "model_import_settings": {
+            "input_type": "mdpa",
+            "input_filename": "test_mdpa_file"
+        },
+        "material_import_settings": {
+            "materials_filename": "MaterialParameters.json"
+        },
+        "time_stepping": {
+            "time_step": 0.1,
+            "max_delta_time_factor": 500
+        },
+        "reduction_factor": 0.5,
+        "increase_factor": 2.0,
+        "buffer_size": 2,
+        "echo_level": 1,
+        "clear_storage": false,
+        "compute_reactions": false,
+        "move_mesh_flag": false,
+        "reform_dofs_at_each_step": false,
+        "nodal_smoothing": true,
+        "block_builder": true,
+        "rebuild_level": 0,
+        "prebuild_dynamics": true,
+        "solution_type": "dynamic",
+        "rayleigh_m": 0.001,
+        "rayleigh_k": 0.001,
+        "calculate_reactions": true,
+        "rotation_dofs": true,
+        "reset_displacements": false,
+        "scheme_type": "newmark",
+        "newmark_beta": 0.35,
+        "newmark_gamma": 0.4,
+        "newmark_theta": 0.6,
+        "strategy_type": "newton_raphson",
+        "max_iterations": 30,
+        "min_iterations": 5,
+        "number_cycles": 50,
+        "convergence_criterion": "displacement_criterion",
+        "displacement_relative_tolerance": 1e-05,
+        "displacement_absolute_tolerance": 1e-07,
+        "linear_solver_settings": {
+            "solver_type": "amgcl",
+            "scaling": false,
+            "tolerance": 1e-08,
+            "max_iteration": 500
+        },
+        "problem_domain_sub_model_part_list": [
+            "soil1"
+        ],
+        "processes_sub_model_part_list": [
+            "load_top",
+            "load_bottom",
+            "load_left",
+            "load_right",
+            "no_displacement"
+        ],
+        "body_domain_sub_model_part_list": [
+            "soil1"
+        ]
+    },
+    "output_processes": {
+        "gid_output": [
+            {
+                "python_module": "gid_output_process",
+                "kratos_module": "KratosMultiphysics",
+                "process_name": "GiDOutputProcess",
+                "Parameters": {
+                    "model_part_name": "PorousDomain.soil1",
+                    "output_name": "dir_test/gid_output_soil1",
+                    "postprocess_parameters": {
+                        "result_file_configuration": {
+                            "gidpost_flags": {
+                                "WriteDeformedMeshFlag": "WriteUndeformed",
+                                "WriteConditionsFlag": "WriteElementsOnly",
+                                "GiDPostMode": "GiD_PostBinary",
+                                "MultiFileFlag": "SingleFile"
+                            },
+                            "file_label": "step",
+                            "output_control_type": "step",
+                            "output_interval": 100,
+                            "body_output": true,
+                            "node_output": false,
+                            "skin_output": false,
+                            "plane_output": [],
+                            "nodal_results": [
+                                "DISPLACEMENT"
+                            ],
+                            "gauss_point_results": [
+                                "GREEN_LAGRANGE_STRAIN_TENSOR",
+                                "CAUCHY_STRESS_TENSOR"
+                            ]
+                        },
+                        "point_data_configuration": []
+                    }
+                }
+            }
+        ]
+    },
+    "processes": {
+        "constraints_process_list": [
+            {
+                "python_module": "apply_vector_constraint_table_process",
+                "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
+                "process_name": "ApplyVectorConstraintTableProcess",
+                "Parameters": {
+                    "active": [
+                        true,
+                        true,
+                        true
+                    ],
+                    "is_fixed": [
+                        true,
+                        true,
+                        true
+                    ],
+                    "value": [
+                        0,
+                        0,
+                        0
+                    ],
+                    "model_part_name": "PorousDomain.no_displacement",
+                    "variable_name": "DISPLACEMENT",
+                    "table": [
+                        0,
+                        0,
+                        0
+                    ]
+                }
+            }
+        ],
+        "loads_process_list": [
+            {
+                "python_module": "apply_vector_constraint_table_process",
+                "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
+                "process_name": "ApplyVectorConstraintTableProcess",
+                "Parameters": {
+                    "active": [
+                        false,
+                        true,
+                        false
+                    ],
+                    "value": [
+                        0,
+                        0,
+                        0
+                    ],
+                    "model_part_name": "PorousDomain.load_top",
+                    "variable_name": "LINE_LOAD",
+                    "table": [
+                        0,
+                        1,
+                        0
+                    ]
+                }
+            },
+            {
+                "python_module": "apply_vector_constraint_table_process",
+                "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
+                "process_name": "ApplyVectorConstraintTableProcess",
+                "Parameters": {
+                    "active": [
+                        false,
+                        true,
+                        false
+                    ],
+                    "value": [
+                        0,
+                        0,
+                        0
+                    ],
+                    "model_part_name": "PorousDomain.load_bottom",
+                    "variable_name": "LINE_LOAD",
+                    "table": [
+                        0,
+                        1,
+                        0
+                    ]
+                }
+            },
+            {
+                "python_module": "apply_vector_constraint_table_process",
+                "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
+                "process_name": "ApplyVectorConstraintTableProcess",
+                "Parameters": {
+                    "active": [
+                        true,
+                        false,
+                        false
+                    ],
+                    "value": [
+                        0,
+                        0,
+                        0
+                    ],
+                    "model_part_name": "PorousDomain.load_left",
+                    "variable_name": "LINE_LOAD",
+                    "table": [
+                        2,
+                        0,
+                        0
+                    ]
+                }
+            },
+            {
+                "python_module": "apply_vector_constraint_table_process",
+                "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
+                "process_name": "ApplyVectorConstraintTableProcess",
+                "Parameters": {
+                    "active": [
+                        true,
+                        false,
+                        false
+                    ],
+                    "value": [
+                        0,
+                        0,
+                        0
+                    ],
+                    "model_part_name": "PorousDomain.load_right",
+                    "variable_name": "LINE_LOAD",
+                    "table": [
+                        2,
+                        0,
+                        0
+                    ]
+                }
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/test_data/expected_boundary_conditions_parameters.json b/tests/test_data/expected_boundary_conditions_parameters.json
index 458cd8ae..ad7210fe 100644
--- a/tests/test_data/expected_boundary_conditions_parameters.json
+++ b/tests/test_data/expected_boundary_conditions_parameters.json
@@ -11,7 +11,7 @@
                     "active": [true, true, false],
                     "is_fixed": [true, true, false],
                     "value": [0.0, 0.0, 0.0],
-                    "table": [0, 0, 0]
+                    "table": [0, 1, 0]
                 }
             },
             {
@@ -24,7 +24,7 @@
                     "active": [false, false, true],
                     "is_fixed": [false, false, true],
                     "value": [0.0, 0.0, 0.0],
-                    "table": [0, 0, 0]
+                    "table": [2, 0, 0]
                 }
             }
         ],
diff --git a/tests/test_data/expected_default_materials.json b/tests/test_data/expected_default_materials.json
index 1b09f6cb..dca82c23 100644
--- a/tests/test_data/expected_default_materials.json
+++ b/tests/test_data/expected_default_materials.json
@@ -56,6 +56,57 @@
                 }
             },
             "Tables": {}
+        },
+                {
+            "model_part_name": "PorousDomain.default_elastic_rail_46E3_2D",
+            "properties_id": 4,
+            "Material": {
+                "constitutive_law": {
+                    "name": "KratosMultiphysics.StructuralMechanicsApplication.BeamConstitutiveLaw"
+                },
+                "Variables": {
+                    "YOUNG_MODULUS": 2.1e11,
+                    "POISSON_RATIO": 0.3,
+                    "DENSITY": 7850,
+                    "CROSS_AREA":0.005944,
+                    "I33":1.606e-05
+                }
+            },
+            "Tables": {}
+        },
+        {
+            "model_part_name": "PorousDomain.default_elastic_rail_54E1_2D",
+            "properties_id": 5,
+            "Material": {
+                "constitutive_law": {
+                    "name": "KratosMultiphysics.StructuralMechanicsApplication.BeamConstitutiveLaw"
+                },
+                "Variables": {
+                    "YOUNG_MODULUS": 2.1e11,
+                    "POISSON_RATIO": 0.3,
+                    "DENSITY": 7850,
+                    "CROSS_AREA":0.006977,
+                    "I33": 2.3372e-05
+                }
+            },
+            "Tables": {}
+        },
+        {
+            "model_part_name": "PorousDomain.default_elastic_rail_60E1_2D",
+            "properties_id": 6,
+            "Material": {
+                "constitutive_law": {
+                    "name": "KratosMultiphysics.StructuralMechanicsApplication.BeamConstitutiveLaw"
+                },
+                "Variables": {
+                    "YOUNG_MODULUS": 2.1e11,
+                    "POISSON_RATIO": 0.3,
+                    "DENSITY": 7850,
+                    "CROSS_AREA":0.00767,
+                    "I33": 3.038e-05
+                }
+            },
+            "Tables": {}
         }
     ]
 }
diff --git a/tests/test_data/expected_geometry_2D.eps b/tests/test_data/expected_geometry_2D.eps
new file mode 100644
index 00000000..ae729eb7
--- /dev/null
+++ b/tests/test_data/expected_geometry_2D.eps
@@ -0,0 +1,1341 @@
+%!PS-Adobe-3.0 EPSF-3.0
+%%Title: expected_geometry_2D.eps
+%%Creator: Matplotlib v3.7.1, https://matplotlib.org/
+%%CreationDate: Tue Aug 22 13:11:57 2023
+%%Orientation: portrait
+%%BoundingBox: 206 240 406 552
+%%HiResBoundingBox: 206.144125 240.572125 405.855875 551.427875
+%%EndComments
+%%BeginProlog
+/mpldict 12 dict def
+mpldict begin
+/_d { bind def } bind def
+/m { moveto } _d
+/l { lineto } _d
+/r { rlineto } _d
+/c { curveto } _d
+/cl { closepath } _d
+/ce { closepath eofill } _d
+/box {
+      m
+      1 index 0 r
+      0 exch r
+      neg 0 r
+      cl
+    } _d
+/clipbox {
+      box
+      clip
+      newpath
+    } _d
+/sc { setcachedevice } _d
+%!PS-Adobe-3.0 Resource-Font
+%%Creator: Converted from TrueType to Type 3 by Matplotlib.
+10 dict begin
+/FontName /DejaVuSans def
+/PaintType 0 def
+/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def
+/FontBBox [-2090 -948 3673 2524] def
+/FontType 3 def
+/Encoding [/space /period /zero /one /two /five /bracketleft /bracketright /a /c /d /e /i /m /n /o /r /s /t /x /y] def
+/CharStrings 22 dict dup begin
+/.notdef 0 def
+/space{651 0 0 0 0 0 sc
+ce} _d
+/period{651 0 219 0 430 254 sc
+219 254 m
+430 254 l
+430 0 l
+219 0 l
+219 254 l
+
+ce} _d
+/zero{1303 0 135 -29 1167 1520 sc
+651 1360 m
+547 1360 469 1309 416 1206 c
+364 1104 338 950 338 745 c
+338 540 364 387 416 284 c
+469 182 547 131 651 131 c
+756 131 834 182 886 284 c
+939 387 965 540 965 745 c
+965 950 939 1104 886 1206 c
+834 1309 756 1360 651 1360 c
+
+651 1520 m
+818 1520 946 1454 1034 1321 c
+1123 1189 1167 997 1167 745 c
+1167 494 1123 302 1034 169 c
+946 37 818 -29 651 -29 c
+484 -29 356 37 267 169 c
+179 302 135 494 135 745 c
+135 997 179 1189 267 1321 c
+356 1454 484 1520 651 1520 c
+
+ce} _d
+/one{1303 0 225 0 1114 1493 sc
+254 170 m
+584 170 l
+584 1309 l
+225 1237 l
+225 1421 l
+582 1493 l
+784 1493 l
+784 170 l
+1114 170 l
+1114 0 l
+254 0 l
+254 170 l
+
+ce} _d
+/two{1303 0 150 0 1098 1520 sc
+393 170 m
+1098 170 l
+1098 0 l
+150 0 l
+150 170 l
+227 249 331 356 463 489 c
+596 623 679 709 713 748 c
+778 821 823 882 848 932 c
+874 983 887 1032 887 1081 c
+887 1160 859 1225 803 1275 c
+748 1325 675 1350 586 1350 c
+523 1350 456 1339 385 1317 c
+315 1295 240 1262 160 1217 c
+160 1421 l
+241 1454 317 1478 388 1495 c
+459 1512 523 1520 582 1520 c
+737 1520 860 1481 952 1404 c
+1044 1327 1090 1223 1090 1094 c
+1090 1033 1078 974 1055 919 c
+1032 864 991 800 930 725 c
+913 706 860 650 771 557 c
+682 465 556 336 393 170 c
+
+ce} _d
+/five{1303 0 158 -29 1124 1493 sc
+221 1493 m
+1014 1493 l
+1014 1323 l
+406 1323 l
+406 957 l
+435 967 465 974 494 979 c
+523 984 553 987 582 987 c
+749 987 881 941 978 850 c
+1075 759 1124 635 1124 479 c
+1124 318 1074 193 974 104 c
+874 15 733 -29 551 -29 c
+488 -29 424 -24 359 -13 c
+294 -2 227 14 158 35 c
+158 238 l
+218 205 280 181 344 165 c
+408 149 476 141 547 141 c
+662 141 754 171 821 232 c
+888 293 922 375 922 479 c
+922 583 888 665 821 726 c
+754 787 662 817 547 817 c
+493 817 439 811 385 799 c
+332 787 277 768 221 743 c
+221 1493 l
+
+ce} _d
+/bracketleft{799 0 176 -270 600 1556 sc
+176 1556 m
+600 1556 l
+600 1413 l
+360 1413 l
+360 -127 l
+600 -127 l
+600 -270 l
+176 -270 l
+176 1556 l
+
+ce} _d
+/bracketright{799 0 199 -270 623 1556 sc
+623 1556 m
+623 -270 l
+199 -270 l
+199 -127 l
+438 -127 l
+438 1413 l
+199 1413 l
+199 1556 l
+623 1556 l
+
+ce} _d
+/a{1255 0 123 -29 1069 1147 sc
+702 563 m
+553 563 450 546 393 512 c
+336 478 307 420 307 338 c
+307 273 328 221 371 182 c
+414 144 473 125 547 125 c
+649 125 731 161 792 233 c
+854 306 885 402 885 522 c
+885 563 l
+702 563 l
+
+1069 639 m
+1069 0 l
+885 0 l
+885 170 l
+843 102 791 52 728 19 c
+665 -13 589 -29 498 -29 c
+383 -29 292 3 224 67 c
+157 132 123 218 123 326 c
+123 452 165 547 249 611 c
+334 675 460 707 627 707 c
+885 707 l
+885 725 l
+885 810 857 875 801 921 c
+746 968 668 991 567 991 c
+503 991 441 983 380 968 c
+319 953 261 930 205 899 c
+205 1069 l
+272 1095 338 1114 401 1127 c
+464 1140 526 1147 586 1147 c
+748 1147 869 1105 949 1021 c
+1029 937 1069 810 1069 639 c
+
+ce} _d
+/c{1126 0 113 -29 999 1147 sc
+999 1077 m
+999 905 l
+947 934 895 955 842 969 c
+790 984 737 991 684 991 c
+565 991 472 953 406 877 c
+340 802 307 696 307 559 c
+307 422 340 316 406 240 c
+472 165 565 127 684 127 c
+737 127 790 134 842 148 c
+895 163 947 184 999 213 c
+999 43 l
+948 19 894 1 839 -11 c
+784 -23 726 -29 664 -29 c
+495 -29 361 24 262 130 c
+163 236 113 379 113 559 c
+113 742 163 885 263 990 c
+364 1095 501 1147 676 1147 c
+733 1147 788 1141 842 1129 c
+896 1118 948 1100 999 1077 c
+
+ce} _d
+/d{1300 0 113 -29 1114 1556 sc
+930 950 m
+930 1556 l
+1114 1556 l
+1114 0 l
+930 0 l
+930 168 l
+891 101 842 52 783 19 c
+724 -13 654 -29 571 -29 c
+436 -29 325 25 240 133 c
+155 241 113 383 113 559 c
+113 735 155 877 240 985 c
+325 1093 436 1147 571 1147 c
+654 1147 724 1131 783 1098 c
+842 1066 891 1017 930 950 c
+
+303 559 m
+303 424 331 317 386 240 c
+442 163 519 125 616 125 c
+713 125 790 163 846 240 c
+902 317 930 424 930 559 c
+930 694 902 800 846 877 c
+790 954 713 993 616 993 c
+519 993 442 954 386 877 c
+331 800 303 694 303 559 c
+
+ce} _d
+/e{1260 0 113 -29 1151 1147 sc
+1151 606 m
+1151 516 l
+305 516 l
+313 389 351 293 419 226 c
+488 160 583 127 705 127 c
+776 127 844 136 910 153 c
+977 170 1043 196 1108 231 c
+1108 57 l
+1042 29 974 8 905 -7 c
+836 -22 765 -29 694 -29 c
+515 -29 374 23 269 127 c
+165 231 113 372 113 549 c
+113 732 162 878 261 985 c
+360 1093 494 1147 662 1147 c
+813 1147 932 1098 1019 1001 c
+1107 904 1151 773 1151 606 c
+
+967 660 m
+966 761 937 841 882 901 c
+827 961 755 991 664 991 c
+561 991 479 962 417 904 c
+356 846 320 764 311 659 c
+967 660 l
+
+ce} _d
+/i{569 0 193 0 377 1556 sc
+193 1120 m
+377 1120 l
+377 0 l
+193 0 l
+193 1120 l
+
+193 1556 m
+377 1556 l
+377 1323 l
+193 1323 l
+193 1556 l
+
+ce} _d
+/m{1995 0 186 0 1821 1147 sc
+1065 905 m
+1111 988 1166 1049 1230 1088 c
+1294 1127 1369 1147 1456 1147 c
+1573 1147 1663 1106 1726 1024 c
+1789 943 1821 827 1821 676 c
+1821 0 l
+1636 0 l
+1636 670 l
+1636 777 1617 857 1579 909 c
+1541 961 1483 987 1405 987 c
+1310 987 1234 955 1179 892 c
+1124 829 1096 742 1096 633 c
+1096 0 l
+911 0 l
+911 670 l
+911 778 892 858 854 909 c
+816 961 757 987 678 987 c
+584 987 509 955 454 891 c
+399 828 371 742 371 633 c
+371 0 l
+186 0 l
+186 1120 l
+371 1120 l
+371 946 l
+413 1015 463 1065 522 1098 c
+581 1131 650 1147 731 1147 c
+812 1147 881 1126 938 1085 c
+995 1044 1038 984 1065 905 c
+
+ce} _d
+/n{1298 0 186 0 1124 1147 sc
+1124 676 m
+1124 0 l
+940 0 l
+940 670 l
+940 776 919 855 878 908 c
+837 961 775 987 692 987 c
+593 987 514 955 457 892 c
+400 829 371 742 371 633 c
+371 0 l
+186 0 l
+186 1120 l
+371 1120 l
+371 946 l
+415 1013 467 1064 526 1097 c
+586 1130 655 1147 733 1147 c
+862 1147 959 1107 1025 1027 c
+1091 948 1124 831 1124 676 c
+
+ce} _d
+/o{1253 0 113 -29 1141 1147 sc
+627 991 m
+528 991 450 952 393 875 c
+336 798 307 693 307 559 c
+307 425 335 319 392 242 c
+449 165 528 127 627 127 c
+725 127 803 166 860 243 c
+917 320 946 426 946 559 c
+946 692 917 797 860 874 c
+803 952 725 991 627 991 c
+
+627 1147 m
+787 1147 913 1095 1004 991 c
+1095 887 1141 743 1141 559 c
+1141 376 1095 232 1004 127 c
+913 23 787 -29 627 -29 c
+466 -29 340 23 249 127 c
+158 232 113 376 113 559 c
+113 743 158 887 249 991 c
+340 1095 466 1147 627 1147 c
+
+ce} _d
+/r{842 0 186 0 842 1147 sc
+842 948 m
+821 960 799 969 774 974 c
+750 980 723 983 694 983 c
+590 983 510 949 454 881 c
+399 814 371 717 371 590 c
+371 0 l
+186 0 l
+186 1120 l
+371 1120 l
+371 946 l
+410 1014 460 1064 522 1097 c
+584 1130 659 1147 748 1147 c
+761 1147 775 1146 790 1144 c
+805 1143 822 1140 841 1137 c
+842 948 l
+
+ce} _d
+/s{1067 0 111 -29 967 1147 sc
+907 1087 m
+907 913 l
+855 940 801 960 745 973 c
+689 986 631 993 571 993 c
+480 993 411 979 365 951 c
+320 923 297 881 297 825 c
+297 782 313 749 346 724 c
+379 700 444 677 543 655 c
+606 641 l
+737 613 829 573 884 522 c
+939 471 967 400 967 309 c
+967 205 926 123 843 62 c
+761 1 648 -29 504 -29 c
+444 -29 381 -23 316 -11 c
+251 0 183 18 111 41 c
+111 231 l
+179 196 246 169 312 151 c
+378 134 443 125 508 125 c
+595 125 661 140 708 169 c
+755 199 778 241 778 295 c
+778 345 761 383 727 410 c
+694 437 620 462 506 487 c
+442 502 l
+328 526 246 563 195 612 c
+144 662 119 730 119 817 c
+119 922 156 1004 231 1061 c
+306 1118 412 1147 549 1147 c
+617 1147 681 1142 741 1132 c
+801 1122 856 1107 907 1087 c
+
+ce} _d
+/t{803 0 55 0 754 1438 sc
+375 1438 m
+375 1120 l
+754 1120 l
+754 977 l
+375 977 l
+375 369 l
+375 278 387 219 412 193 c
+437 167 488 154 565 154 c
+754 154 l
+754 0 l
+565 0 l
+423 0 325 26 271 79 c
+217 132 190 229 190 369 c
+190 977 l
+55 977 l
+55 1120 l
+190 1120 l
+190 1438 l
+375 1438 l
+
+ce} _d
+/x{1212 0 59 0 1145 1120 sc
+1124 1120 m
+719 575 l
+1145 0 l
+928 0 l
+602 440 l
+276 0 l
+59 0 l
+494 586 l
+96 1120 l
+313 1120 l
+610 721 l
+907 1120 l
+1124 1120 l
+
+ce} _d
+/y{1212 0 61 -426 1151 1120 sc
+659 -104 m
+607 -237 556 -324 507 -365 c
+458 -406 392 -426 309 -426 c
+162 -426 l
+162 -272 l
+270 -272 l
+321 -272 360 -260 388 -236 c
+416 -212 447 -155 481 -66 c
+514 18 l
+61 1120 l
+256 1120 l
+606 244 l
+956 1120 l
+1151 1120 l
+659 -104 l
+
+ce} _d
+end readonly def
+
+/BuildGlyph {
+ exch begin
+ CharStrings exch
+ 2 copy known not {pop /.notdef} if
+ true 3 1 roll get exec
+ end
+} _d
+
+/BuildChar {
+ 1 index /Encoding get exch get
+ 1 index /BuildGlyph get exec
+} _d
+
+FontName currentdict end definefont pop
+%!PS-Adobe-3.0 Resource-Font
+%%Creator: Converted from TrueType to Type 3 by Matplotlib.
+10 dict begin
+/FontName /DejaVuSans-Bold def
+/PaintType 0 def
+/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def
+/FontBBox [-2190 -850 4045 2405] def
+/FontType 3 def
+/Encoding [/l /p /one /two /s /three /four /five /six /seven /underscore] def
+/CharStrings 12 dict dup begin
+/.notdef 0 def
+/l{702 0 172 0 530 1556 sc
+172 1556 m
+530 1556 l
+530 0 l
+172 0 l
+172 1556 l
+
+ce} _d
+/p{1466 0 172 -426 1374 1147 sc
+530 162 m
+530 -426 l
+172 -426 l
+172 1120 l
+530 1120 l
+530 956 l
+579 1021 634 1069 694 1100 c
+754 1131 823 1147 901 1147 c
+1039 1147 1152 1092 1241 982 c
+1330 873 1374 732 1374 559 c
+1374 386 1330 245 1241 135 c
+1152 26 1039 -29 901 -29 c
+823 -29 754 -14 694 17 c
+634 48 579 97 530 162 c
+
+768 887 m
+691 887 632 859 591 802 c
+550 746 530 665 530 559 c
+530 453 550 372 591 315 c
+632 259 691 231 768 231 c
+845 231 903 259 943 315 c
+984 371 1004 452 1004 559 c
+1004 666 984 747 943 803 c
+903 859 845 887 768 887 c
+
+ce} _d
+/one{1425 0 231 0 1284 1493 sc
+240 266 m
+580 266 l
+580 1231 l
+231 1159 l
+231 1421 l
+578 1493 l
+944 1493 l
+944 266 l
+1284 266 l
+1284 0 l
+240 0 l
+240 266 l
+
+ce} _d
+/two{1425 0 162 0 1247 1520 sc
+590 283 m
+1247 283 l
+1247 0 l
+162 0 l
+162 283 l
+707 764 l
+756 808 792 851 815 893 c
+838 935 850 979 850 1024 c
+850 1094 826 1150 779 1193 c
+732 1236 670 1257 592 1257 c
+532 1257 466 1244 395 1218 c
+324 1193 247 1155 166 1104 c
+166 1432 l
+253 1461 338 1482 423 1497 c
+508 1512 591 1520 672 1520 c
+851 1520 989 1481 1088 1402 c
+1187 1323 1237 1214 1237 1073 c
+1237 992 1216 916 1174 845 c
+1132 775 1044 681 909 563 c
+590 283 l
+
+ce} _d
+/s{1219 0 106 -29 1122 1147 sc
+1047 1085 m
+1047 813 l
+970 845 896 869 825 885 c
+754 901 686 909 623 909 c
+555 909 504 900 471 883 c
+438 866 422 840 422 805 c
+422 776 434 754 459 739 c
+484 724 529 712 594 705 c
+657 696 l
+840 673 964 634 1027 581 c
+1090 528 1122 444 1122 330 c
+1122 211 1078 121 990 61 c
+902 1 771 -29 596 -29 c
+522 -29 445 -23 366 -11 c
+287 0 206 18 123 41 c
+123 313 l
+194 278 267 252 342 235 c
+417 218 494 209 571 209 c
+641 209 694 219 729 238 c
+764 257 782 286 782 324 c
+782 356 770 380 745 395 c
+721 411 673 423 600 432 c
+537 440 l
+378 460 266 497 202 551 c
+138 605 106 687 106 797 c
+106 916 147 1004 228 1061 c
+309 1118 434 1147 602 1147 c
+668 1147 737 1142 810 1132 c
+883 1122 962 1106 1047 1085 c
+
+ce} _d
+/three{1425 0 137 -29 1262 1520 sc
+954 805 m
+1055 779 1131 734 1183 669 c
+1236 605 1262 523 1262 424 c
+1262 276 1205 163 1092 86 c
+979 9 813 -29 596 -29 c
+519 -29 442 -23 365 -10 c
+288 2 212 20 137 45 c
+137 342 l
+209 306 280 279 351 260 c
+422 242 492 233 561 233 c
+663 233 741 251 795 286 c
+850 321 877 372 877 438 c
+877 506 849 557 793 592 c
+738 627 656 645 547 645 c
+393 645 l
+393 893 l
+555 893 l
+652 893 724 908 771 938 c
+818 969 842 1015 842 1077 c
+842 1134 819 1179 773 1210 c
+727 1241 662 1257 578 1257 c
+516 1257 453 1250 390 1236 c
+327 1222 264 1201 201 1174 c
+201 1456 l
+277 1477 352 1493 427 1504 c
+502 1515 575 1520 647 1520 c
+841 1520 986 1488 1082 1424 c
+1179 1361 1227 1265 1227 1137 c
+1227 1050 1204 978 1158 922 c
+1112 867 1044 828 954 805 c
+
+ce} _d
+/four{1425 0 92 0 1331 1493 sc
+754 1176 m
+332 551 l
+754 551 l
+754 1176 l
+
+690 1493 m
+1118 1493 l
+1118 551 l
+1331 551 l
+1331 272 l
+1118 272 l
+1118 0 l
+754 0 l
+754 272 l
+92 272 l
+92 602 l
+690 1493 l
+
+ce} _d
+/five{1425 0 158 -29 1282 1493 sc
+217 1493 m
+1174 1493 l
+1174 1210 l
+524 1210 l
+524 979 l
+553 987 583 993 612 997 c
+642 1002 673 1004 705 1004 c
+887 1004 1029 958 1130 867 c
+1231 776 1282 650 1282 487 c
+1282 326 1227 199 1116 108 c
+1006 17 853 -29 657 -29 c
+572 -29 488 -21 405 -4 c
+322 12 240 37 158 70 c
+158 373 l
+239 326 316 291 389 268 c
+462 245 531 233 596 233 c
+689 233 763 256 816 301 c
+870 347 897 409 897 487 c
+897 566 870 628 816 673 c
+763 718 689 741 596 741 c
+541 741 482 734 419 719 c
+356 705 289 683 217 653 c
+217 1493 l
+
+ce} _d
+/six{1425 0 127 -29 1315 1518 sc
+741 737 m
+674 737 623 715 589 671 c
+556 628 539 562 539 475 c
+539 388 556 322 589 278 c
+623 235 674 213 741 213 c
+809 213 860 235 893 278 c
+927 322 944 388 944 475 c
+944 562 927 628 893 671 c
+860 715 809 737 741 737 c
+
+1217 1454 m
+1217 1178 l
+1154 1208 1094 1230 1038 1244 c
+982 1259 927 1266 874 1266 c
+759 1266 670 1234 606 1170 c
+542 1107 505 1012 494 887 c
+538 920 586 944 637 960 c
+688 977 744 985 805 985 c
+958 985 1081 940 1174 851 c
+1268 762 1315 645 1315 500 c
+1315 340 1263 212 1158 115 c
+1053 19 913 -29 737 -29 c
+543 -29 393 36 286 167 c
+180 298 127 484 127 725 c
+127 972 189 1165 313 1306 c
+438 1447 608 1518 825 1518 c
+894 1518 960 1513 1025 1502 c
+1090 1491 1154 1475 1217 1454 c
+
+ce} _d
+/seven{1425 0 137 0 1262 1493 sc
+137 1493 m
+1262 1493 l
+1262 1276 l
+680 0 l
+305 0 l
+856 1210 l
+137 1210 l
+137 1493 l
+
+ce} _d
+/underscore{1024 0 0 -483 1024 -293 sc
+1024 -293 m
+1024 -483 l
+0 -483 l
+0 -293 l
+1024 -293 l
+
+ce} _d
+end readonly def
+
+/BuildGlyph {
+ exch begin
+ CharStrings exch
+ 2 copy known not {pop /.notdef} if
+ true 3 1 roll get exec
+ end
+} _d
+
+/BuildChar {
+ 1 index /Encoding get exch get
+ 1 index /BuildGlyph get exec
+} _d
+
+FontName currentdict end definefont pop
+end
+%%EndProlog
+mpldict begin
+206.144 240.572 translate
+199.712 310.856 0 0 clipbox
+gsave
+0 0 m
+199.71175 0 l
+199.71175 310.85575 l
+0 310.85575 l
+cl
+1.000 setgray
+fill
+grestore
+gsave
+43.778125 37.54375 m
+176.834125 37.54375 l
+176.834125 303.65575 l
+43.778125 303.65575 l
+cl
+1.000 setgray
+fill
+grestore
+1.000 setlinewidth
+1 setlinejoin
+0 setlinecap
+[] 0 setdash
+0.000 setgray
+gsave
+133.056 266.112 43.778 37.544 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+54.866125 59.71975 m
+165.746125 59.71975 l
+165.746125 170.59975 l
+54.866125 170.59975 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+133.056 266.112 43.778 37.544 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+165.746125 281.47975 m
+54.866125 281.47975 l
+54.866125 170.59975 l
+165.746125 170.59975 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+0.800 setlinewidth
+gsave
+/o {
+gsave
+newpath
+translate
+0.8 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+0 0 m
+0 -3.5 l
+
+gsave
+0.000 setgray
+fill
+grestore
+stroke
+grestore
+} bind def
+54.8661 37.5438 o
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+46.913 22.95 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+gsave
+/o {
+gsave
+newpath
+translate
+0.8 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+0 0 m
+0 -3.5 l
+
+gsave
+0.000 setgray
+fill
+grestore
+stroke
+grestore
+} bind def
+110.306 37.5438 o
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+102.353 22.95 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /five glyphshow
+grestore
+gsave
+/o {
+gsave
+newpath
+translate
+0.8 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+0 0 m
+0 -3.5 l
+
+gsave
+0.000 setgray
+fill
+grestore
+stroke
+grestore
+} bind def
+165.746 37.5438 o
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+157.793 22.95 translate
+0 rotate
+0 0 m /one glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+66.0639 9.27813 translate
+0 rotate
+0 0 m /x glyphshow
+5.91797 0 m /space glyphshow
+9.09668 0 m /c glyphshow
+14.5947 0 m /o glyphshow
+20.7129 0 m /o glyphshow
+26.8311 0 m /r glyphshow
+30.8174 0 m /d glyphshow
+37.165 0 m /i glyphshow
+39.9434 0 m /n glyphshow
+46.2812 0 m /a glyphshow
+52.4092 0 m /t glyphshow
+56.3301 0 m /e glyphshow
+62.4824 0 m /s glyphshow
+67.6924 0 m /space glyphshow
+70.8711 0 m /bracketleft glyphshow
+74.7725 0 m /m glyphshow
+84.5137 0 m /bracketright glyphshow
+grestore
+gsave
+/o {
+gsave
+newpath
+translate
+0.8 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+-0 0 m
+-3.5 0 l
+
+gsave
+0.000 setgray
+fill
+grestore
+stroke
+grestore
+} bind def
+43.7781 59.7198 o
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+20.8719 55.9229 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+gsave
+/o {
+gsave
+newpath
+translate
+0.8 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+-0 0 m
+-3.5 0 l
+
+gsave
+0.000 setgray
+fill
+grestore
+stroke
+grestore
+} bind def
+43.7781 115.16 o
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+20.8719 111.363 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /five glyphshow
+grestore
+gsave
+/o {
+gsave
+newpath
+translate
+0.8 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+-0 0 m
+-3.5 0 l
+
+gsave
+0.000 setgray
+fill
+grestore
+stroke
+grestore
+} bind def
+43.7781 170.6 o
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+20.8719 166.803 translate
+0 rotate
+0 0 m /one glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+gsave
+/o {
+gsave
+newpath
+translate
+0.8 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+-0 0 m
+-3.5 0 l
+
+gsave
+0.000 setgray
+fill
+grestore
+stroke
+grestore
+} bind def
+43.7781 226.04 o
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+20.8719 222.243 translate
+0 rotate
+0 0 m /one glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /five glyphshow
+grestore
+gsave
+/o {
+gsave
+newpath
+translate
+0.8 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+-0 0 m
+-3.5 0 l
+
+gsave
+0.000 setgray
+fill
+grestore
+stroke
+grestore
+} bind def
+43.7781 281.48 o
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+20.8719 277.683 translate
+0 rotate
+0 0 m /two glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+14.7938 126.358 translate
+90 rotate
+0 0 m /y glyphshow
+5.91797 0 m /space glyphshow
+9.09668 0 m /c glyphshow
+14.5947 0 m /o glyphshow
+20.7129 0 m /o glyphshow
+26.8311 0 m /r glyphshow
+30.8174 0 m /d glyphshow
+37.165 0 m /i glyphshow
+39.9434 0 m /n glyphshow
+46.2812 0 m /a glyphshow
+52.4092 0 m /t glyphshow
+56.3301 0 m /e glyphshow
+62.4824 0 m /s glyphshow
+67.6924 0 m /space glyphshow
+70.8711 0 m /bracketleft glyphshow
+74.7725 0 m /m glyphshow
+84.5137 0 m /bracketright glyphshow
+grestore
+0 setlinejoin
+2 setlinecap
+gsave
+43.778125 37.54375 m
+43.778125 303.65575 l
+stroke
+grestore
+gsave
+176.834125 37.54375 m
+176.834125 303.65575 l
+stroke
+grestore
+gsave
+43.778125 37.54375 m
+176.834125 37.54375 l
+stroke
+grestore
+gsave
+43.778125 303.65575 m
+176.834125 303.65575 l
+stroke
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+110.306 115.16 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+110.306 59.7198 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+165.746 115.16 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+110.306 170.6 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+54.8661 115.16 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+54.8661 59.7198 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+165.746 59.7198 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+165.746 170.6 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+54.8661 170.6 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+165.746 281.48 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+54.8661 281.48 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+110.306 226.04 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+110.306 281.48 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+54.8661 226.04 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+110.306 170.6 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+165.746 226.04 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+54.8661 59.7198 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+165.746 59.7198 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+165.746 170.6 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+54.8661 170.6 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+165.746 281.48 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+54.8661 281.48 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+
+end
+showpage
diff --git a/tests/test_data/expected_geometry_3D.eps b/tests/test_data/expected_geometry_3D.eps
new file mode 100644
index 00000000..3be3c3ce
--- /dev/null
+++ b/tests/test_data/expected_geometry_3D.eps
@@ -0,0 +1,3685 @@
+%!PS-Adobe-3.0 EPSF-3.0
+%%Title: generated_geometry_3D.eps
+%%Creator: Matplotlib v3.7.1, https://matplotlib.org/
+%%CreationDate: Tue Aug 22 13:23:14 2023
+%%Orientation: portrait
+%%BoundingBox: 165 253 447 539
+%%HiResBoundingBox: 165.744000 253.257153 446.256000 538.742847
+%%EndComments
+%%BeginProlog
+/mpldict 12 dict def
+mpldict begin
+/_d { bind def } bind def
+/m { moveto } _d
+/l { lineto } _d
+/r { rlineto } _d
+/c { curveto } _d
+/cl { closepath } _d
+/ce { closepath eofill } _d
+/box {
+      m
+      1 index 0 r
+      0 exch r
+      neg 0 r
+      cl
+    } _d
+/clipbox {
+      box
+      clip
+      newpath
+    } _d
+/sc { setcachedevice } _d
+%!PS-Adobe-3.0 Resource-Font
+%%Creator: Converted from TrueType to Type 3 by Matplotlib.
+10 dict begin
+/FontName /DejaVuSans def
+/PaintType 0 def
+/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def
+/FontBBox [-2090 -948 3673 2524] def
+/FontType 3 def
+/Encoding [/space /period /zero /one /two /four /five /six /eight /bracketleft /bracketright /a /c /d /e /i /m /n /o /r /s /t /x /y /z] def
+/CharStrings 26 dict dup begin
+/.notdef 0 def
+/space{651 0 0 0 0 0 sc
+ce} _d
+/period{651 0 219 0 430 254 sc
+219 254 m
+430 254 l
+430 0 l
+219 0 l
+219 254 l
+
+ce} _d
+/zero{1303 0 135 -29 1167 1520 sc
+651 1360 m
+547 1360 469 1309 416 1206 c
+364 1104 338 950 338 745 c
+338 540 364 387 416 284 c
+469 182 547 131 651 131 c
+756 131 834 182 886 284 c
+939 387 965 540 965 745 c
+965 950 939 1104 886 1206 c
+834 1309 756 1360 651 1360 c
+
+651 1520 m
+818 1520 946 1454 1034 1321 c
+1123 1189 1167 997 1167 745 c
+1167 494 1123 302 1034 169 c
+946 37 818 -29 651 -29 c
+484 -29 356 37 267 169 c
+179 302 135 494 135 745 c
+135 997 179 1189 267 1321 c
+356 1454 484 1520 651 1520 c
+
+ce} _d
+/one{1303 0 225 0 1114 1493 sc
+254 170 m
+584 170 l
+584 1309 l
+225 1237 l
+225 1421 l
+582 1493 l
+784 1493 l
+784 170 l
+1114 170 l
+1114 0 l
+254 0 l
+254 170 l
+
+ce} _d
+/two{1303 0 150 0 1098 1520 sc
+393 170 m
+1098 170 l
+1098 0 l
+150 0 l
+150 170 l
+227 249 331 356 463 489 c
+596 623 679 709 713 748 c
+778 821 823 882 848 932 c
+874 983 887 1032 887 1081 c
+887 1160 859 1225 803 1275 c
+748 1325 675 1350 586 1350 c
+523 1350 456 1339 385 1317 c
+315 1295 240 1262 160 1217 c
+160 1421 l
+241 1454 317 1478 388 1495 c
+459 1512 523 1520 582 1520 c
+737 1520 860 1481 952 1404 c
+1044 1327 1090 1223 1090 1094 c
+1090 1033 1078 974 1055 919 c
+1032 864 991 800 930 725 c
+913 706 860 650 771 557 c
+682 465 556 336 393 170 c
+
+ce} _d
+/four{1303 0 100 0 1188 1493 sc
+774 1317 m
+264 520 l
+774 520 l
+774 1317 l
+
+721 1493 m
+975 1493 l
+975 520 l
+1188 520 l
+1188 352 l
+975 352 l
+975 0 l
+774 0 l
+774 352 l
+100 352 l
+100 547 l
+721 1493 l
+
+ce} _d
+/five{1303 0 158 -29 1124 1493 sc
+221 1493 m
+1014 1493 l
+1014 1323 l
+406 1323 l
+406 957 l
+435 967 465 974 494 979 c
+523 984 553 987 582 987 c
+749 987 881 941 978 850 c
+1075 759 1124 635 1124 479 c
+1124 318 1074 193 974 104 c
+874 15 733 -29 551 -29 c
+488 -29 424 -24 359 -13 c
+294 -2 227 14 158 35 c
+158 238 l
+218 205 280 181 344 165 c
+408 149 476 141 547 141 c
+662 141 754 171 821 232 c
+888 293 922 375 922 479 c
+922 583 888 665 821 726 c
+754 787 662 817 547 817 c
+493 817 439 811 385 799 c
+332 787 277 768 221 743 c
+221 1493 l
+
+ce} _d
+/six{1303 0 143 -29 1174 1520 sc
+676 827 m
+585 827 513 796 460 734 c
+407 672 381 587 381 479 c
+381 372 407 287 460 224 c
+513 162 585 131 676 131 c
+767 131 838 162 891 224 c
+944 287 971 372 971 479 c
+971 587 944 672 891 734 c
+838 796 767 827 676 827 c
+
+1077 1460 m
+1077 1276 l
+1026 1300 975 1318 923 1331 c
+872 1344 821 1350 770 1350 c
+637 1350 535 1305 464 1215 c
+394 1125 354 989 344 807 c
+383 865 433 909 492 940 c
+551 971 617 987 688 987 c
+838 987 956 941 1043 850 c
+1130 759 1174 636 1174 479 c
+1174 326 1129 203 1038 110 c
+947 17 827 -29 676 -29 c
+503 -29 371 37 280 169 c
+189 302 143 494 143 745 c
+143 981 199 1169 311 1309 c
+423 1450 573 1520 762 1520 c
+813 1520 864 1515 915 1505 c
+967 1495 1021 1480 1077 1460 c
+
+ce} _d
+/eight{1303 0 139 -29 1163 1520 sc
+651 709 m
+555 709 479 683 424 632 c
+369 581 342 510 342 420 c
+342 330 369 259 424 208 c
+479 157 555 131 651 131 c
+747 131 823 157 878 208 c
+933 260 961 331 961 420 c
+961 510 933 581 878 632 c
+823 683 748 709 651 709 c
+
+449 795 m
+362 816 295 857 246 916 c
+198 975 174 1048 174 1133 c
+174 1252 216 1347 301 1416 c
+386 1485 503 1520 651 1520 c
+800 1520 916 1485 1001 1416 c
+1086 1347 1128 1252 1128 1133 c
+1128 1048 1104 975 1055 916 c
+1007 857 940 816 854 795 c
+951 772 1027 728 1081 662 c
+1136 596 1163 515 1163 420 c
+1163 275 1119 164 1030 87 c
+942 10 816 -29 651 -29 c
+486 -29 360 10 271 87 c
+183 164 139 275 139 420 c
+139 515 166 596 221 662 c
+276 728 352 772 449 795 c
+
+375 1114 m
+375 1037 399 976 447 933 c
+496 890 564 868 651 868 c
+738 868 805 890 854 933 c
+903 976 928 1037 928 1114 c
+928 1191 903 1252 854 1295 c
+805 1338 738 1360 651 1360 c
+564 1360 496 1338 447 1295 c
+399 1252 375 1191 375 1114 c
+
+ce} _d
+/bracketleft{799 0 176 -270 600 1556 sc
+176 1556 m
+600 1556 l
+600 1413 l
+360 1413 l
+360 -127 l
+600 -127 l
+600 -270 l
+176 -270 l
+176 1556 l
+
+ce} _d
+/bracketright{799 0 199 -270 623 1556 sc
+623 1556 m
+623 -270 l
+199 -270 l
+199 -127 l
+438 -127 l
+438 1413 l
+199 1413 l
+199 1556 l
+623 1556 l
+
+ce} _d
+/a{1255 0 123 -29 1069 1147 sc
+702 563 m
+553 563 450 546 393 512 c
+336 478 307 420 307 338 c
+307 273 328 221 371 182 c
+414 144 473 125 547 125 c
+649 125 731 161 792 233 c
+854 306 885 402 885 522 c
+885 563 l
+702 563 l
+
+1069 639 m
+1069 0 l
+885 0 l
+885 170 l
+843 102 791 52 728 19 c
+665 -13 589 -29 498 -29 c
+383 -29 292 3 224 67 c
+157 132 123 218 123 326 c
+123 452 165 547 249 611 c
+334 675 460 707 627 707 c
+885 707 l
+885 725 l
+885 810 857 875 801 921 c
+746 968 668 991 567 991 c
+503 991 441 983 380 968 c
+319 953 261 930 205 899 c
+205 1069 l
+272 1095 338 1114 401 1127 c
+464 1140 526 1147 586 1147 c
+748 1147 869 1105 949 1021 c
+1029 937 1069 810 1069 639 c
+
+ce} _d
+/c{1126 0 113 -29 999 1147 sc
+999 1077 m
+999 905 l
+947 934 895 955 842 969 c
+790 984 737 991 684 991 c
+565 991 472 953 406 877 c
+340 802 307 696 307 559 c
+307 422 340 316 406 240 c
+472 165 565 127 684 127 c
+737 127 790 134 842 148 c
+895 163 947 184 999 213 c
+999 43 l
+948 19 894 1 839 -11 c
+784 -23 726 -29 664 -29 c
+495 -29 361 24 262 130 c
+163 236 113 379 113 559 c
+113 742 163 885 263 990 c
+364 1095 501 1147 676 1147 c
+733 1147 788 1141 842 1129 c
+896 1118 948 1100 999 1077 c
+
+ce} _d
+/d{1300 0 113 -29 1114 1556 sc
+930 950 m
+930 1556 l
+1114 1556 l
+1114 0 l
+930 0 l
+930 168 l
+891 101 842 52 783 19 c
+724 -13 654 -29 571 -29 c
+436 -29 325 25 240 133 c
+155 241 113 383 113 559 c
+113 735 155 877 240 985 c
+325 1093 436 1147 571 1147 c
+654 1147 724 1131 783 1098 c
+842 1066 891 1017 930 950 c
+
+303 559 m
+303 424 331 317 386 240 c
+442 163 519 125 616 125 c
+713 125 790 163 846 240 c
+902 317 930 424 930 559 c
+930 694 902 800 846 877 c
+790 954 713 993 616 993 c
+519 993 442 954 386 877 c
+331 800 303 694 303 559 c
+
+ce} _d
+/e{1260 0 113 -29 1151 1147 sc
+1151 606 m
+1151 516 l
+305 516 l
+313 389 351 293 419 226 c
+488 160 583 127 705 127 c
+776 127 844 136 910 153 c
+977 170 1043 196 1108 231 c
+1108 57 l
+1042 29 974 8 905 -7 c
+836 -22 765 -29 694 -29 c
+515 -29 374 23 269 127 c
+165 231 113 372 113 549 c
+113 732 162 878 261 985 c
+360 1093 494 1147 662 1147 c
+813 1147 932 1098 1019 1001 c
+1107 904 1151 773 1151 606 c
+
+967 660 m
+966 761 937 841 882 901 c
+827 961 755 991 664 991 c
+561 991 479 962 417 904 c
+356 846 320 764 311 659 c
+967 660 l
+
+ce} _d
+/i{569 0 193 0 377 1556 sc
+193 1120 m
+377 1120 l
+377 0 l
+193 0 l
+193 1120 l
+
+193 1556 m
+377 1556 l
+377 1323 l
+193 1323 l
+193 1556 l
+
+ce} _d
+/m{1995 0 186 0 1821 1147 sc
+1065 905 m
+1111 988 1166 1049 1230 1088 c
+1294 1127 1369 1147 1456 1147 c
+1573 1147 1663 1106 1726 1024 c
+1789 943 1821 827 1821 676 c
+1821 0 l
+1636 0 l
+1636 670 l
+1636 777 1617 857 1579 909 c
+1541 961 1483 987 1405 987 c
+1310 987 1234 955 1179 892 c
+1124 829 1096 742 1096 633 c
+1096 0 l
+911 0 l
+911 670 l
+911 778 892 858 854 909 c
+816 961 757 987 678 987 c
+584 987 509 955 454 891 c
+399 828 371 742 371 633 c
+371 0 l
+186 0 l
+186 1120 l
+371 1120 l
+371 946 l
+413 1015 463 1065 522 1098 c
+581 1131 650 1147 731 1147 c
+812 1147 881 1126 938 1085 c
+995 1044 1038 984 1065 905 c
+
+ce} _d
+/n{1298 0 186 0 1124 1147 sc
+1124 676 m
+1124 0 l
+940 0 l
+940 670 l
+940 776 919 855 878 908 c
+837 961 775 987 692 987 c
+593 987 514 955 457 892 c
+400 829 371 742 371 633 c
+371 0 l
+186 0 l
+186 1120 l
+371 1120 l
+371 946 l
+415 1013 467 1064 526 1097 c
+586 1130 655 1147 733 1147 c
+862 1147 959 1107 1025 1027 c
+1091 948 1124 831 1124 676 c
+
+ce} _d
+/o{1253 0 113 -29 1141 1147 sc
+627 991 m
+528 991 450 952 393 875 c
+336 798 307 693 307 559 c
+307 425 335 319 392 242 c
+449 165 528 127 627 127 c
+725 127 803 166 860 243 c
+917 320 946 426 946 559 c
+946 692 917 797 860 874 c
+803 952 725 991 627 991 c
+
+627 1147 m
+787 1147 913 1095 1004 991 c
+1095 887 1141 743 1141 559 c
+1141 376 1095 232 1004 127 c
+913 23 787 -29 627 -29 c
+466 -29 340 23 249 127 c
+158 232 113 376 113 559 c
+113 743 158 887 249 991 c
+340 1095 466 1147 627 1147 c
+
+ce} _d
+/r{842 0 186 0 842 1147 sc
+842 948 m
+821 960 799 969 774 974 c
+750 980 723 983 694 983 c
+590 983 510 949 454 881 c
+399 814 371 717 371 590 c
+371 0 l
+186 0 l
+186 1120 l
+371 1120 l
+371 946 l
+410 1014 460 1064 522 1097 c
+584 1130 659 1147 748 1147 c
+761 1147 775 1146 790 1144 c
+805 1143 822 1140 841 1137 c
+842 948 l
+
+ce} _d
+/s{1067 0 111 -29 967 1147 sc
+907 1087 m
+907 913 l
+855 940 801 960 745 973 c
+689 986 631 993 571 993 c
+480 993 411 979 365 951 c
+320 923 297 881 297 825 c
+297 782 313 749 346 724 c
+379 700 444 677 543 655 c
+606 641 l
+737 613 829 573 884 522 c
+939 471 967 400 967 309 c
+967 205 926 123 843 62 c
+761 1 648 -29 504 -29 c
+444 -29 381 -23 316 -11 c
+251 0 183 18 111 41 c
+111 231 l
+179 196 246 169 312 151 c
+378 134 443 125 508 125 c
+595 125 661 140 708 169 c
+755 199 778 241 778 295 c
+778 345 761 383 727 410 c
+694 437 620 462 506 487 c
+442 502 l
+328 526 246 563 195 612 c
+144 662 119 730 119 817 c
+119 922 156 1004 231 1061 c
+306 1118 412 1147 549 1147 c
+617 1147 681 1142 741 1132 c
+801 1122 856 1107 907 1087 c
+
+ce} _d
+/t{803 0 55 0 754 1438 sc
+375 1438 m
+375 1120 l
+754 1120 l
+754 977 l
+375 977 l
+375 369 l
+375 278 387 219 412 193 c
+437 167 488 154 565 154 c
+754 154 l
+754 0 l
+565 0 l
+423 0 325 26 271 79 c
+217 132 190 229 190 369 c
+190 977 l
+55 977 l
+55 1120 l
+190 1120 l
+190 1438 l
+375 1438 l
+
+ce} _d
+/x{1212 0 59 0 1145 1120 sc
+1124 1120 m
+719 575 l
+1145 0 l
+928 0 l
+602 440 l
+276 0 l
+59 0 l
+494 586 l
+96 1120 l
+313 1120 l
+610 721 l
+907 1120 l
+1124 1120 l
+
+ce} _d
+/y{1212 0 61 -426 1151 1120 sc
+659 -104 m
+607 -237 556 -324 507 -365 c
+458 -406 392 -426 309 -426 c
+162 -426 l
+162 -272 l
+270 -272 l
+321 -272 360 -260 388 -236 c
+416 -212 447 -155 481 -66 c
+514 18 l
+61 1120 l
+256 1120 l
+606 244 l
+956 1120 l
+1151 1120 l
+659 -104 l
+
+ce} _d
+/z{1075 0 88 0 987 1120 sc
+113 1120 m
+987 1120 l
+987 952 l
+295 147 l
+987 147 l
+987 0 l
+88 0 l
+88 168 l
+780 973 l
+113 973 l
+113 1120 l
+
+ce} _d
+end readonly def
+
+/BuildGlyph {
+ exch begin
+ CharStrings exch
+ 2 copy known not {pop /.notdef} if
+ true 3 1 roll get exec
+ end
+} _d
+
+/BuildChar {
+ 1 index /Encoding get exch get
+ 1 index /BuildGlyph get exec
+} _d
+
+FontName currentdict end definefont pop
+%!PS-Adobe-3.0 Resource-Font
+%%Creator: Converted from TrueType to Type 3 by Matplotlib.
+10 dict begin
+/FontName /DejaVuSans-Bold def
+/PaintType 0 def
+/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def
+/FontBBox [-2190 -850 4045 2405] def
+/FontType 3 def
+/Encoding [/l /p /one /two /s /four /three /five /six /seven /nine /eight /zero /v /underscore] def
+/CharStrings 16 dict dup begin
+/.notdef 0 def
+/l{702 0 172 0 530 1556 sc
+172 1556 m
+530 1556 l
+530 0 l
+172 0 l
+172 1556 l
+
+ce} _d
+/p{1466 0 172 -426 1374 1147 sc
+530 162 m
+530 -426 l
+172 -426 l
+172 1120 l
+530 1120 l
+530 956 l
+579 1021 634 1069 694 1100 c
+754 1131 823 1147 901 1147 c
+1039 1147 1152 1092 1241 982 c
+1330 873 1374 732 1374 559 c
+1374 386 1330 245 1241 135 c
+1152 26 1039 -29 901 -29 c
+823 -29 754 -14 694 17 c
+634 48 579 97 530 162 c
+
+768 887 m
+691 887 632 859 591 802 c
+550 746 530 665 530 559 c
+530 453 550 372 591 315 c
+632 259 691 231 768 231 c
+845 231 903 259 943 315 c
+984 371 1004 452 1004 559 c
+1004 666 984 747 943 803 c
+903 859 845 887 768 887 c
+
+ce} _d
+/one{1425 0 231 0 1284 1493 sc
+240 266 m
+580 266 l
+580 1231 l
+231 1159 l
+231 1421 l
+578 1493 l
+944 1493 l
+944 266 l
+1284 266 l
+1284 0 l
+240 0 l
+240 266 l
+
+ce} _d
+/two{1425 0 162 0 1247 1520 sc
+590 283 m
+1247 283 l
+1247 0 l
+162 0 l
+162 283 l
+707 764 l
+756 808 792 851 815 893 c
+838 935 850 979 850 1024 c
+850 1094 826 1150 779 1193 c
+732 1236 670 1257 592 1257 c
+532 1257 466 1244 395 1218 c
+324 1193 247 1155 166 1104 c
+166 1432 l
+253 1461 338 1482 423 1497 c
+508 1512 591 1520 672 1520 c
+851 1520 989 1481 1088 1402 c
+1187 1323 1237 1214 1237 1073 c
+1237 992 1216 916 1174 845 c
+1132 775 1044 681 909 563 c
+590 283 l
+
+ce} _d
+/s{1219 0 106 -29 1122 1147 sc
+1047 1085 m
+1047 813 l
+970 845 896 869 825 885 c
+754 901 686 909 623 909 c
+555 909 504 900 471 883 c
+438 866 422 840 422 805 c
+422 776 434 754 459 739 c
+484 724 529 712 594 705 c
+657 696 l
+840 673 964 634 1027 581 c
+1090 528 1122 444 1122 330 c
+1122 211 1078 121 990 61 c
+902 1 771 -29 596 -29 c
+522 -29 445 -23 366 -11 c
+287 0 206 18 123 41 c
+123 313 l
+194 278 267 252 342 235 c
+417 218 494 209 571 209 c
+641 209 694 219 729 238 c
+764 257 782 286 782 324 c
+782 356 770 380 745 395 c
+721 411 673 423 600 432 c
+537 440 l
+378 460 266 497 202 551 c
+138 605 106 687 106 797 c
+106 916 147 1004 228 1061 c
+309 1118 434 1147 602 1147 c
+668 1147 737 1142 810 1132 c
+883 1122 962 1106 1047 1085 c
+
+ce} _d
+/four{1425 0 92 0 1331 1493 sc
+754 1176 m
+332 551 l
+754 551 l
+754 1176 l
+
+690 1493 m
+1118 1493 l
+1118 551 l
+1331 551 l
+1331 272 l
+1118 272 l
+1118 0 l
+754 0 l
+754 272 l
+92 272 l
+92 602 l
+690 1493 l
+
+ce} _d
+/three{1425 0 137 -29 1262 1520 sc
+954 805 m
+1055 779 1131 734 1183 669 c
+1236 605 1262 523 1262 424 c
+1262 276 1205 163 1092 86 c
+979 9 813 -29 596 -29 c
+519 -29 442 -23 365 -10 c
+288 2 212 20 137 45 c
+137 342 l
+209 306 280 279 351 260 c
+422 242 492 233 561 233 c
+663 233 741 251 795 286 c
+850 321 877 372 877 438 c
+877 506 849 557 793 592 c
+738 627 656 645 547 645 c
+393 645 l
+393 893 l
+555 893 l
+652 893 724 908 771 938 c
+818 969 842 1015 842 1077 c
+842 1134 819 1179 773 1210 c
+727 1241 662 1257 578 1257 c
+516 1257 453 1250 390 1236 c
+327 1222 264 1201 201 1174 c
+201 1456 l
+277 1477 352 1493 427 1504 c
+502 1515 575 1520 647 1520 c
+841 1520 986 1488 1082 1424 c
+1179 1361 1227 1265 1227 1137 c
+1227 1050 1204 978 1158 922 c
+1112 867 1044 828 954 805 c
+
+ce} _d
+/five{1425 0 158 -29 1282 1493 sc
+217 1493 m
+1174 1493 l
+1174 1210 l
+524 1210 l
+524 979 l
+553 987 583 993 612 997 c
+642 1002 673 1004 705 1004 c
+887 1004 1029 958 1130 867 c
+1231 776 1282 650 1282 487 c
+1282 326 1227 199 1116 108 c
+1006 17 853 -29 657 -29 c
+572 -29 488 -21 405 -4 c
+322 12 240 37 158 70 c
+158 373 l
+239 326 316 291 389 268 c
+462 245 531 233 596 233 c
+689 233 763 256 816 301 c
+870 347 897 409 897 487 c
+897 566 870 628 816 673 c
+763 718 689 741 596 741 c
+541 741 482 734 419 719 c
+356 705 289 683 217 653 c
+217 1493 l
+
+ce} _d
+/six{1425 0 127 -29 1315 1518 sc
+741 737 m
+674 737 623 715 589 671 c
+556 628 539 562 539 475 c
+539 388 556 322 589 278 c
+623 235 674 213 741 213 c
+809 213 860 235 893 278 c
+927 322 944 388 944 475 c
+944 562 927 628 893 671 c
+860 715 809 737 741 737 c
+
+1217 1454 m
+1217 1178 l
+1154 1208 1094 1230 1038 1244 c
+982 1259 927 1266 874 1266 c
+759 1266 670 1234 606 1170 c
+542 1107 505 1012 494 887 c
+538 920 586 944 637 960 c
+688 977 744 985 805 985 c
+958 985 1081 940 1174 851 c
+1268 762 1315 645 1315 500 c
+1315 340 1263 212 1158 115 c
+1053 19 913 -29 737 -29 c
+543 -29 393 36 286 167 c
+180 298 127 484 127 725 c
+127 972 189 1165 313 1306 c
+438 1447 608 1518 825 1518 c
+894 1518 960 1513 1025 1502 c
+1090 1491 1154 1475 1217 1454 c
+
+ce} _d
+/seven{1425 0 137 0 1262 1493 sc
+137 1493 m
+1262 1493 l
+1262 1276 l
+680 0 l
+305 0 l
+856 1210 l
+137 1210 l
+137 1493 l
+
+ce} _d
+/nine{1425 0 106 -29 1294 1518 sc
+205 33 m
+205 309 l
+266 280 325 259 381 244 c
+437 230 492 223 547 223 c
+662 223 751 255 815 318 c
+879 382 917 477 928 602 c
+883 569 834 544 783 527 c
+732 510 676 502 616 502 c
+463 502 340 546 246 635 c
+153 724 106 842 106 987 c
+106 1148 158 1276 262 1373 c
+367 1470 507 1518 682 1518 c
+877 1518 1027 1452 1134 1321 c
+1241 1190 1294 1004 1294 764 c
+1294 517 1232 323 1107 182 c
+982 41 811 -29 594 -29 c
+524 -29 457 -24 393 -13 c
+329 -3 266 12 205 33 c
+
+680 752 m
+747 752 798 774 832 817 c
+866 861 883 927 883 1014 c
+883 1101 866 1166 832 1210 c
+798 1254 747 1276 680 1276 c
+613 1276 562 1254 528 1210 c
+494 1166 477 1101 477 1014 c
+477 927 494 861 528 817 c
+562 774 613 752 680 752 c
+
+ce} _d
+/eight{1425 0 125 -29 1298 1520 sc
+713 668 m
+641 668 586 648 547 609 c
+508 570 489 513 489 440 c
+489 367 508 310 547 271 c
+586 232 641 213 713 213 c
+784 213 839 232 877 271 c
+915 310 934 367 934 440 c
+934 514 915 570 877 609 c
+839 648 784 668 713 668 c
+
+432 795 m
+341 822 273 864 227 921 c
+181 978 158 1048 158 1133 c
+158 1259 205 1355 299 1421 c
+393 1487 531 1520 713 1520 c
+894 1520 1031 1487 1125 1421 c
+1219 1356 1266 1260 1266 1133 c
+1266 1048 1243 978 1196 921 c
+1150 864 1082 822 991 795 c
+1092 767 1169 721 1220 658 c
+1272 595 1298 516 1298 420 c
+1298 272 1249 160 1150 84 c
+1052 9 906 -29 713 -29 c
+519 -29 372 9 273 84 c
+174 160 125 272 125 420 c
+125 516 151 595 202 658 c
+254 721 331 767 432 795 c
+
+522 1094 m
+522 1035 538 989 571 957 c
+604 925 652 909 713 909 c
+773 909 819 925 852 957 c
+885 989 901 1035 901 1094 c
+901 1153 885 1199 852 1230 c
+819 1262 773 1278 713 1278 c
+652 1278 604 1262 571 1230 c
+538 1198 522 1153 522 1094 c
+
+ce} _d
+/zero{1425 0 98 -29 1327 1520 sc
+942 748 m
+942 935 924 1066 889 1142 c
+854 1219 796 1257 713 1257 c
+630 1257 571 1219 536 1142 c
+501 1066 483 935 483 748 c
+483 559 501 426 536 349 c
+571 272 630 233 713 233 c
+795 233 854 272 889 349 c
+924 426 942 559 942 748 c
+
+1327 745 m
+1327 498 1274 307 1167 172 c
+1060 38 909 -29 713 -29 c
+516 -29 365 38 258 172 c
+151 307 98 498 98 745 c
+98 993 151 1184 258 1318 c
+365 1453 516 1520 713 1520 c
+909 1520 1060 1453 1167 1318 c
+1274 1184 1327 993 1327 745 c
+
+ce} _d
+/v{1335 0 31 0 1305 1120 sc
+31 1120 m
+389 1120 l
+668 346 l
+946 1120 l
+1305 1120 l
+864 0 l
+471 0 l
+31 1120 l
+
+ce} _d
+/underscore{1024 0 0 -483 1024 -293 sc
+1024 -293 m
+1024 -483 l
+0 -483 l
+0 -293 l
+1024 -293 l
+
+ce} _d
+end readonly def
+
+/BuildGlyph {
+ exch begin
+ CharStrings exch
+ 2 copy known not {pop /.notdef} if
+ true 3 1 roll get exec
+ end
+} _d
+
+/BuildChar {
+ 1 index /Encoding get exch get
+ 1 index /BuildGlyph get exec
+} _d
+
+FontName currentdict end definefont pop
+end
+%%EndProlog
+mpldict begin
+165.744 253.257 translate
+280.512 285.486 0 0 clipbox
+gsave
+0 0 m
+280.512 0 l
+280.512 285.485693 l
+0 285.485693 l
+cl
+1.000 setgray
+fill
+grestore
+gsave
+7.2 12.173693 m
+273.312 12.173693 l
+273.312 278.285693 l
+7.2 278.285693 l
+cl
+1.000 setgray
+fill
+grestore
+1.000 setlinewidth
+0 setlinejoin
+0 setlinecap
+[] 0 setdash
+0.950 setgray
+gsave
+37.083598 64.027193 m
+150.700514 161.6051 l
+150.952849 253.875783 l
+32.66073 163.722622 l
+gsave
+fill
+grestore
+stroke
+grestore
+0.900 setgray
+gsave
+150.700514 161.6051 m
+242.266852 135.640726 l
+246.012549 229.964549 l
+150.952849 253.875783 l
+gsave
+fill
+grestore
+stroke
+grestore
+0.925 setgray
+gsave
+37.083598 64.027193 m
+135.912987 31.372635 l
+242.266852 135.640726 l
+150.700514 161.6051 l
+gsave
+fill
+grestore
+stroke
+grestore
+0.800 setlinewidth
+1 setlinejoin
+2 setlinecap
+0.000 setgray
+gsave
+37.083598 64.027193 m
+135.912987 31.372635 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+20.5819 20.8365 translate
+341.716 rotate
+0 0 m /x glyphshow
+5.91797 0 m /space glyphshow
+9.09668 0 m /c glyphshow
+14.5947 0 m /o glyphshow
+20.7129 0 m /o glyphshow
+26.8311 0 m /r glyphshow
+30.8174 0 m /d glyphshow
+37.165 0 m /i glyphshow
+39.9434 0 m /n glyphshow
+46.2812 0 m /a glyphshow
+52.4092 0 m /t glyphshow
+56.3301 0 m /e glyphshow
+62.4824 0 m /s glyphshow
+67.6924 0 m /space glyphshow
+70.8711 0 m /bracketleft glyphshow
+74.7725 0 m /m glyphshow
+84.5137 0 m /bracketright glyphshow
+grestore
+0 setlinecap
+0.690 setgray
+gsave
+46.659075 60.863327 m
+159.602745 159.080801 l
+160.184948 251.553549 l
+stroke
+grestore
+gsave
+62.118299 55.755392 m
+173.961205 155.009343 l
+175.079881 247.80689 l
+stroke
+grestore
+gsave
+77.750437 50.590323 m
+188.462922 150.897264 l
+190.128973 244.021455 l
+stroke
+grestore
+gsave
+93.558407 45.367158 m
+203.11005 146.743953 l
+205.33463 240.196637 l
+stroke
+grestore
+gsave
+109.545192 40.084909 m
+217.904786 142.548786 l
+220.699306 236.33182 l
+stroke
+grestore
+gsave
+125.713843 34.74257 m
+232.849374 138.311128 l
+236.22551 232.426372 l
+stroke
+grestore
+2 setlinecap
+0.000 setgray
+gsave
+47.669851 61.742312 m
+44.631704 59.100295 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+28.4666 36.0705 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+gsave
+63.119814 56.644176 m
+60.109471 53.972676 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+43.9722 30.8321 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /two glyphshow
+grestore
+gsave
+78.74242 51.489072 m
+75.760696 48.787592 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+59.6523 25.5346 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /four glyphshow
+grestore
+gsave
+94.540579 46.276039 m
+91.588311 43.544072 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+75.5101 20.1771 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /six glyphshow
+grestore
+gsave
+110.517268 41.004096 m
+107.595315 38.241123 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+91.5484 14.7587 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /eight glyphshow
+grestore
+gsave
+126.675529 35.672237 m
+123.784775 32.877728 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+107.77 9.27812 translate
+0 rotate
+0 0 m /one glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+gsave
+242.266852 135.640726 m
+135.912987 31.372635 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+179.898 31.2089 translate
+44.4326 rotate
+0 0 m /y glyphshow
+5.91797 0 m /space glyphshow
+9.09668 0 m /c glyphshow
+14.5947 0 m /o glyphshow
+20.7129 0 m /o glyphshow
+26.8311 0 m /r glyphshow
+30.8174 0 m /d glyphshow
+37.165 0 m /i glyphshow
+39.9434 0 m /n glyphshow
+46.2812 0 m /a glyphshow
+52.4092 0 m /t glyphshow
+56.3301 0 m /e glyphshow
+62.4824 0 m /s glyphshow
+67.6924 0 m /space glyphshow
+70.8711 0 m /bracketleft glyphshow
+74.7725 0 m /m glyphshow
+84.5137 0 m /bracketright glyphshow
+grestore
+0 setlinecap
+0.690 setgray
+gsave
+45.796764 173.733897 m
+49.651124 74.820594 l
+147.716904 42.945057 l
+stroke
+grestore
+gsave
+71.124552 193.036791 m
+73.917458 95.661312 l
+170.480731 65.262449 l
+stroke
+grestore
+gsave
+95.266958 211.436277 m
+97.090717 115.563262 l
+192.184711 86.540778 l
+stroke
+grestore
+gsave
+118.305294 228.994326 m
+119.243133 134.588477 l
+212.901177 106.85096 l
+stroke
+grestore
+gsave
+140.313605 245.767369 m
+140.440706 152.793643 l
+232.696026 126.2576 l
+stroke
+grestore
+2 setlinecap
+0.000 setgray
+gsave
+146.904959 43.208974 m
+149.342168 42.416777 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+148.17 24.5635 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+gsave
+169.681869 65.513936 m
+172.079774 64.759057 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+170.724 47.128 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /five glyphshow
+grestore
+gsave
+191.398609 86.780695 m
+193.758184 86.060557 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+192.228 68.6424 translate
+0 rotate
+0 0 m /one glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+gsave
+212.127513 107.080086 m
+214.449725 106.392346 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+212.754 89.1784 translate
+0 rotate
+0 0 m /one glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /five glyphshow
+grestore
+gsave
+231.934484 126.476647 m
+234.220285 125.819166 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+232.367 108.801 translate
+0 rotate
+0 0 m /two glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+gsave
+242.266852 135.640726 m
+246.012549 229.964549 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+279.351 149.242 translate
+87.7259 rotate
+0 0 m /z glyphshow
+5.24902 0 m /space glyphshow
+8.42773 0 m /c glyphshow
+13.9258 0 m /o glyphshow
+20.0439 0 m /o glyphshow
+26.1621 0 m /r glyphshow
+30.1484 0 m /d glyphshow
+36.4961 0 m /i glyphshow
+39.2744 0 m /n glyphshow
+45.6123 0 m /a glyphshow
+51.7402 0 m /t glyphshow
+55.6611 0 m /e glyphshow
+61.8135 0 m /s glyphshow
+67.0234 0 m /space glyphshow
+70.2021 0 m /bracketleft glyphshow
+74.1035 0 m /m glyphshow
+83.8447 0 m /bracketright glyphshow
+grestore
+0 setlinecap
+0.690 setgray
+gsave
+242.629016 144.76071 m
+150.724937 170.536008 l
+36.657208 73.638408 l
+stroke
+grestore
+gsave
+243.214056 159.493126 m
+150.764379 184.95872 l
+35.967855 89.177051 l
+stroke
+grestore
+gsave
+243.806067 174.401097 m
+150.804277 199.547926 l
+35.269578 104.916842 l
+stroke
+grestore
+gsave
+244.405175 189.48778 m
+150.844638 214.306528 l
+34.562203 120.86171 l
+stroke
+grestore
+gsave
+245.011508 204.756408 m
+150.88547 229.237492 l
+33.845551 137.015691 l
+stroke
+grestore
+gsave
+245.625198 220.210292 m
+150.926782 244.343856 l
+33.119438 153.382924 l
+stroke
+grestore
+2 setlinecap
+0.000 setgray
+gsave
+241.870545 144.973429 m
+244.147119 144.334945 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+250.322 142.57 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+gsave
+242.450944 159.703328 m
+244.741455 159.072398 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+250.995 157.238 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /two glyphshow
+grestore
+gsave
+243.038257 174.608706 m
+245.342876 173.985558 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+251.676 172.08 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /four glyphshow
+grestore
+gsave
+243.632608 189.692718 m
+245.951511 189.077584 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+252.366 187.098 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /six glyphshow
+grestore
+gsave
+244.234126 204.958596 m
+246.567491 204.351715 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+253.063 202.297 translate
+0 rotate
+0 0 m /zero glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /eight glyphshow
+grestore
+gsave
+244.842939 220.409648 m
+247.190947 219.811266 l
+stroke
+grestore
+/DejaVuSans 10.000 selectfont
+gsave
+
+253.77 217.679 translate
+0 rotate
+0 0 m /one glyphshow
+6.3623 0 m /period glyphshow
+9.54102 0 m /zero glyphshow
+grestore
+1.000 setlinewidth
+0 setlinecap
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+223.499101 138.155128 m
+225.934725 214.018166 l
+149.593329 233.76273 l
+149.427238 159.229136 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+96.8961 108.161 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+57.4523 120.303 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+96.117 148.826 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.471 95.6704 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+97.6502 68.8018 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+160.863 117.083 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.471 95.6704 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+161.142 157.471 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.148 137.482 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+160.593 77.9786 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+143.852 148.826 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.148 137.482 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+143.852 188.205 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.601 159.868 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+143.852 110.653 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+82.094 140.552 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.601 159.868 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+81.108 180.198 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+57.4523 120.303 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+83.0495 102.132 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+121.295 90.2196 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+97.6502 68.8018 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+160.593 77.9786 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+143.852 110.653 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+83.0495 102.132 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+120.564 168.994 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+96.117 148.826 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+161.142 157.471 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+143.852 188.205 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+81.108 180.198 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+120.935 128.979 translate
+0 rotate
+0 0 m /v glyphshow
+9.12598 0 m /underscore glyphshow
+16.126 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+186.62 185.864 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+224.699 175.515 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+187.265 224.019 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.509 195.949 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+185.993 148.826 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+128.049 178.315 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.509 195.949 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+127.808 216.724 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.601 159.868 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+128.283 141.041 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+143.852 148.826 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.148 137.482 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+143.852 188.205 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.601 159.868 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+143.852 110.653 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+204.404 156.938 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.148 137.482 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+205.351 196.054 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /two glyphshow
+21.54 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+224.699 175.515 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+203.485 119.009 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+165.396 130.168 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /one glyphshow
+25.0742 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+185.993 148.826 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+128.283 141.041 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+143.852 110.653 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+203.485 119.009 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+166.061 206.527 translate
+0 rotate
+0 0 m /s glyphshow
+8.33301 0 m /underscore glyphshow
+15.333 0 m /one glyphshow
+25.0742 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+187.265 224.019 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+127.808 216.724 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+143.852 188.205 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /one glyphshow
+21.54 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+205.351 196.054 translate
+0 rotate
+0 0 m /l glyphshow
+4.79883 0 m /underscore glyphshow
+11.7988 0 m /two glyphshow
+21.54 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+58.8208 81.2915 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+56.039 160.591 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.575 55.9597 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /three glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+137.364 136.718 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /four glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+182.538 98.9719 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /five glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+183.778 177.226 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /six glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+106.179 122.028 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /seven glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+105.004 198.888 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /eight glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+223.499 138.155 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /nine glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+225.935 214.018 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /zero glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.427 159.229 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /one glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+149.593 233.763 translate
+0 rotate
+0 0 m /p glyphshow
+10.0215 0 m /underscore glyphshow
+17.0215 0 m /one glyphshow
+26.7627 0 m /two glyphshow
+grestore
+/DejaVuSans-Bold 14.000 selectfont
+gsave
+
+165.724 167.767 translate
+0 rotate
+0 0 m /v glyphshow
+9.12598 0 m /underscore glyphshow
+16.126 0 m /two glyphshow
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+223.499101 138.155128 m
+149.427238 159.229136 l
+106.179112 122.028063 l
+182.5383 98.971868 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+149.427238 159.229136 m
+149.593329 233.76273 l
+105.004287 198.888023 l
+106.179112 122.028063 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+182.5383 98.971868 m
+183.778208 177.226229 l
+105.004287 198.888023 l
+106.179112 122.028063 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+182.5383 98.971868 m
+183.778208 177.226229 l
+105.004287 198.888023 l
+106.179112 122.028063 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+182.5383 98.971868 m
+183.778208 177.226229 l
+225.934725 214.018166 l
+223.499101 138.155128 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+225.934725 214.018166 m
+149.593329 233.76273 l
+105.004287 198.888023 l
+183.778208 177.226229 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+58.820844 81.291537 m
+137.574926 55.959732 l
+182.5383 98.971868 l
+106.179112 122.028063 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+106.179112 122.028063 m
+105.004287 198.888023 l
+56.039033 160.590523 l
+58.820844 81.291537 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+58.820844 81.291537 m
+56.039033 160.590523 l
+137.363579 136.718041 l
+137.574926 55.959732 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+137.574926 55.959732 m
+137.363579 136.718041 l
+183.778208 177.226229 l
+182.5383 98.971868 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+gsave
+266.112 266.112 7.2 12.174 clipbox
+/o {
+gsave
+newpath
+translate
+1.0 setlinewidth
+1 setlinejoin
+
+0 setlinecap
+
+56.039033 160.590523 m
+137.363579 136.718041 l
+183.778208 177.226229 l
+105.004287 198.888023 l
+cl
+
+gsave
+0.000 0.000 1.000 setrgbcolor
+fill
+grestore
+stroke
+grestore
+} bind def
+0 0 o
+grestore
+
+end
+showpage
diff --git a/tests/test_data/expected_geometry_after_sync_3D.pickle b/tests/test_data/expected_geometry_after_sync_3D.pickle
new file mode 100644
index 00000000..5a8c99e7
Binary files /dev/null and b/tests/test_data/expected_geometry_after_sync_3D.pickle differ
diff --git a/tests/test_data/expected_load_parameters.json b/tests/test_data/expected_load_parameters_no_table.json
similarity index 79%
rename from tests/test_data/expected_load_parameters.json
rename to tests/test_data/expected_load_parameters_no_table.json
index 2ea6f7d0..6e91d646 100644
--- a/tests/test_data/expected_load_parameters.json
+++ b/tests/test_data/expected_load_parameters_no_table.json
@@ -36,17 +36,6 @@
                     "table": [0, 0, 0]
                 }
             },  {
-            "python_module": "apply_vector_constraint_table_process",
-            "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
-            "process_name":  "ApplyVectorConstraintTableProcess",
-            "Parameters":    {
-                "model_part_name": "PorousDomain.test_gravity_load",
-                "variable_name":   "VOLUME_ACCELERATION",
-                "active":          [false,true,false],
-                "value":           [0.0,-9.81,0.0],
-                "table":           [0,0,0]
-                }
-            },  {
             "python_module": "set_moving_load_process",
             "kratos_module": "StructuralMechanicsApplication",
             "process_name": "SetMovingLoadProcess",
@@ -56,7 +45,7 @@
                 "load": [0.0, -10, 0.0],
                 "direction": [1.0, 0.0, -1.0],
                 "velocity": 5.0,
-                "origin": [0.0, 1.0, 2.0],
+                "origin": [1.0, 0.1, 0.0],
                 "offset": 3.0
                 }
             }
diff --git a/tests/test_data/expected_load_parameters_with_table.json b/tests/test_data/expected_load_parameters_with_table.json
new file mode 100644
index 00000000..9618851a
--- /dev/null
+++ b/tests/test_data/expected_load_parameters_with_table.json
@@ -0,0 +1,55 @@
+{
+    "processes":
+    {
+        "loads_process_list": [
+            {
+                "python_module": "apply_vector_constraint_table_process",
+                "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
+                "process_name": "ApplyVectorConstraintTableProcess",
+                "Parameters": {
+                    "model_part_name": "PorousDomain.test_point_load",
+                    "variable_name": "POINT_LOAD",
+                    "active": [true, false, true],
+                    "value": [0, -20.0, 0.0],
+                    "table": [1, 0, 0]
+                }
+            }, {
+                "python_module": "apply_vector_constraint_table_process",
+                "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
+                "process_name": "ApplyVectorConstraintTableProcess",
+                "Parameters": {
+                    "model_part_name": "PorousDomain.test_line_load",
+                    "variable_name": "LINE_LOAD",
+                    "active": [false, true, false],
+                    "value": [-10.0, 0.0, 30.0],
+                    "table": [0, 2, 0]
+                }
+            },  {
+                "python_module": "apply_vector_constraint_table_process",
+                "kratos_module": "KratosMultiphysics.GeoMechanicsApplication",
+                "process_name": "ApplyVectorConstraintTableProcess",
+                "Parameters": {
+                    "model_part_name": "PorousDomain.test_surface_load",
+                    "variable_name": "SURFACE_LOAD",
+                    "active": [false, false, true],
+                    "value": [0.0, 0.0, -200.0],
+                    "table": [0, 0, 0]
+                }
+            },  {
+            "python_module": "set_moving_load_process",
+            "kratos_module": "StructuralMechanicsApplication",
+            "process_name": "SetMovingLoadProcess",
+            "Parameters": {
+                "model_part_name": "PorousDomain.test_moving_load",
+                "variable_name": "POINT_LOAD",
+                "load": [0.0, -10, 0.0],
+                "direction": [1.0, 0.0, -1.0],
+                "velocity": 5.0,
+                "origin": [1.0, 0.1, 0.0],
+                "offset": 3.0
+                }
+            }
+        ],
+        "constraints_process_list": []
+    }
+}
diff --git a/tests/test_data/expected_mdpa_file.mdpa b/tests/test_data/expected_mdpa_file.mdpa
new file mode 100644
index 00000000..a97216f2
--- /dev/null
+++ b/tests/test_data/expected_mdpa_file.mdpa
@@ -0,0 +1,158 @@
+
+Begin Properties 1
+End Properties
+
+
+Begin Properties 2
+End Properties
+
+
+Begin Properties 3
+End Properties
+
+
+Begin Properties 4
+End Properties
+
+
+Begin Table 1 TIME VALUE
+   0.0000   0.0000
+   0.5000   5.0000
+   1.0000   10.0000
+   1.5000   5.0000
+   2.0000   0.0000
+   2.5000   0.0000
+End Table
+
+
+Begin Table 2 TIME VALUE
+   0.0000   0.0000
+   0.5000   -5.0000
+   1.0000   -10.0000
+   1.5000   -5.0000
+   2.0000   0.0000
+   2.5000   0.0000
+End Table
+
+
+Begin Nodes
+  1   0.0000000000  0.0000000000  0.0000000000
+  2   1.0000000000  0.0000000000  0.0000000000
+  3   1.0000000000  1.0000000000  0.0000000000
+  4   0.0000000000  1.0000000000  0.0000000000
+  5   0.5000000000  0.5000000000  0.0000000000
+End Nodes
+
+
+Begin Elements UPwSmallStrainElement2D3N
+  5  1  2 5 1
+  6  1  1 5 4
+  7  1  3 5 2
+  8  1  4 5 3
+End Elements
+
+
+Begin Conditions LineLoadCondition2D2N
+  3  1  3 4
+End Conditions
+
+
+Begin Conditions LineLoadCondition2D2N
+  1  2  1 2
+End Conditions
+
+
+Begin Conditions LineLoadCondition2D2N
+  4  3  4 1
+End Conditions
+
+
+Begin Conditions LineLoadCondition2D2N
+  2  4  2 3
+End Conditions
+
+
+Begin SubModelPart soil1
+  Begin SubModelPartTables
+  End SubModelPartTables
+  Begin SubModelPartNodes
+  1
+  2
+  3
+  4
+  5
+  End SubModelPartNodes
+  Begin SubModelPartElements
+  5
+  6
+  7
+  8
+  End SubModelPartElements
+End SubModelPart
+
+
+Begin SubModelPart load_top
+  Begin SubModelPartTables
+  1
+  End SubModelPartTables
+  Begin SubModelPartNodes
+  3
+  4
+  End SubModelPartNodes
+  Begin SubModelPartConditions
+  3
+  End SubModelPartConditions
+End SubModelPart
+
+
+Begin SubModelPart load_bottom
+  Begin SubModelPartTables
+  1
+  End SubModelPartTables
+  Begin SubModelPartNodes
+  1
+  2
+  End SubModelPartNodes
+  Begin SubModelPartConditions
+  1
+  End SubModelPartConditions
+End SubModelPart
+
+
+Begin SubModelPart load_left
+  Begin SubModelPartTables
+  2
+  End SubModelPartTables
+  Begin SubModelPartNodes
+  1
+  4
+  End SubModelPartNodes
+  Begin SubModelPartConditions
+  4
+  End SubModelPartConditions
+End SubModelPart
+
+
+Begin SubModelPart load_right
+  Begin SubModelPartTables
+  2
+  End SubModelPartTables
+  Begin SubModelPartNodes
+  2
+  3
+  End SubModelPartNodes
+  Begin SubModelPartConditions
+  2
+  End SubModelPartConditions
+End SubModelPart
+
+
+Begin SubModelPart no_displacement
+  Begin SubModelPartTables
+  End SubModelPartTables
+  Begin SubModelPartNodes
+  1
+  2
+  End SubModelPartNodes
+End SubModelPart
+
diff --git a/tests/test_data/expected_mdpa_file_3d.mdpa b/tests/test_data/expected_mdpa_file_3d.mdpa
new file mode 100644
index 00000000..856773cd
--- /dev/null
+++ b/tests/test_data/expected_mdpa_file_3d.mdpa
@@ -0,0 +1,197 @@
+
+Begin Properties 1
+End Properties
+
+
+Begin Properties 2
+End Properties
+
+
+Begin Table 1 TIME VALUE
+   0.0000   0.0000
+   0.5000   -5.0000
+   1.0000   -10.0000
+   1.5000   -5.0000
+   2.0000   0.0000
+   2.5000   0.0000
+End Table
+
+
+Begin Table 2 TIME VALUE
+   0.0000   0.0000
+   0.5000   5.0000
+   1.0000   10.0000
+   1.5000   5.0000
+   2.0000   0.0000
+   2.5000   0.0000
+End Table
+
+
+Begin Nodes
+  1   0.0000000000  0.0000000000  0.0000000000
+  2   1.0000000000  0.0000000000  0.0000000000
+  3   1.0000000000  1.0000000000  0.0000000000
+  4   0.0000000000  1.0000000000  0.0000000000
+  5   0.0000000000  0.0000000000  -1.0000000000
+  6   1.0000000000  0.0000000000  -1.0000000000
+  7   1.0000000000  1.0000000000  -1.0000000000
+  8   0.0000000000  1.0000000000  -1.0000000000
+  9   0.0000000000  0.0000000000  1.0000000000
+  10   1.0000000000  0.0000000000  1.0000000000
+  11   1.0000000000  1.0000000000  1.0000000000
+  12   0.0000000000  1.0000000000  1.0000000000
+  13   0.5000000000  0.5000000000  0.0000000000
+  14   0.5000000000  0.0000000000  -0.5000000000
+  15   1.0000000000  0.5000000000  -0.5000000000
+  16   0.5000000000  1.0000000000  -0.5000000000
+  17   0.0000000000  0.5000000000  -0.5000000000
+  18   0.5000000000  0.5000000000  -1.0000000000
+  19   0.5000000000  0.5000000000  1.0000000000
+End Nodes
+
+
+Begin Elements UPwSmallStrainElement3D4N
+  13  1  16 17 18 15
+  14  1  14 18 17 15
+  15  1  17 16 13 15
+  16  1  14 17 13 15
+  17  1  5 18 8 17
+  18  1  8 17 16 4
+  19  1  13 4 17 1
+  20  1  16 4 13 3
+  21  1  1 5 17 14
+  22  1  2 1 13 14
+  23  1  13 2 15 3
+  24  1  18 8 16 7
+  25  1  3 15 16 7
+  26  1  5 18 14 6
+  27  1  7 15 18 6
+  28  1  2 14 15 6
+  29  1  5 18 17 14
+  30  1  18 8 17 16
+  31  1  17 16 4 13
+  32  1  17 13 1 14
+  33  1  13 2 14 15
+  34  1  15 14 18 6
+  35  1  16 13 15 3
+  36  1  18 16 15 7
+End Elements
+
+
+Begin Conditions UPwFaceLoadCondition3D3N
+  9  1  10 19 9
+  10  1  9 19 12
+  11  1  11 19 10
+  12  1  12 19 11
+End Conditions
+
+
+Begin Conditions UPwFaceLoadCondition3D3N
+  1  2  2 13 1
+  2  2  1 13 4
+  3  2  3 13 2
+  4  2  4 13 3
+End Conditions
+
+
+Begin SubModelPart soil1
+  Begin SubModelPartTables
+  End SubModelPartTables
+  Begin SubModelPartNodes
+  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  13
+  14
+  15
+  16
+  17
+  18
+  End SubModelPartNodes
+  Begin SubModelPartElements
+  13
+  14
+  15
+  16
+  17
+  18
+  19
+  20
+  21
+  22
+  23
+  24
+  25
+  26
+  27
+  28
+  29
+  30
+  31
+  32
+  33
+  34
+  35
+  36
+  End SubModelPartElements
+End SubModelPart
+
+
+Begin SubModelPart load_top
+  Begin SubModelPartTables
+  1
+  End SubModelPartTables
+  Begin SubModelPartNodes
+  9
+  10
+  11
+  12
+  19
+  End SubModelPartNodes
+  Begin SubModelPartConditions
+  9
+  10
+  11
+  12
+  End SubModelPartConditions
+End SubModelPart
+
+
+Begin SubModelPart load_bottom
+  Begin SubModelPartTables
+  2
+  End SubModelPartTables
+  Begin SubModelPartNodes
+  1
+  2
+  3
+  4
+  13
+  End SubModelPartNodes
+  Begin SubModelPartConditions
+  1
+  2
+  3
+  4
+  End SubModelPartConditions
+End SubModelPart
+
+
+Begin SubModelPart no_displacement
+  Begin SubModelPartTables
+  2
+  End SubModelPartTables
+  Begin SubModelPartNodes
+  5
+  6
+  7
+  8
+  18
+  End SubModelPartNodes
+End SubModelPart
+
diff --git a/tests/test_data/expected_output_parameters.json b/tests/test_data/expected_output_parameters.json
index d01b5c59..a488380a 100644
--- a/tests/test_data/expected_output_parameters.json
+++ b/tests/test_data/expected_output_parameters.json
@@ -5,7 +5,7 @@
             "process_name": "GiDOutputProcess",
             "Parameters":    {
                 "model_part_name": "PorousDomain.test_gid_output",
-                "output_name": "dir_test/test_gid1",
+                "output_name": "test_gid1",
                 "postprocess_parameters": {
                     "result_file_configuration": {
                         "gidpost_flags":       {
@@ -55,6 +55,35 @@
                     "point_data_configuration":  []
                 }
             }
+        },
+        {
+            "python_module": "gid_output_process",
+            "kratos_module": "KratosMultiphysics",
+            "process_name": "GiDOutputProcess",
+            "Parameters":    {
+                "model_part_name": "PorousDomain.test_gid_output",
+                "output_name": "dir_test/test_gid3",
+                "postprocess_parameters": {
+                    "result_file_configuration": {
+                        "gidpost_flags":       {
+                            "WriteDeformedMeshFlag": "WriteUndeformed",
+                            "WriteConditionsFlag":   "WriteElementsOnly",
+                            "GiDPostMode":           "GiD_PostHDF5",
+                            "MultiFileFlag":         "SingleFile"
+                        },
+                        "file_label":          "step",
+                        "output_control_type": "step",
+                        "output_interval":     100,
+                        "body_output":         true,
+                        "node_output":         false,
+                        "skin_output":         false,
+                        "plane_output":        [],
+                        "nodal_results":       ["WATER_PRESSURE","VOLUME_ACCELERATION"],
+                        "gauss_point_results": ["GREEN_LAGRANGE_STRAIN_TENSOR","ENGINEERING_STRAIN_TENSOR","CAUCHY_STRESS_TENSOR","TOTAL_STRESS_TENSOR"]
+                    },
+                    "point_data_configuration":  []
+                }
+            }
         }],
         "vtk_output": [{
             "python_module": "vtk_output_process",
@@ -62,7 +91,7 @@
             "process_name": "VtkOutputProcess",
             "Parameters":    {
                 "model_part_name": "PorousDomain.test_vtk_output",
-                "output_path": "dir_test/test_vtk1",
+                "output_path": "output_vtk_test_vtk_output",
                 "file_format" :  "binary",
                 "output_precision" : 8,
                 "output_control_type" :  "step",
@@ -77,7 +106,7 @@
             "process_name": "VtkOutputProcess",
             "Parameters":    {
                 "model_part_name": "PorousDomain.test_vtk_output",
-                "output_path": "dir_test/test_vtk2",
+                "output_path": "test_vtk1/output_vtk_test_vtk_output",
                 "file_format" :  "ascii",
                 "output_precision" : 8,
                 "output_control_type" :  "step",
@@ -95,7 +124,7 @@
             "process_name": "JsonOutputProcess",
             "Parameters": {
                 "model_part_name": "PorousDomain.test_json_output1",
-                "output_file_name": "dir_test/test_json_output1.json",
+                "output_file_name": "test_json_output1.json",
                 "output_variables": [
                     "DISPLACEMENT",
                     "TOTAL_DISPLACEMENT"
diff --git a/tests/test_data/expected_solver_settings.json b/tests/test_data/expected_solver_settings.json
index 0f1c7ada..48a32fbb 100644
--- a/tests/test_data/expected_solver_settings.json
+++ b/tests/test_data/expected_solver_settings.json
@@ -56,7 +56,7 @@
             "solver_type":      "amgcl",
             "scaling":          true,
             "tolerance":        1e-08,
-            "max_iterations":   500
+            "max_iteration":   500
         },
         "problem_domain_sub_model_part_list": [
             "BodyModelPart1"
diff --git a/tests/test_data/gmsh_utils_two_blocks_2D.geo b/tests/test_data/gmsh_utils_two_blocks_2D.geo
new file mode 100644
index 00000000..0a684747
--- /dev/null
+++ b/tests/test_data/gmsh_utils_two_blocks_2D.geo
@@ -0,0 +1,35 @@
+// Gmsh project: created with gmsh-3.0.6-Windows64
+
+// Create 2D square mesh
+Mesh.ElementOrder = 1;
+Point(1) = {0, 0, 0};
+Point(2) = {1, 0, 0};
+Point(3) = {1, 1, 0};
+Point(4) = {0, 1, 0};
+
+// create lines
+Line(1) = {1, 2};
+Line(2) = {2, 3};
+Line(3) = {3, 4};
+Line(4) = {4, 1};
+
+// create surface
+Line Loop(1) = {1, 2, 3, 4};
+Plane Surface(1) = 1;
+
+// create new points of second surface
+Point(5) = {0, 2, 0};
+Point(6) = {1, 2, 0};
+
+// create new lines
+Line(5) = {4, 5};
+Line(6) = {5, 6};
+Line(7) = {6, 3};
+
+// create second surface
+Line Loop(2) = {3, 5, 6, 7};
+Plane Surface(2) = 2;
+
+// Define the physical groups
+Physical Surface("group_1") = 1;
+Physical Surface("group_2") = 2;
diff --git a/tests/test_default_materials.py b/tests/test_default_materials.py
index 0182eabf..3075d138 100644
--- a/tests/test_default_materials.py
+++ b/tests/test_default_materials.py
@@ -2,7 +2,6 @@
 
 from stem.IO.kratos_material_io import KratosMaterialIO
 from stem.default_materials import DefaultMaterial
-
 from tests.utils import TestUtils
 
 
@@ -15,10 +14,13 @@ def test_default_structural_materials(self):
         default_materials = [
             DefaultMaterial.Rail_46E3_3D.value,
             DefaultMaterial.Rail_54E1_3D.value,
-            DefaultMaterial.Rail_60E1_3D.value
+            DefaultMaterial.Rail_60E1_3D.value,
+            DefaultMaterial.Rail_46E3_2D.value,
+            DefaultMaterial.Rail_54E1_2D.value,
+            DefaultMaterial.Rail_60E1_2D.value
         ]
 
-        all_materials = {m_obj.name:m_obj for m_obj in default_materials}
+        all_materials = {m_obj.name: m_obj for m_obj in default_materials}
 
         # write json file
         material_io = KratosMaterialIO(ndim=ndim, domain="PorousDomain")
@@ -40,5 +42,5 @@ def test_default_structural_materials(self):
 
         # compare json files using custom dictionary comparison
         TestUtils.assert_dictionary_almost_equal(
-            test_dict, expected_material_parameters_json
+            expected_material_parameters_json, test_dict
         )
diff --git a/tests/test_geometry.py b/tests/test_geometry.py
index ff6906ab..8281eeb9 100644
--- a/tests/test_geometry.py
+++ b/tests/test_geometry.py
@@ -1,5 +1,6 @@
 import pytest
 from gmsh_utils.gmsh_IO import GmshIO
+import numpy.testing as npt
 
 from stem.geometry import *
 
@@ -12,7 +13,7 @@ def expected_geo_data_0D(self):
         Expected geometry data for a 0D geometry group. The group is a geometry of a point
 
         Returns:
-            - expected_geo_data (Dict[str, Any]): dictionary containing the geometry data as generated by the gmsh_io
+            - Dict[str, Any]: dictionary containing the geometry data as generated by the gmsh_io
         """
         expected_points = {1: [0, 0, 0], 2: [0.5, 0, 0]}
         return {"points": expected_points}
@@ -24,7 +25,7 @@ def expected_geo_data_1D(self):
         Expected geometry data for a 1D geometry group. The group is a geometry of a line
 
         Returns:
-            - expected_geo_data (Dict[str, Any]): dictionary containing the geometry data as generated by the gmsh_io
+            - Dict[str, Any]: dictionary containing the geometry data as generated by the gmsh_io
         """
         expected_points = {4: [0, 1.0, 0], 11: [0, 2.0, 0], 12: [0.5, 2.0, 0]}
         expected_lines = {13: [4, 11], 14: [11, 12]}
@@ -38,7 +39,7 @@ def expected_geo_data_2D(self):
         Expected geometry data for a 2D geometry group. The group is a geometry of a square.
 
         Returns:
-            - expected_geo_data (Dict[str, Any]): dictionary containing the geometry data as generated by the gmsh_io
+            - Dict[str, Any]: dictionary containing the geometry data as generated by the gmsh_io
         """
         expected_points = {3: [0.5, 1, 0], 4: [0, 1, 0], 11: [0, 2, 0], 12: [0.5, 2.0, 0]}
         expected_lines = { 7: [3, 4], 13: [4, 11], 14: [11, 12], 15: [12, 3]}
@@ -54,7 +55,7 @@ def expected_geo_data_3D(self):
         Expected geometry data for a 3D geometry group. The group is a geometry of a cubic block
 
         Returns:
-            - expected_geo_data (Dict[str, Any]): dictionary containing the geometry data as generated by the gmsh_io
+            - Dict[str, Any]: dictionary containing the geometry data as generated by the gmsh_io
         """
         expected_points = {3: [0.5, 1., 0.], 4: [0., 1., 0.], 11: [0., 2., 0.],
                            12: [0.5, 2., 0.],  18: [0.5, 1., -0.5],
@@ -70,12 +71,12 @@ def expected_geo_data_3D(self):
                 "surfaces": expected_surfaces,
                 "volumes": expected_volumes}
 
-    def test_create_0d_geometry_from_gmsh_group(self, expected_geo_data_0D):
+    def test_create_0d_geometry_from_gmsh_group(self, expected_geo_data_0D: Dict[str, Any]):
         """
         Test the creation of a 0D geometry from a gmsh group.
 
         Args:
-            - expected_geo_data_0D (Dict[int, Any]): expected geometry data for a 0D geometry group.
+            - expected_geo_data_0D (Dict[str, Any]): expected geometry data for a 0D geometry group.
 
         """
         # Read the gmsh geo file
@@ -84,14 +85,15 @@ def test_create_0d_geometry_from_gmsh_group(self, expected_geo_data_0D):
         geo_data = gmsh_io.geo_data
 
         # Create the geometry from the gmsh group
-        geometry = Geometry().create_geometry_from_gmsh_group(geo_data, "point_group")
+        geometry = Geometry.create_geometry_from_gmsh_group(geo_data, "point_group")
 
         # Assert that the geometry is created correctly
         assert len(geometry.points) == len(expected_geo_data_0D["points"])
-        for point in geometry.points:
-            assert pytest.approx(point.coordinates) == expected_geo_data_0D["points"][point.id]
+        for point_id, point in geometry.points.items():
+            assert point_id == point.id
+            npt.assert_allclose(point.coordinates, expected_geo_data_0D["points"][point.id])
 
-    def test_create_1d_geometry_from_gmsh_group(self, expected_geo_data_1D):
+    def test_create_1d_geometry_from_gmsh_group(self, expected_geo_data_1D: Dict[str, Any]):
         """
         Test the creation of a 1D geometry from a gmsh group.
 
@@ -106,18 +108,20 @@ def test_create_1d_geometry_from_gmsh_group(self, expected_geo_data_1D):
         geo_data = gmsh_io.geo_data
 
         # Create the geometry from the gmsh group
-        geometry = Geometry().create_geometry_from_gmsh_group(geo_data, "line_group")
+        geometry = Geometry.create_geometry_from_gmsh_group(geo_data, "line_group")
 
         # Assert that the geometry is created correctly
         assert len(geometry.points) == len(expected_geo_data_1D["points"])
-        for point in geometry.points:
-            assert pytest.approx(point.coordinates) == expected_geo_data_1D["points"][point.id]
+        for point_id, point in geometry.points.items():
+            assert point_id == point.id
+            npt.assert_allclose(point.coordinates, expected_geo_data_1D["points"][point.id])
 
         assert len(geometry.lines) == len(expected_geo_data_1D["lines"])
-        for line in geometry.lines:
-            assert line.point_ids == expected_geo_data_1D["lines"][line.id]
+        for line_id, line in geometry.lines.items():
+            assert line_id == line.id
+            npt.assert_equal(line.point_ids, expected_geo_data_1D["lines"][line.id])
 
-    def test_create_2d_geometry_from_gmsh_group(self, expected_geo_data_2D):
+    def test_create_2d_geometry_from_gmsh_group(self, expected_geo_data_2D: Dict[str, Any]):
         """
         Test the creation of a 2D geometry from a gmsh group.
 
@@ -132,28 +136,30 @@ def test_create_2d_geometry_from_gmsh_group(self, expected_geo_data_2D):
         geo_data = gmsh_io.geo_data
 
         # Create the geometry from the gmsh group
-        geometry = Geometry().create_geometry_from_gmsh_group(geo_data, "group_2")
+        geometry = Geometry.create_geometry_from_gmsh_group(geo_data, "group_2")
 
         # Assert that the geometry is created correctly
         assert len(geometry.points) == len(expected_geo_data_2D["points"])
-        for point in geometry.points:
-            assert pytest.approx(point.coordinates) == expected_geo_data_2D["points"][point.id]
+        for point_id, point in geometry.points.items():
+            assert point_id == point.id
+            npt.assert_allclose(point.coordinates, expected_geo_data_2D["points"][point.id])
 
         assert len(geometry.lines) == len(expected_geo_data_2D["lines"])
-        for line in geometry.lines:
-            assert line.point_ids == expected_geo_data_2D["lines"][line.id]
+        for line_id, line in geometry.lines.items():
+            assert line_id == line.id
+            npt.assert_equal(line.point_ids, expected_geo_data_2D["lines"][line.id])
 
         assert len(geometry.surfaces) == len(expected_geo_data_2D["surfaces"])
-        for surface in geometry.surfaces:
-            assert surface.line_ids == expected_geo_data_2D["surfaces"][surface.id]
-
+        for surface_id, surface in geometry.surfaces.items():
+            assert surface_id == surface.id
+            npt.assert_equal(surface.line_ids, expected_geo_data_2D["surfaces"][surface.id])
 
     def test_create_3d_geometry_from_gmsh_group(self, expected_geo_data_3D: Dict[str, Any]):
         """
         Test the creation of a 3D geometry from a gmsh group.
 
         Args:
-            - expected_geo_data_3D (Dict[int, Any]): expected geometry data for a 3D geometry group.
+            - expected_geo_data_3D (Dict[str, Any]): expected geometry data for a 3D geometry group.
 
         """
 
@@ -163,23 +169,60 @@ def test_create_3d_geometry_from_gmsh_group(self, expected_geo_data_3D: Dict[str
         geo_data = gmsh_io.geo_data
 
         # Create the geometry from the gmsh group
-        geometry = Geometry().create_geometry_from_gmsh_group(geo_data, "group_2")
+        geometry = Geometry.create_geometry_from_gmsh_group(geo_data, "group_2")
 
         # Assert that the geometry is created correctly
         assert len(geometry.points) == len(expected_geo_data_3D["points"])
-        for point in geometry.points:
-            assert pytest.approx(point.coordinates) == expected_geo_data_3D["points"][point.id]
+        for point_id, point in geometry.points.items():
+            assert point_id == point.id
+            npt.assert_allclose(point.coordinates, expected_geo_data_3D["points"][point.id])
 
         assert len(geometry.lines) == len(expected_geo_data_3D["lines"])
-        for line in geometry.lines:
-            assert line.point_ids == expected_geo_data_3D["lines"][line.id]
+        for line_id, line in geometry.lines.items():
+            assert line_id == line.id
+            npt.assert_equal(line.point_ids, expected_geo_data_3D["lines"][line.id])
 
         assert len(geometry.surfaces) == len(expected_geo_data_3D["surfaces"])
-        for surface in geometry.surfaces:
-            assert surface.line_ids == expected_geo_data_3D["surfaces"][surface.id]
+        for surface_id, surface in geometry.surfaces.items():
+            assert surface_id == surface.id
+            npt.assert_equal(surface.line_ids, expected_geo_data_3D["surfaces"][surface.id])
 
         assert len(geometry.volumes) == len(expected_geo_data_3D["volumes"])
-        for volume in geometry.volumes:
-            assert volume.surface_ids == expected_geo_data_3D["volumes"][volume.id]
+        for volume_id, volume in geometry.volumes.items():
+            assert volume_id == volume.id
+            npt.assert_equal(volume.surface_ids, expected_geo_data_3D["volumes"][volume.id])
+
+    def test_create_geometry_from_geo_data(self, expected_geo_data_3D: Dict[str, Any]):
+        """
+        Test the creation of a 3D geometry from a geo_data dictionary.
+
+        Args:
+            - expected_geo_data_3D (Dict[str, Any]): expected geometry data for a 3D geometry group.
 
+        """
+
+        geo_data = expected_geo_data_3D
+
+        # Create the geometry from the gmsh group
+        geometry = Geometry.create_geometry_from_geo_data(geo_data)
 
+        # Assert that the geometry is created correctly
+        assert len(geometry.points) == len(expected_geo_data_3D["points"])
+        for point_id, point in geometry.points.items():
+            assert point_id == point.id
+            npt.assert_allclose(point.coordinates, expected_geo_data_3D["points"][point.id])
+
+        assert len(geometry.lines) == len(expected_geo_data_3D["lines"])
+        for line_id, line in geometry.lines.items():
+            assert line_id == line.id
+            npt.assert_equal(line.point_ids, expected_geo_data_3D["lines"][line.id])
+
+        assert len(geometry.surfaces) == len(expected_geo_data_3D["surfaces"])
+        for surface_id, surface in geometry.surfaces.items():
+            assert surface_id == surface.id
+            npt.assert_equal(surface.line_ids, expected_geo_data_3D["surfaces"][surface.id])
+
+        assert len(geometry.volumes) == len(expected_geo_data_3D["volumes"])
+        for volume_id, volume in geometry.volumes.items():
+            assert volume_id == volume.id
+            npt.assert_equal(volume.surface_ids, expected_geo_data_3D["volumes"][volume.id])
diff --git a/tests/test_kratos_additional_processes_io.py b/tests/test_kratos_additional_processes_io.py
index 00da44a8..f08da7b1 100644
--- a/tests/test_kratos_additional_processes_io.py
+++ b/tests/test_kratos_additional_processes_io.py
@@ -1,13 +1,11 @@
 import json
 
-import pytest
-
 from stem.IO.kratos_additional_processes_io import KratosAdditionalProcessesIO
 from stem.additional_processes import *
 from tests.utils import TestUtils
 
 
-class KratosAdditionalProcessesIO:
+class TestKratosAdditionalProcessesIO:
 
     def test_create_additional_processes_dictionaries(self):
         """
@@ -17,7 +15,7 @@ def test_create_additional_processes_dictionaries(self):
         # define constraints
 
         # Absorbing boundaries
-        excavation_parameters = Excavation(deactivate_soil_part=True)
+        excavation_parameters = Excavation(deactivate_body_model_part=True)
 
         # collect the part names and parameters into a dictionary
         # TODO: change later when model part is implemented
diff --git a/tests/test_kratos_boundaries_io.py b/tests/test_kratos_boundaries_io.py
index 83b927cc..66bb07cb 100644
--- a/tests/test_kratos_boundaries_io.py
+++ b/tests/test_kratos_boundaries_io.py
@@ -1,9 +1,11 @@
 import json
 
-import pytest
+import numpy as np
 
-from stem.IO.kratos_boundaries_io import KratosBoundariesIO
+from stem.IO.kratos_io import KratosIO
 from stem.boundary import *
+from stem.model import Model
+from stem.soil_material import OnePhaseSoil, LinearElasticSoil, SoilMaterial, SaturatedBelowPhreaticLevelLaw
 from tests.utils import TestUtils
 
 
@@ -14,20 +16,38 @@ def test_create_boundary_condition_dictionaries(self):
         Test the creation of the boundary condition dictionaries for the
         ProjectParameters.json file
         """
-        # define constraints
+
+        # initialise model
+        model = Model(ndim=2)
+        # define a simple square
+
+        layer_coordinates = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+        # define soil material
+        soil_formulation = OnePhaseSoil(2, IS_DRAINED=True, DENSITY_SOLID=2650, POROSITY=0.3)
+        constitutive_law = LinearElasticSoil(YOUNG_MODULUS=100e6, POISSON_RATIO=0.3)
+        soil_material = SoilMaterial(name="soil", soil_formulation=soil_formulation, constitutive_law=constitutive_law,
+                                     retention_parameters=SaturatedBelowPhreaticLevelLaw())
+        # define tables
+        _time = np.array([0, 1, 2, 3, 4, 5])
+
+        _value1 = np.array([0, 5, 10, 5, 0, 0])
+        table1 = Table(times=_time, values=_value1)
+
+        _value2 = np.array([0, -5, 5, -5, 0, 0])
+        table2 = Table(times=_time, values=_value2)
 
         # Displacements
         fix_displacements_parameters = DisplacementConstraint(
             active=[True, True, False],
             is_fixed=[True, True, False],
-            value=[0.0, 0.0, 0.0],
+            value=[0.0, table1, 0.0],
         )
 
         # Rotations
         fix_rotations_parameters = RotationConstraint(
             active=[False, False, True],
             is_fixed=[False, False, True],
-            value=[0.0, 0.0, 0.0],
+            value=[table2, 0.0, 0.0],
         )
 
         # Absorbing boundaries
@@ -35,38 +55,33 @@ def test_create_boundary_condition_dictionaries(self):
             absorbing_factors=[1.0, 1.0], virtual_thickness=1000.0
         )
 
-        # collect the part names and parameters into a dictionary
-        # TODO: change later when model part is implemented
-        all_boundary_parameters = {
-            "test_displacement_constraint":fix_displacements_parameters,
-            "test_rotation_constraint":fix_rotations_parameters,
-            "test_absorbing_boundaries":absorbing_boundaries_parameters
-        }
-
-        # initialize process dictionary
-        test_dictionary: Dict[str, Any] = {
-            "processes": {"constraints_process_list": [], "loads_process_list": []}
-        }
-
-        # write dictionary for the boundary(/ies)
-        boundaries_io = KratosBoundariesIO(domain="PorousDomain")
-        # TODO: when model part are implemented, generate file through kratos_io
-
-        for part_name, part_parameters in all_boundary_parameters.items():
-            _parameters = boundaries_io.create_boundary_condition_dict(
-                part_name=part_name, parameters=part_parameters
-            )
-            _key = "loads_process_list"
-            if part_parameters.is_constraint:
-                _key = "constraints_process_list"
-            test_dictionary["processes"][_key].append(_parameters)
+        model.project_parameters = TestUtils.create_default_solver_settings()
+
+        # add dummy soil layer
+        model.add_soil_layer_by_coordinates(layer_coordinates, soil_material, "dummy_soil")
+        # add loads to process model parts:
+        model.add_boundary_condition_by_geometry_ids(1, [1], fix_displacements_parameters,
+                                                     'test_displacement_constraint')
+        model.add_boundary_condition_by_geometry_ids(1, [2], fix_rotations_parameters, 'test_rotation_constraint')
+        model.add_boundary_condition_by_geometry_ids(1, [3], absorbing_boundaries_parameters,
+                                                     'test_absorbing_boundaries')
+        model.synchronise_geometry()
+        # write dictionary for the load(s)
+        kratos_io = KratosIO(ndim=model.ndim)
+
+        test_dictionary = kratos_io.write_project_parameters_json(
+            model=model,
+            outputs=[],
+            mesh_file_name="test_load_parameters.mdpa",
+            materials_file_name=""
+        )
 
         # load expected dictionary from the json
-        expected_load_parameters_json = json.load(
+        expected_boundary_parameters_json = json.load(
             open("tests/test_data/expected_boundary_conditions_parameters.json")
         )
 
         # assert the objects to be equal
         TestUtils.assert_dictionary_almost_equal(
-            test_dictionary, expected_load_parameters_json
+            expected_boundary_parameters_json, test_dictionary
         )
\ No newline at end of file
diff --git a/tests/test_kratos_io.py b/tests/test_kratos_io.py
new file mode 100644
index 00000000..7ba313f1
--- /dev/null
+++ b/tests/test_kratos_io.py
@@ -0,0 +1,421 @@
+import json
+import sys
+from typing import List
+
+import numpy as np
+import numpy.testing as npt
+import pytest
+from gmsh_utils import gmsh_IO
+
+from stem.IO.kratos_io import KratosIO
+from stem.boundary import DisplacementConstraint
+from stem.load import LineLoad, SurfaceLoad
+from stem.model import Model
+from stem.model_part import *
+from stem.output import NodalOutput, GaussPointOutput, GiDOutputParameters, Output
+from stem.soil_material import OnePhaseSoil, LinearElasticSoil, SaturatedBelowPhreaticLevelLaw
+from stem.solver import AnalysisType, SolutionType, TimeIntegration, DisplacementConvergenceCriteria, \
+    NewtonRaphsonStrategy, NewmarkScheme, Amgcl, StressInitialisationType, SolverSettings, Problem
+from stem.table import Table
+from tests.utils import TestUtils
+
+IS_LINUX = sys.platform == "linux"
+
+
+class TestKratosModelIO:
+
+    @pytest.fixture(autouse=True)
+    def close_gmsh(self):
+        """
+        Initializer to close gmsh if it was not closed before. In case a test fails, the destroyer method is not called
+        on the Model object and gmsh keeps on running. Therefore, nodes, lines, surfaces and volumes ids are not
+        reset to one. This causes also the next test after the failed one to fail as well, which has nothing to do
+        the test itself.
+
+        Returns:
+            - None
+
+        """
+        gmsh_IO.GmshIO().finalize_gmsh()
+
+    @pytest.fixture
+    def create_default_2d_model_and_mesh(self) -> Model:
+        """
+        Sets expected geometry data for a 2D geometry group. And it sets a time dependent line load at the top and
+        bottom and another line load at the sides. The group is a geometry of a square.
+
+        Returns:
+            - :class:`stem.model.Model`: the default 2D model of a square soil layer and line loads.
+        """
+        ndim = 2
+        layer_coordinates = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+
+        load_coordinates_top = [(1, 1, 0), (0, 1, 0)]  # top
+        load_coordinates_bottom = [(0, 0, 0), (1, 0, 0)]  # bottom
+        load_coordinates_left = [(0, 1, 0), (0, 0, 0)]  # left
+        load_coordinates_right = [(1, 0, 0), (1, 1, 0)]  # right
+        # define soil material
+        soil_formulation = OnePhaseSoil(ndim, IS_DRAINED=True, DENSITY_SOLID=2650, POROSITY=0.3)
+        constitutive_law = LinearElasticSoil(YOUNG_MODULUS=100e6, POISSON_RATIO=0.3)
+        soil_material = SoilMaterial(name="soil", soil_formulation=soil_formulation, constitutive_law=constitutive_law,
+                                     retention_parameters=SaturatedBelowPhreaticLevelLaw())
+        # define tables
+        _time = np.arange(6)*0.5
+        _value1 = np.array([0, 5, 10, 5, 0, 0])
+        table1 = Table(times=_time, values=_value1)
+        table2 = Table(times=_time, values=-_value1)
+
+        # define load properties
+        line_load1 = LineLoad(active=[False, True, False], value=[0, table1, 0])
+        line_load2 = LineLoad(active=[True, False, False], value=[table2, 0, 0])
+
+        # create model
+        model = Model(ndim)
+
+        # add soil layer and line load and mesh them
+        model.add_soil_layer_by_coordinates(layer_coordinates, soil_material, "soil1")
+
+        model.add_load_by_coordinates(load_coordinates_top, line_load1, "load_top")
+        model.add_load_by_coordinates(load_coordinates_bottom, line_load1, "load_bottom")
+        model.add_load_by_coordinates(load_coordinates_left, line_load2, "load_left")
+        model.add_load_by_coordinates(load_coordinates_right, line_load2, "load_right")
+
+        # add pin parameters
+        no_displacement_parameters = DisplacementConstraint(active=[True, True, True], is_fixed=[True, True, True],
+                                                            value=[0, 0, 0])
+
+        # add boundary conditions in 0d, 1d and 2d
+        model.add_boundary_condition_by_geometry_ids(1, [1], no_displacement_parameters, "no_displacement")
+
+        model.synchronise_geometry()
+
+        model.set_mesh_size(1)
+        model.generate_mesh()
+
+        return model
+
+    @pytest.fixture
+    def create_default_3d_model_and_mesh(self):
+        """
+        Sets expected geometry data for a 3D geometry group. It sets a surface load on the bottom and another load
+        at the top. The group is a geometry of a cube.
+
+        Returns:
+            - :class:`stem.model.Model`: the default 3D model of a cube soil and a surface load.
+        """
+        ndim = 3
+        layer_coordinates = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+        load_coordinates_bottom = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+        load_coordinates_top = [(0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)]
+        # define soil material
+        soil_formulation = OnePhaseSoil(ndim, IS_DRAINED=True, DENSITY_SOLID=2650, POROSITY=0.3)
+        constitutive_law = LinearElasticSoil(YOUNG_MODULUS=100e6, POISSON_RATIO=0.3)
+        soil_material = SoilMaterial(name="soil", soil_formulation=soil_formulation, constitutive_law=constitutive_law,
+                                     retention_parameters=SaturatedBelowPhreaticLevelLaw())
+
+        # define tables
+        _time = np.arange(6)*0.5
+        _value1 = np.array([0, 5, 10, 5, 0, 0])
+        table1 = Table(times=_time, values=-_value1)
+        table2 = Table(times=_time, values=_value1)
+
+        # define load properties
+
+        # define load properties
+        surface_load_top = SurfaceLoad(active=[False, True, False], value=[0, table1, 0])
+        surface_load_bottom = SurfaceLoad(active=[False, True, False], value=[0, table2, 0])
+
+        # create model
+        model = Model(ndim)
+        model.extrusion_length = [0, 0, -1]
+
+        # add soil layer and line load and mesh them
+        model.add_soil_layer_by_coordinates(layer_coordinates, soil_material, "soil1")
+        model.add_load_by_coordinates(load_coordinates_top, surface_load_top, "load_top")
+        model.add_load_by_coordinates(load_coordinates_bottom, surface_load_bottom, "load_bottom")
+
+        # add pin parameters
+        no_displacement_parameters = DisplacementConstraint(active=[True, True, True], is_fixed=[True, True, True],
+                                                            value=[0, table2, 0])
+
+        # add boundary conditions in 0d, 1d and 2d
+        model.add_boundary_condition_by_geometry_ids(2, [6], no_displacement_parameters, "no_displacement")
+
+        model.synchronise_geometry()
+
+        model.set_mesh_size(1)
+        model.generate_mesh()
+
+        return model
+
+    @pytest.fixture
+    def create_default_outputs(self):
+        """
+        Sets default output parameters.
+
+        Returns:
+            - List[:class:`stem.output.Output`]: list of default output processes.
+        """
+        # Nodal results
+        nodal_results = [NodalOutput.DISPLACEMENT]
+        # Gauss point results
+        gauss_point_results = [
+            GaussPointOutput.GREEN_LAGRANGE_STRAIN_TENSOR,
+            GaussPointOutput.CAUCHY_STRESS_TENSOR
+        ]
+        # define output process
+
+        gid_output_process = Output(
+            part_name="soil1",
+            output_name="gid_output_soil1",
+            output_dir="dir_test",
+            output_parameters=GiDOutputParameters(
+                file_format="binary",
+                output_interval=100,
+                nodal_results=nodal_results,
+                gauss_point_results=gauss_point_results,
+            )
+        )
+
+        return [gid_output_process]
+
+    @pytest.fixture
+    def create_default_solver_settings(self):
+        """
+        Sets default output parameters.
+
+        Returns:
+            - :class:`stem.solver.Problem`: the Problem object containing the solver settings.
+        """
+
+        # set up solver settings
+        analysis_type = AnalysisType.MECHANICAL_GROUNDWATER_FLOW
+        solution_type = SolutionType.DYNAMIC
+        time_integration = TimeIntegration(start_time=0.0, end_time=1.0, delta_time=0.1, reduction_factor=0.5,
+                                           increase_factor=2.0, max_delta_time_factor=500)
+        convergence_criterion = DisplacementConvergenceCriteria(displacement_relative_tolerance=1e-5,
+                                                                displacement_absolute_tolerance=1e-7)
+        strategy_type = NewtonRaphsonStrategy(min_iterations=5, max_iterations=30, number_cycles=50)
+        scheme_type = NewmarkScheme(newmark_beta=0.35, newmark_gamma=0.4, newmark_theta=0.6)
+        linear_solver_settings = Amgcl(tolerance=1e-8, max_iteration=500, scaling=False)
+        stress_initialisation_type = StressInitialisationType.NONE
+        solver_settings = SolverSettings(analysis_type=analysis_type, solution_type=solution_type,
+                                         stress_initialisation_type=stress_initialisation_type,
+                                         time_integration=time_integration,
+                                         is_stiffness_matrix_constant=True, are_mass_and_damping_constant=True,
+                                         convergence_criteria=convergence_criterion,
+                                         strategy_type=strategy_type, scheme=scheme_type,
+                                         linear_solver_settings=linear_solver_settings, rayleigh_k=0.001,
+                                         rayleigh_m=0.001)
+
+        # set up problem data
+        return Problem(problem_name="test", number_of_threads=2, settings=solver_settings)
+
+    def test_write_project_parameters_json(
+        self,
+        create_default_2d_model_and_mesh:Model,
+        create_default_outputs:List[Output],
+        create_default_solver_settings:Problem
+    ):
+        """
+        Test correct writing of the project parameters for the default output, model and settings.
+
+        Args:
+            - create_default_2d_model_and_mesh (:class:`stem.model.Model`): the default 2D model of a square \
+                soil layer and a line load.
+            - create_default_outputs (List[:class:`stem.output.Output`]): list of default output processes.
+            - create_default_solver_settings (:class:`stem.solver.Problem`): the Problem object containing the \
+                solver settings.
+        """
+        model = create_default_2d_model_and_mesh
+        kratos_io = KratosIO(ndim=model.ndim)
+        model.project_parameters = create_default_solver_settings
+
+        actual_dict = kratos_io.write_project_parameters_json(
+            model=model,
+            outputs=create_default_outputs,
+            mesh_file_name="test_mdpa_file.mdpa",
+            materials_file_name="MaterialParameters.json",
+            output_folder="dir_test"
+        )
+        expected_dict = json.load(open("tests/test_data/expected_ProjectParameters.json", 'r'))
+        TestUtils.assert_dictionary_almost_equal(expected_dict, actual_dict)
+
+    def test_write_material_parameters_json(
+        self,
+        create_default_2d_model_and_mesh:Model
+    ):
+        """
+        Test correct writing of the material parameters for the default model.
+        Args:
+            - create_default_2d_model_and_mesh (:class:`stem.model.Model`): the default 2D model of a square \
+                soil layer and a line load.
+        """
+        model = create_default_2d_model_and_mesh
+        kratos_io = KratosIO(ndim=model.ndim)
+
+        actual_dict = kratos_io.write_material_parameters_json(model=model, output_folder="dir_test")
+        expected_dict = json.load(open("tests/test_data/expected_MaterialParameters.json", 'r'))
+        TestUtils.assert_dictionary_almost_equal(expected_dict, actual_dict)
+
+    def test_write_mdpa_file_2d(self, create_default_2d_model_and_mesh: Model, create_default_solver_settings: Problem):
+        """
+        Test correct writing of the mdpa file (mesh) for the default model and solver settings in 2D.
+
+        Args:
+            - create_default_2d_model_and_mesh (:class:`stem.model.Model`): the default 2D model of a square \
+                soil layer and a line load.
+            - create_default_solver_settings (:class:`stem.solver.Problem`): the Problem object containing the \
+                solver settings.
+        """
+        model = create_default_2d_model_and_mesh
+        kratos_io = KratosIO(ndim=model.ndim)
+        model.project_parameters = create_default_solver_settings
+
+        actual_text = kratos_io.write_mesh_to_mdpa(
+            model=model,
+            mesh_file_name="test_mdpa_file.mdpa",
+            output_folder="dir_test"
+        )
+        with open('tests/test_data/expected_mdpa_file.mdpa', 'r') as openfile:
+            expected_text = openfile.readlines()
+
+        npt.assert_equal(actual=actual_text, desired=expected_text)
+
+    @pytest.mark.skipif(IS_LINUX, reason="Linux provides different meshing order. The test is not run on Ubuntu")
+    def test_write_mdpa_file_3d(self, create_default_3d_model_and_mesh: Model, create_default_solver_settings: Problem):
+        """
+        Test correct writing of the mdpa file (mesh) for the default model and solver settings in 3D.
+
+        Args:
+            - create_default_3d_model_and_mesh (:class:`stem.model.Model`): the default 3D model of a cube \
+                soil and a surface load on top.
+            - create_default_solver_settings (:class:`stem.solver.Problem`): the Problem object containing the \
+                solver settings.
+        """
+        model = create_default_3d_model_and_mesh
+        kratos_io = KratosIO(ndim=model.ndim)
+        model.project_parameters = create_default_solver_settings
+
+        actual_text = kratos_io.write_mesh_to_mdpa(
+            model=model,
+            mesh_file_name="test_mdpa_file_3d.mdpa",
+            output_folder="dir_test"
+        )
+        with open('tests/test_data/expected_mdpa_file_3d.mdpa', 'r') as openfile:
+            expected_text = openfile.readlines()
+
+        npt.assert_equal(actual=actual_text, desired=expected_text)
+
+    def test_write_input_files_for_kratos(self, create_default_2d_model_and_mesh: Model,
+                                          create_default_solver_settings: Problem,
+                                          create_default_outputs: List[Output]):
+        """
+        Test correct writing of the mdpa file (mesh) for the default model and solver settings.
+
+        Args:
+            - create_default_2d_model_and_mesh (:class:`stem.model.Model`): the default 2D model of a square \
+                soil layer line loads.
+            - create_default_solver_settings (:class:`stem.solver.Problem`): the Problem object containing the \
+                solver settings.
+            - create_default_outputs (List[:class:`stem.output.Output`]): list of default output processes.
+        """
+        model = create_default_2d_model_and_mesh
+        kratos_io = KratosIO(ndim=model.ndim)
+        model.project_parameters = create_default_solver_settings
+
+        kratos_io.write_input_files_for_kratos(
+            model=model,
+            outputs=create_default_outputs,
+            mesh_file_name="test_mdpa_file.mdpa"
+        )
+
+        # test mdpa
+        with open('./test_mdpa_file.mdpa', 'r') as openfile:
+            actual_text = openfile.readlines()
+        with open('tests/test_data/expected_mdpa_file.mdpa', 'r') as openfile:
+            expected_text = openfile.readlines()
+        npt.assert_equal(actual=actual_text, desired=expected_text)
+
+        # test json material
+        actual_dict = json.load(open('./MaterialParameters.json', 'r'))
+        expected_dict = json.load(open('tests/test_data/expected_MaterialParameters.json', 'r'))
+        TestUtils.assert_dictionary_almost_equal(expected=expected_dict, actual=actual_dict)
+
+        # test json project
+        actual_dict = json.load(open('./ProjectParameters.json', 'r'))
+        expected_dict = json.load(open('tests/test_data/expected_ProjectParameters.json', 'r'))
+        TestUtils.assert_dictionary_almost_equal(expected=expected_dict, actual=actual_dict)
+
+    def test_create_submodelpart_text(self, create_default_2d_model_and_mesh:Model):
+        """
+        Test the creation of the mdpa text of a model part
+
+        Args:
+            - create_default_2d_model_and_mesh (:class:`stem.model.Model`): the default model to use in testing
+        """
+        # load the default 2D model
+        model = create_default_2d_model_and_mesh
+
+        body_model_part_to_write = model.body_model_parts[0]
+        process_model_part_to_write = model.process_model_parts[0]
+
+        # IO object
+        kratos_io = KratosIO(ndim=model.ndim)
+
+        # initialise ids for table, materials and processes
+        kratos_io.initialise_model_ids(model)
+
+        # generate text block body model part: soil1
+        actual_text_body = kratos_io.write_submodelpart_body_model_part(
+            body_model_part=body_model_part_to_write
+        )
+        # define expected block text
+        expected_text_body = ['', 'Begin SubModelPart soil1', '  Begin SubModelPartTables', '  End SubModelPartTables',
+                              '  Begin SubModelPartNodes', '  1', '  2', '  3', '  4', '  5', '  End SubModelPartNodes',
+                              '  Begin SubModelPartElements', '  5', '  6', '  7', '  8',
+                              '  End SubModelPartElements', 'End SubModelPart', '']
+        # assert the objects to be equal
+        npt.assert_equal(actual=actual_text_body, desired=expected_text_body)
+
+        # generate text block body model part: soil1
+        actual_text_load = kratos_io.write_submodelpart_process_model_part(
+            process_model_part=process_model_part_to_write
+        )
+        # define expected block text
+        expected_text_load = ['', 'Begin SubModelPart load_top', '  Begin SubModelPartTables', '  1',
+                              '  End SubModelPartTables', '  Begin SubModelPartNodes', '  3', '  4',
+                              '  End SubModelPartNodes', '  Begin SubModelPartConditions', '  3',
+                              '  End SubModelPartConditions', 'End SubModelPart', '']
+
+        # assert the objects to be equal
+        npt.assert_equal(actual=actual_text_load, desired=expected_text_load)
+
+    def test_write_mdpa_text(self, create_default_2d_model_and_mesh: Model):
+        """
+        Test the creation of the mdpa text of the whole model
+
+        Args:
+            - create_default_2d_model_and_mesh (:class:`stem.model.Model`): the default model to use in testing
+
+        """
+        # load the default 2D model
+        model = create_default_2d_model_and_mesh
+        model.project_parameters = TestUtils.create_default_solver_settings()
+
+        # IO object
+        kratos_io = KratosIO(ndim=model.ndim)
+
+        # initialise ids for table, materials and processes
+        kratos_io.initialise_model_ids(model)
+
+        # generate text block body model part: soil1
+        actual_text_body = kratos_io.write_mdpa_text(model=model)
+
+        # define expected block text
+        with open('tests/test_data/expected_mdpa_file.mdpa', 'r') as openfile:
+            expected_text_body = openfile.readlines()
+
+        expected_text_body = [line.rstrip() for line in expected_text_body]
+        # assert the objects to be equal
+        npt.assert_equal(actual=actual_text_body, desired=expected_text_body)
\ No newline at end of file
diff --git a/tests/test_kratos_loads_io.py b/tests/test_kratos_loads_io.py
index f2d03889..d2c6c86f 100644
--- a/tests/test_kratos_loads_io.py
+++ b/tests/test_kratos_loads_io.py
@@ -1,17 +1,28 @@
 import json
 
+import numpy as np
+
 from stem.IO.kratos_loads_io import KratosLoadsIO
+from stem.model import Model
 from stem.load import *
-
 from tests.utils import TestUtils
 
 
 class TestKratosLoadsIO:
-    def test_create_load_process_dict(self):
+    def test_create_load_process_dict_no_tables(self):
         """
         Test the creation of the load process dictionary for the
-        ProjectParameters.json file
+        ProjectParameters.json file excluding tables.
         """
+
+        # initialise model
+        model = Model(ndim=3)
+        # define points of the load geometry
+        point_load_coords = [(0, 0, 0)]
+        line_load_coords = [(0, 0, 0), (0, 1, 0)]
+        moving_load_coords = [(1, 0, 0), (1, 0.4, 0)]
+        surface_load_coords = [(0, 0, 1), (0, 1, 1), (1, 1, 1), (1, 0, 1)]
+
         # define load(s) parameters
         # point load
         point_load_parameters = PointLoad(
@@ -23,51 +34,112 @@ def test_create_load_process_dict(self):
         surface_load_parameters = SurfaceLoad(
             active=[False, False, True], value=[0, 0, 500]
         )
-        # gravity load
-        gravity_load_parameters = GravityLoad(
-            active=[False, True, False], value=[0, -9.81, 0]
-        )
         # moving (point) load
         moving_point_load_parameters = MovingLoad(
-            origin=[0.0, 1.0, 2.0],
+            origin=[1.0, 0.1, 0.0],
             load=[0.0, -10.0, 0.0],
             direction=[1.0, 0.0, -1.0],
             velocity=5.0,
             offset=3.0,
         )
 
-        # collect the part names and parameters into a dictionary
-        # TODO: change later when model part is implemented
-        all_boundary_parameters = {
-            "test_point_load": point_load_parameters,
-            "test_line_load": line_load_parameters,
-            "test_surface_load": surface_load_parameters,
-            "test_gravity_load" : gravity_load_parameters,
-            "test_moving_load": moving_point_load_parameters,
+        # add loads to process model parts:
+        model.add_load_by_coordinates(point_load_coords, point_load_parameters, 'test_point_load')
+        model.add_load_by_coordinates(line_load_coords, line_load_parameters, 'test_line_load')
+        model.add_load_by_coordinates(surface_load_coords, surface_load_parameters, 'test_surface_load')
+        model.add_load_by_coordinates(moving_load_coords, moving_point_load_parameters, 'test_moving_load')
+        model.synchronise_geometry()
+
+        # create load process dictionary
+        kratos_loads_io = KratosLoadsIO(domain="PorousDomain")
+
+        loads_processes = [kratos_loads_io.create_load_dict("test_point_load", point_load_parameters),
+                           kratos_loads_io.create_load_dict("test_line_load", line_load_parameters),
+                           kratos_loads_io.create_load_dict("test_surface_load", surface_load_parameters),
+                           kratos_loads_io.create_load_dict("test_moving_load", moving_point_load_parameters)]
+
+        test_dictionary = {"loads_process_list": loads_processes,
+                            "constraints_process_list": []}
+
+        # load expected dictionary from the json
+        expected_load_parameters_json = json.load(
+            open("tests/test_data/expected_load_parameters_no_table.json")
+        )
+
+        # assert the objects to be equal
+        TestUtils.assert_dictionary_almost_equal(
+            expected_load_parameters_json["processes"], test_dictionary
+        )
+
+    def test_create_load_process_dict_with_tables(self):
+        """
+        Test the creation of the load process dictionary for the
+        ProjectParameters.json file including tables.
+        """
+
+        # initialise model
+        model = Model(ndim=3)
+        # define points of the load geometry
+        point_load_coords = [(0, 0, 0)]
+        line_load_coords = [(0, 0, 0), (0, 1, 0)]
+        moving_load_coords = [(1, 0, 0), (1, 0.4, 0)]
+        surface_load_coords = [(0, 0, 1), (0, 1, 1), (1, 1, 1), (1, 0, 1)]
+
+        # define tables
+        _time = np.array([0, 1, 2, 3, 4, 5])
+
+        _value1 = np.array([0, 5, 10, 5, 0, 0])
+        table1 = Table(times=_time, values=_value1)
+        table1.id = 1
+
+        _value2 = np.array([0, -5, 5, -5, 0, 0])
+        table2 = Table(times=_time, values=_value2)
+        table2.id = 2
+
+        # define load(s) parameters
+        # point load
+        point_load_parameters = PointLoad(
+            active=[True, False, True], value=[table1, -20, 0]
+        )
+        # line load
+        line_load_parameters = LineLoad(active=[False, True, False], value=[-10, table2, 30])
+        # surface load
+        surface_load_parameters = SurfaceLoad(
+            active=[False, False, True], value=[0, 0, -200]
+        )
+        # moving (point) load
+        moving_point_load_parameters = MovingLoad(
+            origin=[1.0, 0.1, 0.0],
+            load=[0.0, -10.0, 0.0],
+            direction=[1.0, 0.0, -1.0],
+            velocity=5.0,
+            offset=3.0,
+        )
 
-        }
+        # add loads to process model parts:
+        model.add_load_by_coordinates(point_load_coords, point_load_parameters, 'test_point_load')
+        model.add_load_by_coordinates(line_load_coords, line_load_parameters, 'test_line_load')
+        model.add_load_by_coordinates(surface_load_coords, surface_load_parameters, 'test_surface_load')
+        model.add_load_by_coordinates(moving_load_coords, moving_point_load_parameters, 'test_moving_load')
+        model.synchronise_geometry()
 
-        # initialize process dictionary
-        test_dictionary: Dict[str, Any] = {
-            "processes": {"constraints_process_list": [], "loads_process_list": []}
-        }
+        # create load process dictionary
+        kratos_loads_io = KratosLoadsIO(domain="PorousDomain")
 
-        # write dictionary for the load(s)
-        # TODO: when model part are implemented, generate file through kratos_io
-        boundaries_io = KratosLoadsIO(domain="PorousDomain")
+        loads_processes = [kratos_loads_io.create_load_dict("test_point_load", point_load_parameters),
+                           kratos_loads_io.create_load_dict("test_line_load", line_load_parameters),
+                           kratos_loads_io.create_load_dict("test_surface_load", surface_load_parameters),
+                           kratos_loads_io.create_load_dict("test_moving_load", moving_point_load_parameters)]
 
-        for part_name, part_parameters in all_boundary_parameters.items():
-            _parameters = boundaries_io.create_load_dict(
-                part_name=part_name, parameters=part_parameters
-            )
-            test_dictionary["processes"]["loads_process_list"].append(_parameters)
+        test_dictionary = {"loads_process_list": loads_processes,
+                           "constraints_process_list": []}
 
         # load expected dictionary from the json
         expected_load_parameters_json = json.load(
-            open("tests/test_data/expected_load_parameters.json")
+            open("tests/test_data/expected_load_parameters_with_table.json")
         )
 
         # assert the objects to be equal
         TestUtils.assert_dictionary_almost_equal(
-            test_dictionary, expected_load_parameters_json
+            expected_load_parameters_json["processes"], test_dictionary
         )
diff --git a/tests/test_kratos_material_io.py b/tests/test_kratos_material_io.py
index 4697c80c..442d150e 100644
--- a/tests/test_kratos_material_io.py
+++ b/tests/test_kratos_material_io.py
@@ -142,7 +142,7 @@ def test_write_soil_material_dict(self):
 
         # compare json files using custom dictionary comparison
         TestUtils.assert_dictionary_almost_equal(
-            test_dict, expected_material_parameters_json
+            expected_material_parameters_json, test_dict
         )
 
     def test_write_structural_material_dict(self):
@@ -216,5 +216,5 @@ def test_write_structural_material_dict(self):
 
         # compare json files using custom dictionary comparison
         TestUtils.assert_dictionary_almost_equal(
-            test_dict, expected_material_parameters_json
+            expected_material_parameters_json, test_dict
         )
diff --git a/tests/test_kratos_outputs_io.py b/tests/test_kratos_outputs_io.py
index bf238651..9c5c9440 100644
--- a/tests/test_kratos_outputs_io.py
+++ b/tests/test_kratos_outputs_io.py
@@ -1,9 +1,5 @@
 import json
 
-import shutil
-
-import pytest
-
 from stem.IO.kratos_output_io import KratosOutputsIO
 from stem.output import *
 from tests.utils import TestUtils
@@ -15,9 +11,11 @@ def test_create_output_process_dictionary(self):
         Test the creation of the output process dictionary for the
         ProjectParameters.json file
         """
+
         # Nodal results
         nodal_results1 = [NodalOutput.DISPLACEMENT, NodalOutput.TOTAL_DISPLACEMENT]
         nodal_results2 = [NodalOutput.WATER_PRESSURE, NodalOutput.VOLUME_ACCELERATION]
+
         # Gauss point results
         gauss_point_results1 = [
             GaussPointOutput.VON_MISES_STRESS,
@@ -46,6 +44,13 @@ def test_create_output_process_dictionary(self):
             gauss_point_results=gauss_point_results2,
         )
 
+        gid_output_parameters3 = GiDOutputParameters(
+            file_format="hdf5",
+            output_interval=100,
+            nodal_results=nodal_results2,
+            gauss_point_results=gauss_point_results2,
+        )
+
         # 2. Vtk (Paraview)
         vtk_output_parameters1 = VtkOutputParameters(
             file_format="binary",
@@ -77,7 +82,6 @@ def test_create_output_process_dictionary(self):
         # create Load objects and store in the list
         gid_output_process1 = Output(
             part_name="test_gid_output",
-            output_dir=r"dir_test",
             output_name=r"test_gid1",
             output_parameters=gid_output_parameters1,
         )
@@ -87,21 +91,25 @@ def test_create_output_process_dictionary(self):
             output_name=r"test_gid2",
             output_parameters=gid_output_parameters2,
         )
+        gid_output_process3 = Output(
+            part_name="test_gid_output",
+            output_dir=r"dir_test",
+            output_name=r"test_gid3",
+            output_parameters=gid_output_parameters3,
+        )
         vtk_output_process1 = Output(
             part_name="test_vtk_output",
-            output_dir=r"dir_test\test_vtk1",
             output_parameters=vtk_output_parameters1,
         )
         vtk_output_process2 = Output(
             part_name="test_vtk_output",
-            output_dir=r"dir_test\test_vtk2",
+            output_dir=r"test_vtk1",
             output_parameters=vtk_output_parameters2,
         )
 
         json_output_process1 = Output(
             part_name="test_json_output1",
             output_name="test_json_output1",
-            output_dir="dir_test",
             output_parameters=json_output_parameters1,
         )
 
@@ -118,6 +126,7 @@ def test_create_output_process_dictionary(self):
             gid_output_process2,
             vtk_output_process2,
             json_output_process2,
+            gid_output_process3
         ]
 
         # write dictionary for the output(s)
@@ -131,5 +140,5 @@ def test_create_output_process_dictionary(self):
 
         # assert the objects to be equal
         TestUtils.assert_dictionary_almost_equal(
-            test_output, expected_load_parameters_json
+            expected_load_parameters_json, test_output
         )
diff --git a/tests/test_kratos_solver_io.py b/tests/test_kratos_solver_io.py
index e0987247..f3a48155 100644
--- a/tests/test_kratos_solver_io.py
+++ b/tests/test_kratos_solver_io.py
@@ -1,9 +1,8 @@
 import json
 
 from stem.IO.kratos_solver_io import KratosSolverIO
-from stem.solver import *
 from stem.model_part import ModelPart, BodyModelPart
-
+from stem.solver import *
 from tests.utils import TestUtils
 
 
@@ -31,7 +30,7 @@ def test_create_settings_dictionary(self):
 
         scheme_type = NewmarkScheme(newmark_beta=0.35, newmark_gamma=0.4, newmark_theta=0.6)
 
-        linear_solver_settings = Amgcl(tolerance=1e-8, max_iterations=500, scaling=True)
+        linear_solver_settings = Amgcl(tolerance=1e-8, max_iteration=500, scaling=True)
 
         stress_initialisation_type = StressInitialisationType.NONE
 
@@ -48,11 +47,9 @@ def test_create_settings_dictionary(self):
         problem_data = Problem(problem_name="test", number_of_threads=2, settings=solver_settings)
 
         # create model parts
-        model_part1 = ModelPart()
-        model_part1.name = "ModelPart1"
+        model_part1 = ModelPart("ModelPart1")
 
-        body_model_part1 = BodyModelPart()
-        body_model_part1.name = "BodyModelPart1"
+        body_model_part1 = BodyModelPart("BodyModelPart1")
 
         model_parts = [model_part1, body_model_part1]
 
@@ -69,6 +66,33 @@ def test_create_settings_dictionary(self):
         # assert that the settings dictionary is as expected
         TestUtils.assert_dictionary_almost_equal(expected_solver_settings, test_dict)
 
+        # check variants of the settings dictionary
+        # 1. analysis type = MECHANICAL
+        problem_data.settings.analysis_type = AnalysisType.MECHANICAL
+        test_dict = solver_io.create_settings_dictionary(problem_data, "mesh_test_name", "material_test_name.json",
+                                                         model_parts)
+
+        assert test_dict["solver_settings"]["solver_type"] == "U_Pw"
+
+        # 2. analysis type = GROUNDWATER_FLOW
+        problem_data.settings.analysis_type = AnalysisType.GROUNDWATER_FLOW
+        test_dict = solver_io.create_settings_dictionary(problem_data, "mesh_test_name", "material_test_name.json",
+                                                         model_parts)
+
+        assert test_dict["solver_settings"]["solver_type"] == "Pw"
+
+        # 3. solution type = STATIC
+        problem_data.settings.solution_type = SolutionType.QUASI_STATIC
+
+        test_dict = solver_io.create_settings_dictionary(problem_data, "mesh_test_name", "material_test_name.json",
+                                                         model_parts)
+
+        assert test_dict["solver_settings"]["solution_type"] == "quasi_static"
+
+
+
+
+
 
 
 
diff --git a/tests/test_mesh.py b/tests/test_mesh.py
new file mode 100644
index 00000000..fd0f98fe
--- /dev/null
+++ b/tests/test_mesh.py
@@ -0,0 +1,229 @@
+import pytest
+
+from stem.mesh import *
+
+
+class TestMesh:
+
+    def test_create_0d_mesh_from_gmsh_group(self):
+        """
+        Test the creation of a 0D mesh from a gmsh group.
+
+        """
+
+        # Set up the mesh data
+        mesh_data = {"ndim": 0,
+                     "nodes": {1: [0, 0, 0], 2: [0.5, 0, 0]},
+                     "elements": {"POINT_1N": {1: [1], 2: [2]}},
+                     "physical_groups": {"points_group": {"ndim": 0,
+                                                          'element_ids': [1, 2],
+                                                          "node_ids": [1, 2],
+                                                          "element_type": "POINT_1N"}}}
+
+        # Create the mesh from the gmsh group
+        generated_mesh = Mesh.create_mesh_from_gmsh_group(mesh_data, "points_group")
+
+        # set expected mesh
+        expected_nodes = [Node(1, [0, 0, 0]), Node(2, [0.5, 0, 0])]
+        expected_elements = [Element(1, "POINT_1N", [1]), Element(2, "POINT_1N", [2])]
+        expected_mesh = Mesh(0)
+        expected_mesh.nodes = expected_nodes
+        expected_mesh.elements = expected_elements
+
+        # Check the generated mesh
+        assert generated_mesh.ndim == expected_mesh.ndim
+
+        # Check the nodes
+        for generated_node, expected_node in zip(generated_mesh.nodes, expected_mesh.nodes):
+            assert generated_node.id == expected_node.id
+            assert pytest.approx(generated_node.coordinates) == expected_node.coordinates
+
+        # Check the elements
+        for generated_element, expected_element in zip(generated_mesh.elements, expected_mesh.elements):
+            assert generated_element.id == expected_element.id
+            assert generated_element.element_type == expected_element.element_type
+            assert generated_element.node_ids == expected_element.node_ids
+
+    def test_create_1d_mesh_from_gmsh_group(self):
+        """
+        Test the creation of a 1D mesh from a gmsh group.
+
+        """
+
+        # Set up the mesh data
+        mesh_data = {"ndim": 1,
+                     "nodes": {1: [0, 0, 0], 2: [0.5, 0, 0], 3: [1, 0, 0]},
+                     "elements": {"LINE_2N": {1: [1, 2], 2: [2, 3]}},
+                     "physical_groups": {"lines_group": {"ndim": 1,
+                                                         'element_ids': [1, 2],
+                                                         "node_ids": [1, 2, 3],
+                                                         "element_type": "LINE_2N"}}}
+
+        # Create the mesh from the gmsh group
+        generated_mesh = Mesh.create_mesh_from_gmsh_group(mesh_data, "lines_group")
+
+        # set expected mesh
+        expected_nodes = [Node(1, [0, 0, 0]), Node(2, [0.5, 0, 0]), Node(3, [1, 0, 0])]
+        expected_elements = [Element(1, "LINE_2N", [1, 2]), Element(2, "LINE_2N", [2, 3])]
+        expected_mesh = Mesh(1)
+        expected_mesh.nodes = expected_nodes
+        expected_mesh.elements = expected_elements
+
+        # Check the generated mesh
+        assert generated_mesh.ndim == expected_mesh.ndim
+
+        # Check the nodes
+        for generated_node, expected_node in zip(generated_mesh.nodes, expected_mesh.nodes):
+            assert generated_node.id == expected_node.id
+            assert pytest.approx(generated_node.coordinates) == expected_node.coordinates
+
+        # Check the elements
+        for generated_element, expected_element in zip(generated_mesh.elements, expected_mesh.elements):
+            assert generated_element.id == expected_element.id
+            assert generated_element.element_type == expected_element.element_type
+            assert generated_element.node_ids == expected_element.node_ids
+
+    def test_create_2d_mesh_from_gmsh_group(self):
+        """
+        Test the creation of a 2D mesh from a gmsh group.
+
+        """
+
+        # Set up the mesh data
+        mesh_data = {"ndim": 2,
+                     "nodes": {1: [0, 0, 0], 2: [0.5, 0, 0], 3: [1, 0, 0],
+                               4: [0, 0.5, 0], 5: [0.5, 0.5, 0], 6: [1, 0.5, 0]},
+                     "elements": {"TRIANGLE_3N": {1: [1, 2, 4], 2: [2, 3, 5], 3: [3, 6, 5]}},
+                     "physical_groups": {"triangles_group": {"ndim": 2,
+                                                             'element_ids': [1, 2, 3],
+                                                             "node_ids": [1, 2, 3, 4, 5, 6],
+                                                             "element_type": "TRIANGLE_3N"}}}
+
+        # Create the mesh from the gmsh group
+        generated_mesh = Mesh.create_mesh_from_gmsh_group(mesh_data, "triangles_group")
+
+        # set expected mesh
+        expected_nodes = [Node(1, [0, 0, 0]), Node(2, [0.5, 0, 0]), Node(3, [1, 0, 0]),
+                          Node(4, [0, 0.5, 0]), Node(5, [0.5, 0.5, 0]), Node(6, [1, 0.5, 0])]
+        expected_elements = [Element(1, "TRIANGLE_3N", [1, 2, 4]),
+                             Element(2, "TRIANGLE_3N", [2, 3, 5]),
+                             Element(3, "TRIANGLE_3N", [3, 6, 5])]
+
+        expected_mesh = Mesh(2)
+        expected_mesh.nodes = expected_nodes
+        expected_mesh.elements = expected_elements
+
+        # Check the generated mesh
+        assert generated_mesh.ndim == expected_mesh.ndim
+
+        # Check the nodes
+        for generated_node, expected_node in zip(generated_mesh.nodes, expected_mesh.nodes):
+            assert generated_node.id == expected_node.id
+            assert pytest.approx(generated_node.coordinates) == expected_node.coordinates
+
+        # Check the elements
+        for generated_element, expected_element in zip(generated_mesh.elements, expected_mesh.elements):
+            assert generated_element.id == expected_element.id
+            assert generated_element.element_type == expected_element.element_type
+            assert generated_element.node_ids == expected_element.node_ids
+
+    def test_create_3d_mesh_from_gmsh_group(self):
+        """
+        Test the creation of a 3D mesh from a gmsh group.
+
+        """
+
+        # Set up the mesh data
+        mesh_data = {"ndim": 3,
+                     "nodes": {1: [0, 0, 0], 2: [0.5, 0, 0], 3: [1, 0, 0],
+                               4: [0, 0.5, 0], 5: [0.5, 0.5, 0], 6: [1, 0.5, 0],
+                               7: [0, 0, 0.5], 8: [0.5, 0, 0.5], 9: [1, 0, 0.5],
+                               10: [0, 0.5, 0.5], 11: [0.5, 0.5, 0.5], 12: [1, 0.5, 0.5]},
+                     "elements": {"TETRAHEDRON_4N": {1: [1, 2, 4, 7], 2: [2, 3, 5, 8], 3: [3, 6, 5, 9],
+                                                     4: [4, 5, 7, 10], 5: [5, 6, 8, 11], 6: [6, 9, 11, 8]}},
+                     "physical_groups": {"tetrahedral_group": {"ndim": 3,
+                                                               'element_ids': [1, 2, 3, 4, 5, 6],
+                                                               "node_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
+                                                               "element_type": "TETRAHEDRON_4N"}}}
+
+        # Create the mesh from the gmsh group
+        generated_mesh = Mesh.create_mesh_from_gmsh_group(mesh_data, "tetrahedral_group")
+
+        # set expected mesh
+        expected_nodes = [Node(1, [0, 0, 0]), Node(2, [0.5, 0, 0]), Node(3, [1, 0, 0]),
+                          Node(4, [0, 0.5, 0]), Node(5, [0.5, 0.5, 0]), Node(6, [1, 0.5, 0]),
+                          Node(7, [0, 0, 0.5]), Node(8, [0.5, 0, 0.5]), Node(9, [1, 0, 0.5]),
+                          Node(10, [0, 0.5, 0.5]), Node(11, [0.5, 0.5, 0.5]), Node(12, [1, 0.5, 0.5])]
+
+        expected_elements = [Element(1, "TETRAHEDRON_4N", [1, 2, 4, 7]),
+                             Element(2, "TETRAHEDRON_4N", [2, 3, 5, 8]),
+                             Element(3, "TETRAHEDRON_4N", [3, 6, 5, 9]),
+                             Element(4, "TETRAHEDRON_4N", [4, 5, 7, 10]),
+                             Element(5, "TETRAHEDRON_4N", [5, 6, 8, 11]),
+                             Element(6, "TETRAHEDRON_4N", [6, 9, 11, 8])]
+
+        expected_mesh = Mesh(3)
+        expected_mesh.nodes = expected_nodes
+        expected_mesh.elements = expected_elements
+
+        # Check the generated mesh
+        assert generated_mesh.ndim == expected_mesh.ndim
+
+        # Check the nodes
+        for generated_node, expected_node in zip(generated_mesh.nodes, expected_mesh.nodes):
+            assert generated_node.id == expected_node.id
+            assert pytest.approx(generated_node.coordinates) == expected_node.coordinates
+
+        # Check the elements
+        for generated_element, expected_element in zip(generated_mesh.elements, expected_mesh.elements):
+            assert generated_element.id == expected_element.id
+            assert generated_element.element_type == expected_element.element_type
+            assert generated_element.node_ids == expected_element.node_ids
+
+    def test_create_mesh_from_non_existing_group(self):
+        """
+        Test the creation of a mesh from a non-existing gmsh group. Expected to raise a ValueError.
+
+        """
+
+        # Set up the mesh data
+        mesh_data = {"ndim": 0,
+                     "nodes": {1: [0, 0, 0], 2: [0.5, 0, 0]},
+                     "elements": {"POINT_1N": {1: [1], 2: [2]}},
+                     "physical_groups": {"points_group": {"ndim": 0,
+                                                          'element_ids': [1, 2],
+                                                          "node_ids": [1, 2],
+                                                          "element_type": "POINT_1N"}}}
+
+        # Create the mesh from the gmsh group
+        with pytest.raises(ValueError):
+            Mesh.create_mesh_from_gmsh_group(mesh_data, "non_existing_group")
+
+
+class TestMeshSettings:
+    """
+    Test the mesh settings class.
+    """
+
+    def test_validation_element_order_at_initialisation_expected_raise(self):
+        """
+        Test the validation of the element order. Expected to raise a ValueError when the element order is not 1 or 2.
+
+        """
+
+        # test if ValueError is raised when element_order is not 1 or 2
+        with pytest.raises(ValueError):
+
+            MeshSettings(element_order=3)
+
+    def test_validation_element_order_after_initialisation_expected_raise(self):
+        """
+        Test the validation of the element order. Expected to raise a ValueError when the element order is not 1 or 2.
+
+        """
+
+        # test if ValueError is raised when element_order is not 1 or 2
+        mesh_settings = MeshSettings()
+
+        with pytest.raises(ValueError):
+            mesh_settings.element_order = 3
\ No newline at end of file
diff --git a/tests/test_model.py b/tests/test_model.py
new file mode 100644
index 00000000..e356e5c5
--- /dev/null
+++ b/tests/test_model.py
@@ -0,0 +1,1624 @@
+import pickle
+from typing import Tuple
+import re
+
+import numpy.testing as npt
+import pytest
+
+from stem.geometry import *
+from stem.model import *
+from stem.solver import *
+from tests.utils import TestUtils
+
+
+class TestModel:
+
+    @pytest.fixture
+    def expected_geo_data_0D(self):
+        """
+        Expected geometry data for a 0D geometry group. The group is a geometry of a point
+
+        Returns:
+            - Dict[str, Any]: dictionary containing the geometry data as generated by the gmsh_io
+        """
+        expected_points = {1: [0, 0, 0], 2: [0.5, 0, 0]}
+        return {"points": expected_points}
+
+    @pytest.fixture
+    def expected_geometry_single_layer_2D(self):
+        """
+        Sets expected geometry data for a 2D geometry group. The group is a geometry of a square.
+
+        Returns:
+            - :class:`stem.geometry.Geometry`: geometry of a 2D square
+        """
+
+        geometry = Geometry()
+
+        geometry.points = {1: Point.create([0, 0, 0], 1),
+                           2: Point.create([1, 0, 0], 2),
+                           3: Point.create([1, 1, 0], 3),
+                           4: Point.create([0, 1, 0], 4)}
+
+        geometry.lines = {1: Line.create([1, 2], 1),
+                          2: Line.create([2, 3], 2),
+                          3: Line.create([3, 4], 3),
+                          4: Line.create([4, 1], 4)}
+
+        geometry.surfaces = {1: Surface.create([1,2,3,4], 1)}
+
+        geometry.volumes = {}
+
+        return geometry
+
+    @pytest.fixture
+    def expected_geometry_single_layer_3D(self):
+        """
+        Sets expected geometry data for a 2D geometry group. The group is a geometry of a square.
+
+        Returns:
+            - :class:`stem.geometry.Geometry`: geometry of a 2D square
+        """
+
+        geometry = Geometry()
+
+        geometry.points = {1: Point.create([0, 0, 0], 1),
+                           5: Point.create([0, 0, 1], 5),
+                           6: Point.create([1, 0, 1], 6),
+                           2: Point.create([1, 0, 0], 2),
+                           7: Point.create([1, 1, 1], 7),
+                           3: Point.create([1, 1, 0], 3),
+                           8: Point.create([0, 1, 1], 8),
+                           4: Point.create([0, 1, 0], 4)}
+
+        geometry.lines = {5: Line.create([1, 5], 5),
+                          7: Line.create([5, 6], 7),
+                          6: Line.create([2, 6], 6),
+                          1: Line.create([1, 2], 1),
+                          9: Line.create([6, 7], 9),
+                          8: Line.create([3, 7], 8),
+                          2: Line.create([2, 3], 2),
+                          11: Line.create([7, 8], 11),
+                          10: Line.create([4, 8], 10),
+                          3: Line.create([3, 4], 3),
+                          12: Line.create([8, 5], 12),
+                          4: Line.create([4, 1], 4)}
+
+        geometry.surfaces = {2: Surface.create([5, 7, -6, -1], 2),
+                             3: Surface.create([6, 9, -8, -2], 3),
+                             4: Surface.create([8, 11, -10, -3], 4),
+                             5: Surface.create([10, 12, -5, -4], 5),
+                             1: Surface.create([1, 2, 3, 4], 1),
+                             6: Surface.create([7, 9, 11, 12], 6)}
+
+        # The volumes list converted to a dictionary
+        geometry.volumes = {1: Volume.create([-2, -3, -4, -5, -1, 6], 1)}
+
+
+        return geometry
+
+    @pytest.fixture
+    def expected_geometry_two_layers_2D(self):
+        """
+        Sets expected geometries for 2 attached 2D squares.
+
+        Returns:
+            - Tuple[:class:`stem.geometry.Geometry`,:class:`stem.geometry.Geometry`]: \
+                geometries of 2 attached 2D squares
+
+        """
+
+        # geometry_1
+        geometry_1 = Geometry()
+        geometry_1.points = {1: Point.create([0, 0, 0], 1),
+                             2: Point.create([1, 0, 0], 2),
+                             3: Point.create([1, 1, 0], 3),
+                             4: Point.create([0, 1, 0], 4)}
+
+        geometry_1.lines = {1: Line.create([1, 2], 1),
+                            2: Line.create([2, 3], 2),
+                            3: Line.create([3, 4], 3),
+                            4: Line.create([4, 1], 4)}
+
+        geometry_1.surfaces = {1: Surface.create([1, 2, 3, 4], 1)}
+
+        geometry_1.volumes = {}
+
+        # geometry_2
+        geometry_2 = Geometry()
+
+        geometry_2.points = {5: Point.create([1, 2, 0], 5),
+                             6: Point.create([0, 2, 0], 6),
+                             4: Point.create([0, 1, 0], 4),
+                             3: Point.create([1, 1, 0], 3)}
+
+        geometry_2.lines = {5: Line.create([5, 6],5),
+                            6: Line.create([6, 4], 6),
+                            3: Line.create([3, 4], 3),
+                            7: Line.create([3, 5], 7)}
+
+        geometry_2.surfaces = {2: Surface.create([5, 6, -3, 7], 2)}
+
+        geometry_2.volumes = {}
+
+        return geometry_1, geometry_2
+
+    @pytest.fixture
+    def expected_geometry_two_layers_2D_after_sync(self):
+        """
+        Sets expected geometry of two model parts and the whole model after synchronising the geometry.
+
+        Returns:
+            - Tuple[:class:`stem.geometry.Geometry`,:class:`stem.geometry.Geometry`, \
+                :class:`stem.geometry.Geometry`]: geometries of 2 attached 2D squares and the whole model
+        """
+
+        # create expected geometry layer 1
+        geometry_1 = Geometry()
+
+        geometry_1.points = {
+            1: Point.create([0, 0, 0], 1),
+            2: Point.create([1, 0, 0], 2),
+            3: Point.create([1, 1, 0], 3),
+            4: Point.create([0.5, 1, 0], 4),
+            5: Point.create([0, 1, 0], 5)
+        }
+
+        geometry_1.lines = {
+            1: Line.create([1, 2], 1),
+            2: Line.create([2, 3], 2),
+            3: Line.create([3, 4], 3),
+            4: Line.create([4, 5], 4),
+            5: Line.create([5, 1], 5)
+        }
+
+        geometry_1.surfaces = {
+            1: Surface.create([1, 2, 3, 4, 5], 1)
+        }
+
+        geometry_2 = Geometry()
+        geometry_2.points = {
+            6: Point.create([1.0, 2.0, 0.0], 6),
+            7: Point.create([0.5, 2.0, 0.0], 7),
+            4: Point.create([0.5, 1, 0], 4),
+            3: Point.create([1, 1, 0], 3)
+        }
+
+        geometry_2.lines = {
+            6: Line.create([6, 7], 6),
+            7: Line.create([7, 4], 7),
+            3: Line.create([3, 4], 3),
+            8: Line.create([3, 6], 8)
+        }
+
+        geometry_2.surfaces = {
+            2: Surface.create([6, 7, -3, 8], 2)
+        }
+
+        geometry_2.volumes = {}
+
+        # create expected full geometry
+        full_geometry = Geometry()
+        full_geometry.points = {
+            1: Point.create([0, 0, 0], 1),
+            2: Point.create([1, 0, 0], 2),
+            3: Point.create([1, 1, 0], 3),
+            4: Point.create([0.5, 1, 0], 4),
+            5: Point.create([0, 1, 0], 5),
+            6: Point.create([1, 2, 0], 6),
+            7: Point.create([0.5, 2, 0], 7)
+        }
+
+        full_geometry.lines = {
+            1: Line.create([1, 2], 1),
+            2: Line.create([2, 3], 2),
+            3: Line.create([3, 4], 3),
+            4: Line.create([4, 5], 4),
+            5: Line.create([5, 1], 5),
+            6: Line.create([6, 7], 6),
+            7: Line.create([7, 4], 7),
+            8: Line.create([3, 6], 8)
+        }
+
+        full_geometry.surfaces = {
+            1: Surface.create([1, 2, 3, 4, 5], 1),
+            2: Surface.create([6, 7, -3, 8], 2)
+        }
+
+        full_geometry.volumes = {}
+
+        return geometry_1, geometry_2, full_geometry
+
+    @pytest.fixture
+    def expected_geometry_line_load(self):
+        """
+        Sets expected geometry data for a 1D geometry group. The group is a geometry of a multi-line.
+
+        Returns:
+            - :class:`stem.geometry.Geometry`: geometry of a 1D multi-line
+        """
+
+        geometry = Geometry()
+
+        geometry.points = {
+            1: Point.create([0, 0, 0], 1),
+            2: Point.create([3, 0, 0], 2),
+            3: Point.create([4, -1, 0], 3),
+            4: Point.create([10, -1, 0], 4)
+        }
+
+        geometry.lines = {
+            1: Line.create([1, 2], 1),
+            2: Line.create([2, 3], 2),
+            3: Line.create([3, 4], 3)
+        }
+
+        geometry.surfaces = {}
+
+        geometry.volumes = {}
+
+        return geometry
+
+    @pytest.fixture
+    def create_default_2d_soil_material(self):
+        """
+        Create a default soil material for a 2D geometry.
+
+        Returns:
+            - :class:`stem.soil_material.SoilMaterial`: default soil material
+
+        """
+        # define soil material
+        ndim = 2
+        soil_formulation = OnePhaseSoil(ndim, IS_DRAINED=True, DENSITY_SOLID=2650, POROSITY=0.3)
+        constitutive_law = LinearElasticSoil(YOUNG_MODULUS=100e6, POISSON_RATIO=0.3)
+        soil_material = SoilMaterial(name="soil", soil_formulation=soil_formulation, constitutive_law=constitutive_law,
+                                     retention_parameters=SaturatedBelowPhreaticLevelLaw())
+        return soil_material
+
+    @pytest.fixture
+    def create_default_3d_soil_material(self):
+        """
+        Create a default soil material for a 3D geometry.
+
+        Returns:
+            - :class:`stem.soil_material.SoilMaterial`: default soil material
+
+        """
+        # define soil material
+        ndim = 3
+        soil_formulation = OnePhaseSoil(ndim, IS_DRAINED=True, DENSITY_SOLID=2650, POROSITY=0.3)
+        constitutive_law = LinearElasticSoil(YOUNG_MODULUS=100e6, POISSON_RATIO=0.3)
+        soil_material = SoilMaterial(name="soil", soil_formulation=soil_formulation, constitutive_law=constitutive_law,
+                                     retention_parameters=SaturatedBelowPhreaticLevelLaw())
+        return soil_material
+
+    @pytest.fixture
+    def create_default_point_load_parameters(self):
+        """
+        Create a default point load parameters.
+
+        Returns:
+            - :class:`stem.load.PointLoad`: default point load
+
+        """
+        # define soil material
+        return PointLoad(active=[False, True, False], value=[0, -200, 0])
+
+    @pytest.fixture
+    def create_default_line_load_parameters(self):
+        """
+        Create a default line load parameters.
+
+        Returns:
+            - :class:`stem.load.PointLoad`: default point load
+
+        """
+        # define soil material
+        return LineLoad(active=[False, True, False], value=[0, -20, 0])
+
+    @pytest.fixture
+    def create_default_surface_load_parameters(self):
+        """
+        Create a default surface load properties.
+
+        Returns:
+            - :class:`stem.load.SurfaceLoad`: default surface load
+
+        """
+        # define soil material
+        return SurfaceLoad(active=[False, True, False], value=[0, -2, 0])
+
+    @pytest.fixture
+    def create_default_moving_load_parameters(self):
+        """
+        Create a default surface load properties.
+
+        Returns:
+            - :class:`stem.load.SurfaceLoad`: default surface load
+
+        """
+        # define soil material
+        return MovingLoad(
+            origin=[3.5, -0.5, 0.0],
+            load=[0.0, -10.0, 0.0],
+            velocity=5.0,
+            offset=3.0,
+            direction=[1, 1, 1]
+        )
+
+    @pytest.fixture
+    def expected_geometry_two_layers_3D_extruded(self):
+        """
+        Expected geometry data for a 3D geometry create from 2D extrusion. The geometry is 2 stacked blocks, where the
+        top and bottom blocks are in different groups.
+
+        Returns:
+            - Tuple[:class:`stem.geometry.Geometry`,:class:`stem.geometry.Geometry`]: expected geometry data
+        """
+
+        geometry_1 = Geometry()
+        geometry_1.points = {
+            1: Point.create([0, 0, 0], 1),
+            2: Point.create([0, 0, 1], 2),
+            4: Point.create([1, 0, 1], 4),
+            3: Point.create([1, 0, 0], 3),
+            6: Point.create([1, 1, 1], 6),
+            5: Point.create([1, 1, 0], 5),
+            8: Point.create([0, 1, 1], 8),
+            7: Point.create([0, 1, 0], 7)
+        }
+
+        geometry_1.lines = {
+            1: Line.create([1, 2], 1),
+            4: Line.create([2, 4], 4),
+            2: Line.create([3, 4], 2),
+            3: Line.create([1, 3], 3),
+            7: Line.create([4, 6], 7),
+            5: Line.create([5, 6], 5),
+            6: Line.create([3, 5], 6),
+            10: Line.create([6, 8], 10),
+            8: Line.create([7, 8], 8),
+            9: Line.create([5, 7], 9),
+            12: Line.create([8, 2], 12),
+            11: Line.create([7, 1], 11)
+        }
+
+        geometry_1.surfaces = {
+            1: Surface.create([1, 4, -2, -3], 1),
+            2: Surface.create([2, 7, -5, -6], 2),
+            3: Surface.create([5, 10, -8, -9], 3),
+            4: Surface.create([8, 12, -1, -11], 4),
+            5: Surface.create([3, 6, 9, 11], 5),
+            6: Surface.create([4, 7, 10, 12], 6)
+        }
+
+        geometry_1.volumes = {
+            1: Volume.create([-1, -2, -3, -4, -5, 6], 1)
+        }
+
+        geometry_2 = Geometry()
+
+        geometry_2.points = {
+            9: Point.create([1.0, 2.0, 0.0], 9),
+            10: Point.create([1., 2., 1.], 10),
+            12: Point.create([0.0, 2., 1.], 12),
+            11: Point.create([0, 2., 0.], 11),
+            8: Point.create([0., 1., 1], 8),
+            7: Point.create([0., 1., 0], 7),
+            5: Point.create([1, 1., 0], 5),
+            6: Point.create([1, 1., 1], 6)
+        }
+
+        geometry_2.lines = {
+            13: Line.create([9, 10], 13),
+            16: Line.create([10, 12], 16),
+            14: Line.create([11, 12], 14),
+            15: Line.create([9, 11], 15),
+            18: Line.create([12, 8], 18),
+            8: Line.create([7, 8], 8),
+            17: Line.create([11, 7], 17),
+            5: Line.create([5, 6], 5),
+            10: Line.create([6, 8], 10),
+            9: Line.create([5, 7], 9),
+            20: Line.create([6, 10], 20),
+            19: Line.create([5, 9], 19)
+        }
+
+        geometry_2.surfaces = {
+            7: Surface.create([13, 16, -14, -15], 7),
+            8: Surface.create([14, 18, -8, -17], 8),
+            3: Surface.create([5, 10, -8, -9], 3),
+            9: Surface.create([5, 20, -13, -19], 9),
+            10: Surface.create([15, 17, -9, 19], 10),
+            11: Surface.create([16, 18, -10, 20], 11)
+        }
+
+        geometry_2.volumes = {
+            2: Volume.create([-7, -8, 3, -9, -10, 11], 2)
+        }
+
+        return geometry_1, geometry_2
+
+    @pytest.fixture
+    def expected_geometry_two_layers_3D_geo_file(self):
+        """
+        Expected geometry data for a 3D geometry create in a geo file. The geometry is 2 stacked blocks, where the top
+        and bottom blocks are in different groups.
+
+        Returns:
+            - Tuple[:class:`stem.geometry.Geometry`,:class:`stem.geometry.Geometry`]: expected geometry data
+        """
+
+        geometry_1 = Geometry()
+        geometry_1.volumes = {
+            1: Volume.create([-10, 39, 26, 30, 34, 38], 1)
+        }
+
+        geometry_1.surfaces = {
+            10: Surface.create([5, 6, 7, 8], 10),
+            39: Surface.create([19, 20, 21, 22], 39),
+            26: Surface.create([5, 25, -19, -24], 26),
+            30: Surface.create([6, 29, -20, -25], 30),
+            34: Surface.create([7, 33, -21, -29], 34),
+            38: Surface.create([8, 24, -22, -33], 38)
+        }
+
+        geometry_1.lines = {
+            5: Line.create([1, 2], 5),
+            6: Line.create([2, 3], 6),
+            7: Line.create([3, 4], 7),
+            8: Line.create([4, 1], 8),
+            19: Line.create([13, 14], 19),
+            20: Line.create([14, 18], 20),
+            21: Line.create([18, 22], 21),
+            22: Line.create([22, 13], 22),
+            25: Line.create([2, 14], 25),
+            24: Line.create([1, 13], 24),
+            29: Line.create([3, 18], 29),
+            33: Line.create([4, 22], 33)
+        }
+
+        geometry_1.points = {
+            1: Point.create([0., 0., 0.], 1),
+            2: Point.create([0.5, 0., 0.], 2),
+            3: Point.create([0.5, 1., 0.], 3),
+            4: Point.create([0., 1., 0.], 4),
+            13: Point.create([0., 0., -0.5], 13),
+            14: Point.create([0.5, 0., -0.5], 14),
+            18: Point.create([0.5, 1., -0.5], 18),
+            22: Point.create([0., 1., -0.5], 22)
+        }
+
+        geometry_2 = Geometry()
+        geometry_2.volumes = {
+            2: Volume.create([-17, 61, -48, -34, -56, -60], 2)
+        }
+
+        geometry_2.surfaces = {
+            17: Surface.create([-13, -7, -15, -14], 17),
+            61: Surface.create([41, -21, 43, 44], 61),
+            48: Surface.create([-13, 33, -41, -46], 48),
+            34: Surface.create([7, 33, -21, -29], 34),
+            56: Surface.create([-15, 55, -43, -29], 56),
+            60: Surface.create([-14, 46, -44, -55], 60)
+        }
+
+        geometry_2.lines = {
+            13: Line.create([4, 11], 13),
+            7: Line.create([3, 4], 7),
+            15: Line.create([12, 3], 15),
+            14: Line.create([11, 12], 14),
+            41: Line.create([23, 22], 41),
+            21: Line.create([18, 22], 21),
+            43: Line.create([18, 32], 43),
+            44: Line.create([32, 23], 44),
+            33: Line.create([4, 22], 33),
+            46: Line.create([11, 23], 46),
+            29: Line.create([3, 18], 29),
+            55: Line.create([12, 32], 55)
+        }
+
+        geometry_2.points = {
+            4: Point.create([0., 1., 0.], 4),
+            11: Point.create([0., 2., 0.], 11),
+            3: Point.create([0.5, 1., 0.], 3),
+            12: Point.create([0.5, 2., 0.], 12),
+            23: Point.create([0., 2., -0.5], 23),
+            22: Point.create([0., 1., -0.5], 22),
+            18: Point.create([0.5, 1., -0.5], 18),
+            32: Point.create([0.5, 2., -0.5], 32)
+        }
+
+        return geometry_1, geometry_2
+
+    @pytest.fixture(autouse=True)
+    def close_gmsh(self):
+        """
+        Initializer to close gmsh if it was not closed before. In case a test fails, the destroyer method is not called
+        on the Model object and gmsh keeps on running. Therefore, nodes, lines, surfaces and volumes ids are not
+        reset to one. This causes also the next test after the failed one to fail as well, which has nothing to do
+        the test itself.
+
+        Returns:
+            - None
+
+        """
+        gmsh_IO.GmshIO().finalize_gmsh()
+
+    def test_add_single_soil_layer_2D(self, expected_geometry_single_layer_2D: Geometry,
+                                      create_default_2d_soil_material: SoilMaterial):
+        """
+        Test if a single soil layer is added correctly to the model in a 2D space. A single soil layer is generated
+        and a single soil material is created and added to the model.
+
+        Args:
+            - expected_geometry_single_layer_2D (:class:`stem.geometry.Geometry`): expected geometry of the model
+            - create_default_2d_soil_material (:class:`stem.soil_material.SoilMaterial`): default soil material
+
+        """
+
+        ndim = 2
+
+        layer_coordinates = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+
+        # define soil material
+        soil_material = create_default_2d_soil_material
+
+        # create model
+        model = Model(ndim)
+
+        # add soil layer
+        model.add_soil_layer_by_coordinates(layer_coordinates, soil_material, "soil1")
+
+        # check if layer is added correctly
+        assert len(model.body_model_parts) == 1
+        assert model.body_model_parts[0].name == "soil1"
+        assert model.body_model_parts[0].material == soil_material
+
+        # check if geometry is added correctly
+        generated_geometry = model.body_model_parts[0].geometry
+        expected_geometry = expected_geometry_single_layer_2D
+
+        # check if points are added correctly
+        TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_add_single_soil_layer_3D(self, expected_geometry_single_layer_3D: Geometry,
+                                      create_default_3d_soil_material: SoilMaterial):
+        """
+        Test if a single soil layer is added correctly to the model in a 3D space. A single soil layer is generated
+        and a single soil material is created and added to the model.
+
+        Args:
+            - expected_geometry_single_layer_3D (:class:`stem.geometry.Geometry`): expected geometry of the model
+            - create_default_3d_soil_material (:class:`stem.soil_material.SoilMaterial`): default soil material
+
+        """
+
+        ndim = 3
+
+        layer_coordinates = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+
+        # define soil material
+        soil_material = create_default_3d_soil_material
+
+        # create model
+        model = Model(ndim)
+        model.extrusion_length = [0, 0, 1]
+
+        model.project_parameters = TestUtils.create_default_solver_settings()
+
+        # add soil layer
+        model.add_soil_layer_by_coordinates(layer_coordinates, soil_material, "soil1")
+
+        # check if layer is added correctly
+        assert len(model.body_model_parts) == 1
+        assert model.body_model_parts[0].name == "soil1"
+        assert model.body_model_parts[0].material == soil_material
+
+        # check if geometry is added correctly
+        generated_geometry = model.body_model_parts[0].geometry
+        expected_geometry = expected_geometry_single_layer_3D
+
+        # check if points are added correctly
+        TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_add_multiple_soil_layers_2D(self, expected_geometry_two_layers_2D: Tuple[Geometry, Geometry],
+                                         create_default_2d_soil_material: SoilMaterial):
+        """
+        Test if multiple soil layers are added correctly to the model in a 2D space. Multiple soil layers are generated
+        and multiple soil materials are created and added to the model.
+
+        Args:
+            - expected_geometry_two_layers_2D (Tuple[:class:`stem.geometry.Geometry`, :class:`stem.geometry.Geometry`]): \
+                expected geometry of the model
+            - create_default_2d_soil_material (:class:`stem.soil_material.SoilMaterial`): default soil material
+
+        """
+
+        ndim = 2
+
+        layer1_coordinates = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+        layer2_coordinates = [(1, 1, 0), (0, 1, 0), (0, 2, 0), (1, 2, 0)]
+
+        # define soil materials
+        soil_material1 = create_default_2d_soil_material
+        soil_material1.name = "soil1"
+
+        soil_material2 = create_default_2d_soil_material
+        soil_material2.name = "soil2"
+
+        # create model
+        model = Model(ndim)
+
+        # add soil layers
+        model.add_soil_layer_by_coordinates(layer1_coordinates, soil_material1, "layer1")
+        model.add_soil_layer_by_coordinates(layer2_coordinates, soil_material2, "layer2")
+
+        # check if layers are added correctly
+        assert len(model.body_model_parts) == 2
+        assert model.body_model_parts[0].name == "layer1"
+        assert model.body_model_parts[0].material == soil_material1
+        assert model.body_model_parts[1].name == "layer2"
+        assert model.body_model_parts[1].material == soil_material2
+
+        # check if geometry is added correctly for each layer
+        for i in range(len(model.body_model_parts)):
+            generated_geometry = model.body_model_parts[i].geometry
+            expected_geometry = expected_geometry_two_layers_2D[i]
+
+            TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_add_multiple_soil_layers_3D(self, expected_geometry_two_layers_3D_extruded: Tuple[Geometry, Geometry],
+                                         create_default_3d_soil_material: SoilMaterial):
+        """
+        Test if multiple soil layers are added correctly to the model in a 3D space. Multiple soil layers are generated
+        and multiple soil materials are created and added to the model.
+
+        Args:
+            - expected_geometry_two_layers_3D_extruded (Tuple[:class:`stem.geometry.Geometry`, \
+                :class:`stem.geometry.Geometry`]): expected geometry of the model which is created by extruding \
+                a 2D geometry
+            - create_default_3d_soil_material (:class:`stem.soil_material.SoilMaterial`): default soil material
+
+        """
+
+        ndim = 3
+
+        layer1_coordinates = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+        layer2_coordinates = [(1, 1, 0), (0, 1, 0), (0, 2, 0), (1, 2, 0)]
+
+        # define soil materials
+        soil_material1 = create_default_3d_soil_material
+        soil_material1.name = "soil1"
+
+        soil_material2 = create_default_3d_soil_material
+        soil_material2.name = "soil2"
+
+        # create model
+        model = Model(ndim)
+        model.extrusion_length = [0, 0, 1]
+
+        # add soil layers
+        model.add_soil_layer_by_coordinates(layer1_coordinates, soil_material1, "layer1")
+        model.add_soil_layer_by_coordinates(layer2_coordinates, soil_material2, "layer2")
+
+        model.synchronise_geometry()
+
+        # check if layers are added correctly
+        assert len(model.body_model_parts) == 2
+        assert model.body_model_parts[0].name == "layer1"
+        assert model.body_model_parts[0].material == soil_material1
+        assert model.body_model_parts[1].name == "layer2"
+        assert model.body_model_parts[1].material == soil_material2
+
+        # check if geometry is added correctly for each layer
+        for i in range(len(model.body_model_parts)):
+            generated_geometry = model.body_model_parts[i].geometry
+            expected_geometry = expected_geometry_two_layers_3D_extruded[i]
+
+            TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_add_all_layers_from_geo_file_2D(self, expected_geometry_two_layers_2D: Tuple[Geometry, Geometry]):
+        """
+        Tests if all layers are added correctly to the model in a 2D space. A geo file is read and all layers are
+        added to the model.
+
+        Args:
+            - expected_geometry_two_layers_2D (Tuple[:class:`stem.geometry.Geometry`, :class:`stem.geometry.Geometry`]): \
+                expected geometry of the model
+
+        """
+
+        geo_file_name = "tests/test_data/gmsh_utils_two_blocks_2D.geo"
+
+        # create model
+        model = Model(ndim=2)
+        model.add_all_layers_from_geo_file(geo_file_name, ["group_1"])
+
+        # check if body model parts are added correctly
+        assert len(model.body_model_parts) == 1
+        assert model.body_model_parts[0].name == "group_1"
+
+        # check if process model part is added correctly
+        assert len(model.process_model_parts) == 1
+        assert model.process_model_parts[0].name == "group_2"
+
+        # check if geometry is added correctly for each layer
+        for i in range(len(model.body_model_parts)):
+            generated_geometry = model.body_model_parts[i].geometry
+            expected_geometry = expected_geometry_two_layers_2D[i]
+
+            TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_add_all_layers_from_geo_file_3D(self, expected_geometry_two_layers_3D_geo_file: Tuple[Geometry, Geometry]):
+        """
+        Tests if all layers are added correctly to the model in a 3D space. A geo file is read and all layers are
+        added to the model.
+
+        Args:
+            - expected_geometry_two_layers_3D_geo_file (Tuple[:class:`stem.geometry.Geometry`, \
+                :class:`stem.geometry.Geometry`]): expected geometry of the model
+
+        """
+
+        geo_file_name = "tests/test_data/gmsh_utils_column_3D_tetra4.geo"
+
+        # create model
+        model = Model(ndim=3)
+        model.add_all_layers_from_geo_file(geo_file_name, ["group_1"])
+
+        # check if body model parts are added correctly
+        assert len(model.body_model_parts) == 1
+        assert model.body_model_parts[0].name == "group_1"
+
+        # check if process model part is added correctly
+        assert len(model.process_model_parts) == 1
+        assert model.process_model_parts[0].name == "group_2"
+
+        # check if geometry is added correctly
+        all_model_parts = []
+        all_model_parts.extend(model.body_model_parts)
+        all_model_parts.extend(model.process_model_parts)
+
+        # check if geometry is added correctly for each layer
+        for i in range(len(all_model_parts)):
+            generated_geometry = all_model_parts[i].geometry
+            expected_geometry = expected_geometry_two_layers_3D_geo_file[i]
+
+            TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_synchronise_geometry_2D(self, expected_geometry_two_layers_2D_after_sync: Tuple[Geometry, Geometry],
+                                   create_default_2d_soil_material: SoilMaterial):
+        """
+        Test if the geometry is synchronised correctly in 2D after adding a new layer to the model. Where the new layer
+        overlaps with the existing layer, the existing layer is cut and the overlapping part is removed.
+
+        Args:
+            - expected_geometry_two_layers_2D_after_sync (Tuple[:class:`stem.geometry.Geometry`, \
+                :class:`stem.geometry.Geometry`, :class:`stem.geometry.Geometry`]): The expected geometry after \
+                synchronising the geometry.
+            - create_default_2d_soil_material (:class:`stem.soil_material.SoilMaterial`): A default soil material.
+
+        """
+
+        # define layer coordinates
+        ndim = 2
+        layer1_coordinates = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+        layer2_coordinates = [(1, 1, 0), (0.5, 1, 0), (0.5, 2, 0), (1, 2, 0)]
+
+        # define soil materials
+        soil_material1 = create_default_2d_soil_material
+        soil_material1.name = "soil1"
+
+        soil_material2 = create_default_2d_soil_material
+        soil_material2.name = "soil2"
+
+        # create model
+        model = Model(ndim)
+
+        # add soil layers
+        model.add_soil_layer_by_coordinates(layer1_coordinates, soil_material1, "layer1")
+        model.add_soil_layer_by_coordinates(layer2_coordinates, soil_material2, "layer2")
+
+        # synchronise geometry and recalculates the ids
+        model.synchronise_geometry()
+
+        # collect all generated geometries
+        generated_geometries = [model.body_model_parts[0].geometry, model.body_model_parts[1].geometry, model.geometry]
+
+        # check if geometry is added correctly for each layer
+        for generated_geometry, expected_geometry in zip(generated_geometries,
+                                                         expected_geometry_two_layers_2D_after_sync):
+
+            TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_synchronise_geometry_3D(self, create_default_3d_soil_material: SoilMaterial):
+        """
+        Test if the geometry is synchronised correctly in 3D after adding a new layer to the model. Where the new layer
+        overlaps with the existing layer, the existing layer is cut and the overlapping part is removed.
+
+        Args:
+            - create_default_3d_soil_material (:class:`stem.soil_material.SoilMaterial`): A default soil material.
+
+        """
+
+        # define layer coordinates
+        ndim = 3
+        layer1_coordinates = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+        layer2_coordinates = [(1, 1, 0), (0.5, 1, 0), (0.5, 2, 0), (1, 2, 0)]
+
+        # define soil materials
+        soil_material1 = create_default_3d_soil_material
+        soil_material1.name = "soil1"
+
+        soil_material2 = create_default_3d_soil_material
+        soil_material2.name = "soil2"
+
+        # create model
+        model = Model(ndim)
+        model.extrusion_length = [0, 0, 1]
+
+        # add soil layers
+        model.add_soil_layer_by_coordinates(layer1_coordinates, soil_material1, "layer1")
+        model.add_soil_layer_by_coordinates(layer2_coordinates, soil_material2, "layer2")
+
+        # synchronise geometry and recalculates the ids
+        model.synchronise_geometry()
+
+        with open("tests/test_data/expected_geometry_after_sync_3D.pickle", "rb") as f:
+            expected_geometry_two_layers_3D_after_sync = pickle.load(f)
+
+        # collect all generated geometries
+        generated_geometries = [model.body_model_parts[0].geometry, model.body_model_parts[1].geometry, model.geometry]
+
+        # check if geometry is added correctly for each layer
+        for generated_geometry, expected_geometry in zip(generated_geometries,
+                                                         expected_geometry_two_layers_3D_after_sync):
+
+            TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_add_point_loads_to_2_points(self, create_default_point_load_parameters: PointLoad):
+        """
+        Test if a single soil point load is added correctly to the model. Two points are generated
+        and a single load is created and added to the model.
+
+        Args:
+            - create_default_point_load_properties (:class:`stem.load.PointLoad`): default point load parameters
+
+        """
+
+        ndim = 3
+
+        point_coordinates = [(-0.5, 0, 0), (0.5, 0, 0)]
+
+        # define soil material
+        load_parameters = create_default_point_load_parameters
+
+        # create model
+        model = Model(ndim)
+        # add point load
+        model.add_load_by_coordinates(point_coordinates, load_parameters, "point_load_1")
+
+        # check if layer is added correctly
+        assert len(model.process_model_parts) == 1
+        assert model.process_model_parts[0].name == "point_load_1"
+        TestUtils.assert_dictionary_almost_equal(
+            model.process_model_parts[0].parameters.__dict__,
+            load_parameters.__dict__
+        )
+
+        # check if geometry is added correctly
+        generated_geometry = model.process_model_parts[0].geometry
+        expected_geometry = Geometry(
+            points={1:Point.create([-0.5, 0, 0], 1), 2: Point.create([0.5, 0, 0], 2)}
+        )
+
+        TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_add_line_load_to_3_edges(self, expected_geometry_line_load: Geometry,
+                                      create_default_line_load_parameters: PointLoad):
+        """
+        Test if a line load is added correctly to the model when applied on 3 edges. 4 points are generated
+        and a single soil material is created and added to the model.
+
+        Args:
+            - expected_geometry_line_load (:class:`stem.geometry.Geometry`): expected geometry of the model
+            - create_default_line_load_parameters (:class:`stem.load.LineLoad`): default line load parameters
+
+        """
+
+        ndim = 3
+
+        point_coordinates = [(0, 0, 0), (3, 0, 0), (4, -1, 0), (10, -1, 0)]
+
+        # define soil material
+        load_parameters = create_default_line_load_parameters
+
+        # create model
+        model = Model(ndim)
+        # add line load
+        model.add_load_by_coordinates(point_coordinates, load_parameters, "line_load_1")
+
+        # check if layer is added correctly
+        assert len(model.process_model_parts) == 1
+        assert model.process_model_parts[0].name == "line_load_1"
+        TestUtils.assert_dictionary_almost_equal(
+            model.process_model_parts[0].parameters.__dict__,
+            load_parameters.__dict__
+        )
+        # check if geometry is added correctly
+        generated_geometry = model.process_model_parts[0].geometry
+        expected_geometry = expected_geometry_line_load
+
+        TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_add_moving_point_load(self, expected_geometry_line_load: Geometry,
+                                   create_default_moving_load_parameters: MovingLoad):
+        """
+        Test if a single soil point load is added correctly to the model. Two points are generated
+        and a single load is created and added to the model.
+
+        Args:
+            - expected_geometry_line_load (:class:`stem.geometry.Geometry`): expected geometry of the model
+            - create_default_moving_load_parameters (:class:`stem.load.MovingLoad`): default moving load parameters
+
+        """
+
+        ndim = 3
+
+        point_coordinates = [(0, 0, 0), (3, 0, 0), (4, -1, 0), (10, -1, 0)]
+        # origin is in (3.5, -0.5, 0) thus in the trajectory
+
+        # define soil material
+        load_parameters = create_default_moving_load_parameters
+
+        # create model
+        model = Model(ndim)
+        # add moving load
+        model.add_load_by_coordinates(point_coordinates, load_parameters, "moving_load_1")
+
+        # check if layer is added correctly
+        assert len(model.process_model_parts) == 1
+        assert model.process_model_parts[0].name == "moving_load_1"
+        TestUtils.assert_dictionary_almost_equal(
+            model.process_model_parts[0].parameters.__dict__,
+            load_parameters.__dict__
+        )
+
+        # check if geometry is added correctly
+        generated_geometry = model.process_model_parts[0].geometry
+        expected_geometry = expected_geometry_line_load
+
+        TestUtils.assert_almost_equal_geometries(expected_geometry, generated_geometry)
+
+    def test_validation_coordinates(self):
+        """
+        Test that validation raises and error if the points are not correctly specified.
+        """
+
+        ndim = 3
+        model = Model(ndim=ndim)
+
+        # test inputs for numpy arrays:
+        # test for 2D-array, correct number of coordinates (shape 3,2)
+        model.validate_coordinates(np.zeros((2,3)))
+
+        # test for incorrect number of coordinates in array (shape 3,2)
+        with pytest.raises(ValueError, match=f"Coordinates should be 3D but 2 coordinates were given."):
+            model.validate_coordinates(np.zeros((3, 2)))
+
+        # test for incorrect number of dimension in array (1-D array)
+        with pytest.raises(ValueError, match=f"Coordinates are not a sequence of a sequence or a 2D array."):
+            model.validate_coordinates(np.arange(3))
+
+        # test inputs for sequence of floats:
+        # test for incorrect number of coordinates
+        with pytest.raises(ValueError, match=f"Coordinates should be 3D but 4 coordinates were given."):
+            model.validate_coordinates([(0.0, 0.0, 0.0, 4.0)])
+
+        # test for incorrect type (Sequence of float instead of Sequence[Sequence[float]])
+        with pytest.raises(ValueError, match="Coordinates are not a sequence of a sequence or a 2D array."):
+            model.validate_coordinates([0.0, 0.0, 0.0])
+
+        # test for nan numbers
+        with pytest.raises(ValueError, match=f"Coordinates should be a sequence of sequence of real numbers, "
+                                             f"but nan was given."):
+            model.validate_coordinates([(0.0, 0.0, 0.0), (0.0, np.NAN, 0.0)])
+
+        # test for inf numbers
+        with pytest.raises(ValueError, match=f"Coordinates should be a sequence of sequence of real numbers, "
+                                             f"but inf was given."):
+            model.validate_coordinates([(0.0, 0.0, 0.0), (0.0, np.inf, 0.0)])
+
+        # test for complex numbers, different error messages for different python versions and operating systems
+        message_option_1 = f"can't convert complex to float"
+        message_option_2 = f"float() argument must be a string or a real number, not 'complex'"
+
+        with pytest.raises(TypeError,
+                           match=f"{message_option_1}|{re.escape(message_option_2)}"):
+            model.validate_coordinates([(0.0, 0.0, 0.0), (0.0, 1j, 0.0)])
+
+        # test for strings
+        with pytest.raises(ValueError, match=f"could not convert string to float: 'test'"):
+            model.validate_coordinates([(0.0, 0.0, 0.0), (0.0, "test", 0.0)])
+
+    def test_validation_moving_load(self, create_default_moving_load_parameters:MovingLoad):
+        """
+        Test validation of moving load when points is not collinear to the trajectory.
+
+        Args:
+            - create_default_moving_load_parameters (:class:`stem.load.MovingLoad`): default moving load parameters
+
+        """
+
+        ndim = 3
+
+        point_coordinates = [(0.0, 0, 0), (1, 0, 0), (2, 0, 0), (4, 0, 0)]
+        # origin is in (1.5, 0.5, 0) thus not in the trajectory
+
+        # define soil material
+        load_parameters = create_default_moving_load_parameters
+        # create model
+        model = Model(ndim)
+
+        with pytest.raises(ValueError, match="Origin is not in the trajectory of the moving load."):
+            model.add_load_by_coordinates(
+                point_coordinates, load_parameters, "moving_load_1"
+            )
+
+    def test_generate_mesh_with_only_a_body_model_part_2d(self, create_default_2d_soil_material: SoilMaterial):
+        """
+        Test if the mesh is generated correctly in 2D if there is only one body model part.
+
+        Args:
+            - create_default_2d_soil_material (:class:`stem.soil_material.SoilMaterial`): A default soil material.
+
+        """
+        model = Model(2)
+
+        # add soil material
+        soil_material = create_default_2d_soil_material
+
+        # add soil layers
+        model.add_soil_layer_by_coordinates([(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)], soil_material, "layer1")
+        model.synchronise_geometry()
+
+        # generate mesh
+        model.generate_mesh()
+
+        mesh = model.body_model_parts[0].mesh
+
+        assert mesh.ndim == 2
+
+        unique_element_ids = []
+        # check if mesh is generated correctly, i.e. if the number of elements is correct and if the element type is
+        # correct and if the element ids are unique and if the number of nodes per element is correct
+        assert len(mesh.elements) == 162
+        for element in mesh.elements:
+            assert element.element_type == "TRIANGLE_3N"
+            assert element.id not in unique_element_ids
+            assert len(element.node_ids) == 3
+            unique_element_ids.append(element.id)
+
+        # check if nodes are generated correctly, i.e. if there are nodes in the mesh and if the node ids are unique
+        # and if the number of coordinates per node is correct
+        unique_node_ids = []
+        assert len(mesh.nodes) == 98
+        for node in mesh.nodes:
+            assert node.id not in unique_node_ids
+            assert len(node.coordinates) == 3
+            unique_node_ids.append(node.id)
+
+    def test_generate_mesh_with_only_a_body_model_part_3d(self, create_default_3d_soil_material: SoilMaterial):
+        """
+        Test if the mesh is generated correctly in 3D if there is only one body model part.
+
+        Args:
+            - create_default_3d_soil_material (:class:`stem.soil_material.SoilMaterial`): A default soil material.
+
+        """
+        model = Model(3)
+        model.extrusion_length = [0, 0, 1]
+
+        # add soil material
+        soil_material = create_default_3d_soil_material
+
+        # add soil layers
+        model.add_soil_layer_by_coordinates([(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)], soil_material, "layer1")
+        model.synchronise_geometry()
+
+        # generate mesh
+        model.generate_mesh()
+
+        mesh = model.body_model_parts[0].mesh
+
+        assert mesh.ndim == 3
+
+        unique_element_ids = []
+        # check if mesh is generated correctly, i.e. if the number of elements is correct and if the element type is
+        # correct and if the element ids are unique and if the number of nodes per element is correct
+        assert len(mesh.elements) == 1120
+        for element in mesh.elements:
+            assert element.element_type == "TETRAHEDRON_4N"
+            assert element.id not in unique_element_ids
+            assert len(element.node_ids) == 4
+            unique_element_ids.append(element.id)
+
+        # check if nodes are generated correctly, i.e. if there are nodes in the mesh and if the node ids are unique
+        # and if the number of coordinates per node is correct
+        unique_node_ids = []
+        assert len(mesh.nodes) == 340
+        for node in mesh.nodes:
+            assert node.id not in unique_node_ids
+            assert len(node.coordinates) == 3
+            unique_node_ids.append(node.id)
+
+    def test_generate_mesh_with_body_and_process_model_part(self, create_default_2d_soil_material: SoilMaterial):
+        """
+        Test if the mesh is generated correctly in the body model part and a process model part.
+
+        Args:
+            - create_default_2d_soil_material (:class:`stem.soil_material.SoilMaterial`): A default soil material.
+        """
+        model = Model(2)
+
+        # add soil material
+        soil_material = create_default_2d_soil_material
+
+        # add soil layers
+        model.add_soil_layer_by_coordinates([(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)], soil_material, "layer1")
+
+        # add process geometry
+        gmsh_process_input = {"process_0d": {"coordinates": [[0, 0.5, 0]], "ndim": 0}}
+        model.gmsh_io.generate_geometry(gmsh_process_input, "")
+
+        # create process model part
+        process_model_part = ModelPart("process_0d")
+
+        # set the geometry of the process model part
+        process_model_part.get_geometry_from_geo_data(model.gmsh_io.geo_data, "process_0d")
+
+        # add process model part
+        model.process_model_parts.append(process_model_part)
+
+        # synchronise geometry and generate mesh
+        model.synchronise_geometry()
+        model.generate_mesh()
+
+        # check mesh of body model part
+        mesh_body = model.body_model_parts[0].mesh
+
+        assert mesh_body.ndim == 2
+
+        unique_element_ids = []
+        # check if mesh is generated correctly, i.e. if the number of elements is correct and if the element type is
+        # correct and if the element ids are unique and if the number of nodes per element is correct
+        assert len(mesh_body.elements) == 162
+        for element in mesh_body.elements:
+            assert element.element_type == "TRIANGLE_3N"
+            assert element.id not in unique_element_ids
+            assert len(element.node_ids) == 3
+            unique_element_ids.append(element.id)
+
+        # check if nodes are generated correctly, i.e. if there are nodes in the mesh and if the node ids are unique
+        # and if the number of coordinates per node is correct
+        unique_body_node_ids = []
+        assert len(mesh_body.nodes) == 98
+        for node in mesh_body.nodes:
+            assert node.id not in unique_body_node_ids
+            assert len(node.coordinates) == 3
+            unique_body_node_ids.append(node.id)
+
+        # check process model part
+        mesh_process = model.process_model_parts[0].mesh
+
+        assert mesh_process.ndim == 0
+
+        # check elements of process model part, i.e. if the number of elements is correct and if the element type is
+        # correct and if the element ids are unique and if the number of nodes per element is correct
+        assert len(mesh_process.elements) == 1
+        for element in mesh_process.elements:
+            assert element.element_type == "POINT_1N"
+            assert element.id == 1
+            assert element.id not in unique_element_ids
+            assert len(element.node_ids) == 1
+            unique_element_ids.append(element.id)
+
+        # check nodes of process model part, i.e. if there is 1 node in the mesh and if the node ids are present in the
+        # body mesh and if the number of coordinates per node is correct
+        assert len(mesh_process.nodes) == 1
+        for node in mesh_process.nodes:
+
+            # check if node is also available in the body mesh
+            assert node.id in unique_body_node_ids
+            assert len(node.coordinates) == 3
+
+    def test_validate_expected_success(self):
+        """
+        Test if the model is validated correctly. A model is created with two process model parts which both have
+        a unique name.
+
+        """
+
+        model = Model(2)
+
+        model_part1 = ModelPart("test1")
+        model_part2 = ModelPart("test2")
+
+        model.process_model_parts = [model_part1, model_part2]
+
+        model.validate()
+
+    def test_validate_expected_fail_non_unique_names(self):
+        """
+        Test if the model is validated correctly. A model is created with two process model parts which both have
+        the same name. This should raise a ValueError.
+
+        """
+
+        model = Model(2)
+
+        model_part1 = ModelPart("test")
+        model_part2 = ModelPart("test")
+
+        model.process_model_parts = [model_part1, model_part2]
+
+        pytest.raises(ValueError, model.validate)
+
+    def test_validate_expected_fail_no_name(self):
+        """
+        Test if the model is validated correctly. A model is created with a process model part which does not contain
+        a name. This should raise a ValueError.
+
+        """
+
+        model = Model(2)
+
+        model_part1 = ModelPart(None)
+        model.process_model_parts = [model_part1]
+
+        pytest.raises(ValueError, model.validate)
+
+    def test_add_boundary_condition_by_geometry_ids(self,create_default_3d_soil_material: SoilMaterial):
+        """
+        Test if a boundary condition is added correctly to the model. A boundary condition is added to the model by
+        specifying the geometry ids to which the boundary condition should be applied.
+
+        Args:
+            - create_default_3d_soil_material (:class:`stem.soil_material.SoilMaterial`): A default soil material.
+
+        """
+
+
+        # create a 3D model
+        model = Model(3)
+        model.extrusion_length = [0, 0, 1]
+
+        # create multiple boundary condition parameters
+        no_rotation_parameters = RotationConstraint(active=[True, True, True], is_fixed=[True, True, True],
+                                                    value=[0, 0, 0])
+
+        absorbing_parameters = AbsorbingBoundary(absorbing_factors=[1,1], virtual_thickness=0)
+
+        no_displacement_parameters = DisplacementConstraint(active=[True, True, True], is_fixed=[True, True, True],
+                                                            value=[0, 0, 0])
+
+        # add body model part
+        soil_material = create_default_3d_soil_material
+        model.add_soil_layer_by_coordinates([(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)], soil_material, "test_soil")
+
+
+        # add boundary conditions in 0d, 1d and 2d
+        model.add_boundary_condition_by_geometry_ids(0, [1, 2], no_rotation_parameters, "no_rotation")
+        model.add_boundary_condition_by_geometry_ids(1, [8], absorbing_parameters, "absorbing")
+        model.add_boundary_condition_by_geometry_ids(2, [1, 2], no_displacement_parameters, "no_displacement")
+
+        model.synchronise_geometry()
+
+        # set expected parameters of the boundary conditions
+        expected_0d_model_part_parameters = RotationConstraint(active=[True, True, True], is_fixed=[True, True, True],
+                                                               value=[0, 0, 0])
+
+        expected_1d_model_part_parameters = AbsorbingBoundary(absorbing_factors=[1, 1], virtual_thickness=0)
+
+        expected_2d_model_part_parameters = DisplacementConstraint(active=[True, True, True],
+                                                                   is_fixed=[True, True, True], value=[0, 0, 0])
+
+        # set expected geometry 0d boundary condition
+        expected_boundary_points = {1: Point.create([0, 0, 0], 1), 2:  Point.create([1, 0, 0], 2)}
+        expected_boundary_lines = {1: Line.create([1, 2], 1)}
+        expected_boundary_surfaces = {}
+        expected_boundary_volumes = {}
+
+        expected_boundary_geometry_0d = Geometry(expected_boundary_points, expected_boundary_lines,
+                                                 expected_boundary_surfaces, expected_boundary_volumes)
+
+        # set expected geometry 1d boundary condition
+        expected_boundary_points = {3: Point.create([1, 1, 0], 3), 7: Point.create([1, 1, 1], 7)}
+        expected_boundary_lines = {8: Line.create([3, 7], 8)}
+        expected_boundary_surfaces = {}
+        expected_boundary_volumes = {}
+
+        expected_boundary_geometry_1d = Geometry(expected_boundary_points, expected_boundary_lines,
+                                                 expected_boundary_surfaces, expected_boundary_volumes)
+
+        # set expected geometry 2d boundary condition
+
+        expected_boundary_geometry_2d = Geometry()
+        expected_boundary_geometry_2d.points = {
+            1: Point.create([0, 0, 0], 1),
+            2: Point.create([1, 0, 0], 2),
+            3: Point.create([1, 1, 0], 3),
+            4: Point.create([0, 1, 0], 4),
+            5: Point.create([0, 0, 1], 5),
+            6: Point.create([1, 0, 1], 6)
+        }
+
+        expected_boundary_geometry_2d.lines = {
+            1: Line.create([1, 2], 1),
+            2: Line.create([2, 3], 2),
+            3: Line.create([3, 4], 3),
+            4: Line.create([4, 1], 4),
+            5: Line.create([1, 5], 5),
+            7: Line.create([5, 6], 7),
+            6: Line.create([2, 6], 6)
+        }
+
+        expected_boundary_geometry_2d.surfaces = {
+            1: Surface.create([1, 2, 3, 4], 1),
+            2: Surface.create([5, 7, -6, -1], 2)
+        }
+
+        expected_boundary_geometry_2d.volumes = {}
+
+        # collect all expected geometries
+        all_expected_geometries = [expected_boundary_geometry_0d, expected_boundary_geometry_1d,
+                                      expected_boundary_geometry_2d]
+
+        # check 0d parameters
+        npt.assert_allclose(model.process_model_parts[0].parameters.active, expected_0d_model_part_parameters.active)
+        npt.assert_allclose(model.process_model_parts[0].parameters.is_fixed, expected_0d_model_part_parameters.is_fixed)
+        npt.assert_allclose(model.process_model_parts[0].parameters.value, expected_0d_model_part_parameters.value)
+
+        # check 1d parameters
+        npt.assert_allclose(model.process_model_parts[1].parameters.absorbing_factors,
+                            expected_1d_model_part_parameters.absorbing_factors)
+        npt.assert_allclose(model.process_model_parts[1].parameters.virtual_thickness,
+                            expected_1d_model_part_parameters.virtual_thickness)
+
+        # check 2d parameters
+        npt.assert_allclose(model.process_model_parts[2].parameters.active, expected_2d_model_part_parameters.active)
+        npt.assert_allclose(model.process_model_parts[2].parameters.is_fixed, expected_2d_model_part_parameters.is_fixed)
+        npt.assert_allclose(model.process_model_parts[2].parameters.value, expected_2d_model_part_parameters.value)
+
+        for expected_geometry, model_part in zip(all_expected_geometries, model.process_model_parts):
+
+            TestUtils.assert_almost_equal_geometries(expected_geometry, model_part.geometry)
+
+
+    def test_add_gravity_load_1d_and_2d(self, create_default_2d_soil_material: SoilMaterial):
+        """
+        Test if a gravity load is added correctly to the model in a 2d space containing 1d and 2d elements. A gravity
+        load is generated and added to the model.
+
+        Args:
+            - create_default_2d_soil_material (:class:`stem.soil_material.SoilMaterial`): A default soil material.
+
+        """
+
+        # create model
+        model = Model(2)
+
+        # add a 2d layer
+        model.add_soil_layer_by_coordinates([(0, 0, 0), (1, 0, 0), (1, 1, 0)], create_default_2d_soil_material, "soil1")
+
+        # add a 1d layer
+        layer_settings = {"beam": {"ndim": 1,
+                                   "element_size": -1,
+                                   "coordinates": [[0, 0, 0], [1, 0, 0]]}}
+
+        model.gmsh_io.generate_geometry(layer_settings, "")
+        model.synchronise_geometry()
+
+        # add 1d model part to model
+        body_model_part = BodyModelPart("beam")
+        body_model_part.material = EulerBeam(ndim=2, YOUNG_MODULUS=1e6, POISSON_RATIO=0.3, DENSITY=1, CROSS_AREA=1,
+                                             I33=1)
+        body_model_part.get_geometry_from_geo_data(model.gmsh_io.geo_data, "beam")
+
+        model.body_model_parts.append(body_model_part)
+
+        # add gravity load
+        model._Model__add_gravity_load()
+
+        assert len(model.process_model_parts) == 2
+        assert model.process_model_parts[0].name == "gravity_load_1d"
+        assert model.process_model_parts[1].name == "gravity_load_2d"
+
+        # setup expected geometries for 1d and 2d
+        expected_geometry_points_1d = {1: Point.create([0, 0, 0],1), 2: Point.create([1, 0, 0], 2)}
+        expected_geometry_lines_1d = {1: Line.create([1, 2], 1)}
+        expected_geometry_gravity_1d = Geometry(expected_geometry_points_1d, expected_geometry_lines_1d, {}, {})
+
+        expected_geometry_points_2d = {1: Point.create([0, 0, 0], 1), 2: Point.create([1, 0, 0], 2),
+                                       3: Point.create([1, 1, 0], 3)}
+        expected_geometry_lines_2d = {1: Line.create([1, 2], 1), 2: Line.create([2, 3], 2), 3: Line.create([3, 1], 3)}
+        expected_geometry_surfaces_2d = {1: Surface.create([1, 2, 3], 1)}
+        expected_geometry_gravity_2d = Geometry(expected_geometry_points_2d, expected_geometry_lines_2d,
+                                                expected_geometry_surfaces_2d, {})
+
+        expected_geometries = [expected_geometry_gravity_1d, expected_geometry_gravity_2d]
+
+        # check if all process model parts are correct
+        for model_part in model.process_model_parts:
+
+            # check if parameters are added correctly
+            npt.assert_allclose(model_part.parameters.value, [0, -9.81, 0])
+            npt.assert_allclose(model_part.parameters.active, [True, True, True])
+
+            # check if geometry is added correctly
+            generated_model_part = model_part.geometry
+
+            TestUtils.assert_almost_equal_geometries(expected_geometries[0], generated_model_part)
+
+    def test_add_gravity_load_two_layers_same_dimension(self, create_default_2d_soil_material: SoilMaterial):
+        """
+        Test if a gravity load is added correctly to the model in a 2d space containing 2 layers. A gravity load is
+        generated and added to the model.
+
+        Args:
+            - create_default_2d_soil_material (:class:`stem.soil_material.SoilMaterial`): A default soil material.
+
+        """
+
+        # create model
+        model = Model(2)
+
+        # add a 2d layer
+        model.add_soil_layer_by_coordinates([(0, 0, 0), (1, 0, 0), (1, 1, 0)], create_default_2d_soil_material, "soil1")
+        model.add_soil_layer_by_coordinates([(1, 0, 0), (0, 0, 0), (1, -1, 0)], create_default_2d_soil_material, "soil2")
+
+        model.synchronise_geometry()
+
+        # add gravity load
+        model._Model__add_gravity_load(-12,0)
+
+        assert len(model.process_model_parts) == 1
+
+        generated_geometry = model.process_model_parts[0].geometry
+
+        # check if number of points, lines, surfaces are correct, i.e. if the number of points, lines, surfaces are the
+        # same as the number of points, lines, surfaces of the model geometry
+        assert len(generated_geometry.points) == len(model.geometry.points) == 4
+        assert len(generated_geometry.lines) == len(model.geometry.lines) == 5
+        assert len(generated_geometry.surfaces) == len(model.geometry.surfaces) == 2
+
+        assert model.process_model_parts[0].name == "gravity_load_2d"
+        npt.assert_allclose(model.process_model_parts[0].parameters.value, [-12, 0, 0])
+        npt.assert_allclose(model.process_model_parts[0].parameters.active, [True, True, True])
+
+    def test_add_gravity_load_3d(self, create_default_3d_soil_material):
+        """
+        Test if a gravity load is added correctly to the model in a 3d space. A gravity load is generated and added to
+        the model.
+
+        Args:
+            - create_default_3d_soil_material (:class:`stem.soil_material.SoilMaterial`): A default soil material.
+
+        """
+
+        # create model
+        model = Model(3)
+        model.extrusion_length = [0, 0, 1]
+
+        # add a 2d layer
+        model.add_soil_layer_by_coordinates([(0, 0, 0), (1, 0, 0), (1, 1, 0)], create_default_3d_soil_material, "soil1")
+
+        model.synchronise_geometry()
+
+        # add gravity load
+        model._Model__add_gravity_load(vertical_axis=2, gravity_acceleration=-10)
+
+        assert len(model.process_model_parts) == 1
+
+        generated_geometry = model.process_model_parts[0].geometry
+
+        # check if number of points, lines, surfaces are correct, i.e. if the number of points, lines, surfaces and
+        # volumes are the same as the number of points, lines, surfaces and volumes of the model geometry
+        assert len(generated_geometry.points) == len(model.geometry.points) == 6
+        assert len(generated_geometry.lines) == len(model.geometry.lines) == 9
+        assert len(generated_geometry.surfaces) == len(model.geometry.surfaces) == 5
+        assert len(generated_geometry.volumes) == len(model.geometry.volumes) == 1
+
+        assert model.process_model_parts[0].name == "gravity_load_3d"
+        npt.assert_allclose(model.process_model_parts[0].parameters.value, [0, 0, -10])
+        npt.assert_allclose(model.process_model_parts[0].parameters.active, [True, True, True])
+
+    def test_setup_stress_initialisation(self, create_default_2d_soil_material: SoilMaterial):
+        """
+        Test if the stress initialisation is set up correctly. A model is created with a soil layer. It is checked if
+        gravity is added in case the K0 procedure or gravity loading is used.
+
+        Args:
+            - create_default_2d_soil_material (:class:`stem.soil_material.SoilMaterial`): A default soil material.
+
+        """
+
+        # set up solver settings
+        analysis_type = AnalysisType.MECHANICAL_GROUNDWATER_FLOW
+
+        solution_type = SolutionType.QUASI_STATIC
+
+        time_integration = TimeIntegration(start_time=0.0, end_time=1.0, delta_time=0.1, reduction_factor=0.5,
+                                           increase_factor=2.0, max_delta_time_factor=500)
+
+        convergence_criterion = DisplacementConvergenceCriteria()
+
+        stress_initialisation_type = StressInitialisationType.NONE
+
+        solver_settings = SolverSettings(analysis_type=analysis_type, solution_type=solution_type,
+                                         stress_initialisation_type=stress_initialisation_type,
+                                         time_integration=time_integration,
+                                         is_stiffness_matrix_constant=True, are_mass_and_damping_constant=True,
+                                         convergence_criteria=convergence_criterion)
+
+        # set up problem data
+        problem_data = Problem(problem_name="test", number_of_threads=2, settings=solver_settings)
+
+        model_no_gravity = Model(2)
+        model_no_gravity.project_parameters = problem_data
+
+        # set up soil material
+        soil_material = create_default_2d_soil_material
+        model_no_gravity.add_soil_layer_by_coordinates([(0, 0, 0), (1, 0, 0), (1, 1, 0)], soil_material, "soil1")
+        model_no_gravity.synchronise_geometry()
+
+        # setup_stress_initialisation
+        model_no_gravity._Model__setup_stress_initialisation()
+
+        model_k0 = Model(2)
+        model_k0.project_parameters = problem_data
+
+        model_k0.project_parameters.settings.stress_initialisation_type = StressInitialisationType.K0_PROCEDURE
+        model_k0.add_soil_layer_by_coordinates([(0, 0, 0), (1, 0, 0), (1, 1, 0)], soil_material, "soil1")
+        model_k0.synchronise_geometry()
+
+        # setup_stress_initialisation
+        model_k0._Model__setup_stress_initialisation()
+
+        model_gravity_loading = Model(2)
+        model_gravity_loading.project_parameters = problem_data
+
+        model_gravity_loading.project_parameters.settings.stress_initialisation_type = \
+            StressInitialisationType.GRAVITY_LOADING
+        model_gravity_loading.add_soil_layer_by_coordinates([(0, 0, 0), (1, 0, 0), (1, 1, 0)], soil_material, "soil1")
+        model_gravity_loading.synchronise_geometry()
+
+        # setup_stress_initialisation
+        model_gravity_loading._Model__setup_stress_initialisation()
+
+        assert len(model_no_gravity.process_model_parts) == 0
+        assert len(model_k0.process_model_parts) == 1
+        assert len(model_gravity_loading.process_model_parts) == 1
+
+        assert model_k0.process_model_parts[0].name == "gravity_load_2d"
+        assert model_gravity_loading.process_model_parts[0].name == "gravity_load_2d"
+
+    def test_setup_stress_initialisation_without_project_parameters(self):
+        """
+        A model is created without project parameters. It is
+        checked if a ValueError is raised while setting up the stress initialisation.
+
+        """
+        # create model
+        model = Model(2)
+
+        # test if value error is raised
+        with pytest.raises(ValueError,
+                           match=r"Project parameters must be set before setting up the stress initialisation"):
+            model._Model__setup_stress_initialisation()
+
+    @pytest.mark.skip("Not implemented yet")
+    def test_post_setup(self):
+        pass
diff --git a/tests/test_model_part.py b/tests/test_model_part.py
new file mode 100644
index 00000000..481e48c6
--- /dev/null
+++ b/tests/test_model_part.py
@@ -0,0 +1,269 @@
+import re
+
+import pytest
+
+from stem.load import PointLoad, LineLoad, SurfaceLoad, MovingLoad, GravityLoad
+from stem.boundary import DisplacementConstraint, RotationConstraint, AbsorbingBoundary
+from stem.structural_material import StructuralMaterial, EulerBeam, ElasticSpringDamper, NodalConcentrated
+from stem.model_part import ModelPart, BodyModelPart
+from stem.solver import AnalysisType
+
+from tests.utils import TestUtils
+
+
+class TestModelPart:
+
+    def test_get_element_name_loads(self):
+        """
+        Test the get_element_name method of the ModelPart class for load
+
+        """
+
+        # point load does not have element names
+        point_load = PointLoad([True, True, True], [0, 0, 0])
+        point_load_part = ModelPart("point_load_part")
+        point_load_part.parameters = point_load
+
+        assert point_load_part.get_element_name(2, 1, AnalysisType.MECHANICAL) is None
+        assert point_load_part.get_element_name(3, 1, AnalysisType.MECHANICAL) is None
+
+        # wrong point input
+        with pytest.raises(ValueError, match= "Point load can only be applied in mechanical or mechanical groundwater "
+                                              "flow analysis"):
+            point_load_part.get_element_name(2, 1, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError, match=re.escape(r'In 2 dimensions, only [1] noded Point load elements are supported. '
+                                              r'2 nodes were provided.')):
+            point_load_part.get_element_name(2, 2, AnalysisType.MECHANICAL)
+
+        # check line load names
+        line_load = LineLoad([True, True, True], [0, 0, 0])
+        line_load_part = ModelPart("line_load_part")
+        line_load_part.parameters = line_load
+
+        assert line_load_part.get_element_name(2, 2, AnalysisType.MECHANICAL) == "LineLoadCondition2D2N"
+        assert line_load_part.get_element_name(3, 2, AnalysisType.MECHANICAL) == "LineLoadCondition3D2N"
+        assert line_load_part.get_element_name(2, 3, AnalysisType.MECHANICAL) == "LineLoadDiffOrderCondition2D3N"
+        assert line_load_part.get_element_name(3, 3, AnalysisType.MECHANICAL) == "LineLoadCondition3D3N"
+
+        # wrong line_load input
+        with pytest.raises(ValueError, match= "Line load can only be applied in mechanical or mechanical groundwater "
+                                              "flow analysis"):
+            line_load_part.get_element_name(2, 2, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError,
+                           match=re.escape(r'In 2 dimensions, only [2, 3] noded Line load elements are supported. '
+                                           r'4 nodes were provided.')):
+            line_load_part.get_element_name(2, 4, AnalysisType.MECHANICAL)
+
+        # check surface load names
+        surface_load = SurfaceLoad([True, True, True], [0, 0, 0])
+        surface_load_part = ModelPart("surface_load_part")
+        surface_load_part.parameters = surface_load
+
+        assert surface_load_part.get_element_name(3, 3, AnalysisType.MECHANICAL) == "UPwFaceLoadCondition3D3N"
+        assert surface_load_part.get_element_name(3, 4, AnalysisType.MECHANICAL) == "UPwFaceLoadCondition3D4N"
+        assert surface_load_part.get_element_name(3, 6, AnalysisType.MECHANICAL) == "SurfaceLoadDiffOrderCondition3D6N"
+        assert surface_load_part.get_element_name(3, 8, AnalysisType.MECHANICAL) == "SurfaceLoadDiffOrderCondition3D8N"
+
+        # wrong surface_load input
+        with pytest.raises(ValueError, match= "Surface load can only be applied in mechanical or mechanical groundwater "
+                                              "flow analysis"):
+            surface_load_part.get_element_name(3, 3, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError,
+                           match=re.escape('In 3 dimensions, only [3, 4, 6, 8] noded Surface load elements are '
+                                           'supported. 9 nodes were provided.')):
+            surface_load_part.get_element_name(3, 9, AnalysisType.MECHANICAL)
+
+        # check moving load names
+        moving_load = MovingLoad([10, 10, 10], [1, 1, 1], 1, [0, 0, 0])
+        moving_load_part = ModelPart("moving_load_part")
+        moving_load_part.parameters = moving_load
+
+        assert moving_load_part.get_element_name(2, 2, AnalysisType.MECHANICAL) == "MovingLoadCondition2D2N"
+        assert moving_load_part.get_element_name(3, 2, AnalysisType.MECHANICAL) == "MovingLoadCondition3D2N"
+        assert moving_load_part.get_element_name(2, 3, AnalysisType.MECHANICAL) == "MovingLoadCondition2D3N"
+        assert moving_load_part.get_element_name(3, 3, AnalysisType.MECHANICAL) == "MovingLoadCondition3D3N"
+
+        # wrong moving_load input
+        with pytest.raises(ValueError, match= "Moving load can only be applied in mechanical or mechanical groundwater "
+                                              "flow analysis"):
+            moving_load_part.get_element_name(2, 2, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError,
+                           match=re.escape('In 3 dimensions, only [2, 3] noded Moving load elements are supported. '
+                                           '4 nodes were provided.')):
+            moving_load_part.get_element_name(3, 4, AnalysisType.MECHANICAL)
+
+        # gravity load does not have element names
+        gravity_load = GravityLoad([True, True, True], [0, 0, 0])
+        gravity_load_part = ModelPart("moving_load_part")
+        gravity_load_part.parameters = gravity_load
+
+        assert gravity_load_part.get_element_name(2, 2, AnalysisType.MECHANICAL) is None
+
+    def test_get_element_name_boundaries(self):
+        """
+        Tests the available element names for the boundary model parts, i.e. the displacement contstraint, rotation
+        constraints and absorbing boundaries
+        Returns:
+
+        """
+
+        # displacement constraint does not have element names
+        displacement_constraint = DisplacementConstraint([True, True, True],[True, True, True], [0, 0, 0])
+        displacement_constraint_part = ModelPart("displacement_constraint_part")
+        displacement_constraint_part.parameters = displacement_constraint
+
+        assert displacement_constraint_part.get_element_name(2, 2, AnalysisType.MECHANICAL) is None
+
+        # wrong displacement_constraint input
+        with pytest.raises(ValueError, match= "Displacement constraint can only be applied in mechanical or mechanical "
+                                              "groundwater flow analysis"):
+            displacement_constraint_part.get_element_name(2, 2, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError,
+                           match=re.escape('Number of dimensions 1 is not supported for Displacement constraint '
+                                           'elements. Supported dimensions are [2, 3].')):
+            displacement_constraint_part.get_element_name(1, 4, AnalysisType.MECHANICAL)
+
+        # rotation constraint does not have element names
+        rotation_constraint = RotationConstraint([True, True, True],[True, True, True], [0, 0, 0])
+        rotation_constraint_part = ModelPart("rotation_constraint_part")
+        rotation_constraint_part.parameters = rotation_constraint
+
+        assert rotation_constraint_part.get_element_name(2, 2, AnalysisType.MECHANICAL) is None
+
+        # wrong rotation_constraint input
+        with pytest.raises(ValueError, match= "Rotation constraint can only be applied in mechanical or mechanical "
+                                              "groundwater flow analysis"):
+            rotation_constraint_part.get_element_name(2, 2, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError,
+                           match=re.escape('Number of dimensions 1 is not supported for Rotation constraint elements. '
+                                           'Supported dimensions are [2, 3].')):
+            rotation_constraint_part.get_element_name(1, 4, AnalysisType.MECHANICAL)
+
+        # check absorbing boundary names
+        absorbing_boundary = AbsorbingBoundary([1, 1], 1)
+        absorbing_boundary_part = ModelPart("absorbing_boundary_part")
+        absorbing_boundary_part.parameters = absorbing_boundary
+
+        assert (absorbing_boundary_part.get_element_name(2, 2, AnalysisType.MECHANICAL)
+                == "UPwLysmerAbsorbingCondition2D2N")
+        assert (absorbing_boundary_part.get_element_name(2, 3, AnalysisType.MECHANICAL)
+                == "UPwLysmerAbsorbingCondition2D3N")
+        assert (absorbing_boundary_part.get_element_name(3, 3, AnalysisType.MECHANICAL)
+                == "UPwLysmerAbsorbingCondition3D3N")
+        assert (absorbing_boundary_part.get_element_name(3, 4, AnalysisType.MECHANICAL)
+                == "UPwLysmerAbsorbingCondition3D4N")
+
+        # wrong absorbing_boundary input
+        with pytest.raises(ValueError, match= "Absorbing boundary conditions can only be applied in mechanical or "
+                                              "mechanical groundwater flow analysis"):
+            absorbing_boundary_part.get_element_name(2, 2, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError,
+                           match=re.escape('In 3 dimensions, only [3, 4] noded Absorbing boundary elements are '
+                                           'supported. 6 nodes were provided.')):
+            absorbing_boundary_part.get_element_name(3, 6, AnalysisType.MECHANICAL)
+
+    def test_get_element_name_bodies(self):
+        """
+        Tests the available element names for the body model parts, i.e. the soil and the structure.
+
+        """
+
+        soil = TestUtils.create_default_soil_material(2)
+        soil_part = BodyModelPart("soil")
+        soil_part.material = soil
+
+        assert soil_part.get_element_name(2, 3, AnalysisType.MECHANICAL) == "UPwSmallStrainElement2D3N"
+        assert soil_part.get_element_name(2, 4, AnalysisType.MECHANICAL) == "UPwSmallStrainElement2D4N"
+        assert soil_part.get_element_name(2, 6, AnalysisType.MECHANICAL) == "SmallStrainUPwDiffOrderElement2D6N"
+        assert soil_part.get_element_name(2, 8, AnalysisType.MECHANICAL) == "SmallStrainUPwDiffOrderElement2D8N"
+
+        assert soil_part.get_element_name(3, 4, AnalysisType.MECHANICAL) == "UPwSmallStrainElement3D4N"
+        assert soil_part.get_element_name(3, 8, AnalysisType.MECHANICAL) == "UPwSmallStrainElement3D8N"
+        assert soil_part.get_element_name(3, 10, AnalysisType.MECHANICAL) == "SmallStrainUPwDiffOrderElement3D10N"
+        assert soil_part.get_element_name(3, 20, AnalysisType.MECHANICAL) == "SmallStrainUPwDiffOrderElement3D20N"
+
+        # wrong soil input
+        with pytest.raises(ValueError, match= 'Analysis type AnalysisType.GROUNDWATER_FLOW is not implemented yet for '
+                                              'soil material.'):
+            soil_part.get_element_name(2, 3, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError,
+                           match=re.escape('In 3 dimensions, only [4, 8, 10, 20] noded Soil elements are supported. '
+                                           '6 nodes were provided.')):
+            soil_part.get_element_name(3, 6, AnalysisType.MECHANICAL)
+
+        # check beam element names
+        beam = StructuralMaterial(name="beam", material_parameters=EulerBeam(2, 1, 1, 1, 1, 1))
+        beam_part = BodyModelPart("beam")
+        beam_part.material = beam
+
+        assert beam_part.get_element_name(2, 2, AnalysisType.MECHANICAL) == "GeoCrBeamElement2D2N"
+        assert beam_part.get_element_name(3, 2, AnalysisType.MECHANICAL) == "GeoCrBeamElement3D2N"
+
+        # wrong beam input
+
+        with pytest.raises(ValueError, match= 'Analysis type AnalysisType.GROUNDWATER_FLOW is not implemented '
+                                              'for euler beams.'):
+            beam_part.get_element_name(2, 2, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError,
+                           match=re.escape('In 3 dimensions, only [2] noded Euler beam elements are supported. '
+                                           '6 nodes were provided.')):
+            beam_part.get_element_name(3, 6, AnalysisType.MECHANICAL)
+
+        # check ElasticSpringDamper Element names
+        spring = StructuralMaterial(name="spring", material_parameters=ElasticSpringDamper([0, 0, 0], [0, 0, 0],
+                                                                                           [0, 0, 0], [0, 0, 0]))
+        spring_part = BodyModelPart("spring")
+        spring_part.material = spring
+
+        assert spring_part.get_element_name(2, 2, AnalysisType.MECHANICAL) == "SpringDamperElement2D"
+        assert spring_part.get_element_name(3, 2, AnalysisType.MECHANICAL) == "SpringDamperElement3D"
+
+        with pytest.raises(ValueError, match= 'Analysis type AnalysisType.GROUNDWATER_FLOW is not implemented '
+                                              'for elastic spring dampers.'):
+            spring_part.get_element_name(2, 2, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError,
+                           match=re.escape('In 3 dimensions, only [2] noded Elastic spring damper elements are '
+                                           'supported. 6 nodes were provided.')):
+            spring_part.get_element_name(3, 6, AnalysisType.MECHANICAL)
+
+        # check nodal concentrated element names
+        nodal_concentrated = StructuralMaterial(name="nodal_concentrated",
+                                                material_parameters=NodalConcentrated([0, 0, 0], 0, [0, 0, 0]))
+        nodal_concentrated_part = BodyModelPart("nodal_concentrated")
+        nodal_concentrated_part.material = nodal_concentrated
+
+        assert nodal_concentrated_part.get_element_name(2, 1, AnalysisType.MECHANICAL) == "NodalConcentratedElement2D1N"
+        assert nodal_concentrated_part.get_element_name(3, 1, AnalysisType.MECHANICAL) == "NodalConcentratedElement3D1N"
+
+        with pytest.raises(ValueError, match= 'Analysis type AnalysisType.GROUNDWATER_FLOW is not implemented for nodal '
+                                              'concentrated elements.'):
+            nodal_concentrated_part.get_element_name(2, 1, AnalysisType.GROUNDWATER_FLOW)
+
+        # wrong ndim nnodes combination
+        with pytest.raises(ValueError,
+                           match=re.escape('In 3 dimensions, only [1] noded Nodal concentrated elements are supported. '
+                                           '6 nodes were provided.')):
+            nodal_concentrated_part.get_element_name(3, 6, AnalysisType.MECHANICAL)
+
+
+
diff --git a/tests/test_plot_utils.py b/tests/test_plot_utils.py
new file mode 100644
index 00000000..35c70e85
--- /dev/null
+++ b/tests/test_plot_utils.py
@@ -0,0 +1,122 @@
+import pytest
+from pathlib import Path
+
+from stem.model import Model
+from stem.soil_material import SoilMaterial, OnePhaseSoil, LinearElasticSoil, SaturatedBelowPhreaticLevelLaw
+from stem.plot_utils import PlotUtils
+
+
+class TestPlotUtils:
+    """
+    Test class for the :class:`stem.plot_utils.PlotUtils` class.
+
+    """
+
+    @pytest.fixture
+    def create_default_2d_soil_material(self) -> SoilMaterial:
+        """
+        Create a default soil material for a 2D geometry.
+
+        Returns:
+            - :class:`stem.soil_material.SoilMaterial`: default soil material
+
+        """
+        # define soil material
+        ndim = 2
+        soil_formulation = OnePhaseSoil(ndim, IS_DRAINED=True, DENSITY_SOLID=2650, POROSITY=0.3)
+        constitutive_law = LinearElasticSoil(YOUNG_MODULUS=100e6, POISSON_RATIO=0.3)
+        soil_material = SoilMaterial(name="soil", soil_formulation=soil_formulation, constitutive_law=constitutive_law,
+                                     retention_parameters=SaturatedBelowPhreaticLevelLaw())
+        return soil_material
+
+    @pytest.fixture
+    def create_default_3d_soil_material(self) -> SoilMaterial:
+        """
+        Create a default soil material for a 3D geometry.
+
+        Returns:
+            - :class:`stem.soil_material.SoilMaterial`: default soil material
+
+        """
+        # define soil material
+        ndim = 3
+        soil_formulation = OnePhaseSoil(ndim, IS_DRAINED=True, DENSITY_SOLID=2650, POROSITY=0.3)
+        constitutive_law = LinearElasticSoil(YOUNG_MODULUS=100e6, POISSON_RATIO=0.3)
+        soil_material = SoilMaterial(name="soil", soil_formulation=soil_formulation, constitutive_law=constitutive_law,
+                                     retention_parameters=SaturatedBelowPhreaticLevelLaw())
+        return soil_material
+
+    @staticmethod
+    def create_geometry_plot_and_assert(ndim: int, material: SoilMaterial) -> None:
+        """
+        Create a geometry and plots it.
+
+        Args:
+            - ndim (int): dimension of the geometry
+            - material (:class:`stem.soil_material.SoilMaterial`): soil material
+
+        """
+        layer1_coordinates = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
+        layer2_coordinates = [(1, 1, 0), (0, 1, 0), (0, 2, 0), (1, 2, 0)]
+
+        # define soil materials
+        soil_material1 = material
+        soil_material1.name = "soil1"
+
+        soil_material2 = material
+        soil_material2.name = "soil2"
+
+        # create model
+        model = Model(ndim)
+        model.extrusion_length = [0, 0, 1]
+
+        # add soil layers
+        model.add_soil_layer_by_coordinates(layer1_coordinates, soil_material1, "layer1")
+        model.add_soil_layer_by_coordinates(layer2_coordinates, soil_material2, "layer2")
+
+        # synchronise geometry
+        model.synchronise_geometry()
+
+        # create figure
+        fig = PlotUtils.create_geometry_figure(model.ndim, model.geometry, True, True, True, True)
+
+        # save to eps for testing
+        fig.savefig(f"tests/generated_geometry_{ndim}D.eps", format="eps", bbox_inches="tight", pad_inches=0.1)
+
+        with open(f"tests/generated_geometry_{ndim}D.eps", "r") as f:
+            generated_geometry = f.readlines()
+
+        with open(f"tests/test_data/expected_geometry_{ndim}D.eps", "r") as f:
+            expected_geometry = f.readlines()
+
+        # skip checking header lines
+        n_header_lines = 9
+        for i in range(n_header_lines, len(generated_geometry)):
+            assert generated_geometry[i] == expected_geometry[i]
+
+        # remove generated file
+        Path(f"tests/generated_geometry_{ndim}D.eps").unlink()
+
+    def test_plot_geometry_2D(self, create_default_2d_soil_material: SoilMaterial):
+        """
+        Test the plot of a 2D geometry.
+
+        Args:
+            - create_default_2d_soil_material (:class:`stem.soil_material.SoilMaterial`): default soil material
+
+        """
+
+        # create geometry and plot it
+        self.create_geometry_plot_and_assert(2, create_default_2d_soil_material)
+
+    def test_plot_geometry_3D(self, create_default_3d_soil_material: SoilMaterial):
+        """
+        Test the plot of a 3D geometry.
+
+        Args:
+            - create_default_3d_soil_material (:class:`stem.soil_material.SoilMaterial`): default soil material
+
+        """
+
+        # create geometry and plot it
+        self.create_geometry_plot_and_assert(3, create_default_3d_soil_material)
diff --git a/tests/test_table.py b/tests/test_table.py
new file mode 100644
index 00000000..2e49159c
--- /dev/null
+++ b/tests/test_table.py
@@ -0,0 +1,24 @@
+import pytest
+
+from stem.table import Table
+
+
+class TestTable:
+
+    def test_correct_table_creation(self):
+        """
+        Tests that table is created correctly
+        """
+        _time = [0, 1, 2, 3, 4, 5]
+        _value1 = [0, 5, 10, 5, 0, 0]
+
+        Table(times=_time, values=_value1)
+
+        _value2 = [0, 5, 10, 5, 0, 0, 10, 2]
+                       
+        _msg1 = ("Dimension mismatch between times and values in table:\n"
+                 f" - times: {len(_time)}\n"
+                 f" - values: {len(_value2)}\n")
+        with pytest.raises(ValueError, match=_msg1):
+            Table(times=_time, values=_value2)
+        
\ No newline at end of file
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 00000000..ca64497c
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,186 @@
+import re
+
+import numpy as np
+import numpy.testing as npt
+import pytest
+
+from stem.utils import Utils
+from tests.utils import TestUtils
+
+
+class TestUtilsStem:
+
+    def test_check_ndim_nnodes_combinations(self):
+        """
+        Test the check which checks if the combination of n_dim and n_nodes is valid for a certain element type.
+        It checks for valid input, invalid n_dim and invalid n_nodes.
+
+        """
+
+        # successful checks
+        Utils.check_ndim_nnodes_combinations(2,2, {2: [2]}, "test")
+
+        # non valid n_dim
+        with pytest.raises(ValueError, match=re.escape(f"Number of dimensions 3 is not supported for failed_test "
+                                                       f"elements. Supported dimensions are [2].")):
+            Utils.check_ndim_nnodes_combinations(3,2, {2: [2]},
+                                                 "failed_test")
+
+        # non valid n_nodes for dim
+        with pytest.raises(ValueError, match=re.escape(f"In 2 dimensions, only [3, 4, 5] noded failed_test_2 elements "
+                                                       f"are supported. 2 nodes were provided.")):
+            Utils.check_ndim_nnodes_combinations(2,2, {2: [3,4,5]},
+                                                 "failed_test_2")
+
+
+    def test_is_clockwise(self):
+        """
+        Test the check which checks if coordinates are given in clockwise order
+        """
+
+        coordinates = [[0, 0], [2, 0], [2, 2], [0, 2]]
+
+        assert not Utils.are_2d_coordinates_clockwise(coordinates=coordinates)
+        assert Utils.are_2d_coordinates_clockwise(coordinates=coordinates[::-1])
+
+    def test_is_clockwise_non_convex(self):
+        """
+        Test the check which checks if coordinates are given in clockwise order for a non-convex polygon
+
+        """
+
+        coordinates = [[0, 0], [2, 0], [2, 2], [1, -1], [0, 2]]
+
+        assert not Utils.are_2d_coordinates_clockwise(coordinates=coordinates)
+        assert Utils.are_2d_coordinates_clockwise(coordinates=coordinates[::-1])
+
+    def test_collinearity_2d(self):
+        """
+        Check collinearity between 3 points in 2D
+        """
+        p1 = np.array([0, 0])
+        p2 = np.array([-2, -1])
+
+        p_test_1 = np.array([2, 1])
+        p_test_2 = np.array([-5, 1])
+
+        assert Utils.is_collinear(point=p_test_1, start_point=p1, end_point=p2)
+        assert not Utils.is_collinear(point=p_test_2, start_point=p1, end_point=p2)
+
+    def test_collinearity_3d(self):
+        """
+        Check collinearity between 3 points in 3D
+        """
+
+        p1 = np.array([0, 0, 0])
+        p2 = np.array([-2, -2, 2])
+
+        p_test_1 = np.array([2, 2, -2])
+        p_test_2 = np.array([2, -2, 2])
+
+        assert Utils.is_collinear(point=p_test_1, start_point=p1, end_point=p2)
+        assert not Utils.is_collinear(point=p_test_2, start_point=p1, end_point=p2)
+
+    def test_is_in_between_2d(self):
+        """
+        Check if point is in between other 2 points in 2D
+        """
+
+        p1 = np.array([0, 0])
+        p2 = np.array([-2, -2])
+
+        p_test_1 = np.array([2, 2])
+        p_test_2 = np.array([-1, -1])
+
+        assert not Utils.is_point_between_points(
+            point=p_test_1, start_point=p1, end_point=p2
+        )
+        assert Utils.is_point_between_points(
+            point=p_test_2, start_point=p1, end_point=p2
+        )
+
+    def test_is_in_between_3d(self):
+        """
+        Check if point is in between other 2 points in 3D
+        """
+
+        p1 = np.array([0, 0, 0])
+        p2 = np.array([-2, -2, 2])
+
+        p_test_1 = np.array([2, 2, -2])
+        p_test_2 = np.array([-1, -1, 1])
+
+        assert not Utils.is_point_between_points(
+            point=p_test_1, start_point=p1, end_point=p2
+        )
+        assert Utils.is_point_between_points(
+            point=p_test_2, start_point=p1, end_point=p2
+        )
+
+    def test_check_sequence_non_string(self):
+        """
+        Test assertion of non-string sequence
+        """
+
+        assert Utils.is_non_str_sequence([1, 2, 3, 4])
+        assert Utils.is_non_str_sequence((1, 2, 3, 4))
+
+        assert not Utils.is_non_str_sequence({"Hello world"})
+        assert not Utils.is_non_str_sequence({1: {"a": "A"}, 2: {"b": "B"}})
+
+    def test_chain_sequence(self):
+        """
+        Test chain of sequences
+        """
+
+        # test 1: no conflicts in arguments, merge dictionaries with common keys
+        # new keys are added, common keys are expanded
+        seq1 = [1, 2, 3, 4]
+        seq2 = (100, 200, 300)
+
+        actual_sequence = list(Utils.chain_sequence([seq1, seq2]))
+        expected_sequence = [1, 2, 3, 4, 100, 200, 300]
+
+        npt.assert_equal(expected_sequence, actual_sequence)
+
+    def test_merge(self):
+        """
+        Test merging of dictionaries
+        """
+
+        # test 1: no conflicts in arguments, merge dictionaries with common keys
+        # new keys are added, common keys are expanded
+        dict1a = {1: {"a": "A"}, 2: {"b": "B"}}
+        dict1b = {2: {"c": "C"}, 3: {"d": "D"}}
+
+        actual_dict_1 = Utils.merge(dict1a, dict1b)
+        expected_dict_1 = {1: {"a": "A"}, 2: {"c": "C", "b": "B"}, 3: {"d": "D"}}
+
+        # expect it raises an error
+        TestUtils.assert_dictionary_almost_equal(expected_dict_1, actual_dict_1)
+
+        # test 2: conflicts in arguments, merge common keys argument into a list
+        # common keys are expanded into list
+        dict2a = {1: {"a": [1, 2, 3]}, 2: {"b": "B"}}
+        dict2b = {1: {"a": [5, 6]}, 2: {"b": "D"}, 3: {"d": "D"}}
+
+        # expect it raises an error
+        with pytest.raises(
+                ValueError, match="Conflict of merging keys at 2->b. Two non sequence values have been found."
+        ):
+            Utils.merge(dict2a, dict2b)
+
+        # test 3: conflicts in arguments, merge common keys argument into a list
+        # list of lists is preserved into a list of list + 2 extra elements
+        # tuple(int) and list(str) are merged into list(str,int)
+        dict3a = {1: {"a": [[1, 2, 3]]}, 2: {"b": ["B"]}}
+        dict3b = {1: {"a": [5, 6]}, 2: {"b": (1, 2, 3)}, 3: {"d": "D"}}
+
+        actual_dict_3 = Utils.merge(dict3a, dict3b)
+        expected_dict_3 = {
+            1: {"a": [[1, 2, 3], 5, 6]},
+            2: {"b": ["B", 1, 2, 3]},
+            3: {"d": "D"},
+        }
+
+        TestUtils.assert_dictionary_almost_equal(expected_dict_3, actual_dict_3)
diff --git a/tests/utils.py b/tests/utils.py
index 99044283..2a6bb15a 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,19 +1,70 @@
-import pprint
 from typing import Dict, Any
 
 import numpy.testing as npt
-
+from stem.geometry import Geometry
+from stem.soil_material import SoilMaterial, OnePhaseSoil, LinearElasticSoil, SaturatedBelowPhreaticLevelLaw
+from stem.solver import (AnalysisType, SolutionType, TimeIntegration, DisplacementConvergenceCriteria,
+                         StressInitialisationType, SolverSettings, Problem)
 
 class TestUtils:
 
+    @staticmethod
+    def create_default_soil_material(ndim: int):
+        """
+        Creates a default soil material.
+
+        Args:
+            - ndim (int): number of dimensions of the model
+
+        Returns:
+            - :class:`stem.soil_material.SoilMaterial`: default soil material
+
+        """
+        soil_formulation = OnePhaseSoil(ndim, IS_DRAINED=True, DENSITY_SOLID=2650, POROSITY=0.3)
+        constitutive_law = LinearElasticSoil(YOUNG_MODULUS=100e6, POISSON_RATIO=0.3)
+        soil_material = SoilMaterial(name="soil", soil_formulation=soil_formulation, constitutive_law=constitutive_law,
+                                     retention_parameters=SaturatedBelowPhreaticLevelLaw())
+
+        return soil_material
+
+    @staticmethod
+    def create_default_solver_settings() -> Problem:
+        """
+        Sets default solver settings. Which are required to write the mesh and project parameters.
+
+        Returns:
+            - :class:`stem.solver.Problem`: the Problem object containing the solver settings.
+
+        """
+        # set up solver settings
+        analysis_type = AnalysisType.MECHANICAL_GROUNDWATER_FLOW
+
+        solution_type = SolutionType.QUASI_STATIC
+
+        stress_initialisation_type = StressInitialisationType.NONE
+
+        time_integration = TimeIntegration(start_time=0.0, end_time=1.0, delta_time=0.1, reduction_factor=0.5,
+                                           increase_factor=2.0, max_delta_time_factor=500)
+
+        convergence_criteria = DisplacementConvergenceCriteria()
+
+        solver_settings = SolverSettings(analysis_type=analysis_type, solution_type=solution_type,
+                                         stress_initialisation_type=stress_initialisation_type,
+                                         time_integration=time_integration,
+                                         is_stiffness_matrix_constant=False, are_mass_and_damping_constant=False,
+                                         convergence_criteria=convergence_criteria)
+
+        # set up problem data
+        return Problem(problem_name="test", number_of_threads=2, settings=solver_settings)
+
     @staticmethod
     def assert_dictionary_almost_equal(expected: Dict[Any, Any], actual: Dict[Any, Any]):
         """
         Checks whether two dictionaries are equal.
 
         Args:
-            expected: Expected dictionary.
-            actual: Actual dictionary.
+            - expected: Expected dictionary.
+            - actual: Actual dictionary.
 
         """
 
@@ -36,4 +87,44 @@ def assert_dictionary_almost_equal(expected: Dict[Any, Any], actual: Dict[Any, A
                         npt.assert_allclose(v_i, actual_i)
 
             else:
-                npt.assert_allclose(v, actual[k])
\ No newline at end of file
+                npt.assert_allclose(v, actual[k])
+
+    @staticmethod
+    def assert_almost_equal_geometries(expected_geometry: Geometry, actual_geometry:Geometry):
+        """
+        Checks whether two Geometries are (almost) equal.
+
+        Args:
+            - expected_geometry (:class:`stem.geometry.Geometry`): expected geometry of the model
+            - actual_geometry (:class:`stem.geometry.Geometry`): actual geometry of the model
+
+        Returns:
+
+        """
+        # check if points are added correctly
+        for (generated_point_id, generated_point), (expected_point_id, expected_point) in \
+                zip(actual_geometry.points.items(), expected_geometry.points.items()):
+            assert generated_point_id == expected_point_id
+            assert generated_point.id == expected_point.id
+            npt.assert_allclose(generated_point.coordinates, expected_point.coordinates)
+
+        # check if lines are added correctly
+        for (generated_lines_id, generated_line), (expected_line_id, expected_line) in \
+                zip(actual_geometry.lines.items(), expected_geometry.lines.items()):
+            assert generated_lines_id == expected_line_id
+            assert generated_line.id == expected_line.id
+            npt.assert_equal(generated_line.point_ids, expected_line.point_ids)
+
+        # check if surfaces are added correctly
+        for (generated_surface_id, generated_surface), (expected_surface_id, expected_surface) in \
+                zip(actual_geometry.surfaces.items(), expected_geometry.surfaces.items()):
+            assert generated_surface_id == expected_surface_id
+            assert generated_surface.id == expected_surface.id
+            npt.assert_equal(generated_surface.line_ids, expected_surface.line_ids)
+
+        # check if volumes are added correctly
+        for (generated_volume_id, generated_volume), (expected_volume_id, expected_volume) in \
+                zip(actual_geometry.volumes.items(), expected_geometry.volumes.items()):
+            assert generated_volume_id == expected_volume_id
+            assert generated_volume.id == expected_volume.id
+            npt.assert_equal(generated_volume.surface_ids, expected_volume.surface_ids)