Source code for firedrake.extrusion_utils

import collections
import itertools
import numpy
import islpy as isl

import finat
from pyop2 import op2
from pyop2.caching import serial_cache
from firedrake.petsc import PETSc
from firedrake.utils import IntType, RealType, ScalarType
from finat.element_factory import create_element
import loopy as lp
from loopy.version import LOOPY_USE_LANGUAGE_VERSION_2018_2  # noqa: F401
from firedrake.parameters import target
from ufl.domain import extract_unique_domain


[docs] @PETSc.Log.EventDecorator() def make_extruded_coords(extruded_topology, base_coords, ext_coords, layer_height, extrusion_type='uniform', kernel=None): """ Given either a kernel or a (fixed) layer_height, compute an extruded coordinate field for an extruded mesh. :arg extruded_topology: an :class:`~.ExtrudedMeshTopology` to extrude a coordinate field for. :arg base_coords: a :class:`~.Function` to read the base coordinates from. :arg ext_coords: a :class:`~.Function` to write the extruded coordinates into. :arg layer_height: the height for each layer. Either a scalar, where layers will be equi-spaced at the specified height, or a 1D array of variable layer heights to use through the extrusion. :arg extrusion_type: the type of extrusion to use. Predefined options are either "uniform" (creating equi-spaced layers by extruding in the (n+1)dth direction), "radial" (creating equi-spaced layers by extruding in the outward direction from the origin) or "radial_hedgehog" (creating equi-spaced layers by extruding coordinates in the outward cell-normal direction, needs a P1dgxP1 coordinate field). :arg kernel: an optional kernel to carry out coordinate extrusion. The kernel signature (if provided) is:: void kernel(double **base_coords, double **ext_coords, double *layer_height, int layer) The kernel iterates over the cells of the mesh and receives as arguments the coordinates of the base cell (to read), the coordinates on the extruded cell (to write to), the fixed layer height, and the current cell layer. """ _, vert_space = ext_coords.function_space().ufl_element().sub_elements[0].sub_elements if kernel is None and not (vert_space.degree() == 1 and vert_space.family() in ['Lagrange', 'Discontinuous Lagrange']): raise RuntimeError('Extrusion of coordinates is only possible for a P1 or P1dg interval unless a custom kernel is provided') layer_height = numpy.atleast_1d(numpy.array(layer_height, dtype=RealType)) if layer_height.ndim > 1: raise RuntimeError('Extrusion layer height should be 1d or scalar') if layer_height.size > 1: layer_height = numpy.cumsum(numpy.concatenate(([0], layer_height))) layer_heights = layer_height.size layer_height = op2.Global(layer_heights, layer_height, dtype=RealType, comm=extruded_topology._comm) if kernel is not None: op2.ParLoop(kernel, ext_coords.cell_set, ext_coords.dat(op2.WRITE, ext_coords.cell_node_map()), base_coords.dat(op2.READ, base_coords.cell_node_map()), layer_height(op2.READ), pass_layer_arg=True).compute() return ext_fe = create_element(ext_coords.ufl_element()) ext_shape = ext_fe.index_shape base_fe = create_element(base_coords.ufl_element()) base_shape = base_fe.index_shape data = [] data.append(lp.GlobalArg("ext_coords", dtype=ScalarType, shape=ext_shape)) data.append(lp.GlobalArg("base_coords", dtype=ScalarType, shape=base_shape)) data.append(lp.GlobalArg("layer_height", dtype=RealType, shape=(layer_heights,))) data.append(lp.ValueArg('layer')) base_coord_dim = base_coords.function_space().value_size # Deal with tensor product cells adim = len(ext_shape) - 2 # handle single or variable layer heights if layer_heights == 1: height_var = "layer_height[0] * (layer + l)" else: height_var = "layer_height[layer + l]" def _get_arity_axis_inames(_base): return tuple(_base + str(i) for i in range(adim)) def _get_lp_domains(_inames, _extents): domains = [] for idx, extent in zip(_inames, _extents): inames = isl.make_zero_and_vars([idx]) domains.append(((inames[0].le_set(inames[idx])) & (inames[idx].lt_set(inames[0] + extent)))) return domains if extrusion_type == 'uniform': domains = [] dd = _get_arity_axis_inames('d') domains.extend(_get_lp_domains(dd, ext_shape[:adim])) domains.extend(_get_lp_domains(('c',), (base_coord_dim,))) if layer_heights == 1: domains.extend(_get_lp_domains(('l',), (2,))) else: domains.append("[layer] -> { [l] : 0 <= l <= 1 & 0 <= l + layer < %d}" % layer_heights) instructions = """ ext_coords[{dd}, l, c] = base_coords[{dd}, c] ext_coords[{dd}, l, {base_coord_dim}] = ({hv}) """.format(dd=', '.join(dd), base_coord_dim=base_coord_dim, hv=height_var) name = "pyop2_kernel_uniform_extrusion" elif extrusion_type == 'radial': domains = [] dd = _get_arity_axis_inames('d') domains.extend(_get_lp_domains(dd, ext_shape[:adim])) domains.extend(_get_lp_domains(('c', 'k'), (base_coord_dim, ) * 2)) if layer_heights == 1: domains.extend(_get_lp_domains(('l',), (2,))) else: domains.append("[layer] -> { [l] : 0 <= l <= 1 & 0 <= l + layer < %d}" % layer_heights) instructions = """ <{RealType}> tt[{dd}] = 0 <{RealType}> bc[{dd}] = 0 for k bc[{dd}] = real(base_coords[{dd}, k]) tt[{dd}] = tt[{dd}] + bc[{dd}] * bc[{dd}] end tt[{dd}] = sqrt(tt[{dd}]) ext_coords[{dd}, l, c] = base_coords[{dd}, c] + base_coords[{dd}, c] * ({hv}) / tt[{dd}] """.format(RealType=RealType, dd=', '.join(dd), hv=height_var) name = "pyop2_kernel_radial_extrusion" elif extrusion_type == 'radial_hedgehog': # Only implemented for interval in 2D and triangle in 3D. # gdim != tdim already checked in ExtrudedMesh constructor. tdim = extract_unique_domain(base_coords).ufl_cell().topological_dimension() if tdim not in [1, 2]: raise NotImplementedError("Hedgehog extrusion not implemented for %s" % extract_unique_domain(base_coords).ufl_cell()) # tdim == 1: # # normal is: # (0 -1) (x2 - x1) # (1 0) (y2 - y1) # # tdim == 2: # normal is # v0 x v1 # # /\ # v0/ \ # / \ # /------\ # v1 domains = [] dd = _get_arity_axis_inames('d') _dd = _get_arity_axis_inames('_d') domains.extend(_get_lp_domains(dd, ext_shape[:adim])) domains.extend(_get_lp_domains(_dd, ext_shape[:adim])) if tdim == 1: domains.extend(_get_lp_domains(('c0', 'c1', 'c2', 'k', 'l'), (base_coord_dim, ) * 4 + (2, ))) else: domains.extend(_get_lp_domains(('c0', 'c1', 'c2', 'c3', 'k', 'l'), (base_coord_dim, ) * 5 + (2, ))) # Formula for normal, n n_1_1 = """ n[0] = -bc[1, 1] + bc[0, 1] n[1] = bc[1, 0] - bc[0, 0] """ n_2_1 = """ v0[c3] = bc[1, c3] - bc[0, c3] v1[c3] = bc[2, c3] - bc[0, c3] n[0] = v0[1] * v1[2] - v0[2] * v1[1] n[1] = v0[2] * v1[0] - v0[0] * v1[2] n[2] = v0[0] * v1[1] - v0[1] * v1[0] """ n_2_2 = """ v0[c3] = bc[0, 1, c3] - bc[0, 0, c3] v1[c3] = bc[1, 0, c3] - bc[0, 0, c3] n[0] = v0[1] * v1[2] - v0[2] * v1[1] n[1] = v0[2] * v1[0] - v0[0] * v1[2] n[2] = v0[0] * v1[1] - v0[1] * v1[0] """ n_dict = {1: {1: n_1_1}, 2: {1: n_2_1, 2: n_2_2}} instructions = """ <{RealType}> dot = 0 <{RealType}> norm = 0 <{RealType}> v0[c2] = 0 <{RealType}> v1[c2] = 0 <{RealType}> n[c2] = 0 <{RealType}> x[c2] = 0 <{RealType}> bc[{_dd}, c1] = real(base_coords[{_dd}, c1]) for {_dd} x[c1] = x[c1] + bc[{_dd}, c1] end {ninst} for k dot = dot + x[k] * n[k] norm = norm + n[k] * n[k] end norm = sqrt(norm) norm = -norm if dot < 0 else norm ext_coords[{dd}, l, c0] = base_coords[{dd}, c0] + n[c0] * ({hv}) / norm """.format(RealType=RealType, dd=', '.join(dd), _dd=', '.join(_dd), ninst=n_dict[tdim][adim], hv=height_var) name = "pyop2_kernel_radial_hedgehog_extrusion" else: raise NotImplementedError('Unsupported extrusion type "%s"' % extrusion_type) ast = lp.make_function(domains, instructions, data, name=name, target=target, seq_dependencies=True, silenced_warnings=["summing_if_branches_ops"]) kernel = op2.Kernel(ast, name) op2.ParLoop(kernel, ext_coords.cell_set, ext_coords.dat(op2.WRITE, ext_coords.cell_node_map()), base_coords.dat(op2.READ, base_coords.cell_node_map()), layer_height(op2.READ), pass_layer_arg=True).compute()
[docs] def flat_entity_dofs(entity_dofs): flat_entity_dofs = {} for b, v in entity_dofs: # v in [0, 1]. Only look at the ones, then grab the data from zeros. if v == 0: continue flat_entity_dofs[b] = {} for i in entity_dofs[(b, v)]: # This line is fairly magic. # It works because an interval has two points. # We pick up the DoFs from the bottom point, # then the DoFs from the interior of the interval, # then finally the DoFs from the top point. flat_entity_dofs[b][i] = (entity_dofs[(b, 0)][2*i] + entity_dofs[(b, 1)][i] + entity_dofs[(b, 0)][2*i+1]) return flat_entity_dofs
[docs] def flat_entity_permutations(entity_permutations): flat_entity_permutations = {} for b in set(b for b, v in entity_permutations): flat_entity_permutations[b] = {} for eb in set(e // 2 for e in entity_permutations[(b, 0)]): flat_entity_permutations[b][eb] = {} for ob in set(ob for eo, ob, ov in entity_permutations[(b, 0)][2 * eb]): # eo (extrinsic orientation) is always 0 for: # -- quad x interval, # -- triangle x interval, # -- etc. # eo = {0, 1}, but only eo = 0 is relevant for: # -- interval x interval on dim = (1, 1). eo = 0 # Orientation in the extruded direction is always 0 ov = 0 perm0 = entity_permutations[(b, 0)][2 * eb][(eo, ob, ov)] perm1 = entity_permutations[(b, 1)][eb][(eo, ob, ov)] n0, n1 = len(perm0), len(perm1) flat_entity_permutations[b][eb][ob] = \ list(perm0) + \ [n0 + p for p in perm1] + \ [n0 + n1 + p for p in perm0] return flat_entity_permutations
[docs] def entity_indices(cell): """Return a dict mapping topological entities on a cell to their integer index. This provides an iteration ordering for entities on extruded meshes. :arg cell: a FIAT cell. """ subents, = cell.sub_entities[cell.get_dimension()].values() return {e: i for i, e in enumerate(sorted(subents))}
[docs] def entity_reordering(cell): """Return an array reordering extruded cell entities. If we iterate over the base cell, it is natural to then go over all the entities induced by the product with an interval. This iteration order is not the same as the natural iteration order, so we need a reordering. :arg cell: a FIAT tensor product cell. """ def points(t): for k in sorted(t.keys()): yield itertools.repeat(k, len(t[k])) counter = collections.Counter() topos = (c.get_topology() for c in cell.cells) indices = entity_indices(cell) ordering = numpy.zeros(len(indices), dtype=IntType) for i, ent in enumerate(itertools.product(*(itertools.chain(*points(t)) for t in topos))): ordering[i] = indices[ent, counter[ent]] counter[ent] += 1 return ordering
[docs] def entity_closures(cell): """Map entities in a cell to points in the topological closure of the entity. :arg cell: a FIAT cell. """ indices = entity_indices(cell) closure = {} for e, ents in cell.sub_entities.items(): for ent, vals in ents.items(): idx = indices[(e, ent)] closure[idx] = list(map(indices.get, vals)) return closure
[docs] def make_offset_key(finat_element): from firedrake.functionspacedata import entity_dofs_key # scalar-valued elements only if isinstance(finat_element, finat.TensorFiniteElement): finat_element = finat_element.base_element return entity_dofs_key(finat_element.entity_dofs()), is_real_tensor_product_element(finat_element)
[docs] @serial_cache(hashkey=make_offset_key) def calculate_dof_offset(finat_element): """Return the offset between the neighbouring cells of a column for each DoF. :arg finat_element: A FInAT element. :returns: A numpy array containing the offset for each DoF. """ # scalar-valued elements only if isinstance(finat_element, finat.TensorFiniteElement): finat_element = finat_element.base_element dof_offset = numpy.zeros(finat_element.space_dimension(), dtype=IntType) if is_real_tensor_product_element(finat_element): return dof_offset entity_offset = [0] * (1 + finat_element.cell.get_dimension()[0]) for (b, v), entities in finat_element.entity_dofs().items(): entity_offset[b] += len(entities[0]) for (b, v), entities in finat_element.entity_dofs().items(): for dof_indices in entities.values(): for i in dof_indices: dof_offset[i] = entity_offset[b] return dof_offset
[docs] @serial_cache(hashkey=make_offset_key) def calculate_dof_offset_quotient(finat_element): """Return the offset quotient for each DoF within the base cell. :arg finat_element: A FInAT element. :returns: A numpy array containing the offset quotient for each DoF. offset_quotient q of each DoF (in a local cell) is defined as i // o, where i is the local DoF ID of the DoF on the entity and o is the offset of that DoF computed in ``calculate_dof_offset()``. Let DOF(e, l, i) represent a DoF on (base-)entity e on layer l that has local ID i and suppose this DoF has offset o and offset_quotient q. In periodic extrusion it is convenient to identify DOF(e, l, i) as DOF(e, l + q, i % o); this transformation allows one to always work with the "unit cell" in which i < o always holds. In FEA offset_quotient is 0 or 1. Example:: local ID offset offset_quotient 2--2--2 2--2--2 1--1--1 | | | | | | CG2 1 1 1 2 2 2 0 0 0 | | | | | | 0--0--0 2--2--2 0--0--0 +-----+ +-----+ +-----+ | 1 3 | | 4 4 | | 0 0 | DG1 | | | | | | | 0 2 | | 4 4 | | 0 0 | +-----+ +-----+ +-----+ """ # scalar-valued elements only if isinstance(finat_element, finat.TensorFiniteElement): finat_element = finat_element.base_element if is_real_tensor_product_element(finat_element): return None dof_offset_quotient = numpy.zeros(finat_element.space_dimension(), dtype=IntType) for (b, v), entities in finat_element.entity_dofs().items(): for entity, dof_indices in entities.items(): quotient = 1 if v == 0 and entity % 2 == 1 else 0 for i in dof_indices: dof_offset_quotient[i] = quotient if (dof_offset_quotient == 0).all(): # Avoid unnecessary codegen in pyop2/codegen/builder. dof_offset_quotient = None return dof_offset_quotient
[docs] def is_real_tensor_product_element(element): """Is the provided FInAT element a tensor product involving the real space? :arg element: A scalar FInAT element. """ assert not isinstance(element, finat.TensorFiniteElement) if isinstance(element, finat.TensorProductElement): _, factor = element.factors return isinstance(factor, finat.Real) else: return False