diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b33e9acd..1be9106c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,7 @@ jobs: with: repository: firedrakeproject/firedrake path: firedrake-repo - ref: refs/heads/connorjward/pyop3-develop + ref: refs/heads/connorjward/pyop3 - name: Validate single source of truth run: ./firedrake-repo/scripts/check-config @@ -180,9 +180,20 @@ jobs: run: | . venv/bin/activate python3 -m pip install --break-system-packages -e './fiat-repo' + + - uses: actions/checkout@v5 + with: + repository: firedrakeproject/ufl + path: ufl-repo + ref: refs/heads/indiamai/hash_directional_sobolev + + - name: Install checked out UFL + run: | + . venv/bin/activate + python3 -m pip install --break-system-packages -e './ufl-repo' - name: Run tests run: | . venv/bin/activate cd ./fuse-repo - firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" ./test \ No newline at end of file + firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" ./test diff --git a/Makefile b/Makefile index f757a950..71a15132 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,12 @@ tests: @echo " Running all tests" @python3 -m coverage run -p -m pytest -rx test +mini_tests: + @FIREDRAKE_USE_FUSE=1 python3 -m pytest test/test_2d_examples_docs.py + @FIREDRAKE_USE_FUSE=1 python3 -m pytest test/test_convert_to_fiat.py::test_1d + @FIREDRAKE_USE_FUSE=1 python3 -m pytest test/test_orientations.py::test_surface_vec_rt + @FIREDRAKE_USE_FUSE=1 python3 -m pytest test/test_convert_to_fiat.py::test_projection_convergence_3d\[construct_tet_ned-N1curl-1-0.8\] + coverage: @python3 -m coverage combine @python3 -m coverage report -m diff --git a/fuse/__init__.py b/fuse/__init__.py index 5b98b173..a0872bd6 100644 --- a/fuse/__init__.py +++ b/fuse/__init__.py @@ -1,9 +1,10 @@ -from fuse.cells import Point, Edge, polygon, make_tetrahedron, constructCellComplex +from fuse.cells import Point, Edge, polygon, line, make_tetrahedron, constructCellComplex, TensorProductPoint from fuse.groups import S1, S2, S3, D4, Z3, Z4, tet_C2, C3, C4, S4, A4, tet_edges, tet_faces, sq_edges, GroupRepresentation, PermutationSetRepresentation, Permutation, get_cyc_group, get_sym_group + from fuse.dof import DeltaPairing, DOF, L2Pairing, FuseFunction, PointKernel, VectorKernel, BarycentricPolynomialKernel, PolynomialKernel, ComponentKernel from fuse.triples import ElementTriple, DOFGenerator, immerse from fuse.traces import TrH1, TrGrad, TrHess, TrHCurl, TrHDiv -from fuse.tensor_products import tensor_product +from fuse.tensor_products import tensor_product, symmetric_tensor_product from fuse.spaces.element_sobolev_spaces import CellH1, CellL2, CellHDiv, CellHCurl, CellH2 from fuse.spaces.polynomial_spaces import P0, P1, P2, P3, Q2, PolynomialSpace diff --git a/fuse/cells.py b/fuse/cells.py index 57d5dfd9..d3dd74db 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -10,11 +10,13 @@ from matplotlib.patches import FancyArrowPatch from mpl_toolkits.mplot3d import proj3d from sympy.combinatorics.named_groups import SymmetricGroup -from fuse.utils import sympy_to_numpy, fold_reduce, numpy_to_str_tuple, orientation_value +from fuse.utils import sympy_to_numpy, fold_reduce, numpy_to_str_tuple, orientation_value, _SYMBOLS, as_tuple from FIAT.reference_element import Simplex, TensorProductCell as FiatTensorProductCell, Hypercube from FIAT.quadrature_schemes import create_quadrature from ufl.cell import Cell, TensorProductCell from functools import cache +from itertools import product +from collections import defaultdict class Arrow3D(FancyArrowPatch): @@ -151,6 +153,13 @@ def compute_scaled_verts(d, n): raise ValueError("Dimension {} not supported".format(d)) +def line(): + """ + Constructs the default 1D interval + """ + return Point(1, [Point(0), Point(0)], vertex_num=2) + + def polygon(n): """ Constructs the 2D default cell with n sides/vertices @@ -289,7 +298,7 @@ class Point(): id_iter = itertools.count() def __init__(self, d, edges=[], vertex_num=None, oriented=False, group=None, edge_orientations=None, cell_id=None): - if not cell_id: + if cell_id is None: cell_id = next(self.id_iter) self.id = cell_id self.dimension = d @@ -299,7 +308,7 @@ def __init__(self, d, edges=[], vertex_num=None, oriented=False, group=None, edg if d == 0: assert (edges == []) - if vertex_num: + if vertex_num and vertex_num > 1: edges = self.compute_attachments(vertex_num, edges, edge_orientations) self.oriented = oriented @@ -373,6 +382,7 @@ def compute_cell_group(self): """ verts = self.ordered_vertices() v_coords = [self.get_node(v, return_coords=True) for v in verts] + n = len(verts) max_group = SymmetricGroup(n) edges = [edge.ordered_vertices() for edge in self.edges()] @@ -394,6 +404,9 @@ def get_spatial_dimension(self): def dim(self): return self.dimension + def dimensions(self): + return [i for i in range(self.dimension + 1)] + def get_shape(self): num_verts = len(self.vertices()) if num_verts == 1: @@ -502,6 +515,15 @@ def get_starter_ids(self): min_ids = [min(dimension) for dimension in structure] return min_ids + def local_id(self, node): + structure = [sorted(generation) for generation in nx.topological_generations(self.G)] + structure.reverse() + min_id = self.get_starter_ids() + for d in range(len(structure)): + if node.id in structure[d]: + return node.id - min_id[d] + raise ValueError("Node not found in cell") + def graph_dim(self): if self.oriented: dim = self.dimension + 1 @@ -550,6 +572,7 @@ def ordered_vertex_coords(self): def d_entities_ids(self, d): return self.d_entities(d, get_class=False) + @cache def d_entities(self, d, get_class=True): """Get all the d dimensional entities of the cell complex. @@ -648,7 +671,6 @@ def basis_vectors(self, return_coords=True, entity=None, order=False, norm=True) self_levels = [generation for generation in nx.topological_generations(self.G)] vertices = entity.ordered_vertices() if self.dimension == 0: - # return [[] raise ValueError("Dimension 0 entities cannot have Basis Vectors") if self.oriented: # ordered_vertices() handles the orientation so we want to drop the orientation node @@ -822,7 +844,7 @@ def attachment(self, source, dst): return lambda *x: fold_reduce(attachments[0], *x) - def attachment_J(self, source, dst): + def attachment_J_det(self, source, dst): attachment = self.attachment(source, dst) symbol_names = ["x", "y", "z"] symbols = [] @@ -831,7 +853,7 @@ def attachment_J(self, source, dst): for i in range(self.dim_of_node(dst)): symbols += [sp.Symbol(symbol_names[i])] J = sp.Matrix(attachment(*symbols)).jacobian(sp.Matrix(symbols)) - return J + return np.sqrt(abs(float(sp.det(J.T * J)))) def quadrature(self, degree): fiat_el = self.to_fiat() @@ -839,6 +861,34 @@ def quadrature(self, degree): pts, wts = Q.get_points(), Q.get_weights() return pts, wts + def volume(self): + vertices = np.asarray(self.ordered_vertex_coords()) + if self.get_spatial_dimension() == 0: + return 1 + elif self.get_spatial_dimension() == 1: + return abs(vertices[1] - vertices[0])[0] + elif self.get_spatial_dimension() == 2: + x = vertices[:, 0] + y = vertices[:, 1] + return 0.5 * abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1))) + elif self.get_spatial_dimension() == 3: + vertices = np.asarray(vertices) + V = 0.0 + for face in self.d_entities(2): + pts = np.array([self.get_node(v, return_coords=True) for v in face.ordered_vertices()]) + c = pts.mean(axis=0) + + n = np.zeros(3) + for i in range(len(pts)): + v0 = pts[i] + v1 = pts[(i + 1) % len(pts)] + n += np.cross(v0, v1) + V += np.dot(c, n) + V = abs(V) / 3.0 + return V + else: + raise NotImplementedError("Dimension not accounted for") + def cartesian_to_barycentric(self, pts): verts = np.array(self.ordered_vertex_coords()) v_0 = self.ordered_vertex_coords()[0] @@ -891,8 +941,10 @@ def to_ufl(self, name=None): def _to_dict(self): # think this is probably missing stuff + # d, edges=[], vertex_num=None, oriented=False, group=None, edge_orientations=None, cell_id=None): o_dict = {"dim": self.dimension, "edges": [c for c in self.connections], + "vertex_num": len(self.vertices()), "oriented": self.oriented, "id": self.id} return o_dict @@ -903,6 +955,13 @@ def dict_id(self): def _from_dict(o_dict): return Point(o_dict["dim"], o_dict["edges"], oriented=o_dict["oriented"], cell_id=o_dict["id"]) + def equivalent(self, other): + if self.dimension != other.dimension: + return False + if set(self.ordered_vertex_coords()) != set(other.ordered_vertex_coords()): + return False + return self.get_topology() == other.get_topology() + class Edge(): """ @@ -926,7 +985,12 @@ def __call__(self, *x): if hasattr(self.attachment, '__iter__'): res = [] for attach_comp in self.attachment: - res.append(sympy_to_numpy(attach_comp, syms, x)) + if len(attach_comp.atoms(sp.Symbol)) <= len(x): + res.append(sympy_to_numpy(attach_comp, syms, x)) + else: + res_val = attach_comp.subs({syms[i]: x[i] for i in range(len(x))}) + res.append(res_val) + return tuple(res) return sympy_to_numpy(self.attachment, syms, x) return x @@ -957,50 +1021,266 @@ def _from_dict(o_dict): class TensorProductPoint(): + id_iter = itertools.count() - def __init__(self, A, B, flat=False): - self.A = A - self.B = B + def __init__(self, *factors): + self.id = next(self.id_iter) + self.A = factors[0] + self.B = factors[1] + self.factors = factors self.dimension = self.A.dimension + self.B.dimension - self.flat = flat + self.flat = False + self.fiat_elem = None + self.group = self.compute_cell_group() + self.entities = {} + + for d in self.dimensions()[:-1]: + self.entities[d] = [TensorProductPoint(*entities) for entities in product(*(f.d_entities(degree, True) for f, degree in zip(factors, d)))] + self.entities[self.dim()] = [self] def ordered_vertices(self): - return self.A.ordered_vertices() + self.B.ordered_vertices() + return self.entities[0] + + def ordered_vertex_coords(self): + return [sum(verts, ()) for verts in product(*(f.vertices(return_coords=True) for f in self.factors))] + + def component_orientations(self): + from fuse.utils import orientation_value + self.component_os_to_os = {} + for dim in self.to_fiat().get_topology(): + self.component_os_to_os[dim] = {} + ents = [f.d_entities(d)[0] for f, d in zip(self.factors, dim)] + verts = list(product(*(e.vertices() for e in ents))) + ident = [i for i in range(len(verts))] + group = list(product(*(e.group.members() for e in ents))) + for gs in group: + new_verts = list(product(*(g.permute(f.vertices()) for g, f in zip(gs, ents)))) + perm = [verts.index(v) for v in new_verts] + o_val = orientation_value(ident, perm) + if sum(dim) == self.dimension and self.group.group_rep_numbering is not None: + o_val = self.group.group_rep_numbering[o_val] + # if o_val in [m.numeric_rep() for m in self.group.members()]: + self.component_os_to_os[dim][tuple(g.numeric_rep() for g in gs)] = o_val + # else: + # breakpoint() + return self.component_os_to_os + + def compute_cell_group(self): + """ + Systematically work out the symmetry group of the tensor product cell. + """ + verts = self.vertices() + group = list(product(*(f.group.members() for f in self.factors))) + # group = [(g_a, g_b) for g_a in self.A.group.members() for g_b in self.B.group.members()] + perms = [] + for gs in group: + new_verts = list(product(*(g.permute(f.vertices()) for g, f in zip(gs, self.factors)))) + # new_verts = [(v_a, v_b) for v_a in g_a.permute(self.A.vertices()) for v_b in g_b.permute(self.B.vertices())] + perm = [verts.index(v) for v in new_verts] + perms += [fuse_groups.Permutation(perm)] + + grp = fuse_groups.PermutationSetRepresentation(perms).add_cell(self) + return grp + + def get_starter_ids(self): + # this doesn't actually make sense - remove when confirmed all changes to eliminate min ids from triple is done + raise NotImplementedError + a_starts = self.A.get_starter_ids() + b_starts = self.B.get_starter_ids() + ids = [] + for a, b in zip(a_starts, b_starts): + ids += [max(a, b)] + return ids def get_spatial_dimension(self): return self.dimension def get_sub_entities(self): - self.A.get_sub_entities() - self.B.get_sub_entities() + return self.to_fiat().sub_entities - def dimension(self): - return tuple(self.A.dimension, self.B.dimension) + def dim(self): + return self.dimensions()[-1] + + def dimensions(self): + return list(product(*(f.dimensions() for f in self.factors))) def d_entities(self, d, get_class=True): - return self.A.d_entities(d, get_class) + self.B.d_entities(d, get_class) + if isinstance(d, tuple): + if get_class: + return self.entities[d] + return [e.id for e in self.entities[d]] + raise NotImplementedError("Tensor Product point must be indexed by a tuple of dimensions") def vertices(self, get_class=True, return_coords=False): # TODO maybe refactor with get_node - verts = self.d_entities(0, get_class) if return_coords: - a_verts = self.A.vertices(return_coords=return_coords) - b_verts = self.B.vertices(return_coords=return_coords) - return [a + b for a in a_verts for b in b_verts] - return verts + # a_verts = self.A.vertices(return_coords=return_coords) + # b_verts = self.B.vertices(return_coords=return_coords) + # return [a + b for a in a_verts for b in b_verts] + return [sum(verts, ()) for verts in product(*(f.vertices(return_coords=True) for f in self.factors))] + # return [(a, b) for a in self.A.vertices() for b in self.B.vertices()] + return list(product(*(f.vertices() for f in self.factors))) + + def __repr__(self): + return "*".join([str(f) for f in self.factors]) def to_ufl(self, name=None): - if self.flat: - return CellComplexToUFL(self, "quadrilateral") - return TensorProductCell(self.A.to_ufl(), self.B.to_ufl()) + return TensorProductCell(*[f.to_ufl() for f in self.factors]) def to_fiat(self, name=None): - if self.flat: - return CellComplexToFiatHypercube(self, CellComplexToFiatTensorProduct(self, name)) - return CellComplexToFiatTensorProduct(self, name) + if self.fiat_elem is None: + self.fiat_elem = CellComplexToFiatTensorProduct(self, name) + return self.fiat_elem + + def flatten(self): + # Each factor must itself be hypercube-shaped: either a genuine + # interval (dimension == 1)or ann already-flattened cell + assert all(f.dimension == 1 or getattr(f, "flat", False) for f in self.factors) + return FlattenedPoint(*self.factors) + + +class FlattenedPoint(Point, TensorProductPoint): + d_entities_by_total_d = Point.d_entities + + def __init__(self, *factors): + self.A = factors[0] + self.B = factors[1] + self.factors = factors + self.dimension = sum(f.dimension for f in factors) + self.flat = True + fuse_edges = self.construct_fuse_rep() + super().__init__(self.dimension, fuse_edges) + + def to_ufl(self, name=None): + return CellComplexToUFL(self, name=name) + + def to_fiat(self, name=None): + # TODO this should check if it actually is a hypercube + fiat = CellComplexToFiatHypercube(self, CellComplexToFiatTensorProduct(self, name)) + return fiat + + def d_entities(self, d, get_class=True): + if isinstance(d, tuple): + if not get_class: + return [p.id for p in self.all_subpoints[d]] + return self.all_subpoints[d] + return self.d_entities_by_total_d(d, get_class) + + def tensor_attachment_expr(self, axis, factor_edge, parent_mask, child_mask): + """ + Build the tensor-product attachment as a tuple of SymPy expressions. + + Parameters + ---------- + axis: + The factor in which the parent cell is being restricted to a facet. + + factor_edge: + The Fuse Edge from the factor parent entity to the factor child entity. + Its `.attachment` is expected to be a SymPy expression or tuple of + SymPy expressions. + + parent_mask: + Dimension tuple of the parent product entity. + + child_mask: + Dimension tuple of the child product entity. + + Example + ------- + For parent mask (1, 1), child mask (0, 1), axis 0: + + parent coords: (x, y) + attachment might be: (0, y) or (1, y) + + For parent mask (1, 1, 1), child mask (1, 0, 1), axis 1: + + parent coords: (x, y, z) + attachment might be: (x, 0, z) or (x, 1, z) + """ + child_dim = sum(child_mask) + child_syms = _SYMBOLS[:child_dim] + result = tuple() + child_offset = 0 + + for i, (pdim, cdim) in enumerate(zip(parent_mask, child_mask)): + if i == axis: + local_expr = as_tuple(factor_edge.attachment) + # Substitute the child coordinates belonging to this factor. + local_child_syms = child_syms[child_offset:child_offset + cdim] + local_child_symbols = _SYMBOLS[:cdim] + subs = {old: new for old, new in zip(local_child_symbols, local_child_syms)} + mapped = tuple(sp.sympify(expr).subs(subs) for expr in local_expr) + for comp in mapped: + result += comp + child_offset += cdim + else: + # Identity map on unchanged tensor factors. + result += tuple(child_syms[child_offset:child_offset + cdim]) + child_offset += cdim + return result + + def construct_fuse_rep(self): + """ + Construct a Fuse Point for the tensor product of two or three Fuse Point objects. + """ + if len(self.factors) not in (2, 3): + raise NotImplementedError("Only 2- and 3-factor tensor products are supported.") + top_dim = sum(f.dimension for f in self.factors) + # Cache all subentities of each factor by dimension. + factor_entities = [{d: tuple(f.d_entities(d, get_class=True)) for d in range(f.dimension + 1)} + for f in self.factors] + masks_by_total_dim = defaultdict(list) + for mask in product(*(range(f.dimension + 1) for f in self.factors)): + masks_by_total_dim[sum(mask)].append(mask) + + product_points = {} + all_subpoints = {mask: [] + for mask in product(*(range(f.dimension + 1) for f in self.factors))} + + def codim_one_facets(product_entity, mask): + """ + Yield (child_product_entity, axis, factor_edge) for each codim-1 facet. + product_entity is a tuple of factor subentities. + mask is the corresponding tuple of factor dimensions. + """ + for axis, dim in enumerate(mask): + if dim == 0: + continue + factor_parent = product_entity[axis] + for factor_edge in factor_parent.connections: + child_factor_entity = factor_edge.point + + child_entity = list(product_entity) + child_entity[axis] = child_factor_entity + child_entity = tuple(child_entity) + + yield child_entity, axis, factor_edge + + top_level_edges = [] + for total_dim in range(top_dim + 1): + for mask in masks_by_total_dim[total_dim]: + for prod_ent in product(*(factor_entities[i][d] for i, d in enumerate(mask))): + if total_dim == 0: + product_point = Point(0) + else: + boundary = [] + for child_ent, axis, factor_edge in codim_one_facets(prod_ent, mask): + child_point = product_points[child_ent] + child_mask = tuple(e.dimension for e in child_ent) + attach = self.tensor_attachment_expr(axis, factor_edge, mask, child_mask) + boundary.append(Edge(child_point, attach, factor_edge.o)) + product_point = Point(total_dim, boundary) + + if prod_ent == tuple(self.factors): + top_level_edges = boundary + product_points[prod_ent] = product_point + all_subpoints[mask].append(product_point) + self.all_subpoints = all_subpoints + return top_level_edges def flatten(self): - return TensorProductPoint(self.A, self.B, True) + return self class CellComplexToFiatSimplex(Simplex): @@ -1028,7 +1308,7 @@ def __init__(self, cell, name=None, renumber=False): # breakpoint() def cellname(self): - return self.name + return "FUSE_" + self.name def construct_subelement(self, dimension, e_id=0, o=None): """Constructs the reference element of a cell @@ -1061,15 +1341,14 @@ def __new__(cls, cell, name=None, *args, **kwargs): def __init__(self, cell, name=None): self.fe_cell = cell - self.sub_cells = [cell.A.to_fiat(), cell.B.to_fiat()] + fiat_factors = [f.to_fiat() for f in cell.factors] if name is None: - name = " * ".join([s.name for s in self.sub_cells]) + name = " * ".join([s.name for s in fiat_factors]) self.name = name -# , sub_entities=self.fe_cell.get_sub_entities() - super(CellComplexToFiatTensorProduct, self).__init__(cell.A.to_fiat(), cell.B.to_fiat()) + super(CellComplexToFiatTensorProduct, self).__init__(*fiat_factors) def cellname(self): - return self.name + return "FUSE_" + self.name def construct_subelement(self, dimension): """Constructs the reference element of a cell @@ -1097,10 +1376,11 @@ class CellComplexToFiatHypercube(Hypercube): def __init__(self, cell, product): self.fe_cell = cell + self.name = product.name super(CellComplexToFiatHypercube, self).__init__(product.get_spatial_dimension(), product) def cellname(self): - return self.name + return "FUSE_" + self.name def construct_subelement(self, dimension): """Constructs the reference element of a cell @@ -1189,18 +1469,18 @@ def constructCellComplex(name): return polygon(3).to_ufl(name) # return ufc_triangle().to_ufl(name) elif name == "quadrilateral": - interval = Point(1, [Point(0), Point(0)], vertex_num=2) - return TensorProductPoint(interval, interval).flatten().to_ufl(name) - # return ufc_quad().to_ufl(name) + return TensorProductPoint(line(), line()).flatten().to_ufl(name) + # return firedrake_quad().to_ufl(name) # return polygon(4).to_ufl(name) elif name == "tetrahedron": # return ufc_tetrahedron().to_ufl(name) return make_tetrahedron().to_ufl(name) elif name == "hexahedron": - import warnings - warnings.warn("Hexahedron unimplemented in Fuse") - import ufl - return ufl.Cell(name) + # import warnings + # warnings.warn("Hexahedron unimplemented in Fuse") + # import ufl + # return ufl.Cell(name) + return TensorProductPoint(line(), line(), line()).flatten().to_ufl(name) elif "*" in name: components = [constructCellComplex(c.strip()).cell_complex for c in name.split("*")] return TensorProductPoint(*components).to_ufl(name) diff --git a/fuse/dof.py b/fuse/dof.py index 6d8ff2c6..48d7e9af 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -3,6 +3,7 @@ from fuse.traces import TrH1 import numpy as np import sympy as sp +import numbers class Pairing(): @@ -35,7 +36,7 @@ def __call__(self, kernel, v, cell): return v(*kernel.pt) def tabulate(self): - return 1 + return np.eye(self.entity.dim()) def add_entity(self, entity): res = DeltaPairing() @@ -80,8 +81,8 @@ def tabulate(self): if self.orientation: new_bvs = np.array(self.entity.orient(self.orientation).basis_vectors()) basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) - return basis_change - return np.eye(bvs.shape[0]) + return (1/self.entity.volume())*basis_change + return (1/self.entity.volume())*np.eye(bvs.shape[0]) def add_entity(self, entity): res = L2Pairing() @@ -190,11 +191,15 @@ def __call__(self, *args): return self.pt def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape): - if isinstance(self.pt, int): - return Qpts, np.array([wt*self.pt for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] + if len(value_shape) == 0: + comps = [[tuple()] for pt in Qpts] + else: + comps = [[(i,) for v in value_shape for i in range(v)] for pt in Qpts] + if isinstance(self.pt, tuple) or isinstance(self.pt, numbers.Number): + return Qpts, np.array([wt*self.pt for wt in Qwts]).astype(np.float64), comps if not immersed: - return Qpts, np.array([wt*np.matmul(self.pt, basis_change)for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] - return Qpts, np.array([wt*immersed(np.matmul(self.pt, basis_change))for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] + return Qpts, np.array([wt*np.matmul(self.pt, basis_change) for wt in Qwts]).astype(np.float64), comps + return Qpts, np.array([wt*immersed(np.matmul(self.pt, basis_change)) for wt in Qwts]).astype(np.float64), comps def _to_dict(self): o_dict = {"pt": self.pt} @@ -260,24 +265,28 @@ def evaluate(self, Qpts, bary_pts, Qwts, basis_change, immersed, dim, value_shap return Qpts, np.array(wts).astype(np.float64), comps def _to_dict(self): - o_dict = {"fn": self.fn} + o_dict = {"fn": self.fn, "syms": self.syms} return o_dict def dict_id(self): return "BarycentricPolynomialKernel" def _from_dict(obj_dict): - return BarycentricPolynomialKernel(obj_dict["fn"]) + return BarycentricPolynomialKernel(obj_dict["fn"], symbols=obj_dict["syms"]) class PolynomialKernel(BaseKernel): def __init__(self, fn, g=None, symbols=[]): if hasattr(fn, "__iter__"): - if len(symbols) != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(len(fn))): - raise ValueError("Function components must be able to be interpreted as a sympy polynomial") - self.fn = [sp.sympify(fn[i]).as_poly() for i in range(len(fn))] - self.shape = len(fn) + shape = len(fn) + else: + shape = 0 + if len(symbols) != 0 and (shape != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(shape))) and not sp.sympify(fn).as_poly(): + raise ValueError("Function argument or its components must be able to be interpreted as a sympy polynomial") + if shape != 0: + self.fn = [sp.sympify(fn[i]).as_poly() for i in range(shape)] + self.shape = shape else: self.fn = sp.sympify(fn) self.shape = 0 @@ -304,9 +313,7 @@ def __call__(self, *args): if self.shape == 0: res = sympy_to_numpy(self.fn, self.syms, args[:len(self.syms)]) else: - res = [] - for i in range(self.shape): - res += [sympy_to_numpy(self.fn[i], self.syms, args[:len(self.syms)])] + res = [sympy_to_numpy(self.fn[i], self.syms, args[:len(self.syms)]) for i in range(self.shape)] return res def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape): @@ -426,13 +433,7 @@ def convert_to_fiat(self, ref_el, interpolant_degree, value_shape=tuple()): def to_quadrature(self, arg_degree, value_shape): Qpts, Qwts = self.cell_defined_on.quadrature(self.kernel.degree(arg_degree)) Qwts = Qwts.reshape(Qwts.shape + (1,)) - dim = self.cell_defined_on.get_spatial_dimension() - if dim > 0: - bvs = np.array(self.cell_defined_on.basis_vectors()) - new_bvs = np.array(self.cell_defined_on.orient(self.pairing.orientation).basis_vectors()) - basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) - else: - basis_change = np.eye(dim) + basis_change = self.pairing.tabulate() if self.immersed and (isinstance(self.kernel, VectorKernel) or isinstance(self.kernel, BarycentricPolynomialKernel) or isinstance(self.kernel, PolynomialKernel)): def immersed(pt): @@ -461,20 +462,22 @@ def immersed(pt): pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension, value_shape) if self.immersed: - # need to compute jacobian from attachment. pts = np.array([self.cell.attachment(self.cell.id, self.cell_defined_on.id)(*pt) for pt in pts]) - # J_det = self.cell.attachment_J_det(self.cell.id, self.cell_defined_on.id) - J_det = 1 + J_det = self.cell.attachment_J_det(self.cell.id, self.cell_defined_on.id) if not np.allclose(J_det, 1): raise ValueError("Jacobian Determinant is not 1 did you do something wrong") + J_det = 1 + # if self.pairing.orientation: + # immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.pairing.orientation))[0] + # else: immersion = self.target_space.tabulate(pts, self.cell_defined_on) + # Special case - force evaluation on different orientation of entity for construction of matrix transforms + # if self.entity_o: + # immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.entity_o)) if isinstance(self.target_space, TrH1): - new_wts = wts + new_wts = wts * J_det else: new_wts = np.outer(wts * J_det, immersion) - # shape is wrong for 2d face on tet - # if isinstance(self.kernel, BarycentricPolynomialKernel) and self.kernel.shape > 1: - # new_wts = np.array([self.cell.attachment(self.cell.id, self.cell_defined_on.id)(*pt) for pt in new_wts]) else: new_wts = wts # pt dict is { pt: [(weight, component)]} diff --git a/fuse/element_construction.py b/fuse/element_construction.py index dcbd5b75..22ee94f9 100644 --- a/fuse/element_construction.py +++ b/fuse/element_construction.py @@ -254,7 +254,6 @@ def vector_basis_fns(cell, deg, rot=False, interior_only=False): All transformation groups are S1 as these are interior to the cell. """ - print(cell, deg) edge = cell.edges()[0] face = cell.d_entities(2)[0] facet_cell = edge @@ -280,7 +279,7 @@ def vector_basis_fns(cell, deg, rot=False, interior_only=False): xs1 = [DOF(L2Pairing(), BarycentricPolynomialKernel(pf_bf*new_bf(o), symbols=symbols))] dofs += [DOFGenerator(xs1, pf_grp, S1)] counter += (pf_grp).size() - print("facet dofs: ", counter) + # print("facet dofs: ", counter) counter = 0 interior_deg = deg - 2 @@ -293,7 +292,7 @@ def vector_basis_fns(cell, deg, rot=False, interior_only=False): counter += len(new_dofs) dofs += [DOFGenerator(new_dofs, S1, S1)] interior_deg = deg - 3 - print("interior facet dofs:", counter) + # print("interior facet dofs:", counter) counter = 0 basis_funcs, groups, symbols = lagrange_barycentric_basis(cell.dimension, cell.ordered_vertex_coords(), interior_deg) @@ -328,8 +327,8 @@ def vector_basis_fns(cell, deg, rot=False, interior_only=False): xs1 = [DOF(L2Pairing(), BarycentricPolynomialKernel(np.prod(symbols)*vec_bf*bf, symbols=symbols))] dofs += [DOFGenerator(xs1, grp, g2)] counter += (grp).size() - print("interior dofs:", counter) - print("end cell", cell) + # print("interior dofs:", counter) + # print("end cell", cell) return dofs diff --git a/fuse/enriched.py b/fuse/enriched.py new file mode 100644 index 00000000..4816cd16 --- /dev/null +++ b/fuse/enriched.py @@ -0,0 +1,114 @@ +import numpy as np +from fuse.triples import ElementTriple +import finat.ufl + + +class EnrichedElement(ElementTriple): + """ + Non-nodal representation of an enriched element. + + In general, FUSE element triples should be represented nodally, + however this may not be possible for all constructions. + + In particular, we need to preserve tensor product structure. + """ + + def __init__(self, A, B, flat=False, symmetric=True, matrices=True): + from fuse.tensor_products import TensorProductTriple + valid_types = (TensorProductTriple, EnrichedElement) + if not isinstance(A, valid_types) or not isinstance(B, valid_types): + raise ValueError("EnrichedElement should only be used for Tensor product elements. Use + between triples for enrichment.") + self.A = A + self.B = B + self.spaces = (A.spaces[0] + B.spaces[0], A.spaces[1], max([A.spaces[2], B.spaces[2]])) + + self.DOFGenerator = [A.DOFGenerator, B.DOFGenerator] + if A.cell.flat != B.cell.flat: + raise ValueError("Tensor products must both be flat or both not flat for enrichment.") + self.cell = A.cell + self.flat = flat + if hasattr(A, "unflat_cell"): + self.unflat_cell = A.unflat_cell + self.symmetric = symmetric + self.apply_matrices = matrices + if self.apply_matrices: + self.setup_matrices() + + self.pure_perm = not matrices + + @property + def sub_elements(self): + return [self.A, self.B] + + def get_value_shape(self): + if str(self.spaces[1]) in ("HDiv", "HCurl"): + return (self.cell.get_spatial_dimension(),) + return super().get_value_shape() + + def __repr__(self): + return "Enriched(%s, %s)" % (repr(self.A), repr(self.B)) + + def __add__(self, other): + assert self.spaces[0].set_shape == other.spaces[0].set_shape + assert str(self.spaces[1]) == str(other.spaces[1]) + return EnrichedElement(self, other, symmetric=self.symmetric and other.symmetric, + matrices=self.apply_matrices or other.apply_matrices) + + def setup_matrices(self): + if self.cell.flat and not self.symmetric: + raise NotImplementedError("Matrices for flattened cells that are not symmetric not supported") + self.A.to_ufl() + self.B.to_ufl() + oriented_mats_by_entity, flat_by_entity = self._initialise_entity_dicts(self.generate(), tensor=(not self.cell.flat)) + if self.cell.flat: + cell = self.A.unflat_cell + else: + cell = self.cell + top = cell.to_fiat().get_topology() + seen_total_dims = set() + for dim in top.keys(): + total_dim = sum(dim) if self.cell.flat else dim + if total_dim in seen_total_dims: + continue + seen_total_dims.add(total_dim) + ents = self.entity_dofs[total_dim].keys() + # comp_os = cell.component_orientations() + for e_idx, e in enumerate(ents): + ent_dofs = self.entity_dofs[total_dim][e] + if len(ent_dofs) >= 1: + sub_mat = oriented_mats_by_entity[total_dim][e_idx] + a_mat = self.A.matrices[total_dim][e_idx] + a_ent_ids = self.A.entity_dofs[total_dim][e] + b_mat = self.B.matrices[total_dim][e_idx] + b_ent_ids = self.B.entity_dofs[total_dim][e] + + for o in a_mat.keys(): + a_sub_mat = a_mat[o][np.ix_(a_ent_ids, a_ent_ids)] + b_sub_mat = b_mat[o][np.ix_(b_ent_ids, b_ent_ids)] + combined_sub_mat = np.block([[a_sub_mat, np.zeros((a_sub_mat.shape[0], b_sub_mat.shape[1]))], + [np.zeros((b_sub_mat.shape[0], a_sub_mat.shape[1])), b_sub_mat]]) + sub_mat[o][np.ix_(ent_dofs, ent_dofs)] = np.matmul(sub_mat[o][np.ix_(ent_dofs, ent_dofs)], combined_sub_mat) + + self.matrices = oriented_mats_by_entity + self.reversed_matrices = self.reverse_dof_perms(self.matrices) + + def generate(self): + a_dofs = self.A.generate() + b_dofs = self.B.generate() + numAdofs = len(a_dofs) + self.entity_dofs = {} + for dim in self.A.entity_dofs.keys(): + self.entity_dofs[dim] = {} + for ent in self.A.entity_dofs[dim]: + self.entity_dofs[dim][ent] = self.A.entity_dofs[dim][ent] + [b_dof + numAdofs for b_dof in self.B.entity_dofs[dim][ent]] + return a_dofs + b_dofs + + def to_ufl(self): + ufl_sub_elements = [e.to_ufl() for e in self.sub_elements] + return finat.ufl.EnrichedElement(*ufl_sub_elements, triple=self) + + def flatten(self): + return EnrichedElement(self.A.flatten(), self.B.flatten(), flat=True, symmetric=self.symmetric, matrices=self.matrices) + + def unflatten(self): + return EnrichedElement(self.A.unflatten(), self.B.unflatten(), flat=False, symmetric=self.symmetric, matrices=self.matrices) diff --git a/fuse/groups.py b/fuse/groups.py index 0ada9d69..84c1b805 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -65,9 +65,15 @@ def compute_perm(self, base_val=None): return val, val_list def numeric_rep(self): + """ Uses a standard formula to number permutations in the group. + For the case where this doesn't automatically number from 0..n (ie the group is not the full symmetry group), + a mapping is constructed on group creation""" identity = self.group.identity.vertex_order_form m_array = self.vertex_order_form - return orientation_value(identity, m_array) + val = orientation_value(identity, m_array) + if self.group.group_rep_numbering is not None: + return self.group.group_rep_numbering[val] + return val def __eq__(self, x): assert isinstance(x, GroupMemberRep) @@ -197,6 +203,11 @@ def __init__(self, perm_list, cell=None, name=None): counter += 1 # self._members = sorted(self._members, key=lambda g: g.numeric_rep()) + self.group_rep_numbering = None + numeric_reps = [m.numeric_rep() for m in self.members()] + if sorted(numeric_reps) != list(range(len(numeric_reps))): + self.group_rep_numbering = {a: b for a, b in zip(sorted(numeric_reps), list(range(len(numeric_reps))))} + def add_cell(self, cell): return PermutationSetRepresentation(self.perm_list, cell=cell, name=self.name) @@ -297,6 +308,16 @@ def __repr__(self): return self.name + str(self.size()) return "GS" + str(self.size()) + def _to_dict(self): + return {"members": [m.perm.array_form for m in self._members]} + + def dict_id(self): + return "PermutationSet" + + def _from_dict(o_dict): + perm_set = [Permutation(m) for m in o_dict["members"]] + return PermutationSetRepresentation(perm_set) + class GroupRepresentation(PermutationSetRepresentation): """ @@ -336,6 +357,11 @@ def __init__(self, base_group, cell=None, name=None): self.identity = p_rep counter += 1 + self.group_rep_numbering = None + numeric_reps = [m.numeric_rep() for m in self.members()] + if sorted(numeric_reps) != list(range(len(numeric_reps))): + self.group_rep_numbering = {a: b for a, b in zip(sorted(numeric_reps), list(range(len(numeric_reps))))} + # this order produces simpler generator lists # self.generators.reverse() diff --git a/fuse/serialisation.py b/fuse/serialisation.py index c41e6423..fcd8e66c 100644 --- a/fuse/serialisation.py +++ b/fuse/serialisation.py @@ -31,6 +31,7 @@ def __init__(self): "Edge": Edge, "Triple": ElementTriple, "Group": GroupRepresentation, + "PermutationSet": PermutationSetRepresentation, "SobolevSpace": ElementSobolevSpace, "InterpolationSpace": InterpolationSpace, "PolynomialSpace": PolynomialSpace, @@ -40,8 +41,11 @@ def __init__(self): "DOFGen": DOFGenerator, "Delta": DeltaPairing, "L2Inner": L2Pairing, + "BarycentricPolynomialKernel": BarycentricPolynomialKernel, + "VectorKernel": VectorKernel, "PolynomialKernel": PolynomialKernel, "PointKernel": PointKernel, + "ComponentKernel": ComponentKernel, "Trace": Trace } @@ -65,6 +69,10 @@ def encode_traverse(self, obj, path=[]): res_array[i] = dfs_res return res_array + # Some sympy objects are not hashable, so we must accept we may serialise them twice. + if isinstance(obj, sp.core.containers.Tuple) or isinstance(obj, sp.Expr) or isinstance(obj, sp.Matrix) or isinstance(obj, sp.Poly): + return "Sympy/" + sp.srepr(obj) + if obj in self.seen_objs.keys(): return self.seen_objs[obj]["id"] @@ -75,9 +83,6 @@ def encode_traverse(self, obj, path=[]): self.store_obj(obj, obj.dict_id(), obj_id, obj_dict, path) return obj.dict_id() + "/" + str(obj_id) - if isinstance(obj, sp.core.containers.Tuple) or isinstance(obj, sp.Expr): - return "Sympy/" + sp.srepr(obj) - return obj def get_id(self, obj): diff --git a/fuse/spaces/element_sobolev_spaces.py b/fuse/spaces/element_sobolev_spaces.py index 2a3aa4fb..b5964d9d 100644 --- a/fuse/spaces/element_sobolev_spaces.py +++ b/fuse/spaces/element_sobolev_spaces.py @@ -1,4 +1,5 @@ from functools import total_ordering +from ufl.sobolevspace import H1, HDiv, HCurl, L2, H2 @total_ordering @@ -55,6 +56,9 @@ def __init__(self, cell): def __repr__(self): return "H1" + def to_ufl(self): + return H1 + class CellHDiv(ElementSobolevSpace): @@ -64,6 +68,9 @@ def __init__(self, cell): def __repr__(self): return "HDiv" + def to_ufl(self): + return HDiv + class CellHCurl(ElementSobolevSpace): @@ -73,6 +80,9 @@ def __init__(self, cell): def __repr__(self): return "HCurl" + def to_ufl(self): + return HCurl + class CellH2(ElementSobolevSpace): @@ -82,6 +92,9 @@ def __init__(self, cell): def __repr__(self): return "H2" + def to_ufl(self): + return H2 + class CellL2(ElementSobolevSpace): @@ -90,3 +103,6 @@ def __init__(self, cell): def __repr__(self): return "L2" + + def to_ufl(self): + return L2 diff --git a/fuse/spaces/polynomial_spaces.py b/fuse/spaces/polynomial_spaces.py index 422cf053..8e5fa900 100644 --- a/fuse/spaces/polynomial_spaces.py +++ b/fuse/spaces/polynomial_spaces.py @@ -1,13 +1,16 @@ from FIAT.polynomial_set import ONPolynomialSet +from FIAT.expansions import morton_index2, morton_index3 from FIAT.quadrature_schemes import create_quadrature from FIAT.reference_element import cell_to_simplex from FIAT import expansions, polynomial_set, reference_element from itertools import chain -from fuse.utils import tabulate_sympy, max_deg_sp_mat +from fuse.utils import tabulate_sympy, max_deg_sp_expr import sympy as sp import numpy as np from functools import total_ordering +morton_index = {2: morton_index2, 3: morton_index3} + @total_ordering class PolynomialSpace(object): @@ -47,7 +50,6 @@ def degree(self): return self.maxdegree def to_ON_polynomial_set(self, ref_el, k=None): - # how does super/sub degrees work here if not isinstance(ref_el, reference_element.Cell): ref_el = ref_el.to_fiat() sd = ref_el.get_spatial_dimension() @@ -56,18 +58,25 @@ def to_ON_polynomial_set(self, ref_el, k=None): shape = (sd,) else: shape = tuple() + base_ON = ONPolynomialSet(ref_el, self.maxdegree, shape, scale="orthonormal") + indices = None if self.mindegree > 0: - base_ON = ONPolynomialSet(ref_el, self.maxdegree, shape, scale="orthonormal") dimPmin = expansions.polynomial_dimension(ref_el, self.mindegree) dimPmax = expansions.polynomial_dimension(ref_el, self.maxdegree) if self.set_shape: indices = list(chain(*(range(i * dimPmin, i * dimPmax) for i in range(sd)))) else: indices = list(range(dimPmin, dimPmax)) - restricted_ON = base_ON.take(indices) - return restricted_ON - return ONPolynomialSet(ref_el, self.maxdegree, shape, scale="orthonormal") + + if self.contains != self.maxdegree and self.contains != -1: + indices = [morton_index[sd](p, q) for p in range(self.contains + 1) for q in range(self.contains + 1)] + + if indices is None: + return base_ON + + restricted_ON = base_ON.take(indices) + return restricted_ON def __repr__(self): res = "" @@ -87,9 +96,7 @@ def __mul__(self, x): the sympy object on the right. This is due to Sympy's implementation of __mul__ not passing to this handler as it should. """ - if isinstance(x, sp.Symbol): - return ConstructedPolynomialSpace([x], [self]) - elif isinstance(x, sp.Matrix): + if isinstance(x, sp.Symbol) or isinstance(x, sp.Expr) or isinstance(x, sp.Matrix): return ConstructedPolynomialSpace([x], [self]) else: raise TypeError(f'Cannot multiply a PolySpace with {type(x)}') @@ -126,6 +133,9 @@ def __hash__(self): def restrict(self, mindegree, maxdegree): return PolynomialSpace(maxdegree, contains=-1, mindegree=mindegree, set_shape=self.set_shape) + def to_vector(self): + return PolynomialSpace(self.maxdegree, self.contains, self.mindegree, set_shape=True) + def _to_dict(self): return {"set_shape": self.set_shape, "min": self.mindegree, "contains": self.contains, "max": self.maxdegree} @@ -148,7 +158,7 @@ def __init__(self, weights, spaces): self.weights = weights self.spaces = spaces - weight_degrees = [0 if not (isinstance(w, sp.Expr) or isinstance(w, sp.Matrix)) else max_deg_sp_mat(w) for w in self.weights] + weight_degrees = [0 if not (isinstance(w, sp.Expr) or isinstance(w, sp.Matrix)) else max_deg_sp_expr(w) for w in self.weights] maxdegree = max([space.maxdegree + w_deg for space, w_deg in zip(spaces, weight_degrees)]) mindegree = min([space.mindegree + w_deg for space, w_deg in zip(spaces, weight_degrees)]) @@ -163,37 +173,49 @@ def to_ON_polynomial_set(self, ref_el): if not isinstance(ref_el, reference_element.Cell): ref_el = ref_el.to_fiat() k = max([s.maxdegree for s in self.spaces]) - space_poly_sets = [s.to_ON_polynomial_set(ref_el) for s in self.spaces] sd = ref_el.get_spatial_dimension() ref_el = cell_to_simplex(ref_el) - if all([w == 1 for w in self.weights]): - weighted_sets = space_poly_sets - # otherwise have to work on this through tabulation - Q = create_quadrature(ref_el, 2 * (k + 1)) - Qpts, Qwts = Q.get_points(), Q.get_weights() weighted_sets = [] - for (space, w) in zip(space_poly_sets, self.weights): + for (s, w) in zip(self.spaces, self.weights): + space = s.to_ON_polynomial_set(ref_el) + if s.set_shape: + shape = (sd,) + else: + shape = tuple() if not (isinstance(w, sp.Expr) or isinstance(w, sp.Matrix)): weighted_sets.append(space) else: - w_deg = max_deg_sp_mat(w) - Pkpw = ONPolynomialSet(ref_el, space.degree + w_deg, scale="orthonormal") - vec_Pkpw = ONPolynomialSet(ref_el, space.degree + w_deg, (sd,), scale="orthonormal") + if isinstance(w, sp.Expr): + w = sp.Matrix([[w]]) + vec = False + else: + vec = True + w_deg = max_deg_sp_expr(w) + Q = create_quadrature(ref_el, 2 * (k + w_deg + 1)) + Qpts, Qwts = Q.get_points(), Q.get_weights() + Pkpw = ONPolynomialSet(ref_el, space.degree + w_deg, shape, scale="orthonormal") + # vec_Pkpw = ONPolynomialSet(ref_el, space.degree + w_deg, (sd,), scale="orthonormal") space_at_Qpts = space.tabulate(Qpts)[(0,) * sd] Pkpw_at_Qpts = Pkpw.tabulate(Qpts)[(0,) * sd] tabulated_expr = tabulate_sympy(w, Qpts).T - scaled_at_Qpts = space_at_Qpts[:, None, :] * tabulated_expr[None, :, :] + if s.set_shape or vec: + scaled_at_Qpts = space_at_Qpts[:, None, :] * tabulated_expr[None, :, :] + else: + scaled_at_Qpts = space_at_Qpts[:, None, :] * tabulated_expr[None, :, :] + scaled_at_Qpts = scaled_at_Qpts.squeeze() PkHw_coeffs = np.dot(np.multiply(scaled_at_Qpts, Qwts), Pkpw_at_Qpts.T) + if len(PkHw_coeffs.shape) == 1: + PkHw_coeffs = PkHw_coeffs.reshape(1, -1) weighted_sets.append(polynomial_set.PolynomialSet(ref_el, space.degree + w_deg, space.degree + w_deg, - vec_Pkpw.get_expansion_set(), + Pkpw.get_expansion_set(), PkHw_coeffs)) combined_sets = weighted_sets[0] for i in range(1, len(weighted_sets)): @@ -212,6 +234,9 @@ def __add__(self, x): s.extend([x]) return ConstructedPolynomialSpace(w, s) + def to_vector(self): + return ConstructedPolynomialSpace(self.weights, [space.to_vector() for space in self.spaces]) + def _to_dict(self): super_dict = super(ConstructedPolynomialSpace, self)._to_dict() super_dict["spaces"] = self.spaces diff --git a/fuse/tensor_products.py b/fuse/tensor_products.py index cf16e247..8b7a63f5 100644 --- a/fuse/tensor_products.py +++ b/fuse/tensor_products.py @@ -1,54 +1,337 @@ -from fuse.triples import ElementTriple +from fuse.triples import ElementTriple, compute_form_degree +from fuse.traces import TrHCurl, TrHDiv +from fuse.spaces.element_sobolev_spaces import CellHDiv, CellHCurl from fuse.cells import TensorProductPoint -from finat.ufl import TensorProductElement, FuseElement +from fuse.enriched import EnrichedElement +import numpy as np +from finat.ufl import TensorProductElement, FuseElement, HDivElement, HCurlElement +from itertools import product +from functools import reduce +from collections import defaultdict -def tensor_product(A, B): - if not (isinstance(A, ElementTriple) and isinstance(B, ElementTriple)): - raise ValueError("Both components of Tensor Product need to be a Fuse Triple.") - return TensorProductTriple(A, B) +def tensor_product(*factors, matrices=True): + if not all(isinstance(f, ElementTriple) for f in factors): + raise ValueError("All components of Tensor Product need to be a Fuse Triple.") + return TensorProductTriple(*factors, matrices=matrices) + + +def symmetric_tensor_product(*factors, matrices=True): + if not all(isinstance(f, ElementTriple) for f in factors): + raise ValueError("All components of Tensor Product need to be a Fuse Triple.") + return TensorProductTriple(*factors, matrices=matrices, symmetric=True) + + +def flatten_dictionary(tensor_dict): + counters = {} + flat_dict = {} + for dim in tensor_dict.keys(): + total_dim = sum(dim) + if total_dim not in counters.keys(): + counters[total_dim] = 0 + flat_dict[total_dim] = {} + for i in range(len(tensor_dict[dim].keys())): + flat_dict[total_dim][i + counters[total_dim]] = tensor_dict[dim][i] + counters[total_dim] += len(tensor_dict[dim].keys()) + return flat_dict class TensorProductTriple(ElementTriple): - def __init__(self, A, B, flat=False): - self.A = A - self.B = B + def __init__(self, *factors, flat=False, symmetric=True, matrices=True): + self.factors = factors self.spaces = [] - for (a, b) in zip(self.A.spaces, self.B.spaces): - self.spaces.append(a if a >= b else b) + for i in range(len(self.factors[0].spaces)): + self.spaces.append(max(f.spaces[i] for f in self.factors)) - self.DOFGenerator = [A.DOFGenerator, B.DOFGenerator] - self.cell = TensorProductPoint(A.cell, B.cell) + self.DOFGenerator = [f.DOFGenerator for f in self.factors] + self.cell = TensorProductPoint(*[f.cell for f in factors]) + self.symmetric = symmetric self.flat = flat - self.apply_matrices = False + if self.flat: + self.unflat_cell = self.cell + self.cell = self.cell.flatten() + self.dofs = self.generate() + + # Subclasses (HDiv, HCurl) set self.mat_transformer before calling + # this constructor; only default it here if they haven't. + self.mat_transformer = getattr(self, "mat_transformer", None) + self.apply_matrices = matrices + if self.apply_matrices: + self.setup_matrices() + self.pure_perm = not matrices + + @property def sub_elements(self): - return [self.A, self.B] + return self.factors def __repr__(self): - return "TensorProd(%s, %s)" % (repr(self.A), repr(self.B)) + return f"TensorProd({','.join(['{}' for f in self.factors])})".format(*(repr(f) for f in self.factors)) + + def _entity_associations(self, dofs, overall=True): + return self.entity_assocs, None, None def setup_matrices(self): - oriented_mats_by_entity, flat_by_entity = self._initialise_entity_dicts(self.A.generate() + self.B.generate()) - breakpoint() - for dim in range(self.cell.dimension): - for dimA in range(self.A.cell.dimension): - pass - for dimB in range(self.B.cell_dimension): - pass - return super().setup_matrices() + if self.cell.flat and not self.symmetric: + raise NotImplementedError("Matrices for flattened cells that are not symmetric not supported") + for f in self.factors: + f.to_ufl() + oriented_mats_by_entity, flat_by_entity = self._initialise_entity_dicts(self.generate(), tensor=True) + if self.flat: + cell = self.unflat_cell + else: + cell = self.cell + top = cell.to_fiat().get_topology() + if len(self.factors) == 2: + for dim in top.keys(): + total_dim = sum(dim) if self.flat else dim + f_ents = [f.cell.get_topology()[d].keys() for f, d in zip(self.factors, dim)] + ents = list(product(*(f_ents))) + comp_os = cell.component_orientations() + for e, sub_ents in enumerate(ents): + ent_dofs = self.entity_dofs[total_dim][self.ent_mapping[dim][sub_ents]] + if len(ent_dofs) >= 1: + sub_mat = oriented_mats_by_entity[dim][e] + mats = [f.matrices[d][ent] for f, d, ent in zip(self.factors, dim, sub_ents)] + ent_ids = [f.entity_dofs[d][ent] for f, d, ent in zip(self.factors, dim, sub_ents)] + os = list(product(*([mat.keys() for mat in mats]))) + for o in os: + sub_mats = [mat[o_f][np.ix_(ent_id, ent_id)] for mat, o_f, ent_id in zip(mats, o, ent_ids)] + if self.mat_transformer is not None: + o_classes = [f.cell.group.get_member_by_val(o_f) for f, o_f in zip(self.factors, o)] + combined_sub_mat = self.mat_transformer(*sub_mats, o_classes) + else: + combined_sub_mat = reduce(lambda acc, x: np.kron(acc, x), sub_mats) + new_o = comp_os[dim][o] + if new_o in sub_mat.keys(): + sub_mat[new_o][np.ix_(ent_dofs, ent_dofs)] = np.matmul(sub_mat[new_o][np.ix_(ent_dofs, ent_dofs)], combined_sub_mat) + # sub_mat[new_o][np.ix_(ent_dofs, ent_dofs)] = np.eye(np.matmul(sub_mat[new_o][np.ix_(ent_dofs, ent_dofs)], combined_sub_mat).shape[0]) + + if self.cell.flat: + oriented_mats_by_entity = flatten_dictionary(oriented_mats_by_entity) + + self.matrices = oriented_mats_by_entity + self.reversed_matrices = self.reverse_dof_perms(self.matrices) + + def generate(self): + dofs = [f.generate() for f in self.factors] + ent_assocs = [f._entity_associations(dofs_f, overall=False)[0] for f, dofs_f in zip(self.factors, dofs)] + if self.flat: + top = self.unflat_cell.to_fiat().get_topology() + else: + top = self.cell.to_fiat().get_topology() + self.entity_dofs = defaultdict(dict) + self.ent_mapping = {} + self.entity_assocs = defaultdict(dict) + self.dof_ids = {} + dofs = [] + ent_counter = defaultdict(lambda: 0) + dof_counter = 0 + for dim in top.keys(): + total_dim = sum(dim) if self.flat else dim + ents = [ent_assoc[d].keys() for ent_assoc, d in zip(ent_assocs, dim)] + # if total_dim not in self.entity_dofs.keys(): + # self.entity_dofs[total_dim] = {} + # self.entity_assocs[total_dim] = {} + self.ent_mapping[dim] = {} + ent_list = [] + for i, ent in enumerate(list(product(*ents))): + self.ent_mapping[dim][ent] = i + ent_counter[total_dim] if self.flat else ent + self.entity_dofs[total_dim][self.ent_mapping[dim][ent]] = [] + ent_list += [ent] + for es in ent_list: + e_dofs = [[d for dofs in ent_assoc[d][e].values() for d in dofs] for ent_assoc, d, e in zip(ent_assocs, dim, es)] + new_dofs = list(product(*e_dofs)) + dofs += new_dofs + dof_gens = "(" + "*".join([",".join(list(ent_assoc[d][e].keys())) for ent_assoc, d, e in zip(ent_assocs, dim, es)]) + ")" + self.entity_assocs[total_dim][self.ent_mapping[dim][es]] = {dof_gens: new_dofs} + self.entity_dofs[total_dim][self.ent_mapping[dim][es]] += [i + dof_counter for i in range(len(new_dofs))] + for d in new_dofs: + self.dof_ids[d] = dof_counter + dof_counter += 1 + ent_counter[total_dim] += 1 + + return dofs def to_ufl(self): + ufl_sub_elements = [e.to_ufl() for e in self.sub_elements] if self.flat: - return FuseElement(self, self.cell.flatten().to_ufl()) - ufl_sub_elements = [e.to_ufl() for e in self.sub_elements()] - # self.setup_matrices() - # breakpoint() - return TensorProductElement(*ufl_sub_elements, cell=self.cell.to_ufl()) + return FuseElement(self, self.cell.to_ufl()) + return TensorProductElement(*ufl_sub_elements, cell=self.cell.to_ufl(), triple=self) + + def __add__(self, other): + # assert self.cell == other.cell + assert self.spaces[0].set_shape == other.spaces[0].set_shape + assert str(self.spaces[1]) == str(other.spaces[1]) + + return EnrichedElement(self, other, symmetric=self.symmetric and other.symmetric, matrices=self.apply_matrices or other.apply_matrices) + + def flatten(self): + return TensorProductTriple(*self.factors, flat=True, symmetric=self.symmetric, matrices=self.apply_matrices) + + def unflatten(self): + return TensorProductTriple(*self.factors, flat=False, symmetric=self.symmetric, matrices=self.apply_matrices) + + +def compute_matrix_transform(trace, cell, o): + dim = cell.get_spatial_dimension() + bvs = np.array(cell.basis_vectors()) + new_bvs = np.array(cell.orient(~o).basis_vectors()) + if bvs.shape[0] != dim: + # basis_vectors() gives one vector per non-reference vertex, which + # only forms a square (invertible) basis for simplices (vertex + # count == dim + 1). For non-simplex cells (e.g. a quadrilateral + # face of a hex), the vectors from the reference vertex to its two + # adjacent vertices (the first `dim` entries) already form a valid + # basis; later entries are redundant (e.g. diagonals). + bvs = bvs[:dim] + new_bvs = new_bvs[:dim] + basis_change = np.matmul(new_bvs, np.linalg.inv(bvs)) + # if len(ent_dofs_ids) == basis_change.shape[0]: + # sub_mat = basis_change + # elif len(dof_gen_class[dim].g2.members()) == 2 and len(ent_dofs_ids) == 1: + # # equivalently g1 trivial + # sub_mat = trace.manipulate_basis(basis_change) + # else: + # case where value change is a restriction of the full transformation of the basis + value_change = trace(cell).manipulate_basis(basis_change) + # sub_mat = np.kron((~o).matrix_form(), value_change) + return value_change + + +class HDiv(TensorProductTriple): + + def __init__(self, tensor_element): + self.base_element = tensor_element + self.gem_transformer, self.mat_transformer = self.select_fuse_hdiv_transformer(tensor_element) + self.trace = TrHDiv + super(HDiv, self).__init__(*tensor_element.factors, flat=tensor_element.flat, symmetric=tensor_element.symmetric, matrices=tensor_element.matrices) + self.spaces = (self.spaces[0], CellHDiv(self.cell), self.spaces[2]) + + def to_ufl(self): + return HDivElement(super(HDiv, self).to_ufl(), transform=self.gem_transformer) + + def repr(self): + return "HDiv(" + super(HDiv, self).repr() + ")" + + def select_fuse_hdiv_transformer(self, element): + # Assume: something x interval + import gem + assert len(element.sub_elements) == 2 + assert element.sub_elements[1].cell.get_shape() == 1 + # Globally consistent edge orientations of the reference + # quadrilateral: rightward horizontally, upward vertically. + # Their rotation by 90 degrees anticlockwise is interpreted as the + # positive direction for normal vectors. + ks = tuple(compute_form_degree(fe.cell, fe.spaces) for fe in element.sub_elements) + dims = tuple(fe.cell.get_spatial_dimension() for fe in element.sub_elements) + transform = lambda cell, o: compute_matrix_transform(self.trace, cell, o) + if ks == (0, 1) and dims == (1, 1): + # Both factors are 1D intervals (2D quad case). Make the + # scalar value the right hand rule normal on the y-aligned + # edges. + cell = element.sub_elements[1].cell + bv = cell.basis_vectors()[0][0] + mats = lambda m_a, m_b, o: np.kron(transform(cell, o[1]) @ m_a, m_b) + return lambda v: [gem.Product(gem.Literal(bv), v), gem.Zero()], mats + elif ks == (1, 0) and dims == (1, 1): + # Both factors are 1D intervals (2D quad case). Make the + # scalar value the upward-pointing normal on the x-aligned + # edges. + cell = element.sub_elements[0].cell + bv = cell.basis_vectors()[0][0] + return lambda v: [gem.Zero(), gem.Product(gem.Literal(bv), v)], lambda m_a, m_b, o: np.kron(m_a, transform(cell, o[0]) @ m_b) + elif ks == (2, 0) and dims == (2, 1): + # First factor is a plain (unwrapped) scalar DG element on a + # 2D base cell, second is a CG interval + cell = element.sub_elements[0].cell + mats = lambda m_a, m_b, o: np.kron(m_a, transform(cell, o[0]) * m_b) + return lambda v: [gem.Zero(), gem.Zero(), v], mats + elif ks == (1, 1) and dims == (2, 1) and str(element.sub_elements[0].spaces[1]) == "HDiv": + # First factor is an already H(div)-wrapped 2D element (the + # in-plane RT part), second is a DG interval: the horizontal + # (x, y) components of a 3D H(div) field. + cell = element.sub_elements[1].cell + mats = lambda m_a, m_b, o: np.kron(m_a, transform(cell, o[1]) * m_b) + return lambda v: [gem.Indexed(v, (0,)), gem.Indexed(v, (1,)), gem.Zero()], mats + elif ks == (1, 1) and dims == (2, 1) and str(element.sub_elements[0].spaces[1]) == "HCurl": + # First factor is an already H(curl)-wrapped 2D element, + # second is a DG interval: rotate the tangential 2-vector 90 + # degrees anticlockwise into a 3-vector and pad. + cell = element.sub_elements[1].cell + mats = lambda m_a, m_b, o: np.kron(m_a, transform(cell, o[1]) * m_b) + return lambda v: [gem.Indexed(v, (1,)), gem.Product(gem.Literal(-1), gem.Indexed(v, (0,))), gem.Zero()], mats + else: + raise NotImplementedError("Unexpected original mapping!") + assert False, "Unexpected form degree combination!" + + def flatten(self): + return HDiv(self.base_element.flatten()) + + def unflatten(self): + return HDiv(self.base_element.unflatten()) + + +class HCurl(TensorProductTriple): + + def __init__(self, tensor_element): + self.base_element = tensor_element + self.gem_transformer, self.mat_transformer = self.select_fuse_hcurl_transformer(tensor_element) + self.trace = TrHCurl + super(HCurl, self).__init__(*tensor_element.factors, flat=tensor_element.flat, symmetric=tensor_element.symmetric, matrices=tensor_element.matrices) + self.spaces = (self.spaces[0], CellHCurl(self.cell), self.spaces[2]) + + def to_ufl(self): + return HCurlElement(super(HCurl, self).to_ufl(), self.gem_transformer) + + def repr(self): + return "HCurl(" + super(HCurl, self).repr() + ")" + + def select_fuse_hcurl_transformer(self, element): + import gem + # Assume: something x interval + assert len(element.sub_elements) == 2 + assert element.sub_elements[1].cell.get_shape() == 1 + + dim = element.cell.get_spatial_dimension() + ks = tuple(compute_form_degree(fe.cell, fe.spaces) for fe in element.sub_elements) + dims = tuple(fe.cell.get_spatial_dimension() for fe in element.sub_elements) + transform = lambda cell, o: compute_matrix_transform(self.trace, cell, o) + if all(str(fe.spaces[1]) == "H1" or str(fe.spaces[1]) == "L2" for fe in element.sub_elements) and dims == (1, 1): # affine mapping, both factors 1D intervals (2D quad case) + if ks == (1, 0): + # Can only be 2D. Make the scalar value the + # tangential following the cell edge direction on the x-aligned edges. + cell = element.sub_elements[0].cell + bv = element.sub_elements[0].cell.basis_vectors()[0][0] + mats = lambda m_a, m_b, o: np.kron(transform(cell, o[0]) @ m_a, m_b) + return lambda v: [gem.Product(gem.Literal(bv), v), gem.Zero()], mats + elif ks == (0, 1): + # Can be any spatial dimension. Make the scalar value the + # tangential following the cell edge direction . + cell = element.sub_elements[1].cell + bv = element.sub_elements[1].cell.basis_vectors()[0][0] + mats = lambda m_a, m_b, o: np.kron(m_a, transform(cell, o[1]) @ m_b) + return lambda v: [gem.Zero()] * (dim - 1) + [gem.Product(gem.Literal(bv), v)], mats + else: + assert False + elif ks == (1, 0) and dims == (2, 1) and str(element.sub_elements[0].spaces[1]) == "HCurl": + # First factor is an already H(curl)-wrapped 2D element (an + # in-plane tangential edge component), second is a CG interval + mats = lambda m_a, m_b, o: np.kron(m_a, m_b) + return lambda v: [gem.Indexed(v, (0,)), gem.Indexed(v, (1,)), gem.Zero()], mats + elif ks == (0, 1) and dims == (2, 1) and str(element.sub_elements[0].spaces[1]) == "H1": + # First factor is a plain (unwrapped) bilinear (Q1) scalar + # element on a 2D base cell, second is a DG interval + cell = element.sub_elements[1].cell + mats = lambda m_a, m_b, o: np.kron(m_a, transform(cell, o[1]) * m_b) + return lambda v: [gem.Zero(), gem.Zero(), v], mats + else: + raise NotImplementedError("Unexpected original mapping!") + assert False, "Unexpected original mapping!" def flatten(self): - return TensorProductTriple(self.A, self.B, flat=True) + return HCurl(self.base_element.flatten()) def unflatten(self): - return TensorProductTriple(self.A, self.B, flat=False) + return HCurl(self.base_element.unflatten()) diff --git a/fuse/triples.py b/fuse/triples.py index d910bd71..3264b77d 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -7,6 +7,8 @@ from FIAT.dual_set import DualSet from FIAT.finite_element import CiarletElement from FIAT.reference_element import ufc_cell +from functools import cache +from itertools import product import matplotlib as mpl mpl.use('Agg') import matplotlib.pyplot as plt @@ -15,7 +17,21 @@ import warnings import numpy as np import scipy -from functools import cache + + +def compute_form_degree(cell, spaces): + if str(spaces[1]) == "L2": + return cell.dimension + elif str(spaces[1]) == "H1": + return 0 + if cell.dimension < 2: + raise ValueError(f"Cells of dimension {cell.dimension} can only be 0 or 1 forms") + if cell.dimension == 2: + return 1 + elif str(spaces[1]) == "HDiv": + return 1 + elif str(spaces[1]) == "HCurl": + return 2 class ElementTriple(): @@ -47,6 +63,7 @@ def __init__(self, cell, spaces, dof_gen, perm=True): self.spaces = tuple(cell_spaces) self.DOFGenerator = dof_gen self.flat = False + self.symmetric = True self.ref_el = None @@ -56,15 +73,15 @@ def __init__(self, cell, spaces, dof_gen, perm=True): def setup_ids_and_nodes(self): dofs = self.generate() - degree = self.spaces[0].degree() + 1 + degree = self.degree value_shape = self.get_value_shape() top = self.ref_el.get_topology() min_ids = self.cell.get_starter_ids() - entity_ids = {} + entity_dofs = {} nodes = [] for dim in sorted(top): - entity_ids[dim] = {i: [] for i in top[dim]} + entity_dofs[dim] = {i: [] for i in top[dim]} self.dof_id_to_fiat_id = {} entities = [(dim, entity) for dim in sorted(top) for entity in sorted(top[dim])] @@ -74,17 +91,17 @@ def setup_ids_and_nodes(self): for i in range(len(dofs)): if entity[1] == dofs[i].cell_defined_on.id - min_ids[dim]: self.dof_id_to_fiat_id[dofs[i].id] = counter - entity_ids[dim][dofs[i].cell_defined_on.id - min_ids[dim]].append(counter) + entity_dofs[dim][dofs[i].cell_defined_on.id - min_ids[dim]].append(counter) nodes.append(dofs[i].convert_to_fiat(self.ref_el, degree, value_shape)) counter += 1 self.nodes = nodes # for i in range(4): - # entity_ids[2][i] = [entity_ids[2][i][-1]] + entity_ids[2][i][:-1] - return entity_ids, nodes + # entity_dofs[2][i] = [entity_dofs[2][i][-1]] + entity_dofs[2][i][:-1] + return entity_dofs, nodes def setup_matrices(self): - # self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) - matrices, entity_perms, pure_perm = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) + # self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_dofs, self.nodes, self.poly_set) + matrices, entity_perms, pure_perm = self.make_dof_perms(self.ref_el, self.entity_dofs, self.nodes, self.poly_set) reversed_matrices = self.reverse_dof_perms(matrices) if self.perm: self.pure_perm = pure_perm @@ -97,7 +114,6 @@ def setup_matrices(self): self.apply_matrices = True self.entity_perms = entity_perms - self.entity_perms = None return matrices, reversed_matrices def __repr__(self): @@ -143,7 +159,7 @@ def get_dof_info(self, dof, tikz=True): return center, colours[tikz][dof.cell_defined_on.dimension] def get_value_shape(self): - # TODO Shape should be specificed somewhere else probably + # TODO Shape should be specified somewhere else probably if self.spaces[0].set_shape: return (self.cell.get_spatial_dimension(),) else: @@ -154,21 +170,22 @@ def to_ufl(self): # set up for eventual conversion to FIAT if not already done self.ref_el = self.cell.to_fiat() self.poly_set = self.spaces[0].to_ON_polynomial_set(self.ref_el) - self.entity_ids, self.nodes = self.setup_ids_and_nodes() + self.entity_dofs, self.nodes = self.setup_ids_and_nodes() self.matrices, self.reversed_matrices = self.setup_matrices() return FuseElement(self) def to_fiat(self): # call this to ensure set up is complete self.to_ufl() - form_degree = 1 if self.spaces[0].set_shape else 0 + # form_degree = 1 if self.spaces[0].set_shape else 0 + form_degree = compute_form_degree(self.cell, self.spaces) degree = self.spaces[0].degree() # sanity check that the dofs span the space - original_V, original_basis = self.compute_dense_matrix(self.ref_el, self.entity_ids, self.nodes, self.poly_set) + original_V, original_basis = self.compute_dense_matrix(self.ref_el, self.entity_dofs, self.nodes, self.poly_set) if self.pure_perm: - dual = DualSet(self.nodes, self.ref_el, self.entity_ids, self.entity_perms) + dual = DualSet(self.nodes, self.ref_el, self.entity_dofs, self.entity_perms) else: - dual = DualSet(self.nodes, self.ref_el, self.entity_ids) + dual = DualSet(self.nodes, self.ref_el, self.entity_dofs) return CiarletElement(self.poly_set, dual, degree, form_degree) def to_tikz(self, show=True, scale=3): @@ -260,8 +277,8 @@ def plot(self, filename="temp.png"): else: raise ValueError("Plotting not supported in this dimension") - def compute_dense_matrix(self, ref_el, entity_ids, nodes, poly_set): - dual = DualSet(nodes, ref_el, entity_ids) + def compute_dense_matrix(self, ref_el, entity_dofs, nodes, poly_set): + dual = DualSet(nodes, ref_el, entity_dofs) old_coeffs = poly_set.get_coeffs() dualmat = dual.to_riesz(poly_set) @@ -280,7 +297,7 @@ def compute_dense_matrix(self, ref_el, entity_ids, nodes, poly_set): raise np.linalg.LinAlgError("Singular Vandermonde matrix") return A, new_coeffs_flat - def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): + def make_entity_dense_matrices(self, ref_el, entity_dofs, nodes, poly_set): raise NotImplementedError("This should be deprecated") degree = self.spaces[0].degree() min_ids = self.cell.get_starter_ids() @@ -297,7 +314,7 @@ def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): # dof_ids = [self.dof_id_to_fiat_id[d.id] for d in self.generate() if d.cell_defined_on == e] dof_ids = [d.id for d in self.generate() if d.cell_defined_on == e] # res_dict[dim][e_id][0] = np.eye(len(dof_ids)) - original_V, original_basis = self.compute_dense_matrix(ref_el, entity_ids, nodes, poly_set) + original_V, original_basis = self.compute_dense_matrix(ref_el, entity_dofs, nodes, poly_set) for g in self.cell.group.members(): permuted_e, permuted_g = self.cell.permute_entities(g, dim)[e_id] @@ -312,7 +329,7 @@ def make_mat(perm_g): # , entity_o=perm_g new_nodes = [d(g, entity_o=perm_g).convert_to_fiat(ref_el, degree, self.get_value_shape()) if d.cell_defined_on == e else d.convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] # new_nodes = [d(g).convert_to_fiat(ref_el, degree, self.get_value_shape()) if d.cell_defined_on == e else d.convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] - transformed_V, transformed_basis = self.compute_dense_matrix(ref_el, entity_ids, new_nodes, poly_set) + transformed_V, transformed_basis = self.compute_dense_matrix(ref_el, entity_dofs, new_nodes, poly_set) return np.matmul(transformed_basis, original_V.T) temp = make_mat(permuted_g) # if dim == 1 and e_id == 0: @@ -329,7 +346,7 @@ def make_mat(perm_g): res_dict[dim][e_id][val] = temp[np.ix_(dof_ids, dof_ids)] return res_dict - def make_overall_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): + def make_overall_dense_matrices(self, ref_el, entity_dofs, nodes, poly_set): raise NotImplementedError("this function should be unnecessary") min_ids = self.cell.get_starter_ids() dim = self.cell.dim() @@ -337,20 +354,19 @@ def make_overall_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): e_id = e.id - min_ids[dim] res_dict = {dim: {e_id: {}}} degree = self.spaces[0].degree() - original_V, original_basis = self.compute_dense_matrix(ref_el, entity_ids, nodes, poly_set) + original_V, original_basis = self.compute_dense_matrix(ref_el, entity_dofs, nodes, poly_set) for g in self.cell.group.members(): val = g.numeric_rep() if g.perm.is_Identity: res_dict[dim][e_id][val] = np.eye(len(nodes)) else: new_nodes = [d(g).convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] - transformed_V, transformed_basis = self.compute_dense_matrix(ref_el, entity_ids, new_nodes, poly_set) + transformed_V, transformed_basis = self.compute_dense_matrix(ref_el, entity_dofs, new_nodes, poly_set) res_dict[dim][e_id][val] = np.matmul(transformed_basis, original_V.T) return res_dict - def _entity_associations(self, dofs): - min_ids = self.cell.get_starter_ids() - entity_associations = {dim: {e.id - min_ids[dim]: {} for e in self.cell.d_entities(dim)} + def _entity_associations(self, dofs, overall=True): + entity_associations = {dim: {i: {} for i, e in enumerate(self.cell.d_entities(dim))} for dim in range(self.cell.dim() + 1)} cell_dim = self.cell.dim() cell_dict = entity_associations[cell_dim][0] @@ -361,8 +377,13 @@ def _entity_associations(self, dofs): # construct mapping of entities to the dof generators and the dofs they generate for d in dofs: sub_dim = d.cell_defined_on.dim() - sub_dict = entity_associations[sub_dim][d.cell_defined_on.id - min_ids[sub_dim]] - for dim in set([sub_dim, cell_dim]): + cell_defined_on_id = self.cell.d_entities_ids(sub_dim).index(d.cell_defined_on.id) + sub_dict = entity_associations[sub_dim][cell_defined_on_id] + if overall: + dims = set([sub_dim, cell_dim]) + else: + dims = [sub_dim] + for dim in dims: dof_gen = str(d.generation[dim]) num_dofs[dof_gen] = (dim, d.generation[dim].g1.size()) @@ -376,7 +397,6 @@ def _entity_associations(self, dofs): sub_dict[dof_gen] += [d] elif dim < cell_dim or not d.immersed: sub_dict[dof_gen] = [d] - if dof_gen in cell_dict.keys() and dim == cell_dim and d.immersed: cell_dict[dof_gen] += [d] elif dim == cell_dim and d.immersed: @@ -384,17 +404,23 @@ def _entity_associations(self, dofs): return entity_associations, pure_perm, sub_pure_perm - def _initialise_entity_dicts(self, dofs): - min_ids = self.cell.get_starter_ids() + def _initialise_entity_dicts(self, dofs, tensor=False): + # min_ids = self.cell.get_starter_ids() dof_id_mat = np.eye(len(dofs)) oriented_mats_by_entity = {} flat_by_entity = {} - for dim in range(self.cell.dim() + 1): + cell = self.cell + if tensor: + dims = list(product(*(f.dimensions() for f in cell.factors))) + else: + dims = [i for i in range(cell.dimension + 1)] + for dim in dims: oriented_mats_by_entity[dim] = {} flat_by_entity[dim] = {} - ents = self.cell.d_entities(dim) - for e in ents: - e_id = e.id - min_ids[dim] + + ents = cell.d_entities(dim) + for e_id, e in enumerate(ents): + # old_e_id = e.id - min_ids[dim] members = e.group.members() oriented_mats_by_entity[dim][e_id] = {} flat_by_entity[dim][e_id] = {} @@ -405,22 +431,20 @@ def _initialise_entity_dicts(self, dofs): flat_by_entity[dim][e_id][val] = [] return oriented_mats_by_entity, flat_by_entity - def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): + def make_dof_perms(self, ref_el, entity_dofs, nodes, poly_set): dofs = self.generate() - min_ids = self.cell.get_starter_ids() entity_associations, pure_perm, sub_pure_perm = self._entity_associations(dofs) # if pure_perm is False: # #TODO think about where this call goes # return self.matrices_by_entity, None, pure_perm - # return self.make_overall_dense_matrices(ref_el, entity_ids, nodes, poly_set), None, pure_perm + # return self.make_overall_dense_matrices(ref_el, entity_dofs, nodes, poly_set), None, pure_perm oriented_mats_by_entity, flat_by_entity = self._initialise_entity_dicts(dofs) # for each entity, look up generation on that entity and permute the # dof mapping according to the generation for dim in range(self.cell.dim() + 1): ents = self.cell.d_entities(dim) - for e in ents: - e_id = e.id - min_ids[dim] + for e_id, e in enumerate(ents): members = e.group.members() for g in members: val = g.numeric_rep() @@ -500,13 +524,26 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): # Interior matrices for tetrahedrons are tricky - and they don't matter unless you're in 4d warnings.warn("Interior Matrices in 3d not implemented, but are not needed.") oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = np.eye(len(ent_dofs_ids)) - else: - # TODO what if an orientation is not in G1 - warnings.warn("FUSE: orientation case not covered") - # sub_mat = g.matrix_form() - # oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() - # raise NotImplementedError(f"Orientation {g} is not in group {dof_gen_class[dim].g1.members()}") + elif len(dof_gen_class.keys()) == 2 and dim == self.cell.dim(): + # Immersed DOFs revisited at the cell's own top-level entity: the + # matrix for this (dim, e_id, val) block is unconditionally + # recomputed by the immersion-handling block below, so there is + # nothing to do here. pass + elif len(dof_gen_class.keys()) == 1 and dim == self.cell.dim(): + # Non-immersed DOFs defined directly on the cell interior, where the + # dof count doesn't match the vertex count (e.g. interior dofs of + # higher-degree 3d elements). These dofs are never shared with a + # neighbouring cell, so no cross-cell orientation matching is + # required and the identity set by _initialise_entity_dicts is + # correct as-is. + warnings.warn("Interior Matrices in 3d not implemented, but are not needed.") + else: + raise NotImplementedError( + f"Orientation {g} on entity dim {dim} is not covered: " + f"dof_gen_class keys={list(dof_gen_class.keys())}, " + f"ndofs={len(ent_dofs_ids)}, nverts={len(self.cell.vertices())}" + ) if len(dof_gen_class.keys()) == 2 and dim == self.cell.dim(): # Handle immersion - can only happen once so number of keys is max 2 dimensions = list(dof_gen_class.keys()) @@ -518,7 +555,7 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): g_sub_mat = perm_list_to_matrix(identity, [sub_e for sub_e, _ in permuted_ents]) for sub_e, sub_g in permuted_ents: sub_e = self.cell.get_node(sub_e) - sub_e_id = sub_e.id - min_ids[sub_e.dim()] + sub_e_id = self.cell.d_entities(sub_e.dim(), get_class=False).index(sub_e.id) sub_ent_ids = [] for (k, v) in entity_associations[immersed_dim][sub_e_id].items(): sub_ent_ids += [self.dof_id_to_fiat_id[e.id] for e in v] @@ -533,7 +570,7 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): oriented_mats_overall = oriented_mats_by_entity[dim][0] if pure_perm and sub_pure_perm: for val, mat in oriented_mats_overall.items(): - cell_dofs = entity_ids[dim][0] + cell_dofs = entity_dofs[dim][0] flat_by_entity[dim][e_id][val] = perm_matrix_to_perm_array(mat[np.ix_(cell_dofs, cell_dofs)]) return oriented_mats_by_entity, flat_by_entity, True @@ -563,13 +600,14 @@ def orient_mat_perms(self): num_ents += len(ents) def reverse_dof_perms(self, matrices): - min_ids = self.cell.get_starter_ids() reversed_mats = {} + cell = self.cell + # if isinstance(cell, TensorProductPoint)and cell.flat: + # cell = self.unflat_cell for dim in matrices.keys(): reversed_mats[dim] = {} - ents = self.cell.d_entities(dim) - for e in ents: - e_id = e.id - min_ids[dim] + ents = cell.d_entities(dim) + for e_id, e in enumerate(ents): perms_copy = matrices[dim][e_id].copy() members = e.group.members() for m in members: @@ -587,6 +625,25 @@ def reverse_dof_perms(self, matrices): reversed_mats[dim][e_id] = perms_copy return reversed_mats + def __add__(self, other): + """ Construct a new element triple by combining the degrees of freedom + This implementation does not make assertions about the properties + of the resulting element. + + Elements being adding must be defined over the same cell and have the same + value shape and mapping""" + assert self.cell == other.cell + assert self.spaces[0].set_shape == other.spaces[0].set_shape + assert str(self.spaces[1]) == str(other.spaces[1]) + + spaces = (self.spaces[0] + other.spaces[0], self.spaces[1], max([self.spaces[2], other.spaces[2]])) + + from fuse.tensor_products import TensorProductTriple + if isinstance(other, TensorProductTriple): + return other + self + + return ElementTriple(self.cell, spaces, self.DOFGenerator + other.DOFGenerator) + def _to_dict(self): o_dict = {"cell": self.cell, "spaces": self.spaces, "dofs": self.DOFGenerator} return o_dict @@ -624,38 +681,37 @@ def num_dofs(self): return self.dof_numbers def generate(self, cell, space, id_counter): - if self.ls is None: - self.ls = [] - for l_g in self.x: - i = 0 - for g in self.g1.members(): - generated = l_g(g) - if not isinstance(generated, list): - generated = [generated] - for dof in generated: - dof.add_context(self, cell, space, g, id_counter, i) - id_counter += 1 - i += 1 - self.ls.extend(generated) - self.dof_numbers = len(self.ls) - self.dof_ids = [dof.id for dof in self.ls] + self.ls = [] + for l_g in self.x: + i = 0 + for g in self.g1.members(): + generated = l_g(g) + if not isinstance(generated, list): + generated = [generated] + for dof in generated: + dof.add_context(self, cell, space, g, id_counter, i) + id_counter += 1 + i += 1 + self.ls.extend(generated) + self.dof_numbers = len(self.ls) + self.dof_ids = [dof.id for dof in self.ls] return self.ls - def make_entity_ids(self): + def make_entity_dofs(self): dofs = self.ls - entity_ids = {} + entity_dofs = {} min_ids = dofs[0].cell.get_starter_ids() top = dofs[0].cell.get_topology() for dim in sorted(top): - entity_ids[dim] = {i: [] for i in top[dim]} + entity_dofs[dim] = {i: [] for i in top[dim]} for i in range(len(dofs)): entity = dofs[i].cell_defined_on dim = entity.dim() - entity_ids[dim][entity.id - min_ids[dim]].append(i) - return entity_ids + entity_dofs[dim][entity.id - min_ids[dim]].append(i) + return entity_dofs def __repr__(self): repr_str = "DOFGen(" diff --git a/fuse/utils.py b/fuse/utils.py index d57f6904..3d2fb4f9 100644 --- a/fuse/utils.py +++ b/fuse/utils.py @@ -2,6 +2,8 @@ import sympy as sp import math +_SYMBOLS = tuple(sp.Symbol(s) for s in ("x", "y", "z")) + def fold_reduce(func_list, *prev): """ @@ -29,7 +31,7 @@ def sympy_to_numpy(array, symbols, values): """ substituted = array.subs({symbols[i]: values[i] for i in range(len(values))}) - if len(array.atoms(sp.Symbol)) == len(values) and all(not isinstance(v, sp.Expr) for v in values): + if len(array.atoms(sp.Symbol)) <= len(values) and all(not isinstance(v, sp.Expr) for v in values): nparray = np.array(substituted).astype(np.float64) if len(nparray.shape) > 1: @@ -47,9 +49,9 @@ def tabulate_sympy(expr, pts): # expr: sp matrix expression in x,y,z for components of R^d # pts: n values in R^d # returns: evaluation of expr at pts - res = np.array(pts) + res = np.zeros((pts.shape[0],) + (expr.shape[-1],)) i = 0 - syms = ["x", "y", "z"] + syms = _SYMBOLS for pt in pts: if not hasattr(pt, "__iter__"): pt = (pt,) @@ -57,16 +59,20 @@ def tabulate_sympy(expr, pts): subbed = np.array(subbed).astype(np.float64) res[i] = subbed[0] i += 1 - final = res.squeeze() - return final + # final = res.squeeze() + return res -def max_deg_sp_mat(sp_mat): +def max_deg_sp_expr(sp_expr): degs = [] - for comp in sp_mat: - # only compute degree if component is a polynomial - if sp.sympify(comp).as_poly(): - degs += [sp.sympify(comp).as_poly().degree()] + if isinstance(sp_expr, sp.Matrix): + for comp in sp_expr: + # only compute degree if component is a polynomial + if sp.sympify(comp).as_poly(): + degs += [sp.sympify(comp).as_poly().total_degree()] + else: + if sp.sympify(sp_expr).as_poly(): + degs += [sp.sympify(sp_expr).as_poly().total_degree()] return max(degs) @@ -101,3 +107,11 @@ def orientation_value(identity_arg, perm_arg): identity.remove(perm[i]) val += loc * math.factorial(len(perm) - i - 1) return val + + +def as_tuple(expr): + if isinstance(expr, tuple): + return expr + if isinstance(expr, list): + return tuple(expr) + return (expr,) diff --git a/pyproject.toml b/pyproject.toml index cc59f90d..db00e3aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,4 +53,4 @@ testpaths = [ [tool.coverage.run] include=[ "fuse/*", -] \ No newline at end of file +] diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index 713768e9..e3ce6956 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -1,6 +1,7 @@ from fuse import * import sympy as sp import numpy as np +import pytest np.set_printoptions(legacy="1.25") @@ -30,6 +31,33 @@ def construct_dg1(): return dg1 +def construct_dg0_integral(edge=None): + if not edge: + edge = Point(1, [Point(0), Point(0)], vertex_num=2) + xs = [DOF(L2Pairing(), VectorKernel(0.5))] + dg0 = ElementTriple(edge, (P0, CellL2, C0), DOFGenerator(xs, S1, S1)) + return dg0 + + +def construct_dg1_integral(cell=None): + edge = Point(1, [Point(0), Point(0)], vertex_num=2) + x = sp.Symbol("x") + xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] + dg1 = ElementTriple(edge, (P1, CellL2, C0), DOFGenerator(xs, S2, S1)) + return dg1 + + +def construct_dg2_integral(cell=None): + edge = Point(1, [Point(0), Point(0)], vertex_num=2) + x = sp.Symbol("x") + xs = [DOF(L2Pairing(), PolynomialKernel((x/2)*(x + 1), symbols=(x,)))] + centre = [DOF(L2Pairing(), PolynomialKernel((1 - x**2), symbols=(x,)))] + + dofs = [DOFGenerator(xs, S2, S1), DOFGenerator(centre, S1, S1)] + dg2 = ElementTriple(edge, (PolynomialSpace(2), CellL2, C0), dofs) + return dg2 + + def plot_dg1(): dg1 = construct_dg1() dg1.plot() @@ -78,9 +106,11 @@ def test_dg_examples(): assert any(np.isclose(val, dof.eval(test_func)) for val in dof_vals) -def construct_cg1(): +def construct_cg1(edge=None): + # [test_cg1 0] - edge = Point(1, [Point(0), Point(0)], vertex_num=2) + if not edge: + edge = Point(1, [Point(0), Point(0)], vertex_num=2) vert = edge.vertices()[0] xs = [DOF(DeltaPairing(), PointKernel(()))] @@ -345,6 +375,7 @@ def test_nd_example(): for dof in ned.generate(): assert [np.allclose(1, dof.eval(basis_func).flatten()) for basis_func in basis_funcs].count(True) == 1 assert [np.allclose(0, dof.eval(basis_func).flatten()) for basis_func in basis_funcs].count(True) == 2 + ned.to_fiat() def construct_rt(tri=None): @@ -393,6 +424,15 @@ def test_rt_example(): rt.to_fiat() +@pytest.mark.parametrize(["triple", "expected"], [(construct_dg1_integral(), 1), + (construct_cg1(), 0), + (construct_rt(), 1), + (construct_dg1_tri(), 2)]) +def test_form_degree(triple, expected): + from fuse.triples import compute_form_degree + assert compute_form_degree(triple.cell, triple.spaces) == expected + + def construct_hermite(): tri = polygon(3) vert = tri.vertices()[0] diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index b7ce26ed..ec8e593f 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -680,11 +680,11 @@ def test_tet_nd(): def construct_V(elem): nodes = elem.nodes ref_el = elem.ref_el - entity_ids = elem.entity_ids + entity_dofs = elem.entity_dofs poly_set = elem.poly_set from FIAT.dual_set import DualSet - dual = DualSet(nodes, ref_el, entity_ids) + dual = DualSet(nodes, ref_el, entity_dofs) old_coeffs = poly_set.get_coeffs() dualmat = dual.to_riesz(poly_set) diff --git a/test/test_algebra.py b/test/test_algebra.py new file mode 100644 index 00000000..5f0c9a75 --- /dev/null +++ b/test/test_algebra.py @@ -0,0 +1,36 @@ +from fuse import * +from firedrake import * +import numpy as np +import sympy as sp +from test_convert_to_fiat import create_cg2_tri, construct_cg3 + + +def construct_bubble(cell=None): + if cell is None: + cell = polygon(3) + x = sp.Symbol("x") + y = sp.Symbol("y") + f = (3*np.sqrt(3)/4)*(y + np.sqrt(3)/3)*(np.sqrt(3)*x + y - 2*np.sqrt(3)/3)*(-np.sqrt(3)*x + y - 2*np.sqrt(3)/3) + space = PolynomialSpace(3).restrict(0, 0)*f + xs = [DOF(DeltaPairing(), PointKernel((0, 0)))] + bubble = ElementTriple(cell, (space, CellL2, L2), DOFGenerator(xs, S1, S1)) + return bubble + + +def test_bubble(): + mesh = UnitTriangleMesh(use_fuse=True) + x = SpatialCoordinate(mesh) + + tri = polygon(3) + bub = construct_bubble(tri) + cg2 = create_cg2_tri(tri) + p2b3 = bub + cg2 + V = FunctionSpace(mesh, p2b3.to_ufl()) + W = FunctionSpace(mesh, construct_cg3().to_ufl()) + + bubble_func = 27*x[0]*x[1]*(1-x[0]-x[1]) + u = project(bubble_func, V) + exact = Function(W) + exact.interpolate(bubble_func, W) + # make sure that these are the same + assert sqrt(assemble((u-exact)*(u-exact)*dx)) < 1e-14 diff --git a/test/test_cells.py b/test/test_cells.py index 66dbdf3c..35f5a9fc 100644 --- a/test/test_cells.py +++ b/test/test_cells.py @@ -3,11 +3,11 @@ from fuse.cells import ufc_triangle, ufc_tetrahedron import pytest import numpy as np -from FIAT.reference_element import default_simplex, ufc_simplex +from FIAT.reference_element import default_simplex from test_convert_to_fiat import helmholtz_solve -@pytest.fixture(scope='module', params=[0, 1, 2]) +@pytest.fixture(scope='module', params=[0, 1, 2, 3]) def C(request): dim = request.param if dim == 0: @@ -16,6 +16,8 @@ def C(request): return Point(1, [Point(0), Point(0)], vertex_num=2) elif dim == 2: return polygon(3) + elif dim == 3: + return make_tetrahedron() def test_vertices(C): @@ -195,6 +197,16 @@ def test_comparison(): # print(tensor_product1 >= tensor_product1) +def test_self_equality(C): + assert C == C + + +@pytest.mark.parametrize(["A", "B", "res"], [(ufc_triangle(), polygon(3), False), + (line(), line(), True),]) +def test_equivalence(A, B, res): + assert A.equivalent(B) == res + + @pytest.mark.parametrize(["cell"], [(ufc_triangle(),), (polygon(3),)]) def test_connectivity(cell): cell = cell.to_fiat() @@ -236,7 +248,7 @@ def test_new_connectivity(cell): def test_compare_tris(): fuse_tet = polygon(3) ufc_tet = ufc_triangle() - fiat_tet = ufc_simplex(2) + fiat_tet = default_simplex(2) print(fiat_tet.get_topology()) print(fuse_tet.get_topology()) @@ -267,7 +279,7 @@ def test_compare_tets(): # perm = tet.group.get_member([1, 2, 0, 3]) fuse_tet = tet ufc_tet = ufc_tetrahedron() - fiat_tet = ufc_simplex(3) + fiat_tet = default_simplex(3) # breakpoint() print(fiat_tet.get_topology()) print(fuse_tet.get_topology()) diff --git a/test/test_construction.py b/test/test_construction.py index 4415236e..365f84f9 100644 --- a/test/test_construction.py +++ b/test/test_construction.py @@ -121,28 +121,32 @@ def test_polynomial_poisson_solve(deg): assert np.allclose(res, 0) -# def test_plane(): -# from fuse import make_tetrahedron -# cell = make_tetrahedron() -# verts = cell.ordered_vertex_coords() -# res = check_below_plane(verts[1], verts[2], verts[3], (verts[1] + verts[2] + verts[3])/3) -# print(res) - - -# def test_check_line(): -# from fuse import polygon -# cell = polygon(3) -# verts = np.array(sorted(cell.ordered_vertex_coords())) -# midpoint = (verts[1] + verts[2])/2 -# midpoint1 = (verts[0] + verts[2])/2 -# assert check_below_line(verts[0], midpoint, (0, 0)) == 0 -# assert check_on_line(verts[0], midpoint, (0, 0)) -# assert check_on_line(verts[1], verts[2], midpoint) -# assert not check_on_line(verts[1], verts[2], midpoint1) - -# assert check_below_line(verts[0], midpoint, (-0.5, 0)) == -1 -# assert check_below_line(verts[0], midpoint, (0, -0.5)) == 1 - -# assert check_below_line(verts[1], midpoint1, verts[0]) == 1 - -# test_construction3d(1,3, 2) +def test_ned3(): + nd3_pt = periodic_table(1, 3, 1, 3) + pt_gen = nd3_pt.DOFGenerator[1].x[0].triple.DOFGenerator[0].g1.members() + nd3_pt.to_fiat() + from test_3d_examples_docs import construct_tet_ned_2nd_kind_3 + nd3_mn = construct_tet_ned_2nd_kind_3() + mn_gen = nd3_mn.DOFGenerator[1].x[0].triple.DOFGenerator[0].g1.members() + nd3_mn.to_fiat() + + print([nd3_pt.dofs[i].id for i in range(24, 30)]) + print([nd3_mn.dofs[i].id for i in range(24, 30)]) + # for i in range(24, 30): + # print(i) + # print(pt_gen[i - 24], nd3_pt.dofs[i]) + # print(mn_gen[i - 24], nd3_mn.dofs[i]) + + def permute_face(elem, o): + dof_ids = [d.id for d in elem.dofs] + transform_mat = elem.matrices[2][0][o.numeric_rep()] + transformed = np.matmul(transform_mat, dof_ids) + return transformed[24:30] + for o in mn_gen: + print(o) + print(o.numeric_rep()) + for o in pt_gen: + print(o) + print(o.numeric_rep()) + # print(permute_face(nd3_pt, o)) + # print(permute_face(nd3_mn, o)) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index b90440ad..6d3e3dbb 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -6,7 +6,7 @@ from firedrake import * from sympy.combinatorics import Permutation from FIAT.quadrature_schemes import create_quadrature -from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3 +from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3, construct_dg0_integral, construct_dg1_integral, construct_dg2_integral from test_3d_examples_docs import (construct_tet_rt, construct_tet_rt2, construct_tet_rt3, construct_tet_ned, construct_tet_ned_2nd_kind, construct_tet_ned_2nd_kind_2, construct_tet_ned_2nd_kind_2_non_bary, @@ -115,13 +115,11 @@ def create_cg1(cell): def create_cg1_quad(): deg = 1 - cell = polygon(4) - # cell = constructCellComplex("quadrilateral").cell_complex - - vert_dg = create_dg0(cell.vertices()[0]) + cell = TensorProductPoint(line(), line()).flatten() + print(cell, type(cell)) + vert_dg = create_dg1(cell.vertices()[0]) xs = [immerse(cell, vert_dg, TrH1)] - - Pk = PolynomialSpace(deg, deg + 1) + Pk = PolynomialSpace(deg + 1, deg) cg = ElementTriple(cell, (Pk, CellL2, C0), DOFGenerator(xs, get_cyc_group(len(cell.vertices())), S1)) return cg @@ -147,8 +145,12 @@ def create_cg1_flipped(cell): return cg -def create_cg2(cell): +def create_cg2(cell=None): + if cell is None: + cell = line() deg = 2 + if cell is None: + cell = Point(1, [Point(0), Point(0)], vertex_num=2) if cell.dim() > 1: raise NotImplementedError("This method is for cg2 on edges, please use create_cg2_tri for triangles") vert_dg = create_dg0(cell.vertices()[0]) @@ -161,8 +163,10 @@ def create_cg2(cell): return cg -def create_cg2_tri(cell): +def create_cg2_tri(cell=None): deg = 2 + if cell is None: + cell = polygon(3) Pk = PolynomialSpace(deg) vert_dg0 = create_dg0(cell.vertices()[0]) @@ -218,7 +222,9 @@ def create_cg2_tet(cell): return cg2 -def create_cg3_tet(cell, perm=True): +def create_cg3_tet(cell=None, perm=True): + if cell is None: + cell = make_tetrahedron() vert = cell.vertices()[0] edge = cell.edges()[0] @@ -365,7 +371,10 @@ def test_entity_perms(elem_gen, cell): @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg1, "CG", 1), (create_dg1, "DG", 1), - pytest.param(create_dg2, "DG", 2, marks=pytest.mark.xfail(reason='Need to update TSFC in CI')), + (construct_dg0_integral, "DG", 0), + (construct_dg1_integral, "DG", 1), + (construct_dg2_integral, "DG", 2), + (create_dg2, "DG", 2), (create_cg2, "CG", 2) ]) def test_1d(elem_gen, elem_code, deg): @@ -529,6 +538,29 @@ def poisson_solve(r, elem, parameters={}, quadrilateral=False): return sqrt(assemble(inner(u - f, u - f) * dx)) +def run_test_original(r, elem_code, deg, parameters={}, quadrilateral=False): + # Create mesh and define function space + m = UnitSquareMesh(2 ** r, 2 ** r, quadrilateral=quadrilateral) + + x = SpatialCoordinate(m) + V = FunctionSpace(m, elem_code, deg) + # Define variational problem + u = Function(V) + v = TestFunction(V) + a = inner(grad(u), grad(v)) * dx + + bcs = [DirichletBC(V, Constant(0), 3), + DirichletBC(V, Constant(42), 4)] + + # Compute solution + solve(a == 0, u, solver_parameters=parameters, bcs=bcs) + + f = Function(V) + f.interpolate(42*x[1]) + + return sqrt(assemble(inner(u - f, u - f) * dx)) + + @pytest.mark.parametrize(['params', 'elem_gen'], [(p, d) for p in [{}, {'snes_type': 'ksponly', 'ksp_type': 'preonly', 'pc_type': 'lu'}] @@ -540,7 +572,7 @@ def test_poisson_analytic(params, elem_gen): @pytest.mark.parametrize(['elem_gen'], - [(create_cg1_quad_tensor,), pytest.param(create_cg1_quad, marks=pytest.mark.xfail(reason='Need to allow generation on tensor product quads'))]) + [(create_cg1_quad_tensor,), (create_cg1_quad,)]) def test_quad(elem_gen): elem = elem_gen() r = 0 @@ -548,8 +580,13 @@ def test_quad(elem_gen): assert (poisson_solve(r, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) +@pytest.mark.xfail(reason="Issue with quad cell") def test_non_tensor_quad(): - create_cg1_quad() + elem = create_cg1_quad() + # ufl_elem = elem.to_ufl() + print(elem.to_fiat().entity_permutations()) + # elem.cell.hasse_diagram(filename="cg1quad.png") + assert (run_test_original(1, "CG", 1, parameters={}, quadrilateral=True) < 1.e-9) def project(U, mesh, func): @@ -868,10 +905,10 @@ def test_basis_funcs_gen(form_num): for v in basis_funcs[:1]: print(v) vec = as_tensor(sp.lambdify(symbols, v)(x_m[0], x_m[1], x_m[2])[:, 0]) - min_id1 = min([v for e in elem.entity_ids[2].values() for v in e]) - max_id1 = max([v for e in elem.entity_ids[2].values() for v in e]) + 1 - min_id2 = min([v for e in elem2.entity_ids[2].values() for v in e]) - max_id2 = max([v for e in elem2.entity_ids[2].values() for v in e]) + 1 + min_id1 = min([v for e in elem.entity_dofs[2].values() for v in e]) + max_id1 = max([v for e in elem.entity_dofs[2].values() for v in e]) + 1 + min_id2 = min([v for e in elem2.entity_dofs[2].values() for v in e]) + max_id2 = max([v for e in elem2.entity_dofs[2].values() for v in e]) + 1 res = assemble(interpolate(vec, V)).dat.data res2 = assemble(interpolate(vec, V2)).dat.data @@ -1017,11 +1054,14 @@ def evaluate_pt_dict(pt_dict, fn): (construct_tet_cg4, "CG", 4), (construct_tet_cg6, "CG", 6), (construct_tet_ned3_old, "N1curl", 3), - (lambda cell: periodic_table(1, 3, 1, 3), "N2curl", 3), - (lambda cell: periodic_table(0, 3, 1, 3), "N1curl", 3)]) + (periodic_table(1, 3, 1, 3), "N2curl", 3), + (periodic_table(0, 3, 1, 3), "N1curl", 3)]) def test_two_tet_interpolation(elem_gen, elem_code, deg): cell = make_tetrahedron() - elem = elem_gen(cell) + if hasattr(elem_gen, "__call__"): + elem = elem_gen(cell) + else: + elem = elem_gen def vec(mesh): x = SpatialCoordinate(mesh) @@ -1064,11 +1104,11 @@ def vec(mesh): @pytest.mark.parametrize("elem_gen,elem_code,deg,max_err", [(construct_tet_cg6, "CG", 6, 1e-13), - (lambda cell: periodic_table(0, 3, 1, 3), "N1curl", 3, 1e-12), + (periodic_table(0, 3, 1, 3), "N1curl", 3, 1e-12), (create_cg3_tet, "CG", 3, 1e-13), (construct_tet_cg4, "CG", 4, 1e-13), - (lambda cell: periodic_table(0, 3, 0, 4), "CG", 4, 1e-13), - (lambda cell: periodic_table(0, 3, 0, 6), "CG", 6, 1e-13), + (periodic_table(0, 3, 0, 4), "CG", 4, 1e-13), + (periodic_table(0, 3, 0, 6), "CG", 6, 1e-13), (construct_tet_rt2, "RT", 2, 1e-13), (construct_tet_rt3, "RT", 3, 1e-13), (construct_tet_bdm2, "BDM", 2, 1e-13), @@ -1076,13 +1116,15 @@ def vec(mesh): (construct_tet_ned_2nd_kind_2_non_bary, "N2curl", 2, 1e-12), (construct_tet_ned_2nd_kind_3, "N2curl", 3, 1e-12), (construct_tet_ned2, "N1curl", 2, 1e-13), - (lambda cell: periodic_table(1, 3, 1, 3), "N2curl", 3, 1e-12), - (lambda cell: periodic_table(1, 3, 1, 4), "N2curl", 4, 1e-12), + (periodic_table(1, 3, 1, 3), "N2curl", 3, 1e-12), + (periodic_table(1, 3, 1, 4), "N2curl", 4, 1e-11), (construct_tet_ned3_old, "N1curl", 2, 1e-13)]) def test_two_tet_projection(elem_gen, elem_code, deg, max_err): - cell = make_tetrahedron() - elem1 = elem_gen(cell) - ufl_elem1 = elem1.to_ufl() + if hasattr(elem_gen, "__call__"): + elem = elem_gen() + else: + elem = elem_gen + ufl_elem = elem.to_ufl() def expr(mesh): x = SpatialCoordinate(mesh) @@ -1098,7 +1140,7 @@ def expr(mesh): sp.combinatorics.Permutation([0, 3, 2, 1]), sp.combinatorics.Permutation([0, 2, 1, 3])] - for elem in [ufl_elem1]: + for elem in [ufl_elem]: for g in group: mesh = TwoTetMesh(perm=g, use_fuse=True) print(g) diff --git a/test/test_dofs.py b/test/test_dofs.py index 8b2e0cae..bf29ad45 100644 --- a/test/test_dofs.py +++ b/test/test_dofs.py @@ -220,6 +220,6 @@ def test_generate_quadrature(): print("fiat", d.pt_dict) print() for d in elem.generate(): - print("fuse", d.to_quadrature(degree, (2,))) + print("fuse", d.to_quadrature(degree, value_shape=(2,))) elem.to_fiat() diff --git a/test/test_groups.py b/test/test_groups.py index 1b26787e..0c207039 100644 --- a/test/test_groups.py +++ b/test/test_groups.py @@ -117,3 +117,10 @@ def test_perm_mat_conversion(): mat_form = g.matrix_form() array_form = perm_matrix_to_perm_array(mat_form) assert np.allclose(g.perm.array_form, array_form) + + +def test_numeric_reps(): + cell = polygon(4) + rot4 = get_cyc_group(4).add_cell(cell) + + assert sorted([m.numeric_rep() for m in rot4.members()]) == list(range(len(rot4.members()))) diff --git a/test/test_interpolation.py b/test/test_interpolation.py new file mode 100644 index 00000000..7d257828 --- /dev/null +++ b/test/test_interpolation.py @@ -0,0 +1,92 @@ +from firedrake import * +from firedrake.ufl_expr import extract_unique_domain +from fuse import * +import numpy as np +from test_2d_examples_docs import construct_cg3 +from test_convert_to_fiat import create_cg2, create_cg2_tri + + +def test_cross_mesh_tri_to_quad(): + mesh1 = UnitSquareMesh(10, 10, use_fuse=True) + mesh2 = UnitSquareMesh(10, 10, quadrilateral=True, use_fuse=True) + A = create_cg2() + B = create_cg2() + V1 = FunctionSpace(mesh1, create_cg2_tri().to_ufl()) + V2 = FunctionSpace(mesh2, tensor_product(A, B).flatten().to_ufl()) + + f1 = Function(V1) + f2 = Function(V2) + + x = SpatialCoordinate(mesh1) + f1 = f1.interpolate(x[0]**2 + x[1]**2) + f2 = f2.interpolate(f1) + assert np.allclose(sqrt(assemble(inner(f1, f1) * dx)), sqrt(assemble(inner(f2, f2) * dx))) + + +def test_cross_mesh_fuse_to_ufc(): + mesh1 = UnitSquareMesh(10, 10, use_fuse=True) + mesh2 = UnitSquareMesh(10, 10) + V1 = FunctionSpace(mesh1, create_cg2_tri().to_ufl()) + V2 = FunctionSpace(mesh2, "CG", 2) + + f1 = Function(V1) + f2 = Function(V2) + + x = SpatialCoordinate(mesh1) + f1 = f1.interpolate(x[0]**2 + x[1]**2) + f2 = f2.interpolate(f1) + assert np.allclose(sqrt(assemble(inner(f1, f1) * dx)), sqrt(assemble(inner(f2, f2) * dx))) + + +def test_cross_mesh(): + dest_quad = False + atol = 1e-8 + m_src = UnitSquareMesh(2, 3, use_fuse=True) + m_dest = UnitSquareMesh(3, 5, quadrilateral=dest_quad) + coords = np.array( + [[0.56, 0.6], [0.1, 0.9], [0.9, 0.1], [0.9, 0.9], [0.726, 0.6584]] + ) # fairly arbitrary + # add the coordinates of the mesh vertices to test boundaries + vertices_src = m_src.coordinates.dat.data_ro + coords = np.concatenate((coords, vertices_src)) + vertices_dest = m_dest.coordinates.dat.data_ro + coords = np.concatenate((coords, vertices_dest)) + expr_src = product(SpatialCoordinate(m_src)) + expr_dest = product(SpatialCoordinate(m_dest)) + dest_eval = PointEvaluator(m_dest, coords) + expected = np.prod(coords, axis=-1) + + V_src = FunctionSpace(m_src, construct_cg3().to_ufl()) + V_dest = FunctionSpace(m_dest, "CG", 4) + + # test_expression from test_interpolate_cross_mesh in firedrake + f_dest = assemble(interpolate(expr_src, V_dest)) + assert extract_unique_domain(f_dest) is m_dest + got = dest_eval.evaluate(f_dest) + assert np.allclose(got, expected, atol=atol) + f_dest_2 = Function(V_dest).interpolate(expr_dest) + assert np.allclose(f_dest.dat.data_ro, f_dest_2.dat.data_ro, atol=atol) + + # test_function from test_interpolate_cross_mesh in firedrake + f_dest = Function(V_dest).interpolate(expr_src) + assert extract_unique_domain(f_dest) is m_dest + + got = dest_eval.evaluate(f_dest) + assert np.allclose(got, expected, atol=atol) + + f_src = Function(V_src).interpolate(expr_src) + f_dest = assemble(interpolate(f_src, V_dest)) + assert extract_unique_domain(f_dest) is m_dest + got = dest_eval.evaluate(f_dest) + assert np.allclose(got, expected, atol=atol) + + f_dest_2 = Function(V_dest).interpolate(expr_dest) + assert np.allclose(f_dest.dat.data_ro, f_dest_2.dat.data_ro, atol=atol) + + # test Function.interpolate(...) + f_dest = Function(V_dest) + f_dest.interpolate(f_src) + assert extract_unique_domain(f_dest) is m_dest + got = dest_eval.evaluate(f_dest) + assert np.allclose(got, expected, atol=atol) + assert np.allclose(f_dest.dat.data_ro, f_dest_2.dat.data_ro, atol=atol) diff --git a/test/test_polynomial_space.py b/test/test_polynomial_space.py index e7dad66d..baf81206 100644 --- a/test/test_polynomial_space.py +++ b/test/test_polynomial_space.py @@ -41,6 +41,7 @@ def test_restriction(): res_on_set = restricted.to_ON_polynomial_set(cell) P3_on_set = P3.to_ON_polynomial_set(cell) + assert res_on_set.get_num_members() < P3_on_set.get_num_members() not_restricted = P3.restrict(0, 3) @@ -48,6 +49,16 @@ def test_restriction(): assert not_restricted.mindegree == 0 +def test_square_space(): + cell = polygon(3) + q2 = PolynomialSpace(3, 1) + + q2_on_set = q2.to_ON_polynomial_set(cell) + P3_on_set = P3.to_ON_polynomial_set(cell) + + assert q2_on_set.get_num_members() < P3_on_set.get_num_members() + + @pytest.mark.parametrize("deg", [1, 2, 3, 4]) def test_complete_space(deg): cell = polygon(3) diff --git a/test/test_serialisation.py b/test/test_serialisation.py index 56db6689..0974522b 100644 --- a/test/test_serialisation.py +++ b/test/test_serialisation.py @@ -1,7 +1,9 @@ from fuse import * +from firedrake import * from fuse.serialisation import ElementSerialiser from test_convert_to_fiat import create_cg1 -# from test_2d_examples_docs import construct_nd +from test_orientations import interpolate_vs_project, get_expression +import pytest import numpy as np vert = Point(0) @@ -60,13 +62,36 @@ def test_cg_examples(): assert any([np.allclose(dof_val, dof_val2) for dof_val2 in dofs]) -# def test_ned(): -# cell = polygon(3) -# triple = construct_nd(cell) -# converter = ElementSerialiser() -# encoded = converter.encode(triple) +cg_params = [(0, 0, deg, deg + 0.75) for deg in list(range(1, 3))] + [(1, 0, deg, deg + 0.75) for deg in list(range(1, 3))] +nd_params = [(0, 1, deg, deg - 0.2) for deg in list(range(1, 3))] +rt_params = [(0, 2, deg, deg - 0.2) for deg in list(range(1, 3))] +dg_params = [(0, 3, deg, deg + 0.75) for deg in list(range(0, 3))] + [(1, 3, deg, deg + 0.75) for deg in list(range(0, 3))] +nd2_params = [(1, 1, deg, deg + 0.75) for deg in list(range(1, 3))] +bdm_params = [(1, 2, deg, deg + 0.75) for deg in list(range(1, 3))] -# decoded = converter.decode(encoded) -# for d in decoded.generate(): -# dof_val = d.eval(phi_2) -# assert any([np.allclose(dof_val, dof_val2) for dof_val2 in dofs]) + +@pytest.mark.parametrize("col,k,deg,conv_rate", cg_params + nd_params + rt_params + dg_params + nd2_params + bdm_params) +def test_post_serialisation_convergence(col, k, deg, conv_rate): + "Tests that appropriate convergence is still achieved after serialisation." + elem = periodic_table(col, 2, k, deg) + converter = ElementSerialiser() + encoded = converter.encode(elem) + elem_decoded = converter.decode(encoded) + scale_range = range(3, 6) + diff_inte = [0 for i in scale_range] + for n in scale_range: + mesh = UnitSquareMesh(2**n, 2**n, use_fuse=True) + + V = FunctionSpace(mesh, elem_decoded.to_ufl()) + x, y = SpatialCoordinate(mesh) + expr = cos(x*pi*2)*sin(y*pi*2) + if len(elem.get_value_shape()) > 0: + expr = as_vector([expr, expr]) + _, exact = get_expression(V) + _, diff_inte[n-min(scale_range)] = interpolate_vs_project(V, expr, exact) + + print("interpolation l2 error norms:", diff_inte) + diff_inte = np.array(diff_inte) + conv = np.log2(diff_inte[:-1] / diff_inte[1:]) + print("convergence order:", conv) + assert all([c > conv_rate for c in conv]) diff --git a/test/test_tensor_prod.py b/test/test_tensor_prod.py index 8e76ec59..45805ac9 100644 --- a/test/test_tensor_prod.py +++ b/test/test_tensor_prod.py @@ -2,10 +2,94 @@ import numpy as np from fuse import * from firedrake import * -from test_2d_examples_docs import construct_cg1, construct_dg1 +from test_2d_examples_docs import construct_cg1, construct_dg1, construct_dg0_integral, construct_dg1_integral +from test_convert_to_fiat import create_cg2, create_dg0, helmholtz_solve as helmholtz_solve2 +from fuse.tensor_products import HDiv as HDiv_fuse, HCurl as HCurl_fuse # from test_convert_to_fiat import create_cg1 +def create_cg3_interval(cell=None): + if cell is None: + cell = line() + deg = 3 + if cell.dim() > 1: + raise NotImplementedError("This method is for cg3 on edges, please use construct_cg3 for triangles") + vert_dg = create_dg0(cell.vertices()[0]) + xs = [immerse(cell, vert_dg, TrH1)] + interior = [DOF(DeltaPairing(), PointKernel((-1/np.sqrt(5), )))] + + Pk = PolynomialSpace(deg) + cg = ElementTriple(cell, (Pk, CellL2, C0), [DOFGenerator(xs, get_cyc_group(len(cell.vertices())), S1), + DOFGenerator(interior, S2, S1)]) + return cg + + +def ned1_quad(): + cg1 = construct_cg1() + dg0 = construct_dg0_integral() + return HCurl_fuse(tensor_product(cg1, dg0).flatten()) + HCurl_fuse(tensor_product(dg0, cg1).flatten()) + + +def rt1_quad(): + cg1 = construct_cg1() + dg0 = construct_dg0_integral() + return HDiv_fuse(tensor_product(cg1, dg0).flatten()) + HDiv_fuse(tensor_product(dg0, cg1).flatten()) + + +def rt1_hex(): + # In-plane (x, y) RT1-on-quad, extruded by a discontinuous interval in z. + h1 = HDiv_fuse(tensor_product(construct_cg1(), construct_dg0_integral()).flatten()) + h2 = HDiv_fuse(tensor_product(construct_dg0_integral(), construct_cg1()).flatten()) + x_component = HDiv_fuse(tensor_product(h1, construct_dg0_integral())) + y_component = HDiv_fuse(tensor_product(h2, construct_dg0_integral())) + # z-normal component: DG0-on-quad extruded by a continuous interval in z. + dg0_quad = tensor_product(construct_dg0_integral(), construct_dg0_integral()).flatten() + z_component = HDiv_fuse(tensor_product(dg0_quad, construct_cg1())) + return x_component + y_component + z_component + + +def ned1_hex(): + # In-plane (x, y) tangential edge components (Nedelec-1st-kind-on-quad + # pieces), extruded by a continuous interval in z + ex = HCurl_fuse(tensor_product(construct_dg0_integral(), construct_cg1()).flatten()) + ey = HCurl_fuse(tensor_product(construct_cg1(), construct_dg0_integral()).flatten()) + x_component = HCurl_fuse(tensor_product(ex, construct_cg1())) + y_component = HCurl_fuse(tensor_product(ey, construct_cg1())) + # z-tangential component: bilinear (Q1) scalar quad extruded by a + # discontinuous interval in z. + cg1_quad = tensor_product(construct_cg1(), construct_cg1()).flatten() + z_component = HCurl_fuse(tensor_product(cg1_quad, construct_dg0_integral())) + return x_component + y_component + z_component + + +def ned1_tensor(): + cg1 = construct_cg1() + dg0 = construct_dg0_integral() + + CG_1 = FiniteElement("CG", "interval", 1) + DG_0 = FiniteElement("DG", "interval", 0) + P1P0 = TensorProductElement(CG_1, DG_0) + horiz = HCurlElement(P1P0) + P0P1 = TensorProductElement(DG_0, CG_1) + vert = HCurlElement(P0P1) + firedrake_ned1 = horiz + vert + return HCurl_fuse(tensor_product(cg1, dg0)) + HCurl_fuse(tensor_product(dg0, cg1)), firedrake_ned1 + + +def rt1_tensor(): + cg1 = construct_cg1() + dg0 = construct_dg0_integral() + + CG_1 = FiniteElement("CG", "interval", 1) + DG_0 = FiniteElement("DG", "interval", 0) + P1P0 = TensorProductElement(CG_1, DG_0) + RT_horiz = HDivElement(P1P0) + P0P1 = TensorProductElement(DG_0, CG_1) + RT_vert = HDivElement(P0P1) + firedrake_rt1 = RT_horiz + RT_vert + return HDiv_fuse(tensor_product(cg1, dg0)) + HDiv_fuse(tensor_product(dg0, cg1)), firedrake_rt1 + + def helmholtz_solve(mesh, V): u = TrialFunction(V) v = TestFunction(V) @@ -17,6 +101,8 @@ def helmholtz_solve(mesh, V): u = Function(V) solve(a == L, u) f.interpolate(cos(x*pi*2)*cos(y*pi*2)) + print("res", u.dat.data) + print("true", f.dat.data) return sqrt(assemble(dot(u - f, u - f) * dx)) @@ -32,40 +118,48 @@ def mass_solve(U): assemble(L) solve(a == L, out) assert np.allclose(out.dat.data, f.dat.data, rtol=1e-5) + return out.dat.data -@pytest.mark.xfail(reason="tensor prod issues") -@pytest.mark.parametrize("generator, code, deg", [(construct_cg1, "CG", 1), (construct_dg1, "DG", 1)]) -def test_tensor_product_ext_mesh(generator, code, deg): +@pytest.mark.parametrize("generator1, generator2, code1, code2, deg1, deg2", + [(construct_cg1, construct_cg1, "CG", "CG", 1, 1), + (construct_dg1, construct_dg1, "DG", "DG", 1, 1), + (construct_dg1, construct_cg1, "DG", "CG", 1, 1), + (construct_dg1_integral, construct_cg1, "DG", "CG", 1, 1)]) +def test_ext_mesh(generator1, generator2, code1, code2, deg1, deg2): m = UnitIntervalMesh(2, use_fuse=True) mesh = ExtrudedMesh(m, 2) # manual method of creating tensor product elements - horiz_elt = FiniteElement(code, as_cell("interval"), deg) - vert_elt = FiniteElement(code, as_cell("interval"), deg) + horiz_elt = FiniteElement(code1, as_cell("interval"), deg1) + vert_elt = FiniteElement(code2, as_cell("interval"), deg2) elt = TensorProductElement(horiz_elt, vert_elt) U = FunctionSpace(mesh, elt) - mass_solve(U) + res1 = mass_solve(U) # fuseonic way of creating tensor product elements - A = generator() - B = generator() + A = generator1() + B = generator2() elem = tensor_product(A, B) U = FunctionSpace(mesh, elem.to_ufl()) - mass_solve(U) + res2 = mass_solve(U) + assert np.allclose(res1, res2) -@pytest.mark.xfail(reason="tensor prod issues") -def test_helmholtz(): + +@pytest.mark.parametrize(["elem_gen", "elem_code", "deg", "conv_rate"], [(construct_cg1, "CG", 1, 1.8), + (create_cg2, "CG", 2, 3.8), + (create_cg3_interval, "CG", 3, 4.8)]) +def test_helmholtz(elem_gen, elem_code, deg, conv_rate): vals = range(3, 6) res = [] for r in vals: m = UnitIntervalMesh(2**r, use_fuse=True) mesh = ExtrudedMesh(m, 2**r) - A = construct_cg1() - B = construct_cg1() + A = elem_gen() + B = elem_gen() elem = tensor_product(A, B) U = FunctionSpace(mesh, elem.to_ufl()) @@ -74,7 +168,135 @@ def test_helmholtz(): res = np.array(res) conv = np.log2(res[:-1] / res[1:]) print("convergence order:", conv) - assert (np.array(conv) > 1.8).all() + assert (np.array(conv) > conv_rate).all() + + +def project_expr(mesh, U, expr): + x = SpatialCoordinate(mesh) + f = assemble(project(expr(x), U)) + out = Function(U) + u = TrialFunction(U) + v = TestFunction(U) + a = inner(u, v)*dx + L = inner(f, v)*dx + solve(a == L, out) + res = sqrt(assemble(dot(out - expr(x), out - expr(x)) * dx)) + return res + + +@pytest.mark.parametrize(["elem_gen", "elem_code", "deg", "conv_rate"], [(rt1_quad, "RTCF", 1, 1.8), (ned1_quad, "RTCE", 1, 0.8)]) +def test_project_vec_quad(elem_gen, elem_code, deg, conv_rate): + vals = range(3, 6) + function = lambda x, i: cos((3/4)*pi*x[i]) + expr = lambda x: as_vector([function(x, 0), function(x, 1)]) + res_fuse = [] + res_fire = [] + for r in vals: + mesh_fuse = UnitSquareMesh(2**r, 2**r, quadrilateral=True, use_fuse=True) + U = FunctionSpace(mesh_fuse, elem_gen().to_ufl()) + res_fuse += [project_expr(mesh_fuse, U, expr)] + + mesh_fire = UnitSquareMesh(2**r, 2**r, quadrilateral=True) + U = FunctionSpace(mesh_fire, elem_code, deg) + res_fire += [project_expr(mesh_fire, U, expr)] + + print("fuse l2 error norms:", res_fuse) + res_fuse = np.array(res_fuse) + conv_fuse = np.log2(res_fuse[:-1] / res_fuse[1:]) + print("fuse convergence order:", conv_fuse) + + print("fire l2 error norms:", res_fire) + res_fire = np.array(res_fire) + conv_fire = np.log2(res_fire[:-1] / res_fire[1:]) + print("fire convergence order:", conv_fire) + + assert (conv_fuse > conv_rate).all() + assert (conv_fire > conv_rate).all() + + +@pytest.mark.parametrize(["elem_gen", "conv_rate"], [(rt1_tensor, 0.8), (ned1_tensor, 0.8)]) +def test_project_vec_ext(elem_gen, conv_rate): + vals = range(3, 6) + function = lambda x, i: cos((3/4)*pi*x[i]) + expr = lambda x: as_vector([function(x, 0), function(x, 1)]) + res_fuse = [] + res_fire = [] + for r in vals: + fuse_elem, firedrake_elem = elem_gen() + mesh_fuse = ExtrudedMesh(UnitIntervalMesh(2**r, use_fuse=True), 2**r) + U = FunctionSpace(mesh_fuse, fuse_elem.to_ufl()) + res_fuse += [project_expr(mesh_fuse, U, expr)] + + mesh_fire = ExtrudedMesh(UnitIntervalMesh(2**r), 2**r) + U = FunctionSpace(mesh_fire, firedrake_elem) + res_fire += [project_expr(mesh_fire, U, expr)] + + print("fuse l2 error norms:", res_fuse) + res_fuse = np.array(res_fuse) + conv_fuse = np.log2(res_fuse[:-1] / res_fuse[1:]) + print("fuse convergence order:", conv_fuse) + + print("fire l2 error norms:", res_fire) + res_fire = np.array(res_fire) + conv_fire = np.log2(res_fire[:-1] / res_fire[1:]) + print("fire convergence order:", conv_fire) + + assert (conv_fuse > conv_rate).all() + assert (conv_fire > conv_rate).all() + + +@pytest.mark.parametrize(["elem_gen", "conv_rate"], [(rt1_hex, 1.8), (ned1_hex, 0.8)]) +def test_project_vec_hex(elem_gen, conv_rate): + vals = [2, 3] + function = lambda x, i: cos((3/4)*pi*x[i]) + expr = lambda x: as_vector([function(x, 0), function(x, 1), function(x, 2)]) + res_fuse = [] + for r in vals: + mesh_fuse = UnitCubeMesh(2**r, 2**r, 2**r, hexahedral=True, use_fuse=True) + U = FunctionSpace(mesh_fuse, elem_gen().flatten().to_ufl()) + res_fuse += [project_expr(mesh_fuse, U, expr)] + + print("fuse l2 error norms:", res_fuse) + res_fuse = np.array(res_fuse) + conv_fuse = np.log2(res_fuse[:-1] / res_fuse[1:]) + print("fuse convergence order:", conv_fuse) + + assert (conv_fuse > conv_rate).all() + + +@pytest.mark.parametrize(["elem_gen", "elem_code", "deg", "conv_rate"], [(construct_cg1, "CG", 1, 1.8), + (create_cg2, "CG", 2, 3.8), + (create_cg3_interval, "CG", 3, 4.8)]) +def test_helmholtz_3d(elem_gen, elem_code, deg, conv_rate): + vals = range(2, 4) + res_ufc = [] + res_fuse = [] + for r in vals: + m = UnitSquareMesh(2**r, 2**r, quadrilateral=True, use_fuse=True) + mesh_fuse = ExtrudedMesh(m, 2**r) + + A = elem_gen() + B = elem_gen() + C = elem_gen() + elem = tensor_product(tensor_product(A, B).flatten(), C) + + U1 = FunctionSpace(mesh_fuse, elem.to_ufl()) + res_fuse += [helmholtz_solve2(U1, mesh_fuse)] + + m = UnitSquareMesh(2**r, 2**r, quadrilateral=True) + mesh_ufc = ExtrudedMesh(m, 2**r) + U2 = FunctionSpace(mesh_ufc, elem_code, deg) + res_ufc += [helmholtz_solve2(U2, mesh_ufc)] + print("l2 error norms:", res_ufc) + res_ufc = np.array(res_ufc) + conv_ufc = np.log2(res_ufc[:-1] / res_ufc[1:]) + print("convergence order:", conv_ufc) + print("l2 error norms:", res_fuse) + res_fuse = np.array(res_fuse) + conv_fuse = np.log2(res_fuse[:-1] / res_fuse[1:]) + print("convergence order:", conv_fuse) + # assert (np.array(conv_fuse) > conv_rate).all() + # assert (np.array(conv_ufc) > conv_rate).all() def test_on_quad_mesh(): @@ -92,31 +314,383 @@ def test_on_quad_mesh(): mass_solve(U) -def test_quad_mesh_helmholtz(): +def test_cg3(): + r = 1 + mesh = UnitSquareMesh(2 ** r, 2 ** r, quadrilateral=True, use_fuse=True) + res_fuse = [] + A = create_cg3_interval() + B = create_cg3_interval() + # elem = symmetric_tensor_product(A, B, matrices=False).flatten() + # U = FunctionSpace(mesh, elem.to_ufl()) + # res_fuse += [helmholtz_solve(mesh, U)] + elem = symmetric_tensor_product(A, B).flatten() + U = FunctionSpace(mesh, elem.to_ufl()) + res_fuse += [helmholtz_solve(mesh, U)] + assert all(np.array(res_fuse) < 0.003) + + +@pytest.mark.parametrize(["elem_gen", "elem_code", "deg", "conv_rate"], [(construct_cg1, "CG", 1, 1.8), + (create_cg2, "CG", 2, 3.8), + (create_cg3_interval, "CG", 3, 4.8)]) +def test_quad_mesh_helmholtz(elem_gen, elem_code, deg, conv_rate): quadrilateral = True vals = range(3, 6) res_fuse = [] - res_fire = [] + res_fiat = [] for r in vals: mesh_fuse = UnitSquareMesh(2 ** r, 2 ** r, quadrilateral=quadrilateral, use_fuse=True) - - A = construct_cg1() - B = construct_cg1() - elem = tensor_product(A, B).flatten() + A = elem_gen() + B = elem_gen() + elem = symmetric_tensor_product(A, B).flatten() U = FunctionSpace(mesh_fuse, elem.to_ufl()) res_fuse += [helmholtz_solve(mesh_fuse, U)] mesh_ufc = UnitSquareMesh(2 ** r, 2 ** r, quadrilateral=quadrilateral) - U = FunctionSpace(mesh_ufc, "CG", 1) - res_fire += [helmholtz_solve(mesh_ufc, U)] - print("l2 error norms:", res_fuse) + U = FunctionSpace(mesh_ufc, elem_code, deg) + res_fiat += [helmholtz_solve(mesh_ufc, U)] + print("Fuse l2 error norms:", res_fuse) + res = np.array(res_fuse) + conv = np.log2(res[:-1] / res[1:]) + print("Fuse convergence order:", conv) + assert (np.array(conv) > conv_rate).all() + + print("FIAT l2 error norms:", res_fiat) + res = np.array(res_fiat) + conv = np.log2(res[:-1] / res[1:]) + print("Fiat convergence order:", conv) + assert (np.array(conv) > conv_rate).all() + + +@pytest.mark.parametrize(["elem_gen", "elem_code", "deg", "conv_rate"], [(construct_cg1, "CG", 1, 1.7), + (create_cg2, "CG", 2, 3.8), + (create_cg3_interval, "CG", 3, 4.8)]) +def test_ext_mesh_helmholtz_3d(elem_gen, elem_code, deg, conv_rate): + vals = range(2, 4) + res_fuse = [] + res_fiat = [] + for r in vals: + mesh_fuse = ExtrudedMesh(UnitSquareMesh(2 ** r, 2 ** r, quadrilateral=True, use_fuse=True), 2**r) + A = elem_gen() + B = elem_gen() + C = elem_gen() + elem = tensor_product(tensor_product(A, B).flatten(), C) + U = FunctionSpace(mesh_fuse, elem.to_ufl()) + res_fuse += [helmholtz_solve2(U, mesh_fuse)] + + mesh_ufc = ExtrudedMesh(UnitSquareMesh(2 ** r, 2 ** r), 2**r) + U = FunctionSpace(mesh_ufc, elem_code, deg) + res_fiat += [helmholtz_solve2(U, mesh_ufc)] + print("Fuse l2 error norms:", res_fuse) res = np.array(res_fuse) conv_fuse = np.log2(res[:-1] / res[1:]) - print("convergence order:", conv_fuse) + print("Fuse convergence order:", conv_fuse) - print("l2 error norms:", res_fire) - res = np.array(res_fire) - conv_ufc = np.log2(res[:-1] / res[1:]) - print("convergence order:", conv_ufc) - assert (np.array(conv_fuse) > 1.8).all() - assert (np.array(conv_ufc) > 1.8).all() + print("FIAT l2 error norms:", res_fiat) + res = np.array(res_fiat) + conv = np.log2(res[:-1] / res[1:]) + print("Fiat convergence order:", conv) + assert (np.array(conv_fuse) > conv_rate).all() + # assert (np.array(conv) > conv_rate).all() + + +@pytest.mark.parametrize(["elem_gen", "elem_code", "deg", "conv_rate"], [(construct_cg1, "CG", 1, 1.7), + (create_cg2, "CG", 2, 3.8), + (create_cg3_interval, "CG", 3, 4.8)]) +def test_quad_mesh_helmholtz_3d(elem_gen, elem_code, deg, conv_rate): + vals = range(2, 4) + res_fuse = [] + res_fiat = [] + for r in vals: + mesh_fuse = UnitCubeMesh(2 ** r, 2 ** r, 2 ** r, hexahedral=True, use_fuse=True) + A = elem_gen() + B = elem_gen() + C = elem_gen() + elem = symmetric_tensor_product(A, B, C).flatten() + U = FunctionSpace(mesh_fuse, elem.to_ufl()) + res_fuse += [helmholtz_solve2(U, mesh_fuse)] + + mesh_ufc = UnitCubeMesh(2 ** r, 2 ** r, 2 ** r, hexahedral=True) + U = FunctionSpace(mesh_ufc, elem_code, deg) + res_fiat += [helmholtz_solve2(U, mesh_ufc)] + print("Fuse l2 error norms:", res_fuse) + res = np.array(res_fuse) + conv_fuse = np.log2(res[:-1] / res[1:]) + print("Fuse convergence order:", conv_fuse) + + print("FIAT l2 error norms:", res_fiat) + res = np.array(res_fiat) + conv_fiat = np.log2(res[:-1] / res[1:]) + print("Fiat convergence order:", conv_fiat) + assert (np.array(conv_fuse) > conv_rate).all() + assert (np.array(conv_fiat) > conv_rate).all() + + +@pytest.mark.parametrize(["A", "B", "res"], [(Point(0), line(), False), + (line(), line(), True), + (polygon(3), line(), False),]) +def test_flattening(A, B, res): + tensor_cell = TensorProductPoint(A, B) + if not res: + with pytest.raises(AssertionError): + tensor_cell.flatten() + else: + cell = tensor_cell.flatten() + cell.construct_fuse_rep() + + +@pytest.mark.parametrize(["A", "B", "C"], [(line(), line(), line()),]) +def test_creation(A, B, C): + tensor_cell_2d = TensorProductPoint(A, B) + tensor_cell_2d.to_ufl() + tensor_cell_2d.to_fiat() + flat_tensor_cell_2d = tensor_cell_2d.flatten() + print(flat_tensor_cell_2d) + tensor_cell_3d = TensorProductPoint(A, B, C) + tensor_cell_3d.to_ufl() + tensor_cell_3d.to_fiat() + flat_tensor_cell_3d = tensor_cell_3d.flatten() + print(flat_tensor_cell_3d) + + +@pytest.mark.xfail(reason="FUSE has no facet-restricted 'HDiv Trace' analogue yet: " + "tensor_product(...).flatten() produces basis functions with " + "full cell/edge support (see entity_support_dofs), not functions " + "that vanish off their associated facet like FIAT's HDivTrace, so " + "the facet mass form (ds/dS) is not well posed for this space.") +def test_trace_galerkin_projection(): + mesh = UnitSquareMesh(10, 10, quadrilateral=True, use_fuse=True) + + x, y = SpatialCoordinate(mesh) + A = construct_cg1() + B = construct_dg1_integral() + elem = tensor_product(A, B).flatten() + elem2 = tensor_product(B, A).flatten() + + # Define the Trace Space + T = FunctionSpace(mesh, (elem + elem2).to_ufl()) + + # Define trial and test functions + lambdar = TrialFunction(T) + gammar = TestFunction(T) + + # Define right hand side function + + V = FunctionSpace(mesh, tensor_product(A, construct_cg1()).flatten().to_ufl()) + f = Function(V) + f.interpolate(cos(x*pi*2)*cos(y*pi*2)) + + # Construct bilinear form + a = inner(lambdar, gammar) * ds + inner(lambdar('+'), gammar('+')) * dS + + # Construct linear form + l = inner(f, gammar) * ds + inner(f('+'), gammar('+')) * dS + + # Compute the solution + t = Function(T) + solve(a == l, t, solver_parameters={'ksp_rtol': 1e-14}) + + # Compute error in trace norm + trace_error = sqrt(assemble(FacetArea(mesh)*inner((t - f)('+'), (t - f)('+')) * dS)) + + assert trace_error < 1e-13 + + +def test_hdiv(): + np.set_printoptions(linewidth=90, precision=4, suppress=True) + + cg1 = construct_cg1() + dg0 = construct_dg0_integral() + fuse_rt1 = HDiv_fuse(tensor_product(cg1, dg0)) + HDiv_fuse(tensor_product(dg0, cg1)) + + CG_1 = FiniteElement("CG", "interval", 1) + DG_0 = FiniteElement("DG", "interval", 0) + P1P0 = TensorProductElement(CG_1, DG_0) + RT_horiz = HDivElement(P1P0) + P0P1 = TensorProductElement(DG_0, CG_1) + RT_vert = HDivElement(P0P1) + firedrake_rt1 = RT_horiz + RT_vert + + m = UnitIntervalMesh(2) + mesh = ExtrudedMesh(m, 2) + m = UnitIntervalMesh(2, use_fuse=True) + mesh2 = ExtrudedMesh(m, 2) + V = FunctionSpace(mesh, firedrake_rt1) + V2 = FunctionSpace(mesh2, fuse_rt1.to_ufl()) + for V, mesh in zip([V, V2], (mesh, mesh2)): + u = TrialFunction(V) + v = TestFunction(V) + f = Function(V) + x, y = SpatialCoordinate(mesh) + # f_vec = as_vector(((1+8*pi*pi)*cos(x*pi*2)*cos(y*pi*2), (1+8*pi*pi)*cos(x*pi*2)*cos(y*pi*2))) + f_vec = as_vector((2, 3)) + f = project(f_vec, V) + a = (inner(grad(u), grad(v)) + inner(u, v)) * dx + L = inner(f, v) * dx + u = Function(V) + solve(a == L, u) + # f_vec is constant, so grad(f_vec) = 0 and the exact solution of + # (grad(u):grad(v) + u.v)dx = f.v dx is u = f_vec everywhere. + error = sqrt(assemble(dot(u - f_vec, u - f_vec) * dx)) + assert error < 1e-10 + + +def test_hcurl(): + np.set_printoptions(linewidth=90, precision=4, suppress=True) + + cg1 = construct_cg1() + dg0 = construct_dg0_integral() + fuse_ncurl1 = HCurl_fuse(tensor_product(dg0, cg1)) + HCurl_fuse(tensor_product(cg1, dg0)) + + CG_1 = FiniteElement("CG", "interval", 1) + DG_0 = FiniteElement("DG", "interval", 0) + DG0CG1 = TensorProductElement(DG_0, CG_1) + Ned_x = HCurlElement(DG0CG1) + CG1DG0 = TensorProductElement(CG_1, DG_0) + Ned_y = HCurlElement(CG1DG0) + firedrake_ncurl1 = Ned_x + Ned_y + + m = UnitIntervalMesh(2) + mesh = ExtrudedMesh(m, 2) + m = UnitIntervalMesh(2, use_fuse=True) + mesh2 = ExtrudedMesh(m, 2) + V = FunctionSpace(mesh, firedrake_ncurl1) + V2 = FunctionSpace(mesh2, fuse_ncurl1.to_ufl()) + assert V.dim() == V2.dim() + for V, mesh in zip([V, V2], (mesh, mesh2)): + u = TrialFunction(V) + v = TestFunction(V) + x, y = SpatialCoordinate(mesh) + f_vec = as_vector((2, 3)) + f = project(f_vec, V) + a = (inner(grad(u), grad(v)) + inner(u, v)) * dx + L = inner(f, v) * dx + u = Function(V) + solve(a == L, u) + # f_vec is constant, so grad(f_vec) = 0 and the exact solution of + # (grad(u):grad(v) + u.v)dx = f.v dx is u = f_vec everywhere. + error = sqrt(assemble(dot(u - f_vec, u - f_vec) * dx)) + assert error < 1e-10 + + +def test_hdiv_3d_orientation_consistency(): + # If neighbouring cells disagreed on the sign of a shared facet DOF, the + # global RT space could no longer represent a true constant vector field + f_vec = as_vector((2, 3, 5)) + mesh = UnitCubeMesh(3, 3, 3, hexahedral=True, use_fuse=True) + V = FunctionSpace(mesh, rt1_hex().flatten().to_ufl()) + + u = TrialFunction(V) + v = TestFunction(V) + sol = Function(V) + solve(inner(u, v) * dx == inner(f_vec, v) * dx, sol) + + error = sqrt(assemble(dot(sol - f_vec, sol - f_vec) * dx)) + assert error < 1e-10 + + +def test_hcurl_3d_orientation_consistency(): + # exact reproduction of a constant vector field is a genuine cross-cell + # sign-consistency check for the tangential edge DOFs, not just a + # well-posedness check. + f_vec = as_vector((2, 3, 5)) + mesh = UnitCubeMesh(3, 3, 3, hexahedral=True, use_fuse=True) + elem = ned1_hex().flatten() + V = FunctionSpace(mesh, elem.to_ufl()) + u = TrialFunction(V) + v = TestFunction(V) + sol = Function(V) + solve(inner(u, v) * dx == inner(f_vec, v) * dx, sol) + + error = sqrt(assemble(dot(sol - f_vec, sol - f_vec) * dx)) + assert error < 1e-10 + + +def test_transforms(): + edge = Point(1, [Point(0), Point(0)], vertex_num=2) + rev_edge = edge.orient(edge.group.members()[1]) + from fuse.tensor_products import HDiv, HCurl + cg1 = construct_cg1() + rev_cg1 = construct_cg1(rev_edge) + dg0 = construct_dg0_integral() + rev_dg0 = construct_dg0_integral(rev_edge) + import gem + v = gem.Literal(5) + print("HCurl") + print(HCurl(tensor_product(dg0, cg1)).gem_transformer(v)) + print(HCurl(tensor_product(rev_dg0, cg1)).gem_transformer(v)) + print(HCurl(tensor_product(cg1, dg0)).gem_transformer(v)) + print(HCurl(tensor_product(cg1, rev_dg0)).gem_transformer(v)) + print(HCurl(tensor_product(dg0, rev_cg1)).gem_transformer(v)) + print(HCurl(tensor_product(rev_cg1, dg0)).gem_transformer(v)) + print("HDiv") + print(HDiv(tensor_product(dg0, cg1)).gem_transformer(v)) + print(HDiv(tensor_product(rev_dg0, cg1)).gem_transformer(v)) + print(HDiv(tensor_product(cg1, dg0)).gem_transformer(v)) + print(HDiv(tensor_product(cg1, rev_dg0)).gem_transformer(v)) + print(HDiv(tensor_product(dg0, rev_cg1)).gem_transformer(v)) + print(HDiv(tensor_product(rev_cg1, dg0)).gem_transformer(v)) + + +def test_sum_fac(): + # In 2d we have O(N_q^2N_i^4) -> O(p^6) + # Sum factorisation gains 1 factor so we expect O(p^5) + # For CG3 p = 3 so it should be 3x faster + mesh1 = ExtrudedMesh(UnitIntervalMesh(10, use_fuse=True), 10) + mesh2 = ExtrudedMesh(UnitIntervalMesh(10), 10) + A = create_cg3_interval() + B = create_cg3_interval() + elem = tensor_product(A, B) + mesh3 = UnitSquareMesh(10, 10, quadrilateral=True, use_fuse=True) + mesh4 = UnitSquareMesh(10, 10, quadrilateral=True) + C = create_cg3_interval() + D = create_cg3_interval() + elem2 = symmetric_tensor_product(C, D).flatten() + V = FunctionSpace(mesh1, elem.to_ufl()) + V1 = FunctionSpace(mesh2, "CG", 3) + V2 = FunctionSpace(mesh3, elem2.to_ufl()) + V3 = FunctionSpace(mesh4, "CG", 3) + Vs = [V, V1, V2, V3] + for V in Vs: + print(V) + u = TrialFunction(V) + v = TestFunction(V) + a = dot(grad(u), grad(v))*dx # Laplace operator + from tsfc import compile_form + kernel_vanilla, = compile_form(a, parameters={"mode": "vanilla"}) + print("Local assembly FLOPs with vanilla mode is {0:.3g}".format(kernel_vanilla.flop_count)) + kernel_spectral, = compile_form(a) + print("Local assembly FLOPs with spectral mode is {0:.3g}".format(kernel_spectral.flop_count)) + assert (kernel_vanilla.flop_count / kernel_spectral.flop_count) > 3 + + +# @pytest.mark.xfail(reason="3D tensor products not implemented") +def test_sum_fac_3d(): + # In 2d we have O(N_q^3N_i^6) -> O(p^9) + # Sum factorisation gains 2 factors so we expect O(p^7) + # For CG3 p = 3 so it should be 9x faster - seems that it is faster than this in regular firedrake + mesh = ExtrudedMesh(UnitSquareMesh(10, 10, use_fuse=True), 10) + mesh2 = ExtrudedMesh(UnitSquareMesh(10, 10), 10) + A = create_cg3_interval() + B = create_cg3_interval() + C = create_cg3_interval() + elem = tensor_product(tensor_product(A, B).flatten(), C) + mesh3 = UnitCubeMesh(10, 10, 10, hexahedral=True, use_fuse=True) + mesh4 = UnitCubeMesh(10, 10, 10, hexahedral=True) + elem2 = symmetric_tensor_product(A, B, C).flatten() + V = FunctionSpace(mesh, elem.to_ufl()) + V1 = FunctionSpace(mesh2, "CG", 3) + V2 = FunctionSpace(mesh3, elem2.to_ufl()) + V3 = FunctionSpace(mesh4, "CG", 3) + Vs = [V, V1, V2, V3] + names = ["Extruded FUSE", "Extruded FIAT", "Hex FUSE", "Hex FIAT"] + for V, name in zip(Vs, names): + print(name) + u = TrialFunction(V) + v = TestFunction(V) + a = dot(grad(u), grad(v))*dx # Laplace operator + from tsfc import compile_form + kernel_vanilla, = compile_form(a, parameters={"mode": "vanilla"}) + print("Local assembly FLOPs with vanilla mode is {0:.3g}".format(kernel_vanilla.flop_count)) + kernel_spectral, = compile_form(a) + print("Local assembly FLOPs with spectral mode is {0:.3g}".format(kernel_spectral.flop_count)) + print(kernel_vanilla.flop_count / kernel_spectral.flop_count)