Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions finat/discontinuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def point_evaluation(self, order, refcoords, entity=None, coordinate_mapping=Non
def dual_basis(self):
return self.element.dual_basis

def dual_evaluation(self, fn, coordinate_mapping=None):
return self.element.dual_evaluation(fn, coordinate_mapping)

@property
def mapping(self):
return self.element.mapping
192 changes: 167 additions & 25 deletions finat/enriched.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from functools import partial
from functools import partial, singledispatch
from itertools import chain
from operator import add, methodcaller

Expand All @@ -8,8 +8,14 @@
from gem.interpreter import evaluate
from gem.utils import cached_property

from finat.finiteelementbase import FiniteElementBase
from finat.hdivcurl import HCurlElement, HDivElement
from finat.cube import FlattenedDimensions
from finat.discontinuous import DiscontinuousElement
from finat.finiteelementbase import FiniteElementBase, broadcast_tensor
from finat.hdivcurl import HCurlElement, HDivElement, WrapperElementBase
from finat.point_set import UnionPointSet
from finat.quadrature_element import QuadratureElement
from finat.tensor_product import TensorProductElement
from finat.tensorfiniteelement import TensorFiniteElement


class EnrichedElement(FiniteElementBase):
Expand Down Expand Up @@ -160,34 +166,170 @@ def mapping(self):
result, = mappings
return result

def dual_evaluation(self, argument, coordinate_mapping=None):
@cached_property
def _summands(self):
"""The summands that are not themselves direct sums, in basis order.

An element is brought out as a direct sum one level at a time, so a
summand may be a direct sum in turn. These are the elements that
evaluate their dual basis on their own points, and whose points make
up the union that :attr:`dual_basis` works against.
"""
summands = []
for element in self.elements:
expanded = as_enriched(element)
summands.extend(expanded._summands if expanded is not None
else [element])
return tuple(summands)

@property
def dual_basis(self):
"""The weights of the dual basis, on the union of the summands' points.

A summand's functionals evaluate only on that summand's points, so the
weights are block diagonal: each summand's weights sit at its own
offset in the union, and are zero against every other summand's
points. :meth:`_dual_evaluation` contracts each summand on its own
points instead of carrying those zeros.
"""
duals = [element.dual_basis for element in self._summands]
x = UnionPointSet([xk for _, xk in duals])
p, = x.indices
zeta = self.get_value_indices()
# The natural shape of each summand's own points, in the order they
# occupy the union.
shapes = [tuple(i.extent for i in xk.indices) for _, xk in duals]

blocks = []
for k, (element, (Q, xk)) in enumerate(zip(self._summands, duals)):
alpha = element.get_indices()
# Turn this summand's point indices into a shape, so that its
# weights can be embedded at its own offset in the union.
own = gem.ComponentTensor(gem.Indexed(Q, alpha + zeta), xk.indices)
pieces = [own if j == k else gem.Zero(shape)
for j, shape in enumerate(shapes)]
weights = gem.Indexed(gem.Concatenate(*pieces), (p,))
blocks.append(gem.ComponentTensor(weights, alpha))

beta, = self.get_indices()
Q = gem.Indexed(gem.Concatenate(*blocks), (beta,))
return gem.ComponentTensor(Q, (beta,) + zeta), x

def _dual_evaluation(self, fn, coordinate_mapping=None):
"""Dual evaluate each summand on its own points.

:arg fn: Callable representing the function to dual evaluate.
Callable should take in an :class:`AbstractPointSet` and
return a GEM expression for evaluation of the function at
those points.
:arg coordinate_mapping: a
:class:`~.physically_mapped.PhysicalGeometry` object that
provides physical geometry callbacks (may be None).
:returns: an ``(evaluation, point_indices, basis_indices)`` triple, as
:meth:`~finat.finiteelementbase.FiniteElementBase.dual_evaluation`
returns. The points are contracted here, so ``point_indices`` is
empty.

The summands do not share their points, so each one contracts on its
own, and the results stack along the basis index. Concatenating over
a free index is what :func:`~gem.unconcatenate.unconcatenate` splits
downstream; a concatenation over the contracted points could not be.
"""
if not self.is_nodal_enriched:
raise NotImplementedError(
f"Dual evaluation not defined for element {type(self).__name__}"
f"Dual evaluation not defined for non-nodal {type(self).__name__}"
)
# Gather results from all sub-elements
# Each sub_result is (eval_expr, point_indices, local_indices)
sub_results = [sub.dual_evaluation(argument, coordinate_mapping=coordinate_mapping)
for sub in self.elements]

# Extract the evaluation sub-expressions, contracting each over its own points.
# We must ensure that all subindices are in the free indices of subexpr
# before wrapping in ComponentTensor. If some are missing (e.g. if the
# expression simplified to a constant), we multiply by a dummy ones tensor.
evals = []
for subexpr, point_indices, subindices in sub_results:
subexpr = gem.IndexSum(subexpr, point_indices)
missing_indices = tuple(idx for idx in subindices if idx not in subexpr.free_indices)
if missing_indices:
shape = tuple(idx.extent for idx in missing_indices)
ones = gem.Literal(numpy.ones(shape))
dummy = gem.Indexed(ones, missing_indices)
subexpr = gem.Product(subexpr, dummy)
evals.append(gem.ComponentTensor(subexpr, subindices))
for element in self.elements:
expr, point_indices, indices = element.dual_evaluation(
fn, coordinate_mapping=coordinate_mapping)
evals.append(broadcast_tensor(gem.IndexSum(expr, point_indices), indices))

beta = self.get_indices()
expr = gem.Indexed(gem.Concatenate(*evals), beta)
return expr, (), beta
return gem.Indexed(gem.Concatenate(*evals), beta), (), beta


@singledispatch
def as_enriched(element):
"""Rewrite an element as a direct sum, bringing the sum outermost.

:arg element: the :class:`~finat.finiteelementbase.FiniteElementBase` to
rewrite.
:returns: an :class:`EnrichedElement` with the same basis functions in the
same order as ``element``, or ``None`` if ``element`` is not a
direct sum.
"""
return None


@as_enriched.register(EnrichedElement)
def as_enriched_enriched(element):
return element


@as_enriched.register(FlattenedDimensions)
def as_enriched_flattened(element):
return as_enriched(element.product)


@as_enriched.register(DiscontinuousElement)
def as_enriched_discontinuous(element):
return as_enriched(element.element)


@as_enriched.register(WrapperElementBase)
def as_enriched_wrapper(element):
"""Distribute the pullback over the sum the wrapped element is."""
summands = as_enriched(element.wrappee)
if summands is None:
return None
return EnrichedElement([type(element)(e) for e in summands.elements],
is_nodal_enriched=summands.is_nodal_enriched)


@as_enriched.register(QuadratureElement)
def as_enriched_quadrature_element(element):
"""Rewrite a rule on a union of point sets as a sum of one rule each."""
rules = element._summand_rules
if not rules:
return None
return EnrichedElement(
[QuadratureElement(element.cell, rule) for rule in rules],
is_nodal_enriched=True)


@as_enriched.register(TensorFiniteElement)
def as_enriched_tensor_finite_element(element):
"""Distribute the vector/tensor wrapper over the sum its base element is."""
summands = as_enriched(element.base_element)
if summands is None:
return None
return EnrichedElement(
[TensorFiniteElement(e, element._shape, element._transpose) for e in summands.elements],
is_nodal_enriched=summands.is_nodal_enriched)


@as_enriched.register(TensorProductElement)
def as_enriched_tensor_product(element):
"""Distribute the product over the sum its first factor is.

The summands of a sum in the first factor own a contiguous range of the
flat basis index, so they stack in the order the product already numbers
them. A sum in any later factor would interleave with the factors before
it, and stacking would renumber the degrees of freedom.
"""
first, *rest = element.factors
if any(as_enriched(factor) is not None for factor in rest):
raise NotImplementedError(
"Only the first factor of a TensorProductElement may be a direct"
" sum, as the degrees of freedom of a later one do not stack"
)
summands = as_enriched(first)
if summands is None:
return None
return EnrichedElement(
[TensorProductElement((e, *rest)) for e in summands.elements],
is_nodal_enriched=summands.is_nodal_enriched)


def tree_map(f, *args):
Expand Down
75 changes: 75 additions & 0 deletions finat/finiteelementbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@
from finat.quadrature import make_quadrature


def broadcast_tensor(expression, multiindex):
"""Reshape a tensor expression, broadcasting over the indices it lacks.

:arg expression: an indexed tensor.
:arg multiindex: the indices to turn into a shape.
:returns: ``expression`` as a tensor of the extents of ``multiindex``.

:class:`gem.ComponentTensor` requires every index to be free in the
expression, which a cellwise constant, say, does not satisfy. Multiply
by ones to broadcast over the missing indices.
"""
missing = tuple(i for i in multiindex if i not in expression.free_indices)
if missing:
ones = gem.Literal(numpy.ones(tuple(i.extent for i in missing)))
expression = gem.Product(expression, gem.Indexed(ones, missing))
return gem.ComponentTensor(expression, multiindex)


class FiniteElementBase(metaclass=ABCMeta):

@abstractproperty
Expand Down Expand Up @@ -163,6 +181,39 @@ def basis_evaluation(self, order, ps, entity=None, coordinate_mapping=None):
provides physical geometry callbacks (may be None).
'''

def _stack_tabulations(self, order, ps, entity=None, coordinate_mapping=None):
"""Tabulate on each point set of a union, stacking on the point index.

:arg order: return derivatives up to this order.
:arg ps: the :class:`~finat.point_set.UnionPointSet` to tabulate on.
:arg entity: the cell entity on which to tabulate.
:arg coordinate_mapping: a
:class:`~.physically_mapped.PhysicalGeometry` object that
provides physical geometry callbacks (may be None).
:returns: the tabulation on the whole of ``ps``, as
:meth:`basis_evaluation` returns.

A union of points has no structure of its own, so structured elements
tabulate on each point set in turn and stack the tabulations here.
"""
tables = [self.basis_evaluation(order, sub, entity,
coordinate_mapping=coordinate_mapping)
for sub in ps.point_sets]
keys, = set(map(frozenset, tables))
p, = ps.indices
multiindex = tuple(chain(self.get_indices(), self.get_value_indices()))

def concatenate(alpha):
# The point indices are free in each table, so promote them to a
# shape before concatenating the tables along it.
pieces = [broadcast_tensor(gem.Indexed(table[alpha], multiindex),
sub.indices)
for table, sub in zip(tables, ps.point_sets)]
return gem.ComponentTensor(
gem.Indexed(gem.Concatenate(*pieces), (p,)), multiindex)

return {alpha: concatenate(alpha) for alpha in keys}

@abstractmethod
def point_evaluation(self, order, refcoords, entity=None, coordinate_mapping=None):
'''Return code for evaluating the element at an arbitrary points on
Expand Down Expand Up @@ -262,6 +313,30 @@ def dual_evaluation(self, fn, coordinate_mapping=None):
is compiled from ``evaluation`` (alongside any argument
multiindices already encoded within ``fn``)
'''
# Only a direct sum can dual evaluate each summand on its own points,
# so bring the sum outermost first.
from finat.enriched import as_enriched # Avoid circular import
summands = as_enriched(self)
if summands is not None and summands is not self:
return summands.dual_evaluation(fn, coordinate_mapping=coordinate_mapping)
return self._dual_evaluation(fn, coordinate_mapping=coordinate_mapping)

def _dual_evaluation(self, fn, coordinate_mapping=None):
"""Dual evaluate an element that is not a direct sum.

:arg fn: Callable representing the function to dual evaluate.
Callable should take in an :class:`AbstractPointSet` and
return a GEM expression for evaluation of the function at
those points.
:arg coordinate_mapping: a
:class:`~.physically_mapped.PhysicalGeometry` object that
provides physical geometry callbacks (may be None).
:returns: an ``(evaluation, point_indices, basis_indices)`` triple, as
:meth:`dual_evaluation` returns.

:meth:`dual_evaluation` rewrites an element as a direct sum before
calling this, so the element here has a single set of points.
"""
Q, x = self.dual_basis
Q = self.dual_transformation(Q, coordinate_mapping=coordinate_mapping)

Expand Down
23 changes: 23 additions & 0 deletions finat/point_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,29 @@ def almost_equal(self, other, tolerance=1e-12):
for s, o in zip(self.factors, other.factors))


class UnionPointSet(PointSet):
"""All of the points of several point sets, along a single point index.

:arg point_sets: the point sets to take the union of, in the order they
occupy the point index. A union of unions is flattened.

These are the points of a dual basis that evaluates summand by summand,
named so that a function space can be built on them. The point sets are
kept because an element that needs their structure to tabulate -- a tensor
product, which cannot factor the union -- tabulates on each in turn.
"""

def __init__(self, point_sets):
self.point_sets = tuple(chain(*(
ps.point_sets if isinstance(ps, UnionPointSet) else (ps,)
for ps in point_sets)))
super().__init__(numpy.concatenate([ps.points
for ps in self.point_sets]))

def __repr__(self):
return f"{type(self).__name__}({self.point_sets!r})"


class FacetPointSet(AbstractPointSet):
"""A point set on facets.

Expand Down
Loading
Loading