diff --git a/src/bvpy/domains/abstract.py b/src/bvpy/domains/abstract.py index 56c1c57d652e966f323331cb207695285c3e0a05..dc3b1a5a3f9a5e99a827b161ca5d809fb251da88 100644 --- a/src/bvpy/domains/abstract.py +++ b/src/bvpy/domains/abstract.py @@ -22,7 +22,10 @@ import gmsh import sys +import os +import shutil from bvpy import logger as log +from bvpy import MPI_COMM_WORLD import meshio as mio from abc import ABC, abstractmethod import tempfile @@ -157,7 +160,7 @@ class AbstractDomain(ABC): AbstractDomain._domain_instances += 1 if AbstractDomain._domain_instances == 1: - gmsh.initialize() + gmsh.initialize() # for all processes if clear: gmsh.clear() @@ -167,8 +170,15 @@ class AbstractDomain(ABC): self._cell_size = cell_size self._algorithm = algorithm - self._mesh_dir = tempfile.TemporaryDirectory() - self._dir_name = self._mesh_dir.name + if MPI_COMM_WORLD.Get_rank() == 0: + # Creation of the temporary folder only for the process of rank 0 + self._mesh_dir = tempfile.TemporaryDirectory() # keep a reference to avoid suppression of the temp file + dir_name = self._mesh_dir .name + else: + dir_name = None + + # Diffusion of the folder to the other processess + self._dir_name = MPI_COMM_WORLD.bcast(dir_name, root=0) self.set_cell_type(cell_type) self.set_verbosity(verbosity) @@ -194,10 +204,16 @@ class AbstractDomain(ABC): """Clears the temporary file containing the domain. """ - self._mesh_dir.cleanup() + #self._mesh_dir.cleanup() + + if MPI_COMM_WORLD.Get_rank() == 0: + # Remove the temporary folder and its content using process of rank 0 + if self._dir_name and os.path.isdir(self._dir_name): + shutil.rmtree(self._dir_name) + AbstractDomain._domain_instances -= 1 if AbstractDomain._domain_instances == 0: - gmsh.finalize() + gmsh.finalize() # for all processes def __repr__(self): """Returns a short description of the considered domain. @@ -300,6 +316,13 @@ class AbstractDomain(ABC): """ pass + def create_geometry(self, *args, **kwargs): + """ Routine to control the update of the geometry if multi-threading is used""" + + if MPI_COMM_WORLD.Get_rank() == 0: + log.debug('Create geometry of the domain!') + # - only the process of rank 0 updates the geometry + self.geometry(*args, **kwargs) def run_gui(self): """Generates a quick gmsh gui with the current model in it. @@ -433,7 +456,7 @@ class AbstractDomain(ABC): if self.mesh is not None: self.model.mesh.clear() - self._mesh_conversion() + self._mesh_conversion_mpi() self.ds = fe.Measure('ds', domain=self.mesh, subdomain_data=self.bdata) self.dx = fe.Measure('dx', domain=self.mesh, subdomain_data=self.cdata) @@ -463,6 +486,7 @@ class AbstractDomain(ABC): ele_dim = CELL_TYPE[self._cell_type] log.debug(f"Convert a mesh with topological element of dimension {ele_dim}") + # Seul le processus de rang 0 génère et écrit le maillage self.model.mesh.generate(ele_dim) # - Assert that we will get all the entites of the gmsh mesh during the conversion by catching the number of @@ -618,6 +642,204 @@ class AbstractDomain(ABC): self.bdata = _generate_default_boundary_data(mesh) self.sub_boundary_names = {0: 'boundary'} + def _mesh_conversion_mpi(self): + """Converts a temporary gmsh mesh into a fenics (dolphin) one. + + Returns + ------- + mesh : :class:`Mesh<dolfin.cpp.mesh.Mesh>` + Fenics mesh corresponding to the domain. + bdata : :class:`MeshFunctionSizet<dolfin.cpp.mesh.MeshFunction>` + MeshFunction that labels the borders (i.e. outermost facets) + of the mesh of the domain. + cdata : :class:`MeshFunctionSizet<dolfin.cpp.mesh.MeshFunction>` + MeshFunction that labels the inner elements (i.e. cells) + of the mesh of the domain. + + """ + # - Get the finite element topological dimension + ele_dim = CELL_TYPE[self._cell_type] + log.debug(f"Convert a mesh with topological element of dimension {ele_dim}") + + # - Set default values for the domain & boundaries labels/name + has_bdata = False # flag to fill automatically labels to boundary + sub_boundary_names = {0: 'boundary'} # default naming for the boundary + has_cdata = False # flag to fill automatically labels if needed + sub_domain_names = {0: 'domain'} # default naming for the domain + + ### From GMSH to MESHIO + # Only the rank 0 process generate the gmsh mesh, convert it to MeshIO and save it as XDMF file. + # It will avoid duplicates + if MPI_COMM_WORLD.Get_rank() == 0: + log.debug('Generate GMSH mesh and convert to MeshIO') + self.model.mesh.generate(ele_dim) + + # - Assert that we will get all the entites of the gmsh mesh during the conversion by catching the number of + # entities + ELEMENT_GMSH_TYPES = {'line': 1, 'triangle': 2, 'tetra': 4} # Gmsh code for a given element type + element_type, element_tags, _ = self.model.mesh.getElements(ele_dim) # get the entities of dimension `ele_dim` + N = len(element_tags[np.where(element_type == ELEMENT_GMSH_TYPES[self._cell_type])[0][0]]) # number of entities + + gmsh.write(self._dir_name+'/mesh.msh') + + path = self._dir_name + '/mesh.msh' + mesh_io = mio.read(path, file_format='gmsh') + + # - Get the coordinates of the vertices + ## Note : results if embedded space is != 3 ? + mesh_io.points = mesh_io.points[:, :self.dim] + + if self._cell_type not in mesh_io.cells_dict: + raise RuntimeError(f'None elements of the required topological dimension (element_dim = {ele_dim} for ' + f'{self._cell_type}) have been found during the conversion from Gmsh to MeshIo.' + f'\nKnown issue 1: no Entities of the required topological dimension ({ele_dim}) ' + f'are defined in your Gmsh mesh. Use YourAbstractObject.factory.getEntities(dim={ele_dim}) ' + f'to check if it is the case.\nKnown issue 2: some PhysicalGroups have been set for ' + f'Entities of topological dimension < {ele_dim}. If PhysicalGroups are defined, be sure ' + f'to define them for all the Entities at topological dimension = {ele_dim}. If not, ' + f'all the unmarked Entities will be ignored.\nKnown issue 3: ???') + elif len(mesh_io.cells_dict[self._cell_type]) != N: + raise Warning(f'Some elements have been lost during the conversion of the gmsh mesh, one possible ' + f'reason is the partial labelling of entities at topological dimension {ele_dim}. Make ' + f'sure that you label all the entities ! Unlabelled entities at dim {ele_dim} will be' + f' ignored...') + + # - Extract physical properties (and possible name) for the domain + if ('gmsh:physical' in mesh_io.cell_data_dict.keys() + and self._cell_type in mesh_io.cell_data_dict["gmsh:physical"].keys()): + log.debug(f"Found labels at the dimension {ele_dim}") + has_cdata = True + + # Notes: + # 'gmsh: physical' lists the tags given to group of entities of same dimension. Here, the dimension will + # be defined by ele_dim (from the _cell_type). + # There tags can be defined by addPhysicalGroup(ele_dim, ele_tags, tagOfGroup) in gmsh. + # It is possible to set a name for a given PhysicalGroup using setPhysicalName in gmsh. + # Gmsh does not allowed intersection of PhysicalGroup, the first tag for a given entity + # will be the one you will find here. + # Ex: if elements [0, 1] are tagged with 4, + # then 1 is tagged with 2, + # you will see ele. 1 with tag 4 here. + # Important warning : In the case where some entities have a PhysicialGroup, if it exists entities without + # any PhysicialGroup, they will be ignored ! + # TO BE CLEAR : you will loose part of your mesh ! + # Conclusion : using PhysicalGroup, the resulting mesh will always contains PhysicialGroups that form a + # partition of the mesh (at the given topological dimension) + # However, it is possible that some of the PhysicialGroup do not have a PhysicalName, we will + # deal with this situation by setting a default name. + + labels = mesh_io.cell_data_dict["gmsh:physical"][self._cell_type] + + # - Catch PhysicalName associated with PhysicalGroup + # - reformat field_data as {tag : name} and filter PhysicalName (`ele_dim` topological dimension) + if mesh_io.field_data: + sub_domain_names = {lab_dim[0]: lab_name + for lab_name, lab_dim in mesh_io.field_data.items() + if lab_dim[1] == (ele_dim)} # tag: name + log.debug(f"domains : field data => {sub_domain_names} found") + else: + sub_domain_names = {i: f'sub_domain_{i}' for i in np.unique(labels)} + log.debug(f"domains : default field data => {sub_domain_names}") + + if set(labels) != set(sub_domain_names): + log.warning('Domain : the number of PhysicalGroup does not correspond to the number of PhysicalName !') + log.warning('Domain : some of your PhysicalGroup will be automatically named...') + + # - set automatic names for the other labels + sub_domain_names.update({lab: f'sub_domain_{i}' + for i, lab in enumerate(set(labels) - set(sub_domain_names))}) + + mio.write(self._dir_name + '/' + 'mesh.xdmf', + mio.Mesh(points=mesh_io.points, + cells={self._cell_type: + mesh_io.cells_dict[self._cell_type]}, + cell_data={f"{self._cell_type}_labels": [labels]})) + else: + log.debug(f"No labels found at the dimension {ele_dim}, use default.") + + mio.write(self._dir_name + '/' + 'mesh.xdmf', + mio.Mesh(points=mesh_io.points, + cells={self._cell_type: + mesh_io.cells_dict[self._cell_type]})) + + # - Extract physical properties (and possible names) for the boundaries (ele_dim - 1) + bnd_cell_type = [cell_type for cell_type, d in CELL_TYPE.items() if d == (ele_dim - 1)][0] + + if ('gmsh:physical' in mesh_io.cell_data_dict.keys() + and bnd_cell_type in mesh_io.cell_data_dict["gmsh:physical"].keys()): + has_bdata = True + labels = mesh_io.cell_data_dict["gmsh:physical"][bnd_cell_type] + + # - Catch PhysicalName associated with PhysicalGroup + # - reformat field_data as {tag : name} and filter PhysicalName (`ele_dim` - 1 topological dimension) + if mesh_io.field_data: + sub_boundary_names = {lab_dim[0]: lab_name + for lab_name, lab_dim in mesh_io.field_data.items() + if lab_dim[1] == (ele_dim - 1)} # tag: name + log.debug(f"boundary : field data => {sub_boundary_names} found") + else: + sub_boundary_names = {i: f'sub_boundary_{i}' for i in np.unique(labels)} + log.debug(f"boundary : default field data => {sub_boundary_names}") + + if set(labels) != set(sub_boundary_names): + log.warning('Boundary: the number of PhysicalGroup does not correspond to the number of PhysicalName !') + log.warning('Boundary: some of your PhysicalGroup will be automatically named...') + + # - set automatic names for the other labels + sub_boundary_names.update({lab: f'sub_boundary_{i}' + for i, lab in enumerate(set(labels)-set(sub_boundary_names))}) + + bnd_mesh = mio.Mesh(points=mesh_io.points, + cells={bnd_cell_type: + mesh_io.cells_dict[bnd_cell_type]}, + cell_data={f"boundary": [labels]}) + mio.xdmf.write(self._dir_name + '/mesh_boundary.xdmf', bnd_mesh) + + # Update state of other processes from rank 0 (labels and sub-domain names) + has_cdata = MPI_COMM_WORLD.bcast(has_cdata, root=0) + has_bdata = MPI_COMM_WORLD.bcast(has_bdata, root=0) + sub_domain_names = MPI_COMM_WORLD.bcast(sub_domain_names, root=0) + sub_boundary_names = MPI_COMM_WORLD.bcast(sub_boundary_names, root=0) + + # Synchronize the MPI processes (other processes will wait for the rank 0 for the mesh creation + MPI_COMM_WORLD.Barrier() + + ### Reading of the XDMF mesh in parallel (all the processes will get the complete mesh) + log.debug('Generate FeniCS mesh !') + log.debug(f'{self._cell_type} for {MPI_COMM_WORLD.Get_rank()}') + + # Init FeniCS mesh + mesh = fe.Mesh() + + # Read the saved mesh + with fe.XDMFFile(MPI_COMM_WORLD, self._dir_name + '/' + 'mesh.xdmf') as f: + f.read(mesh) + + # Fill the cdata + if has_cdata: + mvc = fe.MeshValueCollection("size_t", mesh, ele_dim) + with fe.XDMFFile(MPI_COMM_WORLD, self._dir_name + '/' + 'mesh.xdmf') as infile: + infile.read(mvc, f'{self._cell_type}_labels') + cdata = fe.cpp.mesh.MeshFunctionSizet(mesh, mvc) + else: + cdata = _generate_default_cell_data(mesh) + + # Fill the bdata + if has_bdata: + mvc = fe.MeshValueCollection("size_t", mesh, ele_dim - 1) + with fe.XDMFFile(MPI_COMM_WORLD, self._dir_name + '/mesh_boundary.xdmf') as infile: + infile.read(mvc, 'boundary') + bdata = fe.cpp.mesh.MeshFunctionSizet(mesh, mvc) + else: + bdata = _generate_default_boundary_data(mesh) + + # Stored the mesh information + self.mesh = mesh + self.cdata = cdata + self.sub_domain_names = sub_domain_names + self.bdata = bdata + self.sub_boundary_names = sub_boundary_names + def info(self): """Prints the descrition of the domain. @@ -628,6 +850,20 @@ class AbstractDomain(ABC): """ print(self._details) +class GmshProxy: + def __init__(self, factory): + self._factory = factory + + def __getattr__(self, name): + """Redirige les appels de méthode vers factory si le rang est 0.""" + def method(*args, **kwargs): + log.debug(f'Call from {MPI_COMM_WORLD.Get_rank()} of the method {name}') + if MPI_COMM_WORLD.Get_rank() == 0: + log.debug(f'Ok for {MPI_COMM_WORLD.Get_rank()} of the method {name}') + return getattr(self._factory, name)(*args, **kwargs) + else: + log.debug(f'NO for {MPI_COMM_WORLD.Get_rank()} of the method {method}') + return method class BuiltInModel(object): """A model based on the Gmsh `geo` factory. @@ -672,7 +908,8 @@ class OccModel(object): """ super().__init__() - self.factory = gmsh.model.occ + #self.factory = gmsh.model.occ + self.factory = GmshProxy(gmsh.model.occ) def __sub__(self, other): """Cutting the left hand side Domain with the right hand side domain. diff --git a/src/bvpy/domains/primitives.py b/src/bvpy/domains/primitives.py index 7b8e18d959d19fd976a7a9e26716ac5403731a06..8c759f0ced54089499fdba466f86e24671ded081 100644 --- a/src/bvpy/domains/primitives.py +++ b/src/bvpy/domains/primitives.py @@ -124,7 +124,7 @@ class Rectangle(AbstractDomain, OccModel): super(Rectangle, self).__init__(**kwargs) self._characteristic_size = {'Length': length, 'Width': width} - self.geometry(x, y, length, width) + self.create_geometry(x, y, length, width) def geometry(self, x, y, length, width): """Adds a rectangle to the Gmsh factory computing the domain. @@ -492,7 +492,7 @@ class Cylinder(AbstractDomain, OccModel): super(Cylinder, self).__init__(**kwargs) self._characteristic_size = {'Length': max(dx, dy, dz), 'Width': min(dx, dy, dz)} - self.geometry(x, y, z, dx, dy, dz, r, open) + self.create_geometry(x, y, z, dx, dy, dz, r, open) def geometry(self, x, y, z, dx, dy, dz, r, open): self.volumes[0] = self.factory.addCylinder(x, y, z, dx, dy, dz, r)