Source code for firedrake.mg.mesh

import numpy as np
import warnings
from fractions import Fraction
from collections import defaultdict
from collections.abc import Sequence
import pyop2
from pyop2.datatypes import IntType

import petsctools
import firedrake
from functools import cached_property

from firedrake import utils
from firedrake.cython import mgimpl as impl
import firedrake.cython.dmcommon as dmcommon
from .utils import set_level

__all__ = ("HierarchyBase", "MeshHierarchy", "ExtrudedMeshHierarchy", "NonNestedHierarchy",
           "SemiCoarsenedExtrudedHierarchy", "SubmeshHierarchy")


def make_unoverlapped_dm(dm):
    """Effectively invert dm.distributeOverlap().

    The resulting plex has the identical data structure as the one before
    distributeOverlap(). This is algorithmically guaranteed.
    """
    tdim = dm.getDimension()
    dm = dmcommon.submesh_create(dm, tdim, "depth", tdim, ignore_label_halo=True)
    dm.removeLabel("pyop2_core")
    dm.removeLabel("pyop2_owned")
    dm.removeLabel("pyop2_ghost")
    return dm


[docs] class HierarchyBase(object): """Create an encapsulation of an hierarchy of meshes. Parameters ---------- meshes : List of meshes (coarse to fine). coarse_to_fine_cells : Optional dictionary of numpy arrays keyed by level pair, mapping each coarse cell to its fine children. Every row is as wide as the busiest coarse cell's count. After adaptive refinement, a coarse cell that was not refined has fewer children, so its row is right-padded with -1. Here -1 is only padding. Omit this and ``fine_to_coarse_cells`` together to derive both maps from ``fine_to_coarse_points``. fine_to_coarse_cells : Optional dictionary of numpy arrays keyed by level, mapping each fine cell to its coarse parent cell. Here -1 marks a fine cell that has no parent in the coarse mesh. This happens in a `SubmeshHierarchy` that contains interior facets. A fine facet inside a coarse cell comes from that volume cell, which is not a cell of the coarse submesh. Omit this and ``coarse_to_fine_cells`` together to derive both maps from ``fine_to_coarse_points``. refinements_per_level : Number of mesh refinements each multigrid level should "see". nested : Is this mesh hierarchy nested? fine_to_coarse_points : Dict of numpy arrays for each level that was refined from the level below it. Each array maps fine DMPlex points to their coarse DMPlex source points, or to -1 when no corresponding coarse point is present. Cell maps derived from this point map follow Firedrake cell numbering. Notes ----- Most of the time, you do not need to create this object yourself, instead using `MeshHierarchy`, `ExtrudedMeshHierarchy`, or `NonNestedHierarchy`. """ def __init__(self, meshes, coarse_to_fine_cells=None, fine_to_coarse_cells=None, refinements_per_level=1, nested=False, fine_to_coarse_points=None): petsctools.cite("Mitchell2016") self._meshes = list(meshes) self.meshes = self._meshes[::refinements_per_level] self.refinements_per_level = refinements_per_level self.nested = nested self.fine_to_coarse_points = dict(fine_to_coarse_points or {}) if (coarse_to_fine_cells is None) != (fine_to_coarse_cells is None): raise ValueError("coarse_to_fine_cells and fine_to_coarse_cells must be provided together") if coarse_to_fine_cells is None: coarse_to_fine_cells = {} fine_to_coarse_cells = {Fraction(0, 1): None} for i, (coarse, fine) in enumerate(zip(self._meshes[:-1], self._meshes[1:])): c2f, f2c = impl.coarse_to_fine_cells( coarse, fine, self.fine_to_coarse_points[Fraction(i+1, refinements_per_level)]) coarse_to_fine_cells[Fraction(i, refinements_per_level)] = c2f fine_to_coarse_cells[Fraction(i+1, refinements_per_level)] = f2c self.coarse_to_fine_cells = coarse_to_fine_cells self.fine_to_coarse_cells = fine_to_coarse_cells for level, m in enumerate(meshes): set_level(m, self, Fraction(level, refinements_per_level)) for level, m in enumerate(self): set_level(m, self, level) self._shared_data_cache = defaultdict(dict)
[docs] @cached_property def comm(self): comm = self[0].comm if not all(m.comm == comm for m in self): raise NotImplementedError("All meshes in hierarchy must be on same communicator") return comm
def __iter__(self): """Iterate over the hierarchy of meshes from coarsest to finest""" for m in self.meshes: yield m def __len__(self): """Return the size of hierarchy""" return len(self.meshes) def __getitem__(self, idx): """Return a mesh in the hierarchy :arg idx: The :func:`~.Mesh` to return""" return self.meshes[idx]
[docs] def add_mesh(self, mesh, coarse_to_fine_cells=None, fine_to_coarse_cells=None): """Add a mesh on top of the finest level of the hierarchy. Only supported for hierarchies with ``refinements_per_level == 1``. Parameters ---------- mesh : The mesh to add, usually obtained by calling :meth:`~firedrake.mesh.MeshGeometry.refine_marked_elements` on the current finest mesh. coarse_to_fine_cells : Map from the cells of the current finest mesh to the cells of ``mesh``. Defaults to the map ``mesh`` recorded when it was adaptively refined. fine_to_coarse_cells : Map from the cells of ``mesh`` to the cells of the current finest mesh. Defaults the same way as ``coarse_to_fine_cells``. Returns ------- MeshGeometry The mesh that was added. """ if self.refinements_per_level != 1: raise NotImplementedError("Cannot add a mesh to a hierarchy with " "refinements_per_level > 1") fine_to_coarse_points = None if mesh._adaptive_parent is self[-1]: fine_to_coarse_points = mesh._adaptive_fine_to_coarse_points if coarse_to_fine_cells is None or fine_to_coarse_cells is None: if fine_to_coarse_points is not None: coarse_to_fine_cells, fine_to_coarse_cells = impl.coarse_to_fine_cells( self[-1], mesh, fine_to_coarse_points) elif self.nested: raise ValueError("Expecting a mesh adaptively refined from the finest " "level of this hierarchy, or explicit cell maps") level = len(self.meshes) self._meshes.append(mesh) self.meshes.append(mesh) set_level(mesh, self, level) mesh.topology_dm.setRefineLevel(level) self.coarse_to_fine_cells[Fraction(level - 1, 1)] = coarse_to_fine_cells self.fine_to_coarse_cells[Fraction(level, 1)] = fine_to_coarse_cells self.fine_to_coarse_points[Fraction(level, 1)] = fine_to_coarse_points return mesh
[docs] def adapt(self, eta, theta: float): """Add a new mesh to the hierarchy by locally refining the finest mesh with a simplified variant of Dorfler marking. Parameters ---------- eta : A DG0 :class:`~firedrake.function.Function` with the local error estimator. theta : The threshold for marking as a fraction of the maximum error. Returns ------- MeshGeometry The mesh that was added. Note ---- Dorfler marking involves sorting all of the elements by decreasing error estimator and taking the minimal set that exceeds some fixed fraction of the total error. What this code implements is the simpler variant that doesn't have a proof of convergence (as far as I know) but works as well in practice. """ if not isinstance(eta, (firedrake.Function, firedrake.Cofunction)): raise TypeError(f"eta must be a Function or Cofunction, not a {type(eta).__name__}") M = eta.function_space() if M.finat_element.space_dimension() != 1: raise ValueError("eta must be a Function or Cofunction in DG0") mesh = self[-1] if M.mesh() is not mesh: raise ValueError("eta must be defined on the finest mesh of the hierarchy") # Take the maximum over all processes with eta.dat.vec_ro as evec: _, eta_max = evec.max() markers = firedrake.Function(M) markers.dat.data_wo[eta.dat.data_ro > theta * eta_max] = 1 return self.add_mesh(mesh.refine_marked_elements(markers))
[docs] def MeshHierarchy(mesh, refinement_levels=0, refinements_per_level=1, netgen_flags=None, reorder=None, distribution_parameters=None, callbacks=None, mesh_builder=firedrake.Mesh, nested=True): """Build a hierarchy of meshes by uniformly refining a coarse mesh. Parameters ---------- mesh : MeshGeometry the coarse mesh to refine refinement_levels : int the number of levels of uniform refinement. This may be dynamically increased by :meth:`HierarchyBase.adapt` or :meth:`HierarchyBase.add_mesh`. refinements_per_level : int the number of refinements for each level in the hierarchy. Adaptive refinement only supports one refinement per level. netgen_flags : dict or None Options for a mesh generated by Netgen. If ``mesh`` was generated by Netgen and this value is ``None``, the hierarchy reuses its Netgen flags. The vertices of each refined mesh are snapped onto the Netgen geometry. The ``"degree"`` option sets the degree of the curved coordinates, either as an integer or as a sequence with one entry per level in the hierarchy. The ``"cg"`` option sets their continuity. distribution_parameters : dict options controlling mesh distribution, see :py:func:`.Mesh` for details. If ``None``, use the same distribution parameters as were used to distribute the coarse mesh, otherwise, these options override the default. reorder : bool optional flag indicating whether to reorder the refined meshes. callbacks : tuple A 2-tuple of callbacks to call before and after refinement of the DM. The before callback receives the DM to be refined (and the current level), the after callback receives the refined DM (and the current level). mesh_builder Function to turn a DM into a ``Mesh``. Used by pyadjoint. nested : bool Are the meshes added to this hierarchy required to be nested? If `False`, :meth:`HierarchyBase.add_mesh` accepts a mesh that was not adaptively refined from the finest level. Returns ------- HierarchyBase The mesh hierarchy. """ nlevels = refinement_levels + 1 is_netgen = hasattr(mesh, "netgen_mesh") if netgen_flags is not None and not isinstance(netgen_flags, dict): raise TypeError("netgen_flags must be a dictionary or None") if netgen_flags is not None and not is_netgen: raise RuntimeError("Cannot pass netgen_flags to a mesh that was not generated by Netgen.") if is_netgen: utils.check_netgen_installed() from firedrake.netgen import _snap_to_netgen, _curve_netgen_mesh if netgen_flags is None: netgen_flags = mesh.netgen_flags degree = netgen_flags.get("degree", 1) if isinstance(degree, int): degree = (degree,) * nlevels if len(degree) != nlevels: raise ValueError(f"Expecting one coordinate degree per level in the hierarchy ({nlevels}), " f"got {len(degree)}.") cg_field = netgen_flags.get("cg") ngmeshes = [mesh.netgen_mesh] mesh = _curve_netgen_mesh(mesh, degree[0], cg_field=cg_field) if callbacks is not None: before, after = callbacks else: before = after = lambda dm, i: None # Refine an unoverlapped plex at each level. Keeping every dm here # unoverlapped means overlap only ever needs to be added once, by # mesh_builder below. cdm = mesh.topology_dm if refinement_levels > 0: cdm = make_unoverlapped_dm(cdm) cdm.setRefinementUniform(True) dms = [cdm] for i in range(refinement_levels*refinements_per_level): if i % refinements_per_level == 0: before(cdm, i) cdm.setSaveTransform() rdm = cdm.refine() if i % refinements_per_level == 0: after(rdm, i) if is_netgen: ngmeshes.append(_snap_to_netgen(rdm, mesh.netgen_mesh)) # Fix up coords if refining embedded circle or sphere if hasattr(mesh, '_radius'): # FIXME, really we need some CAD-like representation # of the boundary we're trying to conform to. This # doesn't DTRT really for cubed sphere meshes (the # refined meshes are no longer gnonomic). coords = rdm.getCoordinatesLocal().array.reshape(-1, mesh.geometric_dimension) scale = mesh._radius / np.linalg.norm(coords, axis=1).reshape(-1, 1) coords *= scale dms.append(rdm) cdm = rdm # mesh_builder adds overlap to each DMPlex in place, so first capture the # lgmaps and read the fine-to-coarse point map from each unoverlapped DM. lgmaps = [impl.create_lgmap(dm) for dm in dms] points = [impl.transform_source_points(dm) for dm in dms[1:]] # Build a mesh for each level, adding overlap here. parameters = {} if distribution_parameters is not None: parameters.update(distribution_parameters) else: parameters.update(mesh._distribution_parameters) parameters["partition"] = False meshes = [mesh] for rdm in dms[1:]: fmesh = mesh_builder( rdm, dim=mesh.geometric_dimension, distribution_parameters=parameters, reorder=reorder, comm=mesh.comm, ) meshes.append(fmesh) with pyop2.mpi.temp_internal_comm(meshes[0].comm) as icomm: num_halo_cells = icomm.allreduce( sum(m.cell_set.total_size - m.cell_set.size for m in meshes) ) if num_halo_cells == 0: lgmaps = [None] * len(dms) if is_netgen: for i in range(1, len(meshes)): meshes[i].netgen_mesh = ngmeshes[i] meshes[i].netgen_flags = netgen_flags level, remainder = divmod(i, refinements_per_level) if remainder == 0: meshes[i] = _curve_netgen_mesh(meshes[i], degree[level], cg_field=cg_field) for i, m in enumerate(meshes): # Firedrake counts multigrid levels, PETSc counts refinements m.topology_dm.setRefineLevel(i) fine_to_coarse_points = { Fraction(i+1, refinements_per_level): impl.overlapped_fine_to_coarse_points( meshes[i], meshes[i+1], points[i], lgmaps[i], lgmaps[i+1]) for i in range(len(points)) } return HierarchyBase(meshes, refinements_per_level=refinements_per_level, nested=nested, fine_to_coarse_points=fine_to_coarse_points)
[docs] def ExtrudedMeshHierarchy(base_hierarchy: HierarchyBase, height: float, base_layer: int = -1, refinement_ratio: int = 2, layers: Sequence[int] | None = None, kernel=None, extrusion_type: str = 'uniform', periodic: bool = False, gdim: int | None = None, mesh_builder=firedrake.ExtrudedMesh ) -> HierarchyBase: """Build a hierarchy of extruded meshes by extruding a hierarchy of meshes. Parameters ---------- base_hierarchy : HierarchyBase The unextruded base mesh hierarchy to extrude. height : float The height of the domain to extrude to. This is in contrast to the extrusion routines, which take ``layer_height``, the height of an individual layer. The height of an individual layer varies when the hierarchy is refined in the extruded dimension. base_layer : int The number of layers to use for the coarsest grid. refinement_ratio : int The ratio by which to increase ``base_layer`` at every refinement. A value of 2 gives standard uniform refinement, and a value of 1 does not refine in the extruded dimension, which gives a semicoarsened hierarchy. layers : Sequence[int] or None An alternative to specifying ``base_layer`` and ``refinement_ratio``. This sequence gives the number of layers at each level in the extruded hierarchy. Do not combine this option with ``base_layer`` and ``refinement_ratio``. The ratio of successive entries must be an integer for the multigrid transfer operators to work. kernel : pyop2.op2.Kernel or None An optional kernel that computes the coordinates of the extruded mesh. extrusion_type : str The algorithm that computes the extruded coordinates. Supported values include ``"uniform"``, ``"radial"``, ``"radial_hedgehog"``, and ``"custom"``. periodic : bool Whether to identify the top and bottom boundaries of each extruded mesh. Periodic extrusion requires a constant number of layers. gdim : int or None The number of spatial dimensions in the resulting mesh. This value is used only when ``kernel`` is provided. mesh_builder : collections.abc.Callable The function that turns a ``Mesh`` into an extruded mesh. This is used by pyadjoint. Returns ------- HierarchyBase The hierarchy of extruded meshes. """ if not isinstance(base_hierarchy, HierarchyBase): raise ValueError("Expecting a HierarchyBase, not a %r" % type(base_hierarchy)) if any(m.cell_set._extruded for m in base_hierarchy): raise ValueError("Meshes in base hierarchy must not be extruded") if layers is None: if base_layer == -1: raise ValueError("Must specify number of layers for coarsest grid with base_layer=N") layers = [base_layer * refinement_ratio**idx for idx in range(len(base_hierarchy._meshes))] else: if base_layer != -1: raise ValueError("Can't specify both layers and base_layer") meshes = [mesh_builder(m, layer, kernel=kernel, layer_height=height/layer, extrusion_type=extrusion_type, periodic=periodic, gdim=gdim) for (m, layer) in zip(base_hierarchy._meshes, layers)] return HierarchyBase(meshes, base_hierarchy.coarse_to_fine_cells, base_hierarchy.fine_to_coarse_cells, refinements_per_level=base_hierarchy.refinements_per_level, nested=base_hierarchy.nested)
[docs] def SemiCoarsenedExtrudedHierarchy(base_mesh, height, nref=1, base_layer=-1, refinement_ratio=2, layers=None, kernel=None, extrusion_type='uniform', gdim=None, mesh_builder=firedrake.ExtrudedMesh): """Build a hierarchy of extruded meshes with refinement only in the extruded dimension. :arg base_mesh: the unextruded base mesh to extrude. :arg nref: Number of refinements. :arg height: the height of the domain to extrude to. This is in contrast to the extrusion routines, which take in layer_height, the height of an individual layer. This is because when refining in the extruded dimension, the height of an individual layer will vary. :arg base_layer: the number of layers to use the extrusion of the coarsest grid. :arg refinement_ratio: the ratio by which base_layer should be increased on every refinement. refinement_ratio = 2 means standard uniform refinement. refinement_ratio = 1 means to not refine in the extruded dimension, i.e. the multigrid hierarchy will use semicoarsening. :arg layers: as an alternative to specifying base_layer and refinement_ratio, one may specify directly the number of layers to be used by each level in the extruded hierarchy. This option cannot be combined with base_layer and refinement_ratio. Note that the ratio of successive entries in this iterable must be an integer for the multigrid transfer operators to work. :arg mesh_builder: function used to turn a ``Mesh`` into an extruded mesh. Used by pyadjoint. See :func:`~.ExtrudedMesh` for the meaning of the remaining parameters. See also :func:`~.ExtrudedMeshHierarchy` if you want to extruded a hierarchy of unstructured meshes. """ if not isinstance(base_mesh, firedrake.mesh.MeshGeometry): raise ValueError(f"Can only extruded a mesh, not a {type(base_mesh)}") if base_mesh.cell_set._extruded: raise ValueError("Base mesh must not be extruded") if layers is None: if base_layer == -1: raise ValueError("Must specify number of layers for coarsest grid with base_layer=N") layers = [base_layer * refinement_ratio**idx for idx in range(nref+1)] else: if base_layer != -1: raise ValueError("Can't specify both layers and base_layer") if len(layers) == nref+1: raise ValueError("Need to provide a number of layers for every refined mesh. " f"Got {len(layers)}, needed {nref+1}") meshes = [mesh_builder(base_mesh, layer, kernel=kernel, layer_height=height/layer, extrusion_type=extrusion_type, gdim=gdim) for layer in layers] refinements_per_level = 1 identity = np.arange(base_mesh.cell_set.size, dtype=IntType).reshape(-1, 1) coarse_to_fine_cells = dict((Fraction(i, refinements_per_level), identity) for i in range(nref)) fine_to_coarse_cells = dict((Fraction(i+1, refinements_per_level), identity) for i in range(nref)) return HierarchyBase(meshes, coarse_to_fine_cells, fine_to_coarse_cells, refinements_per_level=refinements_per_level, nested=True)
def AdaptiveMeshHierarchy(*args, **kwargs): """Deprecated alias for `MeshHierarchy`.""" warnings.warn( "The ``AdaptiveMeshHierarchy`` constructor is deprecated and will be removed in a future " "release. Please use the ``MeshHierarchy`` constructor instead.", FutureWarning ) return MeshHierarchy(*args, **kwargs)
[docs] def NonNestedHierarchy(*meshes): return HierarchyBase(meshes, [None for _ in meshes], [None for _ in meshes], nested=False)
[docs] def SubmeshHierarchy(parent_hierarchy: HierarchyBase, subdim: int | None = None, subdomain_id: int | Sequence | None = None, label_name: str | None = None, name: str | None = None, ignore_halo: bool = False, reorder: bool | None = None, comm=None): """Build a hierarchy of submeshes from an existing mesh hierarchy. Parameters ---------- parent_hierarchy : Parent mesh hierarchy. subdim : Topological dimension of the submesh. Defaults to ``mesh.topological_dimension``. subdomain_id : Subdomain ID representing the submesh. If `None` the submesh will cover the entire domain. This is useful to obtain a codim-1 submesh over all facets or a submesh over a different communicator. label_name : Name of the label to search ``subdomain_id`` in. Defaults to ``'Cell Sets'`` or ``'Face Sets'`` depending on ``subdim``. name : str | None Name of the submesh. Defaults to ``mesh.name + "_submesh"``ยท ignore_halo : Whether to exclude the halo from the submesh. reorder : Whether to reorder the mesh entities. By default, the submesh will be reordered if the parent mesh was reordered. comm : PETSc.Comm | None An optional sub-communicator to define the submesh. By default, the submesh is defined on `mesh.comm`. Returns ------- HierarchyBase The submesh hierarchy. Notes ----- A facet submesh that contains interior facets, such as the skeleton obtained with ``subdim=tdim-1`` and ``subdomain_id=None``, is only partially nested. A fine facet inside a coarse cell has no parent in the coarse submesh. Injection is still possible, because the fine children of each coarse facet cover it. `prolong` and `restrict` raise `NotImplementedError` when a fine node lies only on facets without a parent. """ meshes = [firedrake.Submesh(mesh, subdim=subdim, subdomain_id=subdomain_id, label_name=label_name, name=name, ignore_halo=ignore_halo, reorder=reorder, comm=comm) for mesh in parent_hierarchy._meshes] refinements_per_level = parent_hierarchy.refinements_per_level fine_to_coarse_points = {} for i, (coarse, fine) in enumerate(zip(meshes[:-1], meshes[1:])): level = Fraction(i+1, refinements_per_level) parent_points = parent_hierarchy.fine_to_coarse_points.get(level) if parent_points is None: raise ValueError("SubmeshHierarchy requires a hierarchy whose DMPlex " f"points on level {level} are refined from the level below") # Map fine submesh points through the parent hierarchy, then translate # them into coarse-submesh numbering. A source outside the coarse # submesh remains -1; for example, a new interior facet can have a # coarse volume cell as its source. coarse_subpoints = coarse.topology_dm.getSubpointIS().indices parent_to_coarse = np.full(parent_hierarchy._meshes[i].topology_dm.getChart()[1], -1, dtype=IntType) parent_to_coarse[coarse_subpoints] = np.arange(len(coarse_subpoints), dtype=IntType) points = impl.compose_points(parent_points, fine.topology_dm.getSubpointIS().indices) fine_to_coarse_points[level] = impl.compose_points(parent_to_coarse, points) for i, m in enumerate(meshes): m.topology_dm.setRefineLevel(i) return HierarchyBase(meshes, refinements_per_level=refinements_per_level, nested=parent_hierarchy.nested, fine_to_coarse_points=fine_to_coarse_points)