Source code for firedrake.functionspaceimpl

r"""
This module provides the implementations of :class:`~.FunctionSpace`
and :class:`~.MixedFunctionSpace` objects, along with some utility
classes for attaching extra information to instances of these.
"""

from collections import OrderedDict
from dataclasses import dataclass
from typing import Optional

import numpy

import ufl
import finat.ufl

from pyop2 import op2, mpi

from firedrake import dmhooks, utils
from firedrake.functionspacedata import get_shared_data, create_element
from firedrake.petsc import PETSc


[docs] def check_element(element, top=True): """Run some checks on the provided element. The :class:`finat.ufl.mixedelement.VectorElement` and :class:`finat.ufl.mixedelement.TensorElement` modifiers must be "outermost" for function space construction to work, excepting that they should not wrap a :class:`finat.ufl.mixedelement.MixedElement`. Similarly, a base :class:`finat.ufl.mixedelement.MixedElement` must be outermost (it can contain :class:`finat.ufl.mixedelement.MixedElement` instances, provided they satisfy the other rules). This function checks that. Parameters ---------- element : The :class:`UFL element <finat.ufl.finiteelementbase.FiniteElementBase>` to check. top : bool Are we at the top element (in which case the modifier is legal). Returns ------- ``None`` if the element is legal. Raises ------ ValueError If the element is illegal. """ if element.cell.cellname() == "hexahedron" and \ element.family() not in ["Q", "DQ", "Real"]: raise NotImplementedError("Currently can only use 'Q', 'DQ', and/or 'Real' elements on hexahedral meshes, not", element.family()) if type(element) in (finat.ufl.BrokenElement, finat.ufl.RestrictedElement, finat.ufl.HDivElement, finat.ufl.HCurlElement): inner = (element._element, ) elif type(element) is finat.ufl.EnrichedElement: inner = element._elements elif type(element) is finat.ufl.TensorProductElement: inner = element.sub_elements elif isinstance(element, finat.ufl.MixedElement): if not top: raise ValueError(f"{type(element).__name__} modifier must be outermost") else: inner = element.sub_elements else: inner = () for e in inner: check_element(e, top=False)
[docs] class WithGeometryBase(object): r"""Attach geometric information to a :class:`~.FunctionSpace`. Function spaces on meshes with different geometry but the same topology can share data, except for their UFL cell. This class facilitates that. Users should not instantiate a :class:`WithGeometryBase` object explicitly except in a small number of cases. When instantiating a :class:`WithGeometryBase`, users should call :meth:`WithGeometryBase.create` rather than ``__init__``. :arg mesh: The mesh with geometric information to use. :arg element: The UFL element. :arg component: The component of this space in a parent vector element space, or ``None``. :arg cargo: :class:`FunctionSpaceCargo` instance carrying Firedrake-specific data that is not required for code generation. """ def __init__(self, mesh, element, component=None, cargo=None): assert component is None or isinstance(component, int) assert cargo is None or isinstance(cargo, FunctionSpaceCargo) super().__init__(mesh, element, label=cargo.topological._label or "") self.component = component self.cargo = cargo self.comm = mesh.comm self._comm = mpi.internal_comm(mesh.comm, self)
[docs] @classmethod def create(cls, function_space, mesh): """Create a :class:`WithGeometry`. :arg function_space: The topological function space to attach geometry to. :arg mesh: The mesh with geometric information to use. """ function_space = function_space.topological assert mesh.topology is function_space.mesh() assert mesh.topology is not mesh element = function_space.ufl_element().reconstruct(cell=mesh.ufl_cell()) topological = function_space component = function_space.component if function_space.parent is not None: parent = cls.create(function_space.parent, mesh) else: parent = None cargo = FunctionSpaceCargo(topological, parent) return cls(mesh, element, component=component, cargo=cargo)
def _ufl_signature_data_(self, *args, **kwargs): return (type(self), self.component, super()._ufl_signature_data_(*args, **kwargs)) @property def parent(self): return self.cargo.parent @parent.setter def parent(self, val): self.cargo.parent = val @property def topological(self): return self.cargo.topological @topological.setter def topological(self, val): self.cargo.topological = val @utils.cached_property def subfunctions(self): r"""Split into a tuple of constituent spaces.""" return tuple(type(self).create(subspace, self.mesh()) for subspace in self.topological.subfunctions) mesh = ufl.FunctionSpace.ufl_domain @property def _ad_parent_space(self): return self.parent
[docs] def ufl_function_space(self): r"""The :class:`~ufl.classes.FunctionSpace` this object represents.""" return self
[docs] def ufl_cell(self): r"""The :class:`~ufl.classes.Cell` this FunctionSpace is defined on.""" return self.mesh().ufl_cell()
[docs] @PETSc.Log.EventDecorator() def split(self): import warnings warnings.warn("The .split() method is deprecated, please use the .subfunctions property instead", category=FutureWarning) return self.subfunctions
@utils.cached_property def _components(self): if len(self) == 1: return tuple(type(self).create(self.topological.sub(i), self.mesh()) for i in range(self.value_size)) else: return self.subfunctions
[docs] @PETSc.Log.EventDecorator() def sub(self, i): if len(self) == 1: bound = self.value_size else: bound = len(self) if i < 0 or i >= bound: raise IndexError("Invalid component %d, not in [0, %d)" % (i, bound)) return self._components[i]
@utils.cached_property def dm(self): dm = self._dm() dmhooks.set_function_space(dm, self) return dm @property def num_work_functions(self): r"""The number of checked out work functions.""" from firedrake.functionspacedata import get_work_function_cache cache = get_work_function_cache(self.mesh(), self.ufl_element()) return sum(cache.values()) @property def max_work_functions(self): r"""The maximum number of work functions this :class:`FunctionSpace` supports. See :meth:`get_work_function` for obtaining work functions.""" from firedrake.functionspacedata import get_max_work_functions return get_max_work_functions(self) @max_work_functions.setter def max_work_functions(self, val): r"""Set the number of work functions this :class:`FunctionSpace` supports. :arg val: The new maximum number of work functions. :raises ValueError: if the provided value is smaller than the number of currently checked out work functions. """ # Clear cache from firedrake.functionspacedata import get_work_function_cache, set_max_work_functions cache = get_work_function_cache(self.mesh(), self.ufl_element()) if val < len(cache): for k in list(cache.keys()): if not cache[k]: del cache[k] if val < len(cache): raise ValueError("Can't set work function cache smaller (%d) than current checked out functions (%d)" % (val, len(cache))) set_max_work_functions(self, val)
[docs] def get_work_function(self, zero=True): r"""Get a temporary work :class:`~.Function` on this :class:`FunctionSpace`. :arg zero: Should the :class:`~.Function` be guaranteed zero? If ``zero`` is ``False`` the returned function may or may not be zeroed, and the user is responsible for appropriate zeroing. :raises ValueError: if :attr:`max_work_functions` are already checked out. .. note :: This method is intended to be used for short-lived work functions, if you actually need a function for general usage use the :class:`~.Function` constructor. When you are finished with the work function, you should restore it to the pool of available functions with :meth:`restore_work_function`. """ from firedrake.functionspacedata import get_work_function_cache cache = get_work_function_cache(self.mesh(), self.ufl_element()) for function in cache.keys(): # Check if we've got a free work function available out = cache[function] if not out: cache[function] = True if zero: function.dat.zero() return function if len(cache) == self.max_work_functions: raise ValueError("Can't check out more than %d work functions." % self.max_work_functions) from firedrake import Function function = Function(self) cache[function] = True return function
[docs] def restore_work_function(self, function): r"""Restore a work function obtained with :meth:`get_work_function`. :arg function: The work function to restore :raises ValueError: if the provided function was not obtained with :meth:`get_work_function` or it has already been restored. .. warning:: This does *not* invalidate the name in the calling scope, it is the user's responsibility not to use a work function after restoring it. """ from firedrake.functionspacedata import get_work_function_cache cache = get_work_function_cache(self.mesh(), self.ufl_element()) try: out = cache[function] except KeyError: raise ValueError("Function %s is not a work function" % function) if not out: raise ValueError("Function %s is not checked out, cannot restore" % function) cache[function] = False
def __eq__(self, other): try: return self.topological == other.topological and \ self.mesh() is other.mesh() except AttributeError: return False def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash((self.mesh(), self.topological)) def __len__(self): return len(self.topological) def __repr__(self): return "%s(%r, %r)" % (self.__class__.__name__, self.topological, self.mesh()) def __str__(self): return "%s(%s, %s)" % (self.__class__.__name__, self.topological, self.mesh()) def __iter__(self): return iter(self.subfunctions) def __getitem__(self, i): return self.subfunctions[i] def __mul__(self, other): r"""Create a :class:`.MixedFunctionSpace` composed of this :class:`.FunctionSpace` and other""" from firedrake.functionspace import MixedFunctionSpace return MixedFunctionSpace((self, other)) def __getattr__(self, name): val = getattr(self.topological, name) setattr(self, name, val) return val def __dir__(self): current = super().__dir__() return list(OrderedDict.fromkeys(dir(self.topological) + current))
[docs] def boundary_nodes(self, sub_domain): r"""Return the boundary nodes for this :class:`~.WithGeometryBase`. :arg sub_domain: the mesh marker selecting which subset of facets to consider. :returns: A numpy array of the unique function space nodes on the selected portion of the boundary. See also :class:`~.DirichletBC` for details of the arguments. """ # Have to replicate the definition from FunctionSpace because # we want to access the DM on the WithGeometry object. return self._shared_data.boundary_nodes(self, sub_domain)
[docs] def collapse(self): return type(self).create(self.topological.collapse(), self.mesh())
[docs] @classmethod def make_function_space(cls, mesh, element, name=None): r"""Factory method for :class:`.WithGeometryBase`.""" mesh.init() topology = mesh.topology # Create a new abstract (Mixed/Real)FunctionSpace, these are neither primal nor dual. if type(element) is finat.ufl.MixedElement: spaces = [cls.make_function_space(topology, e) for e in element.sub_elements] new = MixedFunctionSpace(spaces, name=name) else: # Check that any Vector/Tensor/Mixed modifiers are outermost. check_element(element) if element.family() == "Real": new = RealFunctionSpace(topology, element, name=name) else: new = FunctionSpace(topology, element, name=name) # Skip this if we are just building subspaces of an abstract MixedFunctionSpace if mesh is not topology: # Create a concrete WithGeometry or FiredrakeDualSpace on this mesh new = cls.create(new, mesh) return new
[docs] def reconstruct(self, mesh=None, name=None, **kwargs): r"""Reconstruct this :class:`.WithGeometryBase` . :kwarg mesh: the new :func:`~.Mesh` (defaults to same mesh) :kwarg name: the new name (defaults to None) :returns: the new function space of the same class as ``self``. Any extra kwargs are used to reconstruct the finite element. For details see :meth:`finat.ufl.finiteelement.FiniteElement.reconstruct`. """ V_parent = self # Deal with ProxyFunctionSpace indices = [] while True: if V_parent.index is not None: indices.append(V_parent.index) if V_parent.component is not None: indices.append(V_parent.component) if V_parent.parent is not None: V_parent = V_parent.parent else: break if mesh is None: mesh = V_parent.mesh() element = V_parent.ufl_element() cell = mesh.topology.ufl_cell() if len(kwargs) > 0 or element.cell != cell: element = element.reconstruct(cell=cell, **kwargs) V = type(self).make_function_space(mesh, element, name=name) for i in reversed(indices): V = V.sub(i) return V
[docs] class WithGeometry(WithGeometryBase, ufl.FunctionSpace): def __init__(self, mesh, element, component=None, cargo=None): super(WithGeometry, self).__init__(mesh, element, component=component, cargo=cargo)
[docs] def dual(self): return FiredrakeDualSpace.create(self.topological, self.mesh())
[docs] class FiredrakeDualSpace(WithGeometryBase, ufl.functionspace.DualSpace): def __init__(self, mesh, element, component=None, cargo=None): super(FiredrakeDualSpace, self).__init__(mesh, element, component=component, cargo=cargo)
[docs] def dual(self): return WithGeometry.create(self.topological, self.mesh())
[docs] class FunctionSpace(object): r"""A representation of a function space. A :class:`FunctionSpace` associates degrees of freedom with topological mesh entities. The degree of freedom mapping is determined from the provided element. :arg mesh: The :func:`~.Mesh` to build the function space on. :arg element: The :class:`finat.ufl.finiteelementbase.FiniteElementBase` describing the degrees of freedom. :kwarg name: An optional name for this :class:`FunctionSpace`, useful for later identification. The element can be a essentially any :class:`finat.ufl.finiteelementbase.FiniteElementBase`, except for a :class:`finat.ufl.mixedelement.MixedElement`, for which one should use the :class:`MixedFunctionSpace` constructor. To determine whether the space is scalar-, vector- or tensor-valued, one should inspect the :attr:`rank` of the resulting object. Note that function spaces created on *intrinsically* vector-valued finite elements (such as the Raviart-Thomas space) have ``rank`` 0. .. warning:: Users should not build a :class:`FunctionSpace` directly, instead they should use the utility :func:`~.FunctionSpace` function, which provides extra error checking and argument sanitising. """ boundary_set = frozenset() @PETSc.Log.EventDecorator() def __init__(self, mesh, element, name=None): super(FunctionSpace, self).__init__() if type(element) is finat.ufl.MixedElement: raise ValueError("Can't create FunctionSpace for MixedElement") # The function space shape is the number of dofs per node, # hence it is not always the value_shape. Vector and Tensor # element modifiers *must* live on the outside! if type(element) in {finat.ufl.TensorElement, finat.ufl.VectorElement} \ or (isinstance(element, finat.ufl.WithMapping) and type(element.wrapee) in {finat.ufl.TensorElement, finat.ufl.VectorElement}): # The number of "free" dofs is given by reference_value_shape, # not value_shape due to symmetry specifications rvs = element.reference_value_shape # This requires that the sub element is not itself a # tensor element (which is checked by the top level # constructor of function spaces) shape_element = element if isinstance(element, finat.ufl.WithMapping): shape_element = element.wrapee sub = shape_element.sub_elements[0].value_shape self.shape = rvs[:len(rvs) - len(sub)] else: self.shape = () self._label = "" self._ufl_function_space = ufl.FunctionSpace(mesh.ufl_mesh(), element, label=self._label) self._mesh = mesh self.rank = len(self.shape) r"""The rank of this :class:`FunctionSpace`. Spaces where the element is scalar-valued (or intrinsically vector-valued) have rank zero. Spaces built on :class:`finat.ufl.mixedelement.VectorElement` or :class:`finat.ufl.mixedelement.TensorElement` instances have rank equivalent to the number of components of their :attr:`finat.ufl.finiteelementbase.FiniteElementBase.value_shape`.""" self.value_size = int(numpy.prod(self.shape, dtype=int)) r"""The total number of degrees of freedom at each function space node.""" self.name = name r"""The (optional) descriptive name for this space.""" # User comm self.comm = mesh.comm # Internal comm self._comm = mpi.internal_comm(self.comm, self) self.set_shared_data() self.dof_dset = self.make_dof_dset() r"""A :class:`pyop2.types.dataset.DataSet` representing the function space degrees of freedom.""" self.node_set = self.dof_dset.set r"""A :class:`pyop2.types.set.Set` representing the function space nodes."""
[docs] def set_shared_data(self): element = self.ufl_element() sdata = get_shared_data(self._mesh, element) # Need to create finat element again as sdata does not # want to carry finat_element. self.finat_element = create_element(element) # Used for reconstruction of mixed/component spaces. # sdata carries real_tensorproduct. self._shared_data = sdata self.real_tensorproduct = sdata.real_tensorproduct self.extruded = sdata.extruded self.offset = sdata.offset self.offset_quotient = sdata.offset_quotient self.cell_boundary_masks = sdata.cell_boundary_masks self.interior_facet_boundary_masks = sdata.interior_facet_boundary_masks self.global_numbering = sdata.global_numbering
[docs] def make_dof_dset(self): return op2.DataSet(self._shared_data.node_set, self.shape or 1, name=f"{self.name}_nodes_dset")
# These properties are overridden in ProxyFunctionSpaces, but are # provided by FunctionSpace so that we don't have to special case. index = None r"""The position of this space in its parent :class:`MixedFunctionSpace`, or ``None``.""" parent = None r"""The parent space if this space was extracted from one, or ``None``.""" component = None r"""The component of this space in its parent VectorElement space, or ``None``.""" def __eq__(self, other): if not isinstance(other, FunctionSpace): return False # FIXME: Think harder about equality return self.mesh() is other.mesh() and \ self.dof_dset is other.dof_dset and \ self.ufl_element() == other.ufl_element() and \ self.component == other.component def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash((self.mesh(), self.dof_dset, self.ufl_element())) @utils.cached_property def _ad_parent_space(self): return self.parent @utils.cached_property def dm(self): r"""A PETSc DM describing the data layout for this FunctionSpace.""" dm = self._dm() dmhooks.set_function_space(dm, self) return dm def _dm(self): from firedrake.mg.utils import get_level dm = self.dof_dset.dm _, level = get_level(self.mesh()) dmhooks.attach_hooks(dm, level=level, sf=self.mesh().topology_dm.getPointSF(), section=self.global_numbering) # Remember the function space so we can get from DM back to FunctionSpace. dmhooks.set_function_space(dm, self) return dm @utils.cached_property def _ises(self): return self.dof_dset.field_ises @utils.cached_property def cell_node_list(self): r"""A numpy array mapping mesh cells to function space nodes.""" return self._shared_data.entity_node_lists[self.mesh().cell_set] @utils.cached_property def topological(self): r"""Function space on a mesh topology.""" return self
[docs] def mesh(self): return self._mesh
[docs] def ufl_element(self): r"""The :class:`finat.ufl.finiteelementbase.FiniteElementBase` associated with this space.""" return self.ufl_function_space().ufl_element()
[docs] def ufl_function_space(self): r"""The :class:`~ufl.classes.FunctionSpace` associated with this space.""" return self._ufl_function_space
[docs] def label(self): return self._label
def __len__(self): return 1 def __iter__(self): yield self def __repr__(self): return "FunctionSpace(%r, %r, name=%r)" % (self.mesh(), self.ufl_element(), self.name) def __str__(self): return self.__repr__() @utils.cached_property def subfunctions(self): r"""Split into a tuple of constituent spaces.""" return (self, )
[docs] def split(self): import warnings warnings.warn("The .split() method is deprecated, please use the .subfunctions property instead", category=FutureWarning) return self.subfunctions
def __getitem__(self, i): r"""Return the ith subspace.""" if i != 0: raise IndexError("Only index 0 supported on a FunctionSpace") return self @utils.cached_property def _components(self): return tuple(ComponentFunctionSpace(self, i) for i in range(self.value_size))
[docs] def sub(self, i): r"""Return a view into the ith component.""" if self.rank == 0: assert i == 0 return self return self._components[i]
def __mul__(self, other): r"""Create a :class:`.MixedFunctionSpace` composed of this :class:`.FunctionSpace` and other""" from firedrake.functionspace import MixedFunctionSpace return MixedFunctionSpace((self, other)) @utils.cached_property def node_count(self): r"""The number of nodes (includes halo nodes) of this function space on this process. If the :class:`FunctionSpace` has :attr:`FunctionSpace.rank` 0, this is equal to the :attr:`FunctionSpace.dof_count`, otherwise the :attr:`FunctionSpace.dof_count` is :attr:`dim` times the :attr:`node_count`.""" constrained_node_set = set() for sub_domain in self.boundary_set: constrained_node_set.update(self._shared_data.boundary_nodes(self, sub_domain)) return self.node_set.total_size - len(constrained_node_set) @utils.cached_property def dof_count(self): r"""The number of degrees of freedom (includes halo dofs) of this function space on this process. Cf. :attr:`FunctionSpace.node_count` .""" return self.node_count*self.value_size
[docs] def dim(self): r"""The global number of degrees of freedom for this function space. See also :attr:`FunctionSpace.dof_count` and :attr:`FunctionSpace.node_count` .""" return self.dof_dset.layout_vec.getSize()
[docs] def make_dat(self, val=None, valuetype=None, name=None): r"""Return a newly allocated :class:`pyop2.types.dat.Dat` defined on the :attr:`dof_dset` of this :class:`.Function`.""" return op2.Dat(self.dof_dset, val, valuetype, name)
[docs] def entity_node_map(self, source_mesh, source_integral_type, source_subdomain_id, source_all_integer_subdomain_ids): r"""Return entity node map rebased on ``source_mesh``. Parameters ---------- source_mesh : MeshTopology Source (base) mesh topology. source_integral_type : str Integral type on source_mesh. source_subdomain_id : int Subdomain ID on source_mesh. source_all_integer_subdomain_ids : dict All integer subdomain ids on source_mesh. Returns ------- pyop2.types.map.Map or None Entity node map. """ if source_mesh is self.mesh(): target_integral_type = source_integral_type else: composed_map, target_integral_type = self.mesh().trans_mesh_entity_map(source_mesh, source_integral_type, source_subdomain_id, source_all_integer_subdomain_ids) if target_integral_type == "cell": self_map = self.cell_node_map() elif target_integral_type == "exterior_facet_top": self_map = self.cell_node_map() elif target_integral_type == "exterior_facet_bottom": self_map = self.cell_node_map() elif target_integral_type == "interior_facet_horiz": self_map = self.cell_node_map() elif target_integral_type == "exterior_facet": self_map = self.exterior_facet_node_map() elif target_integral_type == "exterior_facet_vert": self_map = self.exterior_facet_node_map() elif target_integral_type == "interior_facet": self_map = self.interior_facet_node_map() elif target_integral_type == "interior_facet_vert": self_map = self.interior_facet_node_map() else: raise ValueError(f"Unknown integral_type: {target_integral_type}") if source_mesh is self.mesh(): return self_map else: return op2.ComposedMap(self_map, composed_map)
[docs] def cell_node_map(self): r"""Return the :class:`pyop2.types.map.Map` from cels to function space nodes.""" sdata = self._shared_data return sdata.get_map(self, self.mesh().cell_set, self.finat_element.space_dimension(), "cell_node", self.offset, self.offset_quotient)
[docs] def interior_facet_node_map(self): r"""Return the :class:`pyop2.types.map.Map` from interior facets to function space nodes.""" sdata = self._shared_data offset = self.cell_node_map().offset if offset is not None: offset = numpy.append(offset, offset) offset_quotient = self.cell_node_map().offset_quotient if offset_quotient is not None: offset_quotient = numpy.append(offset_quotient, offset_quotient) return sdata.get_map(self, self.mesh().interior_facets.set, 2*self.finat_element.space_dimension(), "interior_facet_node", offset, offset_quotient)
[docs] def exterior_facet_node_map(self): r"""Return the :class:`pyop2.types.map.Map` from exterior facets to function space nodes.""" sdata = self._shared_data return sdata.get_map(self, self.mesh().exterior_facets.set, self.finat_element.space_dimension(), "exterior_facet_node", self.offset, self.offset_quotient)
[docs] def boundary_nodes(self, sub_domain): r"""Return the boundary nodes for this :class:`~.FunctionSpace`. :arg sub_domain: the mesh marker selecting which subset of facets to consider. :returns: A numpy array of the unique function space nodes on the selected portion of the boundary. See also :class:`~.DirichletBC` for details of the arguments. """ return self._shared_data.boundary_nodes(self, sub_domain)
[docs] @PETSc.Log.EventDecorator() def local_to_global_map(self, bcs, lgmap=None): r"""Return a map from process local dof numbering to global dof numbering. If BCs is provided, mask out those dofs which match the BC nodes.""" # Caching these things is too complicated, since it depends # not just on the bcs, but also the parent space, and anything # this space has been recursively split out from [e.g. inside # fieldsplit] if bcs is None or len(bcs) == 0: return lgmap or self.dof_dset.lgmap for bc in bcs: fs = bc.function_space() while fs.component is not None and fs.parent is not None: fs = fs.parent if fs.topological != self.topological: raise RuntimeError("DirichletBC defined on a different FunctionSpace!") unblocked = any(bc.function_space().component is not None for bc in bcs) if lgmap is None: lgmap = self.dof_dset.lgmap if unblocked: indices = lgmap.indices.copy() bsize = 1 else: indices = lgmap.block_indices.copy() bsize = lgmap.getBlockSize() assert bsize == self.value_size else: # MatBlock case, LGMap is already unrolled. indices = lgmap.block_indices.copy() bsize = lgmap.getBlockSize() unblocked = True nodes = [] for bc in bcs: if bc.function_space().component is not None: nodes.append(bc.nodes * self.value_size + bc.function_space().component) elif unblocked: tmp = bc.nodes * self.value_size for i in range(self.value_size): nodes.append(tmp + i) else: nodes.append(bc.nodes) nodes = numpy.unique(numpy.concatenate(nodes)) indices[nodes] = -1 return PETSc.LGMap().create(indices, bsize=bsize, comm=lgmap.comm)
[docs] def collapse(self): return type(self)(self.mesh(), self.ufl_element())
[docs] class RestrictedFunctionSpace(FunctionSpace): r"""A representation of a function space, with additional information about where boundary conditions are to be applied. If a :class:`FunctionSpace` is represented as V, we can decompose V into V = V0 + VΓ, where V0 contains functions in the basis of V that vanish on the boundary where a boundary condition is applied, and VΓ contains all other basis functions. The :class:`RestrictedFunctionSpace` corresponding to V takes functions only from V0 when solving problems, or when creating a TestFunction and TrialFunction. The values on the boundary set will remain constant when solving, but are present in the output of the solver. :arg function_space: The :class:`FunctionSpace` to restrict. :kwarg name: An optional name for this :class:`RestrictedFunctionSpace`, useful for later identification. :kwarg boundary_set: A set of subdomains on which a DirichletBC will be applied. Notes ----- If using this class to solve or similar, a list of DirichletBCs will still need to be specified on this space and passed into the function. """ def __init__(self, function_space, name=None, boundary_set=frozenset()): label = "" for boundary_domain in boundary_set: label += str(boundary_domain) label += "_" self.boundary_set = frozenset(boundary_set) super().__init__(function_space._mesh.topology, function_space.ufl_element(), function_space.name) self._label = label self._ufl_function_space = ufl.FunctionSpace(function_space._mesh.ufl_mesh(), function_space.ufl_element(), label=self._label) self.function_space = function_space self.name = name or (function_space.name or "Restricted" + "_" + "_".join(sorted( [str(i) for i in self.boundary_set])))
[docs] def set_shared_data(self): sdata = get_shared_data(self._mesh, self.ufl_element(), self.boundary_set) self._shared_data = sdata self.node_set = sdata.node_set r"""A :class:`pyop2.types.set.Set` representing the function space nodes.""" self.dof_dset = op2.DataSet(self.node_set, self.shape or 1, name="%s_nodes_dset" % self.name) r"""A :class:`pyop2.types.dataset.DataSet` representing the function space degrees of freedom.""" # check not all degrees of freedom are constrained unconstrained_dofs = self.dof_dset.size - self.dof_dset.constrained_size if self.comm.allreduce(unconstrained_dofs) == 0: raise ValueError("All degrees of freedom are constrained.") self.finat_element = create_element(self.ufl_element()) # Used for reconstruction of mixed/component spaces. # sdata carries real_tensorproduct. self.real_tensorproduct = sdata.real_tensorproduct self.extruded = sdata.extruded self.offset = sdata.offset self.offset_quotient = sdata.offset_quotient self.cell_boundary_masks = sdata.cell_boundary_masks self.interior_facet_boundary_masks = sdata.interior_facet_boundary_masks self.global_numbering = sdata.global_numbering
def __eq__(self, other): if not isinstance(other, RestrictedFunctionSpace): return False return self.function_space == other.function_space and \ self.boundary_set == other.boundary_set def __repr__(self): return self.__class__.__name__ + "(%r, name=%r, boundary_set=%r)" % ( str(self.function_space), self.name, self.boundary_set) def __hash__(self): return hash((self.mesh(), self.dof_dset, self.ufl_element(), self.boundary_set))
[docs] def local_to_global_map(self, bcs, lgmap=None): return lgmap or self.dof_dset.lgmap
[docs] class MixedFunctionSpace(object): r"""A function space on a mixed finite element. This is essentially just a bag of individual :class:`FunctionSpace` objects. :arg spaces: The constituent spaces. :kwarg name: An optional name for the mixed space. .. warning:: Users should not build a :class:`MixedFunctionSpace` directly, but should instead use the functional interface provided by :func:`.MixedFunctionSpace`. """ def __init__(self, spaces, name=None): super(MixedFunctionSpace, self).__init__() self._spaces = tuple(IndexedFunctionSpace(i, s, self) for i, s in enumerate(spaces)) mesh, = set(s.mesh() for s in spaces) self._ufl_function_space = ufl.FunctionSpace(mesh.ufl_mesh(), finat.ufl.MixedElement(*[s.ufl_element() for s in spaces])) self.name = name or "_".join(str(s.name) for s in spaces) label = "" for s in spaces: label += "(" + s._label + ")_" self._label = label self.boundary_set = frozenset() self._subspaces = {} self._mesh = mesh self.comm = mesh.comm self._comm = mpi.internal_comm(self.node_set.comm, self) # These properties are so a mixed space can behave like a normal FunctionSpace. index = None component = None parent = None rank = 1
[docs] def mesh(self): return self._mesh
@property def topological(self): r"""Function space on a mesh topology.""" return self
[docs] def ufl_element(self): r"""The :class:`finat.ufl.mixedelement.MixedElement` associated with this space.""" return self.ufl_function_space().ufl_element()
[docs] def ufl_function_space(self): r"""The :class:`~ufl.classes.FunctionSpace` associated with this space.""" return self._ufl_function_space
def __eq__(self, other): if not isinstance(other, MixedFunctionSpace) or len(other) != len(self): return False return all(s == o for s, o in zip(self, other)) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(tuple(self)) @utils.cached_property def subfunctions(self): r"""The list of :class:`FunctionSpace`\s of which this :class:`MixedFunctionSpace` is composed.""" return self._spaces
[docs] def split(self): import warnings warnings.warn("The .split() method is deprecated, please use the .subfunctions property instead", category=FutureWarning) return self.subfunctions
[docs] def sub(self, i): r"""Return the `i`th :class:`FunctionSpace` in this :class:`MixedFunctionSpace`.""" return self._spaces[i]
[docs] def num_sub_spaces(self): r"""Return the number of :class:`FunctionSpace`\s of which this :class:`MixedFunctionSpace` is composed.""" return len(self)
def __len__(self): r"""Return the number of :class:`FunctionSpace`\s of which this :class:`MixedFunctionSpace` is composed.""" return len(self._spaces) def __getitem__(self, i): r"""Return the `i`th :class:`FunctionSpace` in this :class:`MixedFunctionSpace`.""" return self._spaces[i] def __iter__(self): return iter(self._spaces) def __repr__(self): return "MixedFunctionSpace(%s, name=%r)" % \ (", ".join(repr(s) for s in self), self.name) def __str__(self): return "MixedFunctionSpace(%s)" % ", ".join(str(s) for s in self) @utils.cached_property def value_size(self): r"""Return the sum of the :attr:`FunctionSpace.value_size`\s of the :class:`FunctionSpace`\s this :class:`MixedFunctionSpace` is composed of.""" return sum(fs.value_size for fs in self._spaces) @utils.cached_property def node_count(self): r"""Return a tuple of :attr:`FunctionSpace.node_count`\s of the :class:`FunctionSpace`\s of which this :class:`MixedFunctionSpace` is composed.""" return tuple(fs.node_count for fs in self._spaces) @utils.cached_property def dof_count(self): r"""Return a tuple of :attr:`FunctionSpace.dof_count`\s of the :class:`FunctionSpace`\s of which this :class:`MixedFunctionSpace` is composed.""" return tuple(fs.dof_count for fs in self._spaces)
[docs] def dim(self): r"""The global number of degrees of freedom for this function space. See also :attr:`FunctionSpace.dof_count` and :attr:`FunctionSpace.node_count`.""" return self.dof_dset.layout_vec.getSize()
@utils.cached_property def node_set(self): r"""A :class:`pyop2.types.set.MixedSet` containing the nodes of this :class:`MixedFunctionSpace`. This is composed of the :attr:`FunctionSpace.node_set`\s of the underlying :class:`FunctionSpace`\s this :class:`MixedFunctionSpace` is composed of one or (for VectorFunctionSpaces) more degrees of freedom are stored at each node.""" return op2.MixedSet(s.node_set for s in self._spaces) @utils.cached_property def dof_dset(self): r"""A :class:`pyop2.types.dataset.MixedDataSet` containing the degrees of freedom of this :class:`MixedFunctionSpace`. This is composed of the :attr:`FunctionSpace.dof_dset`\s of the underlying :class:`FunctionSpace`\s of which this :class:`MixedFunctionSpace` is composed.""" return op2.MixedDataSet(s.dof_dset for s in self._spaces)
[docs] def entity_node_map(self, source_mesh, source_integral_type, source_subdomain_id, source_all_integer_subdomain_ids): r"""Return entity node map rebased on ``source_mesh``. Parameters ---------- source_mesh : MeshTopology Source (base) mesh topology. source_integral_type : str Integral type on source_mesh. source_subdomain_id : int Subdomain ID on source_mesh. source_all_integer_subdomain_ids : dict All integer subdomain ids on source_mesh. Returns ------- pyop2.types.map.MixedMap Entity node map. """ return op2.MixedMap(s.entity_node_map(source_mesh, source_integral_type, source_subdomain_id, source_all_integer_subdomain_ids) for s in self._spaces)
[docs] def cell_node_map(self): r"""A :class:`pyop2.types.map.MixedMap` from the ``Mesh.cell_set`` of the underlying mesh to the :attr:`node_set` of this :class:`MixedFunctionSpace`. This is composed of the :attr:`FunctionSpace.cell_node_map`\s of the underlying :class:`FunctionSpace`\s of which this :class:`MixedFunctionSpace` is composed.""" return op2.MixedMap(s.cell_node_map() for s in self._spaces)
[docs] def interior_facet_node_map(self): r"""Return the :class:`pyop2.types.map.MixedMap` from interior facets to function space nodes.""" return op2.MixedMap(s.interior_facet_node_map() for s in self)
[docs] def exterior_facet_node_map(self): r"""Return the :class:`pyop2.types.map.Map` from exterior facets to function space nodes.""" return op2.MixedMap(s.exterior_facet_node_map() for s in self)
[docs] def local_to_global_map(self, bcs): r"""Return a map from process local dof numbering to global dof numbering. If BCs is provided, mask out those dofs which match the BC nodes.""" raise NotImplementedError("Not for mixed maps right now sorry!")
[docs] def make_dat(self, val=None, valuetype=None, name=None): r"""Return a newly allocated :class:`pyop2.types.dat.MixedDat` defined on the :attr:`dof_dset` of this :class:`MixedFunctionSpace`.""" if val is not None: assert len(val) == len(self) else: val = [None for _ in self] return op2.MixedDat(s.make_dat(v, valuetype, "%s[cmpt-%d]" % (name, i)) for i, (s, v) in enumerate(zip(self._spaces, val)))
@utils.cached_property def dm(self): r"""A PETSc DM describing the data layout for fieldsplit solvers.""" dm = self._dm() dmhooks.set_function_space(dm, self) return dm def _dm(self): from firedrake.mg.utils import get_level dm = self.dof_dset.dm _, level = get_level(self.mesh()) dmhooks.attach_hooks(dm, level=level) return dm @utils.cached_property def _ises(self): return self.dof_dset.field_ises
[docs] def collapse(self): return type(self)([V_ for V_ in self], self.mesh())
[docs] class ProxyFunctionSpace(FunctionSpace): r"""A :class:`FunctionSpace` that one can attach extra properties to. :arg mesh: The mesh to use. :arg element: The UFL element. :arg name: The name of the function space. .. warning:: Users should not build a :class:`ProxyFunctionSpace` directly, it is mostly used as an internal implementation detail. """ def __new__(cls, mesh, element, name=None): topology = mesh.topology self = super(ProxyFunctionSpace, cls).__new__(cls) if mesh is not topology: return WithGeometry.create(self, mesh) else: return self def __repr__(self): return "%sProxyFunctionSpace(%r, %r, name=%r, index=%r, component=%r)" % \ (str(self.identifier).capitalize(), self.mesh(), self.ufl_element(), self.name, self.index, self.component) def __str__(self): return "%sProxyFunctionSpace(%s, %s, name=%s, index=%s, component=%s)" % \ (str(self.identifier).capitalize(), self.mesh(), self.ufl_element(), self.name, self.index, self.component) identifier = None r"""An optional identifier, for debugging purposes.""" no_dats = False r"""Can this proxy make :class:`pyop2.types.dat.Dat` objects"""
[docs] def make_dat(self, *args, **kwargs): r"""Create a :class:`pyop2.types.dat.Dat`. :raises ValueError: if :attr:`no_dats` is ``True``. """ if self.no_dats: raise ValueError("Can't build Function on %s function space" % self.identifier) return super(ProxyFunctionSpace, self).make_dat(*args, **kwargs)
[docs] class ProxyRestrictedFunctionSpace(RestrictedFunctionSpace): r"""A :class:`RestrictedFunctionSpace` that one can attach extra properties to. :arg function_space: The function space to be restricted. :kwarg name: The name of the restricted function space. :kwarg boundary_set: The boundary domains on which boundary conditions will be specified .. warning:: Users should not build a :class:`ProxyRestrictedFunctionSpace` directly, it is mostly used as an internal implementation detail. """ def __new__(cls, function_space, name=None, boundary_set=frozenset()): topology = function_space._mesh.topology self = super(ProxyRestrictedFunctionSpace, cls).__new__(cls) if function_space._mesh is not topology: return WithGeometry.create(self, function_space._mesh) else: return self def __repr__(self): return "%sProxyRestrictedFunctionSpace(%r, name=%r, boundary_set=%r, index=%r, component=%r)" % \ (str(self.identifier).capitalize(), str(self.function_space), self.name, self.boundary_set, self.index, self.component) def __str__(self): return self.__repr__() identifier = None r"""An optional identifier, for debugging purposes.""" no_dats = False r"""Can this proxy make :class:`pyop2.types.dat.Dat` objects"""
[docs] def make_dat(self, *args, **kwargs): r"""Create a :class:`pyop2.types.dat.Dat`. :raises ValueError: if :attr:`no_dats` is ``True``. """ if self.no_dats: raise ValueError("Can't build Function on %s function space" % self.identifier) return super(ProxyRestrictedFunctionSpace, self).make_dat(*args, **kwargs)
[docs] def IndexedFunctionSpace(index, space, parent): r"""Build a new FunctionSpace that remembers it is a particular subspace of a :class:`MixedFunctionSpace`. :arg index: The index into the parent space. :arg space: The subspace to represent :arg parent: The parent mixed space. :returns: A new :class:`ProxyFunctionSpace` with index and parent set. """ if space.ufl_element().family() == "Real": new = RealFunctionSpace(space.mesh(), space.ufl_element(), name=space.name) elif len(space.boundary_set) > 0: new = ProxyRestrictedFunctionSpace(space.function_space, name=space.name, boundary_set=space.boundary_set) else: new = ProxyFunctionSpace(space.mesh(), space.ufl_element(), name=space.name) new.index = index new.parent = parent new.identifier = "indexed" return new
[docs] def ComponentFunctionSpace(parent, component): r"""Build a new FunctionSpace that remembers it represents a particular component. Used for applying boundary conditions to components of a :func:`.VectorFunctionSpace` or :func:`.TensorFunctionSpace`. :arg parent: The parent space (a FunctionSpace with a VectorElement or TensorElement). :arg component: The component to represent. :returns: A new :class:`ProxyFunctionSpace` with the component set. """ element = parent.ufl_element() assert type(element) in frozenset([finat.ufl.VectorElement, finat.ufl.TensorElement]) if not (0 <= component < parent.value_size): raise IndexError("Invalid component %d. not in [0, %d)" % (component, parent.value_size)) new = ProxyFunctionSpace(parent.mesh(), element.sub_elements[0], name=parent.name) new.identifier = "component" new.component = component new.parent = parent return new
[docs] class RealFunctionSpace(FunctionSpace): r""":class:`FunctionSpace` based on elements of family "Real". A :class`RealFunctionSpace` only has a single global value for the whole mesh. This class should not be directly instantiated by users. Instead, FunctionSpace objects will transform themselves into :class:`RealFunctionSpace` objects as appropriate. """ finat_element = None global_numbering = None def __eq__(self, other): if not isinstance(other, RealFunctionSpace): return False # FIXME: Think harder about equality return self.mesh() is other.mesh() and \ self.ufl_element() == other.ufl_element() def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash((self.mesh(), self.ufl_element()))
[docs] def set_shared_data(self): pass
[docs] def make_dof_dset(self): return op2.GlobalDataSet(self.make_dat())
[docs] def make_dat(self, val=None, valuetype=None, name=None): r"""Return a newly allocated :class:`pyop2.types.glob.Global` representing the data for a :class:`.Function` on this space.""" return op2.Global(self.value_size, val, valuetype, name, self._comm)
[docs] def entity_node_map(self, source_mesh, source_integral_type, source_subdomain_id, source_all_integer_subdomain_ids): return None
[docs] def cell_node_map(self, bcs=None): ":class:`RealFunctionSpace` objects have no cell node map." return None
[docs] def interior_facet_node_map(self, bcs=None): ":class:`RealFunctionSpace` objects have no interior facet node map." return None
[docs] def exterior_facet_node_map(self, bcs=None): ":class:`RealFunctionSpace` objects have no exterior facet node map." return None
[docs] def bottom_nodes(self): ":class:`RealFunctionSpace` objects have no bottom nodes." return None
[docs] def top_nodes(self): ":class:`RealFunctionSpace` objects have no bottom nodes." return None
[docs] def local_to_global_map(self, bcs, lgmap=None): assert len(bcs) == 0 return None
[docs] @dataclass class FunctionSpaceCargo: """Helper class carrying data for a :class:`WithGeometryBase`. It is required because it permits Firedrake to have stripped forms that still know Firedrake-specific information (e.g. that they are a component of a parent function space). """ topological: FunctionSpace parent: Optional[WithGeometryBase]