From 30f04b46261b34d7575cdfe02addfa815b182943 Mon Sep 17 00:00:00 2001 From: Ben Sepanski <ben_sepanski@baylor.edu> Date: Mon, 1 Jun 2020 11:51:08 -0500 Subject: [PATCH] Added an importer for mesh geometries --- meshmode/interop/firedrake/mesh_geometry.py | 408 ++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 meshmode/interop/firedrake/mesh_geometry.py diff --git a/meshmode/interop/firedrake/mesh_geometry.py b/meshmode/interop/firedrake/mesh_geometry.py new file mode 100644 index 00000000..313f7c3e --- /dev/null +++ b/meshmode/interop/firedrake/mesh_geometry.py @@ -0,0 +1,408 @@ +from warnings import warn # noqa +from collections import defaultdict +import numpy as np + +from meshmode.interop import ExternalImportHandler +from meshmode.interop.firedrake.coordinateless_functions_impl import \ + FiredrakeCoordinatelessFunctionImporter + + +class FiredrakeMeshGeometryImporter(ExternalImportHandler): + """ + This takes a :mod:`firedrake` :class:`MeshGeometry` + and converts its data so that :mod:`meshmode` can handle it. + + .. attribute:: data + + A :mod:`firedrake` :class:`MeshGeometry` instance + """ + + def __init__(self, + mesh, + coordinates_importer, + normals=None, + no_normals_warn=True): + """ + :param mesh: A :mod:`firedrake` :class:`MeshGeometry`. + We require that :aram:`mesh` have co-dimesnion + of 0 or 1. + Moreover, if :param:`mesh` is a 2-surface embedded in 3-space, + we _require_ that :function:`init_cell_orientations` + has been called already. + + :param coordinates_importer: An instance of + class :class:`FiredrakeCoordinatelessFunctionImporter` + to use, mapping the points of the topological mesh + to their coordinates (The function is coordinateless + in the sense that its *domain* has no coordinates) + + For other params see :meth:`orientations` + """ + super(FiredrakeMeshGeometryImporter, self).__init__(mesh) + + # {{{ Make sure input data is valid + + # Ensure is not a topological mesh + if mesh.topological == mesh: + raise TypeError(":param:`mesh` must be of type" + " :class:`firedrake.mesh.MeshGeometry`") + + # Ensure dimensions are in appropriate ranges + supported_dims = [1, 2, 3] + if mesh.geometric_dimension() not in supported_dims: + raise ValueError("Geometric dimension is %s. Geometric " + " dimension must be one of range %s" + % (mesh.geometric_dimension(), supported_dims)) + + # Raise warning if co-dimension is not 0 or 1 + co_dimension = mesh.geometric_dimension() - mesh.topological_dimension() + if co_dimension not in [0, 1]: + raise ValueError("Codimension is %s, but must be 0 or 1." % + (co_dimension)) + + # Ensure coordinates are coordinateless + if not isinstance(coordinates_importer, + FiredrakeCoordinatelessFunctionImporter): + raise ValueError(":param:`coordinates_importer` must be of type" + " FiredrakeCoordinatelessFunctionImporter") + + fspace_importer = coordinates_importer.function_space_importer() + topology_importer = fspace_importer.mesh_importer() + + if topology_importer != mesh.topology: + raise ValueError("Topology :param:`coordinates` lives on must be " + "the same " + "topology that :param:`mesh` lives on") + + # }}} + + # For sharing data like in firedrake + self._shared_data_cache = defaultdict(dict) + + # Store input information + self._coordinates_importer = coordinates_importer + self._topology_importer = topology_importer + + self._normals = normals + self._no_normals_warn = no_normals_warn + + # To be computed later + self._vertex_indices = None + self._vertices = None + self._nodes = None + self._group = None + self._orient = None + self._facial_adjacency_groups = None + self._meshmode_mesh = None + + def callback(cl_ctx): + """ + Finish initialization by creating a coordinates function importer + for public access on this mesh which is not "coordinateless" (i.e. + its domain has coordinates) + """ + from meshmode.interop.firedrake.function import \ + FiredrakeFunctionImporter + from meshmode.interop.firedrake.functionspace import \ + FiredrakeWithGeometryImporter + from firedrake import Function + + coordinates_fs = self.data.coordinates.function_space() + coordinates_fs_importer = \ + self._coordinates_importer.function_space_importer() + + V_importer = FiredrakeWithGeometryImporter(cl_ctx, + coordinates_fs, + coordinates_fs_importer, + self) + f = Function(V_importer.data, val=self._coordinates_a.data) + self._coordinates_function_importer = \ + FiredrakeFunctionImporter(f, V_importer) + + del self._callback + + self._callback = callback + + def initialized(self): + return not hasattr(self, '_callback') + + def init(self, cl_ctx): + if not self.initialized(): + self._callback(cl_ctx) + + def __getattr__(self, attr): + """ + If can't find an attribute, look in the underlying + topological mesh + (Done like :class:`firedrake.function.MeshGeometry`) + """ + return getattr(self._topology_importer, attr) + + @property + def coordinates_importer(self): + """ + Return coordinates as a function + + PRECONDITION: Have called :meth:`init` + """ + try: + return self._coordinates_function_importer + except AttributeError: + raise AttributeError("No coordinates function, have you finished" + " initializing this object" + " (i.e. have you called :meth:`init`)?") + + def _compute_vertex_indices_and_vertices(self): + if self._vertex_indices is None: + fspace_importer = self.coordinates_importer.function_space_importer() + finat_element_importer = fspace_importer.finat_element_importer + + # Convert cell node list of mesh to vertex list + unit_vertex_indices = finat_element_importer.unit_vertex_indices() + cfspace = self.data.coordinates.function_space() + if self.icell_to_fd is not None: + cell_node_list = cfspace.cell_node_list[self.icell_to_fd] + else: + cell_node_list = cfspace.cell_node_list + + vertex_indices = cell_node_list[:, unit_vertex_indices] + + # Get maps newnumbering->old and old->new (new numbering comes + # from removing the non-vertex + # nodes) + vert_ndx_to_fd_ndx = np.unique(vertex_indices.flatten()) + fd_ndx_to_vert_ndx = dict(zip(vert_ndx_to_fd_ndx, + np.arange(vert_ndx_to_fd_ndx.shape[0], + dtype=np.int32) + )) + # Get vertices array + vertices = np.real( + self.data.coordinates.dat.data[vert_ndx_to_fd_ndx]) + + #:mod:`meshmode` wants shape to be [ambient_dim][nvertices] + if len(vertices.shape) == 1: + # 1 dim case, (note we're about to transpose) + vertices = vertices.reshape(vertices.shape[0], 1) + vertices = vertices.T.copy() + + # Use new numbering on vertex indices + vertex_indices = np.vectorize(fd_ndx_to_vert_ndx.get)(vertex_indices) + + # store vertex indices and vertices + self._vertex_indices = vertex_indices + self._vertices = vertices + + def vertex_indices(self): + """ + A numpy array of shape *(nelements, nunitvertices)* + holding the vertex indices associated to each element + """ + self._compute_vertex_indices_and_vertices() + return self._vertex_indices + + def vertices(self): + """ + Return the mesh vertices as a numpy array of shape + *(dim, nvertices)* + """ + self._compute_vertex_indices_and_vertices() + return self._vertices + + def nodes(self): + """ + Return the mesh nodes as a numpy array of shape + *(dim, nelements, nunitnodes)* + """ + if self._nodes is None: + coords = self.data.coordinates.dat.data + cfspace = self.data.coordinates.function_space() + + if self.icell_to_fd is not None: + cell_node_list = cfspace.cell_node_list[self.icell_to_fd] + else: + cell_node_list = cfspace.cell_node_list + self._nodes = np.real(coords[cell_node_list]) + + # reshape for 1D so that [nelements][nunit_nodes][dim] + if len(self._nodes.shape) != 3: + self._nodes = np.reshape(self._nodes, self._nodes.shape + (1,)) + + # Change shape to [dim][nelements][nunit_nodes] + self._nodes = np.transpose(self._nodes, (2, 0, 1)) + + return self._nodes + + def group(self): + """ + Return an instance of :class:meshmode.mesh.SimplexElementGroup` + corresponding to the mesh :attr:`data` + """ + if self._group is None: + from meshmode.mesh import SimplexElementGroup + from meshmode.mesh.processing import flip_simplex_element_group + + fspace_importer = self.coordinates_importer.function_space_importer() + finat_element_importer = fspace_importer.finat_element_importer + + # IMPORTANT that set :attr:`_group` because + # :meth:`orientations` may call :meth:`group` + self._group = SimplexElementGroup( + finat_element_importer.data.degree, + self.vertex_indices(), + self.nodes(), + dim=self.cell_dimension(), + unit_nodes=finat_element_importer.unit_nodes()) + + self._group = flip_simplex_element_group(self.vertices(), self._group, + self.orientations() < 0) + + return self._group + + def orientations(self): + """ + Return the orientations of the mesh elements: + an array, the *i*th element is > 0 if the *ith* element + is positively oriented, < 0 if negatively oriented + + :param normals: _Only_ used if :param:`mesh` is a 1-surface + embedded in 2-space. In this case, + - If *None* then + all elements are assumed to be positively oriented. + - Else, should be a list/array whose *i*th entry + is the normal for the *i*th element (*i*th + in :param:`mesh`*.coordinate.function_space()*'s + :attribute:`cell_node_list`) + + :param no_normals_warn: If *True*, raises a warning + if :param:`mesh` is a 1-surface embedded in 2-space + and :param:`normals` is *None*. + """ + if self._orient is None: + # compute orientations + tdim = self.data.topological_dimension() + gdim = self.data.geometric_dimension() + + orient = None + if gdim == tdim: + # We use :mod:`meshmode` to check our orientations + from meshmode.mesh.processing import \ + find_volume_mesh_element_group_orientation + + orient = \ + find_volume_mesh_element_group_orientation(self.vertices(), + self.group()) + + if tdim == 1 and gdim == 2: + # In this case we have a 1-surface embedded in 2-space + orient = np.ones(self.nelements()) + if self._normals: + for i, (normal, vertices) in enumerate(zip( + np.array(self._normals), self.vertices())): + if np.cross(normal, vertices) < 0: + orient[i] = -1.0 + elif self._no_normals_warn: + warn("Assuming all elements are positively-oriented.") + + elif tdim == 2 and gdim == 3: + # In this case we have a 2-surface embedded in 3-space + orient = self.data.cell_orientations().dat.data + r""" + Convert (0 \implies negative, 1 \implies positive) to + (-1 \implies negative, 1 \implies positive) + """ + orient *= 2 + orient -= np.ones(orient.shape, dtype=orient.dtype) + + self._orient = orient + #Make sure the mesh fell into one of the cases + """ + NOTE : This should be guaranteed by previous checks, + but is here anyway in case of future development. + """ + assert self._orient is not None + + return self._orient + + def face_vertex_indices_to_tags(self): + """ + Returns a dict mapping frozensets + of vertex indices (which identify faces) to a + list of boundary tags + """ + finat_element = self.data.coordinates.function_space().finat_element + exterior_facets = self.data.exterior_facets + + # fvi_to_tags maps frozenset(vertex indices) to tags + fvi_to_tags = {} + # maps faces to local vertex indices + connectivity = finat_element.cell.connectivity[(self.cell_dimension()-1, 0)] + + # Compatability for older versions of firedrake + try: + local_fac_number = exterior_facets.local_facet_number + except AttributeError: + local_fac_number = exterior_facets.local_facet_dat.data + + for i, (icells, ifacs) in enumerate(zip(exterior_facets.facet_cell, + local_fac_number)): + # Compatability for older versions of firedrake + try: + iter(ifacs) + except TypeError: + ifacs = [ifacs] + + for icell, ifac in zip(icells, ifacs): + # If necessary, convert to new cell numbering + if self.fd_to_icell is not None: + if icell not in self.fd_to_icell: + continue + else: + icell = self.fd_to_icell[icell] + + # record face vertex indices to tag map + cell_vertices = self.vertex_indices()[icell] + facet_indices = connectivity[ifac] + fvi = frozenset(cell_vertices[list(facet_indices)]) + fvi_to_tags.setdefault(fvi, []) + fvi_to_tags[fvi].append(exterior_facets.markers[i]) + + # }}} + + return fvi_to_tags + + def facial_adjacency_groups(self): + """ + Return a list of :mod:`meshmode` :class:`FacialAdjacencyGroups` + as used in the construction of a :mod:`meshmode` :class:`Mesh` + """ + # {{{ Compute facial adjacency groups if not already done + + if self._facial_adjacency_groups is None: + from meshmode.mesh import _compute_facial_adjacency_from_vertices + + self._facial_adjacency_groups = _compute_facial_adjacency_from_vertices( + [self.group()], + self.bdy_tags(), + np.int32, np.int8, + face_vertex_indices_to_tags=self.face_vertex_indices_to_tags()) + + # }}} + + return self._facial_adjacency_groups + + def meshmode_mesh(self): + """ + PRECONDITION: Have called :meth:`init` + """ + if self._meshmode_mesh is None: + assert self.initialized(), \ + "Must call :meth:`init` before :meth:`meshmode_mesh`" + + from meshmode.mesh import Mesh + self._meshmode_mesh = \ + Mesh(self.vertices(), [self.group()], + boundary_tags=self.bdy_tags(), + nodal_adjacency=self.nodal_adjacency(), + facial_adjacency_groups=self.facial_adjacency_groups()) + + return self._meshmode_mesh -- GitLab