Source code for firedrake.preconditioners.asm

import abc

from pyop2.datatypes import IntType
from firedrake.preconditioners.base import PCBase
from firedrake.petsc import PETSc
from firedrake.dmhooks import get_function_space
from firedrake.logging import warning
from tinyasm import _tinyasm as tinyasm
import numpy


__all__ = ("ASMPatchPC", "ASMStarPC", "ASMVankaPC", "ASMLinesmoothPC", "ASMExtrudedStarPC")


[docs] class ASMPatchPC(PCBase): ''' PC for PETSc PCASM should implement: - :meth:`get_patches` ''' @property @abc.abstractmethod def _prefix(self): "Options prefix for the solver (should end in an underscore)"
[docs] def initialize(self, pc): # Get context from pc _, P = pc.getOperators() dm = pc.getDM() self.prefix = pc.getOptionsPrefix() + self._prefix # Extract function space and mesh to obtain plex and indexing functions V = get_function_space(dm) # Obtain patches from user defined funtion ises = self.get_patches(V) # PCASM expects at least one patch, so we define an empty one on idle processes if len(ises) == 0: ises = [PETSc.IS().createGeneral(numpy.empty(0, dtype=IntType), comm=PETSc.COMM_SELF)] # Create new PC object as ASM type and set index sets for patches asmpc = PETSc.PC().create(comm=pc.comm) asmpc.incrementTabLevel(1, parent=pc) asmpc.setOptionsPrefix(self.prefix + "sub_") asmpc.setOperators(*pc.getOperators()) opts = PETSc.Options(self.prefix) backend = opts.getString("backend", default="petscasm").lower() # Either use PETSc's ASM PC or use TinyASM (as simple ASM # implementation designed to be fast for small block sizes). if backend == "petscasm": asmpc.setType(asmpc.Type.ASM) # Set default solver parameters asmpc.setASMType(PETSc.PC.ASMType.BASIC) sub_opts = PETSc.Options(asmpc.getOptionsPrefix()) if "sub_pc_type" not in sub_opts: sub_opts["sub_pc_type"] = "lu" if "sub_pc_factor_mat_ordering_type" not in sub_opts: # Preserve the natural ordering to avoid zero pivots in saddle-point problems sub_opts["sub_pc_factor_mat_ordering_type"] = "natural" # If an ordering type is provided, PCASM should not sort patch indices, otherwise it can. mat_type = P.getType() if not mat_type.endswith("sbaij"): sentinel = object() ordering = opts.getString("mat_ordering_type", default=sentinel) asmpc.setASMSortIndices(ordering is sentinel) lgmap = V.dof_dset.lgmap # Translate to global numbers ises = tuple(lgmap.applyIS(iset) for iset in ises) asmpc.setASMLocalSubdomains(len(ises), ises) elif backend == "tinyasm": _, P = asmpc.getOperators() lgmap = V.dof_dset.lgmap P.setLGMap(rmap=lgmap, cmap=lgmap) asmpc.setType("tinyasm") # TinyASM wants local numbers, no need to translate tinyasm.SetASMLocalSubdomains( asmpc, ises, [W.dm.getDefaultSF() for W in V], [W.block_size for W in V], sum(W.block_size * W.dof_dset.total_size for W in V)) asmpc.setUp() else: raise ValueError(f"Unknown backend type {backend}") asmpc.setFromOptions() self.asmpc = asmpc
[docs] @abc.abstractmethod def get_patches(self, V): ''' Get the patches used for PETSc PCASM :param V: the :class:`~.FunctionSpace`. :returns: a list of index sets defining the ASM patches in local numbering (before lgmap.apply has been called). ''' pass
[docs] def view(self, pc, viewer=None): self.asmpc.view(viewer=viewer)
[docs] def update(self, pc): # This is required to update an inplace ILU factorization if self.asmpc.getType() == "asm": for sub in self.asmpc.getASMSubKSP(): sub.getOperators()[0].setUnfactored()
[docs] def apply(self, pc, x, y): self.asmpc.apply(x, y)
[docs] def applyTranspose(self, pc, x, y): self.asmpc.applyTranspose(x, y)
[docs] def destroy(self, pc): if hasattr(self, "asmpc"): self.asmpc.destroy()
[docs] class ASMStarPC(ASMPatchPC): '''Patch-based PC using Star of mesh entities implmented as an :class:`ASMPatchPC`. ASMStarPC is an additive Schwarz preconditioner where each patch consists of all DoFs on the topological star of the mesh entity specified by `pc_star_construct_dim`. ''' _prefix = "pc_star_"
[docs] def get_patches(self, V): mesh = V._mesh mesh_dm = mesh.topology_dm if mesh.cell_set._extruded: warning("applying ASMStarPC on an extruded mesh") # Obtain the topological entities to use to construct the stars opts = PETSc.Options(self.prefix) depth = opts.getInt("construct_dim", default=0) ordering = opts.getString("mat_ordering_type", default="natural") # Accessing .indices causes the allocation of a global array, # so we need to cache these for efficiency V_local_ises_indices = tuple(iset.indices for iset in V.dof_dset.local_ises) # Build index sets for the patches ises = [] (start, end) = mesh_dm.getDepthStratum(depth) for seed in range(start, end): # Only build patches over owned DoFs if mesh_dm.getLabelValue("pyop2_ghost", seed) != -1: continue # Create point list from mesh DM pt_array, _ = mesh_dm.getTransitiveClosure(seed, useCone=False) pt_array = order_points(mesh_dm, pt_array, ordering, self.prefix) # Get DoF indices for patch indices = [] for (i, W) in enumerate(V): section = W.dm.getDefaultSection() for p in pt_array.tolist(): dof = section.getDof(p) if dof <= 0: continue off = section.getOffset(p) # Local indices within W W_indices = slice(off*W.block_size, W.block_size * (off + dof)) indices.extend(V_local_ises_indices[i][W_indices]) iset = PETSc.IS().createGeneral(indices, comm=PETSc.COMM_SELF) ises.append(iset) return ises
[docs] class ASMVankaPC(ASMPatchPC): '''Patch-based PC using closure of star of mesh entities implmented as an :class:`ASMPatchPC`. ASMVankaPC is an additive Schwarz preconditioner where each patch consists of all DoFs on the closure of the star of the mesh entity specified by `pc_vanka_construct_dim` (or codim). ''' _prefix = "pc_vanka_"
[docs] def get_patches(self, V): mesh = V._mesh mesh_dm = mesh.topology_dm if mesh.layers: warning("applying ASMVankaPC on an extruded mesh") # Obtain the topological entities to use to construct the stars opts = PETSc.Options(self.prefix) depth = opts.getInt("construct_dim", default=-1) height = opts.getInt("construct_codim", default=-1) if (depth == -1 and height == -1) or (depth != -1 and height != -1): raise ValueError(f"Must set exactly one of {self.prefix}construct_dim or {self.prefix}construct_codim") exclude_subspaces = list(map(int, opts.getString("exclude_subspaces", default="-1").split(","))) include_type = opts.getString("include_type", default="star").lower() if include_type not in ["star", "entity"]: raise ValueError(f"{self.prefix}include_type must be either 'star' or 'entity', not {include_type}") include_star = include_type == "star" ordering = opts.getString("mat_ordering_type", default="natural") # Accessing .indices causes the allocation of a global array, # so we need to cache these for efficiency V_local_ises_indices = tuple(iset.indices for iset in V.dof_dset.local_ises) # Build index sets for the patches ises = [] if depth != -1: (start, end) = mesh_dm.getDepthStratum(depth) else: (start, end) = mesh_dm.getHeightStratum(height) for seed in range(start, end): # Only build patches over owned DoFs if mesh_dm.getLabelValue("pyop2_ghost", seed) != -1: continue # Create point list from mesh DM star, _ = mesh_dm.getTransitiveClosure(seed, useCone=False) star = order_points(mesh_dm, star, ordering, self.prefix) pt_array = [] for pt in reversed(star): closure, _ = mesh_dm.getTransitiveClosure(pt, useCone=True) pt_array.extend(closure) # Grab unique points with stable ordering pt_array = list(reversed(dict.fromkeys(pt_array))) # Get DoF indices for patch indices = [] for (i, W) in enumerate(V): section = W.dm.getDefaultSection() if i in exclude_subspaces: loop_list = star if include_star else [seed] else: loop_list = pt_array for p in loop_list: dof = section.getDof(p) if dof <= 0: continue off = section.getOffset(p) # Local indices within W W_indices = slice(off*W.block_size, W.block_size * (off + dof)) indices.extend(V_local_ises_indices[i][W_indices]) iset = PETSc.IS().createGeneral(indices, comm=PETSc.COMM_SELF) ises.append(iset) return ises
[docs] class ASMLinesmoothPC(ASMPatchPC): '''Linesmoother PC for extruded meshes implemented as an :class:`ASMPatchPC`. ASMLinesmoothPC is an additive Schwarz preconditioner where each patch consists of all dofs associated with a vertical column (and hence extruded meshes are necessary). Three types of columns are possible: columns of horizontal faces (each column built over a face of the base mesh), columns of vertical faces (each column built over an edge of the base mesh), and columns of vertical edges (each column built over a vertex of the base mesh). To select the column type or types for the patches, use 'pc_linesmooth_codims' to set integers giving the codimension of the base mesh entities for the columns. For example, 'pc_linesmooth_codims 0,1' creates patches for each cell and each facet of the base mesh. ''' _prefix = "pc_linesmooth_"
[docs] def get_patches(self, V): mesh = V._mesh assert mesh.cell_set._extruded dm = mesh.topology_dm section = V.dm.getDefaultSection() # Obtain the codimensions to loop over from options, if present opts = PETSc.Options(self.prefix) codim_list = list(map(int, opts.getString("codims", "0, 1").split(","))) # Build index sets for the patches ises = [] for codim in codim_list: for p in range(*dm.getHeightStratum(codim)): # Only want to build patches over owned faces if dm.getLabelValue("pyop2_ghost", p) != -1: continue dof = section.getDof(p) if dof <= 0: continue off = section.getOffset(p) indices = numpy.arange(off*V.block_size, V.block_size * (off + dof), dtype=IntType) iset = PETSc.IS().createGeneral(indices, comm=PETSc.COMM_SELF) ises.append(iset) return ises
def order_points(mesh_dm, points, ordering_type, prefix): '''Order the points (topological entities) of a patch based on the adjacency graph of the mesh. :arg mesh_dm: the `mesh.topology_dm` :arg points: array with point indices forming the patch :arg ordering_type: a `PETSc.Mat.OrderingType` :arg prefix: the prefix associated with additional ordering options :returns: the permuted array of points ''' # Order points by decreasing topological dimension (interiors, faces, edges, vertices) points = points[::-1] if ordering_type == "natural": return points subgraph = [numpy.intersect1d(points, mesh_dm.getAdjacency(p), return_indices=True)[1] for p in points] ia = numpy.cumsum([0] + [len(neigh) for neigh in subgraph]).astype(PETSc.IntType) ja = numpy.concatenate(subgraph).astype(PETSc.IntType) A = PETSc.Mat().createAIJ((len(points), )*2, csr=(ia, ja, numpy.ones(ja.shape, PETSc.RealType)), comm=PETSc.COMM_SELF) A.setOptionsPrefix(prefix) rperm, cperm = A.getOrdering(ordering_type) indices = points[rperm.getIndices()] A.destroy() rperm.destroy() cperm.destroy() return indices def get_basemesh_nodes(W): pstart, pend = W.mesh().topology_dm.getChart() section = W.dm.getDefaultSection() # location of first dof on an entity basemeshoff = numpy.empty(pend - pstart, dtype=IntType) # number of dofs on this entity basemeshdof = numpy.empty(pend - pstart, dtype=IntType) # number of dofs stacked on this entity in each cell basemeshlayeroffset = numpy.empty(pend - pstart, dtype=IntType) # For every base mesh entity, what's the layer offset? layer_offsets = numpy.full(W.node_set.total_size, -1, dtype=IntType) layer_offsets[W.cell_node_map().values_with_halo] = W.cell_node_map().offset nlayers = W.mesh().layers for p in range(pstart, pend): dof = section.getDof(p) off = section.getOffset(p) if dof == 0: dof_per_layer = 0 layer_offset = 0 else: layer_offset = layer_offsets[off] assert layer_offset >= 0 dof_per_layer = dof - (nlayers - 1) * layer_offset basemeshoff[p - pstart] = off basemeshdof[p - pstart] = dof_per_layer basemeshlayeroffset[p - pstart] = layer_offset return basemeshoff, basemeshdof, basemeshlayeroffset
[docs] class ASMExtrudedStarPC(ASMStarPC): '''Patch-based PC using Star of mesh entities implmented as an :class:`ASMPatchPC`. ASMExtrudedStarPC is an additive Schwarz preconditioner where each patch consists of all DoFs on the topological star of the mesh entity specified by `pc_star_construct_dim`. ''' _prefix = 'pc_star_'
[docs] def get_patches(self, V): mesh = V.mesh() mesh_dm = mesh.topology_dm nlayers = mesh.layers if not mesh.cell_set._extruded: return super(ASMExtrudedStarPC, self).get_patches(V) # Obtain the topological entities to use to construct the stars opts = PETSc.Options(self.prefix) depth = opts.getInt("construct_dim", default=0) ordering = opts.getString("mat_ordering_type", default="natural") # Accessing .indices causes the allocation of a global array, # so we need to cache these for efficiency V_ises = tuple(iset.indices for iset in V.dof_dset.local_ises) basemeshoff = [] basemeshdof = [] basemeshlayeroffsets = [] for (i, W) in enumerate(V): boff, bdof, blayer_offsets = get_basemesh_nodes(W) basemeshoff.append(boff) basemeshdof.append(bdof) basemeshlayeroffsets.append(blayer_offsets) # Build index sets for the patches ises = [] # Build a base_depth-star on the base mesh and extrude it by an # interval_depth-star on the interval mesh such that the depths sum to depth # and 0 <= interval_depth <= 1. # # Vertex-stars: depth = 0 = 0 + 0. # 0 + 0 -> vertex-star = (2D vertex-star) x (1D vertex-star) # # Edge-stars: depth = 1 = 1 + 0 = 0 + 1. # 1 + 0 -> horizontal edge-star = (2D edge-star) x (1D vertex-star) # 0 + 1 -> vertical edge-star = (2D vertex-star) x (1D interior) # # Face-stars: depth = 2 = 2 + 0 = 1 + 1. # 2 + 0 -> horizontal face-star = (2D interior) x (1D vertex-star) # 1 + 1 -> vertical face-star = (2D edge-star) x (1D interior) for base_depth in range(depth+1): interval_depth = depth - base_depth if interval_depth > 1: continue start, end = mesh_dm.getDepthStratum(base_depth) pstart, _ = mesh_dm.getChart() for seed in range(start, end): # Only build patches over owned DoFs if mesh_dm.getLabelValue("pyop2_ghost", seed) != -1: continue # Create point list from mesh DM points, _ = mesh_dm.getTransitiveClosure(seed, useCone=False) points = order_points(mesh_dm, points, ordering, self.prefix) points -= pstart # offset by chart start for k in range(nlayers-interval_depth): if interval_depth == 1: # extrude by 1D interior planes = [1] elif k == 0: # extrude by 1D vertex-star on the bottom planes = [1, 0] elif k == nlayers - 1: # extrude by 1D vertex-star on the top planes = [-1, 0] else: # extrude by 1D vertex-star planes = [-1, 1, 0] indices = [] # Get DoF indices for patch for i, W in enumerate(V): iset = V_ises[i] for plane in planes: for p in points: # How to walk up one layer blayer_offset = basemeshlayeroffsets[i][p] if blayer_offset <= 0: # In this case we don't have any dofs on # this entity. continue # Offset in the global array for the bottom of # the column off = basemeshoff[i][p] # Number of dofs in the interior of the # vertical interval cell on top of this base # entity dof = basemeshdof[i][p] # Hard-code taking the star if plane == 0: begin = off + k * blayer_offset end = off + k * blayer_offset + dof else: begin = off + min(k, k+plane) * blayer_offset + dof end = off + max(k, k+plane) * blayer_offset zlice = slice(W.block_size * begin, W.block_size * end) indices.extend(iset[zlice]) iset = PETSc.IS().createGeneral(indices, comm=PETSc.COMM_SELF) ises.append(iset) return ises