# Routine to store some methods to work with meshes

from dolfin import *

import meshio

import numpy as np

from copy import copy, deepcopy

from scipy.spatial import KDTree

from ...PythonicUtilities import programming_tools

from ...PythonicUtilities.dictionary_tools import sort_dictionary_by_values, get_first_key_from_value

from ...CuboidGmsh.tool_box import meshing_tools as tools_gmsh

from ...CuboidGmsh.solids import cuboid_prisms as prism_gmsh

from .parallelization_tools import mpi_print, mpi_execute_function, mpi_barrier

# Defines a class for the mesh data

class MeshData:

    def __init__(self, mesh, dx, ds, n, x, domain_meshCollection, 
    domain_meshFunction, boundary_meshCollection, boundary_meshFunction, 
    domain_physicalGroupsNameToTag, boundary_physicalGroupsNameToTag,
    verbose, mesh_file=None, comm=None):
        
        # Saves the mesh parameters

        self.mesh = mesh
        
        self.dx = dx
        
        self.ds = ds
        
        self.n = n

        self.x = x
        
        self.domain_meshCollection = domain_meshCollection 

        self.domain_meshFunction = domain_meshFunction
        
        self.boundary_meshCollection = boundary_meshCollection
        
        self.boundary_meshFunction = boundary_meshFunction

        self.domain_physicalGroupsNameToTag = domain_physicalGroupsNameToTag
        
        self.boundary_physicalGroupsNameToTag = boundary_physicalGroupsNameToTag
        
        self.verbose = verbose

        self.mesh_file = mesh_file

        # Stores the parallel communicator and the rank number of pro-
        # cessors only if it was really created. Since dumb instances of
        # this class can be created. If a dumb instance is created and a
        # method eventually requires the comm object, the attribute will
        # be non-existant and an error will be thrown

        self.comm = comm

        if comm is not None:

            self.rank = MPI.rank(comm)

########################################################################
#                              Mesh files                              #
########################################################################

# Defines a function to create a simple box mesh

def create_box_mesh(length_x, length_y, length_z, n_divisions_x,
n_divisions_y, n_divisions_z, verbose=False, file_name="box_mesh",
file_directory=None, n_subdomains_z=1, bias_x=1.0, bias_y=1.0, bias_z=
1.0, convert_to_xdmf=True, mesh_polinomial_order=1):
    
    # Defines the names of the surface physical groups

    surface_regions_names = ["bottom", "front", "right", "back", "left",
    "top"]

    # Defines the names of the volumetric physical groups

    volume_regions_names = []

    for i in range(n_subdomains_z):

        volume_regions_names.append("volume "+str(i+1))

    # Updates the transfinite directions by dividing it into the subdo-
    # mains in the z direction

    corrected_n_divisions_z = int(round(max(2, n_divisions_z/
    n_subdomains_z)))

    # Updates the length in z

    corrected_length_z = length_z/n_subdomains_z

    # Uses CuboidGmsh to avoid topology loss with fenics built-in meshes

    geometric_data = tools_gmsh.gmsh_initialization(
    surface_regionsNames=surface_regions_names, volume_regionsNames=
    volume_regions_names)

    # Iterates through the subdomains in the z direction

    for i in range(n_subdomains_z):

        # Gets the corner points

        corner_points = [[length_x, 0.0, (i*corrected_length_z)], [
        length_x, length_y, (i*corrected_length_z)], [0.0, length_y, (i*
        corrected_length_z)], [0.0, 0.0, (i*corrected_length_z)], [
        length_x, 0.0, ((i+1)*corrected_length_z)], [length_x, length_y, 
        ((i+1)*corrected_length_z)], [0.0, length_y, ((i+1)*
        corrected_length_z)], [0.0, 0.0, ((i+1)*corrected_length_z)]]

        # Gets the surface regions

        explicit_surface_physical_group_name = {2: "front", 3: "right", 
        4: "back", 5: "left"}

        # If it is the first cuboid, adds the bottom surface

        if i==0:

            explicit_surface_physical_group_name[1] = "bottom"

        # If it is the last cuboid, adds the top surface

        if i==(n_subdomains_z-1):

            explicit_surface_physical_group_name[6] = "top"

        # Adds the cuboid

        geometric_data = prism_gmsh.hexahedron_from_corners(
        corner_points, transfinite_directions=[n_divisions_x, n_divisions_y, 
        corrected_n_divisions_z], geometric_data=geometric_data, 
        explicit_volume_physical_group_name="volume "+str(i+1), 
        explicit_surface_physical_group_name=
        explicit_surface_physical_group_name, bias_directions={"x": 
        bias_x, "y": bias_y, "z": bias_z})

    tools_gmsh.gmsh_finalize(geometric_data=geometric_data, file_name=
    file_name, verbose=verbose, file_directory=file_directory, 
    convert_to_xdmf=convert_to_xdmf, mesh_polynomialOrder=
    mesh_polinomial_order)

# Defines a function to read a gmsh mesh from a msh file. This function
# handles parallelization aswell

@programming_tools.optional_argumentsInitializer({'desired_elements':
lambda: ['tetra', 'triangle'], 'data_sets': lambda: ["domain", ("bound"+
"ary")]})

def read_mshMesh(given_file_name, desired_elements=None, data_sets=None, 
quadrature_degree=2, verbose=False, comm=None, automatic_comm_generation=
False):
    
    # Copies the file name

    file_name = deepcopy(given_file_name)
    
    # If a comm object is to be automatically generated for paralleliza-
    # tion

    if automatic_comm_generation and (comm is None):

        comm = MPI.comm_world

    # Calls the engine to read the msh mesh and transform it into a xdmf
    # file. Uses the first processor only

    (file_name, domain_physicalGroupsNameToTag, 
    boundary_physicalGroupsNameToTag) = mpi_execute_function(comm, 
    gmsh_mesh_reading_engine, file_name, desired_elements=
    desired_elements, data_sets=data_sets, verbose=verbose, 
    comm_object=comm)

    # Creates a barrier for all processors to synchronize here

    mpi_barrier(comm)

    # Calls the engine to read the xdmf mesh 

    return read_xdmfMesh(file_name, domain_physicalGroupsNameToTag=
    domain_physicalGroupsNameToTag, boundary_physicalGroupsNameToTag=
    boundary_physicalGroupsNameToTag, quadrature_degree=
    quadrature_degree, verbose=verbose, comm=comm)

# Defines a function to read a mesh from a msh file

@programming_tools.optional_argumentsInitializer({'desired_elements':
lambda: ['tetra', 'triangle'], 'data_sets': lambda: ["domain", ("bound"+
"ary")]})

def gmsh_mesh_reading_engine(file_name, desired_elements=None, data_sets=
None, verbose=False, comm_object=None):

    # Tests if the file name is a dictionary, which means the mesh is to
    # be created using FEniCS built-in meshes

    if isinstance(file_name, dict):

        # Tests if the lengths are in the set of keys

        length_x = 0.0

        length_y = 0.0

        length_z = 0.0

        if "length x" in file_name:

            length_x = file_name["length x"]

        else:

            raise KeyError("'file_name' is a dictionary, so a built-in"+
            " mesh is to be used, but no key 'length x' was provided. "+
            "The keys provided are: "+str(file_name.keys()))

        if "length y" in file_name:

            length_y = file_name["length y"]

        else:

            raise KeyError("'file_name' is a dictionary, so a built-in"+
            " mesh is to be used, but no key 'length y' was provided. "+
            "The keys provided are: "+str(file_name.keys()))

        if "length z" in file_name:

            length_z = file_name["length z"]

        else:

            raise KeyError("'file_name' is a dictionary, so a built-in"+
            " mesh is to be used, but no key 'length z' was provided. "+
            "The keys provided are: "+str(file_name.keys()))

        # Adds the divisions

        n_divisions_x = 0

        n_divisions_y = 0

        n_divisions_z = 0

        if "number of divisions in x" in file_name:

            n_divisions_x = file_name["number of divisions in x"]

        else:

            raise KeyError("'file_name' is a dictionary, so a built-in"+
            " mesh is to be used, but no key 'number of divisions in x"+
            "' was provided. The keys provided are: "+str(
            file_name.keys()))

        if "number of divisions in y" in file_name:

            n_divisions_y = file_name["number of divisions in y"]

        else:

            raise KeyError("'file_name' is a dictionary, so a built-in"+
            " mesh is to be used, but no key 'number of divisions in y"+
            "' was provided. The keys provided are: "+str(
            file_name.keys()))

        if "number of divisions in z" in file_name:

            n_divisions_z = file_name["number of divisions in z"]

        else:

            raise KeyError("'file_name' is a dictionary, so a built-in"+
            " mesh is to be used, but no key 'number of divisions in z"+
            "' was provided. The keys provided are: "+str(
            file_name.keys()))
        
        # Adds the verbose flag
        
        verbose_gmsh = verbose
        
        if "verbose" in file_name:

            verbose_gmsh = file_name["verbose"]
        
        # Verifies how many subdomains are to be made in the z direction

        n_subdomains_z = 1

        if "number of subdomains in z direction" in file_name:

            n_subdomains_z = file_name["number of subdomains in z dire"+
            "ction"]

        # Adds the file parameters

        mesh_directory = None

        if "mesh file directory" in file_name:

            mesh_directory = file_name["mesh file directory"]

        else:

            raise KeyError("'file_name' is a dictionary, so a built-in"+
            " mesh is to be used, but no key 'mesh file directory' was"+
            " provided. The keys provided are: "+str(file_name.keys()))
        
        # Selects the biases

        bias_x = 1.0

        bias_y = 1.0

        bias_z = 1.0

        if "bias x" in file_name:

            bias_x = file_name["bias x"]

        if "bias y" in file_name:

            bias_y = file_name["bias y"]

        if "bias z" in file_name:

            bias_z = file_name["bias z"]

        # Overwrites the file name to the file name dictionary

        if "mesh file name" in file_name:

            file_name = file_name["mesh file name"]

        else:

            raise KeyError("'file_name' is a dictionary, so a built-in"+
            " mesh is to be used, but no key 'mesh file name' was prov"+
            "ided. The keys provided are: "+str(file_name.keys()))

        # Retuns the built in mesh

        create_box_mesh(length_x, length_y, length_z, 
        n_divisions_x, n_divisions_y, n_divisions_z, verbose=
        verbose_gmsh, file_name=file_name, file_directory=
        mesh_directory, n_subdomains_z=n_subdomains_z, bias_x=bias_x,
        bias_y=bias_y, bias_z=bias_z)

        file_name = mesh_directory+"//"+file_name

    # Reads the saved gmsh mesh using meshio

    mesh_reading = meshio.read(file_name+".msh")

    # Initializes the dictionary of cells and the list of cell data

    cells_dictionary = dict()

    cell_dataList = []

    # Recovers the physical groups

    domain_physicalGroupsNameToTag = dict()

    boundary_physicalGroupsNameToTag = dict()

    # Gets the physical group through the field data

    field_data = mesh_reading.field_data

    # Iterates through its keys

    for physical_group, tag_dimensionality in field_data.items():

        # If the dimensionality is 2, gets the boundary

        if tag_dimensionality[1]==2:

            boundary_physicalGroupsNameToTag[physical_group] = int(
            tag_dimensionality[0])

        # If the dimensionality is 3, gets the domain

        elif tag_dimensionality[1]==3:

            domain_physicalGroupsNameToTag[physical_group] = int(
            tag_dimensionality[0])

    # Initializes a dictionary whose keys are the element types and the
    # values are the list of physical groups tags to which each element
    # belongs

    physical_groupsElements = dict()

    # Iterates through the elements

    for element in desired_elements:

        # Gets a list of tags

        list_ofTags = []

        try:

            list_ofTags = list(mesh_reading.cell_data_dict["gmsh:physi"+
            "cal"][element])

        except:

            raise KeyError("The required element '"+str(element)+"' ha"+
            "s not been found. Check out the mesh you are providing. P"+
            "robably, either the volume or the boundary hadn't been sa"+
            "ved")

        # Iterates through the domain physical groups
        
        for physical_groupName, physical_groupTag in domain_physicalGroupsNameToTag.items():
        
            # Counts the number of elements that belong to this physical
            # group

            n_elements = list_ofTags.count(physical_groupTag)

            # If there is at least one element, saves the number into 
            # the dictionary of physical groups

            if n_elements>0:

                try:

                    physical_groupsElements[physical_groupName] += (";"+
                    " "+str(n_elements)+" "+str(element)+" elements")

                except:

                    physical_groupsElements[physical_groupName] = str(
                    n_elements)+" "+str(element)+" elements" 

        # Iterates through the domain physical groups
        
        for physical_groupName, physical_groupTag in boundary_physicalGroupsNameToTag.items():
        
            # Counts the number of elements that belong to this physical
            # group

            n_elements = list_ofTags.count(physical_groupTag)

            # If there is at least one element, saves the number into 
            # the dictionary of physical groups

            if n_elements>0:

                try:

                    physical_groupsElements[physical_groupName] += (";"+
                    " "+str(n_elements)+" "+str(element)+" elements")

                except:

                    physical_groupsElements[physical_groupName] = str(
                    n_elements)+" "+str(element)+" elements" 

    mpi_print(comm_object, "##########################################"+
    "##############################\n#                        Mesh - p"+
    "hysical groups                        #\n########################"+
    "################################################\n")

    mpi_print(comm_object, "Finds the following domain physical groups"+
    " with their respective tags:")

    for physical_group, tag in domain_physicalGroupsNameToTag.items():

        mpi_print(comm_object, physical_group, "-", tag)

        try: 

            mpi_print(comm_object, "      - "+physical_groupsElements[
            physical_group])

        except:

            raise ValueError("The physical group "+str(physical_group)+
            " does not have any elements in it.")
        
        mpi_print(comm_object, "")

    mpi_print(comm_object, "\n\nFinds the following boundary physical "+
    "groups with their respective tags:")

    for physical_group, tag in boundary_physicalGroupsNameToTag.items():

        mpi_print(comm_object, physical_group, "-", tag)

        try: 

            mpi_print(comm_object, "      - "+physical_groupsElements[
            physical_group])

        except:

            raise ValueError("The physical group "+str(physical_group)+
            " does not have any elements in it.")
        
        mpi_print(comm_object, "")

    mpi_print(comm_object, "\n")

    # Gets the cells which consist of the desired element

    for i in range(len(desired_elements)):

        mpi_print(comm_object, "Saves the mesh of", data_sets[i], "dat"+
        "aset\n")

        cells_dictionary[desired_elements[i]] = (
        mesh_reading.get_cells_type(desired_elements[i]))

        mpi_print(comm_object, "There are "+str(len(cells_dictionary[
        desired_elements[i]]))+" "+str(desired_elements[i])+" elements"+
        " in the mesh.\n")

        # Gets the physical cell data for this element

        cell_dataPhysical = mesh_reading.get_cell_data("gmsh:physical", 
        desired_elements[i])

        # Gets the geometric cell data for this element

        cell_dataGeometric = mesh_reading.get_cell_data("gmsh:geometri"+
        "cal", desired_elements[i])

        # Adds the geometric cell data to the list of cell data

        cell_dataList.append(cell_dataGeometric)

        # Creates a mesh for this data set and saves it

        mesh_set = meshio.Mesh(points=mesh_reading.points, cells={
        desired_elements[i]: cells_dictionary[desired_elements[i]]},
        cell_data={data_sets[i]: [cell_dataPhysical]})

        meshio.write(file_name+"_"+data_sets[i]+".xdmf", mesh_set)

    # Creates the mesh with all information, including geometric infor-
    # mation and saves it
    
    whole_mesh = meshio.Mesh(points=mesh_reading.points, cells=
    cells_dictionary, cell_data={"whole_mesh": cell_dataList})

    meshio.write(file_name+".xdmf", whole_mesh)

    mpi_print(comm_object, "##########################################"+
    "##############################\n#                    Mesh reading"+
    " has been finalized                   #\n########################"+
    "################################################\n")

    # Then, calls the xdmf reader and returns its output

    return (file_name, domain_physicalGroupsNameToTag, 
    boundary_physicalGroupsNameToTag)

# Defines a function to read a mesh from a xdmf file

@programming_tools.optional_argumentsInitializer({('domain_physicalGro'+
'upsNameToTag'): lambda: dict(), 'boundary_physicalGroupsNameToTag': 
lambda: dict()})

def read_xdmfMesh(file_name, domain_physicalGroupsNameToTag=None, 
boundary_physicalGroupsNameToTag=None, quadrature_degree=2, verbose=
False, comm=None):
    
    # Sets the compiler parameters

    parameters["form_compiler"]["representation"] = "uflacs"

    parameters["allow_extrapolation"] = True

    parameters["form_compiler"]["cpp_optimize"] = True

    parameters["form_compiler"]["quadrature_degree"] = quadrature_degree

    # Verifies the dictionaries of names to tags
    
    if len(domain_physicalGroupsNameToTag.keys())==0 or (len(
    boundary_physicalGroupsNameToTag.keys())==0):
        
        mpi_print(comm, "WARNING: the dictionaries of physical groups' names to "+
        "tags are empty, thus, it won't be possible to use the names i"+
        "n the variational forms. Use a .msh mesh file directly instea"+
        "d.")

    # Verifies whether there is an extension in the file name

    if ("." in file_name) or ('.' in file_name):

        raise NameError("The name of the mesh file, "+file_name+", has"+
        " an extension. This name musn't have a extension, for xdmf fi"+
        "les only can be read with this method.\n")

    # Initializes the mesh object and reads the xdmf file

    mesh = None 

    if comm is None:

        mesh = Mesh()

    else:

        mesh = Mesh(comm)

    # Initializes a mesh value collection to store mesh data of the do-
    # main

    domain_meshCollection = MeshValueCollection("size_t", mesh, 
    mesh.topology().dim())

    # Reads the mesh with domain physical groups

    if comm is None:

        with XDMFFile(file_name+"_domain.xdmf") as infile:

            infile.read(mesh)

            infile.read(domain_meshCollection, "domain")

    else:

        with XDMFFile(comm, file_name+"_domain.xdmf") as infile:

            infile.read(mesh)

            infile.read(domain_meshCollection, "domain")

    # Converts the mesh value collection to mesh function, for mesh va-
    # lue collections are low level and cannot be used for FEM integra-
    # tion and other higher level operations inside FEniCS

    domain_meshFunction = MeshFunction("size_t", mesh, 
    domain_meshCollection)

    # Reinitializes the mesh value colection to the boundary data

    boundary_meshCollection = MeshValueCollection("size_t", mesh,
    mesh.topology().dim()-1)

    # Reads the mesh with surface physical groups

    if comm is None:

        with XDMFFile(file_name+"_boundary.xdmf") as infile:
        
            infile.read(boundary_meshCollection, "boundary")

    else:

        with XDMFFile(comm, file_name+"_boundary.xdmf") as infile:
        
            infile.read(boundary_meshCollection, "boundary")

    # Converts the mesh value collection to mesh function

    boundary_meshFunction = MeshFunction("size_t", mesh, 
    boundary_meshCollection)

    # Sets the integration differentials

    dx = Measure("dx", domain=mesh, subdomain_data=domain_meshFunction,
    metadata={"quadrature_degree": quadrature_degree})

    ds = Measure("ds", domain=mesh, subdomain_data=boundary_meshFunction)#,
    #metadata={"quadrature_degree": quadrature_degree})

    # Sets the normal vector to the mesh's boundary

    n  = FacetNormal(mesh)

    # Sets the position vector

    x_position = SpatialCoordinate(mesh)

    mpi_print(comm, "Finishes creating the mesh functions, measures, and tags di"+
    "ctionaries.\n")

    # Stores these objects inside a class and returns it

    return MeshData(mesh, dx, ds, n, x_position, domain_meshCollection, 
    domain_meshFunction, boundary_meshCollection, boundary_meshFunction, 
    domain_physicalGroupsNameToTag, boundary_physicalGroupsNameToTag,
    verbose, mesh_file=file_name, comm=comm)

########################################################################
#                              Submeshing                              #
########################################################################

# Defines a function to generate a submesh using MeshView and creates

def create_submesh(domain_meshCollection, domain_meshFunction,
volume_physGroupsTags, parent_functionSpace, 
domain_physicalGroupsNameToTag=None, boundary_meshCollection=None, 
boundary_meshFunction=None, boundary_physicalGroupsNameToTag=None):
    
    # Verifies if the volume physical groups are a list of strings

    for i in range(len(volume_physGroupsTags)):

        if isinstance(volume_physGroupsTags[i], str):

            # Converts the string to integer tag

            try:

                volume_physGroupsTags[i] = domain_physicalGroupsNameToTag[
                volume_physGroupsTags[i]]

            except TypeError:

                raise TypeError("The dictionary of volumetric physical"+
                " groups to integer tags has not been provided")
            
            except KeyError:

                raise KeyError("The '"+str(volume_physGroupsTags[i])+
                "' is not a valid tag for a volumetric physical group."+
                " The valid tags are: "+str(domain_physicalGroupsNameToTag.keys()))

    # Gets the mesh, the polynomial degree, and the shape function

    mesh = parent_functionSpace.mesh()

    polynomial_degree = parent_functionSpace.ufl_element().degree()

    shape_function = parent_functionSpace.ufl_element().family()

    # Gets the dimensionality of the parent function space

    n_dimsParentFunctionSpace = len(parent_functionSpace.ufl_element(
    ).value_shape())

    # Creates a new cell markers object to not affect the mesh function
    # at other parts of the code

    submesh_cellMarkers = cpp.mesh.MeshFunctionSizet(mesh, 
    domain_meshCollection)

    # If the submesh is meant to be constructed from different volume 
    # physical groups of the mesh, the object of cell_markers has to be
    # changed, because the MeshView create function works only with a 
    # single marker to define a subdomain and, from there, a submesh. 
    # Therefore, the physical groups that contain the RVE are collapsed 
    # into a single marker; conventionally the first selected marker

    if isinstance(volume_physGroupsTags, list):

        # Iterates through the elements of the parent mesh

        if len(volume_physGroupsTags)>1:

            for element in cells(mesh):

                # Tests if the cell marker is in the list of required 
                # cell markers

                if submesh_cellMarkers[element] in volume_physGroupsTags:

                    # Changes the cell marker to the first required one

                    submesh_cellMarkers[element] = volume_physGroupsTags[
                    0]

        # Collapses the volume physical groups tags list to the first 
        # component

        volume_physGroupsTags = volume_physGroupsTags[0]

    # Creates a submesh for the RVE

    sub_mesh = MeshView.create(submesh_cellMarkers, volume_physGroupsTags)

    # Creates the mapping of elements from the submesh to the parent 
    # mesh, i.e. given the element index in the submesh, it throws the 
    # index in the parent mesh

    sub_toParentCellMap = sub_mesh.topology().mapping()[mesh.id()
    ].cell_map()

    # Creates a new mesh function with physical groups for the submesh

    submesh_meshFunction = MeshFunction("size_t", sub_mesh, 
    sub_mesh.topology().dim(), 0)

    # Iterates over the elements in the cell mapping of the submesh to 
    # the parent mesh to retrieve elements physical groups

    for submesh_cellIndex, parent_cellIndex in enumerate(
    sub_toParentCellMap):

        submesh_meshFunction[submesh_cellIndex] = domain_meshFunction[
        parent_cellIndex]

    # Creates the function spaces

    submesh_functionSpace = 0

    # If the element is mixed, the .family() will return 'Mixed'

    if shape_function=='Mixed':

        submesh_functionSpace = FunctionSpace(sub_mesh,
        parent_functionSpace.ufl_element())

    else:

        # If the field is scalar, the dimensionality is 0

        if n_dimsParentFunctionSpace==0:

            submesh_functionSpace = FunctionSpace(sub_mesh, 
            shape_function, polynomial_degree)

        # If the field is vector function, the dimensionality is 1

        elif n_dimsParentFunctionSpace==1:

            submesh_functionSpace = VectorFunctionSpace(sub_mesh,
            shape_function, polynomial_degree)

        # If the field is a second order tensor function

        elif n_dimsParentFunctionSpace==2:

            submesh_functionSpace = TensorFunctionSpace(sub_mesh, 
            shape_function, polynomial_degree)

        # Handle not implemented function spaces

        else:

            raise NameError("create_submesh in mesh_handling_tools.py "+
            "does not support a function space with dimensionality lar"+
            "ger than "+str(n_dimsParentFunctionSpace)+", for it's not"+
            " been implemented yet or it's not possible to implement.")
        
    # Initializes the function to the solution at the submesh

    submesh_function = Function(submesh_functionSpace)

    # Initializes the DOF mappings for the RVE and for the original mesh

    sub_meshMapping = []

    parent_meshMapping = []

    # Verifies whether there is only on field

    if shape_function!='Mixed':

        sub_meshMapping.append(submesh_functionSpace.dofmap())

        parent_meshMapping.append(parent_functionSpace.dofmap())

    # If there are multiple fields

    else:

        # Iterates through the number of fields

        for i in range(parent_functionSpace.ufl_element().num_sub_elements()):

            # Adds the submesh mapping and the parent mesh mapping

            sub_meshMapping.append(submesh_functionSpace.sub(i).dofmap())

            parent_meshMapping.append(parent_functionSpace.sub(i).dofmap(
            ))
            
    # Sets the integration differential in the submesh

    dx_submesh = Measure("dx", domain=sub_mesh, subdomain_data=
    submesh_meshFunction)

    submesh_physicalGroups = set(dx_submesh.subdomain_data().array())

    physical_groupsList = ""

    for physical_group in submesh_physicalGroups:

        physical_groupsList += str(physical_group)+"\n"

    print("The submesh has the following physical groups:\n"+
    physical_groupsList)

    # Creates a position vector field for the submesh

    V_positionVector = VectorFunctionSpace(sub_mesh, "CG", 1)

    x_submesh = Function(V_positionVector)

    # Assign the mesh coordinates to the function

    x_submesh.interpolate(Expression(("x[0]", "x[1]", "x[2]"), element=
    V_positionVector.ufl_element()))

    # Creates an instance of the class mesh data

    submesh_data_class = MeshData(sub_mesh, dx_submesh, None, None,
    x_submesh, domain_meshCollection, submesh_meshFunction, 
    boundary_meshCollection, boundary_meshFunction, 
    domain_physicalGroupsNameToTag, boundary_physicalGroupsNameToTag, 
    False)

    # Returns the submesh, the updated cell markers, and the DOF mappings

    return (submesh_data_class, submesh_functionSpace, sub_meshMapping, 
    parent_meshMapping, submesh_function, sub_toParentCellMap)

# Defines a function to update the field parameters vector of a submesh 
# given the corresponding vector at the parent mesh

def field_parentToSubmesh(submesh, field_parentMesh, sub_toParentCellMap, 
sub_meshMapping=None, parent_meshMapping=None, field_submesh=None):
    
    # If the field of the submesh is not explicitely given, creates it
    # as a copy of the parent field jsut with a different mesh

    if field_submesh is None:

        field_submesh = Function(FunctionSpace(submesh, 
        field_parentMesh.ufl_element()))

    # If the mesh mappings have not been provided

    if (sub_meshMapping is None) or (parent_meshMapping is None):

        # Gets the parent field function space and its shape function

        submesh_functionSpace = field_submesh.function_space()

        parent_functionSpace = field_parentMesh.function_space()

        shape_function = parent_functionSpace.ufl_element().family()

        # Initializes the DOF mappings for the RVE and for the original 
        # mesh

        sub_meshMapping = []

        parent_meshMapping = []

        # Verifies whether there is only on field

        if shape_function!='Mixed':

            sub_meshMapping.append(submesh_functionSpace.dofmap())

            parent_meshMapping.append(parent_functionSpace.dofmap())

        # If there are multiple fields

        else:

            # Iterates through the number of fields

            for i in range(parent_functionSpace.ufl_element(
            ).num_sub_elements()):

                # Adds the submesh mapping and the parent mesh mapping

                sub_meshMapping.append(submesh_functionSpace.sub(i
                ).dofmap())

                parent_meshMapping.append(parent_functionSpace.sub(i
                ).dofmap())

    # Iterates through the elements of the submesh

    for element in cells(submesh):

        # Gets the index of the element in the submesh

        submesh_index = element.index()

        # Gets the index of the element in the parent mesh

        parent_index = sub_toParentCellMap[submesh_index]

        # Iterates through the fields of the meshes. If the mesh has on-
        # ly one field, displacement for instance, the list of DOF map-
        # ping has only one component

        for i in range(len(sub_meshMapping)):

            # Translates the values of the solution using the DOFs map-
            # ping

            field_submesh.vector()[sub_meshMapping[i].cell_dofs(
            submesh_index)] = field_parentMesh.vector()[
            parent_meshMapping[i].cell_dofs(parent_index)] 

    # Returns the submesh field

    return field_submesh
        
########################################################################
#                             Node finding                             #
########################################################################

# Defines a function to find a node of the mesh nearest to a given point.
# You can provide a class with a .mesh attribute or you can provide the
# mesh proper

def find_nodeClosestToPoint(mesh_dataClass, point_coordinates, 
node_number, node_coordinates, set_ofNodes=None):

    # Tests if the node has already been found

    if (node_number is None) or (node_coordinates is None):

        # Verifies if the node coordinates is a list

        if (not isinstance(point_coordinates, list)) and (not 
        isinstance(point_coordinates, np.ndarray)):

            raise TypeError("point_coordinates must be a list to find "+
            "a node near this coordinates, whereas it is: "+str(
            point_coordinates)+", whose type is: "+str(type(
            point_coordinates)))

        # Gets the coordinates of the mesh

        mesh_coordinates = 0

        # If a special set of nodes has been required

        if set_ofNodes is None:

            # Verifies if the object is already the mesh

            if hasattr(mesh_dataClass, "coordinates"):

                mesh_coordinates = mesh_dataClass.coordinates()

            else:

                mesh_coordinates = mesh_dataClass.mesh.coordinates()

        elif isinstance(set_ofNodes, list):

            # Verifies if the object is already the mesh

            if hasattr(mesh_dataClass, "coordinates"):

                mesh_coordinates = mesh_dataClass.coordinates()[
                set_ofNodes]

            else:

                mesh_coordinates = mesh_dataClass.mesh.coordinates()[
                set_ofNodes]

        else:

            raise TypeError("The set of nodes to find a node closest t"+
            "o a point from must be a list. The provided set of nodes,"+
            " however, is not a list: "+str(set_ofNodes))

        # Gets a tree of these coordinates

        coordinates_tree = KDTree(mesh_coordinates)

        # Gets the number of the node that is closest to the given coor-
        # dinates

        _, node_number = coordinates_tree.query(point_coordinates)

        # Returns the node number

        if set_ofNodes is None:

            node_number = int(node_number)

            return node_number, mesh_coordinates[node_number]

        else:

            # The node number given by the query is not the actual node
            # number, rather the index inside the point coordinates list.
            # Hence, this index must be mapped back to the global index
            # system

            global_nodeNumber = set_ofNodes[int(node_number)]

            return global_nodeNumber, mesh_coordinates[int(node_number)]
    
    else:

        # Returns the node number as it's been given

        return node_number, node_coordinates
    
# Defines a function to find a set of degrees of freedom (DOFs) in a re-
# gion of the domain. A physical group can be given or a function that
# returns True or False for the coordinates

def find_dofs_in_volume(mesh_data_class, functional_data_class,
physical_group_name=None, region_function=None, field_name=None):

    # Gets the map of DOFs

    monolithic_dofmap = None 

    # Verifies if a field name is asked

    if field_name is not None:

        # Verifies if this field name belongs to the dictionary of fields
        # names

        if not (field_name in functional_data_class.fields_names_dict):

            raise ValueError("The field '"+str(field_name)+"' does not"+
            " belong to the dictionary of fields of this problem. See "+
            "the available fields:\n"+str(list(
            functional_data_class.fields_names_dict.keys())))
        
        # Verifies if this problem has more than one field

        if len(functional_data_class.fields_names_dict.keys())>1:

            # Gets the map of DOFs for the selected field only

            monolithic_dofmap = functional_data_class.monolithic_function_space.sub(
            functional_data_class.fields_names_dict[field_name]).dofmap()

    # If the map of DOFs was not created, the field name was not provided
    # or the problem has only one field 

    if monolithic_dofmap is None:

        monolithic_dofmap = functional_data_class.monolithic_function_space.dofmap()

    # If a physical group and a region function are given

    if (physical_group_name is not None) and (region_function is not None):

        # Initializes a set of DOFs

        DOFs_set = set()

        # Iterates through the elements of the mesh

        for element in cells(mesh_data_class.mesh):

            # Verifies if this element belongs to this physical group

            if mesh_data_class.domain_meshFunction[element.index()]==(
            mesh_data_class.domain_physicalGroupsNameToTag[
            physical_group_name]):
                
                # Gets the centroid of this element

                element_centroid = element.midpoint().array()

                # If the centroid is within the region

                if region_function(*element_centroid):
                
                    # Updates the DOFs of this element using the map of 
                    # DOFs

                    DOFs_set.update(monolithic_dofmap.cell_dofs(
                    element.index()))

        # Sorts the list of DOFs

        DOFs_set = sorted(DOFs_set)

        # Returns the map of DOFs as a list

        return list(DOFs_set)

    # If a physical group is given

    elif physical_group_name is not None:

        # Initializes a set of DOFs

        DOFs_set = set()

        # Iterates through the elements of the mesh

        for element in cells(mesh_data_class.mesh):

            # Verifies if this element belongs to this physical group

            if mesh_data_class.domain_meshFunction[element.index()]==(
            mesh_data_class.domain_physicalGroupsNameToTag[
            physical_group_name]):
                
                # Updates the DOFs of this element using the map of DOFs

                DOFs_set.update(monolithic_dofmap.cell_dofs(
                element.index()))

        # Sorts the list of DOFs

        DOFs_set = sorted(DOFs_set)

        # Returns the map of DOFs as a list

        return list(DOFs_set)
    
    # If a function to find a region is given

    elif region_function is not None:

        # Initializes a set of DOFs

        DOFs_set = set()

        # Iterates through the elements of the mesh

        for element in cells(mesh_data_class.mesh):

            # Evaluates the centroid of the element

            element_centroid = element.midpoint().array()

            # Verifies if the centroid of the element is in the region

            if region_function(*element_centroid):
                
                # Updates the DOFs of this element using the map of DOFs

                DOFs_set.update(monolithic_dofmap.cell_dofs(
                element.index()))

        # Sorts the list of DOFs

        DOFs_set = sorted(DOFs_set)

        # Returns the map of DOFs as a list

        return list(DOFs_set)
    
    # Otherwise throws an error

    else:

        raise TypeError("It is not possible to find the DOFs in a volu"+
        "me using 'find_dofs_in_volume', because 'physical_group_name'"+
        " and 'region_function' are None. 'physical_group_name' is mea"+
        "nt to be the name of a desired physical group; whereas 'regio"+
        "n_function' is a function that receives the x, y, and z coord"+
        "inates of a point and spits out True or False, according to i"+
        "f the point is in a volumetric region or not")
    
# Defines a class to find all DOFs (including those related to higher 
# order shape functions)

class DOFsNode:

    def __init__(self, dof_coordinates, dofs_indices, tolerance=1E-6, 
    return_coordinates=False, do_not_throw_error=False, n_closest_nodes=
    None):
        
        self.dof_coordinates = dof_coordinates

        self.dofs_indices = dofs_indices

        self.n_dofs = self.dof_coordinates.shape[0]

        self.tolerance = tolerance

        self.return_coordinates_per_dof_found = return_coordinates

        self.do_not_throw_error = do_not_throw_error

        self.n_closest_nodes = n_closest_nodes

        # Verifies if the number of closest DOFs were provided if an er-
        # ror is not be raised in case of a violated tolerance

        if self.do_not_throw_error:

            if self.n_closest_nodes is None:

                raise ValueError("'do_not_throw_error' is True, but 'n"+
                "_closest_nodes' was not provided. Give a number of cl"+
                "osest nodes to the given coordinate to be returned")
            
    # Defines a method to get the coordinates of a given DOF

    def get_coordinates_from_dof(self, dof_index):

        # Verifies if the index is valid

        if not (dof_index in self.dofs_indices):

            raise IndexError("The DOF index "+str(dof_index)+" is not "+
            "valid. A DOF index to be valid must be between 0 and "+str(
            self.n_dofs-1)+". Besides it must be in the list of querie"+
            "d DOFs. If the asked DOF is in the proper range, it means"+
            " you asked for a higher order DOF while requiring the pro"+
            "gram to compile vertices DOFs only")
        
        # Returns the DOF coordinates. But find first the corresponding 
        # DOF in the list of DOF indices

        return self.dof_coordinates[np.where(self.dofs_indices==dof_index
        )[0],:]

    # Defines a call method to get the node numebr

    def __call__(self, x, y, z):

        # Constructs a point array and calculate the distances from this
        # point to the DOFs coordinates

        distances = np.linalg.norm(self.dof_coordinates-np.array([x, y, 
        z]), axis=1)

        dofs_indices = None

        # If the tolerance is not to be used, but simply the closest DOFs
        # are to be found

        if self.do_not_throw_error:

            # Gets the closest DOFs to the given coordinate

            dofs_indices = np.argsort(distances)
            
            # Initializes a counter of found nodes

            verified_nodes = 1

            # Iterates over the DOFs coordinates using the sorted DOF 
            # indices to look for all DOFs that map to the closest node

            for sorted_index, dof_index in enumerate(dofs_indices):

                # Verifies if the current node is different than the last
                # one

                if not np.allclose(self.dof_coordinates[dof_index],
                self.dof_coordinates[dofs_indices[max(0,sorted_index-1)]
                ]):
                    
                    verified_nodes += 1

                    # Verifies if the number of nodes found is higher 
                    # than the number of selected nodes

                    if verified_nodes>self.n_closest_nodes:

                        # Cuts the list of DOF indices

                        dofs_indices = dofs_indices[0:sorted_index]

                        break

        else:

            # Selects the DOFs indices that are close to the point given
            # the tolerance

            dofs_indices = np.where(distances<self.tolerance)[0]

            # Verifies if the found DOFs are close enough to consider it
            # a valid node

            if len(dofs_indices)==0:

                closest_node = self.dof_coordinates[np.argmin(distances)]

                raise ValueError("Point ("+str(x)+", "+str(y)+", "+str(z
                )+") is not a valid node to look for DOFs. The closest"+
                " node is x="+str(closest_node[0])+", y="+str(
                closest_node[1])+", z="+str(closest_node[2]))
            
        # Reassembles the DOFs to match the list of DOFs indices. Pairs 
        # DOF indices and 

        pairs = list(zip([self.dofs_indices[dof_index] for (dof_index
        ) in dofs_indices], [self.dof_coordinates[i,:] for i in (
        dofs_indices)]))

        # Sorts the pairs by ascending order of DOF indices

        pairs.sort(key=lambda x: x[0])

        correct_dofs_indices = [p[0] for p in pairs]
        
        # If the coordinates of the found nodes are to be returned as 
        # well

        if self.return_coordinates_per_dof_found:
        
            # If there are multiple DOFs in a single location, returns a
            # a list of them

            DOFs_coordinates = [p[1] for p in pairs]

            return correct_dofs_indices, DOFs_coordinates
        
        # Otherwise, returns the dof indices only

        else:
        
            # If there are multiple DOFs in a single location, returns a 
            # list of them

            return correct_dofs_indices
    
# Defines a function to create a class that returns the DOFs in a node
# closest to a point 

def dofs_per_node_finder_class(functional_data_class, field_name=None, 
node_proximity_tolerance=1E-6, return_coordinates_per_dof_found=False,
do_not_throw_error=False, n_closest_nodes=None, get_higher_order_dofs_too=
True):
    
    """
    Function to construct a class that stores a tree query object that,
    when called, returns the closest degrees of freedom to a point in
    space

    functional_data_class: instance of the FunctionalData class with
    information on function space

    field_name: name of the field whose DOFs are to be searched
    """

    # Gets the of coordinates for each DOF and a list of DOFs indices

    dof_coordinates = None 

    dofs_indices = None 

    # Verifies if a field name is asked

    if field_name is not None:

        # Verifies if this field name belongs to the dictionary of fields
        # names

        if not (field_name in functional_data_class.fields_names_dict):

            raise ValueError("The field '"+str(field_name)+"' does not"+
            " belong to the dictionary of fields of this problem. See "+
            "the available fields:\n"+str(list(
            functional_data_class.fields_names_dict.keys())))
        
        # Verifies if this problem has more than one field

        if len(functional_data_class.fields_names_dict.keys())>1:

            # Gets the map of DOFs for the selected field only

            dof_coordinates = functional_data_class.monolithic_function_space.sub(
            functional_data_class.fields_names_dict[field_name]
            ).tabulate_dof_coordinates()

    # If the map of DOFs was not created, the field name was not provided
    # or the problem has only one field 

    if dof_coordinates is None:

        dof_coordinates = functional_data_class.monolithic_function_space.tabulate_dof_coordinates()

    # Reshapes the dof coordinates according to the number of dimensions
    # of the geometry, since FEniCS returns an array of size n_DOFS*
    # n_geometric_dimensions 

    dof_coordinates = dof_coordinates.reshape((-1, 
    functional_data_class.monolithic_function_space.mesh().geometry(
    ).dim()))

    # If the DOFs of the nodes related to higher order shape functions 
    # are to be retrieved

    if get_higher_order_dofs_too:

        # Gets the DOF indices just as an enumeration

        dofs_indices = [i for i in range(dof_coordinates.shape[0])]

    # Otherwise, if only the DOFs related to the finite element vertices
    # are to be retrieved

    else:

        # Initializes the dof map of the monolithic function space

        monolithic_dofmap = None

        # Verifies if a field name is asked

        if field_name is not None:

            # Verifies if this field name belongs to the dictionary of 
            # fields names

            if not (field_name in functional_data_class.fields_names_dict):

                raise ValueError("The field '"+str(field_name)+"' does"+
                " not belong to the dictionary of fields of this probl"+
                "em. See the available fields:\n"+str(list(
                functional_data_class.fields_names_dict.keys())))
            
            # Verifies if this problem has more than one field

            if len(functional_data_class.fields_names_dict.keys())>1:

                # Gets the map of DOFs for the selected field only

                monolithic_dofmap = functional_data_class.monolithic_function_space.sub(
                functional_data_class.fields_names_dict[field_name]
                ).dofmap()

        # If the map of DOFs was not created, the field name was not 
        # provided or the problem has only one field 

        if monolithic_dofmap is None:

            monolithic_dofmap = functional_data_class.monolithic_function_space.dofmap()

        # Initializes a set of DOFs

        dofs_set = set()

        # Retrieves the mesh object

        mesh_object = functional_data_class.monolithic_function_space.mesh()

        # Iterates through the elements

        for element in cells(mesh_object):

            # Iterates through the nodes of this element

            for node in vertices(element):

                # Updates the set of DOfs

                dofs_set.update(monolithic_dofmap.entity_dofs(
                mesh_object, 0, [node.index()]))

        # Converts the set to a list of DOFs indices

        dofs_indices = np.array(sorted(dofs_set))

        # Then, gathers the DOF coordinates according to that list of 
        # DOFs indices

        dof_coordinates = dof_coordinates[dofs_indices,:]

    # Instantiates the class and returns it

    return DOFsNode(dof_coordinates, dofs_indices, tolerance=
    node_proximity_tolerance, return_coordinates=
    return_coordinates_per_dof_found, do_not_throw_error=
    do_not_throw_error, n_closest_nodes=n_closest_nodes)
    
# Defines a function to create a list of nodes indexes that lie on a
# surface

def find_nodesOnSurface(mesh_dataClass, physical_group, 
return_coordinates=False):

    # Verifies if the physical group is a string

    if isinstance(physical_group, str):

        # Tests if it is in the dictionary of physical groups of the 
        # boundary

        if physical_group in mesh_dataClass.boundary_physicalGroupsNameToTag:

            # Converts it

            physical_group = mesh_dataClass.boundary_physicalGroupsNameToTag[
            physical_group]

        else:

            raise KeyError("The physical group '"+str(physical_group)+
            "' is not in the dictionary of boundary physical groups. T"+
            "hus, cannot be used to find the nodes in the boundary of "+
            "surface. Check out the available options of physical grou"+
            "ps in the boundary: "+str(
            mesh_dataClass.boundary_physicalGroupsNameToTag.keys()))
        
    # Do not accept other formats than integer

    elif not isinstance(physical_group, int):

        raise TypeError("The physical group "+str(physical_group)+" is"+
        " not an integer, thus cannot be used to find the nodes in the"+
        " boundary of a surface")
    
    # Initializes a set of nodes

    nodes_set = set()

    # Iterates through the 2D elements of the mesh

    for element in facets(mesh_dataClass.mesh):

        # Verifies if this 2D element belongs to the physical group
        
        if (mesh_dataClass.boundary_meshFunction[element.index()]==(
        physical_group)):
            
            # Iterates through the nodes of this element

            for node in vertices(element):

                nodes_set.add(node.index())

    # If the nodes coordinates is to be given

    if return_coordinates:

        # Gets the coordinates and returns it

        return mesh_dataClass.mesh.coordinates()[list(nodes_set)]

    else:

        # Returns the set of nodes' indices at the boundary and converts
        # to a list

        return list(nodes_set)
    
# Defines a function to create a list of nodes indexes that lie on the
# boundary of surface

def find_nodesOnSurfaceBoundary(mesh_dataClass, physical_group, 
return_coordinates=False):

    # Verifies if the physical group is a string

    if isinstance(physical_group, str):

        # Tests if it is in the dictionary of physical groups of the 
        # boundary

        if physical_group in mesh_dataClass.boundary_physicalGroupsNameToTag:

            # Converts it

            physical_group = mesh_dataClass.boundary_physicalGroupsNameToTag[
            physical_group]

        else:

            raise KeyError("The physical group '"+str(physical_group)+
            "' is not in the dictionary of boundary physical groups. T"+
            "hus, cannot be used to find the nodes in the boundary of "+
            "surface. Check out the available options of physical grou"+
            "ps in the boundary: "+str(
            mesh_dataClass.boundary_physicalGroupsNameToTag.keys()))
        
    # Do not accept other formats than integer

    elif not isinstance(physical_group, int):

        raise TypeError("The physical group "+str(physical_group)+" is"+
        " not an integer, thus cannot be used to find the nodes in the"+
        " boundary of a surface")
    
    # Gets the 2D elements in the mesh that lie on this physical group

    bidimensional_elements = [element for element in facets(
    mesh_dataClass.mesh) if mesh_dataClass.boundary_meshFunction[
    element.index()]==physical_group]

    # Initializes a dictionary of element contours to count the number 
    # of elements where they appear in

    element_contoursCounter = dict()

    # Iterates through the 2D elements

    for element in bidimensional_elements:

        # Iterates through the contour lines

        for contour_line in edges(element):

            # Gets the index of the contour

            contour_index = contour_line.index()

            # Adds this to the counter. Uses the get function to avoid
            # key error, thus, get 0 if the key is not found

            element_contoursCounter[contour_index] = element_contoursCounter.get(
            contour_index, 0)+1

    # Initializes a set to guard the set of nodes that lie on the boun-
    # dary

    boundary_nodes = set()

    # Harvests just the contour lines that appear only once, because the
    # lines that appear more than one are shared between elements and,
    # hence, are inside the region

    for contour_index, elements_count in element_contoursCounter.items():

        if elements_count==1:

            # Iterates through the nodes at the edges of this line

            for node in vertices(Edge(mesh_dataClass.mesh, contour_index
            )):
                
                # Adds this element to the set

                boundary_nodes.add(node.index())

    # If the nodes coordinates is to be given

    if return_coordinates:

        # Gets the coordinates and returns it

        return mesh_dataClass.mesh.coordinates()[list(boundary_nodes)]

    else:

        # Returns the set of nodes' indices at the boundary and converts to
        # a list

        return list(boundary_nodes)

# Defines a function to create a list of nodes indexes that lie on the 
# vertices of elements around a node

def find_nodesOnSurfaceAroundNode(mesh_dataClass, physical_group, 
node_number=None, node_coordinates=None):

    # Verifies if the physical group is a string

    if isinstance(physical_group, str):

        # Tests if it is in the dictionary of physical groups of the 
        # boundary

        if physical_group in mesh_dataClass.boundary_physicalGroupsNameToTag:

            # Converts it

            physical_group = mesh_dataClass.boundary_physicalGroupsNameToTag[
            physical_group]

        else:

            raise KeyError("The physical group '"+str(physical_group)+
            "' is not in the dictionary of boundary physical groups. T"+
            "hus, cannot be used to find the nodes around a node on a "+
            "surface. Check out the available options of physical grou"+
            "ps in the boundary: "+str(
            mesh_dataClass.boundary_physicalGroupsNameToTag.keys()))
        
    # Do not accept other formats than integer

    elif not isinstance(physical_group, int):

        raise TypeError("The physical group "+str(physical_group)+" is"+
        " not an integer, thus cannot be used to find the nodes around"+
        " a node on a surface")

    # Verifies if both the node number and the node coordinates are None

    if (node_number is None) and (node_coordinates is None):

        raise ValueError("The node_number and the node_coordinates can"+
        "not be both None, at least one must be provided to find the a"+
        "djacent nodes to a node on a surface")

    elif not (node_coordinates is None):

        # Finds the node closest to the given coordinates

        node_number, node_coordinates = find_nodeClosestToPoint(
        mesh_dataClass, node_coordinates, None, None)

    else:

        # Checks if the node number is an integer

        if not isinstance(node_number, int):

            raise TypeError("The number of the node given to find the"+
            " adjacent nodes to itself on a boundary surface is not a"+
            "n integer")

        # Checks if the number of the node is inbound with the number of
        # nodes in the mesh

        elif node_number>(len(mesh_dataClass.mesh.coordinates())-1):

            raise ValueError("The number of the node given to find the"+
            " adjacent nodes to itself on a boundary surface is larger"+
            " than the number of nodes in the mesh")
    
    # Gets the 2D elements in the mesh that lie on this physical group

    bidimensional_elements = [element for element in facets(
    mesh_dataClass.mesh) if mesh_dataClass.boundary_meshFunction[
    element.index()]==physical_group]

    # Initializes a list of nodes' indices whose nodes lie on elements
    # that are adjacent to the desired node

    adjacent_nodes = set()

    # Iterates through the 2D elements

    for element in bidimensional_elements:

        # Gets the nodes of this element

        elements_nodes = element.entities(0)

        # Verifies if the sought after node is inside this set

        if node_number in elements_nodes:

            # Adds them to the set

            adjacent_nodes.update(elements_nodes)

    # Transforms the adjacent nodes' numbers to a list and gets their 
    # coordinates

    adjacent_nodes = list(adjacent_nodes)

    adjacent_nodesCoordinates = mesh_dataClass.mesh.coordinates()[
    adjacent_nodes]

    return adjacent_nodes, adjacent_nodesCoordinates

########################################################################
#                            Physical groups                           #
########################################################################

# Defines a function to get the degrees of freedom that belongs to each
# boundary physical group. Gives a dictionary of sets

def get_boundary_dofs_to_physical_group(mesh_data_class, function_space):

    # Initializes the dictionary of sets

    dofs_dictionary = dict()

    # Gets the DOFs map for this function space

    dof_map = function_space.dofmap()

    # Iterates through the boundary physical groups

    for physical_name, physical_tag in (
    mesh_data_class.boundary_physicalGroupsNameToTag.items()):

        # Initializes the set of DOFs

        dofs_set = set()

        # Iterates through the mesh facets

        for facet in facets(mesh_data_class.mesh):

            # Verifies if it belongs to the boundary

            if mesh_data_class.boundary_meshFunction[facet]==(
            physical_tag):
                
                # Iterates through the nodes of this facet
                
                for node in vertices(facet):
                    
                    dofs_set.update(dof_map.entity_dofs(
                    mesh_data_class.mesh, 0, [node.index()]))

        # Adds to the dictionary

        dofs_dictionary[physical_name] = dofs_set

    # Returns the dictionary

    return dofs_dictionary

# Defines a function to get the degrees of freedom that belongs to each
# domain physical group. Gives a dictionary of sets

def get_domain_dofs_to_physical_group(mesh_data_class, function_space):

    # Initializes the dictionary of sets

    dofs_dictionary = dict()

    # Gets the DOFs map for this function space

    dof_map = function_space.dofmap()

    # Iterates through the doamin physical groups

    for physical_name, physical_tag in (
    mesh_data_class.domain_physicalGroupsNameToTag.items()):

        # Initializes the set of DOFs

        dofs_set = set()

        # Iterates through the mesh elements

        for cell in cells(mesh_data_class.mesh):

            # Verifies if it belongs to the domain

            if mesh_data_class.domain_meshFunction[cell]==(
            physical_tag):
                
                # Iterates through the nodes of this cell
                
                for node in vertices(cell):
                    
                    dofs_set.update(dof_map.entity_dofs(
                    mesh_data_class.mesh, 0, [node.index()]))

        # Adds to the dictionary

        dofs_dictionary[physical_name] = dofs_set

    # Returns the dictionary

    return dofs_dictionary

# Defines a function to convert a (possibly) string physical group to 
# the corresponding integer physical group

def convert_physicalGroup(physical_group, mesh_dataClass, region):

    # Makes a copy of the physical group

    original_physicalGroup = copy(physical_group)

    # Verifies if region is either domain or boundary

    if region=="boundary":

        if isinstance(physical_group, str):

            # Tests if it is in the dictionary of physical groups of the 
            # boundary

            if physical_group in mesh_dataClass.boundary_physicalGroupsNameToTag:

                # Converts it

                physical_group = mesh_dataClass.boundary_physicalGroupsNameToTag[
                physical_group]

            else:

                raise KeyError("The physical group '"+str(physical_group
                )+"' is not in the dictionary of boundary physical gro"+
                "ups. Check out the available options of physical grou"+
                "ps in the boundary: "+str(
                mesh_dataClass.boundary_physicalGroupsNameToTag.keys()))
            
        # Does not accept any other formats than integer

        elif not isinstance(physical_group, int):

            raise TypeError("The physical group "+str(physical_group)+
            " is not an integer nor a string. Thus, cannot be converte"+
            "d into a numeric physical group")

    elif region=="domain":

        if isinstance(physical_group, str):

            # Tests if it is in the dictionary of physical groups of the 
            # domain

            if physical_group in mesh_dataClass.domain_physicalGroupsNameToTag:

                # Converts it

                physical_group = mesh_dataClass.domain_physicalGroupsNameToTag[
                physical_group]

            else:

                raise KeyError("The physical group '"+str(physical_group
                )+"' is not in the dictionary of domain physical group"+
                "s. Check out the available options of physical groups"+
                " in the domain: "+str(
                mesh_dataClass.domain_physicalGroupsNameToTag.keys()))
            
        # Does not accept any other formats than integer

        elif not isinstance(physical_group, int):

            raise TypeError("The physical group "+str(physical_group)+
            " is not an integer nor a string. Thus, cannot be converte"+
            "d into a numeric physical group")

    else:

        raise NameError("The region flag must be either 'domain' or 'b"+
        "oundary' to be used to convert a physical group to its true n"+
        "umerical counterpart")

    return physical_group, original_physicalGroup

########################################################################
#                             Surface tools                            #
########################################################################

# Defines a function to create a new boundary mesh function breaking
# boundary physical groups that envelop multiple volumetric physical
# groups

def break_boundary_physical_groups(mesh_data_class, insert_domain_tags=
False): 

    # Retrieves some properties of the mesh data class

    mesh_object = mesh_data_class.mesh 

    boundary_dictionary = mesh_data_class.boundary_physicalGroupsNameToTag

    domain_dictionary = mesh_data_class.domain_physicalGroupsNameToTag

    domain_mesh_function = mesh_data_class.domain_meshFunction

    original_boundary_mesh_function = mesh_data_class.boundary_meshFunction

    # Creates a new boundary mesh function, and set all indices as zero

    boundary_mesh_function = MeshFunction("size_t", mesh_object, 
    mesh_object.topology().dim()-1)

    boundary_mesh_function.set_all(0)

    # Gets the initial number of boundary physical groups

    new_boundary_number = 0

    # Initializes a new dictionary of boundary physical groups

    new_boundary_dict = dict()

    # Sorts the dictionary of boundary physical groups by its values

    boundary_dictionary = sort_dictionary_by_values(boundary_dictionary)

    # Iterates through the original boundary physical groups

    for boundary_physical_group, boundary_tag in boundary_dictionary.items():

        # Adds the key and a corresponding value as another dictionary. 
        # The keys of the second dictionary represent the volumetric phy-
        # sical groups attached to this original boundary physical group.
        # Whereas the values represent the new boundayr physical groups
        # enumeration

        new_boundary_dict[boundary_physical_group] = dict()

        # Iterates through the facet elements

        for facet in facets(mesh_object):

            # Verifies if this facet lies in the boundary

            original_tag = original_boundary_mesh_function[facet.index()]

            # If it is in the current physical group

            if original_tag==boundary_tag:

                # Gets the indices of the elements attached to this facet

                element_indices = facet.entities(mesh_object.topology(
                ).dim())

                # Retrieves the physical group tag of the attached volu-
                # metric element. Use the 0 index, for there is only a 
                # single volumetric element attached to a facet

                physical_group = domain_mesh_function[
                element_indices[0]]

                # If the physical group tags must be converted back into
                # their names, for convenience

                if not insert_domain_tags:

                    physical_group = get_first_key_from_value(
                    domain_dictionary, deepcopy(physical_group))

                # Verifies if this physical group is already registered
                # in the dictionary of volumetric physical groups atta-
                # ched to this boundary physical group. If not, updates
                # the dictionary and the counter of new boundary physical
                # groups

                if not (physical_group in new_boundary_dict[
                boundary_physical_group]):

                    new_boundary_number += 1

                    # Adds this key

                    new_boundary_dict[boundary_physical_group][
                    physical_group] = new_boundary_number
                    
                # Updates the boundary mesh function 

                boundary_mesh_function[facet.index()] = new_boundary_dict[
                boundary_physical_group][physical_group]

    # Returns the new boundary mesh function and the dictionary of phy-
    # sical groups

    return boundary_mesh_function, new_boundary_dict

# Defines a function to evaluate the centroid of a surface region given
# by an integer physical group

def evaluate_centroidSurface(physical_group, mesh_dataClass):

    if not isinstance(physical_group, int):

        # Tries to convert is

        physical_group = convert_physicalGroup(physical_group, 
        mesh_dataClass, "boundary")[0]

    print("Gets centroid of surface tagged as "+str(physical_group)+"\n")

    # Verifies if mesh data class is a string, with the file of the mesh

    if isinstance(mesh_dataClass, str):

        # Calls the mesh reader

        mesh_dataClass = read_mshMesh(mesh_dataClass)

    # Gets the position vector from the mesh

    position_vector = mesh_dataClass.x

    # Evaluates the area of this physical group

    area_inverse = (1.0/float(assemble(1.0*mesh_dataClass.ds(
    physical_group))))

    # Evaluates the centroid coordinates

    centroid_x = (area_inverse*float(assemble(position_vector[0]*
    mesh_dataClass.ds(physical_group))))

    centroid_y = (area_inverse*float(assemble(position_vector[1]*
    mesh_dataClass.ds(physical_group))))

    centroid_z = (area_inverse*float(assemble(position_vector[2]*
    mesh_dataClass.ds(physical_group))))

    return [centroid_x, centroid_y, centroid_z], area_inverse